Cleanup: Remove old modes, add Svelte 5 reference library, update orchestrator
- Delete Mode1Test, Mode2Test, Mode3Test, Mode5Test (keep only Mode6Test) - Delete old documentation, Docker files, and k8s configs - Add comprehensive Svelte 5 reference library (svelte-reference/) - Update ModeSwitch orchestrator: - Fix port to 3005 (never use 3000/3001) - Add API endpoints for mode switching - Add bun-types and proper tsconfig - Clean up mode folder mappings
@@ -1,206 +0,0 @@
|
||||
name: Build & Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME_BACKEND: job-info-backend
|
||||
IMAGE_NAME_FRONTEND: job-info-frontend
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test & Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
|
||||
- name: Install Frontend Dependencies
|
||||
run: |
|
||||
cd frontend
|
||||
npm ci
|
||||
|
||||
- name: Lint Frontend
|
||||
run: |
|
||||
cd frontend
|
||||
npm run lint --if-present
|
||||
|
||||
- name: Type Check Frontend
|
||||
run: |
|
||||
cd frontend
|
||||
npm run type-check --if-present
|
||||
|
||||
- name: Build Frontend
|
||||
run: |
|
||||
cd frontend
|
||||
npm run build
|
||||
|
||||
- name: Test Frontend
|
||||
run: |
|
||||
cd frontend
|
||||
npm test --if-present
|
||||
|
||||
- name: Install Backend Dependencies
|
||||
run: |
|
||||
cd Mode3Test
|
||||
bun install
|
||||
|
||||
- name: Test Backend
|
||||
run: |
|
||||
cd Mode3Test
|
||||
bun test --if-present
|
||||
|
||||
build:
|
||||
name: Build Docker Images
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Backend
|
||||
id: meta-backend
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ github.repository }}/${{ env.IMAGE_NAME_BACKEND }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=sha
|
||||
|
||||
- name: Build and Push Backend
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.backend
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta-backend.outputs.tags }}
|
||||
labels: ${{ steps.meta-backend.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Extract metadata for Frontend
|
||||
id: meta-frontend
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ github.repository }}/${{ env.IMAGE_NAME_FRONTEND }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=sha
|
||||
|
||||
- name: Build and Push Frontend
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.frontend
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta-frontend.outputs.tags }}
|
||||
labels: ${{ steps.meta-frontend.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
deploy:
|
||||
name: Deploy to Kubernetes
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up kubectl
|
||||
uses: azure/setup-kubectl@v3
|
||||
with:
|
||||
version: 'latest'
|
||||
|
||||
- name: Configure kubectl
|
||||
run: |
|
||||
mkdir -p $HOME/.kube
|
||||
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > $HOME/.kube/config
|
||||
chmod 600 $HOME/.kube/config
|
||||
|
||||
- name: Create namespace
|
||||
run: kubectl create namespace job-info --dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
- name: Apply Kubernetes configs
|
||||
run: |
|
||||
kubectl apply -f k8s-config.yaml
|
||||
kubectl apply -f k8s-backend-deployment.yaml
|
||||
kubectl apply -f k8s-frontend-deployment.yaml
|
||||
|
||||
- name: Update image versions
|
||||
run: |
|
||||
kubectl set image deployment/job-info-backend \
|
||||
backend=${{ env.REGISTRY }}/${{ github.repository }}/${{ env.IMAGE_NAME_BACKEND }}:${{ github.sha }} \
|
||||
-n job-info
|
||||
|
||||
kubectl set image deployment/job-info-frontend \
|
||||
frontend=${{ env.REGISTRY }}/${{ github.repository }}/${{ env.IMAGE_NAME_FRONTEND }}:${{ github.sha }} \
|
||||
-n job-info
|
||||
|
||||
- name: Wait for rollout
|
||||
run: |
|
||||
kubectl rollout status deployment/job-info-backend -n job-info --timeout=5m
|
||||
kubectl rollout status deployment/job-info-frontend -n job-info --timeout=5m
|
||||
|
||||
- name: Verify deployment
|
||||
run: |
|
||||
kubectl get pods -n job-info
|
||||
kubectl get svc -n job-info
|
||||
kubectl get ingress -n job-info
|
||||
|
||||
security-scan:
|
||||
name: Security Scan
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- 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 results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
with:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
|
||||
- name: Run npm audit
|
||||
run: |
|
||||
cd frontend
|
||||
npm audit --audit-level=moderate
|
||||
|
||||
- name: Check for secrets
|
||||
uses: trufflesecurity/trufflehog@main
|
||||
with:
|
||||
path: ./
|
||||
base: ${{ github.event.repository.default_branch }}
|
||||
head: HEAD
|
||||
@@ -1,84 +0,0 @@
|
||||
name: Code Quality
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
|
||||
jobs:
|
||||
code-quality:
|
||||
name: Code Quality Checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Frontend - Install & Lint
|
||||
run: |
|
||||
cd frontend
|
||||
npm ci
|
||||
npm run lint --if-present
|
||||
|
||||
- name: Frontend - Type Check
|
||||
run: |
|
||||
cd frontend
|
||||
npm run type-check --if-present
|
||||
|
||||
- name: Frontend - Build Check
|
||||
run: |
|
||||
cd frontend
|
||||
npm run build
|
||||
|
||||
- name: Frontend - Bundle Analysis
|
||||
run: |
|
||||
cd frontend
|
||||
npm run build
|
||||
echo "Build artifacts ready for analysis"
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ./frontend/coverage/coverage-final.json
|
||||
flags: frontend
|
||||
fail_ci_if_error: false
|
||||
|
||||
dependency-check:
|
||||
name: Dependency Vulnerabilities
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Check frontend dependencies
|
||||
run: |
|
||||
cd frontend
|
||||
npm ci
|
||||
npm audit --production --audit-level=high
|
||||
|
||||
performance:
|
||||
name: Performance Testing
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Build and measure
|
||||
run: |
|
||||
cd frontend
|
||||
npm ci
|
||||
npm run build
|
||||
du -sh dist/
|
||||
echo "Build completed - checking asset sizes"
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Copy Mode1Test to Mode5Test and update orchestrator
|
||||
|
||||
set -e
|
||||
|
||||
echo "Creating Mode5Test as copy of Mode1Test..."
|
||||
cd /home/admin/Job-Info-Test
|
||||
|
||||
# Remove if exists
|
||||
if [ -d "Mode5Test" ]; then
|
||||
echo "Mode5Test already exists, removing..."
|
||||
rm -rf Mode5Test
|
||||
fi
|
||||
|
||||
# Copy entire Mode1Test to Mode5Test
|
||||
cp -r Mode1Test Mode5Test
|
||||
echo "✓ Mode5Test created"
|
||||
|
||||
# Update orchestrator to support Mode5Test
|
||||
echo "Updating orchestrator..."
|
||||
|
||||
# Update the MODE_FOLDERS mapping in orchestrator.ts
|
||||
sed -i "s/'Mode3Test': 'Mode3Test',/'Mode3Test': 'Mode3Test',\n 'Mode5Test': 'Mode5Test',/" ModeSwitch/backend/orchestrator.ts
|
||||
|
||||
# Set active mode to Mode5Test
|
||||
echo "Mode5Test" > ModeSwitch/activeMode.txt
|
||||
|
||||
echo "✓ Orchestrator updated"
|
||||
echo "✓ Active mode set to Mode5Test"
|
||||
|
||||
echo ""
|
||||
echo "Done! Mode5Test is ready."
|
||||
echo "Run 'node orchestrator.ts' to start Mode5Test"
|
||||
@@ -1,394 +0,0 @@
|
||||
# 📦 PROJECT DELIVERABLES CHECKLIST
|
||||
|
||||
**Project**: Job Info - Production Svelte OAuth2 Application
|
||||
**Date**: 2026-01-19
|
||||
**Status**: ✅ COMPLETE & PRODUCTION READY
|
||||
|
||||
---
|
||||
|
||||
## 📂 DELIVERABLE INVENTORY
|
||||
|
||||
### FRONTEND APPLICATION ✅
|
||||
- [x] `frontend/` directory with complete Svelte application
|
||||
- [x] Vite configuration (vite.config.ts)
|
||||
- [x] TypeScript strict mode enabled
|
||||
- [x] TailwindCSS styling
|
||||
- [x] package.json with 139 dependencies
|
||||
- [x] Production build (42.70 kB gzip)
|
||||
- [x] Service Worker implementation
|
||||
- [x] Source map support
|
||||
|
||||
**Files**: 50+ TypeScript/Svelte files
|
||||
|
||||
### SERVICES ✅
|
||||
- [x] `src/services/auth.ts` - Authentication store (336 lines)
|
||||
- [x] `src/services/jobs.ts` - Job management store (283 lines)
|
||||
- [x] `src/services/ui.ts` - UI state management (280 lines)
|
||||
- [x] `src/services/oauth.ts` - OAuth2 PKCE flow (280 lines)
|
||||
- [x] `src/services/errorHandler.ts` - Error classification (300 lines)
|
||||
- [x] `src/services/logger.ts` - Structured logging (250+ lines) **NEW**
|
||||
- [x] `src/services/graph.ts` - Microsoft Graph API integration (300+ lines) **UPDATED**
|
||||
|
||||
### COMPONENTS ✅
|
||||
- [x] `src/components/Navigation.svelte` - Header with user profile
|
||||
- [x] `src/components/JobCard.svelte` - Job display card
|
||||
- [x] `src/components/JobList.svelte` - Job list with pagination
|
||||
- [x] `src/components/SearchBar.svelte` - Job search functionality
|
||||
- [x] `src/components/NotificationContainer.svelte` - Toast notifications
|
||||
- [x] `src/components/ErrorBoundary.svelte` - Error UI with recovery **NEW**
|
||||
|
||||
### PAGES ✅
|
||||
- [x] `src/pages/Home.svelte` - Dashboard (authenticated)
|
||||
- [x] `src/pages/SignIn.svelte` - OAuth sign-in page
|
||||
- [x] `src/pages/Callback.svelte` - OAuth callback handler
|
||||
- [x] Hash-based routing (#/)
|
||||
|
||||
### UTILITIES ✅
|
||||
- [x] `src/utils/service-worker-manager.ts` - Service Worker communication
|
||||
- [x] `src/utils/api-request.ts` - Centralized API wrapper with retry **NEW**
|
||||
- [x] `src/lib/db.ts` - IndexedDB operations
|
||||
|
||||
### BACKEND APPLICATION ✅
|
||||
- [x] `Mode3Test/backend/server.ts` - Hono server setup
|
||||
- [x] `Mode3Test/backend/auth-routes.ts` - OAuth endpoints
|
||||
- [x] `Mode3Test/backend/token-service.ts` - Token management
|
||||
- [x] `Mode3Test/package.json` - Dependencies
|
||||
|
||||
**Tech**: Bun runtime, Hono framework, TypeScript
|
||||
|
||||
### DOCKER INFRASTRUCTURE ✅
|
||||
- [x] `Dockerfile.backend` - Multi-stage backend container
|
||||
- [x] `Dockerfile.frontend` - Multi-stage frontend container
|
||||
- [x] `docker-compose.yml` - Full stack orchestration
|
||||
- [x] Health checks configured
|
||||
- [x] Environment variables setup
|
||||
- [x] Volume management
|
||||
|
||||
### KUBERNETES INFRASTRUCTURE ✅
|
||||
- [x] `k8s-backend-deployment.yaml` - Backend deployment (HA)
|
||||
- [x] `k8s-frontend-deployment.yaml` - Frontend deployment (scalable)
|
||||
- [x] `k8s-config.yaml` - Namespace, ConfigMaps, Secrets, Ingress
|
||||
- [x] Pod disruption budgets
|
||||
- [x] Horizontal Pod Autoscaling (2-5 replicas)
|
||||
- [x] Network policies
|
||||
- [x] RBAC ServiceAccounts
|
||||
- [x] TLS/Ingress configuration
|
||||
|
||||
### CI/CD PIPELINES ✅
|
||||
- [x] `.github/workflows/deploy.yml` - Complete deployment pipeline
|
||||
- Test stage (lint, type-check, build, test)
|
||||
- Build stage (Docker images)
|
||||
- Deploy stage (Kubernetes)
|
||||
- Security stage (Trivy, npm audit, TruffleHog)
|
||||
- [x] `.github/workflows/quality.yml` - Code quality checks
|
||||
|
||||
### TESTING FRAMEWORK ✅
|
||||
- [x] 23 test scenarios documented
|
||||
- [x] 8 test categories
|
||||
- [x] OAuth2 Flow tests (6)
|
||||
- [x] Token Management tests (4)
|
||||
- [x] API Error Handling tests (3)
|
||||
- [x] Data Storage tests (2)
|
||||
- [x] UI Components tests (2)
|
||||
- [x] Performance tests (2)
|
||||
- [x] Security tests (2)
|
||||
- [x] Accessibility tests (2)
|
||||
|
||||
### DOCUMENTATION ✅
|
||||
- [x] PROJECT_COMPLETION_FINAL.md (100+ lines)
|
||||
- [x] PHASE_9_DEPLOYMENT_COMPLETE.md (500+ lines)
|
||||
- [x] PHASE_8_QUICK_START.md (500+ lines)
|
||||
- [x] PHASE_8_TESTING_EXECUTION.md (600+ lines)
|
||||
- [x] TESTING_GUIDE.md (500+ lines)
|
||||
- [x] DEPLOYMENT_GUIDE.md (300+ lines)
|
||||
- [x] PROJECT_COMPLETE.md (600+ lines)
|
||||
- [x] QUICK_START.md (100+ lines) **NEW**
|
||||
- [x] SVELTE_DEVELOPMENT_SYSTEM.md (existing)
|
||||
- [x] README.md (existing)
|
||||
|
||||
**Total Documentation**: 4,600+ lines
|
||||
|
||||
---
|
||||
|
||||
## 🎯 QUALITY METRICS
|
||||
|
||||
### Build Quality ✅
|
||||
```
|
||||
JavaScript: 42.70 kB (gzip) ✅ 11% under target
|
||||
CSS: 4.08 kB (gzip) ✅ 18% under target
|
||||
HTML: 0.32 kB ✅ 68% under target
|
||||
Build Time: 2.70 seconds ✅ 46% faster than target
|
||||
Modules: 52 total
|
||||
```
|
||||
|
||||
### Code Quality ✅
|
||||
```
|
||||
TypeScript: Strict mode enabled ✅
|
||||
Type Errors: 0 ✅
|
||||
Console Errors: 0 ✅
|
||||
Lint Errors: 0 ✅
|
||||
Type Coverage: 100% (services/components) ✅
|
||||
```
|
||||
|
||||
### Security ✅
|
||||
```
|
||||
OAuth2 PKCE: ✅ Implemented
|
||||
CSRF Protection: ✅ State token validation
|
||||
XSS Prevention: ✅ Svelte auto-escaping
|
||||
Secret Scanning: ✅ TruffleHog in CI/CD
|
||||
Vulnerability Scan: ✅ Trivy in CI/CD
|
||||
Pod Security: ✅ Non-root, read-only
|
||||
Network Policies: ✅ Ingress/Egress
|
||||
```
|
||||
|
||||
### Performance ✅
|
||||
```
|
||||
Load Time: ~2.5s target ✅ Achievable
|
||||
TTFB: <100ms target ✅ LocalHost
|
||||
FCP: <1s target ✅ Achievable
|
||||
LCP: <2.5s target ✅ Achievable
|
||||
Cache: 24h IndexedDB ✅ Offline support
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 FEATURE COMPLETENESS
|
||||
|
||||
### Authentication ✅
|
||||
- [x] OAuth2 PKCE flow
|
||||
- [x] Token management (storage, expiry, refresh)
|
||||
- [x] Automatic token refresh (Service Worker)
|
||||
- [x] Manual token refresh support
|
||||
- [x] Sign out functionality
|
||||
- [x] User profile display
|
||||
|
||||
### Data Management ✅
|
||||
- [x] IndexedDB caching (24h TTL)
|
||||
- [x] CRUD operations
|
||||
- [x] Offline browsing
|
||||
- [x] Auto-sync on reconnect
|
||||
- [x] Job search/filter
|
||||
|
||||
### Error Handling ✅
|
||||
- [x] Structured logging (5 levels)
|
||||
- [x] ErrorBoundary component
|
||||
- [x] API retry logic
|
||||
- [x] User-friendly messages
|
||||
- [x] Network error handling
|
||||
- [x] Timeout handling
|
||||
|
||||
### User Interface ✅
|
||||
- [x] Responsive design (mobile/tablet/desktop)
|
||||
- [x] Loading states
|
||||
- [x] Error states
|
||||
- [x] Toast notifications
|
||||
- [x] User navigation
|
||||
- [x] Job card display
|
||||
|
||||
### Accessibility ✅
|
||||
- [x] Semantic HTML
|
||||
- [x] ARIA labels
|
||||
- [x] Keyboard navigation
|
||||
- [x] Screen reader support
|
||||
- [x] Focus management
|
||||
- [x] Color contrast
|
||||
|
||||
### Deployment ✅
|
||||
- [x] Docker containerization
|
||||
- [x] Docker Compose setup
|
||||
- [x] Kubernetes manifests
|
||||
- [x] GitHub Actions CI/CD
|
||||
- [x] Auto-scaling (HPA)
|
||||
- [x] Health checks
|
||||
- [x] Rolling updates
|
||||
- [x] Monitoring (Prometheus)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 DEPLOYMENT OPTIONS
|
||||
|
||||
### Option 1: Local Development ✅
|
||||
```bash
|
||||
cd Mode3Test && bun run dev
|
||||
cd frontend && npm run dev
|
||||
```
|
||||
- Quick iteration
|
||||
- Full hot reload
|
||||
- Direct debugging
|
||||
|
||||
### Option 2: Docker Compose ✅
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
- Containerized
|
||||
- Reproducible environment
|
||||
- Easy testing
|
||||
|
||||
### Option 3: Kubernetes ✅
|
||||
```bash
|
||||
kubectl apply -f k8s-config.yaml
|
||||
kubectl apply -f k8s-backend-deployment.yaml
|
||||
kubectl apply -f k8s-frontend-deployment.yaml
|
||||
```
|
||||
- Production-grade
|
||||
- Auto-scaling
|
||||
- HA setup
|
||||
|
||||
### Option 4: GitHub Actions (Automatic) ✅
|
||||
```bash
|
||||
git push origin main
|
||||
```
|
||||
- Fully automated
|
||||
- Test → Build → Deploy
|
||||
- Security scanning included
|
||||
|
||||
---
|
||||
|
||||
## ✅ COMPLIANCE CHECKLIST
|
||||
|
||||
### Code Standards ✅
|
||||
- [x] TypeScript strict mode
|
||||
- [x] No `any` types
|
||||
- [x] ESLint configured
|
||||
- [x] Prettier formatting
|
||||
- [x] Semantic versioning ready
|
||||
|
||||
### Security Standards ✅
|
||||
- [x] OAuth2 PKCE
|
||||
- [x] CSRF protection
|
||||
- [x] XSS prevention
|
||||
- [x] Secret management
|
||||
- [x] Network isolation
|
||||
- [x] Pod security
|
||||
|
||||
### Performance Standards ✅
|
||||
- [x] <50 kB JS gzip
|
||||
- [x] <5 kB CSS gzip
|
||||
- [x] <5s build time
|
||||
- [x] 2-5 replica scaling
|
||||
- [x] Health checks
|
||||
|
||||
### Accessibility Standards ✅
|
||||
- [x] WCAG 2.1 Level AA
|
||||
- [x] Keyboard navigation
|
||||
- [x] Screen reader support
|
||||
- [x] Color contrast
|
||||
- [x] Semantic HTML
|
||||
|
||||
### Deployment Standards ✅
|
||||
- [x] Docker best practices
|
||||
- [x] Multi-stage builds
|
||||
- [x] Kubernetes manifests
|
||||
- [x] CI/CD pipeline
|
||||
- [x] Monitoring setup
|
||||
|
||||
---
|
||||
|
||||
## 📊 PROJECT STATISTICS
|
||||
|
||||
| Category | Count | Status |
|
||||
|----------|-------|--------|
|
||||
| Total Phases | 9 | ✅ 100% |
|
||||
| Code Files | 50+ | ✅ Complete |
|
||||
| Configuration Files | 9 | ✅ Complete |
|
||||
| Documentation Files | 12 | ✅ Complete |
|
||||
| Code Lines | 7,550 | ✅ Complete |
|
||||
| Documentation Lines | 4,600 | ✅ Complete |
|
||||
| Test Scenarios | 23 | ✅ Documented |
|
||||
| Components | 6 | ✅ Complete |
|
||||
| Services | 7 | ✅ Complete |
|
||||
| Docker Files | 3 | ✅ Complete |
|
||||
| Kubernetes Files | 4 | ✅ Complete |
|
||||
| CI/CD Workflows | 2 | ✅ Complete |
|
||||
|
||||
---
|
||||
|
||||
## 🎁 PACKAGE CONTENTS
|
||||
|
||||
### Core Application
|
||||
```
|
||||
✅ Complete frontend application
|
||||
✅ Complete backend application
|
||||
✅ All services and components
|
||||
✅ All utilities and helpers
|
||||
✅ All configuration files
|
||||
```
|
||||
|
||||
### Infrastructure
|
||||
```
|
||||
✅ Docker setup (Compose)
|
||||
✅ Kubernetes manifests
|
||||
✅ GitHub Actions workflows
|
||||
✅ Health checks
|
||||
✅ Monitoring configuration
|
||||
```
|
||||
|
||||
### Documentation
|
||||
```
|
||||
✅ Quick start guide
|
||||
✅ Testing procedures
|
||||
✅ Deployment guide
|
||||
✅ Phase summaries
|
||||
✅ Architecture documentation
|
||||
```
|
||||
|
||||
### Testing
|
||||
```
|
||||
✅ 23 test scenarios
|
||||
✅ Test procedures
|
||||
✅ Expected results
|
||||
✅ Browser commands
|
||||
✅ Evidence methods
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 NEXT STEPS
|
||||
|
||||
### Immediate (Ready Now)
|
||||
1. Start application (see QUICK_START.md)
|
||||
2. Verify all features working
|
||||
3. Run Phase 8 tests (optional)
|
||||
|
||||
### Short Term (Before Production)
|
||||
1. Configure OAuth credentials
|
||||
2. Update domain in K8s configs
|
||||
3. Set up Kubernetes cluster
|
||||
4. Configure GitHub Actions secrets
|
||||
5. Deploy to staging
|
||||
6. Run smoke tests
|
||||
|
||||
### Production (When Ready)
|
||||
1. Deploy to production cluster
|
||||
2. Configure monitoring
|
||||
3. Set up alerting
|
||||
4. Monitor metrics
|
||||
5. Gather user feedback
|
||||
6. Iterate improvements
|
||||
|
||||
---
|
||||
|
||||
## ✅ SIGN-OFF
|
||||
|
||||
**Project Status**: ✅ COMPLETE & PRODUCTION READY
|
||||
|
||||
All 9 phases completed successfully. Application is fully functional, well-documented, and ready for production deployment.
|
||||
|
||||
**Delivered**:
|
||||
- ✅ Complete source code (7,550 lines)
|
||||
- ✅ Complete infrastructure (Docker, K8s, CI/CD)
|
||||
- ✅ Complete documentation (4,600+ lines)
|
||||
- ✅ Complete testing framework (23 tests)
|
||||
- ✅ Production-grade build (42.70 kB gzip)
|
||||
- ✅ Security hardening
|
||||
- ✅ Performance optimization
|
||||
- ✅ Deployment automation
|
||||
|
||||
**Ready for**: Development, Staging, Production Deployment
|
||||
|
||||
---
|
||||
|
||||
**Generated**: 2026-01-19
|
||||
**Version**: 1.0 FINAL
|
||||
**Status**: ✅ ALL DELIVERABLES COMPLETE
|
||||
|
||||
@@ -1,926 +0,0 @@
|
||||
# Deployment Guide - Job Info Application
|
||||
|
||||
## Phase 9: Production Deployment
|
||||
|
||||
This guide covers preparing the Job Info application for production deployment including Docker configuration, environment setup, and CI/CD pipeline.
|
||||
|
||||
---
|
||||
|
||||
## 1. Deployment Architecture
|
||||
|
||||
### Production Stack
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Client Browser (Edge) │
|
||||
│ - Svelte SPA (hash-based routing) │
|
||||
│ - Service Worker (offline + refresh) │
|
||||
│ - IndexedDB (client-side cache) │
|
||||
└─────────────────┬───────────────────────┘
|
||||
│ HTTPS
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ CDN (CloudFlare/CloudFront) │
|
||||
│ - Cache static assets (CSS, JS, HTML) │
|
||||
│ - Serve from edge location │
|
||||
│ - 30-day cache for assets │
|
||||
└─────────────────┬───────────────────────┘
|
||||
│ HTTPS
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Load Balancer (ALB/NLB or Ingress) │
|
||||
│ - Route /api to backend │
|
||||
│ - Route static to CDN │
|
||||
│ - Health checks every 30s │
|
||||
└─────────────────┬───────────────────────┘
|
||||
│ HTTPS
|
||||
┌───────┴──────────┐
|
||||
↓ ↓
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ Backend #1 │ │ Backend #2 │
|
||||
│ Hono Server │ │ Hono Server │
|
||||
│ Port 3005 │ │ Port 3005 │
|
||||
└──────┬───────┘ └──────┬───────┘
|
||||
│ │
|
||||
└────────┬────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ PostgreSQL/Redis │
|
||||
│ (Persistent Storage)│
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Docker Setup
|
||||
|
||||
### 2.1 Dockerfile - Backend
|
||||
|
||||
Create `Mode3Test/Dockerfile`:
|
||||
|
||||
```dockerfile
|
||||
# Build stage
|
||||
FROM oven/bun:latest as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json bun.lockb ./
|
||||
|
||||
# Install dependencies
|
||||
RUN bun install --production
|
||||
|
||||
# Copy source
|
||||
COPY backend/ ./backend/
|
||||
COPY .env ./
|
||||
|
||||
# Runtime stage
|
||||
FROM oven/bun:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy from builder
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/backend ./backend
|
||||
COPY --from=builder /app/.env ./
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3005
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD bun --eval "fetch('http://localhost:3005/health').then(r => r.status === 200 ? process.exit(0) : process.exit(1))"
|
||||
|
||||
# Start server
|
||||
CMD ["bun", "run", "backend/server.ts"]
|
||||
```
|
||||
|
||||
### 2.2 Dockerfile - Frontend
|
||||
|
||||
Create `frontend/Dockerfile`:
|
||||
|
||||
```dockerfile
|
||||
# Build stage
|
||||
FROM oven/bun:latest as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json bun.lockb ./
|
||||
|
||||
# Install dependencies
|
||||
RUN bun install
|
||||
|
||||
# Copy source
|
||||
COPY . .
|
||||
|
||||
# Build
|
||||
RUN bun run build
|
||||
|
||||
# Runtime stage - lightweight
|
||||
FROM nginx:alpine
|
||||
|
||||
# Copy built assets
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# Copy nginx config
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --quiet --tries=1 --spider http://localhost:80/index.html || exit 1
|
||||
|
||||
# Start nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
```
|
||||
|
||||
### 2.3 Nginx Configuration
|
||||
|
||||
Create `frontend/nginx.conf`:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
# Security headers
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' https://graph.microsoft.com https://*.microsoft.com" always;
|
||||
|
||||
# Compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1000;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss;
|
||||
|
||||
# Root location
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
# SPA routing - serve index.html for all non-asset requests
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
expires -1;
|
||||
add_header Cache-Control "public, must-revalidate, proxy-revalidate";
|
||||
}
|
||||
|
||||
# Static assets with long cache
|
||||
location ~* \.(js|css|woff|woff2|eot|ttf|otf|svg|png|jpg|jpeg|gif|ico)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# API proxy
|
||||
location /api/ {
|
||||
proxy_pass http://backend:3005;
|
||||
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;
|
||||
|
||||
# CORS headers
|
||||
add_header Access-Control-Allow-Origin "*" always;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
|
||||
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
|
||||
|
||||
if ($request_method = 'OPTIONS') {
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 Docker Compose - Development
|
||||
|
||||
Create `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# PostgreSQL Database
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: job_info_user
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-dev_password}
|
||||
POSTGRES_DB: job_info_db
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U job_info_user"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Redis Cache
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Backend API
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Mode3Test/Dockerfile
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 3005
|
||||
DATABASE_URL: postgresql://job_info_user:${DB_PASSWORD:-dev_password}@postgres:5432/job_info_db
|
||||
REDIS_URL: redis://redis:6379
|
||||
MICROSOFT_TENANT_ID: ${MICROSOFT_TENANT_ID}
|
||||
MICROSOFT_CLIENT_ID: ${MICROSOFT_CLIENT_ID}
|
||||
MICROSOFT_CLIENT_SECRET: ${MICROSOFT_CLIENT_SECRET}
|
||||
MICROSOFT_REDIRECT_URI: ${MICROSOFT_REDIRECT_URI:-http://localhost:3005/api/auth/callback}
|
||||
ports:
|
||||
- "3005:3005"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3005/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
# Frontend Web Server
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: frontend/Dockerfile
|
||||
ports:
|
||||
- "80:80"
|
||||
depends_on:
|
||||
- backend
|
||||
environment:
|
||||
BACKEND_URL: http://backend:3005
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/index.html"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: job-info-network
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Environment Configuration
|
||||
|
||||
### 3.1 Production .env
|
||||
|
||||
Create `.env.production`:
|
||||
|
||||
```bash
|
||||
# Application
|
||||
NODE_ENV=production
|
||||
PORT=3005
|
||||
|
||||
# Database (update with production credentials)
|
||||
DATABASE_URL=postgresql://user:password@prod-db:5432/job_info_prod
|
||||
REDIS_URL=redis://prod-redis:6379
|
||||
|
||||
# Microsoft OAuth (register app in Azure)
|
||||
MICROSOFT_TENANT_ID=your-tenant-id
|
||||
MICROSOFT_CLIENT_ID=your-client-id
|
||||
MICROSOFT_CLIENT_SECRET=your-client-secret
|
||||
MICROSOFT_REDIRECT_URI=https://app.example.com/api/auth/callback
|
||||
|
||||
# Security
|
||||
JWT_SECRET=your-super-secret-key-min-32-chars
|
||||
SESSION_SECRET=your-session-secret-key
|
||||
|
||||
# Frontend
|
||||
VITE_API_URL=https://app.example.com/api
|
||||
VITE_GRAPH_API_BASE=https://graph.microsoft.com/v1.0
|
||||
VITE_OAUTH_CLIENT_ID=your-client-id
|
||||
VITE_OAUTH_TENANT_ID=your-tenant-id
|
||||
VITE_OAUTH_REDIRECT_URI=https://app.example.com/#/callback
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=info
|
||||
SENTRY_DSN=https://your-sentry-key@sentry.io/project-id
|
||||
|
||||
# Monitoring
|
||||
DATADOG_API_KEY=your-datadog-key (optional)
|
||||
```
|
||||
|
||||
### 3.2 Secrets Management
|
||||
|
||||
**Option A: AWS Secrets Manager**
|
||||
```bash
|
||||
# Store in AWS Secrets Manager
|
||||
aws secretsmanager create-secret \
|
||||
--name job-info/prod \
|
||||
--secret-string '{
|
||||
"db_password": "...",
|
||||
"jwt_secret": "...",
|
||||
"microsoft_client_secret": "..."
|
||||
}'
|
||||
|
||||
# Reference in deployment
|
||||
aws secretsmanager get-secret-value --secret-id job-info/prod
|
||||
```
|
||||
|
||||
**Option B: Kubernetes Secrets**
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: job-info-secrets
|
||||
type: Opaque
|
||||
stringData:
|
||||
database_password: "..."
|
||||
jwt_secret: "..."
|
||||
microsoft_client_secret: "..."
|
||||
```
|
||||
|
||||
**Option C: Environment Variables**
|
||||
```bash
|
||||
# Set directly in container runtime
|
||||
docker run \
|
||||
-e DATABASE_URL="postgresql://..." \
|
||||
-e JWT_SECRET="..." \
|
||||
job-info-backend:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Kubernetes Deployment
|
||||
|
||||
### 4.1 Deployment Manifests
|
||||
|
||||
Create `k8s/backend-deployment.yaml`:
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: job-info-backend
|
||||
labels:
|
||||
app: job-info-backend
|
||||
spec:
|
||||
replicas: 3 # Run 3 backend instances
|
||||
selector:
|
||||
matchLabels:
|
||||
app: job-info-backend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: job-info-backend
|
||||
spec:
|
||||
containers:
|
||||
- name: backend
|
||||
image: your-registry/job-info-backend:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 3005
|
||||
name: http
|
||||
env:
|
||||
- name: NODE_ENV
|
||||
value: "production"
|
||||
- name: DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: job-info-secrets
|
||||
key: database_url
|
||||
- name: MICROSOFT_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: job-info-secrets
|
||||
key: microsoft_client_secret
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3005
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3005
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: job-info-backend-service
|
||||
spec:
|
||||
selector:
|
||||
app: job-info-backend
|
||||
ports:
|
||||
- port: 3005
|
||||
targetPort: 3005
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: job-info-backend-hpa
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: job-info-backend
|
||||
minReplicas: 3
|
||||
maxReplicas: 10
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 70
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 80
|
||||
```
|
||||
|
||||
Create `k8s/frontend-deployment.yaml`:
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: job-info-frontend
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: job-info-frontend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: job-info-frontend
|
||||
spec:
|
||||
containers:
|
||||
- name: frontend
|
||||
image: your-registry/job-info-frontend:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 80
|
||||
resources:
|
||||
requests:
|
||||
memory: "128Mi"
|
||||
cpu: "50m"
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
cpu: "250m"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /index.html
|
||||
port: 80
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: job-info-frontend-service
|
||||
spec:
|
||||
selector:
|
||||
app: job-info-frontend
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
type: LoadBalancer
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: job-info-ingress
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: app.example.com
|
||||
http:
|
||||
paths:
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: job-info-backend-service
|
||||
port:
|
||||
number: 3005
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: job-info-frontend-service
|
||||
port:
|
||||
number: 80
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. CI/CD Pipeline
|
||||
|
||||
### 5.1 GitHub Actions Workflow
|
||||
|
||||
Create `.github/workflows/deploy.yml`:
|
||||
|
||||
```yaml
|
||||
name: Deploy to Production
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd frontend && bun install
|
||||
cd ../Mode3Test && bun install
|
||||
|
||||
- name: Run tests
|
||||
run: bun test
|
||||
|
||||
- name: Lint
|
||||
run: |
|
||||
cd frontend && bun run lint || true
|
||||
cd ../Mode3Test && bun run lint || true
|
||||
|
||||
build:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
|
||||
- name: Build frontend
|
||||
run: |
|
||||
cd frontend
|
||||
bun install
|
||||
bun run build
|
||||
|
||||
- name: Build Docker images
|
||||
run: |
|
||||
docker build -t job-info-backend:${{ github.sha }} -f Mode3Test/Dockerfile .
|
||||
docker build -t job-info-frontend:${{ github.sha }} -f frontend/Dockerfile .
|
||||
|
||||
- name: Push to registry
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
docker tag job-info-backend:${{ github.sha }} your-registry/job-info-backend:latest
|
||||
docker tag job-info-frontend:${{ github.sha }} your-registry/job-info-frontend:latest
|
||||
|
||||
docker push your-registry/job-info-backend:latest
|
||||
docker push your-registry/job-info-frontend:latest
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Deploy to Kubernetes
|
||||
run: |
|
||||
kubectl apply -f k8s/
|
||||
kubectl set image deployment/job-info-backend \
|
||||
job-info-backend=your-registry/job-info-backend:latest
|
||||
kubectl set image deployment/job-info-frontend \
|
||||
job-info-frontend=your-registry/job-info-frontend:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Monitoring & Observability
|
||||
|
||||
### 6.1 Logging
|
||||
|
||||
Setup structured logging:
|
||||
|
||||
```typescript
|
||||
// backend/logging.ts
|
||||
import pino from 'pino';
|
||||
|
||||
export const logger = pino({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
transport: {
|
||||
targets: [
|
||||
{
|
||||
target: 'pino/file',
|
||||
options: { destination: '/var/log/job-info.log' }
|
||||
},
|
||||
{
|
||||
target: 'pino-stackdriver',
|
||||
options: { projectId: 'your-gcp-project' }
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 6.2 Metrics
|
||||
|
||||
Setup Prometheus metrics:
|
||||
|
||||
```typescript
|
||||
// backend/metrics.ts
|
||||
import promClient from 'prom-client';
|
||||
|
||||
export const httpRequestDuration = new promClient.Histogram({
|
||||
name: 'http_request_duration_seconds',
|
||||
help: 'Duration of HTTP requests in seconds',
|
||||
labelNames: ['method', 'route', 'status_code'],
|
||||
});
|
||||
|
||||
export const activeAuthSessions = new promClient.Gauge({
|
||||
name: 'active_auth_sessions',
|
||||
help: 'Number of active authentication sessions',
|
||||
});
|
||||
```
|
||||
|
||||
### 6.3 Error Tracking
|
||||
|
||||
Setup Sentry:
|
||||
|
||||
```typescript
|
||||
// backend/sentry.ts
|
||||
import * as Sentry from "@sentry/node";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.SENTRY_DSN,
|
||||
environment: process.env.NODE_ENV,
|
||||
tracesSampleRate: 1.0,
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Backup & Disaster Recovery
|
||||
|
||||
### 7.1 Database Backup
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# backup-postgres.sh
|
||||
|
||||
BACKUP_DIR="/backups/postgres"
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
|
||||
# Create backup
|
||||
pg_dump -U job_info_user job_info_db | gzip > $BACKUP_DIR/backup_$DATE.sql.gz
|
||||
|
||||
# Upload to S3
|
||||
aws s3 cp $BACKUP_DIR/backup_$DATE.sql.gz s3://job-info-backups/
|
||||
|
||||
# Keep only last 30 days
|
||||
find $BACKUP_DIR -mtime +30 -delete
|
||||
```
|
||||
|
||||
### 7.2 Recovery Procedure
|
||||
|
||||
```bash
|
||||
# Restore from backup
|
||||
gunzip < /backups/postgres/backup_20240101_000000.sql.gz | \
|
||||
psql -U job_info_user job_info_db
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Security Hardening
|
||||
|
||||
### 8.1 TLS/SSL Certificate
|
||||
|
||||
```bash
|
||||
# Using Let's Encrypt with Certbot
|
||||
certbot certonly --standalone \
|
||||
-d app.example.com \
|
||||
-d www.app.example.com
|
||||
|
||||
# Auto-renew
|
||||
certbot renew --dry-run
|
||||
```
|
||||
|
||||
### 8.2 DDoS Protection
|
||||
|
||||
```nginx
|
||||
# In nginx.conf
|
||||
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
|
||||
limit_req_zone $binary_remote_addr zone=api:10m rate=100r/s;
|
||||
|
||||
location / {
|
||||
limit_req zone=general burst=20 nodelay;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
limit_req zone=api burst=200 nodelay;
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 Security Headers
|
||||
|
||||
```yaml
|
||||
# Kubernetes SecurityPolicy
|
||||
apiVersion: policy/v1beta1
|
||||
kind: PodSecurityPolicy
|
||||
metadata:
|
||||
name: job-info-psp
|
||||
spec:
|
||||
privileged: false
|
||||
allowPrivilegeEscalation: false
|
||||
requiredDropCapabilities:
|
||||
- ALL
|
||||
volumes:
|
||||
- 'configMap'
|
||||
- 'emptyDir'
|
||||
- 'projected'
|
||||
- 'secret'
|
||||
- 'downwardAPI'
|
||||
- 'persistentVolumeClaim'
|
||||
hostNetwork: false
|
||||
hostIPC: false
|
||||
hostPID: false
|
||||
runAsUser:
|
||||
rule: 'MustRunAsNonRoot'
|
||||
seLinux:
|
||||
rule: 'MustRunAs'
|
||||
seLinuxOptions:
|
||||
level: "s0:c123,c456"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Scaling & Performance Tuning
|
||||
|
||||
### 9.1 Database Optimization
|
||||
|
||||
```sql
|
||||
-- Create indexes for common queries
|
||||
CREATE INDEX idx_jobs_location ON jobs(location);
|
||||
CREATE INDEX idx_jobs_company ON jobs(company_id);
|
||||
CREATE INDEX idx_users_email ON users(email);
|
||||
|
||||
-- Monitor slow queries
|
||||
SET log_min_duration_statement = 1000; -- Log queries > 1s
|
||||
|
||||
-- Connection pooling with PgBouncer
|
||||
[databases]
|
||||
job_info_db = host=prod-db port=5432 user=job_info_user password=...
|
||||
```
|
||||
|
||||
### 9.2 Redis Caching
|
||||
|
||||
```typescript
|
||||
// Implement caching layer
|
||||
async function getJobsWithCache(userId: string): Promise<Job[]> {
|
||||
const cacheKey = `jobs:${userId}`;
|
||||
|
||||
// Check cache first
|
||||
const cached = await redis.get(cacheKey);
|
||||
if (cached) return JSON.parse(cached);
|
||||
|
||||
// Query database
|
||||
const jobs = await database.query(`SELECT * FROM jobs WHERE user_id = $1`, [userId]);
|
||||
|
||||
// Cache for 5 minutes
|
||||
await redis.setex(cacheKey, 300, JSON.stringify(jobs));
|
||||
|
||||
return jobs;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Deployment Checklist
|
||||
|
||||
**Pre-Deployment**:
|
||||
- [ ] All tests passing (unit, integration, e2e)
|
||||
- [ ] Code review approved
|
||||
- [ ] Security scan completed (SAST, dependency check)
|
||||
- [ ] Database migration tested
|
||||
- [ ] Environment variables configured
|
||||
- [ ] Backup taken
|
||||
- [ ] Rollback plan prepared
|
||||
|
||||
**Deployment**:
|
||||
- [ ] Deploy to staging first
|
||||
- [ ] Run smoke tests on staging
|
||||
- [ ] Verify metrics and logs
|
||||
- [ ] Deploy to production (canary)
|
||||
- [ ] Monitor error rate (< 0.1%)
|
||||
- [ ] Monitor response time (< 200ms p95)
|
||||
- [ ] Monitor CPU/Memory usage
|
||||
|
||||
**Post-Deployment**:
|
||||
- [ ] Verify all endpoints responding
|
||||
- [ ] Test OAuth flow
|
||||
- [ ] Verify token refresh
|
||||
- [ ] Check job loading
|
||||
- [ ] Validate user experience
|
||||
- [ ] Monitor for 24 hours
|
||||
- [ ] Document any issues
|
||||
|
||||
---
|
||||
|
||||
## 11. Rollback Procedure
|
||||
|
||||
If deployment fails or causes issues:
|
||||
|
||||
```bash
|
||||
# Kubernetes rollback
|
||||
kubectl rollout undo deployment/job-info-backend
|
||||
kubectl rollout undo deployment/job-info-frontend
|
||||
|
||||
# Docker rollback
|
||||
docker service update --image job-info-backend:previous job-info-backend
|
||||
|
||||
# Database rollback (if needed)
|
||||
psql -U job_info_user job_info_db < /backups/postgres/backup_20240101_000000.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Production Monitoring Dashboard
|
||||
|
||||
Setup Grafana dashboard with:
|
||||
- Request rate (req/sec)
|
||||
- Error rate (%)
|
||||
- Response time (p50, p95, p99)
|
||||
- CPU/Memory usage
|
||||
- Database connections
|
||||
- Redis hit rate
|
||||
- Active sessions
|
||||
|
||||
**Alerts to configure**:
|
||||
- Error rate > 1%
|
||||
- Response time p95 > 1s
|
||||
- CPU > 80%
|
||||
- Memory > 85%
|
||||
- Database connections > 80%
|
||||
- Disk space < 10%
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Deployment checklist:
|
||||
✅ Docker images built and tested
|
||||
✅ Environment variables configured
|
||||
✅ Database initialized with backups
|
||||
✅ Kubernetes manifests created
|
||||
✅ CI/CD pipeline configured
|
||||
✅ Monitoring and logging setup
|
||||
✅ Security hardening applied
|
||||
✅ Scaling policies defined
|
||||
✅ Rollback procedures documented
|
||||
✅ On-call procedures established
|
||||
|
||||
**Next Steps**:
|
||||
1. Set up production infrastructure
|
||||
2. Run through deployment checklist
|
||||
3. Deploy to staging first
|
||||
4. Execute smoke tests
|
||||
5. Deploy to production
|
||||
6. Monitor for first 24 hours
|
||||
@@ -1,101 +0,0 @@
|
||||
# 📑 DOCUMENTATION INDEX & NAVIGATION
|
||||
|
||||
**Project**: Job Info - Production Svelte OAuth2 Application
|
||||
**Status**: ✅ 100% COMPLETE & PRODUCTION READY
|
||||
**Generated**: 2026-01-19
|
||||
|
||||
---
|
||||
|
||||
## 🚀 START HERE
|
||||
|
||||
### For First-Time Setup
|
||||
1. **[QUICK_START.md](QUICK_START.md)** ⭐ **START HERE**
|
||||
- 2-minute setup guide
|
||||
- 3 deployment options (Local, Docker, K8s)
|
||||
- Quick verification checklist
|
||||
- Troubleshooting guide
|
||||
|
||||
### For Automated Setup
|
||||
2. **[setup.sh](setup.sh)** - Interactive setup script
|
||||
- 9 automated options
|
||||
- System status check
|
||||
- Docker management
|
||||
- Kubernetes deployment
|
||||
|
||||
---
|
||||
|
||||
## 📚 COMPREHENSIVE GUIDES
|
||||
|
||||
### Project Overview
|
||||
- **[PROJECT_COMPLETION_FINAL.md](PROJECT_COMPLETION_FINAL.md)** (100+ lines)
|
||||
- All 9 phases completed
|
||||
- Build metrics (42.70 kB gzip)
|
||||
- Code statistics (12,400+ lines)
|
||||
- Success metrics
|
||||
- Next steps
|
||||
|
||||
### Deliverables Checklist
|
||||
- **[DELIVERABLES.md](DELIVERABLES.md)** (100+ lines)
|
||||
- Complete inventory of all deliverables
|
||||
- Frontend, Backend, Infrastructure
|
||||
- Documentation, Testing, Deployment
|
||||
- Compliance checklist
|
||||
- Quality metrics
|
||||
|
||||
### Testing Procedures
|
||||
- **[PHASE_8_QUICK_START.md](PHASE_8_QUICK_START.md)** (500+ lines)
|
||||
- 23 test scenarios
|
||||
- Step-by-step procedures
|
||||
- Checkbox tracking
|
||||
- Expected results
|
||||
- 2h 45m estimated execution
|
||||
|
||||
- **[TESTING_GUIDE.md](TESTING_GUIDE.md)** (500+ lines)
|
||||
- Detailed test procedures
|
||||
- Browser commands
|
||||
- DevTools instructions
|
||||
- Expected results
|
||||
- Regression testing
|
||||
|
||||
### Deployment Guide
|
||||
- **[DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md)** (300+ lines)
|
||||
- Docker Compose setup
|
||||
- Kubernetes deployment
|
||||
- GitHub Actions CI/CD
|
||||
- Configuration
|
||||
- Troubleshooting
|
||||
|
||||
### Phase Summaries
|
||||
- **[PHASE_9_DEPLOYMENT_COMPLETE.md](PHASE_9_DEPLOYMENT_COMPLETE.md)** (500+ lines)
|
||||
- Docker infrastructure
|
||||
- Kubernetes manifests
|
||||
- CI/CD pipelines
|
||||
- Security features
|
||||
- Scaling configuration
|
||||
|
||||
- **[PHASE_8_TESTING_LOG.md](PHASE_8_TESTING_LOG.md)** (700+ lines)
|
||||
- 8 test categories
|
||||
- 23 individual tests
|
||||
- Detailed procedures
|
||||
- Expected results
|
||||
- Test results template
|
||||
|
||||
---
|
||||
|
||||
## ✅ COMPLETION CHECKLIST
|
||||
|
||||
All 4 final deliverable files created:
|
||||
- [x] QUICK_START.md - Quick setup guide
|
||||
- [x] DELIVERABLES.md - Complete checklist
|
||||
- [x] setup.sh - Automation script
|
||||
- [x] FINAL_COMPLETION_SUMMARY.txt - Project overview
|
||||
|
||||
**Total Documentation**: 4,600+ lines ✅
|
||||
**Total Code**: 7,550+ lines ✅
|
||||
**Build Quality**: 42.70 kB gzip ✅
|
||||
**Type Safety**: 100% strict TypeScript ✅
|
||||
|
||||
---
|
||||
|
||||
**👉 Begin with [QUICK_START.md](QUICK_START.md)**
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
# Backend Dockerfile - Bun Runtime with Hono Framework
|
||||
# Optimized for production deployment
|
||||
|
||||
FROM oven/bun:latest as builder
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy backend source code
|
||||
COPY Mode3Test/backend ./backend
|
||||
COPY Mode3Test/package.json .
|
||||
COPY Mode3Test/bunfig.toml .
|
||||
|
||||
# Install dependencies
|
||||
RUN bun install --production
|
||||
|
||||
# Build step (if needed)
|
||||
# RUN bun run build
|
||||
|
||||
# Runtime stage
|
||||
FROM oven/bun:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy from builder
|
||||
COPY --from=builder /app ./
|
||||
|
||||
# Set environment variables
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3005
|
||||
ENV LOG_LEVEL=info
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD bun run --eval "fetch('http://localhost:3005/health').then(r => r.status === 200 ? process.exit(0) : process.exit(1))" || exit 1
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3005
|
||||
|
||||
# Start application
|
||||
CMD ["bun", "run", "dev"]
|
||||
@@ -1,37 +0,0 @@
|
||||
# Frontend Dockerfile - Node with Static Serving
|
||||
# Optimized for production with minimal size
|
||||
|
||||
FROM node:18-alpine as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy frontend
|
||||
COPY frontend/package.json frontend/package-lock.json ./
|
||||
COPY frontend/. ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci
|
||||
|
||||
# Build production bundle
|
||||
RUN npm run build
|
||||
|
||||
# Runtime stage - nginx lightweight server
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy built files
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
# Install global serve package for static file serving
|
||||
RUN npm install -g serve
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget --quiet --tries=1 --spider http://localhost:5173 || exit 1
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5173
|
||||
|
||||
# Start static server
|
||||
CMD ["serve", "-s", "dist", "-l", "5173"]
|
||||
@@ -1,348 +0,0 @@
|
||||
╔════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ 🎉 JOB INFO - PROJECT COMPLETION SUMMARY 🎉 ║
|
||||
║ ║
|
||||
║ Production-Ready Svelte OAuth2 App ║
|
||||
║ ║
|
||||
╚════════════════════════════════════════════════════════════════════════════╝
|
||||
|
||||
DATE: 2026-01-19
|
||||
STATUS: ✅ 100% COMPLETE & PRODUCTION READY
|
||||
DURATION: 10.5 hours total development time
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
📊 PHASES COMPLETED
|
||||
|
||||
Phase 1: Project Setup ✅ Complete
|
||||
Phase 2: Core Services ✅ Complete
|
||||
Phase 3: UI Components ✅ Complete
|
||||
Phase 4: OAuth2 Integration ✅ Complete
|
||||
Phase 5: Service Worker ✅ Complete
|
||||
Phase 6: Backend OAuth ✅ Complete
|
||||
Phase 7: Error Handling ✅ Complete
|
||||
Phase 8: Testing Framework ✅ Complete
|
||||
Phase 9: Deployment Infrastructure ✅ Complete
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
📈 BUILD METRICS
|
||||
|
||||
JavaScript: 42.70 kB (gzip) ✅ Target: < 50 kB
|
||||
CSS: 4.08 kB (gzip) ✅ Target: < 5 kB
|
||||
HTML: 0.32 kB ✅ Target: < 1 kB
|
||||
Build Time: 2.70 seconds ✅ Target: < 5 seconds
|
||||
|
||||
Type Errors: 0 ✅
|
||||
Console Errors: 0 ✅
|
||||
Warnings: 0 ✅
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
📦 DELIVERABLES
|
||||
|
||||
Frontend:
|
||||
✅ Vite 5.4.21 + Svelte 4.2.20 + TypeScript 5.9.3
|
||||
✅ 6 UI Components
|
||||
✅ 7 Services/Stores
|
||||
✅ Service Worker
|
||||
✅ IndexedDB caching
|
||||
✅ Responsive design
|
||||
|
||||
Backend:
|
||||
✅ Bun runtime + Hono framework
|
||||
✅ OAuth2 PKCE integration
|
||||
✅ Token management
|
||||
✅ API routes
|
||||
|
||||
Infrastructure:
|
||||
✅ 3 Dockerfiles (multi-stage)
|
||||
✅ Docker Compose setup
|
||||
✅ 4 Kubernetes manifests
|
||||
✅ 2 GitHub Actions workflows
|
||||
✅ Auto-scaling (HPA 2-5 replicas)
|
||||
✅ HA configuration
|
||||
✅ Monitoring ready
|
||||
|
||||
Testing:
|
||||
✅ 23 test scenarios documented
|
||||
✅ 8 test categories
|
||||
✅ Complete procedures
|
||||
✅ Expected results defined
|
||||
|
||||
Documentation:
|
||||
✅ 4,600+ lines of guides
|
||||
✅ Quick start guide
|
||||
✅ Deployment procedures
|
||||
✅ Testing guidelines
|
||||
✅ Architecture documentation
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
🚀 DEPLOYMENT OPTIONS
|
||||
|
||||
Option 1: Local Development (2 minutes)
|
||||
cd Mode3Test && bun run dev
|
||||
cd frontend && npm run dev
|
||||
Open: http://localhost:5173
|
||||
|
||||
Option 2: Docker Compose (3 minutes)
|
||||
docker-compose up -d
|
||||
Open: http://localhost:5173
|
||||
|
||||
Option 3: Kubernetes (5 minutes)
|
||||
kubectl apply -f k8s-config.yaml
|
||||
kubectl apply -f k8s-backend-deployment.yaml
|
||||
kubectl apply -f k8s-frontend-deployment.yaml
|
||||
|
||||
Option 4: GitHub Actions (Automatic)
|
||||
Push to main branch
|
||||
Automated test → build → deploy
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
✅ FEATURES IMPLEMENTED
|
||||
|
||||
Authentication:
|
||||
✅ OAuth2 PKCE with Microsoft
|
||||
✅ Automatic token refresh (60s poll)
|
||||
✅ 1-hour token expiry + 5min buffer
|
||||
✅ Secure token storage (sessionStorage)
|
||||
|
||||
Data Management:
|
||||
✅ IndexedDB caching (24-hour TTL)
|
||||
✅ Offline browsing support
|
||||
✅ Auto-sync on reconnect
|
||||
✅ Job CRUD operations
|
||||
|
||||
Error Handling:
|
||||
✅ Structured logging (5 levels)
|
||||
✅ ErrorBoundary component
|
||||
✅ Automatic retry (exponential backoff)
|
||||
✅ User-friendly error messages
|
||||
|
||||
UI/UX:
|
||||
✅ Responsive design (375-1920px)
|
||||
✅ Loading states
|
||||
✅ Toast notifications
|
||||
✅ Keyboard navigation
|
||||
✅ Screen reader support
|
||||
|
||||
Security:
|
||||
✅ OAuth2 PKCE protection
|
||||
✅ CSRF state validation
|
||||
✅ XSS auto-escaping (Svelte)
|
||||
✅ HTTPS/TLS ready
|
||||
✅ Pod security hardened
|
||||
✅ Network policies
|
||||
✅ Secret scanning
|
||||
✅ Vulnerability scanning
|
||||
|
||||
Performance:
|
||||
✅ 42.70 kB JavaScript (gzip)
|
||||
✅ 4.08 kB CSS (gzip)
|
||||
✅ 2.70 second build time
|
||||
✅ Optimized bundle (52 modules)
|
||||
✅ Vite + esbuild optimization
|
||||
|
||||
Scaling:
|
||||
✅ Horizontal Pod Autoscaling
|
||||
✅ 2-5 replica configuration
|
||||
✅ Load balancing
|
||||
✅ Rolling updates
|
||||
✅ Zero-downtime deployment
|
||||
|
||||
Monitoring:
|
||||
✅ ServiceMonitor for Prometheus
|
||||
✅ Structured logging
|
||||
✅ Health checks (K8s + Docker)
|
||||
✅ Performance metrics
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
📚 DOCUMENTATION FILES
|
||||
|
||||
Quick Start:
|
||||
📄 QUICK_START.md - 2-minute setup guide
|
||||
📄 setup.sh - Automated setup script
|
||||
|
||||
Phase Summaries:
|
||||
📄 PROJECT_COMPLETION_FINAL.md - Full project overview
|
||||
📄 PHASE_9_DEPLOYMENT_COMPLETE.md - Deployment details
|
||||
📄 PHASE_8_QUICK_START.md - Testing procedures
|
||||
📄 DELIVERABLES.md - Complete checklist
|
||||
|
||||
Reference Guides:
|
||||
📄 TESTING_GUIDE.md - Test procedures
|
||||
📄 DEPLOYMENT_GUIDE.md - Deployment guide
|
||||
📄 PROJECT_COMPLETE.md - Project summary
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
📊 CODE STATISTICS
|
||||
|
||||
Frontend Code: ~5,500 lines
|
||||
Backend Code: ~600 lines
|
||||
Infrastructure Code: ~1,450 lines
|
||||
Documentation: ~4,600 lines
|
||||
Configuration: ~250 lines
|
||||
────────────────────────────────────
|
||||
TOTAL DELIVERABLES: ~12,400 lines
|
||||
|
||||
Modules: 52
|
||||
Components: 6
|
||||
Services: 7
|
||||
Test Scenarios: 23
|
||||
Docker Files: 3
|
||||
Kubernetes Files: 4
|
||||
CI/CD Workflows: 2
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
🎯 QUALITY ASSURANCE
|
||||
|
||||
Code Quality:
|
||||
✅ TypeScript strict mode enabled
|
||||
✅ 100% type coverage (services/components)
|
||||
✅ Zero type errors
|
||||
✅ Zero console errors
|
||||
✅ Zero lint errors
|
||||
|
||||
Security:
|
||||
✅ OAuth2 PKCE implemented
|
||||
✅ CSRF protection (state tokens)
|
||||
✅ XSS prevention (auto-escaping)
|
||||
✅ Secret scanning enabled
|
||||
✅ Vulnerability scanning enabled
|
||||
✅ Pod security hardened
|
||||
|
||||
Performance:
|
||||
✅ Build size: 42.70 kB (11% under target)
|
||||
✅ Build time: 2.70s (46% under target)
|
||||
✅ Bundle optimized (52 modules)
|
||||
✅ Gzip compression enabled
|
||||
|
||||
Accessibility:
|
||||
✅ Semantic HTML
|
||||
✅ ARIA labels
|
||||
✅ Keyboard navigation
|
||||
✅ Screen reader compatible
|
||||
✅ Color contrast verified
|
||||
|
||||
Deployment:
|
||||
✅ Docker best practices
|
||||
✅ Multi-stage builds
|
||||
✅ Kubernetes manifests
|
||||
✅ CI/CD pipeline
|
||||
✅ Health checks
|
||||
✅ Monitoring ready
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
🏆 PROJECT HIGHLIGHTS
|
||||
|
||||
✨ Production-Grade Quality:
|
||||
• Enterprise-class security
|
||||
• Comprehensive error handling
|
||||
• Structured logging throughout
|
||||
• Full test coverage
|
||||
|
||||
✨ Optimized Performance:
|
||||
• 11% below bundle size target
|
||||
• 46% faster than build time target
|
||||
• Optimized for mobile/tablet/desktop
|
||||
• Service Worker caching
|
||||
|
||||
✨ Fully Documented:
|
||||
• 4,600+ lines of documentation
|
||||
• 23 test scenarios
|
||||
• Step-by-step deployment guides
|
||||
• Architecture documentation
|
||||
|
||||
✨ Deployment Ready:
|
||||
• Docker containerization
|
||||
• Kubernetes orchestration
|
||||
• GitHub Actions CI/CD
|
||||
• Auto-scaling configuration
|
||||
• Monitoring integration
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
🚀 QUICK START (Choose One)
|
||||
|
||||
1. Local Development:
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ cd Mode3Test && bun run dev │
|
||||
│ cd frontend && npm run dev │
|
||||
│ Open: http://localhost:5173 │
|
||||
└─────────────────────────────────────────────┘
|
||||
|
||||
2. Docker Compose:
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ docker-compose up -d │
|
||||
│ Open: http://localhost:5173 │
|
||||
└─────────────────────────────────────────────┘
|
||||
|
||||
3. Use Setup Script:
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ chmod +x setup.sh │
|
||||
│ ./setup.sh │
|
||||
│ Select option 1 or 2 │
|
||||
└─────────────────────────────────────────────┘
|
||||
|
||||
For detailed instructions: See QUICK_START.md
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
📞 SUPPORT RESOURCES
|
||||
|
||||
Documentation:
|
||||
• QUICK_START.md - 2-minute setup
|
||||
• DELIVERABLES.md - Complete checklist
|
||||
• PROJECT_COMPLETION_FINAL.md - Full overview
|
||||
|
||||
Testing:
|
||||
• PHASE_8_QUICK_START.md - Test procedures
|
||||
• TESTING_GUIDE.md - Complete test guide
|
||||
• 23 documented test scenarios
|
||||
|
||||
Deployment:
|
||||
• DEPLOYMENT_GUIDE.md - Step-by-step
|
||||
• docker-compose.yml - Local containers
|
||||
• k8s-*.yaml - Kubernetes manifests
|
||||
• .github/workflows/ - CI/CD pipeline
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
✅ COMPLETION CHECKLIST
|
||||
|
||||
Project Setup: ✅ Complete
|
||||
Code Development: ✅ Complete
|
||||
Testing Framework: ✅ Complete
|
||||
Documentation: ✅ Complete
|
||||
Docker Setup: ✅ Complete
|
||||
Kubernetes Setup: ✅ Complete
|
||||
CI/CD Pipeline: ✅ Complete
|
||||
Security Hardening: ✅ Complete
|
||||
Performance Verified: ✅ Complete
|
||||
Build Quality: ✅ Verified
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
🎉 PROJECT STATUS: PRODUCTION READY 🚀
|
||||
|
||||
All phases complete.
|
||||
All code complete.
|
||||
All infrastructure ready.
|
||||
All documentation complete.
|
||||
|
||||
Ready for immediate deployment.
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Generated: 2026-01-19
|
||||
Status: ✅ COMPLETE
|
||||
Version: 1.0 FINAL
|
||||
|
||||
Thank you for using Job Info - Production Svelte OAuth2 Application!
|
||||
|
||||
@@ -1,521 +0,0 @@
|
||||
# Job Info Application - Complete Index
|
||||
|
||||
**Status**: Phase 7 Complete ✅ | Phases 8-9 Ready 🔄
|
||||
**Project Progress**: 60% Complete
|
||||
**Quality**: Production Ready
|
||||
**Last Updated**: 2026-01-19
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentation Index
|
||||
|
||||
### Quick References
|
||||
1. **[PROJECT_COMPLETE.md](PROJECT_COMPLETE.md)** - Main documentation (600+ lines)
|
||||
- Quick start guide
|
||||
- Architecture overview
|
||||
- Technology stack
|
||||
- Configuration guide
|
||||
- Development workflow
|
||||
- Troubleshooting guide
|
||||
|
||||
2. **[PHASE_7_SUMMARY.md](PHASE_7_SUMMARY.md)** - Phase 7 detailed summary (400+ lines)
|
||||
- Deliverables list
|
||||
- Build results
|
||||
- Code quality metrics
|
||||
- Architecture overview
|
||||
- Feature completeness
|
||||
- Testing readiness
|
||||
|
||||
3. **[PHASE_7_COMPLETION_CHECKLIST.md](PHASE_7_COMPLETION_CHECKLIST.md)** - Verification checklist
|
||||
- All tasks verified
|
||||
- Build validation
|
||||
- Integration testing
|
||||
- Production readiness
|
||||
- Next phase readiness
|
||||
|
||||
### Testing & Deployment
|
||||
4. **[TESTING_GUIDE.md](TESTING_GUIDE.md)** - Phase 8 procedures (500+ lines)
|
||||
- 14 test section categories
|
||||
- OAuth2 flow testing
|
||||
- Token management validation
|
||||
- API error scenarios
|
||||
- Data storage testing
|
||||
- UI component testing
|
||||
- Performance benchmarks
|
||||
- Security testing
|
||||
- Accessibility compliance
|
||||
- Regression checklist
|
||||
|
||||
5. **[DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md)** - Phase 9 procedures (300+ lines)
|
||||
- Architecture diagram
|
||||
- Docker setup
|
||||
- Kubernetes manifests
|
||||
- CI/CD pipeline
|
||||
- Environment configuration
|
||||
- Monitoring setup
|
||||
- Backup procedures
|
||||
- Security hardening
|
||||
|
||||
### Reference Guides
|
||||
6. **[OAUTH2_REFERENCE_GUIDE.md](OAUTH2_REFERENCE_GUIDE.md)** - OAuth2 implementation reference
|
||||
- PKCE flow detailed
|
||||
- Token management
|
||||
- Refresh strategy
|
||||
- Security considerations
|
||||
- Troubleshooting
|
||||
|
||||
7. **[SVELTE_PATTERNS_REFERENCE.md](SVELTE_PATTERNS_REFERENCE.md)** - Svelte patterns used
|
||||
- Store patterns
|
||||
- Component patterns
|
||||
- Type patterns
|
||||
- Error handling patterns
|
||||
|
||||
8. **[SVELTE_DEVELOPMENT_SYSTEM.md](SVELTE_DEVELOPMENT_SYSTEM.md)** - Development practices
|
||||
- Folder structure
|
||||
- Naming conventions
|
||||
- Development workflow
|
||||
- Build process
|
||||
|
||||
9. **[PREPARATION_COMPLETE.md](PREPARATION_COMPLETE.md)** - Project preparation summary
|
||||
- Architecture decisions
|
||||
- Technology choices
|
||||
- Setup procedures
|
||||
|
||||
---
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
/home/admin/Job-Info-Test/
|
||||
│
|
||||
├── 📄 Documentation
|
||||
│ ├── PROJECT_COMPLETE.md ⭐ Main docs
|
||||
│ ├── PHASE_7_SUMMARY.md ⭐ Phase 7 details
|
||||
│ ├── PHASE_7_COMPLETION_CHECKLIST.md ✅ Verification
|
||||
│ ├── TESTING_GUIDE.md 🧪 Phase 8
|
||||
│ ├── DEPLOYMENT_GUIDE.md 🚀 Phase 9
|
||||
│ ├── OAUTH2_REFERENCE_GUIDE.md 📖 OAuth reference
|
||||
│ ├── SVELTE_PATTERNS_REFERENCE.md 📖 Patterns
|
||||
│ ├── SVELTE_DEVELOPMENT_SYSTEM.md 📖 Development
|
||||
│ ├── SVELTE_BUILD_PLAN.md 📖 Build details
|
||||
│ └── PREPARATION_COMPLETE.md 📖 Preparation
|
||||
│
|
||||
├── 💻 Frontend Application
|
||||
│ ├── frontend/
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── main.ts Entry point
|
||||
│ │ │ ├── App.svelte Root component
|
||||
│ │ │ ├── app.css Global styles
|
||||
│ │ │ │
|
||||
│ │ │ ├── pages/
|
||||
│ │ │ │ ├── Home.svelte Dashboard
|
||||
│ │ │ │ ├── SignIn.svelte Login page
|
||||
│ │ │ │ └── Callback.svelte OAuth callback
|
||||
│ │ │ │
|
||||
│ │ │ ├── components/
|
||||
│ │ │ │ ├── Navigation.svelte Header
|
||||
│ │ │ │ ├── JobCard.svelte Job display
|
||||
│ │ │ │ ├── JobList.svelte Job grid
|
||||
│ │ │ │ ├── SearchBar.svelte Search
|
||||
│ │ │ │ ├── NotificationContainer Toasts
|
||||
│ │ │ │ └── ErrorBoundary.svelte Error UI ⭐
|
||||
│ │ │ │
|
||||
│ │ │ ├── stores/
|
||||
│ │ │ │ ├── auth.ts Auth state
|
||||
│ │ │ │ ├── jobs.ts Jobs state
|
||||
│ │ │ │ └── ui.ts UI state
|
||||
│ │ │ │
|
||||
│ │ │ ├── services/
|
||||
│ │ │ │ ├── oauth.ts OAuth2 flow
|
||||
│ │ │ │ ├── graph.ts Graph API ⭐ Updated
|
||||
│ │ │ │ ├── errorHandler.ts Error classification
|
||||
│ │ │ │ └── logger.ts Logging ⭐ NEW
|
||||
│ │ │ │
|
||||
│ │ │ ├── utils/
|
||||
│ │ │ │ ├── api-request.ts Fetch wrapper ⭐ NEW
|
||||
│ │ │ │ ├── types.ts TypeScript types
|
||||
│ │ │ │ ├── constants.ts Configuration
|
||||
│ │ │ │ └── service-worker-manager.ts SW comm
|
||||
│ │ │ │
|
||||
│ │ │ └── lib/
|
||||
│ │ │ └── db.ts IndexedDB operations
|
||||
│ │ │
|
||||
│ │ ├── public/
|
||||
│ │ │ └── service-worker.js Service Worker
|
||||
│ │ │
|
||||
│ │ ├── package.json Dependencies (139)
|
||||
│ │ ├── vite.config.ts Build config
|
||||
│ │ ├── svelte.config.js Svelte config
|
||||
│ │ ├── tsconfig.json TypeScript
|
||||
│ │ ├── tailwind.config.js TailwindCSS
|
||||
│ │ ├── postcss.config.js PostCSS
|
||||
│ │ ├── index.html HTML entry
|
||||
│ │ ├── Dockerfile Container
|
||||
│ │ └── nginx.conf Web server
|
||||
│
|
||||
├── 🖥️ Backend API
|
||||
│ ├── Mode3Test/
|
||||
│ │ ├── backend/
|
||||
│ │ │ ├── auth-routes.ts OAuth endpoints
|
||||
│ │ │ ├── server.ts Hono server
|
||||
│ │ │ └── ... (other modules)
|
||||
│ │ ├── package.json
|
||||
│ │ ├── .env Configuration
|
||||
│ │ └── Dockerfile
|
||||
│ │
|
||||
├── ☸️ Deployment
|
||||
│ ├── docker-compose.yml Dev stack
|
||||
│ └── k8s/
|
||||
│ ├── backend-deployment.yaml K8s backend
|
||||
│ └── frontend-deployment.yaml K8s frontend
|
||||
│
|
||||
└── 📋 Logging
|
||||
└── logs/
|
||||
└── SVELTE_SESSION_LOG.txt Development log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start (10 minutes)
|
||||
|
||||
### 1. Start Backend
|
||||
```bash
|
||||
cd Mode3Test
|
||||
bun install
|
||||
bun run dev # http://localhost:3005
|
||||
```
|
||||
|
||||
### 2. Start Frontend (new terminal)
|
||||
```bash
|
||||
cd frontend
|
||||
bun install
|
||||
bun run dev # http://localhost:5173
|
||||
```
|
||||
|
||||
### 3. Test OAuth
|
||||
1. Navigate to http://localhost:5173
|
||||
2. Click "Sign In with Microsoft"
|
||||
3. Complete OAuth flow
|
||||
4. View job listings
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing Guide (Phase 8)
|
||||
|
||||
**Documentation**: [TESTING_GUIDE.md](TESTING_GUIDE.md)
|
||||
|
||||
**Test Categories** (23 test scenarios):
|
||||
1. OAuth2 flow (6 tests)
|
||||
2. Token management (4 tests)
|
||||
3. API error handling (3 tests)
|
||||
4. Data storage (2 tests)
|
||||
5. UI components (2 tests)
|
||||
6. Performance (2 tests)
|
||||
7. Security (2 tests)
|
||||
8. Accessibility (2 tests)
|
||||
|
||||
**Time Estimate**: 2-3 hours
|
||||
|
||||
---
|
||||
|
||||
## 📦 Deployment Guide (Phase 9)
|
||||
|
||||
**Documentation**: [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md)
|
||||
|
||||
**Deployment Options**:
|
||||
1. **Docker**: Single machine deployment
|
||||
2. **Kubernetes**: Scalable cloud deployment
|
||||
3. **CI/CD**: Automated GitHub Actions pipeline
|
||||
|
||||
**Time Estimate**: 2-3 hours
|
||||
|
||||
---
|
||||
|
||||
## 📊 Project Statistics
|
||||
|
||||
### Code Size
|
||||
- **Total**: ~7,000+ lines
|
||||
- **Frontend**: ~5,000 lines
|
||||
- **Backend**: ~500 lines
|
||||
- **Config**: ~500 lines
|
||||
- **Documentation**: ~2,300 lines
|
||||
|
||||
### Performance
|
||||
- **Build Size**: 42.70 kB JS (gzip: 14.77 kB)
|
||||
- **CSS Size**: 17.71 kB (gzip: 4.08 kB)
|
||||
- **Modules**: 52 total
|
||||
- **Build Time**: 2.70 seconds
|
||||
|
||||
### Quality
|
||||
- **TypeScript**: 100% strict mode
|
||||
- **Test Coverage**: 23 scenarios
|
||||
- **Documentation**: 100% complete
|
||||
- **Error Handling**: Comprehensive
|
||||
- **Logging**: Structured
|
||||
|
||||
---
|
||||
|
||||
## ✨ Key Features
|
||||
|
||||
### Authentication ✅
|
||||
- OAuth2 PKCE flow
|
||||
- Automatic token refresh
|
||||
- Session security
|
||||
- Sign out with cleanup
|
||||
- User profile display
|
||||
|
||||
### Job Management ✅
|
||||
- Browse job listings
|
||||
- Search with debouncing
|
||||
- Pagination (12/page)
|
||||
- Job details view
|
||||
- Client-side caching
|
||||
|
||||
### Error Handling ✅
|
||||
- Network error detection
|
||||
- API error classification
|
||||
- Automatic retry logic
|
||||
- User-friendly messages
|
||||
- Error recovery suggestions
|
||||
|
||||
### Offline Support ✅
|
||||
- Service Worker
|
||||
- Background token refresh
|
||||
- Offline browsing
|
||||
- Graceful messaging
|
||||
- Auto sync on reconnect
|
||||
|
||||
### Developer Experience ✅
|
||||
- Full TypeScript support
|
||||
- Structured logging
|
||||
- Error boundary
|
||||
- Log export
|
||||
- Development details
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Security Features
|
||||
|
||||
✅ OAuth2 PKCE (prevents code interception)
|
||||
✅ No localStorage tokens (XSS protection)
|
||||
✅ SessionStorage tokens (lost on close)
|
||||
✅ HTTPS ready
|
||||
✅ Service Worker isolation
|
||||
✅ Content Security Policy headers
|
||||
✅ Structured error logging (no sensitive data)
|
||||
✅ Token refresh isolation
|
||||
|
||||
---
|
||||
|
||||
## 📈 Development Timeline
|
||||
|
||||
| Phase | Task | Status | Time |
|
||||
|-------|------|--------|------|
|
||||
| 1 | Project Setup | ✅ | 30 min |
|
||||
| 2 | Core Services | ✅ | 45 min |
|
||||
| 3 | UI Components | ✅ | 60 min |
|
||||
| 4 | OAuth Integration | ✅ | 45 min |
|
||||
| 5 | Service Worker | ✅ | 60 min |
|
||||
| 6 | Backend OAuth | ✅ | 45 min |
|
||||
| 7 | Error Handling | ✅ | 60 min |
|
||||
| 8 | Testing | 🔄 | 120 min |
|
||||
| 9 | Deployment | 🔄 | 120 min |
|
||||
| **Total** | **Complete** | **60%** | **~12 hours** |
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Development
|
||||
|
||||
### Commands
|
||||
|
||||
**Frontend**:
|
||||
```bash
|
||||
bun run dev # Start dev server (hot reload)
|
||||
bun run build # Production build
|
||||
bun run preview # Preview build locally
|
||||
```
|
||||
|
||||
**Backend**:
|
||||
```bash
|
||||
bun run dev # Start server
|
||||
bun run start # Production start
|
||||
```
|
||||
|
||||
### Debugging
|
||||
|
||||
**Logger Access** (in browser console):
|
||||
```javascript
|
||||
window.logger.exportLogs() // Download logs.json
|
||||
window.logger.clearLogs() // Clear log buffer
|
||||
```
|
||||
|
||||
**Service Worker** (in browser console):
|
||||
```javascript
|
||||
navigator.serviceWorker.getRegistrations() // Check SW
|
||||
navigator.serviceWorker.controller.postMessage({ // Trigger refresh
|
||||
type: 'REFRESH_TOKEN'
|
||||
})
|
||||
```
|
||||
|
||||
**IndexedDB** (in DevTools):
|
||||
- Application → IndexedDB → job-info-db
|
||||
- View: jobs, fileCache, userProfile stores
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### Frontend (.env.local)
|
||||
```bash
|
||||
VITE_OAUTH_CLIENT_ID=your-id
|
||||
VITE_OAUTH_TENANT_ID=your-tenant
|
||||
VITE_OAUTH_REDIRECT_URI=http://localhost:5173/#/callback
|
||||
VITE_API_URL=http://localhost:5173/api
|
||||
VITE_GRAPH_API_BASE=https://graph.microsoft.com/v1.0
|
||||
```
|
||||
|
||||
### Backend (.env)
|
||||
```bash
|
||||
MICROSOFT_TENANT_ID=your-tenant
|
||||
MICROSOFT_CLIENT_ID=your-id
|
||||
MICROSOFT_CLIENT_SECRET=your-secret
|
||||
MICROSOFT_REDIRECT_URI=http://localhost:3005/api/auth/callback
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
### Troubleshooting
|
||||
1. Check [PROJECT_COMPLETE.md](PROJECT_COMPLETE.md) troubleshooting section
|
||||
2. Review browser console for errors
|
||||
3. Export logs: `window.logger.exportLogs()`
|
||||
4. Check [logs/SVELTE_SESSION_LOG.txt](logs/SVELTE_SESSION_LOG.txt)
|
||||
|
||||
### Common Issues
|
||||
- **Token expired**: Service Worker refreshes automatically
|
||||
- **Offline mode**: Works if jobs cached in IndexedDB
|
||||
- **CORS errors**: Check vite.config.ts proxy
|
||||
- **Search not working**: Check debounce timeout
|
||||
|
||||
---
|
||||
|
||||
## 📚 Learning Resources
|
||||
|
||||
### Official Docs
|
||||
- [Svelte Documentation](https://svelte.dev/docs)
|
||||
- [Vite Documentation](https://vitejs.dev/)
|
||||
- [TypeScript Handbook](https://www.typescriptlang.org/docs/)
|
||||
- [TailwindCSS Docs](https://tailwindcss.com/docs)
|
||||
- [Microsoft Graph API](https://learn.microsoft.com/en-us/graph/)
|
||||
|
||||
### Project-Specific
|
||||
- [OAUTH2_REFERENCE_GUIDE.md](OAUTH2_REFERENCE_GUIDE.md)
|
||||
- [SVELTE_PATTERNS_REFERENCE.md](SVELTE_PATTERNS_REFERENCE.md)
|
||||
- [SVELTE_DEVELOPMENT_SYSTEM.md](SVELTE_DEVELOPMENT_SYSTEM.md)
|
||||
|
||||
---
|
||||
|
||||
## 📋 Next Steps
|
||||
|
||||
### For Testing (Phase 8)
|
||||
1. Read [TESTING_GUIDE.md](TESTING_GUIDE.md)
|
||||
2. Set up test environment
|
||||
3. Execute 23 test scenarios
|
||||
4. Document results
|
||||
5. Fix any issues found
|
||||
|
||||
### For Deployment (Phase 9)
|
||||
1. Read [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md)
|
||||
2. Prepare production environment
|
||||
3. Configure secrets
|
||||
4. Deploy to staging
|
||||
5. Run smoke tests
|
||||
6. Deploy to production
|
||||
|
||||
---
|
||||
|
||||
## 📝 Document Types
|
||||
|
||||
### Type 1: Application Docs
|
||||
- [PROJECT_COMPLETE.md](PROJECT_COMPLETE.md) - Complete application guide
|
||||
- [README.md](README.md) - Original project readme
|
||||
|
||||
### Type 2: Phase Docs
|
||||
- [PHASE_7_SUMMARY.md](PHASE_7_SUMMARY.md) - Phase 7 completion details
|
||||
- [PHASE_7_COMPLETION_CHECKLIST.md](PHASE_7_COMPLETION_CHECKLIST.md) - Verification checklist
|
||||
|
||||
### Type 3: Operational Docs
|
||||
- [TESTING_GUIDE.md](TESTING_GUIDE.md) - Testing procedures
|
||||
- [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md) - Deployment procedures
|
||||
|
||||
### Type 4: Reference Docs
|
||||
- [OAUTH2_REFERENCE_GUIDE.md](OAUTH2_REFERENCE_GUIDE.md) - OAuth2 reference
|
||||
- [SVELTE_PATTERNS_REFERENCE.md](SVELTE_PATTERNS_REFERENCE.md) - Code patterns
|
||||
- [SVELTE_DEVELOPMENT_SYSTEM.md](SVELTE_DEVELOPMENT_SYSTEM.md) - Development system
|
||||
- [SVELTE_BUILD_PLAN.md](SVELTE_BUILD_PLAN.md) - Build details
|
||||
- [PREPARATION_COMPLETE.md](PREPARATION_COMPLETE.md) - Preparation summary
|
||||
|
||||
### Type 5: Development
|
||||
- [logs/SVELTE_SESSION_LOG.txt](logs/SVELTE_SESSION_LOG.txt) - Development log
|
||||
|
||||
---
|
||||
|
||||
## ✅ Verification Status
|
||||
|
||||
| Component | Status | Details |
|
||||
|-----------|--------|---------|
|
||||
| **Frontend Build** | ✅ PASS | 52 modules, no errors |
|
||||
| **TypeScript** | ✅ PASS | Strict mode all pass |
|
||||
| **Services** | ✅ PASS | Logger, API wrapper, error handler |
|
||||
| **Components** | ✅ PASS | ErrorBoundary + 6 UI components |
|
||||
| **Documentation** | ✅ PASS | 10 documents, 5,000+ lines |
|
||||
| **Code Quality** | ✅ PASS | Type safe, well documented |
|
||||
| **Production Ready** | ✅ PASS | Secure, performant, reliable |
|
||||
| **Phase 8 Ready** | ✅ PASS | All test procedures documented |
|
||||
| **Phase 9 Ready** | ✅ PASS | All deployment procedures documented |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Summary
|
||||
|
||||
### Current Status
|
||||
✅ **Phase 7**: Error Handling & Logging - COMPLETE
|
||||
🔄 **Phase 8**: Testing - READY (See [TESTING_GUIDE.md](TESTING_GUIDE.md))
|
||||
🔄 **Phase 9**: Deployment - READY (See [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md))
|
||||
|
||||
### Overall Progress
|
||||
- **Completed**: Phases 1-7 (60%)
|
||||
- **Remaining**: Phases 8-9 (40%)
|
||||
- **Estimated Time**: 4 hours to completion
|
||||
|
||||
### Quality Level
|
||||
🏆 **PRODUCTION READY**
|
||||
- Secure (OAuth2 PKCE)
|
||||
- Performant (42.7 kB gzip)
|
||||
- Well-tested (23 test scenarios)
|
||||
- Well-documented (10 guides)
|
||||
- Type-safe (100% TypeScript strict)
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2026-01-19
|
||||
**Status**: Phase 7 Complete ✅
|
||||
**Ready for**: Phase 8 Testing
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Quick Links
|
||||
|
||||
| Document | Purpose | Status |
|
||||
|----------|---------|--------|
|
||||
| [PROJECT_COMPLETE.md](PROJECT_COMPLETE.md) | Main documentation | ✅ |
|
||||
| [TESTING_GUIDE.md](TESTING_GUIDE.md) | Phase 8 procedures | ✅ |
|
||||
| [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md) | Phase 9 procedures | ✅ |
|
||||
| [PHASE_7_SUMMARY.md](PHASE_7_SUMMARY.md) | Phase 7 details | ✅ |
|
||||
| [OAUTH2_REFERENCE_GUIDE.md](OAUTH2_REFERENCE_GUIDE.md) | OAuth reference | ✅ |
|
||||
| [SVELTE_PATTERNS_REFERENCE.md](SVELTE_PATTERNS_REFERENCE.md) | Code patterns | ✅ |
|
||||
|
||||
---
|
||||
|
||||
**Status**: Ready for Phase 8 Testing ✅
|
||||
@@ -1,73 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
|
||||
|
||||
Copyright 2025 aewing
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,75 +0,0 @@
|
||||
# Universal Auth Module
|
||||
|
||||
## Overview
|
||||
This module provides a standardized, immutable authentication and token management system for any web project using PocketBase and Microsoft Graph. It supports user and agent tokens, delegated and app-only flows, and a consistent popup UI for login prompts.
|
||||
|
||||
## Features
|
||||
- Pattern-based: Choose 'pb-user', 'pb-agent', 'graph-user', 'graph-agent', or any combination (e.g., 'pb-user+graph-agent') for your project needs
|
||||
- Supports both user and agent tokens for PocketBase and Microsoft Graph
|
||||
- Special handling for Valkey/Redis agent login and token storage (for backend/automation flows)
|
||||
- Single source of truth for token storage, retrieval, and refresh
|
||||
- Consistent, hidden popup for all user login prompts
|
||||
- Extracts user info (display name, email) from PocketBase
|
||||
- Works in any frontend project with PocketBase
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Include in your project
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script>
|
||||
<script src="auth/auth-universal.js"></script>
|
||||
```
|
||||
|
||||
### 2. Initialize the pattern
|
||||
```js
|
||||
// Choose your pattern:
|
||||
// 'pb-user' - PocketBase user login
|
||||
// 'pb-agent' - PocketBase agent/service login
|
||||
// 'graph-user' - Microsoft Graph delegated (user) login
|
||||
// 'graph-agent' - Microsoft Graph agent/app-only (client credentials)
|
||||
// Combine as needed: 'pb-user+graph-agent', 'pb-agent+graph-user', etc.
|
||||
Auth.use('pb-user+graph-agent');
|
||||
```
|
||||
|
||||
### 3. Get and use tokens
|
||||
```js
|
||||
const pbToken = await Auth.ensureToken('pb');
|
||||
const graphToken = await Auth.ensureToken('graph');
|
||||
const userInfo = Auth.getUserInfo();
|
||||
```
|
||||
|
||||
### 4. Handle reauthentication
|
||||
- If a token is missing or expired, Auth will automatically show a popup for login.
|
||||
- The popup is hidden at all other times.
|
||||
|
||||
### 5. Clear tokens (for testing or logout)
|
||||
```js
|
||||
Auth.clearToken('pb');
|
||||
Auth.clearToken('graph');
|
||||
```
|
||||
|
||||
## Secrets and Environment
|
||||
- For frontend, all secrets are handled by PocketBase OAuth (no client secrets exposed).
|
||||
- For backend/agent flows, use environment variables and server-side code to store secrets securely.
|
||||
- The module does not expose or require secrets in the frontend.
|
||||
|
||||
## Example Test Page
|
||||
See `auth-test.html` for a safe way to test all flows without affecting your main app.
|
||||
|
||||
## Rules for Copilot Instructions
|
||||
- Always use Auth.use() to set the pattern before requesting tokens. Supported types: 'pb-user', 'pb-agent', 'graph-user', 'graph-agent', or any combination.
|
||||
- Always use Auth.ensureToken(type) to get a valid token; it will handle refresh and prompt if needed.
|
||||
- Never store secrets in frontend code; use PocketBase OAuth for user login.
|
||||
- All login prompts must use the provided popup, never custom dialogs.
|
||||
- Token keys are immutable: 'pbUserToken', 'pbAgentToken', 'graphUserToken', 'graphAgentToken'.
|
||||
- User info extraction must use Auth.getUserInfo().
|
||||
- For agent/app-only tokens, use backend code, environment variables, and optionally Valkey/Redis for secure storage and retrieval.
|
||||
|
||||
## FAQ
|
||||
- **Can I use this in any project?** Yes, as long as you include PocketBase and this module.
|
||||
- **Does it work for both user and agent tokens?** Yes, with the correct pattern and backend support for agent tokens (including Valkey/Redis for automation).
|
||||
- **Is the popup customizable?** The style can be changed, but the flow must remain consistent for all projects.
|
||||
- **How are secrets handled?** Only via backend environment variables for agent tokens; never exposed in frontend.
|
||||
|
||||
---
|
||||
For further integration, see the comments in `auth-universal.js` and the example test page.
|
||||
@@ -1,85 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Universal Auth Test</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script>
|
||||
<script src="auth-universal.js"></script>
|
||||
<style>
|
||||
body { font-family: sans-serif; background: #f3f4f6; margin: 0; padding: 2em; }
|
||||
.test-block { background: #fff; border-radius: 1em; box-shadow: 0 2px 12px #0001; padding: 2em; margin-bottom: 2em; }
|
||||
.pattern-btn { margin: 0.2em 0.5em 0.2em 0; padding: 0.5em 1.2em; border-radius: 0.5em; border: none; background: #2563eb; color: #fff; cursor: pointer; font-size: 1em; }
|
||||
.pattern-btn.active { background: #059669; }
|
||||
#output { font-family: monospace; background: #f9fafb; border-radius: 0.5em; padding: 1em; margin-top: 1em; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="test-block">
|
||||
<h2>Choose Auth Pattern</h2>
|
||||
<div id="patterns"></div>
|
||||
<div style="margin-top:1em;">
|
||||
<button onclick="clearAll()" class="pattern-btn" style="background:#b91c1c;">Clear All Tokens</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="test-block">
|
||||
<h2>Test Actions</h2>
|
||||
<button onclick="testToken('pb-user')" class="pattern-btn">Test PB User</button>
|
||||
<button onclick="testToken('pb-agent')" class="pattern-btn">Test PB Agent</button>
|
||||
<button onclick="testToken('graph-user')" class="pattern-btn">Test Graph User</button>
|
||||
<button onclick="testToken('graph-agent')" class="pattern-btn">Test Graph Agent</button>
|
||||
<button onclick="showUserInfo()" class="pattern-btn" style="background:#6d28d9;">Show User Info</button>
|
||||
<div id="output"></div>
|
||||
</div>
|
||||
<script>
|
||||
const patterns = [
|
||||
'pb-user',
|
||||
'pb-agent',
|
||||
'graph-user',
|
||||
'graph-agent',
|
||||
'pb-user+graph-user',
|
||||
'pb-user+graph-agent',
|
||||
'pb-agent+graph-user',
|
||||
'pb-agent+graph-agent',
|
||||
'pb-user+pb-agent+graph-user+graph-agent'
|
||||
];
|
||||
let currentPattern = patterns[0];
|
||||
function setPattern(p) {
|
||||
currentPattern = p;
|
||||
Auth.use(p);
|
||||
document.querySelectorAll('.pattern-btn').forEach(btn => btn.classList.remove('active'));
|
||||
document.querySelectorAll('.pattern-btn[data-pattern="' + p + '"]').forEach(btn => btn.classList.add('active'));
|
||||
document.getElementById('output').textContent = `Pattern set: ${p}`;
|
||||
}
|
||||
function renderPatterns() {
|
||||
const el = document.getElementById('patterns');
|
||||
el.innerHTML = '';
|
||||
patterns.forEach(p => {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = p;
|
||||
btn.className = 'pattern-btn' + (p === currentPattern ? ' active' : '');
|
||||
btn.setAttribute('data-pattern', p);
|
||||
btn.onclick = () => setPattern(p);
|
||||
el.appendChild(btn);
|
||||
});
|
||||
}
|
||||
renderPatterns();
|
||||
setPattern(currentPattern);
|
||||
async function testToken(type) {
|
||||
try {
|
||||
const token = await Auth.ensureToken(type);
|
||||
document.getElementById('output').textContent = `${type} token: ${token ? token.substring(0, 40) + '...' : 'NONE'}`;
|
||||
} catch (err) {
|
||||
document.getElementById('output').textContent = `Error: ${err.message}`;
|
||||
}
|
||||
}
|
||||
function clearAll() {
|
||||
['pb-user','pb-agent','graph-user','graph-agent'].forEach(Auth.clearToken);
|
||||
document.getElementById('output').textContent = 'All tokens cleared.';
|
||||
}
|
||||
function showUserInfo() {
|
||||
const info = Auth.getUserInfo();
|
||||
document.getElementById('output').textContent = 'User Info: ' + JSON.stringify(info, null, 2);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,220 +0,0 @@
|
||||
// Universal Auth Module: Immutable, Pattern-Based, Project-Agnostic
|
||||
// Usage: import and call Auth.use('pb-graph') or Auth.use('pb') or Auth.use('graph')
|
||||
// Provides: getToken(type), ensureToken(type), refreshToken(type), getUserInfo(), promptReauth()
|
||||
// Token types: 'pb' (PocketBase user/agent), 'graph' (Graph delegated), 'graphAgent' (Graph app-only)
|
||||
// All storage, retrieval, refresh, and UI are standardized and immutable
|
||||
|
||||
const TOKEN_KEYS = {
|
||||
'pb-user': 'pbUserToken',
|
||||
'pb-agent': 'pbAgentToken',
|
||||
'graph-user': 'graphUserToken',
|
||||
'graph-agent': 'graphAgentToken'
|
||||
};
|
||||
|
||||
function parsePattern(pattern) {
|
||||
return pattern.split('+').map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
window.Auth = (() => {
|
||||
let pattern = 'pb-user'; // default
|
||||
let enabledTypes = ['pb-user'];
|
||||
let pb = null;
|
||||
let popup = null;
|
||||
let agentCreds = { email: '', password: '' };
|
||||
let superuserCreds = { email: '', password: '' };
|
||||
|
||||
// --- Setup ---
|
||||
function use(type) {
|
||||
pattern = type;
|
||||
enabledTypes = parsePattern(type);
|
||||
if (enabledTypes.some(t => t.startsWith('pb'))) {
|
||||
pb = window.PocketBase ? new PocketBase('https://pocketbase.ccllc.pro') : null;
|
||||
}
|
||||
// Only one-time setup
|
||||
if (!document.getElementById('authPopup')) {
|
||||
createPopup();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Token Storage ---
|
||||
function getToken(type) {
|
||||
return localStorage.getItem(TOKEN_KEYS[type]) || '';
|
||||
}
|
||||
function setToken(type, value) {
|
||||
if (value) localStorage.setItem(TOKEN_KEYS[type], value);
|
||||
}
|
||||
function clearToken(type) {
|
||||
localStorage.removeItem(TOKEN_KEYS[type]);
|
||||
}
|
||||
|
||||
// --- Info Extraction ---
|
||||
function getUserInfo() {
|
||||
if (pb && pb.authStore.model) {
|
||||
return {
|
||||
displayName: pb.authStore.model.display_name || pb.authStore.model.name || pb.authStore.model.email || pb.authStore.model.username || 'Unknown',
|
||||
email: pb.authStore.model.email || pb.authStore.model.username || null
|
||||
};
|
||||
}
|
||||
return { displayName: '', email: '' };
|
||||
}
|
||||
|
||||
// --- Token Ensure/Refresh ---
|
||||
async function ensureToken(type) {
|
||||
let token = getToken(type);
|
||||
if (token) return token;
|
||||
if (type === 'pb-user' && pb) {
|
||||
if (pb.authStore.isValid) {
|
||||
setToken('pb-user', pb.authStore.token);
|
||||
return pb.authStore.token;
|
||||
}
|
||||
// Choose login method: email/password or OAuth
|
||||
if (pattern.includes('pb-user-oauth')) {
|
||||
return await promptReauth('pb-user-oauth');
|
||||
} else {
|
||||
return await promptReauth('pb-user');
|
||||
}
|
||||
}
|
||||
if (type === 'pb-superuser' && pb) {
|
||||
// Prompt for superuser credentials
|
||||
await promptReauth('pb-superuser');
|
||||
// Use superuser credentials to login
|
||||
const authData = await pb.collection('_superuser').authWithPassword(superuserCreds.email, superuserCreds.password);
|
||||
setToken('pb-superuser', pb.authStore.token);
|
||||
return pb.authStore.token;
|
||||
}
|
||||
if (type === 'pb-agent' && pb) {
|
||||
// Prompt for agent credentials if not set
|
||||
if (!agentCreds.email || !agentCreds.password) {
|
||||
await promptReauth('pb-agent');
|
||||
}
|
||||
// Use agent credentials to login
|
||||
const authData = await pb.collection('users').authWithPassword(agentCreds.email, agentCreds.password);
|
||||
setToken('pb-agent', pb.authStore.token);
|
||||
return pb.authStore.token;
|
||||
}
|
||||
if (type === 'graph-user' && pb) {
|
||||
// OAuth only
|
||||
return await promptReauth('graph-user');
|
||||
}
|
||||
if (type === 'graph-agent') {
|
||||
// Prompt for agent credentials if not set
|
||||
if (!agentCreds.email || !agentCreds.password) {
|
||||
await promptReauth('graph-agent');
|
||||
}
|
||||
// Use agent credentials to get token (simulate, real flow is backend)
|
||||
setToken('graph-agent', 'AGENT_TOKEN_' + agentCreds.email);
|
||||
return getToken('graph-agent');
|
||||
}
|
||||
throw new Error('Unknown token type');
|
||||
}
|
||||
|
||||
async function refreshToken(type) {
|
||||
if (type === 'graph-user' && pb) {
|
||||
try {
|
||||
const authData = await pb.collection('users').authRefresh();
|
||||
const meta = authData?.meta || pb?.authStore?.model?.meta || {};
|
||||
const token = meta.graphAccessToken || meta.graph_token || meta.graphToken || meta.accessToken || meta.access_token || meta.token || meta.rawToken || meta?.authData?.access_token || '';
|
||||
setToken('graph-user', token);
|
||||
return token || '';
|
||||
} catch (err) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
// Add refresh logic for other types as needed
|
||||
return '';
|
||||
}
|
||||
|
||||
// --- Reauth Popup ---
|
||||
function createPopup() {
|
||||
popup = document.createElement('div');
|
||||
popup.id = 'authPopup';
|
||||
popup.style = 'display:none;position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.4);z-index:9999;align-items:center;justify-content:center;';
|
||||
popup.innerHTML = `
|
||||
<div style="background:#fff;padding:2em 2.5em;border-radius:1em;box-shadow:0 2px 24px #0002;text-align:center;max-width:350px;margin:auto;">
|
||||
<h2 style="font-size:1.3em;margin-bottom:1em;">Sign In Required</h2>
|
||||
<div id="authPopupForms"></div>
|
||||
<div id="authPopupError" style="color:#b91c1c;margin-top:1em;display:none;"></div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(popup);
|
||||
}
|
||||
|
||||
function showForm(type) {
|
||||
const forms = {
|
||||
'pb-user': `<input id="pbUserEmail" type="email" placeholder="Email" style="width:90%;margin-bottom:0.5em;"><br><input id="pbUserPassword" type="password" placeholder="Password" style="width:90%;margin-bottom:0.5em;"><br><button id="authPopupBtn" style="background:#2563eb;color:#fff;padding:0.7em 2em;border:none;border-radius:0.5em;font-size:1em;cursor:pointer;">Sign in</button>`,
|
||||
'pb-user-oauth': `<button id="authPopupBtn" style="background:#2563eb;color:#fff;padding:0.7em 2em;border:none;border-radius:0.5em;font-size:1em;cursor:pointer;">Sign in with Microsoft</button>`,
|
||||
'pb-superuser': `<input id="pbSuperuserEmail" type="email" placeholder="Superuser Email" style="width:90%;margin-bottom:0.5em;"><br><input id="pbSuperuserPassword" type="password" placeholder="Superuser Password" style="width:90%;margin-bottom:0.5em;"><br><button id="authPopupBtn" style="background:#d97706;color:#fff;padding:0.7em 2em;border:none;border-radius:0.5em;font-size:1em;cursor:pointer;">Sign in as Superuser</button>`,
|
||||
'pb-agent': `<input id="pbAgentEmail" type="email" placeholder="Agent Email" style="width:90%;margin-bottom:0.5em;"><br><input id="pbAgentPassword" type="password" placeholder="Agent Password" style="width:90%;margin-bottom:0.5em;"><br><button id="authPopupBtn" style="background:#059669;color:#fff;padding:0.7em 2em;border:none;border-radius:0.5em;font-size:1em;cursor:pointer;">Sign in as Agent</button>`,
|
||||
'graph-user': `<button id="authPopupBtn" style="background:#2563eb;color:#fff;padding:0.7em 2em;border:none;border-radius:0.5em;font-size:1em;cursor:pointer;">Sign in with Microsoft</button>`,
|
||||
'graph-agent': `<input id="graphAgentEmail" type="email" placeholder="Agent Email" style="width:90%;margin-bottom:0.5em;"><br><input id="graphAgentPassword" type="password" placeholder="Agent Password" style="width:90%;margin-bottom:0.5em;"><br><button id="authPopupBtn" style="background:#059669;color:#fff;padding:0.7em 2em;border:none;border-radius:0.5em;font-size:1em;cursor:pointer;">Sign in as Graph Agent</button>`
|
||||
};
|
||||
document.getElementById('authPopupForms').innerHTML = forms[type] || '';
|
||||
}
|
||||
|
||||
async function promptReauth(type) {
|
||||
showForm(type);
|
||||
popup.style.display = 'flex';
|
||||
return new Promise((resolve, reject) => {
|
||||
document.getElementById('authPopupBtn').onclick = async () => {
|
||||
try {
|
||||
if (type === 'pb-user') {
|
||||
const email = document.getElementById('pbUserEmail').value;
|
||||
const password = document.getElementById('pbUserPassword').value;
|
||||
const authData = await pb.collection('users').authWithPassword(email, password);
|
||||
setToken('pb-user', pb.authStore.token);
|
||||
popup.style.display = 'none';
|
||||
resolve(pb.authStore.token);
|
||||
} else if (type === 'pb-user-oauth') {
|
||||
const authData = await pb.collection('users').authWithOAuth2({ provider: 'microsoft' });
|
||||
setToken('pb-user', authData?.meta?.graphAccessToken || '');
|
||||
popup.style.display = 'none';
|
||||
resolve(authData?.meta?.graphAccessToken || '');
|
||||
} else if (type === 'pb-superuser') {
|
||||
superuserCreds.email = document.getElementById('pbSuperuserEmail').value;
|
||||
superuserCreds.password = document.getElementById('pbSuperuserPassword').value;
|
||||
popup.style.display = 'none';
|
||||
resolve();
|
||||
} else if (type === 'pb-agent') {
|
||||
agentCreds.email = document.getElementById('pbAgentEmail').value;
|
||||
agentCreds.password = document.getElementById('pbAgentPassword').value;
|
||||
popup.style.display = 'none';
|
||||
resolve();
|
||||
} else if (type === 'graph-user') {
|
||||
const authData = await pb.collection('users').authWithOAuth2({ provider: 'microsoft' });
|
||||
setToken('graph-user', authData?.meta?.graphAccessToken || '');
|
||||
popup.style.display = 'none';
|
||||
resolve(authData?.meta?.graphAccessToken || '');
|
||||
} else if (type === 'graph-agent') {
|
||||
agentCreds.email = document.getElementById('graphAgentEmail').value;
|
||||
agentCreds.password = document.getElementById('graphAgentPassword').value;
|
||||
popup.style.display = 'none';
|
||||
resolve();
|
||||
}
|
||||
} catch (err) {
|
||||
document.getElementById('authPopupError').textContent = err.message || 'Authentication failed.';
|
||||
document.getElementById('authPopupError').style.display = '';
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// --- API ---
|
||||
return {
|
||||
use,
|
||||
getToken,
|
||||
setToken,
|
||||
clearToken,
|
||||
getUserInfo,
|
||||
ensureToken,
|
||||
refreshToken,
|
||||
promptReauth
|
||||
};
|
||||
})();
|
||||
|
||||
window.Auth = Auth;
|
||||
|
||||
// Example usage:
|
||||
// Auth.use('pb-graph');
|
||||
// const token = await Auth.ensureToken('graph');
|
||||
// const user = Auth.getUserInfo();
|
||||
// All login prompts are handled via the popup, which is hidden unless needed.
|
||||
@@ -1,37 +0,0 @@
|
||||
// Express API for Redis/Valkey: keys list and single key fetch (for fast UI)
|
||||
const express = require('express');
|
||||
const Redis = require('ioredis');
|
||||
const app = express();
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST || '127.0.0.1',
|
||||
port: Number(process.env.REDIS_PORT || 6379),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
});
|
||||
|
||||
// List all keys (no values)
|
||||
app.get('/api/cache/keys', async (req, res) => {
|
||||
try {
|
||||
const keys = await redis.keys('*');
|
||||
// Optionally, add summary/metadata here
|
||||
res.json(keys.map(key => ({ key })));
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch value for a single key
|
||||
app.get('/api/cache/:key', async (req, res) => {
|
||||
try {
|
||||
const key = req.params.key;
|
||||
let value = await redis.get(key);
|
||||
try { value = JSON.parse(value); } catch {}
|
||||
res.json({ key, value });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
const port = process.env.CACHE_API_PORT || 3006;
|
||||
app.listen(port, () => {
|
||||
console.log(`Cache API (keys+single) running on http://localhost:${port}/api/cache/keys`);
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
// Simple Express API to serve all Redis/Valkey cache keys and values as JSON (CommonJS)
|
||||
const express = require('express');
|
||||
const Redis = require('ioredis');
|
||||
const app = express();
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST || '127.0.0.1',
|
||||
port: Number(process.env.REDIS_PORT || 6379),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
});
|
||||
|
||||
app.get('/api/cache', async (req, res) => {
|
||||
try {
|
||||
const keys = await redis.keys('*');
|
||||
const result = [];
|
||||
for (const key of keys) {
|
||||
let value = await redis.get(key);
|
||||
try { value = JSON.parse(value); } catch {}
|
||||
result.push({ key, value });
|
||||
}
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
const port = process.env.CACHE_API_PORT || 3006;
|
||||
app.listen(port, () => {
|
||||
console.log(`Cache API running on http://localhost:${port}/api/cache`);
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
// Simple Express API to serve all Redis/Valkey cache keys and values as JSON
|
||||
const express = require('express');
|
||||
const Redis = require('ioredis');
|
||||
const app = express();
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST || '127.0.0.1',
|
||||
port: Number(process.env.REDIS_PORT || 6379),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
});
|
||||
|
||||
app.get('/api/cache', async (req, res) => {
|
||||
try {
|
||||
const keys = await redis.keys('*');
|
||||
const result = [];
|
||||
for (const key of keys) {
|
||||
let value = await redis.get(key);
|
||||
try { value = JSON.parse(value); } catch {}
|
||||
result.push({ key, value });
|
||||
}
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
const port = process.env.CACHE_API_PORT || 3030;
|
||||
app.listen(port, () => {
|
||||
console.log(`Cache API running on http://localhost:${port}/api/cache`);
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
// Script to list all Redis/Valkey keys and their values for visualization (CommonJS)
|
||||
const Redis = require('ioredis');
|
||||
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST || '127.0.0.1',
|
||||
port: Number(process.env.REDIS_PORT || 6379),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
});
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const keys = await redis.keys('*');
|
||||
const result = [];
|
||||
for (const key of keys) {
|
||||
let value = await redis.get(key);
|
||||
try { value = JSON.parse(value); } catch {}
|
||||
result.push({ key, value });
|
||||
}
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error('Error listing Redis keys:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
@@ -1,25 +0,0 @@
|
||||
// Script to list all Redis/Valkey keys and their values for visualization
|
||||
const Redis = require('ioredis');
|
||||
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST || '127.0.0.1',
|
||||
port: Number(process.env.REDIS_PORT || 6379),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
});
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const keys = await redis.keys('*');
|
||||
const result = [];
|
||||
for (const key of keys) {
|
||||
let value = await redis.get(key);
|
||||
try { value = JSON.parse(value); } catch {}
|
||||
result.push({ key, value });
|
||||
}
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error('Error listing Redis keys:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
@@ -1,101 +0,0 @@
|
||||
import Redis from 'ioredis';
|
||||
|
||||
// Create Redis client
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: Number(process.env.REDIS_PORT || 6379),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
retryStrategy(times) {
|
||||
const delay = Math.min(times * 50, 2000);
|
||||
return delay;
|
||||
},
|
||||
maxRetriesPerRequest: 3,
|
||||
lazyConnect: true,
|
||||
});
|
||||
|
||||
// Handle connection events
|
||||
redis.on('connect', () => {
|
||||
console.log('✓ Redis connected');
|
||||
});
|
||||
|
||||
redis.on('error', (err) => {
|
||||
console.error('Redis error:', err.message);
|
||||
});
|
||||
|
||||
redis.on('close', () => {
|
||||
console.log('Redis connection closed');
|
||||
});
|
||||
|
||||
// Connect to Redis
|
||||
(async () => {
|
||||
try {
|
||||
await redis.connect();
|
||||
} catch (err) {
|
||||
console.error('Failed to connect to Redis:', err);
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* Get a value from cache
|
||||
*/
|
||||
export async function getCache(key: string): Promise<any | null> {
|
||||
try {
|
||||
const value = await redis.get(key);
|
||||
if (!value) return null;
|
||||
return JSON.parse(value);
|
||||
} catch (err) {
|
||||
console.error(`Cache get error for key "${key}":`, err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a value in cache with TTL in seconds
|
||||
*/
|
||||
export async function setCache(key: string, value: any, ttl: number = 300): Promise<boolean> {
|
||||
try {
|
||||
const serialized = JSON.stringify(value);
|
||||
await redis.setex(key, ttl, serialized);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(`Cache set error for key "${key}":`, err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a key from cache
|
||||
*/
|
||||
export async function deleteCache(key: string): Promise<boolean> {
|
||||
try {
|
||||
await redis.del(key);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(`Cache delete error for key "${key}":`, err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cache keys matching a pattern
|
||||
*/
|
||||
export async function clearCachePattern(pattern: string): Promise<number> {
|
||||
try {
|
||||
const keys = await redis.keys(pattern);
|
||||
if (keys.length === 0) return 0;
|
||||
await redis.del(...keys);
|
||||
return keys.length;
|
||||
} catch (err) {
|
||||
console.error(`Cache clear error for pattern "${pattern}":`, err);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Redis is connected
|
||||
*/
|
||||
export function isRedisConnected(): boolean {
|
||||
return redis.status === 'ready';
|
||||
}
|
||||
|
||||
export default redis;
|
||||
@@ -1,513 +0,0 @@
|
||||
import { Hono } from 'hono';
|
||||
import { serveStatic } from 'hono/bun';
|
||||
import fs from 'fs';
|
||||
// @ts-ignore Bun project may not have dotenv types configured
|
||||
import { config } from 'dotenv';
|
||||
import { getCache, setCache, isRedisConnected } from './redis-cache';
|
||||
|
||||
config();
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
// Minimal log helpers
|
||||
const logLine = (path: string, line: string) => {
|
||||
try {
|
||||
fs.appendFileSync(path, `${new Date().toISOString()} ${line}\n`);
|
||||
} catch (err) {
|
||||
console.error('Failed to write log:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Version endpoint sourced from package.json
|
||||
app.get('/version', (c) => {
|
||||
try {
|
||||
const pkgRaw = fs.readFileSync('./package.json', 'utf-8');
|
||||
const pkg = JSON.parse(pkgRaw);
|
||||
return c.json({ version: pkg.version || '0.0.0' });
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `version endpoint error: ${(err as Error)?.message || String(err)}`);
|
||||
return c.json({ version: '0.0.0' }, 200);
|
||||
}
|
||||
});
|
||||
|
||||
// Jobs endpoint with Redis caching
|
||||
app.get('/api/jobs', async (c) => {
|
||||
const page = parseInt(c.req.query('page') || '1');
|
||||
const perPage = parseInt(c.req.query('perPage') || '50');
|
||||
const sort = c.req.query('sort') || '-Job_Number';
|
||||
|
||||
const cacheKey = `jobs:page:${page}:perPage:${perPage}:sort:${sort}`;
|
||||
const ttl = 300; // 5 minutes cache
|
||||
|
||||
try {
|
||||
// Try to get from cache first
|
||||
if (isRedisConnected()) {
|
||||
const cached = await getCache(cacheKey);
|
||||
if (cached) {
|
||||
logLine('logs/server.log', `Cache HIT for ${cacheKey}`);
|
||||
return c.json(cached);
|
||||
}
|
||||
logLine('logs/server.log', `Cache MISS for ${cacheKey}`);
|
||||
}
|
||||
|
||||
// Fetch from PocketBase
|
||||
const pbUrl = `https://pocketbase.ccllc.pro/api/collections/pbc_1407612689/records?page=${page}&perPage=${perPage}&sort=${sort}`;
|
||||
const authHeader = c.req.header('Authorization') || '';
|
||||
|
||||
const response = await fetch(pbUrl, {
|
||||
headers: authHeader ? { Authorization: authHeader } : {}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`PocketBase returned ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Cache the result
|
||||
if (isRedisConnected()) {
|
||||
await setCache(cacheKey, data, ttl);
|
||||
logLine('logs/server.log', `Cached ${cacheKey} for ${ttl}s`);
|
||||
}
|
||||
|
||||
return c.json(data);
|
||||
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `jobs endpoint error: ${(err as Error)?.message || String(err)}`);
|
||||
return c.json({ error: 'Failed to fetch jobs' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Fast endpoint: returns all jobs in a single cached request
|
||||
app.get('/api/jobs-all', async (c) => {
|
||||
const cacheKey = 'jobs:all';
|
||||
const ttl = 1800; // 30 minutes cache
|
||||
try {
|
||||
// Try cache first for instant response
|
||||
if (isRedisConnected()) {
|
||||
const cached = await getCache(cacheKey);
|
||||
if (cached) {
|
||||
logLine('logs/server.log', `Cache HIT for ${cacheKey} (${cached.items?.length || 0} jobs)`);
|
||||
// Return cached data immediately
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
await refreshJobsCache(cacheKey, ttl, c.req.header('Authorization') || '');
|
||||
} catch (err) {}
|
||||
});
|
||||
// Return all cached jobs in a single response
|
||||
return c.json({ ...cached, partial: false });
|
||||
} else {
|
||||
// No cache: return empty array instantly, trigger background fetch
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
const result = await fetchAllJobsFromPocketBase(c.req.header('Authorization') || '');
|
||||
if (isRedisConnected()) {
|
||||
await setCache(cacheKey, result, ttl);
|
||||
logLine('logs/server.log', `Cached ${cacheKey} with ${result.items.length} jobs for ${ttl}s`);
|
||||
}
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `jobs-all background fetch error: ${(err as Error)?.message || String(err)}`);
|
||||
}
|
||||
});
|
||||
return c.json({ items: [], partial: true });
|
||||
}
|
||||
}
|
||||
// Redis not connected: fallback to direct fetch (blocking)
|
||||
const result = await fetchAllJobsFromPocketBase(c.req.header('Authorization') || '');
|
||||
return c.json(result);
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `jobs-all endpoint error: ${(err as Error)?.message || String(err)}`);
|
||||
return c.json({ error: 'Failed to fetch jobs' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Helper function to fetch all jobs from PocketBase
|
||||
async function fetchAllJobsFromPocketBase(authHeader: string): Promise<any> {
|
||||
const allItems: any[] = [];
|
||||
let page = 1;
|
||||
const perPage = 500;
|
||||
|
||||
while (true) {
|
||||
const pbUrl = `https://pocketbase.ccllc.pro/api/collections/pbc_1407612689/records?page=${page}&perPage=${perPage}&sort=-Job_Number`;
|
||||
const response = await fetch(pbUrl, {
|
||||
headers: authHeader ? { Authorization: authHeader } : {}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`PocketBase returned ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const items = data.items || [];
|
||||
|
||||
if (items.length === 0) break;
|
||||
allItems.push(...items);
|
||||
|
||||
if (allItems.length >= (data.totalItems || 0)) break;
|
||||
page++;
|
||||
}
|
||||
|
||||
return {
|
||||
page: 1,
|
||||
perPage: allItems.length,
|
||||
totalItems: allItems.length,
|
||||
totalPages: 1,
|
||||
items: allItems
|
||||
};
|
||||
}
|
||||
|
||||
// Helper function to refresh jobs cache in background
|
||||
async function refreshJobsCache(cacheKey: string, ttl: number, authHeader: string): Promise<void> {
|
||||
try {
|
||||
const result = await fetchAllJobsFromPocketBase(authHeader);
|
||||
await setCache(cacheKey, result, ttl);
|
||||
logLine('logs/server.log', `Background refresh: cached ${cacheKey} with ${result.items.length} jobs`);
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `Background refresh error: ${(err as Error)?.message || String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Microsoft Graph helpers ---
|
||||
const getGraphToken = (c?: any) => {
|
||||
const headerToken = c?.req?.header ? c.req.header('x-graph-token') : '';
|
||||
const envToken = process.env.GRAPH_TOKEN || process.env.MS_GRAPH_TOKEN || '';
|
||||
const token = headerToken || envToken;
|
||||
console.log('[getGraphToken] Header token:', headerToken ? headerToken.substring(0, 20) + '...' : 'NONE');
|
||||
console.log('[getGraphToken] Env token:', envToken ? 'SET' : 'NOT SET');
|
||||
console.log('[getGraphToken] Using:', token ? token.substring(0, 20) + '...' : 'NONE');
|
||||
return token;
|
||||
};
|
||||
|
||||
const b64Url = (input: string) =>
|
||||
Buffer.from(input).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||
|
||||
type DriveItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
webUrl?: string;
|
||||
size?: number;
|
||||
lastModifiedDateTime?: string;
|
||||
file?: { mimeType?: string };
|
||||
folder?: { childCount?: number };
|
||||
parentReference?: { path?: string; driveId?: string };
|
||||
};
|
||||
|
||||
const fetchJson = async (url: string, token: string) => {
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
const err: any = new Error(`Graph ${res.status}: ${text}`);
|
||||
err.status = res.status;
|
||||
throw err;
|
||||
}
|
||||
return res.json();
|
||||
};
|
||||
|
||||
const resolveShareLink = async (link: string, token: string) => {
|
||||
// Decode the link in case it comes pre-encoded from the database
|
||||
const decodedLink = decodeURIComponent(link);
|
||||
console.log('[resolveShareLink] Decoded link:', decodedLink);
|
||||
|
||||
// Check if this is a short sharing link (/:f:/ or /:b:/) or a direct document library URL
|
||||
if (decodedLink.includes('/:f:/') || decodedLink.includes('/:b:/')) {
|
||||
// Short sharing link - use shares API
|
||||
const encoded = `u!${b64Url(decodedLink)}`;
|
||||
console.log('[resolveShareLink] Using shares API, encoded:', encoded);
|
||||
const data = await fetchJson(`https://graph.microsoft.com/v1.0/shares/${encoded}/driveItem`, token);
|
||||
return { driveId: data.parentReference?.driveId as string, itemId: data.id as string };
|
||||
} else {
|
||||
// Direct document library URL - parse it and use drive API
|
||||
// Example: https://czflex.sharepoint.com/sites/Team/Shared Documents/Forms/AllItems.aspx?RootFolder=/sites/Team/Shared Documents/General/...
|
||||
const url = new URL(decodedLink);
|
||||
const pathMatch = url.hostname.match(/^([^.]+)\.sharepoint\.com$/);
|
||||
if (!pathMatch) throw new Error('Invalid SharePoint URL');
|
||||
|
||||
const hostname = pathMatch[1]; // e.g., 'czflex'
|
||||
const sitePath = url.pathname.split('/Shared')[0]; // e.g., /sites/Team
|
||||
const rootFolderParam = url.searchParams.get('RootFolder');
|
||||
if (!rootFolderParam) throw new Error('RootFolder parameter missing');
|
||||
|
||||
// Extract the folder path relative to the document library
|
||||
// RootFolder format: /sites/Team/Shared Documents/General/Operations [Server]/...
|
||||
// We need: General/Operations [Server]/...
|
||||
const folderPath = rootFolderParam.split('/Shared Documents/')[1];
|
||||
if (!folderPath) throw new Error('Could not parse folder path');
|
||||
|
||||
console.log('[resolveShareLink] Hostname:', hostname);
|
||||
console.log('[resolveShareLink] Site path:', sitePath);
|
||||
console.log('[resolveShareLink] Folder path:', folderPath);
|
||||
|
||||
// Get the site ID first using hostname:sitePath format
|
||||
const siteData = await fetchJson(`https://graph.microsoft.com/v1.0/sites/${hostname}.sharepoint.com:${sitePath}`, token);
|
||||
const siteId = siteData.id;
|
||||
|
||||
// Get the default document library drive
|
||||
const driveData = await fetchJson(`https://graph.microsoft.com/v1.0/sites/${siteId}/drive`, token);
|
||||
const driveId = driveData.id;
|
||||
|
||||
// Get the folder item by path
|
||||
const itemData = await fetchJson(
|
||||
`https://graph.microsoft.com/v1.0/drives/${driveId}/root:/${folderPath}`,
|
||||
token
|
||||
);
|
||||
|
||||
console.log('[resolveShareLink] Resolved to driveId:', driveId, 'itemId:', itemData.id);
|
||||
return { driveId, itemId: itemData.id };
|
||||
}
|
||||
};
|
||||
|
||||
const listChildrenRecursive = async (
|
||||
driveId: string,
|
||||
itemId: string,
|
||||
depth = 0,
|
||||
maxDepth = 5,
|
||||
token: string,
|
||||
): Promise<DriveItem[]> => {
|
||||
const out: DriveItem[] = [];
|
||||
const data = await fetchJson(`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/children`, token);
|
||||
for (const child of data.value || []) {
|
||||
out.push(child as DriveItem);
|
||||
if (child.folder && depth < maxDepth) {
|
||||
const nested = await listChildrenRecursive(driveId, child.id, depth + 1, maxDepth, token);
|
||||
out.push(...nested);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const searchWithinFolder = async (driveId: string, itemId: string, query: string, token: string): Promise<DriveItem[]> => {
|
||||
const data = await fetchJson(
|
||||
`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/search(q='${encodeURIComponent(query)}')`,
|
||||
token,
|
||||
);
|
||||
return (data.value || []) as DriveItem[];
|
||||
};
|
||||
|
||||
const filterByCategory = (items: DriveItem[], category?: string, pdfOnly: boolean = true) => {
|
||||
// First filter: show PDF files and Word documents (which will be converted to PDF)
|
||||
let filtered = items;
|
||||
if (pdfOnly) {
|
||||
filtered = items.filter((i) => {
|
||||
if (i.folder) return false;
|
||||
const name = i.name.toLowerCase();
|
||||
const mimeType = i.file?.mimeType?.toLowerCase() || '';
|
||||
// Include PDFs, Word documents, and images
|
||||
return name.endsWith('.pdf') || mimeType.includes('pdf') ||
|
||||
name.endsWith('.doc') || name.endsWith('.docx') || mimeType.includes('word') ||
|
||||
name.endsWith('.jpg') || name.endsWith('.jpeg') || name.endsWith('.png') ||
|
||||
name.endsWith('.gif') || name.endsWith('.bmp') || name.endsWith('.tiff') ||
|
||||
name.endsWith('.webp') || mimeType.includes('image');
|
||||
});
|
||||
}
|
||||
|
||||
// Second filter: category filtering (if specified)
|
||||
if (!category) return filtered;
|
||||
const nameHas = (name: string, words: string[]) => words.some((w) => name.includes(w));
|
||||
const lc = (s: string) => (s || '').toLowerCase();
|
||||
const contractWords = ['contract', 'estimate', 'proposal', 'bid', 'award', 'agreement', 'sow'];
|
||||
const planWords = ['plan', 'drawing', 'dwg', 'pdf', 'sheet', 'layout'];
|
||||
return filtered.filter((i) => {
|
||||
const n = lc(i.name);
|
||||
if (category === 'contracts') return nameHas(n, contractWords);
|
||||
if (category === 'plans') return nameHas(n, planWords);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
// List/search job files via Graph using a shared folder link
|
||||
app.get('/api/job-files', async (c) => {
|
||||
try {
|
||||
console.log('[job-files] Request received');
|
||||
const token = getGraphToken(c);
|
||||
if (!token) {
|
||||
console.log('[job-files] No token found');
|
||||
return c.json({ error: 'GRAPH_TOKEN missing' }, 500);
|
||||
}
|
||||
console.log('[job-files] Token found:', token.substring(0, 20) + '...');
|
||||
const link = c.req.query('link');
|
||||
const q = c.req.query('q') || '';
|
||||
const category = c.req.query('category') || '';
|
||||
if (!link) return c.json({ error: 'link required' }, 400);
|
||||
console.log('[job-files] Link:', link);
|
||||
|
||||
const { driveId, itemId } = await resolveShareLink(link, token);
|
||||
|
||||
let items: DriveItem[] = [];
|
||||
if (q) {
|
||||
items = await searchWithinFolder(driveId, itemId, q, token);
|
||||
} else {
|
||||
// Walk subfolders up to depth 5
|
||||
items = await listChildrenRecursive(driveId, itemId, 0, 5, token);
|
||||
}
|
||||
|
||||
// Filter to show only PDFs by default (can be disabled with pdfOnly=false query param)
|
||||
const pdfOnly = c.req.query('pdfOnly') !== 'false';
|
||||
const filtered = filterByCategory(items, category, pdfOnly);
|
||||
console.log(`[job-files] Found ${items.length} total items, ${filtered.length} after filtering`);
|
||||
|
||||
const mapped = filtered.map((i) => ({
|
||||
id: i.id,
|
||||
name: i.name,
|
||||
url: i.webUrl,
|
||||
driveId: i.parentReference?.driveId || driveId,
|
||||
size: i.size,
|
||||
modified: i.lastModifiedDateTime,
|
||||
contentType: i.file?.mimeType,
|
||||
isFolder: Boolean(i.folder),
|
||||
path: i.parentReference?.path || '',
|
||||
}));
|
||||
|
||||
return c.json({ items: mapped, total: mapped.length, source: q ? 'search' : 'walk', driveId });
|
||||
} catch (err) {
|
||||
const message = (err as Error)?.message || String(err);
|
||||
const status = (err as any)?.status || 500;
|
||||
console.error('[job-files] ERROR:', message);
|
||||
logLine('logs/error.log', `/api/job-files error: ${message}`);
|
||||
return c.json({ error: message }, status);
|
||||
}
|
||||
});
|
||||
|
||||
// Stream file content via Graph to avoid CSP issues for inline preview
|
||||
app.get('/api/job-file-content', async (c) => {
|
||||
try {
|
||||
const token = getGraphToken(c);
|
||||
if (!token) return c.json({ error: 'GRAPH_TOKEN missing' }, 500);
|
||||
const driveId = c.req.query('driveId');
|
||||
const itemId = c.req.query('itemId');
|
||||
if (!driveId || !itemId) return c.json({ error: 'driveId and itemId required' }, 400);
|
||||
|
||||
const res = await fetch(`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/content`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
logLine('logs/error.log', `/api/job-file-content failed: ${res.status} - ${text}`);
|
||||
return c.json({ error: `Graph ${res.status}: ${text}` }, 502);
|
||||
}
|
||||
const headers: Record<string, string> = {};
|
||||
const ct = res.headers.get('content-type');
|
||||
const cd = res.headers.get('content-disposition');
|
||||
if (ct) headers['Content-Type'] = ct;
|
||||
if (cd) headers['Content-Disposition'] = cd;
|
||||
return new Response(res.body, { status: 200, headers });
|
||||
} catch (err) {
|
||||
const message = (err as Error)?.message || String(err);
|
||||
const status = (err as any)?.status || 500;
|
||||
logLine('logs/error.log', `/api/job-file-content error: ${message}`);
|
||||
return c.json({ error: message }, status);
|
||||
}
|
||||
});
|
||||
|
||||
// Convert Word documents to PDF for display
|
||||
app.get('/api/job-file-pdf', async (c) => {
|
||||
try {
|
||||
const token = getGraphToken(c);
|
||||
if (!token) return c.json({ error: 'GRAPH_TOKEN missing' }, 500);
|
||||
const driveId = c.req.query('driveId');
|
||||
const itemId = c.req.query('itemId');
|
||||
if (!driveId || !itemId) return c.json({ error: 'driveId and itemId required' }, 400);
|
||||
|
||||
// Use Graph API to convert to PDF format
|
||||
const res = await fetch(`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/content?format=pdf`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
logLine('logs/error.log', `/api/job-file-pdf conversion failed: ${res.status} - ${text}`);
|
||||
return c.json({ error: `PDF conversion failed: ${res.status}` }, 502);
|
||||
}
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/pdf',
|
||||
};
|
||||
const cd = res.headers.get('content-disposition');
|
||||
if (cd) {
|
||||
// Replace .docx/.doc extension with .pdf in filename
|
||||
headers['Content-Disposition'] = cd.replace(/\.(docx?)/gi, '.pdf');
|
||||
}
|
||||
logLine('logs/server.log', `Successfully converted document to PDF: driveId=${driveId}, itemId=${itemId}`);
|
||||
return new Response(res.body, { status: 200, headers });
|
||||
} catch (err) {
|
||||
const message = (err as Error)?.message || String(err);
|
||||
const status = (err as any)?.status || 500;
|
||||
logLine('logs/error.log', `/api/job-file-pdf error: ${message}`);
|
||||
return c.json({ error: message }, status);
|
||||
}
|
||||
});
|
||||
|
||||
// Mutation logging endpoint
|
||||
app.post('/log-change', async (c) => {
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const { action = 'unknown', user = 'unknown', target = '', detail = '' } = body || {};
|
||||
const payload = JSON.stringify({ action, user, target, detail });
|
||||
logLine('logs/changes.log', payload);
|
||||
return c.json({ status: 'ok' });
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `log-change error: ${(err as Error)?.message || String(err)}`);
|
||||
return c.text('Bad Request', 400);
|
||||
}
|
||||
});
|
||||
|
||||
// Serve static files from the frontend directory (must come after API routes)
|
||||
// Add cache headers for static assets
|
||||
app.use('/assets/*', async (c, next) => {
|
||||
await next();
|
||||
// Cache assets for 1 year
|
||||
c.header('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
});
|
||||
app.use('/*', serveStatic({ root: './frontend' }));
|
||||
|
||||
// Error handler
|
||||
app.onError((err, c) => {
|
||||
logLine('logs/error.log', `Unhandled error: ${err?.message || err}`);
|
||||
return c.text('Internal Server Error', 500);
|
||||
});
|
||||
|
||||
// Default to 3005 for test instance; can be overridden by Environment=PORT
|
||||
const PORT = Number(process.env.PORT || 3005);
|
||||
|
||||
logLine('logs/server.log', `Starting frontend server (Redis version) on port ${PORT}`);
|
||||
|
||||
// Warm up cache on startup
|
||||
(async () => {
|
||||
try {
|
||||
if (isRedisConnected()) {
|
||||
logLine('logs/server.log', 'Warming up jobs cache...');
|
||||
const result = await fetchAllJobsFromPocketBase('');
|
||||
await setCache('jobs:all', result, 1800);
|
||||
logLine('logs/server.log', `✓ Cache warmed with ${result.items.length} jobs`);
|
||||
}
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `Cache warmup failed: ${(err as Error)?.message || String(err)}`);
|
||||
}
|
||||
})();
|
||||
|
||||
// Background refresh every 5 minutes to keep cache hot
|
||||
setInterval(async () => {
|
||||
try {
|
||||
if (isRedisConnected()) {
|
||||
await refreshJobsCache('jobs:all', 1800, '');
|
||||
}
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `Periodic refresh error: ${(err as Error)?.message || String(err)}`);
|
||||
}
|
||||
}, 5 * 60 * 1000); // Every 5 minutes
|
||||
|
||||
// Graceful shutdown logging
|
||||
const shutdown = (signal: string) => {
|
||||
logLine('logs/server.log', `Shutting down due to ${signal}`);
|
||||
process.exit(0);
|
||||
};
|
||||
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||
|
||||
export default {
|
||||
port: PORT,
|
||||
fetch: app.fetch,
|
||||
};
|
||||
@@ -1,600 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "job-info",
|
||||
"dependencies": {
|
||||
"dotenv": "^17.2.3",
|
||||
"hono": "^4.10.8",
|
||||
"ioredis": "^5.8.2",
|
||||
"pocketbase": "^0.26.5",
|
||||
"tailwind": "^4.0.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@babel/runtime": ["@babel/runtime@7.3.4", "", { "dependencies": { "regenerator-runtime": "^0.12.0" } }, "sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g=="],
|
||||
|
||||
"@ioredis/commands": ["@ioredis/commands@1.4.0", "", {}, "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
|
||||
|
||||
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.4", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A=="],
|
||||
|
||||
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
|
||||
|
||||
"ajv": ["ajv@6.10.0", "", { "dependencies": { "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg=="],
|
||||
|
||||
"amqplib": ["amqplib@0.5.2", "", { "dependencies": { "bitsyntax": "~0.0.4", "bluebird": "^3.4.6", "buffer-more-ints": "0.0.2", "readable-stream": "1.x >=1.1.9", "safe-buffer": "^5.0.1" } }, "sha512-l9mCs6LbydtHqRniRwYkKdqxVa6XMz3Vw1fh+2gJaaVgTM6Jk3o8RccAKWKtlhT1US5sWrFh+KKxsVUALURSIA=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
||||
|
||||
"app-root-path": ["app-root-path@2.1.0", "", {}, "sha512-z5BqVjscbjmJBybKlICogJR2jCr2q/Ixu7Pvui5D4y97i7FLsJlvEG9XOR/KJRlkxxZz7UaaS2TMwQh1dRJ2dA=="],
|
||||
|
||||
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
|
||||
|
||||
"array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="],
|
||||
|
||||
"array.prototype.reduce": ["array.prototype.reduce@1.0.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-array-method-boxes-properly": "^1.0.0", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "is-string": "^1.1.1" } }, "sha512-DwuEqgXFBwbmZSRqt3BpQigWNUoqw9Ml2dTWdF3B2zQlQX4OeUE0zyuzX0fX0IbTvjdkZbcBTU3idgpO78qkTw=="],
|
||||
|
||||
"arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
|
||||
|
||||
"asn1": ["asn1@0.2.3", "", {}, "sha512-6i37w/+EhlWlGUJff3T/Q8u1RGmP5wgbiwYnOnbOqvtrPxT63/sYFyP9RcpxtxGymtfA075IvmOnL7ycNOWl3w=="],
|
||||
|
||||
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
||||
|
||||
"async-limiter": ["async-limiter@1.0.1", "", {}, "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="],
|
||||
|
||||
"async-retry": ["async-retry@1.2.3", "", { "dependencies": { "retry": "0.12.0" } }, "sha512-tfDb02Th6CE6pJUF2gjW5ZVjsgwlucVXOEQMvEX9JgSJMs9gAX+Nz3xRuJBKuUYjTSYORqvDBORdAQ3LU59g7Q=="],
|
||||
|
||||
"autoprefixer": ["autoprefixer@10.4.23", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001760", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA=="],
|
||||
|
||||
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
||||
|
||||
"babel-runtime": ["babel-runtime@6.26.0", "", { "dependencies": { "core-js": "^2.4.0", "regenerator-runtime": "^0.11.0" } }, "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.8", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Y1fOuNDowLfgKOypdc9SPABfoWXuZHBOyCS4cD52IeZBhr4Md6CLLs6atcxVrzRmQ06E7hSlm5bHHApPKR/byA=="],
|
||||
|
||||
"basic-auth": ["basic-auth@2.0.1", "", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="],
|
||||
|
||||
"bitsyntax": ["bitsyntax@0.0.4", "", { "dependencies": { "buffer-more-ints": "0.0.2" } }, "sha512-Pav3HSZXD2NLQOWfJldY3bpJLt8+HS2nUo5Z1bLLmHg2vCE/cM1qfEvNjlYo7GgYQPneNr715Bh42i01ZHZPvw=="],
|
||||
|
||||
"bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
|
||||
|
||||
"body-parser": ["body-parser@1.18.3", "", { "dependencies": { "bytes": "3.0.0", "content-type": "~1.0.4", "debug": "2.6.9", "depd": "~1.1.2", "http-errors": "~1.6.3", "iconv-lite": "0.4.23", "on-finished": "~2.3.0", "qs": "6.5.2", "raw-body": "2.3.3", "type-is": "~1.6.16" } }, "sha512-YQyoqQG3sO8iCmf8+hyVpgHHOv0/hCEFiS4zTGUwTA1HjAFX66wRcNQrVCeJq9pgESMRvUAOvSil5MJlmccuKQ=="],
|
||||
|
||||
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
||||
|
||||
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||
|
||||
"buffer-more-ints": ["buffer-more-ints@0.0.2", "", {}, "sha512-PDgX2QJgUc5+Jb2xAoBFP5MxhtVUmZHR33ak+m/SDxRdCrbnX1BggRIaxiW7ImwfmO4iJeCQKN18ToSXWGjYkA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="],
|
||||
|
||||
"bytes": ["bytes@3.0.0", "", {}, "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw=="],
|
||||
|
||||
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||
|
||||
"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.30001760", "", {}, "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw=="],
|
||||
|
||||
"chalk": ["chalk@2.4.1", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ=="],
|
||||
|
||||
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"commands-events": ["commands-events@1.0.4", "", { "dependencies": { "@babel/runtime": "7.2.0", "formats": "1.0.0", "uuidv4": "2.0.0" } }, "sha512-HdP/+1Anoc7z+6L2h7nd4Imz54+LW+BjMGt30riBZrZ3ZeP/8el93wD8Jj8ltAaqVslqNgjX6qlhSBJwuDSmpg=="],
|
||||
|
||||
"comparejs": ["comparejs@1.0.0", "", {}, "sha512-Ue/Zd9aOucHzHXwaCe4yeHR7jypp7TKrIBZ5yls35nPNiVXlW14npmNVKM1ZaLlQTKZ6/4ewA//gYKHHIwCpOw=="],
|
||||
|
||||
"compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="],
|
||||
|
||||
"compression": ["compression@1.7.3", "", { "dependencies": { "accepts": "~1.3.5", "bytes": "3.0.0", "compressible": "~2.0.14", "debug": "2.6.9", "on-headers": "~1.0.1", "safe-buffer": "5.1.2", "vary": "~1.1.2" } }, "sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg=="],
|
||||
|
||||
"content-disposition": ["content-disposition@0.5.2", "", {}, "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA=="],
|
||||
|
||||
"content-type": ["content-type@1.0.4", "", {}, "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="],
|
||||
|
||||
"cookie": ["cookie@0.3.1", "", {}, "sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw=="],
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="],
|
||||
|
||||
"core-js": ["core-js@2.6.12", "", {}, "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ=="],
|
||||
|
||||
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
|
||||
|
||||
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
|
||||
|
||||
"crypto2": ["crypto2@2.0.0", "", { "dependencies": { "babel-runtime": "6.26.0", "node-rsa": "0.4.2", "util.promisify": "1.0.0" } }, "sha512-jdXdAgdILldLOF53md25FiQ6ybj2kUFTiRjs7msKTUoZrzgT/M1FPX5dYGJjbbwFls+RJIiZxNTC02DE/8y0ZQ=="],
|
||||
|
||||
"csstype": ["csstype@3.2.0", "", {}, "sha512-si++xzRAY9iPp60roQiFta7OFbhrgvcthrhlNAGeQptSY25uJjkfUV8OArC3KLocB8JT8ohz+qgxWCmz8RhjIg=="],
|
||||
|
||||
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
||||
|
||||
"data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="],
|
||||
|
||||
"data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
|
||||
|
||||
"datasette": ["datasette@1.0.1", "", { "dependencies": { "comparejs": "1.0.0", "eventemitter2": "5.0.1", "lodash": "4.17.5" } }, "sha512-aJdlCBToEJUP4M57r67r4V6tltwGKa3qetnjpBtXYIlqbX9tM9jsoDMxb4xd9AGjpp3282oHRmqI5Z8TVAU0Mg=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
|
||||
|
||||
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
||||
|
||||
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
|
||||
|
||||
"depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="],
|
||||
|
||||
"destroy": ["destroy@1.0.4", "", {}, "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg=="],
|
||||
|
||||
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
|
||||
|
||||
"draht": ["draht@1.0.1", "", { "dependencies": { "eventemitter2": "5.0.1" } }, "sha512-yNNHL864dniNmIE9ZKD++mKypiAUAvVZtyV0QrbXH/ak3ebzFqo5xsmRBRqV8pZVhImOSBiyq500Wcmrf44zAg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="],
|
||||
|
||||
"encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
|
||||
|
||||
"es-abstract": ["es-abstract@1.24.0", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg=="],
|
||||
|
||||
"es-array-method-boxes-properly": ["es-array-method-boxes-properly@1.0.0", "", {}, "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
|
||||
|
||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
||||
"eventemitter2": ["eventemitter2@5.0.1", "", {}, "sha512-5EM1GHXycJBS6mauYAbVKT1cVs7POKWb2NXD4Vyt8dDqeZa7LaDK1/sjtL+Zb0lzTpSNil4596Dyu97hz37QLg=="],
|
||||
|
||||
"express": ["express@4.16.4", "", { "dependencies": { "accepts": "~1.3.5", "array-flatten": "1.1.1", "body-parser": "1.18.3", "content-disposition": "0.5.2", "content-type": "~1.0.4", "cookie": "0.3.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "~1.1.2", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.1.1", "fresh": "0.5.2", "merge-descriptors": "1.0.1", "methods": "~1.1.2", "on-finished": "~2.3.0", "parseurl": "~1.3.2", "path-to-regexp": "0.1.7", "proxy-addr": "~2.0.4", "qs": "6.5.2", "range-parser": "~1.2.0", "safe-buffer": "5.1.2", "send": "0.16.2", "serve-static": "1.13.2", "setprototypeof": "1.1.0", "statuses": "~1.4.0", "type-is": "~1.6.16", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@2.0.1", "", {}, "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="],
|
||||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||
|
||||
"finalhandler": ["finalhandler@1.1.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "on-finished": "~2.3.0", "parseurl": "~1.3.2", "statuses": "~1.4.0", "unpipe": "~1.0.0" } }, "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg=="],
|
||||
|
||||
"find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="],
|
||||
|
||||
"flaschenpost": ["flaschenpost@1.1.3", "", { "dependencies": { "@babel/runtime": "7.2.0", "app-root-path": "2.1.0", "babel-runtime": "6.26.0", "chalk": "2.4.1", "find-root": "1.1.0", "lodash": "4.17.11", "moment": "2.22.2", "processenv": "1.1.0", "split2": "3.0.0", "stack-trace": "0.0.10", "stringify-object": "3.3.0", "untildify": "3.0.3", "util.promisify": "1.0.0", "varname": "2.0.3" }, "bin": { "flaschenpost-uncork": "dist/bin/flaschenpost-uncork.js", "flaschenpost-normalize": "dist/bin/flaschenpost-normalize.js" } }, "sha512-1VAYPvDsVBGFJyUrOa/6clnJwZYC3qVq9nJLcypy6lvaaNbo1wOQiH8HQ+4Fw/k51pVG7JHzSf5epb8lmIW86g=="],
|
||||
|
||||
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
||||
|
||||
"formats": ["formats@1.0.0", "", {}, "sha512-For0Y8egwEK96JgJo4NONErPhtl7H2QzeB2NYGmzeGeJ8a1JZqPgLYOtM3oJRCYhmgsdDFd6KGRYyfe37XY4Yg=="],
|
||||
|
||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
|
||||
"fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
|
||||
|
||||
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="],
|
||||
|
||||
"functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
|
||||
|
||||
"generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
|
||||
|
||||
"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-own-enumerable-property-symbols": ["get-own-enumerable-property-symbols@3.0.2", "", {}, "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
|
||||
|
||||
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
|
||||
|
||||
"has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
|
||||
|
||||
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
|
||||
|
||||
"has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"hase": ["hase@2.0.0", "", { "dependencies": { "@babel/runtime": "7.1.2", "amqplib": "0.5.2" } }, "sha512-L83pBR/oZvQQNjv4kw9aUpTqBxERPiY7B42jsmkt1VDeUaRVhYkEIKzkCqrppjtxHe2EZqzZJzuhMXsWsxYIsw=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hono": ["hono@4.10.8", "", {}, "sha512-DDT0A0r6wzhe8zCGoYOmMeuGu3dyTAE40HHjwUsWFTEy5WxK1x2WDSsBPlEXgPbRIFY6miDualuUDbasPogIww=="],
|
||||
|
||||
"http-errors": ["http-errors@1.6.3", "", { "dependencies": { "depd": "~1.1.2", "inherits": "2.0.3", "setprototypeof": "1.1.0", "statuses": ">= 1.4.0 < 2" } }, "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.4.23", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.3", "", {}, "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="],
|
||||
|
||||
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
||||
|
||||
"ioredis": ["ioredis@5.8.2", "", { "dependencies": { "@ioredis/commands": "1.4.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
|
||||
|
||||
"is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="],
|
||||
|
||||
"is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="],
|
||||
|
||||
"is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="],
|
||||
|
||||
"is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
|
||||
|
||||
"is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="],
|
||||
|
||||
"is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="],
|
||||
|
||||
"is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="],
|
||||
|
||||
"is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="],
|
||||
|
||||
"is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
|
||||
|
||||
"is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="],
|
||||
|
||||
"is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
|
||||
|
||||
"is-obj": ["is-obj@1.0.1", "", {}, "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg=="],
|
||||
|
||||
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
|
||||
|
||||
"is-regexp": ["is-regexp@1.0.0", "", {}, "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA=="],
|
||||
|
||||
"is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
|
||||
|
||||
"is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="],
|
||||
|
||||
"is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="],
|
||||
|
||||
"is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="],
|
||||
|
||||
"is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
|
||||
|
||||
"is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="],
|
||||
|
||||
"is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="],
|
||||
|
||||
"is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="],
|
||||
|
||||
"isarray": ["isarray@0.0.1", "", {}, "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="],
|
||||
|
||||
"json-lines": ["json-lines@1.0.0", "", { "dependencies": { "timer2": "1.0.0" } }, "sha512-ytuLZb4RBQb3bTRsG/QBenyIo5oHLpjeCVph3s2NnoAsZE9K6h+uR+OWpEOWV1UeHdX63tYctGppBpGAc+JNMA=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"jsonwebtoken": ["jsonwebtoken@8.5.0", "", { "dependencies": { "jws": "^3.2.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^5.6.0" } }, "sha512-IqEycp0znWHNA11TpYi77bVgyBO/pGESDh7Ajhas+u0ttkGkKYIIAjniL4Bw5+oVejVF+SYkaI7XKfwCCyeTuA=="],
|
||||
|
||||
"jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="],
|
||||
|
||||
"jws": ["jws@3.2.3", "", { "dependencies": { "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } }, "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g=="],
|
||||
|
||||
"limes": ["limes@2.0.0", "", { "dependencies": { "@babel/runtime": "7.3.4", "jsonwebtoken": "8.5.0" } }, "sha512-evWD0pnTgPX7QueaSoJl5JBUL30T1ZVzo34ke97tIKmeagqhBTYK/JkKL0vtG3MpNApw8ZY9TlbybfwEz9knBA=="],
|
||||
|
||||
"lodash": ["lodash@4.17.11", "", {}, "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="],
|
||||
|
||||
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
|
||||
|
||||
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
|
||||
|
||||
"lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="],
|
||||
|
||||
"lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
|
||||
|
||||
"lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="],
|
||||
|
||||
"lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="],
|
||||
|
||||
"lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
|
||||
|
||||
"lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="],
|
||||
|
||||
"lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="],
|
||||
|
||||
"lusca": ["lusca@1.6.1", "", { "dependencies": { "tsscmp": "^1.0.5" } }, "sha512-+JzvUMH/rsE/4XfHdDOl70bip0beRcHSviYATQM0vtls59uVtdn1JMu4iD7ZShBpAmFG8EnaA+PrYG9sECMIOQ=="],
|
||||
|
||||
"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.1", "", {}, "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="],
|
||||
|
||||
"methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="],
|
||||
|
||||
"mime": ["mime@1.4.1", "", { "bin": { "mime": "cli.js" } }, "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"moment": ["moment@2.22.2", "", {}, "sha512-LRvkBHaJGnrcWvqsElsOhHCzj8mU39wLx5pQ0pc6s153GynCTsPdGdqsVNKAQD9sKnWj11iF7TZx9fpLwdD3fw=="],
|
||||
|
||||
"morgan": ["morgan@1.9.1", "", { "dependencies": { "basic-auth": "~2.0.0", "debug": "2.6.9", "depd": "~1.1.2", "on-finished": "~2.3.0", "on-headers": "~1.0.1" } }, "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||
|
||||
"nocache": ["nocache@2.0.0", "", {}, "sha512-YdKcy2x0dDwOh+8BEuHvA+mnOKAhmMQDgKBOCUGaLpewdmsRYguYZSom3yA+/OrE61O/q+NMQANnun65xpI1Hw=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
||||
|
||||
"node-rsa": ["node-rsa@0.4.2", "", { "dependencies": { "asn1": "0.2.3" } }, "sha512-Bvso6Zi9LY4otIZefYrscsUpo2mUpiAVIEmSZV2q41sP8tHZoert3Yu6zv4f/RXJqMNZQKCtnhDugIuCma23YA=="],
|
||||
|
||||
"node-statsd": ["node-statsd@0.1.1", "", {}, "sha512-QDf6R8VXF56QVe1boek8an/Rb3rSNaxoFWb7Elpsv2m1+Noua1yy0F1FpKpK5VluF8oymWM4w764A4KsYL4pDg=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
|
||||
|
||||
"object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="],
|
||||
|
||||
"object.getownpropertydescriptors": ["object.getownpropertydescriptors@2.1.9", "", { "dependencies": { "array.prototype.reduce": "^1.0.8", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "gopd": "^1.2.0", "safe-array-concat": "^1.1.3" } }, "sha512-mt8YM6XwsTTovI+kdZdHSxoyF2DI59up034orlC9NfweclcWOt7CVascNNLp6U+bjFVCVCIh9PwS76tDM/rH8g=="],
|
||||
|
||||
"on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="],
|
||||
|
||||
"on-headers": ["on-headers@1.0.2", "", {}, "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="],
|
||||
|
||||
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"partof": ["partof@1.0.0", "", {}, "sha512-+TXdhKCySpJDynCxgAPoGVyAkiK3QPusQ63/BdU5t68QcYzyU6zkP/T7F3gkMQBVUYqdWEADKa6Kx5zg8QIKrg=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@0.1.7", "", {}, "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"pocketbase": ["pocketbase@0.26.5", "", {}, "sha512-SXcq+sRvVpNxfLxPB1C+8eRatL7ZY4o3EVl/0OdE3MeR9fhPyZt0nmmxLqYmkLvXCN9qp3lXWV/0EUYb3MmMXQ=="],
|
||||
|
||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||
|
||||
"processenv": ["processenv@1.1.0", "", { "dependencies": { "babel-runtime": "6.26.0" } }, "sha512-SymqIsn8GjEUy8nG7HiyEjgbfk1xFosRIakUX1NHLpriq3vVpKniGrr9RdMWCaGYWByIovbRt2f/WvmP/IOApQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"qs": ["qs@6.5.2", "", {}, "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"raw-body": ["raw-body@2.3.3", "", { "dependencies": { "bytes": "3.0.0", "http-errors": "1.6.3", "iconv-lite": "0.4.23", "unpipe": "1.0.0" } }, "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
|
||||
|
||||
"redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="],
|
||||
|
||||
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
||||
|
||||
"regenerator-runtime": ["regenerator-runtime@0.12.1", "", {}, "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="],
|
||||
|
||||
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
|
||||
|
||||
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
|
||||
|
||||
"safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
||||
|
||||
"safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="],
|
||||
|
||||
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
|
||||
|
||||
"send": ["send@0.16.2", "", { "dependencies": { "debug": "2.6.9", "depd": "~1.1.2", "destroy": "~1.0.4", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "~1.6.2", "mime": "1.4.1", "ms": "2.0.0", "on-finished": "~2.3.0", "range-parser": "~1.2.0", "statuses": "~1.4.0" } }, "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw=="],
|
||||
|
||||
"serve-static": ["serve-static@1.13.2", "", { "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "parseurl": "~1.3.2", "send": "0.16.2" } }, "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw=="],
|
||||
|
||||
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
||||
|
||||
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
|
||||
|
||||
"set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="],
|
||||
|
||||
"setprototypeof": ["setprototypeof@1.1.0", "", {}, "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="],
|
||||
|
||||
"sha-1": ["sha-1@0.1.1", "", {}, "sha512-dexizf3hB7d4Jq6Cd0d/NYQiqgEqIfZIpuMfwPfvSb6h06DZKmHyUe55jYwpHC12R42wpqXO6ouhiBpRzIcD/g=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"split2": ["split2@3.0.0", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-Cp7G+nUfKJyHCrAI8kze3Q00PFGEG1pMgrAlTFlDbn+GW24evSZHJuMl+iUJx1w/NTRDeBiTgvwnf6YOt94FMw=="],
|
||||
|
||||
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
|
||||
|
||||
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
|
||||
|
||||
"statuses": ["statuses@1.4.0", "", {}, "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="],
|
||||
|
||||
"stethoskop": ["stethoskop@1.0.0", "", { "dependencies": { "node-statsd": "0.1.1" } }, "sha512-4JnZ+UmTs9SFfDjSHFlD/EoXcb1bfwntkt4h1ipNGrpxtRzmHTxOmdquCJvIrVu608Um7a09cGX0ZSOSllWJNQ=="],
|
||||
|
||||
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
||||
|
||||
"string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="],
|
||||
|
||||
"string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="],
|
||||
|
||||
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"stringify-object": ["stringify-object@3.3.0", "", { "dependencies": { "get-own-enumerable-property-symbols": "^3.0.0", "is-obj": "^1.0.1", "is-regexp": "^1.0.0" } }, "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw=="],
|
||||
|
||||
"supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="],
|
||||
|
||||
"tailwind": ["tailwind@4.0.0", "", { "dependencies": { "@babel/runtime": "7.3.4", "ajv": "6.10.0", "app-root-path": "2.1.0", "async-retry": "1.2.3", "body-parser": "1.18.3", "commands-events": "1.0.4", "compression": "1.7.3", "content-type": "1.0.4", "cors": "2.8.5", "crypto2": "2.0.0", "datasette": "1.0.1", "draht": "1.0.1", "express": "4.16.4 ", "flaschenpost": "1.1.3", "hase": "2.0.0", "json-lines": "1.0.0", "limes": "2.0.0", "lodash": "4.17.11", "lusca": "1.6.1", "morgan": "1.9.1", "nocache": "2.0.0", "partof": "1.0.0", "processenv": "1.1.0", "stethoskop": "1.0.0", "timer2": "1.0.0", "uuidv4": "3.0.1", "ws": "6.2.0" } }, "sha512-LlUNoD/5maFG1h5kQ6/hXfFPdcnYw+1Z7z+kUD/W/E71CUMwcnrskxiBM8c3G8wmPsD1VvCuqGYMHviI8+yrmg=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
|
||||
|
||||
"timer2": ["timer2@1.0.0", "", {}, "sha512-UOZql+P2ET0da+B7V3/RImN3IhC5ghb+9cpecfUhmYGIm0z73dDr3A781nBLnFYmRzeT1AmoT4w9Lgr8n7n7xg=="],
|
||||
|
||||
"tsscmp": ["tsscmp@1.0.6", "", {}, "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA=="],
|
||||
|
||||
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
|
||||
|
||||
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
|
||||
|
||||
"typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="],
|
||||
|
||||
"typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="],
|
||||
|
||||
"typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"untildify": ["untildify@3.0.3", "", {}, "sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"util.promisify": ["util.promisify@1.0.0", "", { "dependencies": { "define-properties": "^1.1.2", "object.getownpropertydescriptors": "^2.0.3" } }, "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA=="],
|
||||
|
||||
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
|
||||
|
||||
"uuid": ["uuid@3.3.2", "", { "bin": { "uuid": "./bin/uuid" } }, "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="],
|
||||
|
||||
"uuidv4": ["uuidv4@3.0.1", "", { "dependencies": { "uuid": "3.3.2" } }, "sha512-PPzksdWRl2a5C9hrs3OOYrArTeyoR0ftJ3jtOy+BnVHkT2UlrrzPNt9nTdiGuxmQItHM/AcTXahwZZC57Njojg=="],
|
||||
|
||||
"varname": ["varname@2.0.3", "", {}, "sha512-+DofT9mJAUALhnr9ipZ5Z2icwaEZ7DAajOZT4ffXy3MQqnXtG3b7atItLQEJCkfcJTOf9WcsywneOEibD4eqJg=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
|
||||
|
||||
"which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="],
|
||||
|
||||
"which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="],
|
||||
|
||||
"which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="],
|
||||
|
||||
"ws": ["ws@6.2.0", "", { "dependencies": { "async-limiter": "~1.0.0" } }, "sha512-deZYUNlt2O4buFCa3t5bKLf8A7FPP/TVjwOeVNpw818Ma5nk4MLXls2eoEGS39o8119QIYxTrTDoPQ5B/gTD6w=="],
|
||||
|
||||
"amqplib/readable-stream": ["readable-stream@1.1.14", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "0.0.1", "string_decoder": "~0.10.x" } }, "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ=="],
|
||||
|
||||
"babel-runtime/regenerator-runtime": ["regenerator-runtime@0.11.1", "", {}, "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="],
|
||||
|
||||
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"commands-events/@babel/runtime": ["@babel/runtime@7.2.0", "", { "dependencies": { "regenerator-runtime": "^0.12.0" } }, "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg=="],
|
||||
|
||||
"commands-events/uuidv4": ["uuidv4@2.0.0", "", { "dependencies": { "sha-1": "0.1.1", "uuid": "3.3.2" } }, "sha512-sAUlwUVepcVk6bwnaW/oi6LCwMdueako5QQzRr90ioAVVcms6p1mV0PaSxK8gyAC4CRvKddsk217uUpZUbKd2Q=="],
|
||||
|
||||
"compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"datasette/lodash": ["lodash@4.17.5", "", {}, "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw=="],
|
||||
|
||||
"ecdsa-sig-formatter/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"flaschenpost/@babel/runtime": ["@babel/runtime@7.2.0", "", { "dependencies": { "regenerator-runtime": "^0.12.0" } }, "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg=="],
|
||||
|
||||
"hase/@babel/runtime": ["@babel/runtime@7.1.2", "", { "dependencies": { "regenerator-runtime": "^0.12.0" } }, "sha512-Y3SCjmhSupzFB6wcv1KmmFucH6gDVnI30WjOcicV10ju0cZjak3Jcs67YLIXBrmZYw1xCrVeJPbycFwrqNyxpg=="],
|
||||
|
||||
"mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"morgan/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"safe-array-concat/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
||||
"safe-push-apply/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
||||
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"send/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
||||
"amqplib/readable-stream/string_decoder": ["string_decoder@0.10.31", "", {}, "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="],
|
||||
|
||||
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"morgan/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 1.6 MiB |
@@ -1,39 +0,0 @@
|
||||
// PocketBase auth bootstrap shared by pages
|
||||
(function (global) {
|
||||
const pb = new PocketBase('https://pocketbase.ccllc.pro');
|
||||
|
||||
function authHeaders() {
|
||||
if (pb.authStore.isValid && pb.authStore.token) {
|
||||
return { Authorization: `Bearer ${pb.authStore.token}` };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function getDisplayName() {
|
||||
const model = pb.authStore.model || {};
|
||||
return (
|
||||
model.display_name ||
|
||||
model.name ||
|
||||
model.email ||
|
||||
model.username ||
|
||||
'Unknown'
|
||||
);
|
||||
}
|
||||
|
||||
function getEmail() {
|
||||
const model = pb.authStore.model || {};
|
||||
// Prefer email, but fall back to username if email missing
|
||||
return model.email || model.username || null;
|
||||
}
|
||||
|
||||
async function ensureAuth() {
|
||||
if (pb.authStore.isValid) return true;
|
||||
const path = new URL(window.location.href).pathname;
|
||||
if (!path.endsWith('signin.html')) {
|
||||
window.location.href = 'signin.html';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
global.Auth = { pb, authHeaders, ensureAuth, getDisplayName, getEmail };
|
||||
})(window);
|
||||
@@ -1,5 +0,0 @@
|
||||
/* Generated Tailwind CSS - Basic version */
|
||||
/* This will be updated by tailwind CLI on dev */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -1,131 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sign In</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script>
|
||||
<script src="auth.js"></script>
|
||||
</head>
|
||||
<body class="font-sans bg-gradient-to-br from-indigo-500 to-purple-600 min-h-screen flex items-center justify-center">
|
||||
<div class="bg-white p-12 rounded-2xl shadow-2xl text-center max-w-md w-[90%]">
|
||||
<h1 class="text-gray-800 mb-2 text-3xl">Welcome</h1>
|
||||
<p class="text-gray-600 mb-8">Sign in with your Microsoft account</p>
|
||||
|
||||
<button id="loginBtn" onclick="login()" class="bg-blue-600 text-white border-none py-3.5 px-8 text-base rounded-md cursor-pointer transition-colors duration-200 font-medium hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed">Sign in with Microsoft</button>
|
||||
|
||||
<div class="text-red-600 mt-4 hidden" id="error"></div>
|
||||
|
||||
<div class="hidden mt-8 text-left bg-gray-50 p-6 rounded-lg" id="userInfo">
|
||||
<h2 class="text-lg mb-4 text-gray-800">Signed in</h2>
|
||||
<p class="mb-3 text-gray-600 break-all"><strong class="text-gray-800">Name:</strong> <span id="displayName"></span></p>
|
||||
<p class="mb-3 text-gray-600 break-all"><strong class="text-gray-800">Email:</strong> <span id="email"></span></p>
|
||||
|
||||
<button id="continueBtn" onclick="goToIndex()" class="hidden mt-4 bg-blue-600 text-white border-none py-3.5 px-8 text-base rounded-md cursor-pointer transition-colors duration-200 font-medium hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed w-full">Continue to Job Info</button>
|
||||
<button onclick="logout()" class="bg-gray-600 text-white border-none py-3.5 px-8 text-base rounded-md cursor-pointer transition-colors duration-200 font-medium hover:bg-gray-700 mt-4">Sign out</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const { pb, getDisplayName } = window.Auth;
|
||||
|
||||
const GRAPH_TOKEN_KEY = 'graphAccessToken';
|
||||
const extractGraphToken = (authData) => {
|
||||
return (
|
||||
authData?.meta?.graphAccessToken ||
|
||||
authData?.meta?.graph_token ||
|
||||
authData?.meta?.graphToken ||
|
||||
authData?.meta?.accessToken ||
|
||||
authData?.meta?.access_token ||
|
||||
authData?.meta?.token ||
|
||||
authData?.meta?.rawToken ||
|
||||
authData?.meta?.authData?.access_token ||
|
||||
''
|
||||
);
|
||||
};
|
||||
|
||||
const loginBtn = document.getElementById('loginBtn');
|
||||
const errorEl = document.getElementById('error');
|
||||
const userInfo = document.getElementById('userInfo');
|
||||
const continueBtn = document.getElementById('continueBtn');
|
||||
const GRAPH_REAUTH_FLAG = 'graphReauthAttempted';
|
||||
|
||||
function goToIndex() {
|
||||
window.location.href = 'index.html';
|
||||
}
|
||||
|
||||
function showAuthedUI(displayName, email) {
|
||||
loginBtn.classList.add('hidden');
|
||||
loginBtn.disabled = false;
|
||||
loginBtn.textContent = 'Sign in with Microsoft';
|
||||
userInfo.classList.remove('hidden');
|
||||
continueBtn.classList.remove('hidden');
|
||||
document.getElementById('displayName').textContent = displayName || 'Unknown';
|
||||
document.getElementById('email').textContent = email || 'Not available';
|
||||
}
|
||||
|
||||
async function login() {
|
||||
loginBtn.disabled = true;
|
||||
loginBtn.textContent = 'Signing in...';
|
||||
errorEl.classList.add('hidden');
|
||||
|
||||
try {
|
||||
const authData = await pb.collection('users').authWithOAuth2({
|
||||
provider: 'microsoft',
|
||||
});
|
||||
displayUserInfo(authData);
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
errorEl.textContent = error.message || 'Authentication failed. Please try again.';
|
||||
errorEl.classList.remove('hidden');
|
||||
loginBtn.disabled = false;
|
||||
loginBtn.textContent = 'Sign in with Microsoft';
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
pb.authStore.clear();
|
||||
localStorage.removeItem(GRAPH_TOKEN_KEY);
|
||||
loginBtn.classList.remove('hidden');
|
||||
userInfo.classList.add('hidden');
|
||||
continueBtn.classList.add('hidden');
|
||||
loginBtn.disabled = false;
|
||||
loginBtn.textContent = 'Sign in with Microsoft';
|
||||
}
|
||||
|
||||
function displayUserInfo(authData) {
|
||||
console.log('Auth response:', authData);
|
||||
console.log('Auth meta:', authData?.meta);
|
||||
const displayName = getDisplayName();
|
||||
const email = authData?.meta?.rawUser?.email || authData?.record?.email || pb.authStore.model?.email || 'Not available';
|
||||
const graphToken = extractGraphToken(authData);
|
||||
console.log('Extracted Graph token:', graphToken ? graphToken.substring(0, 30) + '...' : 'NOT FOUND');
|
||||
if (graphToken) {
|
||||
localStorage.setItem(GRAPH_TOKEN_KEY, graphToken);
|
||||
localStorage.removeItem(GRAPH_REAUTH_FLAG);
|
||||
}
|
||||
showAuthedUI(displayName, email);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const reauth = params.get('reauth') === '1';
|
||||
if (pb.authStore.isValid) {
|
||||
try {
|
||||
const authData = await pb.collection('users').authRefresh();
|
||||
displayUserInfo(authData);
|
||||
// If we came from a reauth flow and now have a token, go back automatically
|
||||
const hasToken = localStorage.getItem(GRAPH_TOKEN_KEY);
|
||||
if (reauth && hasToken) {
|
||||
window.location.href = 'index.html';
|
||||
}
|
||||
} catch (err) {
|
||||
pb.authStore.clear();
|
||||
logout();
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,3 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"name": "job-info-pb",
|
||||
"version": "1.0.0-beta3",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "bun --watch backend/server.ts",
|
||||
"start": "bun run backend/server.ts",
|
||||
"build:css": "tailwindcss -i input.css -o frontend/output.css --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^17.2.3",
|
||||
"hono": "^4.10.8",
|
||||
"pocketbase": "^0.26.5",
|
||||
"tailwind": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
'./frontend/**/*.{html,js}',
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
# Mode2Test
|
||||
|
||||
Complete OAuth2 authentication system with persistent token management.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
Mode2Test/
|
||||
AuthAndToken/ # Everything related to auth and tokens
|
||||
package.json
|
||||
backend/
|
||||
server.ts # Hono server
|
||||
token-service.ts # Token storage & refresh
|
||||
auth-routes.ts # Hono API endpoints
|
||||
frontend/
|
||||
index.html # Main app (requires auth)
|
||||
signin.html # Microsoft OAuth2 login
|
||||
auth-manager.ts # Frontend auth helpers
|
||||
README.md # Component documentation
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
cd Mode2Test/AuthAndToken
|
||||
bun install
|
||||
```
|
||||
|
||||
2. Set environment variables (in `secrets/.env`):
|
||||
```
|
||||
POCKETBASE_URL=https://pocketbase.ccllc.pro
|
||||
```
|
||||
|
||||
3. Run:
|
||||
```bash
|
||||
bun run backend/server.ts
|
||||
```
|
||||
|
||||
4. Open `http://localhost:3005` → redirects to signin.html if not authenticated
|
||||
|
||||
## How It Works
|
||||
|
||||
### Token Lifecycle
|
||||
|
||||
1. **Login**: User signs in via Microsoft OAuth2 → PB and Graph tokens acquired → stored in `tokens.json` → background refresh starts
|
||||
2. **Refresh**: Every 5 minutes, both tokens auto-refresh silently
|
||||
3. **Access**: Frontend/backend call `/api/auth/tokens` to get current state
|
||||
4. **Logout**: User clicks logout → tokens cleared
|
||||
|
||||
### Startup Behavior
|
||||
|
||||
- If `tokens.json` exists with valid PB token: background refresh resumes immediately
|
||||
- If no valid token: frontend redirects to `signin.html` to require login
|
||||
- Tokens are **never deleted** until fresh ones are confirmed available
|
||||
|
||||
## API Endpoints
|
||||
|
||||
- `GET /api/auth/status` - Check auth status
|
||||
- `GET /api/auth/tokens` - Get current tokens (requires auth)
|
||||
- `POST /api/auth/login` - Store tokens after OAuth login
|
||||
- `POST /api/auth/logout` - Clear all tokens
|
||||
|
||||
## Tokens
|
||||
|
||||
- **PocketBase Token**: Required. Refreshes automatically every 5 minutes.
|
||||
- **Graph Token**: Optional. Refreshes automatically if available.
|
||||
|
||||
Both stay fresh at all times after initial login—no user intervention needed.
|
||||
@@ -1,82 +0,0 @@
|
||||
import { Hono } from 'hono';
|
||||
import type { Context } from 'hono';
|
||||
import { tokenService } from './token-service';
|
||||
|
||||
// Authentication API routes
|
||||
// Endpoints for frontend to check auth status, get tokens, logout
|
||||
// Depends on: tokenService singleton (centralized token management)
|
||||
|
||||
export function createAuthRoutes(): Hono {
|
||||
const router = new Hono();
|
||||
|
||||
// PERMANENT: Check if user is authenticated
|
||||
// Returns current auth status and which tokens are available
|
||||
router.get('/status', async (c: Context) => {
|
||||
const tokens = await tokenService.getTokens();
|
||||
const hasToken = !!tokens?.pbToken;
|
||||
return c.json({
|
||||
authenticated: hasToken,
|
||||
pbTokenValid: hasToken,
|
||||
assumedFresh: hasToken,
|
||||
message: hasToken ? 'Assuming tokens are valid (auto-refresh running)' : 'No PB token yet; login once'
|
||||
});
|
||||
});
|
||||
|
||||
// PERMANENT: Get current tokens
|
||||
// Frontend/backend can call this to get fresh token state
|
||||
// Only returns tokens if PB token is valid (auth required)
|
||||
router.get('/tokens', async (c: Context) => {
|
||||
const tokens = await tokenService.getTokens();
|
||||
return c.json({
|
||||
pbToken: tokens?.pbToken || null,
|
||||
graphToken: tokens?.graphToken || null,
|
||||
pbTokenExpiry: tokens?.pbTokenExpiry || null,
|
||||
graphTokenExpiry: tokens?.graphTokenExpiry || null,
|
||||
lastRefresh: tokens?.lastRefresh || null,
|
||||
assumedFresh: !!tokens?.pbToken
|
||||
});
|
||||
});
|
||||
|
||||
// PERMANENT: Handle login callback from OAuth2
|
||||
// Frontend sends back the PB auth response after successful login
|
||||
// Stores both PB and Graph tokens
|
||||
router.post('/login', async (c: Context) => {
|
||||
try {
|
||||
const body = await c.req.json() as any;
|
||||
const pbToken = body.pbToken;
|
||||
const graphToken = body.graphToken;
|
||||
|
||||
if (!pbToken) {
|
||||
return c.json({ error: 'Missing pbToken' }, 400);
|
||||
}
|
||||
|
||||
await tokenService.saveTokens(pbToken, graphToken);
|
||||
return c.json({
|
||||
success: true,
|
||||
message: 'Login successful',
|
||||
authenticated: true
|
||||
});
|
||||
} catch (err) {
|
||||
console.log('[AuthAndToken] Login error:', (err as Error)?.message);
|
||||
return c.json({ error: 'Login failed' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// PERMANENT: Logout - clear all tokens
|
||||
// Called when user clicks logout
|
||||
router.post('/logout', async (c: Context) => {
|
||||
try {
|
||||
await tokenService.clearTokens();
|
||||
return c.json({
|
||||
success: true,
|
||||
message: 'Logged out',
|
||||
authenticated: false
|
||||
});
|
||||
} catch (err) {
|
||||
console.log('[AuthAndToken] Logout error:', (err as Error)?.message);
|
||||
return c.json({ error: 'Logout failed' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import { Hono } from 'hono';
|
||||
import type { Context } from 'hono';
|
||||
import { tokenService } from './token-service';
|
||||
|
||||
// PERMANENT: Jobs API routes
|
||||
// Provides access to job data from PocketBase
|
||||
// Requires authentication (valid PB token)
|
||||
//
|
||||
// Dependencies:
|
||||
// - tokenService for PB token retrieval
|
||||
// - process.env.POCKETBASE_URL for PB API endpoint
|
||||
const JOBS_COLLECTION = 'pbc_1407612689';
|
||||
//
|
||||
// Endpoints:
|
||||
// - GET /api/jobs - List jobs with pagination, filtering, field selection
|
||||
|
||||
export function createJobsRoutes(): Hono {
|
||||
const router = new Hono();
|
||||
|
||||
// GET /api/jobs - Fetch jobs from PocketBase
|
||||
// Query params:
|
||||
// - page: Page number (default 1)
|
||||
// - perPage: Items per page (default 50, max 1000)
|
||||
// - fields: Comma-separated field names to return (optional, for cache optimization)
|
||||
// - filter: PocketBase filter syntax (optional)
|
||||
// - sort: Sort field (default -Job_Number)
|
||||
router.get('/', async (c: Context) => {
|
||||
try {
|
||||
// Get token (assume present after login)
|
||||
const tokens = await tokenService.getTokens();
|
||||
const pbToken = tokens?.pbToken;
|
||||
|
||||
// Parse query params
|
||||
const page = parseInt(c.req.query('page') || '1');
|
||||
const perPage = Math.min(parseInt(c.req.query('perPage') || '50'), 1000);
|
||||
const fields = c.req.query('fields'); // Optional: comma-separated field names
|
||||
const filter = c.req.query('filter'); // Optional: PocketBase filter
|
||||
const sort = c.req.query('sort') || '-Job_Number';
|
||||
|
||||
// Build PocketBase API URL
|
||||
const pbUrl = new URL(`${process.env.POCKETBASE_URL}/api/collections/${JOBS_COLLECTION}/records`);
|
||||
pbUrl.searchParams.set('page', page.toString());
|
||||
pbUrl.searchParams.set('perPage', perPage.toString());
|
||||
pbUrl.searchParams.set('sort', sort);
|
||||
|
||||
if (fields) {
|
||||
pbUrl.searchParams.set('fields', fields);
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
pbUrl.searchParams.set('filter', filter);
|
||||
}
|
||||
|
||||
// Fetch from PocketBase
|
||||
// temporary: dev-only SSL bypass; global rejectUnauthorized already set, keep here as explicit
|
||||
const response = await fetch(pbUrl.toString(), {
|
||||
headers: pbToken ? { 'Authorization': `Bearer ${pbToken}` } : {},
|
||||
// @ts-ignore Bun TLS option
|
||||
tls: { rejectUnauthorized: false }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('PocketBase jobs fetch failed:', response.status, errorText);
|
||||
return c.json({ error: 'Failed to fetch jobs from PocketBase' }, response.status);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return c.json(data);
|
||||
} catch (err) {
|
||||
console.error('Jobs API error:', (err as Error)?.message);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/jobs/:id - Fetch single job by ID
|
||||
router.get('/:id', async (c: Context) => {
|
||||
try {
|
||||
// Check authentication
|
||||
const tokens = await tokenService.getTokens();
|
||||
const pbToken = tokens?.pbToken;
|
||||
|
||||
const jobId = c.req.param('id');
|
||||
const pbUrl = `${process.env.POCKETBASE_URL}/api/collections/${JOBS_COLLECTION}/records/${jobId}`;
|
||||
|
||||
// Fetch from PocketBase
|
||||
const response = await fetch(pbUrl, {
|
||||
headers: pbToken ? { 'Authorization': `Bearer ${pbToken}` } : {}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return c.json({ error: 'Job not found' }, 404);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return c.json(data);
|
||||
} catch (err) {
|
||||
console.error('Job fetch error:', (err as Error)?.message);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Hono } from 'hono';
|
||||
import { serveStatic } from 'hono/bun';
|
||||
import { join } from 'path';
|
||||
import { tokenService } from './token-service';
|
||||
import { createAuthRoutes } from './auth-routes';
|
||||
import { createJobsRoutes } from './jobs-routes';
|
||||
|
||||
// temporary: dev-only SSL bypass for upstream PocketBase cert issues
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||
|
||||
// PERMANENT: Load environment variables
|
||||
// Check if POCKETBASE_URL is set, use default if not
|
||||
if (!process.env.POCKETBASE_URL) {
|
||||
process.env.POCKETBASE_URL = 'https://pocketbase.ccllc.pro';
|
||||
console.log('[Mode2Test] Using default POCKETBASE_URL:', process.env.POCKETBASE_URL);
|
||||
}
|
||||
|
||||
// PERMANENT: Initialize token service and start background refresh
|
||||
// Loads existing tokens from file if available
|
||||
// Starts 5-minute background refresh cycle
|
||||
tokenService.initialize();
|
||||
tokenService.startBackgroundRefresh();
|
||||
|
||||
// Create Hono app
|
||||
const app = new Hono();
|
||||
|
||||
// Mount auth routes
|
||||
app.route('/api/auth', createAuthRoutes());
|
||||
|
||||
// Mount jobs routes
|
||||
app.route('/api/jobs', createJobsRoutes());
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', (c) => {
|
||||
return c.text('OK');
|
||||
});
|
||||
|
||||
// Version endpoint
|
||||
app.get('/version', (c) => {
|
||||
return c.json({ version: '1.0.0-beta1' });
|
||||
});
|
||||
|
||||
// PERMANENT: Serve static frontend files
|
||||
// Serves from frontend/ directory
|
||||
const publicDir = join(import.meta.dir, '../frontend');
|
||||
app.use('/*', serveStatic({ root: publicDir }));
|
||||
|
||||
// Start server on port 3005
|
||||
const port = 3005;
|
||||
console.log(`Mode2Test (AuthAndToken) running on http://localhost:${port}`);
|
||||
console.log(`Auth API: /api/auth/*`);
|
||||
console.log(`SignIn page: /signin.html`);
|
||||
|
||||
export default {
|
||||
fetch: app.fetch,
|
||||
port,
|
||||
};
|
||||
@@ -1,176 +0,0 @@
|
||||
import { join } from 'path';
|
||||
|
||||
// PERMANENT: Token Service Module
|
||||
// Singleton module for centralized token acquisition, storage, and refresh
|
||||
// Available to any part of the application via: import { tokenService } from './token-service.ts'
|
||||
//
|
||||
// Token Lifecycle:
|
||||
// - Tokens stored in tokens.json (persistent across restarts)
|
||||
// - Never delete old token before new one acquired
|
||||
// - Background refresh every 5 minutes
|
||||
// - Startup: if PB token exists, resume refresh; else require login
|
||||
//
|
||||
// Usage:
|
||||
// const tokens = await tokenService.getTokens();
|
||||
// await tokenService.saveTokens(pbToken, graphToken);
|
||||
// await tokenService.initialize();
|
||||
// tokenService.startBackgroundRefresh();
|
||||
|
||||
interface TokenData {
|
||||
pbToken: string;
|
||||
pbTokenExpiry: number;
|
||||
graphToken?: string;
|
||||
graphTokenExpiry?: number;
|
||||
lastRefresh: number;
|
||||
}
|
||||
|
||||
const tokensFilePath = join(import.meta.dir, '../../tokens.json');
|
||||
|
||||
class TokenService {
|
||||
private backgroundStarted = false;
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
// Load tokens from file if they exist
|
||||
try {
|
||||
const file = Bun.file(tokensFilePath);
|
||||
if (await file.exists()) {
|
||||
const content = await file.text();
|
||||
const data = JSON.parse(content) as TokenData;
|
||||
console.log('[AuthAndToken] Tokens loaded from file');
|
||||
console.log('[AuthAndToken] PB token valid:', data.pbToken ? 'yes' : 'no');
|
||||
console.log('[AuthAndToken] Graph token valid:', data.graphToken ? 'yes' : 'no');
|
||||
} else {
|
||||
console.log('[AuthAndToken] No tokens.json found - login required on startup');
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('[AuthAndToken] Token file init error (will create on first login):', (err as Error)?.message);
|
||||
}
|
||||
}
|
||||
|
||||
startBackgroundRefresh(): void {
|
||||
if (this.backgroundStarted) return;
|
||||
this.backgroundStarted = true;
|
||||
|
||||
// PERMANENT: Background token refresh
|
||||
// Runs every 5 minutes, attempts to refresh both PB and Graph tokens
|
||||
// Safe: never deletes old token before new one is confirmed
|
||||
setInterval(async () => {
|
||||
try {
|
||||
await this.refreshTokens();
|
||||
} catch (err) {
|
||||
console.log('[AuthAndToken] Background refresh error:', (err as Error)?.message);
|
||||
}
|
||||
}, 5 * 60 * 1000); // 5 minutes
|
||||
|
||||
// Kick off an immediate refresh attempt so we start from a fresh token without waiting
|
||||
this.refreshTokens().catch((err) => {
|
||||
console.log('[AuthAndToken] Immediate refresh error:', (err as Error)?.message);
|
||||
});
|
||||
|
||||
console.log('[AuthAndToken] Background refresh started (every 5 minutes + immediate)');
|
||||
}
|
||||
|
||||
async saveTokens(pbToken: string, graphToken?: string): Promise<void> {
|
||||
// PERMANENT: Token persistence
|
||||
// Called after login or successful refresh
|
||||
// Stores both tokens with expiry times
|
||||
|
||||
const data: TokenData = {
|
||||
pbToken,
|
||||
pbTokenExpiry: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7 days (PB default)
|
||||
graphToken,
|
||||
graphTokenExpiry: graphToken ? Date.now() + 60 * 60 * 1000 : undefined, // 1 hour (Graph default)
|
||||
lastRefresh: Date.now()
|
||||
};
|
||||
|
||||
await Bun.write(tokensFilePath, JSON.stringify(data, null, 2));
|
||||
console.log('[AuthAndToken] Tokens saved');
|
||||
}
|
||||
|
||||
async getTokens(): Promise<TokenData | null> {
|
||||
try {
|
||||
const file = Bun.file(tokensFilePath);
|
||||
if (!(await file.exists())) return null;
|
||||
|
||||
const content = await file.text();
|
||||
return JSON.parse(content) as TokenData;
|
||||
} catch (err) {
|
||||
console.log('[AuthAndToken] Error reading tokens:', (err as Error)?.message);
|
||||
try {
|
||||
const fs = await import('fs/promises');
|
||||
await fs.unlink(tokensFilePath);
|
||||
console.log('[AuthAndToken] Corrupt tokens file removed');
|
||||
} catch (innerErr) {
|
||||
console.log('[AuthAndToken] Failed to remove corrupt tokens file:', (innerErr as Error)?.message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async clearTokens(): Promise<void> {
|
||||
try {
|
||||
const file = Bun.file(tokensFilePath);
|
||||
if (await file.exists()) {
|
||||
const fs = await import('fs/promises');
|
||||
await fs.unlink(tokensFilePath);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('[AuthAndToken] Error clearing tokens:', (err as Error)?.message);
|
||||
}
|
||||
}
|
||||
|
||||
async hasValidPbToken(): Promise<boolean> {
|
||||
// Simplified: treat presence of PB token as valid; no expiry enforcement
|
||||
const tokens = await this.getTokens();
|
||||
return !!tokens?.pbToken;
|
||||
}
|
||||
|
||||
private async refreshTokens(): Promise<void> {
|
||||
// PERMANENT: Auto-refresh logic for both tokens
|
||||
// Called every 5 minutes by background job
|
||||
// Fetches fresh tokens from PocketBase, attempts Graph token refresh
|
||||
// Only updates file after new tokens confirmed valid
|
||||
|
||||
const tokens = await this.getTokens();
|
||||
if (!tokens || !tokens.pbToken) {
|
||||
console.log('[AuthAndToken] No active tokens to refresh');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Refresh via PocketBase endpoint
|
||||
const pbUrl = `${process.env.POCKETBASE_URL}/api/collections/users/auth-refresh`;
|
||||
const response = await fetch(pbUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${tokens.pbToken}`
|
||||
},
|
||||
// @ts-ignore Bun TLS option
|
||||
tls: { rejectUnauthorized: false }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.log('[AuthAndToken] Refresh failed:', response.status);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json() as any;
|
||||
const newPbToken = data?.token;
|
||||
|
||||
if (!newPbToken) {
|
||||
console.log('[AuthAndToken] No new PB token in response');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update tokens with new PB token
|
||||
await this.saveTokens(newPbToken, tokens.graphToken);
|
||||
console.log('[AuthAndToken] Tokens refreshed successfully');
|
||||
} catch (err) {
|
||||
console.log('[AuthAndToken] Refresh error:', (err as Error)?.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PERMANENT: Singleton export
|
||||
// Import as: import { tokenService } from './token-service.ts'
|
||||
export const tokenService = new TokenService();
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "mode2test",
|
||||
"dependencies": {
|
||||
"dotenv": "17.2.3",
|
||||
"hono": "4.11.4",
|
||||
"pocketbase": "0.26.5",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"typescript": "5.9.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
||||
|
||||
"@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
||||
|
||||
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
|
||||
|
||||
"hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="],
|
||||
|
||||
"pocketbase": ["pocketbase@0.26.5", "", {}, "sha512-SXcq+sRvVpNxfLxPB1C+8eRatL7ZY4o3EVl/0OdE3MeR9fhPyZt0nmmxLqYmkLvXCN9qp3lXWV/0EUYb3MmMXQ=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 1.6 MiB |
@@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Job Details</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="font-sans bg-gray-100 p-4">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<h1 class="text-2xl font-bold mb-4 text-blue-600">Job Details</h1>
|
||||
<p class="text-gray-600">Details view - Under construction</p>
|
||||
<a href="/home.html" class="text-blue-600 underline mt-4 inline-block">← Back to Home</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,104 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" maximum-scale="1" />
|
||||
<title>Job Info — Home</title>
|
||||
<link rel="preload" href="assets/CCwhiteApp.png" as="image">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
if (window.tailwind) window.tailwind.config = { corePlugins: { preflight: true } }
|
||||
</script>
|
||||
</head>
|
||||
<body class="font-sans bg-gray-100 m-0 text-gray-900 overflow-hidden touch-pan-y h-screen flex flex-col">
|
||||
<div class="max-w-full md:max-w-2xl mx-auto p-4 sm:p-3 text-[17px] flex flex-col flex-1 overflow-hidden">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="w-full bg-gray-400 border border-gray-300 rounded-lg px-0.5 py-0.5 mb-3 shadow-sm flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-6 w-[186px] rounded-lg bg-black flex items-center justify-center overflow-hidden">
|
||||
<img src="assets/CCwhiteApp.png" alt="CC" class="h-6 w-auto object-cover scale-80" loading="lazy">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-0">
|
||||
<a href="https://idea-form.ccllc.pro" target="_blank" rel="noopener" class="h-6 w-12 rounded-3xl overflow-hidden flex items-center justify-center bg-transparent hover:bg-gray-50/20" title="Submit an idea">
|
||||
<img src="assets/lightbulb.jpg" alt="Idea" class="h-full w-auto object-cover" loading="lazy">
|
||||
</a>
|
||||
<a href="https://czflex.sharepoint.com/:x:/r/sites/Team/Shared%20Documents/General/ALFONSO/Comprehensive%20Asset%20Inventory.xlsx?d=w9005440917e945d680e9885f33ccf2ba&csf=1&web=1&e=rifTzf" target="_blank" rel="noopener" class="h-6 w-12 rounded-3xl overflow-hidden flex items-center justify-center bg-transparent hover:bg-gray-50/20" title="Open Asset Inventory">
|
||||
<img src="assets/PalmIsland.png" alt="Palm Island" class="h-full w-auto object-cover" loading="lazy">
|
||||
</a>
|
||||
<a href="https://czflex.sharepoint.com/:x:/r/sites/Team/Shared%20Documents/General/ALFONSO/GM%20Rollup.xlsx?d=w3e59f37ee1c04cb798e14f86bd87c968&csf=1&web=1&e=vCnfbR" target="_blank" rel="noopener" class="h-6 w-12 rounded-3xl overflow-hidden flex items-center justify-center bg-transparent hover:bg-gray-50/20" title="Open Operations Dashboard">
|
||||
<img src="assets/DailyCheck.png" alt="Operations Dashboard" class="h-full w-auto object-cover" loading="lazy">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 class="m-0 mb-3 text-center text-blue-600 text-[33px] font-bold">Job Info</h1>
|
||||
|
||||
<!-- Search Box -->
|
||||
<div class="relative mb-2">
|
||||
<input id="searchBox" placeholder="Search jobs (just start typing or use voice)" class="w-full p-2.5 pr-12 rounded-lg border border-gray-300 text-lg placeholder:text-gray-400">
|
||||
<button id="voiceSearchBtn" class="absolute right-2 top-1/2 -translate-y-1/2 w-8 h-8 rounded-full flex items-center justify-center hover:bg-gray-100 transition-colors" title="Voice search">
|
||||
<svg id="micIcon" class="w-5 h-5 text-gray-600 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"></path>
|
||||
</svg>
|
||||
<svg id="micRecordingIcon" class="hidden w-5 h-5 text-red-600 animate-pulse" fill="currentColor" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="8"></circle>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Filter Toggle -->
|
||||
<button id="filterToggle" class="bg-blue-600 text-white py-2 px-2.5 rounded-lg border-none cursor-pointer mb-2 inline-block min-w-[140px]">Show Filters</button>
|
||||
|
||||
<!-- Filters Panel -->
|
||||
<div id="filtersPanel" class="hidden bg-white border border-gray-200 rounded-lg p-2.5 mb-2.5">
|
||||
<div class="mb-2 text-sm">
|
||||
<strong>Division</strong><br/>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-division" value="C#"> Commercial</label>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-division" value="R#"> Residential</label>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-division" value="S#"> Small Jobs</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 text-sm">
|
||||
<strong>Active</strong><br/>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-active" value="Yes"> Yes</label>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-active" value="No"> No</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 text-sm">
|
||||
<strong>Estimator</strong><br/>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-estimator" value="Steve Brewer"> Steve</label>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-estimator" value="Beth Cardoza"> Beth</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 text-sm">
|
||||
<strong>Status</strong><br/>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="Estimating"> Estimating</label>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="In Progress"> In Progress</label>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="Awarded"> Awarded</label>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="Billed (In Progress)"> Billed (In Progress)</label>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="Not Bidding"> Not Bidding</label>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="Est Sent"> Est Sent</label>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="Not Awarded"> Not Awarded</label>
|
||||
</div>
|
||||
<button id="clearFilters" class="bg-gray-500 text-white py-2 px-2 rounded-lg border-none">Clear Filters</button>
|
||||
</div>
|
||||
|
||||
<!-- Results -->
|
||||
<div id="results" aria-live="polite" class="flex flex-col gap-3 flex-1 overflow-auto p-0"></div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="text-xs text-gray-500 mt-5 pt-3 border-t border-gray-200 flex items-center justify-between">
|
||||
<span id="userEmail" class="truncate"></span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="signOutBtn" class="px-2 py-1 text-[11px] bg-gray-200 hover:bg-gray-300 rounded border border-gray-300 text-gray-700">Sign out</button>
|
||||
<span id="modeLabel" class="px-2 py-1 text-[11px] bg-blue-100 text-blue-700 rounded border border-blue-300">[Mode2Test]</span>
|
||||
<span id="versionLabel">v1.0.0-beta1</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="js/home.js?v=2"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,69 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mode2Test</title>
|
||||
<script>
|
||||
// Redirect to appropriate page based on auth status
|
||||
async function checkAuthAndRedirect() {
|
||||
try {
|
||||
const response = await fetch('/api/auth/status');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.authenticated) {
|
||||
// Redirect to home
|
||||
window.location.href = '/home.html';
|
||||
} else {
|
||||
// Redirect to signin
|
||||
window.location.href = '/signin.html';
|
||||
}
|
||||
} catch (err) {
|
||||
// On error, go to signin
|
||||
window.location.href = '/signin.html';
|
||||
}
|
||||
}
|
||||
|
||||
checkAuthAndRedirect();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div style="text-align: center; padding: 50px;">
|
||||
<p>Redirecting...</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
} catch {
|
||||
window.location.href = '/signin.html';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Logged in - show app
|
||||
document.getElementById('userName').textContent = pb.authStore.model?.display_name || pb.authStore.model?.email || 'User';
|
||||
document.getElementById('userEmail').textContent = pb.authStore.model?.email || '';
|
||||
|
||||
const response = await fetch('/api/auth/tokens');
|
||||
if (response.ok) {
|
||||
const tokens = await response.json();
|
||||
const pbValid = tokens.pbToken ? '✓ Valid' : '✗ Missing';
|
||||
const graphValid = tokens.graphToken ? '✓ Valid' : '✗ Not acquired';
|
||||
document.getElementById('tokenStatus').innerHTML = `
|
||||
<div><p><strong>PocketBase Token:</strong> ${pbValid}</p></div>
|
||||
<div><p><strong>Graph Token:</strong> ${graphValid}</p></div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
if (confirm('Logout?')) {
|
||||
pb.authStore.clear();
|
||||
fetch('/api/auth/logout', { method: 'POST' });
|
||||
window.location.href = '/signin.html';
|
||||
}
|
||||
}
|
||||
|
||||
checkAuth();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,344 +0,0 @@
|
||||
// job-cache.js
|
||||
var DB_NAME = "JobInfoCache";
|
||||
var DB_VERSION = 2;
|
||||
var JOBS_STORE = "jobs";
|
||||
var META_STORE = "metadata";
|
||||
var CACHE_TTL = 5 * 60 * 1000;
|
||||
var PAGE_SIZE = 500;
|
||||
|
||||
class JobCache {
|
||||
db = null;
|
||||
async initialize() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result;
|
||||
resolve();
|
||||
};
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
if (!db.objectStoreNames.contains(JOBS_STORE)) {
|
||||
db.createObjectStore(JOBS_STORE, { keyPath: "Job_Number" });
|
||||
}
|
||||
if (!db.objectStoreNames.contains(META_STORE)) {
|
||||
db.createObjectStore(META_STORE);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
async getCachedJobs() {
|
||||
if (!this.db)
|
||||
throw new Error("Database not initialized");
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([JOBS_STORE], "readonly");
|
||||
const store = transaction.objectStore(JOBS_STORE);
|
||||
const request = store.getAll();
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
async isCacheValid() {
|
||||
if (!this.db)
|
||||
return false;
|
||||
try {
|
||||
const metadata = await this.getMetadata();
|
||||
if (!metadata || !metadata.lastUpdated)
|
||||
return false;
|
||||
const age = Date.now() - metadata.lastUpdated;
|
||||
return age < CACHE_TTL;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
async saveJobs(jobs) {
|
||||
if (!this.db)
|
||||
throw new Error("Database not initialized");
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([JOBS_STORE, META_STORE], "readwrite");
|
||||
const jobsStore = transaction.objectStore(JOBS_STORE);
|
||||
const metaStore = transaction.objectStore(META_STORE);
|
||||
jobsStore.clear();
|
||||
jobs.forEach((job) => jobsStore.add(job));
|
||||
const metadata = {
|
||||
lastUpdated: Date.now(),
|
||||
version: DB_VERSION
|
||||
};
|
||||
metaStore.put(metadata, "main");
|
||||
transaction.oncomplete = () => resolve();
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
});
|
||||
}
|
||||
async refreshJobs() {
|
||||
try {
|
||||
let page = 1;
|
||||
let all = [];
|
||||
let totalPages = 1;
|
||||
while (page <= totalPages) {
|
||||
const url = `/api/jobs?page=${page}&perPage=${PAGE_SIZE}&sort=-Job_Number`;
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch jobs (status ${response.status})`);
|
||||
}
|
||||
const data = await response.json();
|
||||
const items = data.items || [];
|
||||
const reportedTotalPages = data.totalPages || 1;
|
||||
all = all.concat(items);
|
||||
totalPages = Math.max(totalPages, reportedTotalPages);
|
||||
page += 1;
|
||||
}
|
||||
await this.saveJobs(all);
|
||||
return all;
|
||||
} catch (err) {
|
||||
console.error("Failed to refresh jobs cache:", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
async getMetadata() {
|
||||
if (!this.db)
|
||||
return null;
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([META_STORE], "readonly");
|
||||
const store = transaction.objectStore(META_STORE);
|
||||
const request = store.get("main");
|
||||
request.onsuccess = () => resolve(request.result || null);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
async clearCache() {
|
||||
if (!this.db)
|
||||
throw new Error("Database not initialized");
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([JOBS_STORE, META_STORE], "readwrite");
|
||||
transaction.objectStore(JOBS_STORE).clear();
|
||||
transaction.objectStore(META_STORE).clear();
|
||||
transaction.oncomplete = () => resolve();
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
});
|
||||
}
|
||||
async getCacheAge() {
|
||||
const metadata = await this.getMetadata();
|
||||
if (!metadata || !metadata.lastUpdated)
|
||||
return null;
|
||||
return Date.now() - metadata.lastUpdated;
|
||||
}
|
||||
}
|
||||
var jobCache = new JobCache;
|
||||
|
||||
// home.ts
|
||||
var allJobs = [];
|
||||
var filteredJobs = [];
|
||||
var currentPage = 1;
|
||||
var perPage = 50;
|
||||
var isRefreshing = false;
|
||||
async function checkAuth() {
|
||||
try {
|
||||
const response = await fetch("/api/auth/status");
|
||||
const data = await response.json();
|
||||
if (!data.authenticated) {
|
||||
window.location.href = "/signin.html";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error("Auth check failed:", err);
|
||||
window.location.href = "/signin.html";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
async function fetchJobs() {
|
||||
try {
|
||||
await jobCache.initialize();
|
||||
const isCacheValid = await jobCache.isCacheValid();
|
||||
if (isCacheValid) {
|
||||
const cachedJobs = await jobCache.getCachedJobs();
|
||||
allJobs = cachedJobs;
|
||||
filteredJobs = [...allJobs];
|
||||
renderJobs();
|
||||
const cacheAge = await jobCache.getCacheAge();
|
||||
console.log(`Loaded ${allJobs.length} jobs from cache (age: ${Math.round((cacheAge || 0) / 1000)}s)`);
|
||||
refreshJobsInBackground();
|
||||
} else {
|
||||
const jobs = await jobCache.refreshJobs();
|
||||
allJobs = jobs;
|
||||
filteredJobs = [...allJobs];
|
||||
renderJobs();
|
||||
console.log(`Loaded ${allJobs.length} jobs from server (cache updated)`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch jobs:", err);
|
||||
const results = document.getElementById("results");
|
||||
results.innerHTML = '<div class="text-center text-red-600 p-4">Failed to load jobs. Please refresh the page.</div>';
|
||||
}
|
||||
}
|
||||
async function refreshJobsInBackground() {
|
||||
if (isRefreshing)
|
||||
return;
|
||||
isRefreshing = true;
|
||||
try {
|
||||
const freshJobs = await jobCache.refreshJobs();
|
||||
allJobs = freshJobs;
|
||||
applyFilters();
|
||||
console.log(`Background refresh complete (${freshJobs.length} jobs)`);
|
||||
} catch (err) {
|
||||
console.error("Background refresh failed:", err);
|
||||
} finally {
|
||||
isRefreshing = false;
|
||||
}
|
||||
}
|
||||
function renderJobs() {
|
||||
const results = document.getElementById("results");
|
||||
if (filteredJobs.length === 0) {
|
||||
results.innerHTML = '<div class="text-center text-gray-500 p-4">No jobs found</div>';
|
||||
return;
|
||||
}
|
||||
const start = (currentPage - 1) * perPage;
|
||||
const end = start + perPage;
|
||||
const pageJobs = filteredJobs.slice(start, end);
|
||||
results.innerHTML = pageJobs.map((job) => {
|
||||
const isActiveTrue = job.Active === true;
|
||||
const isActiveFalse = job.Active === false;
|
||||
const nameVal = job.Job_Full_Name || "";
|
||||
const contactVal = job.Contact_Person || "";
|
||||
return `
|
||||
<div class="bg-white rounded-lg p-2.5 border border-blue-100 shadow-sm cursor-pointer relative flex flex-col justify-center gap-1 hover:bg-gray-50 transition-colors" data-job-number="${escapeHtml(String(job.Job_Number || ""))}">
|
||||
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Job#</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(String(job.Job_Number || ""))}</div></div>
|
||||
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Job Name:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(String(nameVal))}</div></div>
|
||||
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Contact:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(String(contactVal))}</div></div>
|
||||
<div class="absolute top-2.5 right-2.5 w-3.5 h-3.5 rounded-full border-2 border-white shadow-sm" style="background:${isActiveTrue ? "green" : isActiveFalse ? "red" : "#cbd5e1"}"></div>
|
||||
</div>
|
||||
`;
|
||||
}).join("");
|
||||
results.querySelectorAll("[data-job-number]").forEach((el) => {
|
||||
el.addEventListener("click", (e) => {
|
||||
const jobNumber = e.currentTarget.dataset.jobNumber;
|
||||
if (jobNumber) {
|
||||
openDetails(jobNumber);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function openDetails(jobNumber) {
|
||||
window.location.href = `/details.html?job=${encodeURIComponent(jobNumber)}`;
|
||||
}
|
||||
function setupSearch() {
|
||||
const searchBox = document.getElementById("searchBox");
|
||||
searchBox.addEventListener("input", () => {
|
||||
const query = searchBox.value.toLowerCase().trim();
|
||||
if (!query) {
|
||||
filteredJobs = [...allJobs];
|
||||
} else {
|
||||
filteredJobs = allJobs.filter((job) => {
|
||||
return job.Job_Number?.toString().toLowerCase().includes(query) || job.Job_Full_Name?.toLowerCase().includes(query) || job.Job_Address?.toLowerCase().includes(query) || job.Contact_Person?.toLowerCase().includes(query) || job.Company_Client?.toLowerCase().includes(query) || job.Notes?.toLowerCase().includes(query) || job.Job_Status?.toLowerCase().includes(query) || job.Status?.toLowerCase().includes(query) || job.Estimator?.toLowerCase().includes(query);
|
||||
});
|
||||
}
|
||||
currentPage = 1;
|
||||
renderJobs();
|
||||
});
|
||||
}
|
||||
function setupFilters() {
|
||||
const filterToggle = document.getElementById("filterToggle");
|
||||
const filtersPanel = document.getElementById("filtersPanel");
|
||||
const clearFilters = document.getElementById("clearFilters");
|
||||
filterToggle.addEventListener("click", () => {
|
||||
const isHidden = filtersPanel.classList.contains("hidden");
|
||||
if (isHidden) {
|
||||
filtersPanel.classList.remove("hidden");
|
||||
filterToggle.textContent = "Hide Filters";
|
||||
} else {
|
||||
filtersPanel.classList.add("hidden");
|
||||
filterToggle.textContent = "Show Filters";
|
||||
}
|
||||
});
|
||||
const filterInputs = filtersPanel.querySelectorAll('input[type="checkbox"]');
|
||||
filterInputs.forEach((input) => {
|
||||
input.addEventListener("change", applyFilters);
|
||||
});
|
||||
clearFilters.addEventListener("click", () => {
|
||||
filterInputs.forEach((input) => {
|
||||
input.checked = false;
|
||||
});
|
||||
applyFilters();
|
||||
});
|
||||
}
|
||||
function applyFilters() {
|
||||
const divisions = Array.from(document.querySelectorAll(".filter-division:checked")).map((el) => el.value);
|
||||
const actives = Array.from(document.querySelectorAll(".filter-active:checked")).map((el) => el.value);
|
||||
const estimators = Array.from(document.querySelectorAll(".filter-estimator:checked")).map((el) => el.value.toLowerCase());
|
||||
const statuses = Array.from(document.querySelectorAll(".filter-status:checked")).map((el) => el.value);
|
||||
filteredJobs = allJobs.filter((job) => {
|
||||
const divisionVal = job.Job_Division || job.Division;
|
||||
const activeVal = job.Active === true || job.Active === "Yes" ? "Yes" : job.Active === false || job.Active === "No" ? "No" : "—";
|
||||
const estimatorVal = (job.Estimator || "").toLowerCase();
|
||||
const statusVal = job.Job_Status || job.Status;
|
||||
if (divisions.length > 0 && !divisions.some((d) => divisionVal?.startsWith(d)))
|
||||
return false;
|
||||
if (actives.length > 0 && !actives.includes(activeVal))
|
||||
return false;
|
||||
if (estimators.length > 0 && !estimators.some((e) => estimatorVal.includes(e)))
|
||||
return false;
|
||||
if (statuses.length > 0 && !statuses.includes(statusVal || ""))
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
currentPage = 1;
|
||||
renderJobs();
|
||||
}
|
||||
function setupVoiceSearch() {
|
||||
const voiceBtn = document.getElementById("voiceSearchBtn");
|
||||
const micIcon = document.getElementById("micIcon");
|
||||
const micRecordingIcon = document.getElementById("micRecordingIcon");
|
||||
const searchBox = document.getElementById("searchBox");
|
||||
if (!("webkitSpeechRecognition" in window) && !("SpeechRecognition" in window)) {
|
||||
voiceBtn.style.display = "none";
|
||||
return;
|
||||
}
|
||||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
const recognition = new SpeechRecognition;
|
||||
recognition.continuous = false;
|
||||
recognition.interimResults = false;
|
||||
voiceBtn.addEventListener("click", () => {
|
||||
recognition.start();
|
||||
micIcon.classList.add("hidden");
|
||||
micRecordingIcon.classList.remove("hidden");
|
||||
});
|
||||
recognition.onresult = (event) => {
|
||||
const transcript = event.results[0][0].transcript;
|
||||
searchBox.value = transcript;
|
||||
searchBox.dispatchEvent(new Event("input"));
|
||||
};
|
||||
recognition.onend = () => {
|
||||
micIcon.classList.remove("hidden");
|
||||
micRecordingIcon.classList.add("hidden");
|
||||
};
|
||||
recognition.onerror = () => {
|
||||
micIcon.classList.remove("hidden");
|
||||
micRecordingIcon.classList.add("hidden");
|
||||
};
|
||||
}
|
||||
function setupSignOut() {
|
||||
const signOutBtn = document.getElementById("signOutBtn");
|
||||
signOutBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
await fetch("/api/auth/logout", { method: "POST" });
|
||||
window.location.href = "/signin.html";
|
||||
} catch (err) {
|
||||
console.error("Sign out failed:", err);
|
||||
window.location.href = "/signin.html";
|
||||
}
|
||||
});
|
||||
}
|
||||
async function init() {
|
||||
const isAuthenticated = await checkAuth();
|
||||
if (!isAuthenticated)
|
||||
return;
|
||||
setupSearch();
|
||||
setupFilters();
|
||||
setupVoiceSearch();
|
||||
setupSignOut();
|
||||
await fetchJobs();
|
||||
}
|
||||
function escapeHtml(str) {
|
||||
return str.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
||||
}
|
||||
init();
|
||||
@@ -1,330 +0,0 @@
|
||||
// PERMANENT: Home view - Jobs listing with zero-delay loading via IndexedDB cache
|
||||
// Wired to tokenService via backend API routes
|
||||
// Visual match to Mode1Test but uses new architecture
|
||||
//
|
||||
// Dependencies:
|
||||
// - Backend /api/auth/status for authentication check
|
||||
// - Backend /api/jobs for job data
|
||||
// - job-cache.ts for IndexedDB caching
|
||||
//
|
||||
// Features:
|
||||
// - Zero-delay load (displays cached jobs instantly)
|
||||
// - Background refresh after displaying cache
|
||||
// - Search with voice support (operates on cached data)
|
||||
// - Filters (division, active, estimator, status)
|
||||
// - Pagination
|
||||
// - Click job to open Details view
|
||||
|
||||
import { jobCache, type JobCard } from './job-cache.js';
|
||||
|
||||
interface Job {
|
||||
Job_Number: string;
|
||||
Job_Name?: string;
|
||||
Job_Full_Name?: string;
|
||||
Job_Address?: string;
|
||||
Job_Division?: string;
|
||||
Active?: boolean | string;
|
||||
Status?: string;
|
||||
Job_Status?: string;
|
||||
Estimator?: string;
|
||||
Contact_Person?: string;
|
||||
Company_Client?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
let allJobs: Job[] = [];
|
||||
let filteredJobs: Job[] = [];
|
||||
let currentPage = 1;
|
||||
const perPage = 50;
|
||||
let isRefreshing = false;
|
||||
|
||||
// Check authentication on page load
|
||||
async function checkAuth(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch('/api/auth/status');
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.authenticated) {
|
||||
// Redirect to login
|
||||
window.location.href = '/signin.html';
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Auth check failed:', err);
|
||||
window.location.href = '/signin.html';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch jobs from backend (or cache)
|
||||
async function fetchJobs(): Promise<void> {
|
||||
try {
|
||||
// Initialize cache
|
||||
await jobCache.initialize();
|
||||
|
||||
// Check if cache is valid
|
||||
const isCacheValid = await jobCache.isCacheValid();
|
||||
|
||||
if (isCacheValid) {
|
||||
// Display cached jobs immediately (zero delay)
|
||||
const cachedJobs = await jobCache.getCachedJobs();
|
||||
allJobs = cachedJobs;
|
||||
filteredJobs = [...allJobs];
|
||||
renderJobs();
|
||||
|
||||
// Show cache age in console for debugging
|
||||
const cacheAge = await jobCache.getCacheAge();
|
||||
console.log(`Loaded ${allJobs.length} jobs from cache (age: ${Math.round((cacheAge || 0) / 1000)}s)`);
|
||||
|
||||
// Refresh in background (don't block UI)
|
||||
refreshJobsInBackground();
|
||||
} else {
|
||||
// No valid cache, fetch fresh data (no progress bar)
|
||||
const jobs = await jobCache.refreshJobs();
|
||||
allJobs = jobs;
|
||||
filteredJobs = [...allJobs];
|
||||
|
||||
renderJobs();
|
||||
console.log(`Loaded ${allJobs.length} jobs from server (cache updated)`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch jobs:', err);
|
||||
|
||||
// Show error message
|
||||
const results = document.getElementById('results') as HTMLDivElement;
|
||||
results.innerHTML = '<div class="text-center text-red-600 p-4">Failed to load jobs. Please refresh the page.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Background refresh (silent, doesn't interrupt user)
|
||||
async function refreshJobsInBackground(): Promise<void> {
|
||||
if (isRefreshing) return;
|
||||
|
||||
isRefreshing = true;
|
||||
|
||||
try {
|
||||
const freshJobs = await jobCache.refreshJobs();
|
||||
|
||||
// Update in memory without interrupting user
|
||||
allJobs = freshJobs;
|
||||
|
||||
// Reapply current filters and search
|
||||
applyFilters();
|
||||
|
||||
console.log(`Background refresh complete (${freshJobs.length} jobs)`);
|
||||
} catch (err) {
|
||||
console.error('Background refresh failed:', err);
|
||||
} finally {
|
||||
isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Render jobs to DOM
|
||||
function renderJobs(): void {
|
||||
const results = document.getElementById('results') as HTMLDivElement;
|
||||
|
||||
if (filteredJobs.length === 0) {
|
||||
results.innerHTML = '<div class="text-center text-gray-500 p-4">No jobs found</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const start = (currentPage - 1) * perPage;
|
||||
const end = start + perPage;
|
||||
const pageJobs = filteredJobs.slice(start, end);
|
||||
|
||||
results.innerHTML = pageJobs.map(job => {
|
||||
const isActiveTrue = job.Active === true;
|
||||
const isActiveFalse = job.Active === false;
|
||||
const nameVal = job.Job_Full_Name || '';
|
||||
const contactVal = job.Contact_Person || '';
|
||||
|
||||
return `
|
||||
<div class="bg-white rounded-lg p-2.5 border border-blue-100 shadow-sm cursor-pointer relative flex flex-col justify-center gap-1 hover:bg-gray-50 transition-colors" data-job-number="${escapeHtml(String(job.Job_Number||''))}">
|
||||
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Job#</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(String(job.Job_Number||''))}</div></div>
|
||||
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Job Name:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(String(nameVal))}</div></div>
|
||||
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Contact:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(String(contactVal))}</div></div>
|
||||
<div class="absolute top-2.5 right-2.5 w-3.5 h-3.5 rounded-full border-2 border-white shadow-sm" style="background:${isActiveTrue?'green':(isActiveFalse?'red':'#cbd5e1')}"></div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Add click handlers
|
||||
results.querySelectorAll('[data-job-number]').forEach(el => {
|
||||
el.addEventListener('click', (e) => {
|
||||
const jobNumber = (e.currentTarget as HTMLElement).dataset.jobNumber;
|
||||
if (jobNumber) {
|
||||
openDetails(jobNumber);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Open Details view
|
||||
function openDetails(jobNumber: string): void {
|
||||
// Navigate to details view with job number
|
||||
window.location.href = `/details.html?job=${encodeURIComponent(jobNumber)}`;
|
||||
}
|
||||
|
||||
// Search functionality
|
||||
function setupSearch(): void {
|
||||
const searchBox = document.getElementById('searchBox') as HTMLInputElement;
|
||||
|
||||
searchBox.addEventListener('input', () => {
|
||||
const query = searchBox.value.toLowerCase().trim();
|
||||
|
||||
if (!query) {
|
||||
filteredJobs = [...allJobs];
|
||||
} else {
|
||||
filteredJobs = allJobs.filter(job => {
|
||||
return (
|
||||
job.Job_Number?.toString().toLowerCase().includes(query) ||
|
||||
job.Job_Full_Name?.toLowerCase().includes(query) ||
|
||||
job.Job_Address?.toLowerCase().includes(query) ||
|
||||
job.Contact_Person?.toLowerCase().includes(query) ||
|
||||
job.Company_Client?.toLowerCase().includes(query) ||
|
||||
job.Notes?.toLowerCase().includes(query) ||
|
||||
job.Job_Status?.toLowerCase().includes(query) ||
|
||||
job.Status?.toLowerCase().includes(query) ||
|
||||
job.Estimator?.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
currentPage = 1;
|
||||
renderJobs();
|
||||
});
|
||||
}
|
||||
|
||||
// Filter functionality
|
||||
function setupFilters(): void {
|
||||
const filterToggle = document.getElementById('filterToggle') as HTMLButtonElement;
|
||||
const filtersPanel = document.getElementById('filtersPanel') as HTMLDivElement;
|
||||
const clearFilters = document.getElementById('clearFilters') as HTMLButtonElement;
|
||||
|
||||
filterToggle.addEventListener('click', () => {
|
||||
const isHidden = filtersPanel.classList.contains('hidden');
|
||||
if (isHidden) {
|
||||
filtersPanel.classList.remove('hidden');
|
||||
filterToggle.textContent = 'Hide Filters';
|
||||
} else {
|
||||
filtersPanel.classList.add('hidden');
|
||||
filterToggle.textContent = 'Show Filters';
|
||||
}
|
||||
});
|
||||
|
||||
// Apply filters on change
|
||||
const filterInputs = filtersPanel.querySelectorAll('input[type="checkbox"]');
|
||||
filterInputs.forEach(input => {
|
||||
input.addEventListener('change', applyFilters);
|
||||
});
|
||||
|
||||
clearFilters.addEventListener('click', () => {
|
||||
filterInputs.forEach(input => {
|
||||
(input as HTMLInputElement).checked = false;
|
||||
});
|
||||
applyFilters();
|
||||
});
|
||||
}
|
||||
|
||||
function applyFilters(): void {
|
||||
const divisions = Array.from(document.querySelectorAll('.filter-division:checked')).map(el => (el as HTMLInputElement).value);
|
||||
const actives = Array.from(document.querySelectorAll('.filter-active:checked')).map(el => (el as HTMLInputElement).value);
|
||||
const estimators = Array.from(document.querySelectorAll('.filter-estimator:checked')).map(el => (el as HTMLInputElement).value.toLowerCase());
|
||||
const statuses = Array.from(document.querySelectorAll('.filter-status:checked')).map(el => (el as HTMLInputElement).value);
|
||||
|
||||
filteredJobs = allJobs.filter(job => {
|
||||
const divisionVal = job.Job_Division || job.Division;
|
||||
const activeVal = job.Active === true || job.Active === 'Yes' ? 'Yes' : job.Active === false || job.Active === 'No' ? 'No' : '—';
|
||||
const estimatorVal = (job.Estimator || '').toLowerCase();
|
||||
const statusVal = job.Job_Status || job.Status;
|
||||
|
||||
if (divisions.length > 0 && !divisions.some(d => divisionVal?.startsWith(d))) return false;
|
||||
if (actives.length > 0 && !actives.includes(activeVal)) return false;
|
||||
if (estimators.length > 0 && !estimators.some(e => estimatorVal.includes(e))) return false;
|
||||
if (statuses.length > 0 && !statuses.includes(statusVal || '')) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
currentPage = 1;
|
||||
renderJobs();
|
||||
}
|
||||
|
||||
// Voice search
|
||||
function setupVoiceSearch(): void {
|
||||
const voiceBtn = document.getElementById('voiceSearchBtn') as HTMLButtonElement;
|
||||
const micIcon = document.getElementById('micIcon') as SVGElement;
|
||||
const micRecordingIcon = document.getElementById('micRecordingIcon') as SVGElement;
|
||||
const searchBox = document.getElementById('searchBox') as HTMLInputElement;
|
||||
|
||||
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
|
||||
voiceBtn.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
|
||||
const recognition = new SpeechRecognition();
|
||||
recognition.continuous = false;
|
||||
recognition.interimResults = false;
|
||||
|
||||
voiceBtn.addEventListener('click', () => {
|
||||
recognition.start();
|
||||
micIcon.classList.add('hidden');
|
||||
micRecordingIcon.classList.remove('hidden');
|
||||
});
|
||||
|
||||
recognition.onresult = (event: any) => {
|
||||
const transcript = event.results[0][0].transcript;
|
||||
searchBox.value = transcript;
|
||||
searchBox.dispatchEvent(new Event('input'));
|
||||
};
|
||||
|
||||
recognition.onend = () => {
|
||||
micIcon.classList.remove('hidden');
|
||||
micRecordingIcon.classList.add('hidden');
|
||||
};
|
||||
|
||||
recognition.onerror = () => {
|
||||
micIcon.classList.remove('hidden');
|
||||
micRecordingIcon.classList.add('hidden');
|
||||
};
|
||||
}
|
||||
|
||||
// Sign out
|
||||
function setupSignOut(): void {
|
||||
const signOutBtn = document.getElementById('signOutBtn') as HTMLButtonElement;
|
||||
|
||||
signOutBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
await fetch('/api/auth/logout', { method: 'POST' });
|
||||
window.location.href = '/signin.html';
|
||||
} catch (err) {
|
||||
console.error('Sign out failed:', err);
|
||||
window.location.href = '/signin.html';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
async function init(): Promise<void> {
|
||||
const isAuthenticated = await checkAuth();
|
||||
if (!isAuthenticated) return;
|
||||
|
||||
setupSearch();
|
||||
setupFilters();
|
||||
setupVoiceSearch();
|
||||
setupSignOut();
|
||||
|
||||
await fetchJobs();
|
||||
}
|
||||
|
||||
function escapeHtml(str: string): string {
|
||||
return str
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>');
|
||||
}
|
||||
|
||||
// Start
|
||||
init();
|
||||
@@ -1,129 +0,0 @@
|
||||
// job-cache.ts
|
||||
var DB_NAME = "JobInfoCache";
|
||||
var DB_VERSION = 2;
|
||||
var JOBS_STORE = "jobs";
|
||||
var META_STORE = "metadata";
|
||||
var CACHE_TTL = 5 * 60 * 1000;
|
||||
var PAGE_SIZE = 500;
|
||||
|
||||
class JobCache {
|
||||
db = null;
|
||||
async initialize() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result;
|
||||
resolve();
|
||||
};
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
if (!db.objectStoreNames.contains(JOBS_STORE)) {
|
||||
db.createObjectStore(JOBS_STORE, { keyPath: "Job_Number" });
|
||||
}
|
||||
if (!db.objectStoreNames.contains(META_STORE)) {
|
||||
db.createObjectStore(META_STORE);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
async getCachedJobs() {
|
||||
if (!this.db)
|
||||
throw new Error("Database not initialized");
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([JOBS_STORE], "readonly");
|
||||
const store = transaction.objectStore(JOBS_STORE);
|
||||
const request = store.getAll();
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
async isCacheValid() {
|
||||
if (!this.db)
|
||||
return false;
|
||||
try {
|
||||
const metadata = await this.getMetadata();
|
||||
if (!metadata || !metadata.lastUpdated)
|
||||
return false;
|
||||
const age = Date.now() - metadata.lastUpdated;
|
||||
return age < CACHE_TTL;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
async saveJobs(jobs) {
|
||||
if (!this.db)
|
||||
throw new Error("Database not initialized");
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([JOBS_STORE, META_STORE], "readwrite");
|
||||
const jobsStore = transaction.objectStore(JOBS_STORE);
|
||||
const metaStore = transaction.objectStore(META_STORE);
|
||||
jobsStore.clear();
|
||||
jobs.forEach((job) => jobsStore.add(job));
|
||||
const metadata = {
|
||||
lastUpdated: Date.now(),
|
||||
version: DB_VERSION
|
||||
};
|
||||
metaStore.put(metadata, "main");
|
||||
transaction.oncomplete = () => resolve();
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
});
|
||||
}
|
||||
async refreshJobs() {
|
||||
try {
|
||||
let page = 1;
|
||||
let all = [];
|
||||
let totalPages = 1;
|
||||
while (page <= totalPages) {
|
||||
const url = `/api/jobs?page=${page}&perPage=${PAGE_SIZE}&sort=-Job_Number`;
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch jobs (status ${response.status})`);
|
||||
}
|
||||
const data = await response.json();
|
||||
const items = data.items || [];
|
||||
const reportedTotalPages = data.totalPages || 1;
|
||||
all = all.concat(items);
|
||||
totalPages = Math.max(totalPages, reportedTotalPages);
|
||||
page += 1;
|
||||
}
|
||||
await this.saveJobs(all);
|
||||
return all;
|
||||
} catch (err) {
|
||||
console.error("Failed to refresh jobs cache:", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
async getMetadata() {
|
||||
if (!this.db)
|
||||
return null;
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([META_STORE], "readonly");
|
||||
const store = transaction.objectStore(META_STORE);
|
||||
const request = store.get("main");
|
||||
request.onsuccess = () => resolve(request.result || null);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
async clearCache() {
|
||||
if (!this.db)
|
||||
throw new Error("Database not initialized");
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([JOBS_STORE, META_STORE], "readwrite");
|
||||
transaction.objectStore(JOBS_STORE).clear();
|
||||
transaction.objectStore(META_STORE).clear();
|
||||
transaction.oncomplete = () => resolve();
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
});
|
||||
}
|
||||
async getCacheAge() {
|
||||
const metadata = await this.getMetadata();
|
||||
if (!metadata || !metadata.lastUpdated)
|
||||
return null;
|
||||
return Date.now() - metadata.lastUpdated;
|
||||
}
|
||||
}
|
||||
var jobCache = new JobCache;
|
||||
export {
|
||||
jobCache
|
||||
};
|
||||
@@ -1,189 +0,0 @@
|
||||
// PERMANENT: IndexedDB cache for jobs data
|
||||
// Provides lightning-fast job listing with zero-delay loading
|
||||
//
|
||||
// Strategy:
|
||||
// - Display cached jobs instantly on page load (0ms delay)
|
||||
// - Fetch fresh data in background after displaying cache
|
||||
// - Cache TTL: 5 minutes (configurable)
|
||||
// - Stores minimal fields for card display only
|
||||
//
|
||||
// Fields cached: Job_Number, Job_Name, Division, Active, Status, Estimator
|
||||
//
|
||||
// Usage:
|
||||
// await jobCache.initialize();
|
||||
// const jobs = await jobCache.getCachedJobs(); // Instant
|
||||
// await jobCache.refreshJobs(); // Background refresh
|
||||
|
||||
export interface JobCard {
|
||||
Job_Number: string;
|
||||
Job_Name: string;
|
||||
Division: string;
|
||||
Active: string;
|
||||
Status: string;
|
||||
Estimator: string;
|
||||
}
|
||||
|
||||
interface CacheMetadata {
|
||||
lastUpdated: number;
|
||||
version: number;
|
||||
}
|
||||
|
||||
const DB_NAME = 'JobInfoCache';
|
||||
const DB_VERSION = 2; // bump to invalidate old minimal-field cache
|
||||
const JOBS_STORE = 'jobs';
|
||||
const META_STORE = 'metadata';
|
||||
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||||
const PAGE_SIZE = 500; // match Mode1 bulk fetch
|
||||
|
||||
class JobCache {
|
||||
private db: IDBDatabase | null = null;
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result;
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
|
||||
// Create jobs store with Job_Number as key
|
||||
if (!db.objectStoreNames.contains(JOBS_STORE)) {
|
||||
db.createObjectStore(JOBS_STORE, { keyPath: 'Job_Number' });
|
||||
}
|
||||
|
||||
// Create metadata store
|
||||
if (!db.objectStoreNames.contains(META_STORE)) {
|
||||
db.createObjectStore(META_STORE);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getCachedJobs(): Promise<JobCard[]> {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db!.transaction([JOBS_STORE], 'readonly');
|
||||
const store = transaction.objectStore(JOBS_STORE);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
async isCacheValid(): Promise<boolean> {
|
||||
if (!this.db) return false;
|
||||
|
||||
try {
|
||||
const metadata = await this.getMetadata();
|
||||
if (!metadata || !metadata.lastUpdated) return false;
|
||||
|
||||
const age = Date.now() - metadata.lastUpdated;
|
||||
return age < CACHE_TTL;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async saveJobs(jobs: JobCard[]): Promise<void> {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db!.transaction([JOBS_STORE, META_STORE], 'readwrite');
|
||||
const jobsStore = transaction.objectStore(JOBS_STORE);
|
||||
const metaStore = transaction.objectStore(META_STORE);
|
||||
|
||||
// Clear existing jobs
|
||||
jobsStore.clear();
|
||||
|
||||
// Save new jobs
|
||||
jobs.forEach(job => jobsStore.add(job));
|
||||
|
||||
// Update metadata
|
||||
const metadata: CacheMetadata = {
|
||||
lastUpdated: Date.now(),
|
||||
version: DB_VERSION
|
||||
};
|
||||
metaStore.put(metadata, 'main');
|
||||
|
||||
transaction.oncomplete = () => resolve();
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
});
|
||||
}
|
||||
|
||||
async refreshJobs(): Promise<JobCard[]> {
|
||||
try {
|
||||
// Fetch fresh data from backend with pagination to mirror Mode1 behavior
|
||||
let page = 1;
|
||||
let all: JobCard[] = [];
|
||||
let totalPages = 1;
|
||||
|
||||
while (page <= totalPages) {
|
||||
const url = `/api/jobs?page=${page}&perPage=${PAGE_SIZE}&sort=-Job_Number`;
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch jobs (status ${response.status})`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const items: JobCard[] = data.items || [];
|
||||
const reportedTotalPages = data.totalPages || 1;
|
||||
|
||||
all = all.concat(items);
|
||||
totalPages = Math.max(totalPages, reportedTotalPages);
|
||||
page += 1;
|
||||
}
|
||||
|
||||
// Save to cache
|
||||
await this.saveJobs(all);
|
||||
|
||||
return all;
|
||||
} catch (err) {
|
||||
console.error('Failed to refresh jobs cache:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async getMetadata(): Promise<CacheMetadata | null> {
|
||||
if (!this.db) return null;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db!.transaction([META_STORE], 'readonly');
|
||||
const store = transaction.objectStore(META_STORE);
|
||||
const request = store.get('main');
|
||||
|
||||
request.onsuccess = () => resolve(request.result || null);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
async clearCache(): Promise<void> {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db!.transaction([JOBS_STORE, META_STORE], 'readwrite');
|
||||
|
||||
transaction.objectStore(JOBS_STORE).clear();
|
||||
transaction.objectStore(META_STORE).clear();
|
||||
|
||||
transaction.oncomplete = () => resolve();
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
});
|
||||
}
|
||||
|
||||
async getCacheAge(): Promise<number | null> {
|
||||
const metadata = await this.getMetadata();
|
||||
if (!metadata || !metadata.lastUpdated) return null;
|
||||
|
||||
return Date.now() - metadata.lastUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
export const jobCache = new JobCache();
|
||||
@@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Manager</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="font-sans bg-gray-100 p-4">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<h1 class="text-2xl font-bold mb-4 text-blue-600">Manager</h1>
|
||||
<p class="text-gray-600">Manager view - Under construction</p>
|
||||
<a href="/home.html" class="text-blue-600 underline mt-4 inline-block">← Back to Home</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Navigator</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="font-sans bg-gray-100 p-4">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<h1 class="text-2xl font-bold mb-4 text-blue-600">Navigator</h1>
|
||||
<p class="text-gray-600">Navigator view - Under construction</p>
|
||||
<a href="/home.html" class="text-blue-600 underline mt-4 inline-block">← Back to Home</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,87 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sign In - Mode2Test</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script>
|
||||
</head>
|
||||
<body class="font-sans bg-gradient-to-br from-blue-500 to-indigo-600 min-h-screen flex items-center justify-center">
|
||||
<div class="bg-white p-12 rounded-2xl shadow-2xl text-center max-w-md w-[90%]">
|
||||
<h1 class="text-gray-800 mb-2 text-3xl font-bold">Mode2Test</h1>
|
||||
<p class="text-gray-600 mb-8">Sign in with your Microsoft account</p>
|
||||
|
||||
<button id="loginBtn" onclick="handleLogin()" class="w-full bg-blue-600 text-white border-none py-3.5 px-8 text-base rounded-md cursor-pointer transition-colors duration-200 font-medium hover:bg-blue-700 disabled:bg-gray-400">
|
||||
Sign in with Microsoft
|
||||
</button>
|
||||
|
||||
<div id="error" class="text-red-600 mt-4 hidden"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const pb = new PocketBase('https://pocketbase.ccllc.pro');
|
||||
|
||||
async function handleLogin() {
|
||||
const btn = document.getElementById('loginBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Signing in...';
|
||||
document.getElementById('error').classList.add('hidden');
|
||||
|
||||
try {
|
||||
const authData = await pb.collection('users').authWithOAuth2({ provider: 'microsoft' });
|
||||
|
||||
// Log what we got for debugging
|
||||
console.log('[Auth] OAuth response:', authData);
|
||||
console.log('[Auth] Meta:', authData?.meta);
|
||||
|
||||
// Try to extract graph token from various possible locations
|
||||
const graphToken =
|
||||
authData?.meta?.accessToken ||
|
||||
authData?.meta?.access_token ||
|
||||
authData?.meta?.graphAccessToken ||
|
||||
authData?.meta?.graph_token ||
|
||||
authData?.meta?.graphToken ||
|
||||
authData?.meta?.rawUser?.access_token ||
|
||||
'';
|
||||
|
||||
console.log('[Auth] Graph token found:', !!graphToken);
|
||||
|
||||
// Send to backend to store
|
||||
const saveResponse = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ pbToken: authData.token, graphToken: graphToken || undefined })
|
||||
});
|
||||
|
||||
if (!saveResponse.ok) {
|
||||
throw new Error('Failed to save tokens to backend');
|
||||
}
|
||||
|
||||
console.log('[Auth] Tokens saved to backend, redirecting to home');
|
||||
|
||||
// Clear PocketBase local storage to avoid conflicts
|
||||
pb.authStore.clear();
|
||||
|
||||
// Redirect to home page
|
||||
window.location.href = '/home.html';
|
||||
} catch (err) {
|
||||
document.getElementById('error').textContent = err.message || 'Login failed';
|
||||
document.getElementById('error').classList.remove('hidden');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Sign in with Microsoft';
|
||||
}
|
||||
}
|
||||
|
||||
// Check if already logged in via backend only
|
||||
fetch('/api/auth/status')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.authenticated) {
|
||||
window.location.href = '/home.html';
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Viewer</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="font-sans bg-gray-100 p-4">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<h1 class="text-2xl font-bold mb-4 text-blue-600">Viewer</h1>
|
||||
<p class="text-gray-600">Viewer view - Under construction</p>
|
||||
<a href="/home.html" class="text-blue-600 underline mt-4 inline-block">← Back to Home</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"name": "mode2test",
|
||||
"version": "1.0.0-beta1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run backend/server.ts",
|
||||
"start": "bun run backend/server.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"hono": "4.11.4",
|
||||
"dotenv": "17.2.3",
|
||||
"pocketbase": "0.26.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"pbToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJleHAiOjE4MDAyMjIzMjgsImlkIjoiMWhiaWY5ZGJxbmc4Nmg5IiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.lkHN__Eh6ECrVtlQmCjl7sVZ1B2KMg75FnRjUNzsUtU",
|
||||
"pbTokenExpiry": 1769291128701,
|
||||
"graphToken": "eyJ0eXAiOiJKV1QiLCJub25jZSI6InRSOVQyM1h4RXhKRHN1YXJuSkdmcmYzd2FDS094TF9xWUkwd2dLUjhMVTQiLCJhbGciOiJSUzI1NiIsIng1dCI6IlBjWDk4R1g0MjBUMVg2c0JEa3poUW1xZ3dNVSIsImtpZCI6IlBjWDk4R1g0MjBUMVg2c0JEa3poUW1xZ3dNVSJ9.eyJhdWQiOiIwMDAwMDAwMy0wMDAwLTAwMDAtYzAwMC0wMDAwMDAwMDAwMDAiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC8zZmQ5N2VhNy1iMTI0LTQxZjEtODU1Zi01MmQ4YWMzYjE2YzcvIiwiaWF0IjoxNzY4Njc4Mjk1LCJuYmYiOjE3Njg2NzgyOTUsImV4cCI6MTc2ODY4MjIxMSwiYWNjdCI6MCwiYWNyIjoiMSIsImFjcnMiOlsicDEiXSwiYWlvIjoiQWRRQUsvOGJBQUFBMW1BM04rN1pMMFU5OURDcUtsZ2JiOE1pSlhYRXZOT0dMQ2FjVllmdWZMTVM3b2N1emlvdkVmd1dZQnRBV2VyK0U5bGZxUU9FZ1lVZnBOODAxK2srMXBqOW5NRHlqdHhuRWVkaWI5Q1hZbThTMmFLeVNmZWFaQTFFZVdOa2VqWkduN2ZRQSsxLzE5ckVKcWlqMlRRRWZBMm9RTTlDamwxSE5QbGpGQ05iTHZwcmtETFIrT29CVlM4VlBXZi8zTU00Q1pzQ2ZiZnNRb3FuSGpzUHBUTzdFMzk1Qk5UQVhveXoxL1lRTkFucVhmNHRGajVaMUZVWW9jY1hES0NubEc0NXBmTVMxSHMreE1ZWVdnT3h0MkhJeVE9PSIsImFtciI6WyJyc2EiLCJtZmEiXSwiYXBwX2Rpc3BsYXluYW1lIjoiSm9iX0luZm9fU3luYyIsImFwcGlkIjoiM2M4NDZlNzEtOTYwOS00MGUxLWI0NTgtMGViODA1ZTIxYjlmIiwiYXBwaWRhY3IiOiIxIiwiZGV2aWNlaWQiOiJkZjdmZmUyOC04NDQ2LTQ5NjUtOWNlMC1jNDJiNjIxZmFiYjciLCJmYW1pbHlfbmFtZSI6IkV3aW5nIiwiZ2l2ZW5fbmFtZSI6IkFsZm9uc28iLCJpZHR5cCI6InVzZXIiLCJpcGFkZHIiOiIxNDMuMTA1LjI0Ny4xNDYiLCJuYW1lIjoiQWxmb25zbyBFd2luZyIsIm9pZCI6IjFhNmU5ZDEwLTEzOGEtNDNlNC1hMDllLTFmNTA0NjMxNDhjOSIsInBsYXRmIjoiMyIsInB1aWQiOiIxMDAzMjAwNTFGRUI4QUMwIiwicmgiOiIxLkFUVUFwMzdaUHlTeDhVR0ZYMUxZckRzV3h3TUFBQUFBQUFBQXdBQUFBQUFBQUFCbEFVazFBQS4iLCJzY3AiOiJDaGFubmVsLlJlYWRCYXNpYy5BbGwgQ2hhbm5lbE1lbWJlci5SZWFkLkFsbCBDaGFubmVsTWVtYmVyLlJlYWRXcml0ZS5BbGwgQ2hhbm5lbE1lc3NhZ2UuRWRpdCBDaGFubmVsTWVzc2FnZS5SZWFkLkFsbCBDaGFubmVsTWVzc2FnZS5SZWFkV3JpdGUgQ2hhbm5lbE1lc3NhZ2UuU2VuZCBDaGFubmVsU2V0dGluZ3MuUmVhZC5BbGwgQ2hhbm5lbFNldHRpbmdzLlJlYWRXcml0ZS5BbGwgQ2hhdE1lc3NhZ2UuUmVhZCBDaGF0TWVzc2FnZS5TZW5kIERpcmVjdG9yeS5BY2Nlc3NBc1VzZXIuQWxsIERpcmVjdG9yeS5SZWFkLkFsbCBEaXJlY3RvcnkuUmVhZFdyaXRlLkFsbCBlbWFpbCBGaWxlcy5SZWFkV3JpdGUuQWxsIEdyb3VwLlJlYWQuQWxsIEdyb3VwLlJlYWRXcml0ZS5BbGwgR3JvdXAtQ29udmVyc2F0aW9uLlJlYWQuQWxsIEdyb3VwLUNvbnZlcnNhdGlvbi5SZWFkV3JpdGUuQWxsIEdyb3VwTWVtYmVyLlJlYWQuQWxsIEdyb3VwTWVtYmVyLlJlYWRXcml0ZS5BbGwgR3JvdXBTZXR0aW5ncy5SZWFkLkFsbCBHcm91cFNldHRpbmdzLlJlYWRXcml0ZS5BbGwgcHJvZmlsZSBTaXRlcy5SZWFkV3JpdGUuQWxsIFRhc2tzLlJlYWQgVGFza3MuUmVhZC5TaGFyZWQgVGFza3MuUmVhZFdyaXRlIFRhc2tzLlJlYWRXcml0ZS5TaGFyZWQgVGVhbXNBY3Rpdml0eS5SZWFkIFRlYW1TZXR0aW5ncy5SZWFkLkFsbCBUZWFtU2V0dGluZ3MuUmVhZFdyaXRlLkFsbCBUZWFtc1BvbGljeVVzZXJBc3NpZ24uUmVhZFdyaXRlLkFsbCBVc2VyLlJlYWQgb3BlbmlkIiwic2lkIjoiMDA4ZDJjYjktODU5MS1jMDdkLTVhMjQtYzk2MjhkYzc1ZDIzIiwic2lnbmluX3N0YXRlIjpbImttc2kiXSwic3ViIjoiQXNWc0d6U3NUTmFWblJiTWxDWnlWeWtZMlh6NEhyS0RpRWx1eUVaWXZTdyIsInRlbmFudF9yZWdpb25fc2NvcGUiOiJOQSIsInRpZCI6IjNmZDk3ZWE3LWIxMjQtNDFmMS04NTVmLTUyZDhhYzNiMTZjNyIsInVuaXF1ZV9uYW1lIjoiYWV3aW5nQGNhcmRvemEuY29uc3RydWN0aW9uIiwidXBuIjoiYWV3aW5nQGNhcmRvemEuY29uc3RydWN0aW9uIiwidXRpIjoiVDljb0FWV3ZXVTJTbTBGLUFqS3lBQSIsInZlciI6IjEuMCIsIndpZHMiOlsiZmU5MzBiZTctNWU2Mi00N2RiLTkxYWYtOThjM2E0OWEzOGIxIiwiNjJlOTAzOTQtNjlmNS00MjM3LTkxOTAtMDEyMTc3MTQ1ZTEwIiwiMjkyMzJjZGYtOTMyMy00MmZkLWFkZTItMWQwOTdhZjNlNGRlIiwiNjkwOTEyNDYtMjBlOC00YTU2LWFhNGQtMDY2MDc1YjJhN2E4IiwiYjc5ZmJmNGQtM2VmOS00Njg5LTgxNDMtNzZiMTk0ZTg1NTA5Il0sInhtc19hY2QiOjE3NjM0MDU4NjQsInhtc19hY3RfZmN0IjoiOSAzIiwieG1zX2Z0ZCI6IkYyUXJpNWN3ZEhLQzVuZzFrYjdsU1huX3BlWXFaR3JUT21KeDYyN21LZUVCZFhOM1pYTjBNeTFrYzIxeiIsInhtc19pZHJlbCI6IjggMSIsInhtc19zdCI6eyJzdWIiOiJBSGY2UURxRXRkdkR5em1aMHlMMnRmZEktSW1ZUnFGVElRYUxzN2JKdlBjIn0sInhtc19zdWJfZmN0IjoiMyA2IiwieG1zX3RjZHQiOjE1NTg1NDYwMDcsInhtc190bnRfZmN0IjoiNiAzIn0.UqBTFe_GkrUqp9oHgjO93KtXckuDH91RQ03fV8ZzgUit4Vre1EXctVC-Zm5j5kfSM_6pb99VKasBPAolbIa4U-_J1yGidc81sghJj3Haz048v9_074Ky4Qzl0czNQ7OYTn2N1xKuYeBEzXCMt6Cc8gpGJv372seP0CVTs6ZxFTAfRXw2PvHsrJdiEJQMpMoSLBRC_bF9s223o6PiN3v1XgGbAqwP3WI-0GhtyK5Tnul0WJIH5i9G2sA3DlDAmxFm51RwxZtaeqhYPGNEkHrUQqBkNHeAiZ_VqvmGHyuj-D7E-XHOGkF8bQPCHWVXeLdZJNkOxxBEgqYNBpp2QWBWVg",
|
||||
"graphTokenExpiry": 1768689928701,
|
||||
"lastRefresh": 1768686328701
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
# Universal Auth Module
|
||||
|
||||
## Overview
|
||||
This module provides a standardized, immutable authentication and token management system for any web project using PocketBase and Microsoft Graph. It supports user and agent tokens, delegated and app-only flows, and a consistent popup UI for login prompts.
|
||||
|
||||
## Features
|
||||
- Pattern-based: Choose 'pb-user', 'pb-agent', 'graph-user', 'graph-agent', or any combination (e.g., 'pb-user+graph-agent') for your project needs
|
||||
- Supports both user and agent tokens for PocketBase and Microsoft Graph
|
||||
- Special handling for Valkey/Redis agent login and token storage (for backend/automation flows)
|
||||
- Single source of truth for token storage, retrieval, and refresh
|
||||
- Consistent, hidden popup for all user login prompts
|
||||
- Extracts user info (display name, email) from PocketBase
|
||||
- Works in any frontend project with PocketBase
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Include in your project
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script>
|
||||
<script src="auth/auth-universal.js"></script>
|
||||
```
|
||||
|
||||
### 2. Initialize the pattern
|
||||
```js
|
||||
// Choose your pattern:
|
||||
// 'pb-user' - PocketBase user login
|
||||
// 'pb-agent' - PocketBase agent/service login
|
||||
// 'graph-user' - Microsoft Graph delegated (user) login
|
||||
// 'graph-agent' - Microsoft Graph agent/app-only (client credentials)
|
||||
// Combine as needed: 'pb-user+graph-agent', 'pb-agent+graph-user', etc.
|
||||
Auth.use('pb-user+graph-agent');
|
||||
```
|
||||
|
||||
### 3. Get and use tokens
|
||||
```js
|
||||
const pbToken = await Auth.ensureToken('pb');
|
||||
const graphToken = await Auth.ensureToken('graph');
|
||||
const userInfo = Auth.getUserInfo();
|
||||
```
|
||||
|
||||
### 4. Handle reauthentication
|
||||
- If a token is missing or expired, Auth will automatically show a popup for login.
|
||||
- The popup is hidden at all other times.
|
||||
|
||||
### 5. Clear tokens (for testing or logout)
|
||||
```js
|
||||
Auth.clearToken('pb');
|
||||
Auth.clearToken('graph');
|
||||
```
|
||||
|
||||
## Secrets and Environment
|
||||
- For frontend, all secrets are handled by PocketBase OAuth (no client secrets exposed).
|
||||
- For backend/agent flows, use environment variables and server-side code to store secrets securely.
|
||||
- The module does not expose or require secrets in the frontend.
|
||||
|
||||
## Example Test Page
|
||||
See `auth-test.html` for a safe way to test all flows without affecting your main app.
|
||||
|
||||
## Rules for Copilot Instructions
|
||||
- Always use Auth.use() to set the pattern before requesting tokens. Supported types: 'pb-user', 'pb-agent', 'graph-user', 'graph-agent', or any combination.
|
||||
- Always use Auth.ensureToken(type) to get a valid token; it will handle refresh and prompt if needed.
|
||||
- Never store secrets in frontend code; use PocketBase OAuth for user login.
|
||||
- All login prompts must use the provided popup, never custom dialogs.
|
||||
- Token keys are immutable: 'pbUserToken', 'pbAgentToken', 'graphUserToken', 'graphAgentToken'.
|
||||
- User info extraction must use Auth.getUserInfo().
|
||||
- For agent/app-only tokens, use backend code, environment variables, and optionally Valkey/Redis for secure storage and retrieval.
|
||||
|
||||
## FAQ
|
||||
- **Can I use this in any project?** Yes, as long as you include PocketBase and this module.
|
||||
- **Does it work for both user and agent tokens?** Yes, with the correct pattern and backend support for agent tokens (including Valkey/Redis for automation).
|
||||
- **Is the popup customizable?** The style can be changed, but the flow must remain consistent for all projects.
|
||||
- **How are secrets handled?** Only via backend environment variables for agent tokens; never exposed in frontend.
|
||||
|
||||
---
|
||||
For further integration, see the comments in `auth-universal.js` and the example test page.
|
||||
@@ -1,85 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Universal Auth Test</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script>
|
||||
<script src="auth-universal.js"></script>
|
||||
<style>
|
||||
body { font-family: sans-serif; background: #f3f4f6; margin: 0; padding: 2em; }
|
||||
.test-block { background: #fff; border-radius: 1em; box-shadow: 0 2px 12px #0001; padding: 2em; margin-bottom: 2em; }
|
||||
.pattern-btn { margin: 0.2em 0.5em 0.2em 0; padding: 0.5em 1.2em; border-radius: 0.5em; border: none; background: #2563eb; color: #fff; cursor: pointer; font-size: 1em; }
|
||||
.pattern-btn.active { background: #059669; }
|
||||
#output { font-family: monospace; background: #f9fafb; border-radius: 0.5em; padding: 1em; margin-top: 1em; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="test-block">
|
||||
<h2>Choose Auth Pattern</h2>
|
||||
<div id="patterns"></div>
|
||||
<div style="margin-top:1em;">
|
||||
<button onclick="clearAll()" class="pattern-btn" style="background:#b91c1c;">Clear All Tokens</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="test-block">
|
||||
<h2>Test Actions</h2>
|
||||
<button onclick="testToken('pb-user')" class="pattern-btn">Test PB User</button>
|
||||
<button onclick="testToken('pb-agent')" class="pattern-btn">Test PB Agent</button>
|
||||
<button onclick="testToken('graph-user')" class="pattern-btn">Test Graph User</button>
|
||||
<button onclick="testToken('graph-agent')" class="pattern-btn">Test Graph Agent</button>
|
||||
<button onclick="showUserInfo()" class="pattern-btn" style="background:#6d28d9;">Show User Info</button>
|
||||
<div id="output"></div>
|
||||
</div>
|
||||
<script>
|
||||
const patterns = [
|
||||
'pb-user',
|
||||
'pb-agent',
|
||||
'graph-user',
|
||||
'graph-agent',
|
||||
'pb-user+graph-user',
|
||||
'pb-user+graph-agent',
|
||||
'pb-agent+graph-user',
|
||||
'pb-agent+graph-agent',
|
||||
'pb-user+pb-agent+graph-user+graph-agent'
|
||||
];
|
||||
let currentPattern = patterns[0];
|
||||
function setPattern(p) {
|
||||
currentPattern = p;
|
||||
Auth.use(p);
|
||||
document.querySelectorAll('.pattern-btn').forEach(btn => btn.classList.remove('active'));
|
||||
document.querySelectorAll('.pattern-btn[data-pattern="' + p + '"]').forEach(btn => btn.classList.add('active'));
|
||||
document.getElementById('output').textContent = `Pattern set: ${p}`;
|
||||
}
|
||||
function renderPatterns() {
|
||||
const el = document.getElementById('patterns');
|
||||
el.innerHTML = '';
|
||||
patterns.forEach(p => {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = p;
|
||||
btn.className = 'pattern-btn' + (p === currentPattern ? ' active' : '');
|
||||
btn.setAttribute('data-pattern', p);
|
||||
btn.onclick = () => setPattern(p);
|
||||
el.appendChild(btn);
|
||||
});
|
||||
}
|
||||
renderPatterns();
|
||||
setPattern(currentPattern);
|
||||
async function testToken(type) {
|
||||
try {
|
||||
const token = await Auth.ensureToken(type);
|
||||
document.getElementById('output').textContent = `${type} token: ${token ? token.substring(0, 40) + '...' : 'NONE'}`;
|
||||
} catch (err) {
|
||||
document.getElementById('output').textContent = `Error: ${err.message}`;
|
||||
}
|
||||
}
|
||||
function clearAll() {
|
||||
['pb-user','pb-agent','graph-user','graph-agent'].forEach(Auth.clearToken);
|
||||
document.getElementById('output').textContent = 'All tokens cleared.';
|
||||
}
|
||||
function showUserInfo() {
|
||||
const info = Auth.getUserInfo();
|
||||
document.getElementById('output').textContent = 'User Info: ' + JSON.stringify(info, null, 2);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,220 +0,0 @@
|
||||
// Universal Auth Module: Immutable, Pattern-Based, Project-Agnostic
|
||||
// Usage: import and call Auth.use('pb-graph') or Auth.use('pb') or Auth.use('graph')
|
||||
// Provides: getToken(type), ensureToken(type), refreshToken(type), getUserInfo(), promptReauth()
|
||||
// Token types: 'pb' (PocketBase user/agent), 'graph' (Graph delegated), 'graphAgent' (Graph app-only)
|
||||
// All storage, retrieval, refresh, and UI are standardized and immutable
|
||||
|
||||
const TOKEN_KEYS = {
|
||||
'pb-user': 'pbUserToken',
|
||||
'pb-agent': 'pbAgentToken',
|
||||
'graph-user': 'graphUserToken',
|
||||
'graph-agent': 'graphAgentToken'
|
||||
};
|
||||
|
||||
function parsePattern(pattern) {
|
||||
return pattern.split('+').map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
window.Auth = (() => {
|
||||
let pattern = 'pb-user'; // default
|
||||
let enabledTypes = ['pb-user'];
|
||||
let pb = null;
|
||||
let popup = null;
|
||||
let agentCreds = { email: '', password: '' };
|
||||
let superuserCreds = { email: '', password: '' };
|
||||
|
||||
// --- Setup ---
|
||||
function use(type) {
|
||||
pattern = type;
|
||||
enabledTypes = parsePattern(type);
|
||||
if (enabledTypes.some(t => t.startsWith('pb'))) {
|
||||
pb = window.PocketBase ? new PocketBase('https://pocketbase.ccllc.pro') : null;
|
||||
}
|
||||
// Only one-time setup
|
||||
if (!document.getElementById('authPopup')) {
|
||||
createPopup();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Token Storage ---
|
||||
function getToken(type) {
|
||||
return localStorage.getItem(TOKEN_KEYS[type]) || '';
|
||||
}
|
||||
function setToken(type, value) {
|
||||
if (value) localStorage.setItem(TOKEN_KEYS[type], value);
|
||||
}
|
||||
function clearToken(type) {
|
||||
localStorage.removeItem(TOKEN_KEYS[type]);
|
||||
}
|
||||
|
||||
// --- Info Extraction ---
|
||||
function getUserInfo() {
|
||||
if (pb && pb.authStore.model) {
|
||||
return {
|
||||
displayName: pb.authStore.model.display_name || pb.authStore.model.name || pb.authStore.model.email || pb.authStore.model.username || 'Unknown',
|
||||
email: pb.authStore.model.email || pb.authStore.model.username || null
|
||||
};
|
||||
}
|
||||
return { displayName: '', email: '' };
|
||||
}
|
||||
|
||||
// --- Token Ensure/Refresh ---
|
||||
async function ensureToken(type) {
|
||||
let token = getToken(type);
|
||||
if (token) return token;
|
||||
if (type === 'pb-user' && pb) {
|
||||
if (pb.authStore.isValid) {
|
||||
setToken('pb-user', pb.authStore.token);
|
||||
return pb.authStore.token;
|
||||
}
|
||||
// Choose login method: email/password or OAuth
|
||||
if (pattern.includes('pb-user-oauth')) {
|
||||
return await promptReauth('pb-user-oauth');
|
||||
} else {
|
||||
return await promptReauth('pb-user');
|
||||
}
|
||||
}
|
||||
if (type === 'pb-superuser' && pb) {
|
||||
// Prompt for superuser credentials
|
||||
await promptReauth('pb-superuser');
|
||||
// Use superuser credentials to login
|
||||
const authData = await pb.collection('_superuser').authWithPassword(superuserCreds.email, superuserCreds.password);
|
||||
setToken('pb-superuser', pb.authStore.token);
|
||||
return pb.authStore.token;
|
||||
}
|
||||
if (type === 'pb-agent' && pb) {
|
||||
// Prompt for agent credentials if not set
|
||||
if (!agentCreds.email || !agentCreds.password) {
|
||||
await promptReauth('pb-agent');
|
||||
}
|
||||
// Use agent credentials to login
|
||||
const authData = await pb.collection('users').authWithPassword(agentCreds.email, agentCreds.password);
|
||||
setToken('pb-agent', pb.authStore.token);
|
||||
return pb.authStore.token;
|
||||
}
|
||||
if (type === 'graph-user' && pb) {
|
||||
// OAuth only
|
||||
return await promptReauth('graph-user');
|
||||
}
|
||||
if (type === 'graph-agent') {
|
||||
// Prompt for agent credentials if not set
|
||||
if (!agentCreds.email || !agentCreds.password) {
|
||||
await promptReauth('graph-agent');
|
||||
}
|
||||
// Use agent credentials to get token (simulate, real flow is backend)
|
||||
setToken('graph-agent', 'AGENT_TOKEN_' + agentCreds.email);
|
||||
return getToken('graph-agent');
|
||||
}
|
||||
throw new Error('Unknown token type');
|
||||
}
|
||||
|
||||
async function refreshToken(type) {
|
||||
if (type === 'graph-user' && pb) {
|
||||
try {
|
||||
const authData = await pb.collection('users').authRefresh();
|
||||
const meta = authData?.meta || pb?.authStore?.model?.meta || {};
|
||||
const token = meta.graphAccessToken || meta.graph_token || meta.graphToken || meta.accessToken || meta.access_token || meta.token || meta.rawToken || meta?.authData?.access_token || '';
|
||||
setToken('graph-user', token);
|
||||
return token || '';
|
||||
} catch (err) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
// Add refresh logic for other types as needed
|
||||
return '';
|
||||
}
|
||||
|
||||
// --- Reauth Popup ---
|
||||
function createPopup() {
|
||||
popup = document.createElement('div');
|
||||
popup.id = 'authPopup';
|
||||
popup.style = 'display:none;position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.4);z-index:9999;align-items:center;justify-content:center;';
|
||||
popup.innerHTML = `
|
||||
<div style="background:#fff;padding:2em 2.5em;border-radius:1em;box-shadow:0 2px 24px #0002;text-align:center;max-width:350px;margin:auto;">
|
||||
<h2 style="font-size:1.3em;margin-bottom:1em;">Sign In Required</h2>
|
||||
<div id="authPopupForms"></div>
|
||||
<div id="authPopupError" style="color:#b91c1c;margin-top:1em;display:none;"></div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(popup);
|
||||
}
|
||||
|
||||
function showForm(type) {
|
||||
const forms = {
|
||||
'pb-user': `<input id="pbUserEmail" type="email" placeholder="Email" style="width:90%;margin-bottom:0.5em;"><br><input id="pbUserPassword" type="password" placeholder="Password" style="width:90%;margin-bottom:0.5em;"><br><button id="authPopupBtn" style="background:#2563eb;color:#fff;padding:0.7em 2em;border:none;border-radius:0.5em;font-size:1em;cursor:pointer;">Sign in</button>`,
|
||||
'pb-user-oauth': `<button id="authPopupBtn" style="background:#2563eb;color:#fff;padding:0.7em 2em;border:none;border-radius:0.5em;font-size:1em;cursor:pointer;">Sign in with Microsoft</button>`,
|
||||
'pb-superuser': `<input id="pbSuperuserEmail" type="email" placeholder="Superuser Email" style="width:90%;margin-bottom:0.5em;"><br><input id="pbSuperuserPassword" type="password" placeholder="Superuser Password" style="width:90%;margin-bottom:0.5em;"><br><button id="authPopupBtn" style="background:#d97706;color:#fff;padding:0.7em 2em;border:none;border-radius:0.5em;font-size:1em;cursor:pointer;">Sign in as Superuser</button>`,
|
||||
'pb-agent': `<input id="pbAgentEmail" type="email" placeholder="Agent Email" style="width:90%;margin-bottom:0.5em;"><br><input id="pbAgentPassword" type="password" placeholder="Agent Password" style="width:90%;margin-bottom:0.5em;"><br><button id="authPopupBtn" style="background:#059669;color:#fff;padding:0.7em 2em;border:none;border-radius:0.5em;font-size:1em;cursor:pointer;">Sign in as Agent</button>`,
|
||||
'graph-user': `<button id="authPopupBtn" style="background:#2563eb;color:#fff;padding:0.7em 2em;border:none;border-radius:0.5em;font-size:1em;cursor:pointer;">Sign in with Microsoft</button>`,
|
||||
'graph-agent': `<input id="graphAgentEmail" type="email" placeholder="Agent Email" style="width:90%;margin-bottom:0.5em;"><br><input id="graphAgentPassword" type="password" placeholder="Agent Password" style="width:90%;margin-bottom:0.5em;"><br><button id="authPopupBtn" style="background:#059669;color:#fff;padding:0.7em 2em;border:none;border-radius:0.5em;font-size:1em;cursor:pointer;">Sign in as Graph Agent</button>`
|
||||
};
|
||||
document.getElementById('authPopupForms').innerHTML = forms[type] || '';
|
||||
}
|
||||
|
||||
async function promptReauth(type) {
|
||||
showForm(type);
|
||||
popup.style.display = 'flex';
|
||||
return new Promise((resolve, reject) => {
|
||||
document.getElementById('authPopupBtn').onclick = async () => {
|
||||
try {
|
||||
if (type === 'pb-user') {
|
||||
const email = document.getElementById('pbUserEmail').value;
|
||||
const password = document.getElementById('pbUserPassword').value;
|
||||
const authData = await pb.collection('users').authWithPassword(email, password);
|
||||
setToken('pb-user', pb.authStore.token);
|
||||
popup.style.display = 'none';
|
||||
resolve(pb.authStore.token);
|
||||
} else if (type === 'pb-user-oauth') {
|
||||
const authData = await pb.collection('users').authWithOAuth2({ provider: 'microsoft' });
|
||||
setToken('pb-user', authData?.meta?.graphAccessToken || '');
|
||||
popup.style.display = 'none';
|
||||
resolve(authData?.meta?.graphAccessToken || '');
|
||||
} else if (type === 'pb-superuser') {
|
||||
superuserCreds.email = document.getElementById('pbSuperuserEmail').value;
|
||||
superuserCreds.password = document.getElementById('pbSuperuserPassword').value;
|
||||
popup.style.display = 'none';
|
||||
resolve();
|
||||
} else if (type === 'pb-agent') {
|
||||
agentCreds.email = document.getElementById('pbAgentEmail').value;
|
||||
agentCreds.password = document.getElementById('pbAgentPassword').value;
|
||||
popup.style.display = 'none';
|
||||
resolve();
|
||||
} else if (type === 'graph-user') {
|
||||
const authData = await pb.collection('users').authWithOAuth2({ provider: 'microsoft' });
|
||||
setToken('graph-user', authData?.meta?.graphAccessToken || '');
|
||||
popup.style.display = 'none';
|
||||
resolve(authData?.meta?.graphAccessToken || '');
|
||||
} else if (type === 'graph-agent') {
|
||||
agentCreds.email = document.getElementById('graphAgentEmail').value;
|
||||
agentCreds.password = document.getElementById('graphAgentPassword').value;
|
||||
popup.style.display = 'none';
|
||||
resolve();
|
||||
}
|
||||
} catch (err) {
|
||||
document.getElementById('authPopupError').textContent = err.message || 'Authentication failed.';
|
||||
document.getElementById('authPopupError').style.display = '';
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// --- API ---
|
||||
return {
|
||||
use,
|
||||
getToken,
|
||||
setToken,
|
||||
clearToken,
|
||||
getUserInfo,
|
||||
ensureToken,
|
||||
refreshToken,
|
||||
promptReauth
|
||||
};
|
||||
})();
|
||||
|
||||
window.Auth = Auth;
|
||||
|
||||
// Example usage:
|
||||
// Auth.use('pb-graph');
|
||||
// const token = await Auth.ensureToken('graph');
|
||||
// const user = Auth.getUserInfo();
|
||||
// All login prompts are handled via the popup, which is hidden unless needed.
|
||||
@@ -1,37 +0,0 @@
|
||||
// Express API for Redis/Valkey: keys list and single key fetch (for fast UI)
|
||||
const express = require('express');
|
||||
const Redis = require('ioredis');
|
||||
const app = express();
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST || '127.0.0.1',
|
||||
port: Number(process.env.REDIS_PORT || 6379),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
});
|
||||
|
||||
// List all keys (no values)
|
||||
app.get('/api/cache/keys', async (req, res) => {
|
||||
try {
|
||||
const keys = await redis.keys('*');
|
||||
// Optionally, add summary/metadata here
|
||||
res.json(keys.map(key => ({ key })));
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch value for a single key
|
||||
app.get('/api/cache/:key', async (req, res) => {
|
||||
try {
|
||||
const key = req.params.key;
|
||||
let value = await redis.get(key);
|
||||
try { value = JSON.parse(value); } catch {}
|
||||
res.json({ key, value });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
const port = process.env.CACHE_API_PORT || 3006;
|
||||
app.listen(port, () => {
|
||||
console.log(`Cache API (keys+single) running on http://localhost:${port}/api/cache/keys`);
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
// Simple Express API to serve all Redis/Valkey cache keys and values as JSON (CommonJS)
|
||||
const express = require('express');
|
||||
const Redis = require('ioredis');
|
||||
const app = express();
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST || '127.0.0.1',
|
||||
port: Number(process.env.REDIS_PORT || 6379),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
});
|
||||
|
||||
app.get('/api/cache', async (req, res) => {
|
||||
try {
|
||||
const keys = await redis.keys('*');
|
||||
const result = [];
|
||||
for (const key of keys) {
|
||||
let value = await redis.get(key);
|
||||
try { value = JSON.parse(value); } catch {}
|
||||
result.push({ key, value });
|
||||
}
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
const port = process.env.CACHE_API_PORT || 3006;
|
||||
app.listen(port, () => {
|
||||
console.log(`Cache API running on http://localhost:${port}/api/cache`);
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
// Simple Express API to serve all Redis/Valkey cache keys and values as JSON
|
||||
const express = require('express');
|
||||
const Redis = require('ioredis');
|
||||
const app = express();
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST || '127.0.0.1',
|
||||
port: Number(process.env.REDIS_PORT || 6379),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
});
|
||||
|
||||
app.get('/api/cache', async (req, res) => {
|
||||
try {
|
||||
const keys = await redis.keys('*');
|
||||
const result = [];
|
||||
for (const key of keys) {
|
||||
let value = await redis.get(key);
|
||||
try { value = JSON.parse(value); } catch {}
|
||||
result.push({ key, value });
|
||||
}
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
const port = process.env.CACHE_API_PORT || 3030;
|
||||
app.listen(port, () => {
|
||||
console.log(`Cache API running on http://localhost:${port}/api/cache`);
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
// Script to list all Redis/Valkey keys and their values for visualization (CommonJS)
|
||||
const Redis = require('ioredis');
|
||||
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST || '127.0.0.1',
|
||||
port: Number(process.env.REDIS_PORT || 6379),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
});
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const keys = await redis.keys('*');
|
||||
const result = [];
|
||||
for (const key of keys) {
|
||||
let value = await redis.get(key);
|
||||
try { value = JSON.parse(value); } catch {}
|
||||
result.push({ key, value });
|
||||
}
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error('Error listing Redis keys:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
@@ -1,25 +0,0 @@
|
||||
// Script to list all Redis/Valkey keys and their values for visualization
|
||||
const Redis = require('ioredis');
|
||||
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST || '127.0.0.1',
|
||||
port: Number(process.env.REDIS_PORT || 6379),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
});
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const keys = await redis.keys('*');
|
||||
const result = [];
|
||||
for (const key of keys) {
|
||||
let value = await redis.get(key);
|
||||
try { value = JSON.parse(value); } catch {}
|
||||
result.push({ key, value });
|
||||
}
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error('Error listing Redis keys:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
@@ -1,101 +0,0 @@
|
||||
import Redis from 'ioredis';
|
||||
|
||||
// Create Redis client
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: Number(process.env.REDIS_PORT || 6379),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
retryStrategy(times) {
|
||||
const delay = Math.min(times * 50, 2000);
|
||||
return delay;
|
||||
},
|
||||
maxRetriesPerRequest: 3,
|
||||
lazyConnect: true,
|
||||
});
|
||||
|
||||
// Handle connection events
|
||||
redis.on('connect', () => {
|
||||
console.log('✓ Redis connected');
|
||||
});
|
||||
|
||||
redis.on('error', (err) => {
|
||||
console.error('Redis error:', err.message);
|
||||
});
|
||||
|
||||
redis.on('close', () => {
|
||||
console.log('Redis connection closed');
|
||||
});
|
||||
|
||||
// Connect to Redis
|
||||
(async () => {
|
||||
try {
|
||||
await redis.connect();
|
||||
} catch (err) {
|
||||
console.error('Failed to connect to Redis:', err);
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* Get a value from cache
|
||||
*/
|
||||
export async function getCache(key: string): Promise<any | null> {
|
||||
try {
|
||||
const value = await redis.get(key);
|
||||
if (!value) return null;
|
||||
return JSON.parse(value);
|
||||
} catch (err) {
|
||||
console.error(`Cache get error for key "${key}":`, err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a value in cache with TTL in seconds
|
||||
*/
|
||||
export async function setCache(key: string, value: any, ttl: number = 300): Promise<boolean> {
|
||||
try {
|
||||
const serialized = JSON.stringify(value);
|
||||
await redis.setex(key, ttl, serialized);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(`Cache set error for key "${key}":`, err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a key from cache
|
||||
*/
|
||||
export async function deleteCache(key: string): Promise<boolean> {
|
||||
try {
|
||||
await redis.del(key);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(`Cache delete error for key "${key}":`, err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cache keys matching a pattern
|
||||
*/
|
||||
export async function clearCachePattern(pattern: string): Promise<number> {
|
||||
try {
|
||||
const keys = await redis.keys(pattern);
|
||||
if (keys.length === 0) return 0;
|
||||
await redis.del(...keys);
|
||||
return keys.length;
|
||||
} catch (err) {
|
||||
console.error(`Cache clear error for pattern "${pattern}":`, err);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Redis is connected
|
||||
*/
|
||||
export function isRedisConnected(): boolean {
|
||||
return redis.status === 'ready';
|
||||
}
|
||||
|
||||
export default redis;
|
||||
@@ -1,513 +0,0 @@
|
||||
import { Hono } from 'hono';
|
||||
import { serveStatic } from 'hono/bun';
|
||||
import fs from 'fs';
|
||||
// @ts-ignore Bun project may not have dotenv types configured
|
||||
import { config } from 'dotenv';
|
||||
import { getCache, setCache, isRedisConnected } from './redis-cache';
|
||||
|
||||
config();
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
// Minimal log helpers
|
||||
const logLine = (path: string, line: string) => {
|
||||
try {
|
||||
fs.appendFileSync(path, `${new Date().toISOString()} ${line}\n`);
|
||||
} catch (err) {
|
||||
console.error('Failed to write log:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Version endpoint sourced from package.json
|
||||
app.get('/version', (c) => {
|
||||
try {
|
||||
const pkgRaw = fs.readFileSync('./package.json', 'utf-8');
|
||||
const pkg = JSON.parse(pkgRaw);
|
||||
return c.json({ version: pkg.version || '0.0.0' });
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `version endpoint error: ${(err as Error)?.message || String(err)}`);
|
||||
return c.json({ version: '0.0.0' }, 200);
|
||||
}
|
||||
});
|
||||
|
||||
// Jobs endpoint with Redis caching
|
||||
app.get('/api/jobs', async (c) => {
|
||||
const page = parseInt(c.req.query('page') || '1');
|
||||
const perPage = parseInt(c.req.query('perPage') || '50');
|
||||
const sort = c.req.query('sort') || '-Job_Number';
|
||||
|
||||
const cacheKey = `jobs:page:${page}:perPage:${perPage}:sort:${sort}`;
|
||||
const ttl = 300; // 5 minutes cache
|
||||
|
||||
try {
|
||||
// Try to get from cache first
|
||||
if (isRedisConnected()) {
|
||||
const cached = await getCache(cacheKey);
|
||||
if (cached) {
|
||||
logLine('logs/server.log', `Cache HIT for ${cacheKey}`);
|
||||
return c.json(cached);
|
||||
}
|
||||
logLine('logs/server.log', `Cache MISS for ${cacheKey}`);
|
||||
}
|
||||
|
||||
// Fetch from PocketBase
|
||||
const pbUrl = `https://pocketbase.ccllc.pro/api/collections/pbc_1407612689/records?page=${page}&perPage=${perPage}&sort=${sort}`;
|
||||
const authHeader = c.req.header('Authorization') || '';
|
||||
|
||||
const response = await fetch(pbUrl, {
|
||||
headers: authHeader ? { Authorization: authHeader } : {}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`PocketBase returned ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Cache the result
|
||||
if (isRedisConnected()) {
|
||||
await setCache(cacheKey, data, ttl);
|
||||
logLine('logs/server.log', `Cached ${cacheKey} for ${ttl}s`);
|
||||
}
|
||||
|
||||
return c.json(data);
|
||||
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `jobs endpoint error: ${(err as Error)?.message || String(err)}`);
|
||||
return c.json({ error: 'Failed to fetch jobs' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Fast endpoint: returns all jobs in a single cached request
|
||||
app.get('/api/jobs-all', async (c) => {
|
||||
const cacheKey = 'jobs:all';
|
||||
const ttl = 1800; // 30 minutes cache
|
||||
try {
|
||||
// Try cache first for instant response
|
||||
if (isRedisConnected()) {
|
||||
const cached = await getCache(cacheKey);
|
||||
if (cached) {
|
||||
logLine('logs/server.log', `Cache HIT for ${cacheKey} (${cached.items?.length || 0} jobs)`);
|
||||
// Return cached data immediately
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
await refreshJobsCache(cacheKey, ttl, c.req.header('Authorization') || '');
|
||||
} catch (err) {}
|
||||
});
|
||||
// Return all cached jobs in a single response
|
||||
return c.json({ ...cached, partial: false });
|
||||
} else {
|
||||
// No cache: return empty array instantly, trigger background fetch
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
const result = await fetchAllJobsFromPocketBase(c.req.header('Authorization') || '');
|
||||
if (isRedisConnected()) {
|
||||
await setCache(cacheKey, result, ttl);
|
||||
logLine('logs/server.log', `Cached ${cacheKey} with ${result.items.length} jobs for ${ttl}s`);
|
||||
}
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `jobs-all background fetch error: ${(err as Error)?.message || String(err)}`);
|
||||
}
|
||||
});
|
||||
return c.json({ items: [], partial: true });
|
||||
}
|
||||
}
|
||||
// Redis not connected: fallback to direct fetch (blocking)
|
||||
const result = await fetchAllJobsFromPocketBase(c.req.header('Authorization') || '');
|
||||
return c.json(result);
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `jobs-all endpoint error: ${(err as Error)?.message || String(err)}`);
|
||||
return c.json({ error: 'Failed to fetch jobs' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Helper function to fetch all jobs from PocketBase
|
||||
async function fetchAllJobsFromPocketBase(authHeader: string): Promise<any> {
|
||||
const allItems: any[] = [];
|
||||
let page = 1;
|
||||
const perPage = 500;
|
||||
|
||||
while (true) {
|
||||
const pbUrl = `https://pocketbase.ccllc.pro/api/collections/pbc_1407612689/records?page=${page}&perPage=${perPage}&sort=-Job_Number`;
|
||||
const response = await fetch(pbUrl, {
|
||||
headers: authHeader ? { Authorization: authHeader } : {}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`PocketBase returned ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const items = data.items || [];
|
||||
|
||||
if (items.length === 0) break;
|
||||
allItems.push(...items);
|
||||
|
||||
if (allItems.length >= (data.totalItems || 0)) break;
|
||||
page++;
|
||||
}
|
||||
|
||||
return {
|
||||
page: 1,
|
||||
perPage: allItems.length,
|
||||
totalItems: allItems.length,
|
||||
totalPages: 1,
|
||||
items: allItems
|
||||
};
|
||||
}
|
||||
|
||||
// Helper function to refresh jobs cache in background
|
||||
async function refreshJobsCache(cacheKey: string, ttl: number, authHeader: string): Promise<void> {
|
||||
try {
|
||||
const result = await fetchAllJobsFromPocketBase(authHeader);
|
||||
await setCache(cacheKey, result, ttl);
|
||||
logLine('logs/server.log', `Background refresh: cached ${cacheKey} with ${result.items.length} jobs`);
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `Background refresh error: ${(err as Error)?.message || String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Microsoft Graph helpers ---
|
||||
const getGraphToken = (c?: any) => {
|
||||
const headerToken = c?.req?.header ? c.req.header('x-graph-token') : '';
|
||||
const envToken = process.env.GRAPH_TOKEN || process.env.MS_GRAPH_TOKEN || '';
|
||||
const token = headerToken || envToken;
|
||||
console.log('[getGraphToken] Header token:', headerToken ? headerToken.substring(0, 20) + '...' : 'NONE');
|
||||
console.log('[getGraphToken] Env token:', envToken ? 'SET' : 'NOT SET');
|
||||
console.log('[getGraphToken] Using:', token ? token.substring(0, 20) + '...' : 'NONE');
|
||||
return token;
|
||||
};
|
||||
|
||||
const b64Url = (input: string) =>
|
||||
Buffer.from(input).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||
|
||||
type DriveItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
webUrl?: string;
|
||||
size?: number;
|
||||
lastModifiedDateTime?: string;
|
||||
file?: { mimeType?: string };
|
||||
folder?: { childCount?: number };
|
||||
parentReference?: { path?: string; driveId?: string };
|
||||
};
|
||||
|
||||
const fetchJson = async (url: string, token: string) => {
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
const err: any = new Error(`Graph ${res.status}: ${text}`);
|
||||
err.status = res.status;
|
||||
throw err;
|
||||
}
|
||||
return res.json();
|
||||
};
|
||||
|
||||
const resolveShareLink = async (link: string, token: string) => {
|
||||
// Decode the link in case it comes pre-encoded from the database
|
||||
const decodedLink = decodeURIComponent(link);
|
||||
console.log('[resolveShareLink] Decoded link:', decodedLink);
|
||||
|
||||
// Check if this is a short sharing link (/:f:/ or /:b:/) or a direct document library URL
|
||||
if (decodedLink.includes('/:f:/') || decodedLink.includes('/:b:/')) {
|
||||
// Short sharing link - use shares API
|
||||
const encoded = `u!${b64Url(decodedLink)}`;
|
||||
console.log('[resolveShareLink] Using shares API, encoded:', encoded);
|
||||
const data = await fetchJson(`https://graph.microsoft.com/v1.0/shares/${encoded}/driveItem`, token);
|
||||
return { driveId: data.parentReference?.driveId as string, itemId: data.id as string };
|
||||
} else {
|
||||
// Direct document library URL - parse it and use drive API
|
||||
// Example: https://czflex.sharepoint.com/sites/Team/Shared Documents/Forms/AllItems.aspx?RootFolder=/sites/Team/Shared Documents/General/...
|
||||
const url = new URL(decodedLink);
|
||||
const pathMatch = url.hostname.match(/^([^.]+)\.sharepoint\.com$/);
|
||||
if (!pathMatch) throw new Error('Invalid SharePoint URL');
|
||||
|
||||
const hostname = pathMatch[1]; // e.g., 'czflex'
|
||||
const sitePath = url.pathname.split('/Shared')[0]; // e.g., /sites/Team
|
||||
const rootFolderParam = url.searchParams.get('RootFolder');
|
||||
if (!rootFolderParam) throw new Error('RootFolder parameter missing');
|
||||
|
||||
// Extract the folder path relative to the document library
|
||||
// RootFolder format: /sites/Team/Shared Documents/General/Operations [Server]/...
|
||||
// We need: General/Operations [Server]/...
|
||||
const folderPath = rootFolderParam.split('/Shared Documents/')[1];
|
||||
if (!folderPath) throw new Error('Could not parse folder path');
|
||||
|
||||
console.log('[resolveShareLink] Hostname:', hostname);
|
||||
console.log('[resolveShareLink] Site path:', sitePath);
|
||||
console.log('[resolveShareLink] Folder path:', folderPath);
|
||||
|
||||
// Get the site ID first using hostname:sitePath format
|
||||
const siteData = await fetchJson(`https://graph.microsoft.com/v1.0/sites/${hostname}.sharepoint.com:${sitePath}`, token);
|
||||
const siteId = siteData.id;
|
||||
|
||||
// Get the default document library drive
|
||||
const driveData = await fetchJson(`https://graph.microsoft.com/v1.0/sites/${siteId}/drive`, token);
|
||||
const driveId = driveData.id;
|
||||
|
||||
// Get the folder item by path
|
||||
const itemData = await fetchJson(
|
||||
`https://graph.microsoft.com/v1.0/drives/${driveId}/root:/${folderPath}`,
|
||||
token
|
||||
);
|
||||
|
||||
console.log('[resolveShareLink] Resolved to driveId:', driveId, 'itemId:', itemData.id);
|
||||
return { driveId, itemId: itemData.id };
|
||||
}
|
||||
};
|
||||
|
||||
const listChildrenRecursive = async (
|
||||
driveId: string,
|
||||
itemId: string,
|
||||
depth = 0,
|
||||
maxDepth = 5,
|
||||
token: string,
|
||||
): Promise<DriveItem[]> => {
|
||||
const out: DriveItem[] = [];
|
||||
const data = await fetchJson(`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/children`, token);
|
||||
for (const child of data.value || []) {
|
||||
out.push(child as DriveItem);
|
||||
if (child.folder && depth < maxDepth) {
|
||||
const nested = await listChildrenRecursive(driveId, child.id, depth + 1, maxDepth, token);
|
||||
out.push(...nested);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const searchWithinFolder = async (driveId: string, itemId: string, query: string, token: string): Promise<DriveItem[]> => {
|
||||
const data = await fetchJson(
|
||||
`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/search(q='${encodeURIComponent(query)}')`,
|
||||
token,
|
||||
);
|
||||
return (data.value || []) as DriveItem[];
|
||||
};
|
||||
|
||||
const filterByCategory = (items: DriveItem[], category?: string, pdfOnly: boolean = true) => {
|
||||
// First filter: show PDF files and Word documents (which will be converted to PDF)
|
||||
let filtered = items;
|
||||
if (pdfOnly) {
|
||||
filtered = items.filter((i) => {
|
||||
if (i.folder) return false;
|
||||
const name = i.name.toLowerCase();
|
||||
const mimeType = i.file?.mimeType?.toLowerCase() || '';
|
||||
// Include PDFs, Word documents, and images
|
||||
return name.endsWith('.pdf') || mimeType.includes('pdf') ||
|
||||
name.endsWith('.doc') || name.endsWith('.docx') || mimeType.includes('word') ||
|
||||
name.endsWith('.jpg') || name.endsWith('.jpeg') || name.endsWith('.png') ||
|
||||
name.endsWith('.gif') || name.endsWith('.bmp') || name.endsWith('.tiff') ||
|
||||
name.endsWith('.webp') || mimeType.includes('image');
|
||||
});
|
||||
}
|
||||
|
||||
// Second filter: category filtering (if specified)
|
||||
if (!category) return filtered;
|
||||
const nameHas = (name: string, words: string[]) => words.some((w) => name.includes(w));
|
||||
const lc = (s: string) => (s || '').toLowerCase();
|
||||
const contractWords = ['contract', 'estimate', 'proposal', 'bid', 'award', 'agreement', 'sow'];
|
||||
const planWords = ['plan', 'drawing', 'dwg', 'pdf', 'sheet', 'layout'];
|
||||
return filtered.filter((i) => {
|
||||
const n = lc(i.name);
|
||||
if (category === 'contracts') return nameHas(n, contractWords);
|
||||
if (category === 'plans') return nameHas(n, planWords);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
// List/search job files via Graph using a shared folder link
|
||||
app.get('/api/job-files', async (c) => {
|
||||
try {
|
||||
console.log('[job-files] Request received');
|
||||
const token = getGraphToken(c);
|
||||
if (!token) {
|
||||
console.log('[job-files] No token found');
|
||||
return c.json({ error: 'GRAPH_TOKEN missing' }, 500);
|
||||
}
|
||||
console.log('[job-files] Token found:', token.substring(0, 20) + '...');
|
||||
const link = c.req.query('link');
|
||||
const q = c.req.query('q') || '';
|
||||
const category = c.req.query('category') || '';
|
||||
if (!link) return c.json({ error: 'link required' }, 400);
|
||||
console.log('[job-files] Link:', link);
|
||||
|
||||
const { driveId, itemId } = await resolveShareLink(link, token);
|
||||
|
||||
let items: DriveItem[] = [];
|
||||
if (q) {
|
||||
items = await searchWithinFolder(driveId, itemId, q, token);
|
||||
} else {
|
||||
// Walk subfolders up to depth 5
|
||||
items = await listChildrenRecursive(driveId, itemId, 0, 5, token);
|
||||
}
|
||||
|
||||
// Filter to show only PDFs by default (can be disabled with pdfOnly=false query param)
|
||||
const pdfOnly = c.req.query('pdfOnly') !== 'false';
|
||||
const filtered = filterByCategory(items, category, pdfOnly);
|
||||
console.log(`[job-files] Found ${items.length} total items, ${filtered.length} after filtering`);
|
||||
|
||||
const mapped = filtered.map((i) => ({
|
||||
id: i.id,
|
||||
name: i.name,
|
||||
url: i.webUrl,
|
||||
driveId: i.parentReference?.driveId || driveId,
|
||||
size: i.size,
|
||||
modified: i.lastModifiedDateTime,
|
||||
contentType: i.file?.mimeType,
|
||||
isFolder: Boolean(i.folder),
|
||||
path: i.parentReference?.path || '',
|
||||
}));
|
||||
|
||||
return c.json({ items: mapped, total: mapped.length, source: q ? 'search' : 'walk', driveId });
|
||||
} catch (err) {
|
||||
const message = (err as Error)?.message || String(err);
|
||||
const status = (err as any)?.status || 500;
|
||||
console.error('[job-files] ERROR:', message);
|
||||
logLine('logs/error.log', `/api/job-files error: ${message}`);
|
||||
return c.json({ error: message }, status);
|
||||
}
|
||||
});
|
||||
|
||||
// Stream file content via Graph to avoid CSP issues for inline preview
|
||||
app.get('/api/job-file-content', async (c) => {
|
||||
try {
|
||||
const token = getGraphToken(c);
|
||||
if (!token) return c.json({ error: 'GRAPH_TOKEN missing' }, 500);
|
||||
const driveId = c.req.query('driveId');
|
||||
const itemId = c.req.query('itemId');
|
||||
if (!driveId || !itemId) return c.json({ error: 'driveId and itemId required' }, 400);
|
||||
|
||||
const res = await fetch(`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/content`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
logLine('logs/error.log', `/api/job-file-content failed: ${res.status} - ${text}`);
|
||||
return c.json({ error: `Graph ${res.status}: ${text}` }, 502);
|
||||
}
|
||||
const headers: Record<string, string> = {};
|
||||
const ct = res.headers.get('content-type');
|
||||
const cd = res.headers.get('content-disposition');
|
||||
if (ct) headers['Content-Type'] = ct;
|
||||
if (cd) headers['Content-Disposition'] = cd;
|
||||
return new Response(res.body, { status: 200, headers });
|
||||
} catch (err) {
|
||||
const message = (err as Error)?.message || String(err);
|
||||
const status = (err as any)?.status || 500;
|
||||
logLine('logs/error.log', `/api/job-file-content error: ${message}`);
|
||||
return c.json({ error: message }, status);
|
||||
}
|
||||
});
|
||||
|
||||
// Convert Word documents to PDF for display
|
||||
app.get('/api/job-file-pdf', async (c) => {
|
||||
try {
|
||||
const token = getGraphToken(c);
|
||||
if (!token) return c.json({ error: 'GRAPH_TOKEN missing' }, 500);
|
||||
const driveId = c.req.query('driveId');
|
||||
const itemId = c.req.query('itemId');
|
||||
if (!driveId || !itemId) return c.json({ error: 'driveId and itemId required' }, 400);
|
||||
|
||||
// Use Graph API to convert to PDF format
|
||||
const res = await fetch(`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/content?format=pdf`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
logLine('logs/error.log', `/api/job-file-pdf conversion failed: ${res.status} - ${text}`);
|
||||
return c.json({ error: `PDF conversion failed: ${res.status}` }, 502);
|
||||
}
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/pdf',
|
||||
};
|
||||
const cd = res.headers.get('content-disposition');
|
||||
if (cd) {
|
||||
// Replace .docx/.doc extension with .pdf in filename
|
||||
headers['Content-Disposition'] = cd.replace(/\.(docx?)/gi, '.pdf');
|
||||
}
|
||||
logLine('logs/server.log', `Successfully converted document to PDF: driveId=${driveId}, itemId=${itemId}`);
|
||||
return new Response(res.body, { status: 200, headers });
|
||||
} catch (err) {
|
||||
const message = (err as Error)?.message || String(err);
|
||||
const status = (err as any)?.status || 500;
|
||||
logLine('logs/error.log', `/api/job-file-pdf error: ${message}`);
|
||||
return c.json({ error: message }, status);
|
||||
}
|
||||
});
|
||||
|
||||
// Mutation logging endpoint
|
||||
app.post('/log-change', async (c) => {
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const { action = 'unknown', user = 'unknown', target = '', detail = '' } = body || {};
|
||||
const payload = JSON.stringify({ action, user, target, detail });
|
||||
logLine('logs/changes.log', payload);
|
||||
return c.json({ status: 'ok' });
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `log-change error: ${(err as Error)?.message || String(err)}`);
|
||||
return c.text('Bad Request', 400);
|
||||
}
|
||||
});
|
||||
|
||||
// Serve static files from the frontend directory (must come after API routes)
|
||||
// Add cache headers for static assets
|
||||
app.use('/assets/*', async (c, next) => {
|
||||
await next();
|
||||
// Cache assets for 1 year
|
||||
c.header('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
});
|
||||
app.use('/*', serveStatic({ root: './frontend' }));
|
||||
|
||||
// Error handler
|
||||
app.onError((err, c) => {
|
||||
logLine('logs/error.log', `Unhandled error: ${err?.message || err}`);
|
||||
return c.text('Internal Server Error', 500);
|
||||
});
|
||||
|
||||
// Default to 3005 for test instance; can be overridden by Environment=PORT
|
||||
const PORT = Number(process.env.PORT || 3005);
|
||||
|
||||
logLine('logs/server.log', `Starting frontend server (Redis version) on port ${PORT}`);
|
||||
|
||||
// Warm up cache on startup
|
||||
(async () => {
|
||||
try {
|
||||
if (isRedisConnected()) {
|
||||
logLine('logs/server.log', 'Warming up jobs cache...');
|
||||
const result = await fetchAllJobsFromPocketBase('');
|
||||
await setCache('jobs:all', result, 1800);
|
||||
logLine('logs/server.log', `✓ Cache warmed with ${result.items.length} jobs`);
|
||||
}
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `Cache warmup failed: ${(err as Error)?.message || String(err)}`);
|
||||
}
|
||||
})();
|
||||
|
||||
// Background refresh every 5 minutes to keep cache hot
|
||||
setInterval(async () => {
|
||||
try {
|
||||
if (isRedisConnected()) {
|
||||
await refreshJobsCache('jobs:all', 1800, '');
|
||||
}
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `Periodic refresh error: ${(err as Error)?.message || String(err)}`);
|
||||
}
|
||||
}, 5 * 60 * 1000); // Every 5 minutes
|
||||
|
||||
// Graceful shutdown logging
|
||||
const shutdown = (signal: string) => {
|
||||
logLine('logs/server.log', `Shutting down due to ${signal}`);
|
||||
process.exit(0);
|
||||
};
|
||||
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||
|
||||
export default {
|
||||
port: PORT,
|
||||
fetch: app.fetch,
|
||||
};
|
||||
@@ -1,600 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "job-info",
|
||||
"dependencies": {
|
||||
"dotenv": "^17.2.3",
|
||||
"hono": "^4.10.8",
|
||||
"ioredis": "^5.8.2",
|
||||
"pocketbase": "^0.26.5",
|
||||
"tailwind": "^4.0.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@babel/runtime": ["@babel/runtime@7.3.4", "", { "dependencies": { "regenerator-runtime": "^0.12.0" } }, "sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g=="],
|
||||
|
||||
"@ioredis/commands": ["@ioredis/commands@1.4.0", "", {}, "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
|
||||
|
||||
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.4", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A=="],
|
||||
|
||||
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
|
||||
|
||||
"ajv": ["ajv@6.10.0", "", { "dependencies": { "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg=="],
|
||||
|
||||
"amqplib": ["amqplib@0.5.2", "", { "dependencies": { "bitsyntax": "~0.0.4", "bluebird": "^3.4.6", "buffer-more-ints": "0.0.2", "readable-stream": "1.x >=1.1.9", "safe-buffer": "^5.0.1" } }, "sha512-l9mCs6LbydtHqRniRwYkKdqxVa6XMz3Vw1fh+2gJaaVgTM6Jk3o8RccAKWKtlhT1US5sWrFh+KKxsVUALURSIA=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
||||
|
||||
"app-root-path": ["app-root-path@2.1.0", "", {}, "sha512-z5BqVjscbjmJBybKlICogJR2jCr2q/Ixu7Pvui5D4y97i7FLsJlvEG9XOR/KJRlkxxZz7UaaS2TMwQh1dRJ2dA=="],
|
||||
|
||||
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
|
||||
|
||||
"array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="],
|
||||
|
||||
"array.prototype.reduce": ["array.prototype.reduce@1.0.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-array-method-boxes-properly": "^1.0.0", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "is-string": "^1.1.1" } }, "sha512-DwuEqgXFBwbmZSRqt3BpQigWNUoqw9Ml2dTWdF3B2zQlQX4OeUE0zyuzX0fX0IbTvjdkZbcBTU3idgpO78qkTw=="],
|
||||
|
||||
"arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
|
||||
|
||||
"asn1": ["asn1@0.2.3", "", {}, "sha512-6i37w/+EhlWlGUJff3T/Q8u1RGmP5wgbiwYnOnbOqvtrPxT63/sYFyP9RcpxtxGymtfA075IvmOnL7ycNOWl3w=="],
|
||||
|
||||
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
||||
|
||||
"async-limiter": ["async-limiter@1.0.1", "", {}, "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="],
|
||||
|
||||
"async-retry": ["async-retry@1.2.3", "", { "dependencies": { "retry": "0.12.0" } }, "sha512-tfDb02Th6CE6pJUF2gjW5ZVjsgwlucVXOEQMvEX9JgSJMs9gAX+Nz3xRuJBKuUYjTSYORqvDBORdAQ3LU59g7Q=="],
|
||||
|
||||
"autoprefixer": ["autoprefixer@10.4.23", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001760", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA=="],
|
||||
|
||||
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
||||
|
||||
"babel-runtime": ["babel-runtime@6.26.0", "", { "dependencies": { "core-js": "^2.4.0", "regenerator-runtime": "^0.11.0" } }, "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.8", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Y1fOuNDowLfgKOypdc9SPABfoWXuZHBOyCS4cD52IeZBhr4Md6CLLs6atcxVrzRmQ06E7hSlm5bHHApPKR/byA=="],
|
||||
|
||||
"basic-auth": ["basic-auth@2.0.1", "", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="],
|
||||
|
||||
"bitsyntax": ["bitsyntax@0.0.4", "", { "dependencies": { "buffer-more-ints": "0.0.2" } }, "sha512-Pav3HSZXD2NLQOWfJldY3bpJLt8+HS2nUo5Z1bLLmHg2vCE/cM1qfEvNjlYo7GgYQPneNr715Bh42i01ZHZPvw=="],
|
||||
|
||||
"bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
|
||||
|
||||
"body-parser": ["body-parser@1.18.3", "", { "dependencies": { "bytes": "3.0.0", "content-type": "~1.0.4", "debug": "2.6.9", "depd": "~1.1.2", "http-errors": "~1.6.3", "iconv-lite": "0.4.23", "on-finished": "~2.3.0", "qs": "6.5.2", "raw-body": "2.3.3", "type-is": "~1.6.16" } }, "sha512-YQyoqQG3sO8iCmf8+hyVpgHHOv0/hCEFiS4zTGUwTA1HjAFX66wRcNQrVCeJq9pgESMRvUAOvSil5MJlmccuKQ=="],
|
||||
|
||||
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
||||
|
||||
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||
|
||||
"buffer-more-ints": ["buffer-more-ints@0.0.2", "", {}, "sha512-PDgX2QJgUc5+Jb2xAoBFP5MxhtVUmZHR33ak+m/SDxRdCrbnX1BggRIaxiW7ImwfmO4iJeCQKN18ToSXWGjYkA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="],
|
||||
|
||||
"bytes": ["bytes@3.0.0", "", {}, "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw=="],
|
||||
|
||||
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||
|
||||
"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.30001760", "", {}, "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw=="],
|
||||
|
||||
"chalk": ["chalk@2.4.1", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ=="],
|
||||
|
||||
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"commands-events": ["commands-events@1.0.4", "", { "dependencies": { "@babel/runtime": "7.2.0", "formats": "1.0.0", "uuidv4": "2.0.0" } }, "sha512-HdP/+1Anoc7z+6L2h7nd4Imz54+LW+BjMGt30riBZrZ3ZeP/8el93wD8Jj8ltAaqVslqNgjX6qlhSBJwuDSmpg=="],
|
||||
|
||||
"comparejs": ["comparejs@1.0.0", "", {}, "sha512-Ue/Zd9aOucHzHXwaCe4yeHR7jypp7TKrIBZ5yls35nPNiVXlW14npmNVKM1ZaLlQTKZ6/4ewA//gYKHHIwCpOw=="],
|
||||
|
||||
"compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="],
|
||||
|
||||
"compression": ["compression@1.7.3", "", { "dependencies": { "accepts": "~1.3.5", "bytes": "3.0.0", "compressible": "~2.0.14", "debug": "2.6.9", "on-headers": "~1.0.1", "safe-buffer": "5.1.2", "vary": "~1.1.2" } }, "sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg=="],
|
||||
|
||||
"content-disposition": ["content-disposition@0.5.2", "", {}, "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA=="],
|
||||
|
||||
"content-type": ["content-type@1.0.4", "", {}, "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="],
|
||||
|
||||
"cookie": ["cookie@0.3.1", "", {}, "sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw=="],
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="],
|
||||
|
||||
"core-js": ["core-js@2.6.12", "", {}, "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ=="],
|
||||
|
||||
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
|
||||
|
||||
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
|
||||
|
||||
"crypto2": ["crypto2@2.0.0", "", { "dependencies": { "babel-runtime": "6.26.0", "node-rsa": "0.4.2", "util.promisify": "1.0.0" } }, "sha512-jdXdAgdILldLOF53md25FiQ6ybj2kUFTiRjs7msKTUoZrzgT/M1FPX5dYGJjbbwFls+RJIiZxNTC02DE/8y0ZQ=="],
|
||||
|
||||
"csstype": ["csstype@3.2.0", "", {}, "sha512-si++xzRAY9iPp60roQiFta7OFbhrgvcthrhlNAGeQptSY25uJjkfUV8OArC3KLocB8JT8ohz+qgxWCmz8RhjIg=="],
|
||||
|
||||
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
||||
|
||||
"data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="],
|
||||
|
||||
"data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
|
||||
|
||||
"datasette": ["datasette@1.0.1", "", { "dependencies": { "comparejs": "1.0.0", "eventemitter2": "5.0.1", "lodash": "4.17.5" } }, "sha512-aJdlCBToEJUP4M57r67r4V6tltwGKa3qetnjpBtXYIlqbX9tM9jsoDMxb4xd9AGjpp3282oHRmqI5Z8TVAU0Mg=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
|
||||
|
||||
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
||||
|
||||
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
|
||||
|
||||
"depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="],
|
||||
|
||||
"destroy": ["destroy@1.0.4", "", {}, "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg=="],
|
||||
|
||||
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
|
||||
|
||||
"draht": ["draht@1.0.1", "", { "dependencies": { "eventemitter2": "5.0.1" } }, "sha512-yNNHL864dniNmIE9ZKD++mKypiAUAvVZtyV0QrbXH/ak3ebzFqo5xsmRBRqV8pZVhImOSBiyq500Wcmrf44zAg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="],
|
||||
|
||||
"encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
|
||||
|
||||
"es-abstract": ["es-abstract@1.24.0", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg=="],
|
||||
|
||||
"es-array-method-boxes-properly": ["es-array-method-boxes-properly@1.0.0", "", {}, "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
|
||||
|
||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
||||
"eventemitter2": ["eventemitter2@5.0.1", "", {}, "sha512-5EM1GHXycJBS6mauYAbVKT1cVs7POKWb2NXD4Vyt8dDqeZa7LaDK1/sjtL+Zb0lzTpSNil4596Dyu97hz37QLg=="],
|
||||
|
||||
"express": ["express@4.16.4", "", { "dependencies": { "accepts": "~1.3.5", "array-flatten": "1.1.1", "body-parser": "1.18.3", "content-disposition": "0.5.2", "content-type": "~1.0.4", "cookie": "0.3.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "~1.1.2", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.1.1", "fresh": "0.5.2", "merge-descriptors": "1.0.1", "methods": "~1.1.2", "on-finished": "~2.3.0", "parseurl": "~1.3.2", "path-to-regexp": "0.1.7", "proxy-addr": "~2.0.4", "qs": "6.5.2", "range-parser": "~1.2.0", "safe-buffer": "5.1.2", "send": "0.16.2", "serve-static": "1.13.2", "setprototypeof": "1.1.0", "statuses": "~1.4.0", "type-is": "~1.6.16", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@2.0.1", "", {}, "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="],
|
||||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||
|
||||
"finalhandler": ["finalhandler@1.1.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "on-finished": "~2.3.0", "parseurl": "~1.3.2", "statuses": "~1.4.0", "unpipe": "~1.0.0" } }, "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg=="],
|
||||
|
||||
"find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="],
|
||||
|
||||
"flaschenpost": ["flaschenpost@1.1.3", "", { "dependencies": { "@babel/runtime": "7.2.0", "app-root-path": "2.1.0", "babel-runtime": "6.26.0", "chalk": "2.4.1", "find-root": "1.1.0", "lodash": "4.17.11", "moment": "2.22.2", "processenv": "1.1.0", "split2": "3.0.0", "stack-trace": "0.0.10", "stringify-object": "3.3.0", "untildify": "3.0.3", "util.promisify": "1.0.0", "varname": "2.0.3" }, "bin": { "flaschenpost-uncork": "dist/bin/flaschenpost-uncork.js", "flaschenpost-normalize": "dist/bin/flaschenpost-normalize.js" } }, "sha512-1VAYPvDsVBGFJyUrOa/6clnJwZYC3qVq9nJLcypy6lvaaNbo1wOQiH8HQ+4Fw/k51pVG7JHzSf5epb8lmIW86g=="],
|
||||
|
||||
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
||||
|
||||
"formats": ["formats@1.0.0", "", {}, "sha512-For0Y8egwEK96JgJo4NONErPhtl7H2QzeB2NYGmzeGeJ8a1JZqPgLYOtM3oJRCYhmgsdDFd6KGRYyfe37XY4Yg=="],
|
||||
|
||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
|
||||
"fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
|
||||
|
||||
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="],
|
||||
|
||||
"functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
|
||||
|
||||
"generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
|
||||
|
||||
"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-own-enumerable-property-symbols": ["get-own-enumerable-property-symbols@3.0.2", "", {}, "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
|
||||
|
||||
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
|
||||
|
||||
"has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
|
||||
|
||||
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
|
||||
|
||||
"has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"hase": ["hase@2.0.0", "", { "dependencies": { "@babel/runtime": "7.1.2", "amqplib": "0.5.2" } }, "sha512-L83pBR/oZvQQNjv4kw9aUpTqBxERPiY7B42jsmkt1VDeUaRVhYkEIKzkCqrppjtxHe2EZqzZJzuhMXsWsxYIsw=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hono": ["hono@4.10.8", "", {}, "sha512-DDT0A0r6wzhe8zCGoYOmMeuGu3dyTAE40HHjwUsWFTEy5WxK1x2WDSsBPlEXgPbRIFY6miDualuUDbasPogIww=="],
|
||||
|
||||
"http-errors": ["http-errors@1.6.3", "", { "dependencies": { "depd": "~1.1.2", "inherits": "2.0.3", "setprototypeof": "1.1.0", "statuses": ">= 1.4.0 < 2" } }, "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.4.23", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.3", "", {}, "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="],
|
||||
|
||||
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
||||
|
||||
"ioredis": ["ioredis@5.8.2", "", { "dependencies": { "@ioredis/commands": "1.4.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
|
||||
|
||||
"is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="],
|
||||
|
||||
"is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="],
|
||||
|
||||
"is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="],
|
||||
|
||||
"is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
|
||||
|
||||
"is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="],
|
||||
|
||||
"is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="],
|
||||
|
||||
"is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="],
|
||||
|
||||
"is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="],
|
||||
|
||||
"is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
|
||||
|
||||
"is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="],
|
||||
|
||||
"is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
|
||||
|
||||
"is-obj": ["is-obj@1.0.1", "", {}, "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg=="],
|
||||
|
||||
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
|
||||
|
||||
"is-regexp": ["is-regexp@1.0.0", "", {}, "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA=="],
|
||||
|
||||
"is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
|
||||
|
||||
"is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="],
|
||||
|
||||
"is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="],
|
||||
|
||||
"is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="],
|
||||
|
||||
"is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
|
||||
|
||||
"is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="],
|
||||
|
||||
"is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="],
|
||||
|
||||
"is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="],
|
||||
|
||||
"isarray": ["isarray@0.0.1", "", {}, "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="],
|
||||
|
||||
"json-lines": ["json-lines@1.0.0", "", { "dependencies": { "timer2": "1.0.0" } }, "sha512-ytuLZb4RBQb3bTRsG/QBenyIo5oHLpjeCVph3s2NnoAsZE9K6h+uR+OWpEOWV1UeHdX63tYctGppBpGAc+JNMA=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"jsonwebtoken": ["jsonwebtoken@8.5.0", "", { "dependencies": { "jws": "^3.2.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^5.6.0" } }, "sha512-IqEycp0znWHNA11TpYi77bVgyBO/pGESDh7Ajhas+u0ttkGkKYIIAjniL4Bw5+oVejVF+SYkaI7XKfwCCyeTuA=="],
|
||||
|
||||
"jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="],
|
||||
|
||||
"jws": ["jws@3.2.3", "", { "dependencies": { "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } }, "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g=="],
|
||||
|
||||
"limes": ["limes@2.0.0", "", { "dependencies": { "@babel/runtime": "7.3.4", "jsonwebtoken": "8.5.0" } }, "sha512-evWD0pnTgPX7QueaSoJl5JBUL30T1ZVzo34ke97tIKmeagqhBTYK/JkKL0vtG3MpNApw8ZY9TlbybfwEz9knBA=="],
|
||||
|
||||
"lodash": ["lodash@4.17.11", "", {}, "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="],
|
||||
|
||||
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
|
||||
|
||||
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
|
||||
|
||||
"lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="],
|
||||
|
||||
"lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
|
||||
|
||||
"lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="],
|
||||
|
||||
"lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="],
|
||||
|
||||
"lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
|
||||
|
||||
"lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="],
|
||||
|
||||
"lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="],
|
||||
|
||||
"lusca": ["lusca@1.6.1", "", { "dependencies": { "tsscmp": "^1.0.5" } }, "sha512-+JzvUMH/rsE/4XfHdDOl70bip0beRcHSviYATQM0vtls59uVtdn1JMu4iD7ZShBpAmFG8EnaA+PrYG9sECMIOQ=="],
|
||||
|
||||
"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.1", "", {}, "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="],
|
||||
|
||||
"methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="],
|
||||
|
||||
"mime": ["mime@1.4.1", "", { "bin": { "mime": "cli.js" } }, "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"moment": ["moment@2.22.2", "", {}, "sha512-LRvkBHaJGnrcWvqsElsOhHCzj8mU39wLx5pQ0pc6s153GynCTsPdGdqsVNKAQD9sKnWj11iF7TZx9fpLwdD3fw=="],
|
||||
|
||||
"morgan": ["morgan@1.9.1", "", { "dependencies": { "basic-auth": "~2.0.0", "debug": "2.6.9", "depd": "~1.1.2", "on-finished": "~2.3.0", "on-headers": "~1.0.1" } }, "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||
|
||||
"nocache": ["nocache@2.0.0", "", {}, "sha512-YdKcy2x0dDwOh+8BEuHvA+mnOKAhmMQDgKBOCUGaLpewdmsRYguYZSom3yA+/OrE61O/q+NMQANnun65xpI1Hw=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
||||
|
||||
"node-rsa": ["node-rsa@0.4.2", "", { "dependencies": { "asn1": "0.2.3" } }, "sha512-Bvso6Zi9LY4otIZefYrscsUpo2mUpiAVIEmSZV2q41sP8tHZoert3Yu6zv4f/RXJqMNZQKCtnhDugIuCma23YA=="],
|
||||
|
||||
"node-statsd": ["node-statsd@0.1.1", "", {}, "sha512-QDf6R8VXF56QVe1boek8an/Rb3rSNaxoFWb7Elpsv2m1+Noua1yy0F1FpKpK5VluF8oymWM4w764A4KsYL4pDg=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
|
||||
|
||||
"object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="],
|
||||
|
||||
"object.getownpropertydescriptors": ["object.getownpropertydescriptors@2.1.9", "", { "dependencies": { "array.prototype.reduce": "^1.0.8", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "gopd": "^1.2.0", "safe-array-concat": "^1.1.3" } }, "sha512-mt8YM6XwsTTovI+kdZdHSxoyF2DI59up034orlC9NfweclcWOt7CVascNNLp6U+bjFVCVCIh9PwS76tDM/rH8g=="],
|
||||
|
||||
"on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="],
|
||||
|
||||
"on-headers": ["on-headers@1.0.2", "", {}, "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="],
|
||||
|
||||
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"partof": ["partof@1.0.0", "", {}, "sha512-+TXdhKCySpJDynCxgAPoGVyAkiK3QPusQ63/BdU5t68QcYzyU6zkP/T7F3gkMQBVUYqdWEADKa6Kx5zg8QIKrg=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@0.1.7", "", {}, "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"pocketbase": ["pocketbase@0.26.5", "", {}, "sha512-SXcq+sRvVpNxfLxPB1C+8eRatL7ZY4o3EVl/0OdE3MeR9fhPyZt0nmmxLqYmkLvXCN9qp3lXWV/0EUYb3MmMXQ=="],
|
||||
|
||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||
|
||||
"processenv": ["processenv@1.1.0", "", { "dependencies": { "babel-runtime": "6.26.0" } }, "sha512-SymqIsn8GjEUy8nG7HiyEjgbfk1xFosRIakUX1NHLpriq3vVpKniGrr9RdMWCaGYWByIovbRt2f/WvmP/IOApQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"qs": ["qs@6.5.2", "", {}, "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"raw-body": ["raw-body@2.3.3", "", { "dependencies": { "bytes": "3.0.0", "http-errors": "1.6.3", "iconv-lite": "0.4.23", "unpipe": "1.0.0" } }, "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
|
||||
|
||||
"redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="],
|
||||
|
||||
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
||||
|
||||
"regenerator-runtime": ["regenerator-runtime@0.12.1", "", {}, "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="],
|
||||
|
||||
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
|
||||
|
||||
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
|
||||
|
||||
"safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
||||
|
||||
"safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="],
|
||||
|
||||
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
|
||||
|
||||
"send": ["send@0.16.2", "", { "dependencies": { "debug": "2.6.9", "depd": "~1.1.2", "destroy": "~1.0.4", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "~1.6.2", "mime": "1.4.1", "ms": "2.0.0", "on-finished": "~2.3.0", "range-parser": "~1.2.0", "statuses": "~1.4.0" } }, "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw=="],
|
||||
|
||||
"serve-static": ["serve-static@1.13.2", "", { "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "parseurl": "~1.3.2", "send": "0.16.2" } }, "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw=="],
|
||||
|
||||
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
||||
|
||||
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
|
||||
|
||||
"set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="],
|
||||
|
||||
"setprototypeof": ["setprototypeof@1.1.0", "", {}, "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="],
|
||||
|
||||
"sha-1": ["sha-1@0.1.1", "", {}, "sha512-dexizf3hB7d4Jq6Cd0d/NYQiqgEqIfZIpuMfwPfvSb6h06DZKmHyUe55jYwpHC12R42wpqXO6ouhiBpRzIcD/g=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"split2": ["split2@3.0.0", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-Cp7G+nUfKJyHCrAI8kze3Q00PFGEG1pMgrAlTFlDbn+GW24evSZHJuMl+iUJx1w/NTRDeBiTgvwnf6YOt94FMw=="],
|
||||
|
||||
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
|
||||
|
||||
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
|
||||
|
||||
"statuses": ["statuses@1.4.0", "", {}, "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="],
|
||||
|
||||
"stethoskop": ["stethoskop@1.0.0", "", { "dependencies": { "node-statsd": "0.1.1" } }, "sha512-4JnZ+UmTs9SFfDjSHFlD/EoXcb1bfwntkt4h1ipNGrpxtRzmHTxOmdquCJvIrVu608Um7a09cGX0ZSOSllWJNQ=="],
|
||||
|
||||
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
||||
|
||||
"string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="],
|
||||
|
||||
"string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="],
|
||||
|
||||
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"stringify-object": ["stringify-object@3.3.0", "", { "dependencies": { "get-own-enumerable-property-symbols": "^3.0.0", "is-obj": "^1.0.1", "is-regexp": "^1.0.0" } }, "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw=="],
|
||||
|
||||
"supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="],
|
||||
|
||||
"tailwind": ["tailwind@4.0.0", "", { "dependencies": { "@babel/runtime": "7.3.4", "ajv": "6.10.0", "app-root-path": "2.1.0", "async-retry": "1.2.3", "body-parser": "1.18.3", "commands-events": "1.0.4", "compression": "1.7.3", "content-type": "1.0.4", "cors": "2.8.5", "crypto2": "2.0.0", "datasette": "1.0.1", "draht": "1.0.1", "express": "4.16.4 ", "flaschenpost": "1.1.3", "hase": "2.0.0", "json-lines": "1.0.0", "limes": "2.0.0", "lodash": "4.17.11", "lusca": "1.6.1", "morgan": "1.9.1", "nocache": "2.0.0", "partof": "1.0.0", "processenv": "1.1.0", "stethoskop": "1.0.0", "timer2": "1.0.0", "uuidv4": "3.0.1", "ws": "6.2.0" } }, "sha512-LlUNoD/5maFG1h5kQ6/hXfFPdcnYw+1Z7z+kUD/W/E71CUMwcnrskxiBM8c3G8wmPsD1VvCuqGYMHviI8+yrmg=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
|
||||
|
||||
"timer2": ["timer2@1.0.0", "", {}, "sha512-UOZql+P2ET0da+B7V3/RImN3IhC5ghb+9cpecfUhmYGIm0z73dDr3A781nBLnFYmRzeT1AmoT4w9Lgr8n7n7xg=="],
|
||||
|
||||
"tsscmp": ["tsscmp@1.0.6", "", {}, "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA=="],
|
||||
|
||||
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
|
||||
|
||||
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
|
||||
|
||||
"typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="],
|
||||
|
||||
"typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="],
|
||||
|
||||
"typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"untildify": ["untildify@3.0.3", "", {}, "sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"util.promisify": ["util.promisify@1.0.0", "", { "dependencies": { "define-properties": "^1.1.2", "object.getownpropertydescriptors": "^2.0.3" } }, "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA=="],
|
||||
|
||||
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
|
||||
|
||||
"uuid": ["uuid@3.3.2", "", { "bin": { "uuid": "./bin/uuid" } }, "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="],
|
||||
|
||||
"uuidv4": ["uuidv4@3.0.1", "", { "dependencies": { "uuid": "3.3.2" } }, "sha512-PPzksdWRl2a5C9hrs3OOYrArTeyoR0ftJ3jtOy+BnVHkT2UlrrzPNt9nTdiGuxmQItHM/AcTXahwZZC57Njojg=="],
|
||||
|
||||
"varname": ["varname@2.0.3", "", {}, "sha512-+DofT9mJAUALhnr9ipZ5Z2icwaEZ7DAajOZT4ffXy3MQqnXtG3b7atItLQEJCkfcJTOf9WcsywneOEibD4eqJg=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
|
||||
|
||||
"which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="],
|
||||
|
||||
"which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="],
|
||||
|
||||
"which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="],
|
||||
|
||||
"ws": ["ws@6.2.0", "", { "dependencies": { "async-limiter": "~1.0.0" } }, "sha512-deZYUNlt2O4buFCa3t5bKLf8A7FPP/TVjwOeVNpw818Ma5nk4MLXls2eoEGS39o8119QIYxTrTDoPQ5B/gTD6w=="],
|
||||
|
||||
"amqplib/readable-stream": ["readable-stream@1.1.14", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "0.0.1", "string_decoder": "~0.10.x" } }, "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ=="],
|
||||
|
||||
"babel-runtime/regenerator-runtime": ["regenerator-runtime@0.11.1", "", {}, "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="],
|
||||
|
||||
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"commands-events/@babel/runtime": ["@babel/runtime@7.2.0", "", { "dependencies": { "regenerator-runtime": "^0.12.0" } }, "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg=="],
|
||||
|
||||
"commands-events/uuidv4": ["uuidv4@2.0.0", "", { "dependencies": { "sha-1": "0.1.1", "uuid": "3.3.2" } }, "sha512-sAUlwUVepcVk6bwnaW/oi6LCwMdueako5QQzRr90ioAVVcms6p1mV0PaSxK8gyAC4CRvKddsk217uUpZUbKd2Q=="],
|
||||
|
||||
"compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"datasette/lodash": ["lodash@4.17.5", "", {}, "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw=="],
|
||||
|
||||
"ecdsa-sig-formatter/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"flaschenpost/@babel/runtime": ["@babel/runtime@7.2.0", "", { "dependencies": { "regenerator-runtime": "^0.12.0" } }, "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg=="],
|
||||
|
||||
"hase/@babel/runtime": ["@babel/runtime@7.1.2", "", { "dependencies": { "regenerator-runtime": "^0.12.0" } }, "sha512-Y3SCjmhSupzFB6wcv1KmmFucH6gDVnI30WjOcicV10ju0cZjak3Jcs67YLIXBrmZYw1xCrVeJPbycFwrqNyxpg=="],
|
||||
|
||||
"mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"morgan/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"safe-array-concat/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
||||
"safe-push-apply/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
||||
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"send/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
||||
"amqplib/readable-stream/string_decoder": ["string_decoder@0.10.31", "", {}, "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="],
|
||||
|
||||
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"morgan/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 1.6 MiB |
@@ -1,39 +0,0 @@
|
||||
// PocketBase auth bootstrap shared by pages
|
||||
(function (global) {
|
||||
const pb = new PocketBase('https://pocketbase.ccllc.pro');
|
||||
|
||||
function authHeaders() {
|
||||
if (pb.authStore.isValid && pb.authStore.token) {
|
||||
return { Authorization: `Bearer ${pb.authStore.token}` };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function getDisplayName() {
|
||||
const model = pb.authStore.model || {};
|
||||
return (
|
||||
model.display_name ||
|
||||
model.name ||
|
||||
model.email ||
|
||||
model.username ||
|
||||
'Unknown'
|
||||
);
|
||||
}
|
||||
|
||||
function getEmail() {
|
||||
const model = pb.authStore.model || {};
|
||||
// Prefer email, but fall back to username if email missing
|
||||
return model.email || model.username || null;
|
||||
}
|
||||
|
||||
async function ensureAuth() {
|
||||
if (pb.authStore.isValid) return true;
|
||||
const path = new URL(window.location.href).pathname;
|
||||
if (!path.endsWith('signin.html')) {
|
||||
window.location.href = 'signin.html';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
global.Auth = { pb, authHeaders, ensureAuth, getDisplayName, getEmail };
|
||||
})(window);
|
||||
@@ -1,5 +0,0 @@
|
||||
/* Generated Tailwind CSS - Basic version */
|
||||
/* This will be updated by tailwind CLI on dev */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -1,131 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sign In</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script>
|
||||
<script src="auth.js"></script>
|
||||
</head>
|
||||
<body class="font-sans bg-gradient-to-br from-indigo-500 to-purple-600 min-h-screen flex items-center justify-center">
|
||||
<div class="bg-white p-12 rounded-2xl shadow-2xl text-center max-w-md w-[90%]">
|
||||
<h1 class="text-gray-800 mb-2 text-3xl">Welcome</h1>
|
||||
<p class="text-gray-600 mb-8">Sign in with your Microsoft account</p>
|
||||
|
||||
<button id="loginBtn" onclick="login()" class="bg-blue-600 text-white border-none py-3.5 px-8 text-base rounded-md cursor-pointer transition-colors duration-200 font-medium hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed">Sign in with Microsoft</button>
|
||||
|
||||
<div class="text-red-600 mt-4 hidden" id="error"></div>
|
||||
|
||||
<div class="hidden mt-8 text-left bg-gray-50 p-6 rounded-lg" id="userInfo">
|
||||
<h2 class="text-lg mb-4 text-gray-800">Signed in</h2>
|
||||
<p class="mb-3 text-gray-600 break-all"><strong class="text-gray-800">Name:</strong> <span id="displayName"></span></p>
|
||||
<p class="mb-3 text-gray-600 break-all"><strong class="text-gray-800">Email:</strong> <span id="email"></span></p>
|
||||
|
||||
<button id="continueBtn" onclick="goToIndex()" class="hidden mt-4 bg-blue-600 text-white border-none py-3.5 px-8 text-base rounded-md cursor-pointer transition-colors duration-200 font-medium hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed w-full">Continue to Job Info</button>
|
||||
<button onclick="logout()" class="bg-gray-600 text-white border-none py-3.5 px-8 text-base rounded-md cursor-pointer transition-colors duration-200 font-medium hover:bg-gray-700 mt-4">Sign out</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const { pb, getDisplayName } = window.Auth;
|
||||
|
||||
const GRAPH_TOKEN_KEY = 'graphAccessToken';
|
||||
const extractGraphToken = (authData) => {
|
||||
return (
|
||||
authData?.meta?.graphAccessToken ||
|
||||
authData?.meta?.graph_token ||
|
||||
authData?.meta?.graphToken ||
|
||||
authData?.meta?.accessToken ||
|
||||
authData?.meta?.access_token ||
|
||||
authData?.meta?.token ||
|
||||
authData?.meta?.rawToken ||
|
||||
authData?.meta?.authData?.access_token ||
|
||||
''
|
||||
);
|
||||
};
|
||||
|
||||
const loginBtn = document.getElementById('loginBtn');
|
||||
const errorEl = document.getElementById('error');
|
||||
const userInfo = document.getElementById('userInfo');
|
||||
const continueBtn = document.getElementById('continueBtn');
|
||||
const GRAPH_REAUTH_FLAG = 'graphReauthAttempted';
|
||||
|
||||
function goToIndex() {
|
||||
window.location.href = 'index.html';
|
||||
}
|
||||
|
||||
function showAuthedUI(displayName, email) {
|
||||
loginBtn.classList.add('hidden');
|
||||
loginBtn.disabled = false;
|
||||
loginBtn.textContent = 'Sign in with Microsoft';
|
||||
userInfo.classList.remove('hidden');
|
||||
continueBtn.classList.remove('hidden');
|
||||
document.getElementById('displayName').textContent = displayName || 'Unknown';
|
||||
document.getElementById('email').textContent = email || 'Not available';
|
||||
}
|
||||
|
||||
async function login() {
|
||||
loginBtn.disabled = true;
|
||||
loginBtn.textContent = 'Signing in...';
|
||||
errorEl.classList.add('hidden');
|
||||
|
||||
try {
|
||||
const authData = await pb.collection('users').authWithOAuth2({
|
||||
provider: 'microsoft',
|
||||
});
|
||||
displayUserInfo(authData);
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
errorEl.textContent = error.message || 'Authentication failed. Please try again.';
|
||||
errorEl.classList.remove('hidden');
|
||||
loginBtn.disabled = false;
|
||||
loginBtn.textContent = 'Sign in with Microsoft';
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
pb.authStore.clear();
|
||||
localStorage.removeItem(GRAPH_TOKEN_KEY);
|
||||
loginBtn.classList.remove('hidden');
|
||||
userInfo.classList.add('hidden');
|
||||
continueBtn.classList.add('hidden');
|
||||
loginBtn.disabled = false;
|
||||
loginBtn.textContent = 'Sign in with Microsoft';
|
||||
}
|
||||
|
||||
function displayUserInfo(authData) {
|
||||
console.log('Auth response:', authData);
|
||||
console.log('Auth meta:', authData?.meta);
|
||||
const displayName = getDisplayName();
|
||||
const email = authData?.meta?.rawUser?.email || authData?.record?.email || pb.authStore.model?.email || 'Not available';
|
||||
const graphToken = extractGraphToken(authData);
|
||||
console.log('Extracted Graph token:', graphToken ? graphToken.substring(0, 30) + '...' : 'NOT FOUND');
|
||||
if (graphToken) {
|
||||
localStorage.setItem(GRAPH_TOKEN_KEY, graphToken);
|
||||
localStorage.removeItem(GRAPH_REAUTH_FLAG);
|
||||
}
|
||||
showAuthedUI(displayName, email);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const reauth = params.get('reauth') === '1';
|
||||
if (pb.authStore.isValid) {
|
||||
try {
|
||||
const authData = await pb.collection('users').authRefresh();
|
||||
displayUserInfo(authData);
|
||||
// If we came from a reauth flow and now have a token, go back automatically
|
||||
const hasToken = localStorage.getItem(GRAPH_TOKEN_KEY);
|
||||
if (reauth && hasToken) {
|
||||
window.location.href = 'index.html';
|
||||
}
|
||||
} catch (err) {
|
||||
pb.authStore.clear();
|
||||
logout();
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,3 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"name": "job-info-pb",
|
||||
"version": "1.0.0-beta3",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "bun --watch backend/server.ts",
|
||||
"start": "bun run backend/server.ts",
|
||||
"build:css": "tailwindcss -i input.css -o frontend/output.css --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^17.2.3",
|
||||
"hono": "^4.10.8",
|
||||
"pocketbase": "^0.26.5",
|
||||
"tailwind": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
'./frontend/**/*.{html,js}',
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
# Universal Auth Module
|
||||
|
||||
## Overview
|
||||
This module provides a standardized, immutable authentication and token management system for any web project using PocketBase and Microsoft Graph. It supports user and agent tokens, delegated and app-only flows, and a consistent popup UI for login prompts.
|
||||
|
||||
## Features
|
||||
- Pattern-based: Choose 'pb-user', 'pb-agent', 'graph-user', 'graph-agent', or any combination (e.g., 'pb-user+graph-agent') for your project needs
|
||||
- Supports both user and agent tokens for PocketBase and Microsoft Graph
|
||||
- Special handling for Valkey/Redis agent login and token storage (for backend/automation flows)
|
||||
- Single source of truth for token storage, retrieval, and refresh
|
||||
- Consistent, hidden popup for all user login prompts
|
||||
- Extracts user info (display name, email) from PocketBase
|
||||
- Works in any frontend project with PocketBase
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Include in your project
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script>
|
||||
<script src="auth/auth-universal.js"></script>
|
||||
```
|
||||
|
||||
### 2. Initialize the pattern
|
||||
```js
|
||||
// Choose your pattern:
|
||||
// 'pb-user' - PocketBase user login
|
||||
// 'pb-agent' - PocketBase agent/service login
|
||||
// 'graph-user' - Microsoft Graph delegated (user) login
|
||||
// 'graph-agent' - Microsoft Graph agent/app-only (client credentials)
|
||||
// Combine as needed: 'pb-user+graph-agent', 'pb-agent+graph-user', etc.
|
||||
Auth.use('pb-user+graph-agent');
|
||||
```
|
||||
|
||||
### 3. Get and use tokens
|
||||
```js
|
||||
const pbToken = await Auth.ensureToken('pb');
|
||||
const graphToken = await Auth.ensureToken('graph');
|
||||
const userInfo = Auth.getUserInfo();
|
||||
```
|
||||
|
||||
### 4. Handle reauthentication
|
||||
- If a token is missing or expired, Auth will automatically show a popup for login.
|
||||
- The popup is hidden at all other times.
|
||||
|
||||
### 5. Clear tokens (for testing or logout)
|
||||
```js
|
||||
Auth.clearToken('pb');
|
||||
Auth.clearToken('graph');
|
||||
```
|
||||
|
||||
## Secrets and Environment
|
||||
- For frontend, all secrets are handled by PocketBase OAuth (no client secrets exposed).
|
||||
- For backend/agent flows, use environment variables and server-side code to store secrets securely.
|
||||
- The module does not expose or require secrets in the frontend.
|
||||
|
||||
## Example Test Page
|
||||
See `auth-test.html` for a safe way to test all flows without affecting your main app.
|
||||
|
||||
## Rules for Copilot Instructions
|
||||
- Always use Auth.use() to set the pattern before requesting tokens. Supported types: 'pb-user', 'pb-agent', 'graph-user', 'graph-agent', or any combination.
|
||||
- Always use Auth.ensureToken(type) to get a valid token; it will handle refresh and prompt if needed.
|
||||
- Never store secrets in frontend code; use PocketBase OAuth for user login.
|
||||
- All login prompts must use the provided popup, never custom dialogs.
|
||||
- Token keys are immutable: 'pbUserToken', 'pbAgentToken', 'graphUserToken', 'graphAgentToken'.
|
||||
- User info extraction must use Auth.getUserInfo().
|
||||
- For agent/app-only tokens, use backend code, environment variables, and optionally Valkey/Redis for secure storage and retrieval.
|
||||
|
||||
## FAQ
|
||||
- **Can I use this in any project?** Yes, as long as you include PocketBase and this module.
|
||||
- **Does it work for both user and agent tokens?** Yes, with the correct pattern and backend support for agent tokens (including Valkey/Redis for automation).
|
||||
- **Is the popup customizable?** The style can be changed, but the flow must remain consistent for all projects.
|
||||
- **How are secrets handled?** Only via backend environment variables for agent tokens; never exposed in frontend.
|
||||
|
||||
---
|
||||
For further integration, see the comments in `auth-universal.js` and the example test page.
|
||||
@@ -1,85 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Universal Auth Test</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script>
|
||||
<script src="auth-universal.js"></script>
|
||||
<style>
|
||||
body { font-family: sans-serif; background: #f3f4f6; margin: 0; padding: 2em; }
|
||||
.test-block { background: #fff; border-radius: 1em; box-shadow: 0 2px 12px #0001; padding: 2em; margin-bottom: 2em; }
|
||||
.pattern-btn { margin: 0.2em 0.5em 0.2em 0; padding: 0.5em 1.2em; border-radius: 0.5em; border: none; background: #2563eb; color: #fff; cursor: pointer; font-size: 1em; }
|
||||
.pattern-btn.active { background: #059669; }
|
||||
#output { font-family: monospace; background: #f9fafb; border-radius: 0.5em; padding: 1em; margin-top: 1em; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="test-block">
|
||||
<h2>Choose Auth Pattern</h2>
|
||||
<div id="patterns"></div>
|
||||
<div style="margin-top:1em;">
|
||||
<button onclick="clearAll()" class="pattern-btn" style="background:#b91c1c;">Clear All Tokens</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="test-block">
|
||||
<h2>Test Actions</h2>
|
||||
<button onclick="testToken('pb-user')" class="pattern-btn">Test PB User</button>
|
||||
<button onclick="testToken('pb-agent')" class="pattern-btn">Test PB Agent</button>
|
||||
<button onclick="testToken('graph-user')" class="pattern-btn">Test Graph User</button>
|
||||
<button onclick="testToken('graph-agent')" class="pattern-btn">Test Graph Agent</button>
|
||||
<button onclick="showUserInfo()" class="pattern-btn" style="background:#6d28d9;">Show User Info</button>
|
||||
<div id="output"></div>
|
||||
</div>
|
||||
<script>
|
||||
const patterns = [
|
||||
'pb-user',
|
||||
'pb-agent',
|
||||
'graph-user',
|
||||
'graph-agent',
|
||||
'pb-user+graph-user',
|
||||
'pb-user+graph-agent',
|
||||
'pb-agent+graph-user',
|
||||
'pb-agent+graph-agent',
|
||||
'pb-user+pb-agent+graph-user+graph-agent'
|
||||
];
|
||||
let currentPattern = patterns[0];
|
||||
function setPattern(p) {
|
||||
currentPattern = p;
|
||||
Auth.use(p);
|
||||
document.querySelectorAll('.pattern-btn').forEach(btn => btn.classList.remove('active'));
|
||||
document.querySelectorAll('.pattern-btn[data-pattern="' + p + '"]').forEach(btn => btn.classList.add('active'));
|
||||
document.getElementById('output').textContent = `Pattern set: ${p}`;
|
||||
}
|
||||
function renderPatterns() {
|
||||
const el = document.getElementById('patterns');
|
||||
el.innerHTML = '';
|
||||
patterns.forEach(p => {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = p;
|
||||
btn.className = 'pattern-btn' + (p === currentPattern ? ' active' : '');
|
||||
btn.setAttribute('data-pattern', p);
|
||||
btn.onclick = () => setPattern(p);
|
||||
el.appendChild(btn);
|
||||
});
|
||||
}
|
||||
renderPatterns();
|
||||
setPattern(currentPattern);
|
||||
async function testToken(type) {
|
||||
try {
|
||||
const token = await Auth.ensureToken(type);
|
||||
document.getElementById('output').textContent = `${type} token: ${token ? token.substring(0, 40) + '...' : 'NONE'}`;
|
||||
} catch (err) {
|
||||
document.getElementById('output').textContent = `Error: ${err.message}`;
|
||||
}
|
||||
}
|
||||
function clearAll() {
|
||||
['pb-user','pb-agent','graph-user','graph-agent'].forEach(Auth.clearToken);
|
||||
document.getElementById('output').textContent = 'All tokens cleared.';
|
||||
}
|
||||
function showUserInfo() {
|
||||
const info = Auth.getUserInfo();
|
||||
document.getElementById('output').textContent = 'User Info: ' + JSON.stringify(info, null, 2);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,220 +0,0 @@
|
||||
// Universal Auth Module: Immutable, Pattern-Based, Project-Agnostic
|
||||
// Usage: import and call Auth.use('pb-graph') or Auth.use('pb') or Auth.use('graph')
|
||||
// Provides: getToken(type), ensureToken(type), refreshToken(type), getUserInfo(), promptReauth()
|
||||
// Token types: 'pb' (PocketBase user/agent), 'graph' (Graph delegated), 'graphAgent' (Graph app-only)
|
||||
// All storage, retrieval, refresh, and UI are standardized and immutable
|
||||
|
||||
const TOKEN_KEYS = {
|
||||
'pb-user': 'pbUserToken',
|
||||
'pb-agent': 'pbAgentToken',
|
||||
'graph-user': 'graphUserToken',
|
||||
'graph-agent': 'graphAgentToken'
|
||||
};
|
||||
|
||||
function parsePattern(pattern) {
|
||||
return pattern.split('+').map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
window.Auth = (() => {
|
||||
let pattern = 'pb-user'; // default
|
||||
let enabledTypes = ['pb-user'];
|
||||
let pb = null;
|
||||
let popup = null;
|
||||
let agentCreds = { email: '', password: '' };
|
||||
let superuserCreds = { email: '', password: '' };
|
||||
|
||||
// --- Setup ---
|
||||
function use(type) {
|
||||
pattern = type;
|
||||
enabledTypes = parsePattern(type);
|
||||
if (enabledTypes.some(t => t.startsWith('pb'))) {
|
||||
pb = window.PocketBase ? new PocketBase('https://pocketbase.ccllc.pro') : null;
|
||||
}
|
||||
// Only one-time setup
|
||||
if (!document.getElementById('authPopup')) {
|
||||
createPopup();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Token Storage ---
|
||||
function getToken(type) {
|
||||
return localStorage.getItem(TOKEN_KEYS[type]) || '';
|
||||
}
|
||||
function setToken(type, value) {
|
||||
if (value) localStorage.setItem(TOKEN_KEYS[type], value);
|
||||
}
|
||||
function clearToken(type) {
|
||||
localStorage.removeItem(TOKEN_KEYS[type]);
|
||||
}
|
||||
|
||||
// --- Info Extraction ---
|
||||
function getUserInfo() {
|
||||
if (pb && pb.authStore.model) {
|
||||
return {
|
||||
displayName: pb.authStore.model.display_name || pb.authStore.model.name || pb.authStore.model.email || pb.authStore.model.username || 'Unknown',
|
||||
email: pb.authStore.model.email || pb.authStore.model.username || null
|
||||
};
|
||||
}
|
||||
return { displayName: '', email: '' };
|
||||
}
|
||||
|
||||
// --- Token Ensure/Refresh ---
|
||||
async function ensureToken(type) {
|
||||
let token = getToken(type);
|
||||
if (token) return token;
|
||||
if (type === 'pb-user' && pb) {
|
||||
if (pb.authStore.isValid) {
|
||||
setToken('pb-user', pb.authStore.token);
|
||||
return pb.authStore.token;
|
||||
}
|
||||
// Choose login method: email/password or OAuth
|
||||
if (pattern.includes('pb-user-oauth')) {
|
||||
return await promptReauth('pb-user-oauth');
|
||||
} else {
|
||||
return await promptReauth('pb-user');
|
||||
}
|
||||
}
|
||||
if (type === 'pb-superuser' && pb) {
|
||||
// Prompt for superuser credentials
|
||||
await promptReauth('pb-superuser');
|
||||
// Use superuser credentials to login
|
||||
const authData = await pb.collection('_superuser').authWithPassword(superuserCreds.email, superuserCreds.password);
|
||||
setToken('pb-superuser', pb.authStore.token);
|
||||
return pb.authStore.token;
|
||||
}
|
||||
if (type === 'pb-agent' && pb) {
|
||||
// Prompt for agent credentials if not set
|
||||
if (!agentCreds.email || !agentCreds.password) {
|
||||
await promptReauth('pb-agent');
|
||||
}
|
||||
// Use agent credentials to login
|
||||
const authData = await pb.collection('users').authWithPassword(agentCreds.email, agentCreds.password);
|
||||
setToken('pb-agent', pb.authStore.token);
|
||||
return pb.authStore.token;
|
||||
}
|
||||
if (type === 'graph-user' && pb) {
|
||||
// OAuth only
|
||||
return await promptReauth('graph-user');
|
||||
}
|
||||
if (type === 'graph-agent') {
|
||||
// Prompt for agent credentials if not set
|
||||
if (!agentCreds.email || !agentCreds.password) {
|
||||
await promptReauth('graph-agent');
|
||||
}
|
||||
// Use agent credentials to get token (simulate, real flow is backend)
|
||||
setToken('graph-agent', 'AGENT_TOKEN_' + agentCreds.email);
|
||||
return getToken('graph-agent');
|
||||
}
|
||||
throw new Error('Unknown token type');
|
||||
}
|
||||
|
||||
async function refreshToken(type) {
|
||||
if (type === 'graph-user' && pb) {
|
||||
try {
|
||||
const authData = await pb.collection('users').authRefresh();
|
||||
const meta = authData?.meta || pb?.authStore?.model?.meta || {};
|
||||
const token = meta.graphAccessToken || meta.graph_token || meta.graphToken || meta.accessToken || meta.access_token || meta.token || meta.rawToken || meta?.authData?.access_token || '';
|
||||
setToken('graph-user', token);
|
||||
return token || '';
|
||||
} catch (err) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
// Add refresh logic for other types as needed
|
||||
return '';
|
||||
}
|
||||
|
||||
// --- Reauth Popup ---
|
||||
function createPopup() {
|
||||
popup = document.createElement('div');
|
||||
popup.id = 'authPopup';
|
||||
popup.style = 'display:none;position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.4);z-index:9999;align-items:center;justify-content:center;';
|
||||
popup.innerHTML = `
|
||||
<div style="background:#fff;padding:2em 2.5em;border-radius:1em;box-shadow:0 2px 24px #0002;text-align:center;max-width:350px;margin:auto;">
|
||||
<h2 style="font-size:1.3em;margin-bottom:1em;">Sign In Required</h2>
|
||||
<div id="authPopupForms"></div>
|
||||
<div id="authPopupError" style="color:#b91c1c;margin-top:1em;display:none;"></div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(popup);
|
||||
}
|
||||
|
||||
function showForm(type) {
|
||||
const forms = {
|
||||
'pb-user': `<input id="pbUserEmail" type="email" placeholder="Email" style="width:90%;margin-bottom:0.5em;"><br><input id="pbUserPassword" type="password" placeholder="Password" style="width:90%;margin-bottom:0.5em;"><br><button id="authPopupBtn" style="background:#2563eb;color:#fff;padding:0.7em 2em;border:none;border-radius:0.5em;font-size:1em;cursor:pointer;">Sign in</button>`,
|
||||
'pb-user-oauth': `<button id="authPopupBtn" style="background:#2563eb;color:#fff;padding:0.7em 2em;border:none;border-radius:0.5em;font-size:1em;cursor:pointer;">Sign in with Microsoft</button>`,
|
||||
'pb-superuser': `<input id="pbSuperuserEmail" type="email" placeholder="Superuser Email" style="width:90%;margin-bottom:0.5em;"><br><input id="pbSuperuserPassword" type="password" placeholder="Superuser Password" style="width:90%;margin-bottom:0.5em;"><br><button id="authPopupBtn" style="background:#d97706;color:#fff;padding:0.7em 2em;border:none;border-radius:0.5em;font-size:1em;cursor:pointer;">Sign in as Superuser</button>`,
|
||||
'pb-agent': `<input id="pbAgentEmail" type="email" placeholder="Agent Email" style="width:90%;margin-bottom:0.5em;"><br><input id="pbAgentPassword" type="password" placeholder="Agent Password" style="width:90%;margin-bottom:0.5em;"><br><button id="authPopupBtn" style="background:#059669;color:#fff;padding:0.7em 2em;border:none;border-radius:0.5em;font-size:1em;cursor:pointer;">Sign in as Agent</button>`,
|
||||
'graph-user': `<button id="authPopupBtn" style="background:#2563eb;color:#fff;padding:0.7em 2em;border:none;border-radius:0.5em;font-size:1em;cursor:pointer;">Sign in with Microsoft</button>`,
|
||||
'graph-agent': `<input id="graphAgentEmail" type="email" placeholder="Agent Email" style="width:90%;margin-bottom:0.5em;"><br><input id="graphAgentPassword" type="password" placeholder="Agent Password" style="width:90%;margin-bottom:0.5em;"><br><button id="authPopupBtn" style="background:#059669;color:#fff;padding:0.7em 2em;border:none;border-radius:0.5em;font-size:1em;cursor:pointer;">Sign in as Graph Agent</button>`
|
||||
};
|
||||
document.getElementById('authPopupForms').innerHTML = forms[type] || '';
|
||||
}
|
||||
|
||||
async function promptReauth(type) {
|
||||
showForm(type);
|
||||
popup.style.display = 'flex';
|
||||
return new Promise((resolve, reject) => {
|
||||
document.getElementById('authPopupBtn').onclick = async () => {
|
||||
try {
|
||||
if (type === 'pb-user') {
|
||||
const email = document.getElementById('pbUserEmail').value;
|
||||
const password = document.getElementById('pbUserPassword').value;
|
||||
const authData = await pb.collection('users').authWithPassword(email, password);
|
||||
setToken('pb-user', pb.authStore.token);
|
||||
popup.style.display = 'none';
|
||||
resolve(pb.authStore.token);
|
||||
} else if (type === 'pb-user-oauth') {
|
||||
const authData = await pb.collection('users').authWithOAuth2({ provider: 'microsoft' });
|
||||
setToken('pb-user', authData?.meta?.graphAccessToken || '');
|
||||
popup.style.display = 'none';
|
||||
resolve(authData?.meta?.graphAccessToken || '');
|
||||
} else if (type === 'pb-superuser') {
|
||||
superuserCreds.email = document.getElementById('pbSuperuserEmail').value;
|
||||
superuserCreds.password = document.getElementById('pbSuperuserPassword').value;
|
||||
popup.style.display = 'none';
|
||||
resolve();
|
||||
} else if (type === 'pb-agent') {
|
||||
agentCreds.email = document.getElementById('pbAgentEmail').value;
|
||||
agentCreds.password = document.getElementById('pbAgentPassword').value;
|
||||
popup.style.display = 'none';
|
||||
resolve();
|
||||
} else if (type === 'graph-user') {
|
||||
const authData = await pb.collection('users').authWithOAuth2({ provider: 'microsoft' });
|
||||
setToken('graph-user', authData?.meta?.graphAccessToken || '');
|
||||
popup.style.display = 'none';
|
||||
resolve(authData?.meta?.graphAccessToken || '');
|
||||
} else if (type === 'graph-agent') {
|
||||
agentCreds.email = document.getElementById('graphAgentEmail').value;
|
||||
agentCreds.password = document.getElementById('graphAgentPassword').value;
|
||||
popup.style.display = 'none';
|
||||
resolve();
|
||||
}
|
||||
} catch (err) {
|
||||
document.getElementById('authPopupError').textContent = err.message || 'Authentication failed.';
|
||||
document.getElementById('authPopupError').style.display = '';
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// --- API ---
|
||||
return {
|
||||
use,
|
||||
getToken,
|
||||
setToken,
|
||||
clearToken,
|
||||
getUserInfo,
|
||||
ensureToken,
|
||||
refreshToken,
|
||||
promptReauth
|
||||
};
|
||||
})();
|
||||
|
||||
window.Auth = Auth;
|
||||
|
||||
// Example usage:
|
||||
// Auth.use('pb-graph');
|
||||
// const token = await Auth.ensureToken('graph');
|
||||
// const user = Auth.getUserInfo();
|
||||
// All login prompts are handled via the popup, which is hidden unless needed.
|
||||
@@ -1,296 +0,0 @@
|
||||
/**
|
||||
* ROUTES: OAuth Authentication
|
||||
*
|
||||
* PURPOSE: Handle OAuth2 authorization and token exchange
|
||||
* ENDPOINTS:
|
||||
* - POST /api/auth/authorize - Exchange authorization code for token
|
||||
* - POST /api/auth/refresh - Refresh access token using refresh token
|
||||
* - POST /api/auth/logout - Logout and invalidate tokens
|
||||
*
|
||||
* SECURITY:
|
||||
* - Client secret kept server-side only
|
||||
* - Refresh tokens stored in HttpOnly cookies
|
||||
* - PKCE verification with code verifier
|
||||
* - CORS protection
|
||||
*/
|
||||
|
||||
import { Context } from 'hono';
|
||||
|
||||
// ============================================================================
|
||||
// TYPES
|
||||
// ============================================================================
|
||||
|
||||
interface AuthorizeRequest {
|
||||
code: string;
|
||||
state: string;
|
||||
codeVerifier: string;
|
||||
}
|
||||
|
||||
interface TokenResponse {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
expiresIn: number;
|
||||
user: {
|
||||
id: string;
|
||||
displayName: string;
|
||||
mail: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface RefreshTokenRequest {
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CONFIGURATION
|
||||
// ============================================================================
|
||||
|
||||
const MICROSOFT_TENANT_ID = process.env.MICROSOFT_TENANT_ID || 'common';
|
||||
const MICROSOFT_CLIENT_ID = process.env.MICROSOFT_CLIENT_ID || '';
|
||||
const MICROSOFT_CLIENT_SECRET = process.env.MICROSOFT_CLIENT_SECRET || '';
|
||||
const REDIRECT_URI = process.env.MICROSOFT_REDIRECT_URI || '';
|
||||
|
||||
// Token endpoints
|
||||
const TOKEN_ENDPOINT = `https://login.microsoftonline.com/${MICROSOFT_TENANT_ID}/oauth2/v2.0/token`;
|
||||
const GRAPH_ENDPOINT = 'https://graph.microsoft.com/v1.0/me';
|
||||
|
||||
// In-memory token store (in production, use Redis or database)
|
||||
const tokenStore = new Map<string, { refreshToken: string; expiresAt: number }>();
|
||||
|
||||
// ============================================================================
|
||||
// AUTHORIZATION ENDPOINT
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* POST /api/auth/authorize
|
||||
*
|
||||
* Exchange authorization code for access token
|
||||
*
|
||||
* REQUEST:
|
||||
* {
|
||||
* code: string (from Microsoft OAuth redirect)
|
||||
* state: string (CSRF token)
|
||||
* codeVerifier: string (PKCE code verifier)
|
||||
* }
|
||||
*
|
||||
* RESPONSE:
|
||||
* {
|
||||
* accessToken: string
|
||||
* refreshToken: string
|
||||
* expiresIn: number (seconds)
|
||||
* user: { id, displayName, mail }
|
||||
* }
|
||||
*/
|
||||
export async function handleAuthorize(c: Context): Promise<Response> {
|
||||
try {
|
||||
const body = await c.req.json() as AuthorizeRequest;
|
||||
const { code, state, codeVerifier } = body;
|
||||
|
||||
// Validate inputs
|
||||
if (!code || !state || !codeVerifier) {
|
||||
return c.json(
|
||||
{ error: 'Missing required parameters', details: 'code, state, codeVerifier required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate configuration
|
||||
if (!MICROSOFT_CLIENT_ID || !MICROSOFT_CLIENT_SECRET || !REDIRECT_URI) {
|
||||
console.error('[Auth] Missing OAuth configuration');
|
||||
return c.json(
|
||||
{ error: 'OAuth configuration incomplete' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('[Auth] Exchanging authorization code for token');
|
||||
|
||||
// Step 1: Exchange code for token with Microsoft
|
||||
const tokenParams = new URLSearchParams({
|
||||
client_id: MICROSOFT_CLIENT_ID,
|
||||
client_secret: MICROSOFT_CLIENT_SECRET,
|
||||
code: code,
|
||||
code_verifier: codeVerifier,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
grant_type: 'authorization_code',
|
||||
scope: 'offline_access',
|
||||
});
|
||||
|
||||
const tokenResponse = await fetch(TOKEN_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: tokenParams.toString(),
|
||||
});
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const error = await tokenResponse.text();
|
||||
console.error('[Auth] Token exchange failed:', error);
|
||||
return c.json(
|
||||
{ error: 'Failed to exchange code for token', details: error },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const tokenData = await tokenResponse.json() as any;
|
||||
const accessToken = tokenData.access_token;
|
||||
const refreshToken = tokenData.refresh_token;
|
||||
const expiresIn = tokenData.expires_in || 3600;
|
||||
|
||||
if (!accessToken) {
|
||||
console.error('[Auth] No access token in response');
|
||||
return c.json(
|
||||
{ error: 'No access token in response' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('[Auth] Successfully exchanged code for token');
|
||||
|
||||
// Step 2: Get user profile from Microsoft Graph
|
||||
const userResponse = await fetch(GRAPH_ENDPOINT, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
if (!userResponse.ok) {
|
||||
console.error('[Auth] Failed to fetch user profile');
|
||||
return c.json(
|
||||
{ error: 'Failed to fetch user profile' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const user = await userResponse.json() as any;
|
||||
|
||||
console.log('[Auth] Retrieved user profile:', user.displayName);
|
||||
|
||||
// Step 3: Store refresh token server-side
|
||||
if (refreshToken) {
|
||||
const expiresAt = Date.now() + (tokenData.refresh_token_expires_in || 90 * 24 * 60 * 60) * 1000;
|
||||
tokenStore.set(user.id, { refreshToken, expiresAt });
|
||||
console.log('[Auth] Stored refresh token for user:', user.id);
|
||||
}
|
||||
|
||||
// Step 4: Return response
|
||||
const response: TokenResponse = {
|
||||
accessToken,
|
||||
refreshToken: refreshToken || '',
|
||||
expiresIn,
|
||||
user: {
|
||||
id: user.id,
|
||||
displayName: user.displayName,
|
||||
mail: user.mail,
|
||||
},
|
||||
};
|
||||
|
||||
return c.json(response);
|
||||
} catch (error) {
|
||||
console.error('[Auth] Authorization handler error:', error);
|
||||
return c.json(
|
||||
{ error: 'Internal server error', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOKEN REFRESH ENDPOINT
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* POST /api/auth/refresh
|
||||
*
|
||||
* Refresh access token using refresh token
|
||||
*
|
||||
* REQUEST:
|
||||
* {
|
||||
* refreshToken: string
|
||||
* }
|
||||
*
|
||||
* RESPONSE:
|
||||
* {
|
||||
* accessToken: string
|
||||
* expiresIn: number (seconds)
|
||||
* }
|
||||
*/
|
||||
export async function handleRefresh(c: Context): Promise<Response> {
|
||||
try {
|
||||
const body = await c.req.json() as RefreshTokenRequest;
|
||||
const { refreshToken } = body;
|
||||
|
||||
if (!refreshToken) {
|
||||
return c.json(
|
||||
{ error: 'Refresh token required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('[Auth] Refreshing access token');
|
||||
|
||||
// Exchange refresh token for new access token
|
||||
const tokenParams = new URLSearchParams({
|
||||
client_id: MICROSOFT_CLIENT_ID,
|
||||
client_secret: MICROSOFT_CLIENT_SECRET,
|
||||
refresh_token: refreshToken,
|
||||
grant_type: 'refresh_token',
|
||||
scope: 'offline_access',
|
||||
});
|
||||
|
||||
const tokenResponse = await fetch(TOKEN_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: tokenParams.toString(),
|
||||
});
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const error = await tokenResponse.text();
|
||||
console.error('[Auth] Token refresh failed:', error);
|
||||
return c.json(
|
||||
{ error: 'Failed to refresh token' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const tokenData = await tokenResponse.json() as any;
|
||||
const newAccessToken = tokenData.access_token;
|
||||
const expiresIn = tokenData.expires_in || 3600;
|
||||
|
||||
console.log('[Auth] Successfully refreshed access token');
|
||||
|
||||
return c.json({
|
||||
accessToken: newAccessToken,
|
||||
expiresIn,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Auth] Refresh handler error:', error);
|
||||
return c.json(
|
||||
{ error: 'Internal server error', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// LOGOUT ENDPOINT
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* POST /api/auth/logout
|
||||
*
|
||||
* Logout and invalidate tokens
|
||||
*/
|
||||
export async function handleLogout(c: Context): Promise<Response> {
|
||||
try {
|
||||
console.log('[Auth] Logout requested');
|
||||
|
||||
// In production, would invalidate refresh token in database
|
||||
// For now, just return success
|
||||
return c.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('[Auth] Logout handler error:', error);
|
||||
return c.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
// Express API for Redis/Valkey: keys list and single key fetch (for fast UI)
|
||||
const express = require('express');
|
||||
const Redis = require('ioredis');
|
||||
const app = express();
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST || '127.0.0.1',
|
||||
port: Number(process.env.REDIS_PORT || 6379),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
});
|
||||
|
||||
// List all keys (no values)
|
||||
app.get('/api/cache/keys', async (req, res) => {
|
||||
try {
|
||||
const keys = await redis.keys('*');
|
||||
// Optionally, add summary/metadata here
|
||||
res.json(keys.map(key => ({ key })));
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch value for a single key
|
||||
app.get('/api/cache/:key', async (req, res) => {
|
||||
try {
|
||||
const key = req.params.key;
|
||||
let value = await redis.get(key);
|
||||
try { value = JSON.parse(value); } catch {}
|
||||
res.json({ key, value });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
const port = process.env.CACHE_API_PORT || 3006;
|
||||
app.listen(port, () => {
|
||||
console.log(`Cache API (keys+single) running on http://localhost:${port}/api/cache/keys`);
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
// Simple Express API to serve all Redis/Valkey cache keys and values as JSON (CommonJS)
|
||||
const express = require('express');
|
||||
const Redis = require('ioredis');
|
||||
const app = express();
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST || '127.0.0.1',
|
||||
port: Number(process.env.REDIS_PORT || 6379),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
});
|
||||
|
||||
app.get('/api/cache', async (req, res) => {
|
||||
try {
|
||||
const keys = await redis.keys('*');
|
||||
const result = [];
|
||||
for (const key of keys) {
|
||||
let value = await redis.get(key);
|
||||
try { value = JSON.parse(value); } catch {}
|
||||
result.push({ key, value });
|
||||
}
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
const port = process.env.CACHE_API_PORT || 3006;
|
||||
app.listen(port, () => {
|
||||
console.log(`Cache API running on http://localhost:${port}/api/cache`);
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
// Simple Express API to serve all Redis/Valkey cache keys and values as JSON
|
||||
const express = require('express');
|
||||
const Redis = require('ioredis');
|
||||
const app = express();
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST || '127.0.0.1',
|
||||
port: Number(process.env.REDIS_PORT || 6379),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
});
|
||||
|
||||
app.get('/api/cache', async (req, res) => {
|
||||
try {
|
||||
const keys = await redis.keys('*');
|
||||
const result = [];
|
||||
for (const key of keys) {
|
||||
let value = await redis.get(key);
|
||||
try { value = JSON.parse(value); } catch {}
|
||||
result.push({ key, value });
|
||||
}
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
const port = process.env.CACHE_API_PORT || 3030;
|
||||
app.listen(port, () => {
|
||||
console.log(`Cache API running on http://localhost:${port}/api/cache`);
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
// Script to list all Redis/Valkey keys and their values for visualization (CommonJS)
|
||||
const Redis = require('ioredis');
|
||||
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST || '127.0.0.1',
|
||||
port: Number(process.env.REDIS_PORT || 6379),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
});
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const keys = await redis.keys('*');
|
||||
const result = [];
|
||||
for (const key of keys) {
|
||||
let value = await redis.get(key);
|
||||
try { value = JSON.parse(value); } catch {}
|
||||
result.push({ key, value });
|
||||
}
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error('Error listing Redis keys:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
@@ -1,25 +0,0 @@
|
||||
// Script to list all Redis/Valkey keys and their values for visualization
|
||||
const Redis = require('ioredis');
|
||||
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST || '127.0.0.1',
|
||||
port: Number(process.env.REDIS_PORT || 6379),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
});
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const keys = await redis.keys('*');
|
||||
const result = [];
|
||||
for (const key of keys) {
|
||||
let value = await redis.get(key);
|
||||
try { value = JSON.parse(value); } catch {}
|
||||
result.push({ key, value });
|
||||
}
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error('Error listing Redis keys:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
@@ -1,101 +0,0 @@
|
||||
import Redis from 'ioredis';
|
||||
|
||||
// Create Redis client
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: Number(process.env.REDIS_PORT || 6379),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
retryStrategy(times) {
|
||||
const delay = Math.min(times * 50, 2000);
|
||||
return delay;
|
||||
},
|
||||
maxRetriesPerRequest: 3,
|
||||
lazyConnect: true,
|
||||
});
|
||||
|
||||
// Handle connection events
|
||||
redis.on('connect', () => {
|
||||
console.log('✓ Redis connected');
|
||||
});
|
||||
|
||||
redis.on('error', (err) => {
|
||||
console.error('Redis error:', err.message);
|
||||
});
|
||||
|
||||
redis.on('close', () => {
|
||||
console.log('Redis connection closed');
|
||||
});
|
||||
|
||||
// Connect to Redis
|
||||
(async () => {
|
||||
try {
|
||||
await redis.connect();
|
||||
} catch (err) {
|
||||
console.error('Failed to connect to Redis:', err);
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* Get a value from cache
|
||||
*/
|
||||
export async function getCache(key: string): Promise<any | null> {
|
||||
try {
|
||||
const value = await redis.get(key);
|
||||
if (!value) return null;
|
||||
return JSON.parse(value);
|
||||
} catch (err) {
|
||||
console.error(`Cache get error for key "${key}":`, err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a value in cache with TTL in seconds
|
||||
*/
|
||||
export async function setCache(key: string, value: any, ttl: number = 300): Promise<boolean> {
|
||||
try {
|
||||
const serialized = JSON.stringify(value);
|
||||
await redis.setex(key, ttl, serialized);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(`Cache set error for key "${key}":`, err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a key from cache
|
||||
*/
|
||||
export async function deleteCache(key: string): Promise<boolean> {
|
||||
try {
|
||||
await redis.del(key);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(`Cache delete error for key "${key}":`, err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cache keys matching a pattern
|
||||
*/
|
||||
export async function clearCachePattern(pattern: string): Promise<number> {
|
||||
try {
|
||||
const keys = await redis.keys(pattern);
|
||||
if (keys.length === 0) return 0;
|
||||
await redis.del(...keys);
|
||||
return keys.length;
|
||||
} catch (err) {
|
||||
console.error(`Cache clear error for pattern "${pattern}":`, err);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Redis is connected
|
||||
*/
|
||||
export function isRedisConnected(): boolean {
|
||||
return redis.status === 'ready';
|
||||
}
|
||||
|
||||
export default redis;
|
||||
@@ -1,737 +0,0 @@
|
||||
import { Hono } from 'hono';
|
||||
import { serveStatic } from 'hono/bun';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
// @ts-ignore Bun project may not have dotenv types configured
|
||||
import { config } from 'dotenv';
|
||||
import { getCache, setCache, isRedisConnected } from './redis-cache';
|
||||
|
||||
config();
|
||||
|
||||
const app = new Hono();
|
||||
const JOBS_CACHE_FILE = path.join(import.meta.dir, '../jobs-cache.json');
|
||||
|
||||
// In-memory cache for fast access
|
||||
let cachedJobs: any = null;
|
||||
let lastCacheTime = 0;
|
||||
let isFetching = false;
|
||||
let partialJobs: any[] = [];
|
||||
|
||||
// Minimal log helpers
|
||||
const logLine = (path: string, line: string) => {
|
||||
try {
|
||||
fs.appendFileSync(path, `${new Date().toISOString()} ${line}\n`);
|
||||
} catch (err) {
|
||||
console.error('Failed to write log:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Cache helper functions
|
||||
const saveCacheFile = async (data: any): Promise<void> => {
|
||||
try {
|
||||
await Bun.write(JOBS_CACHE_FILE, JSON.stringify(data, null, 2));
|
||||
cachedJobs = data;
|
||||
lastCacheTime = Date.now();
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `Failed to save cache file: ${err}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Fast load: returns first 30 items immediately, full cache lazily
|
||||
const loadCacheFileFast = async (): Promise<any | null> => {
|
||||
try {
|
||||
const file = Bun.file(JOBS_CACHE_FILE);
|
||||
if (!(await file.exists())) return null;
|
||||
|
||||
// Read as text and parse (we're already async, so it's fine)
|
||||
const text = await file.text();
|
||||
const data = JSON.parse(text);
|
||||
cachedJobs = data;
|
||||
lastCacheTime = Date.now();
|
||||
return data;
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `Failed to load cache file: ${err}`);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const loadCacheFile = async (): Promise<any | null> => {
|
||||
try {
|
||||
const file = Bun.file(JOBS_CACHE_FILE);
|
||||
if (await file.exists()) {
|
||||
const data = JSON.parse(await file.text());
|
||||
cachedJobs = data;
|
||||
lastCacheTime = Date.now();
|
||||
return data;
|
||||
}
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `Failed to load cache file: ${err}`);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Version endpoint sourced from package.json
|
||||
app.get('/version', (c) => {
|
||||
try {
|
||||
const pkgRaw = fs.readFileSync('./package.json', 'utf-8');
|
||||
const pkg = JSON.parse(pkgRaw);
|
||||
return c.json({ version: pkg.version || '0.0.0' });
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `version endpoint error: ${(err as Error)?.message || String(err)}`);
|
||||
return c.json({ version: '0.0.0' }, 200);
|
||||
}
|
||||
});
|
||||
|
||||
// Get Graph token from PocketBase auth data
|
||||
app.get('/api/graph-token', async (c) => {
|
||||
try {
|
||||
const authHeader = c.req.header('Authorization');
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return c.json({ error: 'Unauthorized' }, 401);
|
||||
}
|
||||
|
||||
const pbToken = authHeader.substring(7);
|
||||
|
||||
// Fetch the authenticated user's data from PocketBase
|
||||
const response = await fetch('https://pocketbase.ccllc.pro/api/collections/users/auth-refresh', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${pbToken}` }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return c.json({ error: 'Failed to refresh auth' }, response.status);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const meta = data?.meta || {};
|
||||
|
||||
// Extract Graph token from all possible locations
|
||||
const graphToken =
|
||||
meta?.graphAccessToken ||
|
||||
meta?.graph_token ||
|
||||
meta?.graphToken ||
|
||||
meta?.accessToken ||
|
||||
meta?.access_token ||
|
||||
meta?.token ||
|
||||
meta?.rawToken ||
|
||||
meta?.authData?.access_token ||
|
||||
'';
|
||||
|
||||
return c.json({ token: graphToken });
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `graph-token endpoint error: ${(err as Error)?.message || String(err)}`);
|
||||
return c.json({ error: 'Failed to get token', token: '' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Jobs endpoint with Redis caching
|
||||
app.get('/api/jobs', async (c) => {
|
||||
const page = parseInt(c.req.query('page') || '1');
|
||||
const perPage = parseInt(c.req.query('perPage') || '50');
|
||||
const sort = c.req.query('sort') || '-Job_Number';
|
||||
|
||||
const cacheKey = `jobs:page:${page}:perPage:${perPage}:sort:${sort}`;
|
||||
const ttl = 300; // 5 minutes cache
|
||||
|
||||
try {
|
||||
// Try to get from cache first
|
||||
if (isRedisConnected()) {
|
||||
const cached = await getCache(cacheKey);
|
||||
if (cached) {
|
||||
logLine('logs/server.log', `Cache HIT for ${cacheKey}`);
|
||||
return c.json(cached);
|
||||
}
|
||||
logLine('logs/server.log', `Cache MISS for ${cacheKey}`);
|
||||
}
|
||||
|
||||
// Fetch from PocketBase
|
||||
const pbUrl = `https://pocketbase.ccllc.pro/api/collections/pbc_1407612689/records?page=${page}&perPage=${perPage}&sort=${sort}`;
|
||||
const authHeader = c.req.header('Authorization') || '';
|
||||
|
||||
const response = await fetch(pbUrl, {
|
||||
headers: authHeader ? { Authorization: authHeader } : {}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`PocketBase returned ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Cache the result
|
||||
if (isRedisConnected()) {
|
||||
await setCache(cacheKey, data, ttl);
|
||||
logLine('logs/server.log', `Cached ${cacheKey} for ${ttl}s`);
|
||||
}
|
||||
|
||||
return c.json(data);
|
||||
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `jobs endpoint error: ${(err as Error)?.message || String(err)}`);
|
||||
return c.json({ error: 'Failed to fetch jobs' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Fast endpoint: returns jobs with pagination
|
||||
app.get('/api/jobs-all', async (c) => {
|
||||
try {
|
||||
// Get page and perPage from query params, default to first page with 100 items
|
||||
const page = parseInt(c.req.query('page') || '1');
|
||||
const perPage = parseInt(c.req.query('perPage') || '100');
|
||||
|
||||
// Return in-memory cache if available
|
||||
if (cachedJobs && cachedJobs.items && Array.isArray(cachedJobs.items)) {
|
||||
const allJobs = cachedJobs.items;
|
||||
const totalItems = allJobs.length;
|
||||
const totalPages = Math.ceil(totalItems / perPage);
|
||||
const startIdx = (page - 1) * perPage;
|
||||
const endIdx = startIdx + perPage;
|
||||
const pageJobs = allJobs.slice(startIdx, endIdx);
|
||||
|
||||
// Refresh in background if older than 5 minutes
|
||||
if (Date.now() - lastCacheTime > 5 * 60 * 1000) {
|
||||
fetchAllJobsProgressively(c.req.header('Authorization') || '').catch(err => {
|
||||
logLine('logs/error.log', `Background refresh error: ${err}`);
|
||||
});
|
||||
}
|
||||
|
||||
return c.json({
|
||||
page,
|
||||
perPage: pageJobs.length,
|
||||
totalItems,
|
||||
totalPages,
|
||||
items: pageJobs
|
||||
});
|
||||
}
|
||||
|
||||
// Try to load from disk if not in memory
|
||||
const diskCache = await loadCacheFile();
|
||||
if (diskCache && diskCache.items && Array.isArray(diskCache.items)) {
|
||||
const allJobs = diskCache.items;
|
||||
const totalItems = allJobs.length;
|
||||
const totalPages = Math.ceil(totalItems / perPage);
|
||||
const startIdx = (page - 1) * perPage;
|
||||
const endIdx = startIdx + perPage;
|
||||
const pageJobs = allJobs.slice(startIdx, endIdx);
|
||||
|
||||
// Refresh in background
|
||||
fetchAllJobsProgressively(c.req.header('Authorization') || '').catch(err => {
|
||||
logLine('logs/error.log', `Background refresh error: ${err}`);
|
||||
});
|
||||
|
||||
return c.json({
|
||||
page,
|
||||
perPage: pageJobs.length,
|
||||
totalItems,
|
||||
totalPages,
|
||||
items: pageJobs
|
||||
});
|
||||
}
|
||||
|
||||
// No cache: fetch first page from PocketBase
|
||||
if (!isFetching) {
|
||||
isFetching = true;
|
||||
fetchAllJobsProgressively(c.req.header('Authorization') || '').catch(err => {
|
||||
logLine('logs/error.log', `Progressive fetch error: ${err}`);
|
||||
isFetching = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Return first batch immediately
|
||||
try {
|
||||
const pbUrl = `https://pocketbase.ccllc.pro/api/collections/pbc_1407612689/records?page=1&perPage=100&sort=-Job_Number`;
|
||||
const response = await fetch(pbUrl, {
|
||||
headers: c.req.header('Authorization') ? { Authorization: c.req.header('Authorization')! } : {}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const items = data.items || [];
|
||||
return c.json({
|
||||
page: 1,
|
||||
perPage: items.length,
|
||||
totalItems: items.length,
|
||||
totalPages: 1,
|
||||
items: items
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `Failed to get first page: ${err}`);
|
||||
}
|
||||
|
||||
return c.json({ error: 'No cache available and initial fetch failed' }, 503);
|
||||
} catch(err) {
|
||||
logLine('logs/error.log', `jobs-all endpoint error: ${err}`);
|
||||
return c.json({ error: 'Failed to fetch jobs' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Progressive fetch helper: fetches all jobs in background, updating cache as it goes
|
||||
async function fetchAllJobsProgressively(authHeader: string): Promise<void> {
|
||||
try {
|
||||
const perPage = 500;
|
||||
|
||||
// Get first page to determine total
|
||||
const firstUrl = `https://pocketbase.ccllc.pro/api/collections/pbc_1407612689/records?page=1&perPage=${perPage}&sort=-Job_Number`;
|
||||
const firstResponse = await fetch(firstUrl, {
|
||||
headers: authHeader ? { Authorization: authHeader } : {}
|
||||
});
|
||||
|
||||
if (!firstResponse.ok) {
|
||||
throw new Error(`PocketBase returned ${firstResponse.status}`);
|
||||
}
|
||||
|
||||
const firstData = await firstResponse.json();
|
||||
const totalItems = firstData.totalItems || 0;
|
||||
const totalPages = Math.ceil(totalItems / perPage);
|
||||
|
||||
const allItems = [...(firstData.items || [])];
|
||||
partialJobs = allItems;
|
||||
|
||||
// Fetch remaining pages in parallel
|
||||
if (totalPages > 1) {
|
||||
const pagePromises = [];
|
||||
for (let page = 2; page <= totalPages; page++) {
|
||||
const pbUrl = `https://pocketbase.ccllc.pro/api/collections/pbc_1407612689/records?page=${page}&perPage=${perPage}&sort=-Job_Number`;
|
||||
pagePromises.push(
|
||||
fetch(pbUrl, {
|
||||
headers: authHeader ? { Authorization: authHeader } : {}
|
||||
}).then(r => r.json())
|
||||
);
|
||||
}
|
||||
|
||||
const results = await Promise.all(pagePromises);
|
||||
for (const result of results) {
|
||||
allItems.push(...(result.items || []));
|
||||
}
|
||||
}
|
||||
|
||||
// Save final result to cache file
|
||||
const cacheData = {
|
||||
page: 1,
|
||||
perPage: allItems.length,
|
||||
totalItems: allItems.length,
|
||||
totalPages: 1,
|
||||
items: allItems
|
||||
};
|
||||
|
||||
await saveCacheFile(cacheData);
|
||||
isFetching = false;
|
||||
logLine('logs/server.log', `✓ Progressive fetch complete: ${allItems.length} jobs cached`);
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `Progressive fetch failed: ${err}`);
|
||||
isFetching = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to fetch all jobs from PocketBase
|
||||
async function fetchAllJobsFromPocketBase(authHeader: string): Promise<any> {
|
||||
const perPage = 500;
|
||||
|
||||
// First, get total count
|
||||
const firstUrl = `https://pocketbase.ccllc.pro/api/collections/pbc_1407612689/records?page=1&perPage=${perPage}&sort=-Job_Number`;
|
||||
const firstResponse = await fetch(firstUrl, {
|
||||
headers: authHeader ? { Authorization: authHeader } : {}
|
||||
});
|
||||
|
||||
if (!firstResponse.ok) {
|
||||
throw new Error(`PocketBase returned ${firstResponse.status}`);
|
||||
}
|
||||
|
||||
const firstData = await firstResponse.json();
|
||||
const totalItems = firstData.totalItems || 0;
|
||||
const totalPages = Math.ceil(totalItems / perPage);
|
||||
|
||||
// Fetch all pages in parallel
|
||||
const allItems = [...(firstData.items || [])];
|
||||
|
||||
if (totalPages > 1) {
|
||||
const pagePromises = [];
|
||||
for (let page = 2; page <= totalPages; page++) {
|
||||
const pbUrl = `https://pocketbase.ccllc.pro/api/collections/pbc_1407612689/records?page=${page}&perPage=${perPage}&sort=-Job_Number`;
|
||||
pagePromises.push(
|
||||
fetch(pbUrl, {
|
||||
headers: authHeader ? { Authorization: authHeader } : {}
|
||||
}).then(r => r.json())
|
||||
);
|
||||
}
|
||||
|
||||
const results = await Promise.all(pagePromises);
|
||||
for (const result of results) {
|
||||
allItems.push(...(result.items || []));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
page: 1,
|
||||
perPage: allItems.length,
|
||||
totalItems: allItems.length,
|
||||
totalPages: 1,
|
||||
items: allItems
|
||||
};
|
||||
}
|
||||
|
||||
// Helper function to refresh jobs cache in background
|
||||
async function refreshJobsCache(cacheKey: string, ttl: number, authHeader: string): Promise<void> {
|
||||
try {
|
||||
const result = await fetchAllJobsFromPocketBase(authHeader);
|
||||
await setCache(cacheKey, result, ttl);
|
||||
logLine('logs/server.log', `Background refresh: cached ${cacheKey} with ${result.items.length} jobs`);
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `Background refresh error: ${(err as Error)?.message || String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Microsoft Graph helpers ---
|
||||
const getGraphToken = (c?: any) => {
|
||||
const headerToken = c?.req?.header ? c.req.header('x-graph-token') : '';
|
||||
const envToken = process.env.GRAPH_TOKEN || process.env.MS_GRAPH_TOKEN || '';
|
||||
const token = headerToken || envToken;
|
||||
console.log('[getGraphToken] Header token:', headerToken ? headerToken.substring(0, 20) + '...' : 'NONE');
|
||||
console.log('[getGraphToken] Env token:', envToken ? 'SET' : 'NOT SET');
|
||||
console.log('[getGraphToken] Using:', token ? token.substring(0, 20) + '...' : 'NONE');
|
||||
return token;
|
||||
};
|
||||
|
||||
const b64Url = (input: string) =>
|
||||
Buffer.from(input).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||
|
||||
type DriveItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
webUrl?: string;
|
||||
size?: number;
|
||||
lastModifiedDateTime?: string;
|
||||
file?: { mimeType?: string };
|
||||
folder?: { childCount?: number };
|
||||
parentReference?: { path?: string; driveId?: string };
|
||||
};
|
||||
|
||||
const fetchJson = async (url: string, token: string) => {
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
const err: any = new Error(`Graph ${res.status}: ${text}`);
|
||||
err.status = res.status;
|
||||
throw err;
|
||||
}
|
||||
return res.json();
|
||||
};
|
||||
|
||||
const resolveShareLink = async (link: string, token: string) => {
|
||||
// Decode the link in case it comes pre-encoded from the database
|
||||
const decodedLink = decodeURIComponent(link);
|
||||
console.log('[resolveShareLink] Decoded link:', decodedLink);
|
||||
|
||||
// Check if this is a short sharing link (/:f:/ or /:b:/) or a direct document library URL
|
||||
if (decodedLink.includes('/:f:/') || decodedLink.includes('/:b:/')) {
|
||||
// Short sharing link - use shares API
|
||||
const encoded = `u!${b64Url(decodedLink)}`;
|
||||
console.log('[resolveShareLink] Using shares API, encoded:', encoded);
|
||||
const data = await fetchJson(`https://graph.microsoft.com/v1.0/shares/${encoded}/driveItem`, token);
|
||||
return { driveId: data.parentReference?.driveId as string, itemId: data.id as string };
|
||||
} else {
|
||||
// Direct document library URL - parse it and use drive API
|
||||
// Example: https://czflex.sharepoint.com/sites/Team/Shared Documents/Forms/AllItems.aspx?RootFolder=/sites/Team/Shared Documents/General/...
|
||||
const url = new URL(decodedLink);
|
||||
const pathMatch = url.hostname.match(/^([^.]+)\.sharepoint\.com$/);
|
||||
if (!pathMatch) throw new Error('Invalid SharePoint URL');
|
||||
|
||||
const hostname = pathMatch[1]; // e.g., 'czflex'
|
||||
const sitePath = url.pathname.split('/Shared')[0]; // e.g., /sites/Team
|
||||
const rootFolderParam = url.searchParams.get('RootFolder');
|
||||
if (!rootFolderParam) throw new Error('RootFolder parameter missing');
|
||||
|
||||
// Extract the folder path relative to the document library
|
||||
// RootFolder format: /sites/Team/Shared Documents/General/Operations [Server]/...
|
||||
// We need: General/Operations [Server]/...
|
||||
const folderPath = rootFolderParam.split('/Shared Documents/')[1];
|
||||
if (!folderPath) throw new Error('Could not parse folder path');
|
||||
|
||||
console.log('[resolveShareLink] Hostname:', hostname);
|
||||
console.log('[resolveShareLink] Site path:', sitePath);
|
||||
console.log('[resolveShareLink] Folder path:', folderPath);
|
||||
|
||||
// Get the site ID first using hostname:sitePath format
|
||||
const siteData = await fetchJson(`https://graph.microsoft.com/v1.0/sites/${hostname}.sharepoint.com:${sitePath}`, token);
|
||||
const siteId = siteData.id;
|
||||
|
||||
// Get the default document library drive
|
||||
const driveData = await fetchJson(`https://graph.microsoft.com/v1.0/sites/${siteId}/drive`, token);
|
||||
const driveId = driveData.id;
|
||||
|
||||
// Get the folder item by path
|
||||
const itemData = await fetchJson(
|
||||
`https://graph.microsoft.com/v1.0/drives/${driveId}/root:/${folderPath}`,
|
||||
token
|
||||
);
|
||||
|
||||
console.log('[resolveShareLink] Resolved to driveId:', driveId, 'itemId:', itemData.id);
|
||||
return { driveId, itemId: itemData.id };
|
||||
}
|
||||
};
|
||||
|
||||
const listChildrenRecursive = async (
|
||||
driveId: string,
|
||||
itemId: string,
|
||||
depth = 0,
|
||||
maxDepth = 5,
|
||||
token: string,
|
||||
): Promise<DriveItem[]> => {
|
||||
const out: DriveItem[] = [];
|
||||
const data = await fetchJson(`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/children`, token);
|
||||
for (const child of data.value || []) {
|
||||
out.push(child as DriveItem);
|
||||
if (child.folder && depth < maxDepth) {
|
||||
const nested = await listChildrenRecursive(driveId, child.id, depth + 1, maxDepth, token);
|
||||
out.push(...nested);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const searchWithinFolder = async (driveId: string, itemId: string, query: string, token: string): Promise<DriveItem[]> => {
|
||||
const data = await fetchJson(
|
||||
`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/search(q='${encodeURIComponent(query)}')`,
|
||||
token,
|
||||
);
|
||||
return (data.value || []) as DriveItem[];
|
||||
};
|
||||
|
||||
const filterByCategory = (items: DriveItem[], category?: string, pdfOnly: boolean = true) => {
|
||||
// First filter: show PDF files and Word documents (which will be converted to PDF)
|
||||
let filtered = items;
|
||||
if (pdfOnly) {
|
||||
filtered = items.filter((i) => {
|
||||
if (i.folder) return false;
|
||||
const name = i.name.toLowerCase();
|
||||
const mimeType = i.file?.mimeType?.toLowerCase() || '';
|
||||
// Include PDFs, Word documents, and images
|
||||
return name.endsWith('.pdf') || mimeType.includes('pdf') ||
|
||||
name.endsWith('.doc') || name.endsWith('.docx') || mimeType.includes('word') ||
|
||||
name.endsWith('.jpg') || name.endsWith('.jpeg') || name.endsWith('.png') ||
|
||||
name.endsWith('.gif') || name.endsWith('.bmp') || name.endsWith('.tiff') ||
|
||||
name.endsWith('.webp') || mimeType.includes('image');
|
||||
});
|
||||
}
|
||||
|
||||
// Second filter: category filtering (if specified)
|
||||
if (!category) return filtered;
|
||||
const nameHas = (name: string, words: string[]) => words.some((w) => name.includes(w));
|
||||
const lc = (s: string) => (s || '').toLowerCase();
|
||||
const contractWords = ['contract', 'estimate', 'proposal', 'bid', 'award', 'agreement', 'sow'];
|
||||
const planWords = ['plan', 'drawing', 'dwg', 'pdf', 'sheet', 'layout'];
|
||||
return filtered.filter((i) => {
|
||||
const n = lc(i.name);
|
||||
if (category === 'contracts') return nameHas(n, contractWords);
|
||||
if (category === 'plans') return nameHas(n, planWords);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
// List/search job files via Graph using a shared folder link
|
||||
app.get('/api/job-files', async (c) => {
|
||||
try {
|
||||
console.log('[job-files] Request received');
|
||||
const token = getGraphToken(c);
|
||||
if (!token) {
|
||||
console.log('[job-files] No token found');
|
||||
return c.json({ error: 'GRAPH_TOKEN missing' }, 500);
|
||||
}
|
||||
console.log('[job-files] Token found:', token.substring(0, 20) + '...');
|
||||
const link = c.req.query('link');
|
||||
const q = c.req.query('q') || '';
|
||||
const category = c.req.query('category') || '';
|
||||
if (!link) return c.json({ error: 'link required' }, 400);
|
||||
console.log('[job-files] Link:', link);
|
||||
|
||||
const { driveId, itemId } = await resolveShareLink(link, token);
|
||||
|
||||
let items: DriveItem[] = [];
|
||||
if (q) {
|
||||
items = await searchWithinFolder(driveId, itemId, q, token);
|
||||
} else {
|
||||
// Walk subfolders up to depth 5
|
||||
items = await listChildrenRecursive(driveId, itemId, 0, 5, token);
|
||||
}
|
||||
|
||||
// Filter to show only PDFs by default (can be disabled with pdfOnly=false query param)
|
||||
const pdfOnly = c.req.query('pdfOnly') !== 'false';
|
||||
const filtered = filterByCategory(items, category, pdfOnly);
|
||||
console.log(`[job-files] Found ${items.length} total items, ${filtered.length} after filtering`);
|
||||
|
||||
const mapped = filtered.map((i) => ({
|
||||
id: i.id,
|
||||
name: i.name,
|
||||
url: i.webUrl,
|
||||
driveId: i.parentReference?.driveId || driveId,
|
||||
size: i.size,
|
||||
modified: i.lastModifiedDateTime,
|
||||
contentType: i.file?.mimeType,
|
||||
isFolder: Boolean(i.folder),
|
||||
path: i.parentReference?.path || '',
|
||||
}));
|
||||
|
||||
return c.json({ items: mapped, total: mapped.length, source: q ? 'search' : 'walk', driveId });
|
||||
} catch (err) {
|
||||
const message = (err as Error)?.message || String(err);
|
||||
const status = (err as any)?.status || 500;
|
||||
console.error('[job-files] ERROR:', message);
|
||||
logLine('logs/error.log', `/api/job-files error: ${message}`);
|
||||
return c.json({ error: message }, status);
|
||||
}
|
||||
});
|
||||
|
||||
// Stream file content via Graph to avoid CSP issues for inline preview
|
||||
app.get('/api/job-file-content', async (c) => {
|
||||
try {
|
||||
const token = getGraphToken(c);
|
||||
if (!token) return c.json({ error: 'GRAPH_TOKEN missing' }, 500);
|
||||
const driveId = c.req.query('driveId');
|
||||
const itemId = c.req.query('itemId');
|
||||
if (!driveId || !itemId) return c.json({ error: 'driveId and itemId required' }, 400);
|
||||
|
||||
const res = await fetch(`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/content`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
logLine('logs/error.log', `/api/job-file-content failed: ${res.status} - ${text}`);
|
||||
return c.json({ error: `Graph ${res.status}: ${text}` }, 502);
|
||||
}
|
||||
const headers: Record<string, string> = {};
|
||||
const ct = res.headers.get('content-type');
|
||||
const cd = res.headers.get('content-disposition');
|
||||
if (ct) headers['Content-Type'] = ct;
|
||||
if (cd) headers['Content-Disposition'] = cd;
|
||||
return new Response(res.body, { status: 200, headers });
|
||||
} catch (err) {
|
||||
const message = (err as Error)?.message || String(err);
|
||||
const status = (err as any)?.status || 500;
|
||||
logLine('logs/error.log', `/api/job-file-content error: ${message}`);
|
||||
return c.json({ error: message }, status);
|
||||
}
|
||||
});
|
||||
|
||||
// Convert Word documents to PDF for display
|
||||
app.get('/api/job-file-pdf', async (c) => {
|
||||
try {
|
||||
const token = getGraphToken(c);
|
||||
if (!token) return c.json({ error: 'GRAPH_TOKEN missing' }, 500);
|
||||
const driveId = c.req.query('driveId');
|
||||
const itemId = c.req.query('itemId');
|
||||
if (!driveId || !itemId) return c.json({ error: 'driveId and itemId required' }, 400);
|
||||
|
||||
// Use Graph API to convert to PDF format
|
||||
const res = await fetch(`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/content?format=pdf`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
logLine('logs/error.log', `/api/job-file-pdf conversion failed: ${res.status} - ${text}`);
|
||||
return c.json({ error: `PDF conversion failed: ${res.status}` }, 502);
|
||||
}
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/pdf',
|
||||
};
|
||||
const cd = res.headers.get('content-disposition');
|
||||
if (cd) {
|
||||
// Replace .docx/.doc extension with .pdf in filename
|
||||
headers['Content-Disposition'] = cd.replace(/\.(docx?)/gi, '.pdf');
|
||||
}
|
||||
logLine('logs/server.log', `Successfully converted document to PDF: driveId=${driveId}, itemId=${itemId}`);
|
||||
return new Response(res.body, { status: 200, headers });
|
||||
} catch (err) {
|
||||
const message = (err as Error)?.message || String(err);
|
||||
const status = (err as any)?.status || 500;
|
||||
logLine('logs/error.log', `/api/job-file-pdf error: ${message}`);
|
||||
return c.json({ error: message }, status);
|
||||
}
|
||||
});
|
||||
|
||||
// Mutation logging endpoint
|
||||
app.post('/log-change', async (c) => {
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const { action = 'unknown', user = 'unknown', target = '', detail = '' } = body || {};
|
||||
const payload = JSON.stringify({ action, user, target, detail });
|
||||
logLine('logs/changes.log', payload);
|
||||
return c.json({ status: 'ok' });
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `log-change error: ${(err as Error)?.message || String(err)}`);
|
||||
return c.text('Bad Request', 400);
|
||||
}
|
||||
});
|
||||
|
||||
// Serve static files from the frontend directory (must come after API routes)
|
||||
// Add cache headers for static assets
|
||||
app.use('/assets/*', async (c, next) => {
|
||||
await next();
|
||||
// Cache assets for 1 year
|
||||
c.header('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
});
|
||||
app.use('/*', serveStatic({ root: './frontend' }));
|
||||
|
||||
// Error handler
|
||||
app.onError((err, c) => {
|
||||
logLine('logs/error.log', `Unhandled error: ${err?.message || err}`);
|
||||
return c.text('Internal Server Error', 500);
|
||||
});
|
||||
|
||||
// Default to 3005 for test instance; can be overridden by Environment=PORT
|
||||
const PORT = Number(process.env.PORT || 3005);
|
||||
|
||||
logLine('logs/server.log', `Starting frontend server on port ${PORT}`);
|
||||
|
||||
// Load cache on startup
|
||||
(async () => {
|
||||
try {
|
||||
const cached = await loadCacheFile();
|
||||
if (cached) {
|
||||
logLine('logs/server.log', `✓ Loaded cache from disk: ${cached.items?.length || 0} jobs`);
|
||||
} else {
|
||||
logLine('logs/server.log', 'No cache file found, will fetch from PocketBase on first request');
|
||||
}
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `Startup cache load error: ${(err as Error)?.message || String(err)}`);
|
||||
}
|
||||
})();
|
||||
|
||||
// Immediately load cache synchronously if it exists (for fastest first request)
|
||||
(async () => {
|
||||
try {
|
||||
await loadCacheFile();
|
||||
} catch (err) {
|
||||
// Silent fail
|
||||
}
|
||||
})();
|
||||
|
||||
// Background refresh every 5 minutes to keep cache fresh
|
||||
setInterval(async () => {
|
||||
try {
|
||||
const result = await fetchAllJobsFromPocketBase('');
|
||||
await saveCacheFile(result);
|
||||
logLine('logs/server.log', `✓ Background refresh: updated cache with ${result.items?.length || 0} jobs`);
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `Periodic refresh error: ${(err as Error)?.message || String(err)}`);
|
||||
}
|
||||
}, 5 * 60 * 1000); // Every 5 minutes
|
||||
|
||||
// Graceful shutdown logging
|
||||
const shutdown = (signal: string) => {
|
||||
logLine('logs/server.log', `Shutting down due to ${signal}`);
|
||||
process.exit(0);
|
||||
};
|
||||
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||
|
||||
export default {
|
||||
port: PORT,
|
||||
fetch: app.fetch,
|
||||
};
|
||||
@@ -1,600 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "job-info",
|
||||
"dependencies": {
|
||||
"dotenv": "^17.2.3",
|
||||
"hono": "^4.10.8",
|
||||
"ioredis": "^5.9.2",
|
||||
"pocketbase": "^0.26.5",
|
||||
"tailwind": "^4.0.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@babel/runtime": ["@babel/runtime@7.3.4", "", { "dependencies": { "regenerator-runtime": "^0.12.0" } }, "sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g=="],
|
||||
|
||||
"@ioredis/commands": ["@ioredis/commands@1.5.0", "", {}, "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
|
||||
|
||||
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.4", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A=="],
|
||||
|
||||
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
|
||||
|
||||
"ajv": ["ajv@6.10.0", "", { "dependencies": { "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg=="],
|
||||
|
||||
"amqplib": ["amqplib@0.5.2", "", { "dependencies": { "bitsyntax": "~0.0.4", "bluebird": "^3.4.6", "buffer-more-ints": "0.0.2", "readable-stream": "1.x >=1.1.9", "safe-buffer": "^5.0.1" } }, "sha512-l9mCs6LbydtHqRniRwYkKdqxVa6XMz3Vw1fh+2gJaaVgTM6Jk3o8RccAKWKtlhT1US5sWrFh+KKxsVUALURSIA=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
||||
|
||||
"app-root-path": ["app-root-path@2.1.0", "", {}, "sha512-z5BqVjscbjmJBybKlICogJR2jCr2q/Ixu7Pvui5D4y97i7FLsJlvEG9XOR/KJRlkxxZz7UaaS2TMwQh1dRJ2dA=="],
|
||||
|
||||
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
|
||||
|
||||
"array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="],
|
||||
|
||||
"array.prototype.reduce": ["array.prototype.reduce@1.0.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-array-method-boxes-properly": "^1.0.0", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "is-string": "^1.1.1" } }, "sha512-DwuEqgXFBwbmZSRqt3BpQigWNUoqw9Ml2dTWdF3B2zQlQX4OeUE0zyuzX0fX0IbTvjdkZbcBTU3idgpO78qkTw=="],
|
||||
|
||||
"arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
|
||||
|
||||
"asn1": ["asn1@0.2.3", "", {}, "sha512-6i37w/+EhlWlGUJff3T/Q8u1RGmP5wgbiwYnOnbOqvtrPxT63/sYFyP9RcpxtxGymtfA075IvmOnL7ycNOWl3w=="],
|
||||
|
||||
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
||||
|
||||
"async-limiter": ["async-limiter@1.0.1", "", {}, "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="],
|
||||
|
||||
"async-retry": ["async-retry@1.2.3", "", { "dependencies": { "retry": "0.12.0" } }, "sha512-tfDb02Th6CE6pJUF2gjW5ZVjsgwlucVXOEQMvEX9JgSJMs9gAX+Nz3xRuJBKuUYjTSYORqvDBORdAQ3LU59g7Q=="],
|
||||
|
||||
"autoprefixer": ["autoprefixer@10.4.23", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001760", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA=="],
|
||||
|
||||
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
||||
|
||||
"babel-runtime": ["babel-runtime@6.26.0", "", { "dependencies": { "core-js": "^2.4.0", "regenerator-runtime": "^0.11.0" } }, "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.8", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Y1fOuNDowLfgKOypdc9SPABfoWXuZHBOyCS4cD52IeZBhr4Md6CLLs6atcxVrzRmQ06E7hSlm5bHHApPKR/byA=="],
|
||||
|
||||
"basic-auth": ["basic-auth@2.0.1", "", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="],
|
||||
|
||||
"bitsyntax": ["bitsyntax@0.0.4", "", { "dependencies": { "buffer-more-ints": "0.0.2" } }, "sha512-Pav3HSZXD2NLQOWfJldY3bpJLt8+HS2nUo5Z1bLLmHg2vCE/cM1qfEvNjlYo7GgYQPneNr715Bh42i01ZHZPvw=="],
|
||||
|
||||
"bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
|
||||
|
||||
"body-parser": ["body-parser@1.18.3", "", { "dependencies": { "bytes": "3.0.0", "content-type": "~1.0.4", "debug": "2.6.9", "depd": "~1.1.2", "http-errors": "~1.6.3", "iconv-lite": "0.4.23", "on-finished": "~2.3.0", "qs": "6.5.2", "raw-body": "2.3.3", "type-is": "~1.6.16" } }, "sha512-YQyoqQG3sO8iCmf8+hyVpgHHOv0/hCEFiS4zTGUwTA1HjAFX66wRcNQrVCeJq9pgESMRvUAOvSil5MJlmccuKQ=="],
|
||||
|
||||
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
||||
|
||||
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||
|
||||
"buffer-more-ints": ["buffer-more-ints@0.0.2", "", {}, "sha512-PDgX2QJgUc5+Jb2xAoBFP5MxhtVUmZHR33ak+m/SDxRdCrbnX1BggRIaxiW7ImwfmO4iJeCQKN18ToSXWGjYkA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="],
|
||||
|
||||
"bytes": ["bytes@3.0.0", "", {}, "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw=="],
|
||||
|
||||
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||
|
||||
"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.30001760", "", {}, "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw=="],
|
||||
|
||||
"chalk": ["chalk@2.4.1", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ=="],
|
||||
|
||||
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"commands-events": ["commands-events@1.0.4", "", { "dependencies": { "@babel/runtime": "7.2.0", "formats": "1.0.0", "uuidv4": "2.0.0" } }, "sha512-HdP/+1Anoc7z+6L2h7nd4Imz54+LW+BjMGt30riBZrZ3ZeP/8el93wD8Jj8ltAaqVslqNgjX6qlhSBJwuDSmpg=="],
|
||||
|
||||
"comparejs": ["comparejs@1.0.0", "", {}, "sha512-Ue/Zd9aOucHzHXwaCe4yeHR7jypp7TKrIBZ5yls35nPNiVXlW14npmNVKM1ZaLlQTKZ6/4ewA//gYKHHIwCpOw=="],
|
||||
|
||||
"compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="],
|
||||
|
||||
"compression": ["compression@1.7.3", "", { "dependencies": { "accepts": "~1.3.5", "bytes": "3.0.0", "compressible": "~2.0.14", "debug": "2.6.9", "on-headers": "~1.0.1", "safe-buffer": "5.1.2", "vary": "~1.1.2" } }, "sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg=="],
|
||||
|
||||
"content-disposition": ["content-disposition@0.5.2", "", {}, "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA=="],
|
||||
|
||||
"content-type": ["content-type@1.0.4", "", {}, "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="],
|
||||
|
||||
"cookie": ["cookie@0.3.1", "", {}, "sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw=="],
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="],
|
||||
|
||||
"core-js": ["core-js@2.6.12", "", {}, "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ=="],
|
||||
|
||||
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
|
||||
|
||||
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
|
||||
|
||||
"crypto2": ["crypto2@2.0.0", "", { "dependencies": { "babel-runtime": "6.26.0", "node-rsa": "0.4.2", "util.promisify": "1.0.0" } }, "sha512-jdXdAgdILldLOF53md25FiQ6ybj2kUFTiRjs7msKTUoZrzgT/M1FPX5dYGJjbbwFls+RJIiZxNTC02DE/8y0ZQ=="],
|
||||
|
||||
"csstype": ["csstype@3.2.0", "", {}, "sha512-si++xzRAY9iPp60roQiFta7OFbhrgvcthrhlNAGeQptSY25uJjkfUV8OArC3KLocB8JT8ohz+qgxWCmz8RhjIg=="],
|
||||
|
||||
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
||||
|
||||
"data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="],
|
||||
|
||||
"data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
|
||||
|
||||
"datasette": ["datasette@1.0.1", "", { "dependencies": { "comparejs": "1.0.0", "eventemitter2": "5.0.1", "lodash": "4.17.5" } }, "sha512-aJdlCBToEJUP4M57r67r4V6tltwGKa3qetnjpBtXYIlqbX9tM9jsoDMxb4xd9AGjpp3282oHRmqI5Z8TVAU0Mg=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
|
||||
|
||||
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
||||
|
||||
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
|
||||
|
||||
"depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="],
|
||||
|
||||
"destroy": ["destroy@1.0.4", "", {}, "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg=="],
|
||||
|
||||
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
|
||||
|
||||
"draht": ["draht@1.0.1", "", { "dependencies": { "eventemitter2": "5.0.1" } }, "sha512-yNNHL864dniNmIE9ZKD++mKypiAUAvVZtyV0QrbXH/ak3ebzFqo5xsmRBRqV8pZVhImOSBiyq500Wcmrf44zAg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="],
|
||||
|
||||
"encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
|
||||
|
||||
"es-abstract": ["es-abstract@1.24.0", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg=="],
|
||||
|
||||
"es-array-method-boxes-properly": ["es-array-method-boxes-properly@1.0.0", "", {}, "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
|
||||
|
||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
||||
"eventemitter2": ["eventemitter2@5.0.1", "", {}, "sha512-5EM1GHXycJBS6mauYAbVKT1cVs7POKWb2NXD4Vyt8dDqeZa7LaDK1/sjtL+Zb0lzTpSNil4596Dyu97hz37QLg=="],
|
||||
|
||||
"express": ["express@4.16.4", "", { "dependencies": { "accepts": "~1.3.5", "array-flatten": "1.1.1", "body-parser": "1.18.3", "content-disposition": "0.5.2", "content-type": "~1.0.4", "cookie": "0.3.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "~1.1.2", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.1.1", "fresh": "0.5.2", "merge-descriptors": "1.0.1", "methods": "~1.1.2", "on-finished": "~2.3.0", "parseurl": "~1.3.2", "path-to-regexp": "0.1.7", "proxy-addr": "~2.0.4", "qs": "6.5.2", "range-parser": "~1.2.0", "safe-buffer": "5.1.2", "send": "0.16.2", "serve-static": "1.13.2", "setprototypeof": "1.1.0", "statuses": "~1.4.0", "type-is": "~1.6.16", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@2.0.1", "", {}, "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="],
|
||||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||
|
||||
"finalhandler": ["finalhandler@1.1.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "on-finished": "~2.3.0", "parseurl": "~1.3.2", "statuses": "~1.4.0", "unpipe": "~1.0.0" } }, "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg=="],
|
||||
|
||||
"find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="],
|
||||
|
||||
"flaschenpost": ["flaschenpost@1.1.3", "", { "dependencies": { "@babel/runtime": "7.2.0", "app-root-path": "2.1.0", "babel-runtime": "6.26.0", "chalk": "2.4.1", "find-root": "1.1.0", "lodash": "4.17.11", "moment": "2.22.2", "processenv": "1.1.0", "split2": "3.0.0", "stack-trace": "0.0.10", "stringify-object": "3.3.0", "untildify": "3.0.3", "util.promisify": "1.0.0", "varname": "2.0.3" }, "bin": { "flaschenpost-uncork": "dist/bin/flaschenpost-uncork.js", "flaschenpost-normalize": "dist/bin/flaschenpost-normalize.js" } }, "sha512-1VAYPvDsVBGFJyUrOa/6clnJwZYC3qVq9nJLcypy6lvaaNbo1wOQiH8HQ+4Fw/k51pVG7JHzSf5epb8lmIW86g=="],
|
||||
|
||||
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
||||
|
||||
"formats": ["formats@1.0.0", "", {}, "sha512-For0Y8egwEK96JgJo4NONErPhtl7H2QzeB2NYGmzeGeJ8a1JZqPgLYOtM3oJRCYhmgsdDFd6KGRYyfe37XY4Yg=="],
|
||||
|
||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
|
||||
"fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
|
||||
|
||||
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="],
|
||||
|
||||
"functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
|
||||
|
||||
"generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
|
||||
|
||||
"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-own-enumerable-property-symbols": ["get-own-enumerable-property-symbols@3.0.2", "", {}, "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
|
||||
|
||||
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
|
||||
|
||||
"has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
|
||||
|
||||
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
|
||||
|
||||
"has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"hase": ["hase@2.0.0", "", { "dependencies": { "@babel/runtime": "7.1.2", "amqplib": "0.5.2" } }, "sha512-L83pBR/oZvQQNjv4kw9aUpTqBxERPiY7B42jsmkt1VDeUaRVhYkEIKzkCqrppjtxHe2EZqzZJzuhMXsWsxYIsw=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hono": ["hono@4.10.8", "", {}, "sha512-DDT0A0r6wzhe8zCGoYOmMeuGu3dyTAE40HHjwUsWFTEy5WxK1x2WDSsBPlEXgPbRIFY6miDualuUDbasPogIww=="],
|
||||
|
||||
"http-errors": ["http-errors@1.6.3", "", { "dependencies": { "depd": "~1.1.2", "inherits": "2.0.3", "setprototypeof": "1.1.0", "statuses": ">= 1.4.0 < 2" } }, "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.4.23", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.3", "", {}, "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="],
|
||||
|
||||
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
||||
|
||||
"ioredis": ["ioredis@5.9.2", "", { "dependencies": { "@ioredis/commands": "1.5.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
|
||||
|
||||
"is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="],
|
||||
|
||||
"is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="],
|
||||
|
||||
"is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="],
|
||||
|
||||
"is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
|
||||
|
||||
"is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="],
|
||||
|
||||
"is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="],
|
||||
|
||||
"is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="],
|
||||
|
||||
"is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="],
|
||||
|
||||
"is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
|
||||
|
||||
"is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="],
|
||||
|
||||
"is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
|
||||
|
||||
"is-obj": ["is-obj@1.0.1", "", {}, "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg=="],
|
||||
|
||||
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
|
||||
|
||||
"is-regexp": ["is-regexp@1.0.0", "", {}, "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA=="],
|
||||
|
||||
"is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
|
||||
|
||||
"is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="],
|
||||
|
||||
"is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="],
|
||||
|
||||
"is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="],
|
||||
|
||||
"is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
|
||||
|
||||
"is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="],
|
||||
|
||||
"is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="],
|
||||
|
||||
"is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="],
|
||||
|
||||
"isarray": ["isarray@0.0.1", "", {}, "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="],
|
||||
|
||||
"json-lines": ["json-lines@1.0.0", "", { "dependencies": { "timer2": "1.0.0" } }, "sha512-ytuLZb4RBQb3bTRsG/QBenyIo5oHLpjeCVph3s2NnoAsZE9K6h+uR+OWpEOWV1UeHdX63tYctGppBpGAc+JNMA=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"jsonwebtoken": ["jsonwebtoken@8.5.0", "", { "dependencies": { "jws": "^3.2.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^5.6.0" } }, "sha512-IqEycp0znWHNA11TpYi77bVgyBO/pGESDh7Ajhas+u0ttkGkKYIIAjniL4Bw5+oVejVF+SYkaI7XKfwCCyeTuA=="],
|
||||
|
||||
"jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="],
|
||||
|
||||
"jws": ["jws@3.2.3", "", { "dependencies": { "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } }, "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g=="],
|
||||
|
||||
"limes": ["limes@2.0.0", "", { "dependencies": { "@babel/runtime": "7.3.4", "jsonwebtoken": "8.5.0" } }, "sha512-evWD0pnTgPX7QueaSoJl5JBUL30T1ZVzo34ke97tIKmeagqhBTYK/JkKL0vtG3MpNApw8ZY9TlbybfwEz9knBA=="],
|
||||
|
||||
"lodash": ["lodash@4.17.11", "", {}, "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="],
|
||||
|
||||
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
|
||||
|
||||
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
|
||||
|
||||
"lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="],
|
||||
|
||||
"lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
|
||||
|
||||
"lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="],
|
||||
|
||||
"lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="],
|
||||
|
||||
"lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
|
||||
|
||||
"lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="],
|
||||
|
||||
"lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="],
|
||||
|
||||
"lusca": ["lusca@1.6.1", "", { "dependencies": { "tsscmp": "^1.0.5" } }, "sha512-+JzvUMH/rsE/4XfHdDOl70bip0beRcHSviYATQM0vtls59uVtdn1JMu4iD7ZShBpAmFG8EnaA+PrYG9sECMIOQ=="],
|
||||
|
||||
"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.1", "", {}, "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="],
|
||||
|
||||
"methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="],
|
||||
|
||||
"mime": ["mime@1.4.1", "", { "bin": { "mime": "cli.js" } }, "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"moment": ["moment@2.22.2", "", {}, "sha512-LRvkBHaJGnrcWvqsElsOhHCzj8mU39wLx5pQ0pc6s153GynCTsPdGdqsVNKAQD9sKnWj11iF7TZx9fpLwdD3fw=="],
|
||||
|
||||
"morgan": ["morgan@1.9.1", "", { "dependencies": { "basic-auth": "~2.0.0", "debug": "2.6.9", "depd": "~1.1.2", "on-finished": "~2.3.0", "on-headers": "~1.0.1" } }, "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||
|
||||
"nocache": ["nocache@2.0.0", "", {}, "sha512-YdKcy2x0dDwOh+8BEuHvA+mnOKAhmMQDgKBOCUGaLpewdmsRYguYZSom3yA+/OrE61O/q+NMQANnun65xpI1Hw=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
||||
|
||||
"node-rsa": ["node-rsa@0.4.2", "", { "dependencies": { "asn1": "0.2.3" } }, "sha512-Bvso6Zi9LY4otIZefYrscsUpo2mUpiAVIEmSZV2q41sP8tHZoert3Yu6zv4f/RXJqMNZQKCtnhDugIuCma23YA=="],
|
||||
|
||||
"node-statsd": ["node-statsd@0.1.1", "", {}, "sha512-QDf6R8VXF56QVe1boek8an/Rb3rSNaxoFWb7Elpsv2m1+Noua1yy0F1FpKpK5VluF8oymWM4w764A4KsYL4pDg=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
|
||||
|
||||
"object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="],
|
||||
|
||||
"object.getownpropertydescriptors": ["object.getownpropertydescriptors@2.1.9", "", { "dependencies": { "array.prototype.reduce": "^1.0.8", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "gopd": "^1.2.0", "safe-array-concat": "^1.1.3" } }, "sha512-mt8YM6XwsTTovI+kdZdHSxoyF2DI59up034orlC9NfweclcWOt7CVascNNLp6U+bjFVCVCIh9PwS76tDM/rH8g=="],
|
||||
|
||||
"on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="],
|
||||
|
||||
"on-headers": ["on-headers@1.0.2", "", {}, "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="],
|
||||
|
||||
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"partof": ["partof@1.0.0", "", {}, "sha512-+TXdhKCySpJDynCxgAPoGVyAkiK3QPusQ63/BdU5t68QcYzyU6zkP/T7F3gkMQBVUYqdWEADKa6Kx5zg8QIKrg=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@0.1.7", "", {}, "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"pocketbase": ["pocketbase@0.26.5", "", {}, "sha512-SXcq+sRvVpNxfLxPB1C+8eRatL7ZY4o3EVl/0OdE3MeR9fhPyZt0nmmxLqYmkLvXCN9qp3lXWV/0EUYb3MmMXQ=="],
|
||||
|
||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||
|
||||
"processenv": ["processenv@1.1.0", "", { "dependencies": { "babel-runtime": "6.26.0" } }, "sha512-SymqIsn8GjEUy8nG7HiyEjgbfk1xFosRIakUX1NHLpriq3vVpKniGrr9RdMWCaGYWByIovbRt2f/WvmP/IOApQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"qs": ["qs@6.5.2", "", {}, "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"raw-body": ["raw-body@2.3.3", "", { "dependencies": { "bytes": "3.0.0", "http-errors": "1.6.3", "iconv-lite": "0.4.23", "unpipe": "1.0.0" } }, "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
|
||||
|
||||
"redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="],
|
||||
|
||||
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
||||
|
||||
"regenerator-runtime": ["regenerator-runtime@0.12.1", "", {}, "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="],
|
||||
|
||||
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
|
||||
|
||||
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
|
||||
|
||||
"safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
||||
|
||||
"safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="],
|
||||
|
||||
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
|
||||
|
||||
"send": ["send@0.16.2", "", { "dependencies": { "debug": "2.6.9", "depd": "~1.1.2", "destroy": "~1.0.4", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "~1.6.2", "mime": "1.4.1", "ms": "2.0.0", "on-finished": "~2.3.0", "range-parser": "~1.2.0", "statuses": "~1.4.0" } }, "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw=="],
|
||||
|
||||
"serve-static": ["serve-static@1.13.2", "", { "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "parseurl": "~1.3.2", "send": "0.16.2" } }, "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw=="],
|
||||
|
||||
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
||||
|
||||
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
|
||||
|
||||
"set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="],
|
||||
|
||||
"setprototypeof": ["setprototypeof@1.1.0", "", {}, "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="],
|
||||
|
||||
"sha-1": ["sha-1@0.1.1", "", {}, "sha512-dexizf3hB7d4Jq6Cd0d/NYQiqgEqIfZIpuMfwPfvSb6h06DZKmHyUe55jYwpHC12R42wpqXO6ouhiBpRzIcD/g=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"split2": ["split2@3.0.0", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-Cp7G+nUfKJyHCrAI8kze3Q00PFGEG1pMgrAlTFlDbn+GW24evSZHJuMl+iUJx1w/NTRDeBiTgvwnf6YOt94FMw=="],
|
||||
|
||||
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
|
||||
|
||||
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
|
||||
|
||||
"statuses": ["statuses@1.4.0", "", {}, "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="],
|
||||
|
||||
"stethoskop": ["stethoskop@1.0.0", "", { "dependencies": { "node-statsd": "0.1.1" } }, "sha512-4JnZ+UmTs9SFfDjSHFlD/EoXcb1bfwntkt4h1ipNGrpxtRzmHTxOmdquCJvIrVu608Um7a09cGX0ZSOSllWJNQ=="],
|
||||
|
||||
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
||||
|
||||
"string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="],
|
||||
|
||||
"string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="],
|
||||
|
||||
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"stringify-object": ["stringify-object@3.3.0", "", { "dependencies": { "get-own-enumerable-property-symbols": "^3.0.0", "is-obj": "^1.0.1", "is-regexp": "^1.0.0" } }, "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw=="],
|
||||
|
||||
"supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="],
|
||||
|
||||
"tailwind": ["tailwind@4.0.0", "", { "dependencies": { "@babel/runtime": "7.3.4", "ajv": "6.10.0", "app-root-path": "2.1.0", "async-retry": "1.2.3", "body-parser": "1.18.3", "commands-events": "1.0.4", "compression": "1.7.3", "content-type": "1.0.4", "cors": "2.8.5", "crypto2": "2.0.0", "datasette": "1.0.1", "draht": "1.0.1", "express": "4.16.4 ", "flaschenpost": "1.1.3", "hase": "2.0.0", "json-lines": "1.0.0", "limes": "2.0.0", "lodash": "4.17.11", "lusca": "1.6.1", "morgan": "1.9.1", "nocache": "2.0.0", "partof": "1.0.0", "processenv": "1.1.0", "stethoskop": "1.0.0", "timer2": "1.0.0", "uuidv4": "3.0.1", "ws": "6.2.0" } }, "sha512-LlUNoD/5maFG1h5kQ6/hXfFPdcnYw+1Z7z+kUD/W/E71CUMwcnrskxiBM8c3G8wmPsD1VvCuqGYMHviI8+yrmg=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
|
||||
|
||||
"timer2": ["timer2@1.0.0", "", {}, "sha512-UOZql+P2ET0da+B7V3/RImN3IhC5ghb+9cpecfUhmYGIm0z73dDr3A781nBLnFYmRzeT1AmoT4w9Lgr8n7n7xg=="],
|
||||
|
||||
"tsscmp": ["tsscmp@1.0.6", "", {}, "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA=="],
|
||||
|
||||
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
|
||||
|
||||
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
|
||||
|
||||
"typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="],
|
||||
|
||||
"typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="],
|
||||
|
||||
"typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"untildify": ["untildify@3.0.3", "", {}, "sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"util.promisify": ["util.promisify@1.0.0", "", { "dependencies": { "define-properties": "^1.1.2", "object.getownpropertydescriptors": "^2.0.3" } }, "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA=="],
|
||||
|
||||
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
|
||||
|
||||
"uuid": ["uuid@3.3.2", "", { "bin": { "uuid": "./bin/uuid" } }, "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="],
|
||||
|
||||
"uuidv4": ["uuidv4@3.0.1", "", { "dependencies": { "uuid": "3.3.2" } }, "sha512-PPzksdWRl2a5C9hrs3OOYrArTeyoR0ftJ3jtOy+BnVHkT2UlrrzPNt9nTdiGuxmQItHM/AcTXahwZZC57Njojg=="],
|
||||
|
||||
"varname": ["varname@2.0.3", "", {}, "sha512-+DofT9mJAUALhnr9ipZ5Z2icwaEZ7DAajOZT4ffXy3MQqnXtG3b7atItLQEJCkfcJTOf9WcsywneOEibD4eqJg=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
|
||||
|
||||
"which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="],
|
||||
|
||||
"which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="],
|
||||
|
||||
"which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="],
|
||||
|
||||
"ws": ["ws@6.2.0", "", { "dependencies": { "async-limiter": "~1.0.0" } }, "sha512-deZYUNlt2O4buFCa3t5bKLf8A7FPP/TVjwOeVNpw818Ma5nk4MLXls2eoEGS39o8119QIYxTrTDoPQ5B/gTD6w=="],
|
||||
|
||||
"amqplib/readable-stream": ["readable-stream@1.1.14", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "0.0.1", "string_decoder": "~0.10.x" } }, "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ=="],
|
||||
|
||||
"babel-runtime/regenerator-runtime": ["regenerator-runtime@0.11.1", "", {}, "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="],
|
||||
|
||||
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"commands-events/@babel/runtime": ["@babel/runtime@7.2.0", "", { "dependencies": { "regenerator-runtime": "^0.12.0" } }, "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg=="],
|
||||
|
||||
"commands-events/uuidv4": ["uuidv4@2.0.0", "", { "dependencies": { "sha-1": "0.1.1", "uuid": "3.3.2" } }, "sha512-sAUlwUVepcVk6bwnaW/oi6LCwMdueako5QQzRr90ioAVVcms6p1mV0PaSxK8gyAC4CRvKddsk217uUpZUbKd2Q=="],
|
||||
|
||||
"compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"datasette/lodash": ["lodash@4.17.5", "", {}, "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw=="],
|
||||
|
||||
"ecdsa-sig-formatter/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"flaschenpost/@babel/runtime": ["@babel/runtime@7.2.0", "", { "dependencies": { "regenerator-runtime": "^0.12.0" } }, "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg=="],
|
||||
|
||||
"hase/@babel/runtime": ["@babel/runtime@7.1.2", "", { "dependencies": { "regenerator-runtime": "^0.12.0" } }, "sha512-Y3SCjmhSupzFB6wcv1KmmFucH6gDVnI30WjOcicV10ju0cZjak3Jcs67YLIXBrmZYw1xCrVeJPbycFwrqNyxpg=="],
|
||||
|
||||
"mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"morgan/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"safe-array-concat/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
||||
"safe-push-apply/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
||||
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"send/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
||||
"amqplib/readable-stream/string_decoder": ["string_decoder@0.10.31", "", {}, "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="],
|
||||
|
||||
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"morgan/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 1.6 MiB |
@@ -1,39 +0,0 @@
|
||||
// PocketBase auth bootstrap shared by pages
|
||||
(function (global) {
|
||||
const pb = new PocketBase('https://pocketbase.ccllc.pro');
|
||||
|
||||
function authHeaders() {
|
||||
if (pb.authStore.isValid && pb.authStore.token) {
|
||||
return { Authorization: `Bearer ${pb.authStore.token}` };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function getDisplayName() {
|
||||
const model = pb.authStore.model || {};
|
||||
return (
|
||||
model.display_name ||
|
||||
model.name ||
|
||||
model.email ||
|
||||
model.username ||
|
||||
'Unknown'
|
||||
);
|
||||
}
|
||||
|
||||
function getEmail() {
|
||||
const model = pb.authStore.model || {};
|
||||
// Prefer email, but fall back to username if email missing
|
||||
return model.email || model.username || null;
|
||||
}
|
||||
|
||||
async function ensureAuth() {
|
||||
if (pb.authStore.isValid) return true;
|
||||
const path = new URL(window.location.href).pathname;
|
||||
if (!path.endsWith('signin.html')) {
|
||||
window.location.href = 'signin.html';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
global.Auth = { pb, authHeaders, ensureAuth, getDisplayName, getEmail };
|
||||
})(window);
|
||||
@@ -1,5 +0,0 @@
|
||||
/* Generated Tailwind CSS - Basic version */
|
||||
/* This will be updated by tailwind CLI on dev */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -1,169 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sign In</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script>
|
||||
<script src="auth.js"></script>
|
||||
</head>
|
||||
<body class="font-sans bg-gradient-to-br from-indigo-500 to-purple-600 min-h-screen flex items-center justify-center">
|
||||
<div class="bg-white p-12 rounded-2xl shadow-2xl text-center max-w-md w-[90%]">
|
||||
<h1 class="text-gray-800 mb-2 text-3xl">Welcome</h1>
|
||||
<p class="text-gray-600 mb-8">Sign in with your Microsoft account</p>
|
||||
|
||||
<button id="loginBtn" onclick="login()" class="bg-blue-600 text-white border-none py-3.5 px-8 text-base rounded-md cursor-pointer transition-colors duration-200 font-medium hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed">Sign in with Microsoft</button>
|
||||
|
||||
<div class="text-red-600 mt-4 hidden" id="error"></div>
|
||||
|
||||
<div class="hidden mt-8 text-left bg-gray-50 p-6 rounded-lg" id="userInfo">
|
||||
<h2 class="text-lg mb-4 text-gray-800">Signed in</h2>
|
||||
<p class="mb-3 text-gray-600 break-all"><strong class="text-gray-800">Name:</strong> <span id="displayName"></span></p>
|
||||
<p class="mb-3 text-gray-600 break-all"><strong class="text-gray-800">Email:</strong> <span id="email"></span></p>
|
||||
|
||||
<button id="continueBtn" onclick="goToIndex()" class="hidden mt-4 bg-blue-600 text-white border-none py-3.5 px-8 text-base rounded-md cursor-pointer transition-colors duration-200 font-medium hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed w-full">Continue to Job Info</button>
|
||||
<button onclick="logout()" class="bg-gray-600 text-white border-none py-3.5 px-8 text-base rounded-md cursor-pointer transition-colors duration-200 font-medium hover:bg-gray-700 mt-4">Sign out</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const { pb, getDisplayName } = window.Auth;
|
||||
|
||||
const GRAPH_TOKEN_KEY = 'graphAccessToken';
|
||||
const extractGraphToken = (authData) => {
|
||||
return (
|
||||
authData?.meta?.graphAccessToken ||
|
||||
authData?.meta?.graph_token ||
|
||||
authData?.meta?.graphToken ||
|
||||
authData?.meta?.accessToken ||
|
||||
authData?.meta?.access_token ||
|
||||
authData?.meta?.token ||
|
||||
authData?.meta?.rawToken ||
|
||||
authData?.meta?.authData?.access_token ||
|
||||
''
|
||||
);
|
||||
};
|
||||
|
||||
const loginBtn = document.getElementById('loginBtn');
|
||||
const errorEl = document.getElementById('error');
|
||||
const userInfo = document.getElementById('userInfo');
|
||||
const continueBtn = document.getElementById('continueBtn');
|
||||
const GRAPH_REAUTH_FLAG = 'graphReauthAttempted';
|
||||
|
||||
function goToIndex() {
|
||||
window.location.href = 'index.html';
|
||||
}
|
||||
|
||||
function showAuthedUI(displayName, email) {
|
||||
loginBtn.classList.add('hidden');
|
||||
loginBtn.disabled = false;
|
||||
loginBtn.textContent = 'Sign in with Microsoft';
|
||||
userInfo.classList.remove('hidden');
|
||||
continueBtn.classList.remove('hidden');
|
||||
document.getElementById('displayName').textContent = displayName || 'Unknown';
|
||||
document.getElementById('email').textContent = email || 'Not available';
|
||||
}
|
||||
|
||||
async function login() {
|
||||
loginBtn.disabled = true;
|
||||
loginBtn.textContent = 'Signing in...';
|
||||
errorEl.classList.add('hidden');
|
||||
|
||||
try {
|
||||
const authData = await pb.collection('users').authWithOAuth2({
|
||||
provider: 'microsoft',
|
||||
});
|
||||
displayUserInfo(authData);
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
errorEl.textContent = error.message || 'Authentication failed. Please try again.';
|
||||
errorEl.classList.remove('hidden');
|
||||
loginBtn.disabled = false;
|
||||
loginBtn.textContent = 'Sign in with Microsoft';
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
pb.authStore.clear();
|
||||
localStorage.removeItem(GRAPH_TOKEN_KEY);
|
||||
loginBtn.classList.remove('hidden');
|
||||
userInfo.classList.add('hidden');
|
||||
continueBtn.classList.add('hidden');
|
||||
loginBtn.disabled = false;
|
||||
loginBtn.textContent = 'Sign in with Microsoft';
|
||||
}
|
||||
|
||||
function displayUserInfo(authData) {
|
||||
console.log('Auth response:', authData);
|
||||
console.log('Auth meta:', authData?.meta);
|
||||
const displayName = getDisplayName();
|
||||
const email = authData?.meta?.rawUser?.email || authData?.record?.email || pb.authStore.model?.email || 'Not available';
|
||||
const graphToken = extractGraphToken(authData);
|
||||
console.log('Extracted Graph token:', graphToken ? graphToken.substring(0, 30) + '...' : 'NOT FOUND');
|
||||
console.log('Full meta object:', JSON.stringify(authData?.meta, null, 2));
|
||||
|
||||
if (graphToken) {
|
||||
localStorage.setItem(GRAPH_TOKEN_KEY, graphToken);
|
||||
localStorage.removeItem(GRAPH_REAUTH_FLAG);
|
||||
console.log('✓ Graph token stored');
|
||||
} else {
|
||||
console.error('❌ Graph token NOT found in auth response!');
|
||||
console.error('Available meta keys:', Object.keys(authData?.meta || {}));
|
||||
// If token not in response, try fetching it from the backend
|
||||
console.log('Attempting to fetch token from backend...');
|
||||
fetchTokenFromBackend().then(token => {
|
||||
if (token) {
|
||||
localStorage.setItem(GRAPH_TOKEN_KEY, token);
|
||||
console.log('✓ Graph token fetched from backend');
|
||||
} else {
|
||||
console.warn('⚠ Graph token could not be obtained from backend');
|
||||
}
|
||||
showAuthedUI(displayName, email);
|
||||
}).catch(err => {
|
||||
console.error('Failed to fetch token from backend:', err);
|
||||
showAuthedUI(displayName, email);
|
||||
});
|
||||
return;
|
||||
}
|
||||
showAuthedUI(displayName, email);
|
||||
}
|
||||
|
||||
async function fetchTokenFromBackend() {
|
||||
try {
|
||||
const response = await fetch('/api/graph-token', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${pb.authStore.token}`
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
return data.token || '';
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Token fetch error:', err.message);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const reauth = params.get('reauth') === '1';
|
||||
if (pb.authStore.isValid) {
|
||||
try {
|
||||
const authData = await pb.collection('users').authRefresh();
|
||||
displayUserInfo(authData);
|
||||
// If we came from a reauth flow and now have a token, go back automatically
|
||||
const hasToken = localStorage.getItem(GRAPH_TOKEN_KEY);
|
||||
if (reauth && hasToken) {
|
||||
window.location.href = 'index.html';
|
||||
}
|
||||
} catch (err) {
|
||||
pb.authStore.clear();
|
||||
logout();
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||