diff --git a/api/.env.example b/api/.env.example new file mode 100644 index 0000000..9f514f0 --- /dev/null +++ b/api/.env.example @@ -0,0 +1,25 @@ +# Database Configuration +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=sankofa +DB_USER=postgres +# For development: minimum 8 characters +# For production: minimum 32 characters with uppercase, lowercase, numbers, and special characters +DB_PASSWORD=your_secure_password_here + +# Application Configuration +NODE_ENV=development +PORT=4000 + +# Keycloak Configuration (for Identity Service) +KEYCLOAK_URL=http://localhost:8080 +KEYCLOAK_REALM=master +KEYCLOAK_CLIENT_ID=sankofa-api +KEYCLOAK_CLIENT_SECRET=your_keycloak_client_secret + +# JWT Configuration +# For production: minimum 64 characters +JWT_SECRET=your_jwt_secret_here_minimum_64_chars_for_production + +# Logging +LOG_LEVEL=info diff --git a/api/.env.template b/api/.env.template new file mode 100644 index 0000000..2648714 --- /dev/null +++ b/api/.env.template @@ -0,0 +1,11 @@ +# Database Configuration +# IMPORTANT: Update DB_PASSWORD with your actual PostgreSQL password +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=sankofa +DB_USER=postgres +DB_PASSWORD=YOUR_ACTUAL_DATABASE_PASSWORD_HERE + +# Application Configuration +NODE_ENV=development +PORT=4000 diff --git a/api/DATABASE_SETUP.md b/api/DATABASE_SETUP.md new file mode 100644 index 0000000..600068b --- /dev/null +++ b/api/DATABASE_SETUP.md @@ -0,0 +1,148 @@ +# Database Setup Guide + +## Current Issue + +The setup is failing with database authentication error (28P01). This means: +- The database password in `.env` doesn't match your PostgreSQL password, OR +- PostgreSQL is not running, OR +- The database `sankofa` doesn't exist + +## Quick Fix + +### Option 1: Use Interactive Setup (Recommended) + +```bash +cd /home/intlc/projects/Sankofa/api +./scripts/setup-with-password.sh +``` + +This script will: +1. Prompt you for your actual PostgreSQL password +2. Update `.env` automatically +3. Run all setup steps + +### Option 2: Manual Setup + +#### Step 1: Find Your PostgreSQL Password + +If you don't know your PostgreSQL password, you can: + +**Option A**: Reset postgres user password +```bash +sudo -u postgres psql +ALTER USER postgres PASSWORD 'your_new_password'; +\q +``` + +**Option B**: Check if you have a password in your system +```bash +# Check common locations +cat ~/.pgpass 2>/dev/null +# or check if you have it saved elsewhere +``` + +#### Step 2: Update .env + +Edit `.env` and set the correct password: +```bash +cd /home/intlc/projects/Sankofa/api +nano .env # or use your preferred editor +``` + +Set: +```env +DB_PASSWORD=your_actual_postgres_password +NODE_ENV=development +``` + +#### Step 3: Create Database (if needed) + +```bash +# Connect to PostgreSQL +sudo -u postgres psql + +# Create database +CREATE DATABASE sankofa; + +# Exit +\q +``` + +#### Step 4: Run Setup + +```bash +./scripts/setup-sovereign-stack.sh +``` + +## Verify Database Connection + +Test your connection manually: + +```bash +psql -h localhost -U postgres -d sankofa +``` + +If this works, your password is correct. If it fails, update your password. + +## Common Solutions + +### Solution 1: Use Default PostgreSQL Setup + +If PostgreSQL was just installed, you might need to set a password: + +```bash +sudo -u postgres psql +ALTER USER postgres PASSWORD 'dev_sankofa_2024'; +\q +``` + +Then update `.env`: +```env +DB_PASSWORD=dev_sankofa_2024 +``` + +### Solution 2: Use Peer Authentication (Local Development) + +If you're running as the postgres user or have peer authentication: + +```bash +# Try connecting without password +sudo -u postgres psql -d sankofa + +# If that works, you can use empty password or configure .env differently +``` + +### Solution 3: Check PostgreSQL Status + +```bash +# Check if PostgreSQL is running +sudo systemctl status postgresql + +# Start if not running +sudo systemctl start postgresql + +# Enable on boot +sudo systemctl enable postgresql +``` + +## After Database is Configured + +Once your `.env` has the correct password: + +```bash +cd /home/intlc/projects/Sankofa/api +./scripts/setup-sovereign-stack.sh +``` + +Or use the interactive script: +```bash +./scripts/setup-with-password.sh +``` + +## Next Steps + +After successful setup: +1. ✅ All 9 services will be registered +2. ✅ Phoenix publisher will be created +3. ✅ You can query services via GraphQL +4. ✅ Services appear in marketplace diff --git a/api/FINAL_SETUP_INSTRUCTIONS.md b/api/FINAL_SETUP_INSTRUCTIONS.md new file mode 100644 index 0000000..d450638 --- /dev/null +++ b/api/FINAL_SETUP_INSTRUCTIONS.md @@ -0,0 +1,79 @@ +# Final Setup - Run This Now + +## ✅ Everything is Ready! + +All code is implemented. You just need to run **ONE command** to complete setup. + +## 🚀 Run This Command + +```bash +cd /home/intlc/projects/Sankofa/api +./ONE_COMMAND_SETUP.sh +``` + +**That's it!** This single script will: +1. ✅ Configure `.env` file +2. ✅ Create `sankofa` database +3. ✅ Set PostgreSQL password +4. ✅ Run all migrations +5. ✅ Seed all 9 services +6. ✅ Verify everything worked + +## What to Expect + +When you run the script: +- You'll be prompted for your **sudo password** (for database setup) +- The script will automatically do everything else +- At the end, you'll see: `✅ SETUP COMPLETE!` + +## If Sudo Requires Password + +The script needs sudo to: +- Create the database +- Set the PostgreSQL password + +Just enter your sudo password when prompted. + +## Alternative: Manual Database Setup + +If you prefer to set up the database manually first: + +```bash +# 1. Set up database (one command) +sudo -u postgres psql << 'EOSQL' +CREATE DATABASE sankofa; +ALTER USER postgres PASSWORD 'dev_sankofa_2024_secure'; +\q +EOSQL + +# 2. Then run the automated setup +cd /home/intlc/projects/Sankofa/api +./RUN_ME.sh +``` + +## After Setup + +Once complete, you'll have: +- ✅ Phoenix Cloud Services publisher +- ✅ 9 Sovereign Stack services registered +- ✅ All services with versions and pricing +- ✅ Services queryable via GraphQL +- ✅ Services visible in marketplace + +## Verify It Worked + +```bash +cd /home/intlc/projects/Sankofa/api +pnpm verify:sovereign-stack +``` + +Expected output: +``` +✅ Phoenix publisher found: Phoenix Cloud Services +✅ Found 9 Phoenix services +✅ All 9 expected services found! +``` + +--- + +**Ready?** Just run: `./ONE_COMMAND_SETUP.sh` 🎉 diff --git a/api/ONE_COMMAND_SETUP.sh b/api/ONE_COMMAND_SETUP.sh new file mode 100755 index 0000000..a00ef23 --- /dev/null +++ b/api/ONE_COMMAND_SETUP.sh @@ -0,0 +1,120 @@ +#!/bin/bash +# ONE COMMAND to set up everything - run this script + +set -e + +cd /home/intlc/projects/Sankofa/api + +echo "==========================================" +echo "Sovereign Stack - Complete Setup" +echo "==========================================" +echo "" +echo "This will:" +echo " 1. Set up PostgreSQL database" +echo " 2. Configure .env file" +echo " 3. Run migrations" +echo " 4. Seed all services" +echo " 5. Verify setup" +echo "" +echo "You may be prompted for your sudo password." +echo "" +echo "Starting setup in 2 seconds..." +sleep 2 + +# Step 1: Ensure .env is configured +echo "" +echo "Step 1: Configuring .env..." +if [ ! -f .env ]; then + cat > .env << 'ENVEOF' +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=sankofa +DB_USER=postgres +DB_PASSWORD=dev_sankofa_2024_secure +NODE_ENV=development +PORT=4000 +ENVEOF + echo "✅ Created .env file" +else + # Update password + sed -i 's|^DB_PASSWORD=.*|DB_PASSWORD=dev_sankofa_2024_secure|' .env || echo "DB_PASSWORD=dev_sankofa_2024_secure" >> .env + sed -i 's|^NODE_ENV=.*|NODE_ENV=development|' .env || echo "NODE_ENV=development" >> .env + echo "✅ Updated .env file" +fi + +# Step 2: Set up database +echo "" +echo "Step 2: Setting up database..." +echo "(You may be prompted for sudo password)" + +if ! sudo -u postgres psql << 'EOSQL'; then +-- Create database if it doesn't exist +SELECT 'CREATE DATABASE sankofa' +WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'sankofa')\gexec + +-- Set password +ALTER USER postgres WITH PASSWORD 'dev_sankofa_2024_secure'; +EOSQL + echo "❌ Database setup failed" + echo "" + echo "Please run manually:" + echo " sudo -u postgres psql" + echo " CREATE DATABASE sankofa;" + echo " ALTER USER postgres PASSWORD 'dev_sankofa_2024_secure';" + echo " \\q" + exit 1 +fi + +echo "✅ Database configured" + +# Step 3: Test connection +echo "" +echo "Step 3: Testing database connection..." +sleep 1 + +if PGPASSWORD="dev_sankofa_2024_secure" psql -h localhost -U postgres -d sankofa -c "SELECT 1;" >/dev/null 2>&1; then + echo "✅ Database connection successful" +else + echo "❌ Database connection failed" + echo "Please verify PostgreSQL is running and try again" + exit 1 +fi + +# Step 4: Run migrations +echo "" +echo "Step 4: Running migrations..." +pnpm db:migrate:up || { + echo "❌ Migration failed" + exit 1 +} +echo "✅ Migrations completed" + +# Step 5: Seed services +echo "" +echo "Step 5: Seeding Sovereign Stack services..." +pnpm db:seed:sovereign-stack || { + echo "❌ Seeding failed" + exit 1 +} +echo "✅ Services seeded" + +# Step 6: Verify +echo "" +echo "Step 6: Verifying setup..." +pnpm verify:sovereign-stack || { + echo "⚠ Verification found issues" + exit 1 +} + +echo "" +echo "==========================================" +echo "✅ SETUP COMPLETE!" +echo "==========================================" +echo "" +echo "All 9 Sovereign Stack services are now registered!" +echo "" +echo "Next steps:" +echo " 1. Access marketplace: https://portal.sankofa.nexus/marketplace" +echo " 2. Query via GraphQL API" +echo " 3. Browse Phoenix Cloud Services offerings" +echo "" diff --git a/api/QUICK_FIX_SYNTAX.md b/api/QUICK_FIX_SYNTAX.md new file mode 100644 index 0000000..cc06ffc --- /dev/null +++ b/api/QUICK_FIX_SYNTAX.md @@ -0,0 +1,59 @@ +# ✅ Syntax Error Fixed! + +The syntax error in `ONE_COMMAND_SETUP.sh` has been fixed. The script is now ready to run. + +## Run the Setup + +The script needs your **sudo password** to create the database. Run: + +```bash +cd /home/intlc/projects/Sankofa/api +./ONE_COMMAND_SETUP.sh +``` + +When prompted, enter your sudo password. + +## What the Script Does + +1. ✅ Configures `.env` file (already done) +2. ⏳ Creates `sankofa` database (needs sudo) +3. ⏳ Sets PostgreSQL password (needs sudo) +4. ⏳ Runs migrations +5. ⏳ Seeds all 9 services +6. ⏳ Verifies setup + +## Alternative: Manual Database Setup + +If you prefer to set up the database manually first: + +```bash +# 1. Create database and set password (one command) +sudo -u postgres psql << 'EOSQL' +CREATE DATABASE sankofa; +ALTER USER postgres PASSWORD 'dev_sankofa_2024_secure'; +\q +EOSQL + +# 2. Then run automated setup (no sudo needed) +cd /home/intlc/projects/Sankofa/api +./RUN_ME.sh +``` + +## After Setup + +Once complete, verify: + +```bash +pnpm verify:sovereign-stack +``` + +You should see: +``` +✅ Phoenix publisher found: Phoenix Cloud Services +✅ Found 9 Phoenix services +✅ All 9 expected services found! +``` + +--- + +**The script is fixed and ready!** Just run it and enter your sudo password when prompted. 🚀 diff --git a/api/README_SOVEREIGN_STACK.md b/api/README_SOVEREIGN_STACK.md new file mode 100644 index 0000000..21c0d04 --- /dev/null +++ b/api/README_SOVEREIGN_STACK.md @@ -0,0 +1,130 @@ +# Sovereign Stack Marketplace Services + +This document provides quick reference for the Sovereign Stack services implementation. + +## Quick Start + +### Setup (One Command) + +```bash +cd /home/intlc/projects/Sankofa/api +./scripts/setup-sovereign-stack.sh +``` + +### Manual Steps + +```bash +# 1. Run migration to add categories +pnpm db:migrate:up + +# 2. Seed all services +pnpm db:seed:sovereign-stack + +# 3. Verify everything worked +pnpm verify:sovereign-stack +``` + +## What Was Created + +### Database +- **Migration 025**: Adds 5 new product categories + Phoenix publisher +- **Seed Script**: Registers 9 services with versions and pricing + +### Services +- 9 service implementation stubs in `src/services/sovereign-stack/` +- All services follow the master plan architecture + +### Documentation +- Complete API documentation for each service +- Setup guide and implementation summary + +## Services Overview + +| Service | Category | Pricing Model | Free Tier | +|---------|----------|---------------|-----------| +| Ledger Service | LEDGER_SERVICES | Usage-based | 10K entries/month | +| Identity Service | IDENTITY_SERVICES | Subscription | - | +| Wallet Registry | WALLET_SERVICES | Hybrid | - | +| Transaction Orchestrator | ORCHESTRATION_SERVICES | Usage-based | 1K tx/month | +| Messaging Orchestrator | ORCHESTRATION_SERVICES | Usage-based | 1K messages/month | +| Voice Orchestrator | ORCHESTRATION_SERVICES | Usage-based | 100 syntheses/month | +| Event Bus | PLATFORM_SERVICES | Subscription | - | +| Audit Service | PLATFORM_SERVICES | Storage-based | 100K logs/month | +| Observability | PLATFORM_SERVICES | Usage-based | 1M metrics/month | + +## GraphQL Queries + +### List All Phoenix Services + +```graphql +query { + publisher(name: "phoenix-cloud-services") { + id + displayName + products { + id + name + slug + category + status + } + } +} +``` + +### Filter by Category + +```graphql +query { + products(filter: { category: LEDGER_SERVICES }) { + name + description + pricing { + pricingType + basePrice + usageRates + } + } +} +``` + +## File Locations + +- **Migration**: `src/db/migrations/025_sovereign_stack_marketplace.ts` +- **Seed Script**: `src/db/seeds/sovereign_stack_services.ts` +- **Services**: `src/services/sovereign-stack/*.ts` +- **Documentation**: `docs/marketplace/sovereign-stack/*.md` +- **Setup Script**: `scripts/setup-sovereign-stack.sh` +- **Verification**: `scripts/verify-sovereign-stack.ts` + +## Troubleshooting + +### Migration Fails +- Check database connection in `.env` +- Ensure PostgreSQL is running +- Verify user has CREATE/ALTER permissions + +### Seed Fails +- Ensure migration 025 ran successfully +- Check that Phoenix publisher exists +- Review error logs + +### Services Not Appearing +- Run verification: `pnpm verify:sovereign-stack` +- Re-run seed: `pnpm db:seed:sovereign-stack` +- Check GraphQL query filters + +## Next Steps + +1. ✅ Run setup script +2. ✅ Verify services appear in marketplace +3. ⏳ Implement full service logic (stubs are ready) +4. ⏳ Build provider adapters +5. ⏳ Create API endpoints +6. ⏳ Build frontend marketplace UI + +## Support + +- **Documentation**: `docs/marketplace/sovereign-stack/` +- **Setup Guide**: `docs/marketplace/sovereign-stack/SETUP.md` +- **Implementation Summary**: `docs/marketplace/sovereign-stack/IMPLEMENTATION_SUMMARY.md` diff --git a/api/RUN_ME.sh b/api/RUN_ME.sh new file mode 100755 index 0000000..b326992 --- /dev/null +++ b/api/RUN_ME.sh @@ -0,0 +1,67 @@ +#!/bin/bash +# Complete setup script - run this after database is configured + +cd /home/intlc/projects/Sankofa/api + +echo "==========================================" +echo "Sovereign Stack Complete Setup" +echo "==========================================" +echo "" + +# Check database connection first +DB_PASS=$(grep "^DB_PASSWORD=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' | tr -d "'" | xargs) + +if [ -z "$DB_PASS" ] || [ "$DB_PASS" = "your_secure_password_here" ]; then + echo "❌ DB_PASSWORD not set in .env" + echo "" + echo "Please run first:" + echo " ./scripts/manual-db-setup.sh" + exit 1 +fi + +# Test connection +if ! PGPASSWORD="$DB_PASS" psql -h localhost -U postgres -d sankofa -c "SELECT 1;" >/dev/null 2>&1; then + echo "❌ Cannot connect to database" + echo "" + echo "Please:" + echo " 1. Verify database exists: psql -U postgres -l | grep sankofa" + echo " 2. Verify password is correct in .env" + echo " 3. Run: ./scripts/manual-db-setup.sh" + exit 1 +fi + +echo "✅ Database connection verified" +echo "" + +# Run migrations +echo "Step 1: Running migrations..." +pnpm db:migrate:up && echo "✅ Migrations completed" || { + echo "❌ Migration failed" + exit 1 +} + +echo "" + +# Seed services +echo "Step 2: Seeding services..." +pnpm db:seed:sovereign-stack && echo "✅ Services seeded" || { + echo "❌ Seeding failed" + exit 1 +} + +echo "" + +# Verify +echo "Step 3: Verifying..." +pnpm verify:sovereign-stack && { + echo "" + echo "==========================================" + echo "✅ SETUP COMPLETE!" + echo "==========================================" + echo "" + echo "All 9 Sovereign Stack services are now registered!" + echo "Access them via GraphQL API or marketplace portal." +} || { + echo "⚠ Verification found issues" + exit 1 +} diff --git a/api/RUN_SETUP_NOW.md b/api/RUN_SETUP_NOW.md new file mode 100644 index 0000000..52ed78e --- /dev/null +++ b/api/RUN_SETUP_NOW.md @@ -0,0 +1,134 @@ +# Run Setup Now - Step by Step + +## Current Status + +✅ All code is implemented and ready +⚠ Database needs to be configured +⚠ PostgreSQL password needs to be set + +## Quick Setup (Choose One Method) + +### Method 1: Interactive Setup (Easiest) + +```bash +cd /home/intlc/projects/Sankofa/api + +# This will guide you through database setup +./scripts/manual-db-setup.sh + +# Then run the main setup +./scripts/setup-sovereign-stack.sh +``` + +### Method 2: Manual Database Setup + +#### Step 1: Create Database + +```bash +# Option A: With sudo +sudo -u postgres createdb sankofa + +# Option B: If you have postgres access +createdb -U postgres sankofa +``` + +#### Step 2: Set PostgreSQL Password + +```bash +# Connect to PostgreSQL +sudo -u postgres psql + +# Set password (choose a password that's at least 8 characters) +ALTER USER postgres PASSWORD 'dev_sankofa_2024_secure'; +\q +``` + +#### Step 3: Update .env + +```bash +cd /home/intlc/projects/Sankofa/api + +# Edit .env and set: +# DB_PASSWORD=dev_sankofa_2024_secure +# NODE_ENV=development + +nano .env # or your preferred editor +``` + +#### Step 4: Run Setup + +```bash +./scripts/setup-sovereign-stack.sh +``` + +### Method 3: If You Know Your Current Password + +If you already know your PostgreSQL password: + +```bash +cd /home/intlc/projects/Sankofa/api + +# 1. Update .env with your password +nano .env +# Set: DB_PASSWORD=your_actual_password + +# 2. Create database if needed +createdb -U postgres sankofa # or use sudo if needed + +# 3. Run setup +./scripts/setup-sovereign-stack.sh +``` + +## What Will Happen + +Once database is configured, the setup will: + +1. ✅ Run migration 025 (adds new categories + Phoenix publisher) +2. ✅ Seed all 9 Sovereign Stack services +3. ✅ Create product versions (v1.0.0) +4. ✅ Set up pricing models +5. ✅ Verify everything worked + +## Expected Output + +After successful setup: + +``` +✅ Migrations completed +✅ Services seeded +✅ Phoenix publisher found: Phoenix Cloud Services +✅ Found 9 Phoenix services +✅ All 9 expected services found! +✅ Sovereign Stack setup complete! +``` + +## Troubleshooting + +**"Database does not exist"** +```bash +sudo -u postgres createdb sankofa +``` + +**"Password authentication failed"** +```bash +# Set password +sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'your_password';" + +# Update .env +nano .env # Set DB_PASSWORD=your_password +``` + +**"Permission denied"** +- You may need sudo access for database operations +- Or configure PostgreSQL to allow your user + +## Next Steps After Setup + +1. ✅ Services will be in marketplace +2. ✅ Query via GraphQL API +3. ✅ Access via portal +4. ⏳ Implement full service logic (stubs ready) + +--- + +**Ready to proceed?** Run `./scripts/manual-db-setup.sh` to get started! diff --git a/api/SETUP_INSTRUCTIONS.md b/api/SETUP_INSTRUCTIONS.md new file mode 100644 index 0000000..8de20e5 --- /dev/null +++ b/api/SETUP_INSTRUCTIONS.md @@ -0,0 +1,135 @@ +# Setup Instructions - Sovereign Stack Marketplace + +## Prerequisites + +1. **PostgreSQL Database** running and accessible +2. **Node.js 18+** and **pnpm** installed +3. **Environment Variables** configured + +## Quick Setup + +### Option 1: Automated (Recommended) + +```bash +cd /home/intlc/projects/Sankofa/api +./scripts/setup-sovereign-stack.sh +``` + +The script will: +- Check for `.env` file and help create it if missing +- Run database migrations +- Seed all 9 Sovereign Stack services +- Verify the setup + +### Option 2: Manual Steps + +```bash +cd /home/intlc/projects/Sankofa/api + +# 1. Create .env file +pnpm create-env +# Then edit .env and set DB_PASSWORD + +# 2. Run migrations +pnpm db:migrate:up + +# 3. Seed services +pnpm db:seed:sovereign-stack + +# 4. Verify +pnpm verify:sovereign-stack +``` + +## Environment Configuration + +### Create .env File + +```bash +# Use helper script +pnpm create-env + +# Or copy manually +cp .env.example .env +``` + +### Required Variables + +Edit `.env` and set: + +```env +# Database (REQUIRED) +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=sankofa +DB_USER=postgres +DB_PASSWORD=your_secure_password_here # ⚠ REQUIRED + +# Application +NODE_ENV=development # Set to 'development' for relaxed password requirements +PORT=4000 +``` + +### Password Requirements + +**Development Mode** (`NODE_ENV=development`): +- Minimum 8 characters +- Not in insecure secrets list + +**Production Mode** (`NODE_ENV=production`): +- Minimum 32 characters +- Must contain: uppercase, lowercase, numbers, special characters + +**Example Development Password**: `dev_sankofa_2024` + +**Example Production Password**: `MySecureP@ssw0rd123!WithSpecialChars` + +## Troubleshooting + +### Error: "DB_PASSWORD is required but not provided" + +**Fix**: Create `.env` file and set `DB_PASSWORD`: +```bash +pnpm create-env +# Edit .env and set DB_PASSWORD +``` + +### Error: "Secret uses an insecure default value" + +**Fix**: Use a different password (not: password, admin, root, etc.) + +### Error: "Secret must be at least 32 characters" + +**Fix**: Either: +1. Set `NODE_ENV=development` in `.env` (relaxes to 8 chars) +2. Use a longer password (32+ chars with all requirements) + +See [TROUBLESHOOTING.md](../docs/marketplace/sovereign-stack/TROUBLESHOOTING.md) for more help. + +## Verification + +After setup, verify services: + +```bash +pnpm verify:sovereign-stack +``` + +Expected output: +``` +✅ Phoenix publisher found: Phoenix Cloud Services +✅ Found 9 Phoenix services +✅ All 9 expected services found! +``` + +## Next Steps + +1. ✅ Services are now registered in marketplace +2. ⏳ Access via GraphQL API or portal +3. ⏳ Subscribe to services as needed +4. ⏳ Implement full service logic (stubs are ready) + +## Documentation + +- **Quick Start**: `QUICK_START_SOVEREIGN_STACK.md` +- **Setup Guide**: `docs/marketplace/sovereign-stack/SETUP.md` +- **Troubleshooting**: `docs/marketplace/sovereign-stack/TROUBLESHOOTING.md` +- **Service Docs**: `docs/marketplace/sovereign-stack/*.md` diff --git a/api/docs/README.md b/api/docs/README.md new file mode 100644 index 0000000..167f55b --- /dev/null +++ b/api/docs/README.md @@ -0,0 +1,5 @@ +# Phoenix API — API docs + +- **GraphQL:** `POST /graphql`. Schema: `GET /graphql/schema`. Interactive: `GET /graphql-playground`. +- **OpenAPI (GraphQL):** [openapi-graphql.yaml](./openapi-graphql.yaml) +- **REST railing (Infra/VE/Health, tenant me):** Same paths as Phoenix Deploy API; when `PHOENIX_RAILING_URL` is set, Sankofa API proxies to it. Full OpenAPI for the railing is in the `phoenix-deploy-api` repo (`openapi.yaml`). Tenant-scoped: `GET /api/v1/tenants/me/resources`, `GET /api/v1/tenants/me/health` (require JWT or X-API-Key with tenant). diff --git a/api/docs/openapi-graphql.yaml b/api/docs/openapi-graphql.yaml new file mode 100644 index 0000000..269ee24 --- /dev/null +++ b/api/docs/openapi-graphql.yaml @@ -0,0 +1,51 @@ +# Minimal OpenAPI 3 description for Phoenix API GraphQL endpoint. +# Full schema: GET /graphql/schema (SDL). Interactive: /graphql-playground +openapi: 3.0.3 +info: + title: Phoenix API — GraphQL + description: Sankofa Phoenix API GraphQL endpoint. Auth via JWT (Bearer) or X-API-Key for /api/v1/*. + version: 1.0.0 +servers: + - url: http://localhost:4000 + description: Default +paths: + /graphql: + post: + summary: GraphQL query or mutation + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [query] + properties: + query: { type: string } + operationName: { type: string } + variables: { type: object } + responses: + '200': + description: GraphQL response (data or errors) + '401': + description: Unauthorized (optional; many operations allow unauthenticated) + /graphql/schema: + get: + summary: GraphQL schema (SDL) + responses: + '200': + description: Schema as text/plain + /graphql-playground: + get: + summary: GraphQL docs and Sandbox link + responses: + '200': + description: HTML with endpoint and schema links +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + ApiKeyAuth: + type: apiKey + in: header + name: X-API-Key diff --git a/api/package.json b/api/package.json index 61c1d52..7be744a 100644 --- a/api/package.json +++ b/api/package.json @@ -16,7 +16,10 @@ "db:migrate:up": "tsx src/db/migrate.ts up", "db:migrate:down": "tsx src/db/migrate.ts down", "db:migrate:status": "tsx src/db/migrate.ts status", - "db:seed": "tsx src/db/seed.ts" + "db:seed": "tsx src/db/seed.ts", + "db:seed:sovereign-stack": "tsx src/db/seeds/sovereign_stack_services.ts", + "verify:sovereign-stack": "tsx scripts/verify-sovereign-stack.ts", + "create-env": "bash scripts/create-env.sh" }, "dependencies": { "@apollo/server": "^4.9.5", diff --git a/api/scripts/auto-setup-db.sh b/api/scripts/auto-setup-db.sh new file mode 100755 index 0000000..89ce594 --- /dev/null +++ b/api/scripts/auto-setup-db.sh @@ -0,0 +1,100 @@ +#!/bin/bash +# Automated database setup - tries multiple methods + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +API_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +cd "$API_DIR" + +echo "==========================================" +echo "Automated Database Setup" +echo "==========================================" +echo "" + +# Ensure .env exists with correct password +if [ ! -f .env ]; then + cat > .env << 'ENVEOF' +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=sankofa +DB_USER=postgres +DB_PASSWORD=dev_sankofa_2024_secure +NODE_ENV=development +PORT=4000 +ENVEOF + echo "✅ Created .env file" +else + # Update password in .env + if ! grep -q "^DB_PASSWORD=dev_sankofa_2024_secure" .env; then + sed -i 's|^DB_PASSWORD=.*|DB_PASSWORD=dev_sankofa_2024_secure|' .env || echo "DB_PASSWORD=dev_sankofa_2024_secure" >> .env + echo "✅ Updated .env with password" + fi + # Ensure NODE_ENV is development + if ! grep -q "^NODE_ENV=development" .env; then + sed -i 's|^NODE_ENV=.*|NODE_ENV=development|' .env || echo "NODE_ENV=development" >> .env + fi +fi + +echo "" +echo "Attempting to set up database..." +echo "" + +# Method 1: Try with sudo (may require password) +echo "Method 1: Trying with sudo..." +if sudo -n true 2>/dev/null; then + echo "Sudo access available (no password required)" + sudo -u postgres psql -c "CREATE DATABASE sankofa;" 2>/dev/null && echo "✅ Database created" || echo "Database may already exist" + sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'dev_sankofa_2024_secure';" 2>/dev/null && echo "✅ Password set" || echo "⚠ Could not set password (may already be set)" +else + echo "⚠ Sudo requires password - will try other methods" +fi + +# Method 2: Try direct connection +echo "" +echo "Method 2: Trying direct PostgreSQL connection..." +if psql -U postgres -d postgres -c "SELECT 1;" >/dev/null 2>&1; then + echo "✅ Can connect to PostgreSQL" + psql -U postgres -d postgres -c "CREATE DATABASE sankofa;" 2>/dev/null && echo "✅ Database created" || echo "Database may already exist" + psql -U postgres -d postgres -c "ALTER USER postgres PASSWORD 'dev_sankofa_2024_secure';" 2>/dev/null && echo "✅ Password set" || echo "⚠ Could not set password" +else + echo "⚠ Cannot connect directly" +fi + +# Method 3: Try with createdb +echo "" +echo "Method 3: Trying createdb command..." +if command -v createdb >/dev/null 2>&1; then + createdb -U postgres sankofa 2>/dev/null && echo "✅ Database created" || echo "Database may already exist" +else + echo "⚠ createdb command not found" +fi + +# Final test +echo "" +echo "Testing database connection..." +sleep 1 + +if PGPASSWORD="dev_sankofa_2024_secure" psql -h localhost -U postgres -d sankofa -c "SELECT 1;" >/dev/null 2>&1; then + echo "✅ Database connection successful!" + echo "" + echo "Database is ready. You can now run:" + echo " ./RUN_ME.sh" + echo "" + exit 0 +else + echo "❌ Database connection failed" + echo "" + echo "Please run manually:" + echo "" + echo " sudo -u postgres psql << EOSQL" + echo " CREATE DATABASE sankofa;" + echo " ALTER USER postgres PASSWORD 'dev_sankofa_2024_secure';" + echo " \\q" + echo " EOSQL" + echo "" + echo "Or see: setup-db-commands.txt" + echo "" + exit 1 +fi diff --git a/api/scripts/create-env.sh b/api/scripts/create-env.sh new file mode 100755 index 0000000..37ff41a --- /dev/null +++ b/api/scripts/create-env.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Helper script to create .env file from .env.example + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +API_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +cd "$API_DIR" + +if [ -f .env ]; then + echo "⚠ .env file already exists. Skipping creation." + echo "If you want to recreate it, delete .env first." + exit 0 +fi + +if [ ! -f .env.example ]; then + echo "❌ .env.example not found. Cannot create .env file." + exit 1 +fi + +echo "Creating .env file from .env.example..." +cp .env.example .env + +echo "" +echo "✅ .env file created!" +echo "" +echo "⚠ IMPORTANT: Please edit .env and set your database password:" +echo " DB_PASSWORD=your_secure_password_here" +echo "" +echo "For development: minimum 8 characters" +echo "For production: minimum 32 characters with uppercase, lowercase, numbers, and special characters" +echo "" diff --git a/api/scripts/manual-db-setup.sh b/api/scripts/manual-db-setup.sh new file mode 100755 index 0000000..a2aef88 --- /dev/null +++ b/api/scripts/manual-db-setup.sh @@ -0,0 +1,106 @@ +#!/bin/bash +# Manual database setup script - run this with appropriate permissions + +echo "==========================================" +echo "Manual Database Setup for Sovereign Stack" +echo "==========================================" +echo "" +echo "This script will help you set up the database." +echo "You may need to run some commands with sudo." +echo "" + +# Check if database exists +echo "Step 1: Checking if database 'sankofa' exists..." +if psql -U postgres -lqt 2>/dev/null | cut -d \| -f 1 | grep -qw sankofa; then + echo "✅ Database 'sankofa' already exists" +else + echo "Database 'sankofa' not found." + echo "" + echo "Please run ONE of the following commands:" + echo "" + echo "Option A (if you have sudo access):" + echo " sudo -u postgres createdb sankofa" + echo "" + echo "Option B (if you have postgres user access):" + echo " createdb -U postgres sankofa" + echo "" + echo "Option C (if you have a different PostgreSQL user):" + echo " createdb -U your_user sankofa" + echo "" + read -p "Press Enter after you've created the database..." +fi + +# Check current password +echo "" +echo "Step 2: Testing database connection..." +echo "Current .env password: $(grep '^DB_PASSWORD=' .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' | tr -d "'" || echo 'not set')" +echo "" + +# Try to connect +DB_PASS=$(grep "^DB_PASSWORD=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' | tr -d "'" | xargs) + +if [ -n "$DB_PASS" ] && [ "$DB_PASS" != "your_secure_password_here" ]; then + if PGPASSWORD="$DB_PASS" psql -h localhost -U postgres -d sankofa -c "SELECT 1;" >/dev/null 2>&1; then + echo "✅ Database connection successful with current password" + CONNECTION_OK=true + else + echo "❌ Database connection failed with current password" + CONNECTION_OK=false + fi +else + echo "⚠ No valid password set in .env" + CONNECTION_OK=false +fi + +if [ "$CONNECTION_OK" != "true" ]; then + echo "" + echo "You need to set the correct PostgreSQL password." + echo "" + echo "Option 1: Set password for postgres user" + echo " Run: sudo -u postgres psql" + echo " Then: ALTER USER postgres PASSWORD 'your_password';" + echo " Then: \\q" + echo "" + echo "Option 2: Update .env with existing password" + echo " Edit .env and set DB_PASSWORD to your actual PostgreSQL password" + echo "" + read -p "Press Enter when password is configured..." + + # Update .env if user wants + echo "" + read -p "Would you like to update .env with a new password now? (y/N) " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + read -sp "Enter PostgreSQL password: " NEW_PASS + echo "" + if [ -n "$NEW_PASS" ]; then + sed -i "s|^DB_PASSWORD=.*|DB_PASSWORD=$NEW_PASS|" .env + echo "✅ .env updated" + fi + fi +fi + +# Final connection test +echo "" +echo "Step 3: Final connection test..." +DB_PASS=$(grep "^DB_PASSWORD=" .env | cut -d'=' -f2- | tr -d '"' | tr -d "'" | xargs) + +if PGPASSWORD="$DB_PASS" psql -h localhost -U postgres -d sankofa -c "SELECT 1;" >/dev/null 2>&1; then + echo "✅ Database connection successful!" + echo "" + echo "You can now run:" + echo " ./scripts/setup-sovereign-stack.sh" + echo "" + echo "Or continue with migrations:" + echo " pnpm db:migrate:up" + echo " pnpm db:seed:sovereign-stack" + echo " pnpm verify:sovereign-stack" +else + echo "❌ Database connection still failing" + echo "" + echo "Please:" + echo " 1. Verify PostgreSQL is running: sudo systemctl status postgresql" + echo " 2. Verify database exists: psql -U postgres -l | grep sankofa" + echo " 3. Verify password is correct in .env" + echo " 4. Try connecting manually: psql -U postgres -d sankofa" +fi diff --git a/api/scripts/quick-setup.sh b/api/scripts/quick-setup.sh new file mode 100755 index 0000000..f1ce178 --- /dev/null +++ b/api/scripts/quick-setup.sh @@ -0,0 +1,136 @@ +#!/bin/bash +# Quick setup that handles database creation and password setup + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +API_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +cd "$API_DIR" + +echo "==========================================" +echo "Sovereign Stack - Quick Setup" +echo "==========================================" +echo "" + +# Step 1: Ensure .env exists +if [ ! -f .env ]; then + echo "Creating .env file..." + cp .env.example .env 2>/dev/null || { + cat > .env << 'EOF' +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=sankofa +DB_USER=postgres +DB_PASSWORD= +NODE_ENV=development +PORT=4000 +EOF + } +fi + +# Step 2: Check if database exists, create if not +echo "Checking database..." +DB_EXISTS=$(sudo -u postgres psql -lqt 2>/dev/null | cut -d \| -f 1 | grep -w sankofa | wc -l) + +if [ "$DB_EXISTS" -eq 0 ]; then + echo "Database 'sankofa' not found. Creating..." + sudo -u postgres createdb sankofa 2>/dev/null && echo "✅ Database created" || { + echo "⚠ Could not create database automatically." + echo "Please run manually: sudo -u postgres createdb sankofa" + } +else + echo "✅ Database 'sankofa' exists" +fi + +# Step 3: Get database password +CURRENT_PASS=$(grep "^DB_PASSWORD=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' | tr -d "'" | xargs) + +if [ -z "$CURRENT_PASS" ] || [ "$CURRENT_PASS" = "your_secure_password_here" ] || [ "$CURRENT_PASS" = "YOUR_ACTUAL_DATABASE_PASSWORD_HERE" ]; then + echo "" + echo "Database password needed." + echo "" + echo "Options:" + echo " 1. Enter your PostgreSQL password" + echo " 2. Set a new password for postgres user (recommended for development)" + echo "" + read -p "Choose option (1 or 2): " OPTION + + if [ "$OPTION" = "2" ]; then + echo "" + echo "Setting new password for postgres user..." + read -sp "Enter new password (min 8 chars): " NEW_PASS + echo "" + sudo -u postgres psql -c "ALTER USER postgres PASSWORD '$NEW_PASS';" 2>/dev/null && { + sed -i "s|^DB_PASSWORD=.*|DB_PASSWORD=$NEW_PASS|" .env + echo "✅ Password set and .env updated" + } || { + echo "❌ Failed to set password. Please set manually." + exit 1 + } + else + echo "" + read -sp "Enter PostgreSQL password: " DB_PASS + echo "" + sed -i "s|^DB_PASSWORD=.*|DB_PASSWORD=$DB_PASS|" .env + echo "✅ Password updated in .env" + fi +fi + +# Ensure NODE_ENV is development +if ! grep -q "^NODE_ENV=development" .env; then + sed -i 's/^NODE_ENV=.*/NODE_ENV=development/' .env || echo "NODE_ENV=development" >> .env +fi + +# Step 4: Test connection +echo "" +echo "Testing database connection..." +DB_PASS=$(grep "^DB_PASSWORD=" .env | cut -d'=' -f2- | tr -d '"' | tr -d "'" | xargs) +if PGPASSWORD="$DB_PASS" psql -h localhost -U postgres -d sankofa -c "SELECT 1;" >/dev/null 2>&1; then + echo "✅ Database connection successful" +else + echo "❌ Database connection failed" + echo "" + echo "Please verify:" + echo " 1. PostgreSQL is running: sudo systemctl status postgresql" + echo " 2. Password is correct in .env" + echo " 3. Database exists: sudo -u postgres psql -l | grep sankofa" + echo "" + echo "You can reset the postgres password with:" + echo " sudo -u postgres psql -c \"ALTER USER postgres PASSWORD 'your_password';\"" + exit 1 +fi + +# Step 5: Run migrations +echo "" +echo "Step 1: Running database migrations..." +echo "----------------------------------------" +pnpm db:migrate:up || { + echo "❌ Migration failed" + exit 1 +} +echo "✅ Migrations completed" + +# Step 6: Seed services +echo "" +echo "Step 2: Seeding Sovereign Stack services..." +echo "----------------------------------------" +pnpm db:seed:sovereign-stack || { + echo "❌ Seeding failed" + exit 1 +} +echo "✅ Services seeded" + +# Step 7: Verify +echo "" +echo "Step 3: Verifying setup..." +echo "----------------------------------------" +pnpm verify:sovereign-stack || { + echo "⚠ Verification found issues" + exit 1 +} + +echo "" +echo "==========================================" +echo "✅ Sovereign Stack setup complete!" +echo "==========================================" diff --git a/api/scripts/setup-sovereign-stack.sh b/api/scripts/setup-sovereign-stack.sh new file mode 100755 index 0000000..f5000a0 --- /dev/null +++ b/api/scripts/setup-sovereign-stack.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# Setup script for Sovereign Stack marketplace services +# This script runs migrations and seeds the marketplace + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +API_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +cd "$API_DIR" + +echo "==========================================" +echo "Sovereign Stack Marketplace Setup" +echo "==========================================" +echo "" +echo "This script will:" +echo " 1. Run database migrations (adds new categories)" +echo " 2. Seed all 9 Sovereign Stack services" +echo " 3. Verify the setup" +echo "" + +# Check if .env exists +if [ ! -f .env ]; then + echo "⚠ Warning: .env file not found." + echo "" + echo "Would you like to create one from .env.example? (y/N)" + read -p "> " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + if [ -f .env.example ]; then + ./scripts/create-env.sh + echo "" + echo "⚠ Please edit .env and set your database password before continuing." + echo "Press Enter when ready, or Ctrl+C to exit..." + read + else + echo "❌ .env.example not found. Please create .env manually." + exit 1 + fi + else + echo "" + echo "Please create a .env file with the following variables:" + echo " DB_HOST=localhost" + echo " DB_PORT=5432" + echo " DB_NAME=sankofa" + echo " DB_USER=postgres" + echo " DB_PASSWORD=your_password_here" + echo "" + echo "For development, DB_PASSWORD must be at least 8 characters." + echo "For production, DB_PASSWORD must be at least 32 characters with uppercase, lowercase, numbers, and special characters." + echo "" + read -p "Press Enter when .env is ready, or Ctrl+C to exit..." + fi +fi + +# Step 1: Run migrations +echo "Step 1: Running database migrations..." +echo "----------------------------------------" +pnpm db:migrate:up || { + echo "❌ Migration failed. Please check database connection and try again." + exit 1 +} +echo "✅ Migrations completed" +echo "" + +# Step 2: Seed Sovereign Stack services +echo "Step 2: Seeding Sovereign Stack services..." +echo "----------------------------------------" +pnpm db:seed:sovereign-stack || { + echo "❌ Seeding failed. Please check the error above." + exit 1 +} +echo "✅ Services seeded" +echo "" + +# Step 3: Verify setup +echo "Step 3: Verifying setup..." +echo "----------------------------------------" +pnpm verify:sovereign-stack || { + echo "⚠ Verification found issues. Please review the output above." + exit 1 +} +echo "" + +echo "==========================================" +echo "✅ Sovereign Stack setup complete!" +echo "==========================================" +echo "" +echo "Next steps:" +echo "1. Access the marketplace at: https://portal.sankofa.nexus/marketplace" +echo "2. Browse Phoenix Cloud Services offerings" +echo "3. Subscribe to services as needed" +echo "" diff --git a/api/scripts/setup-with-password.sh b/api/scripts/setup-with-password.sh new file mode 100755 index 0000000..0dea3b7 --- /dev/null +++ b/api/scripts/setup-with-password.sh @@ -0,0 +1,119 @@ +#!/bin/bash +# Interactive setup script that prompts for database password + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +API_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +cd "$API_DIR" + +echo "==========================================" +echo "Sovereign Stack Marketplace Setup" +echo "==========================================" +echo "" + +# Check if .env exists, if not create from template +if [ ! -f .env ]; then + echo "Creating .env file..." + if [ -f .env.example ]; then + cp .env.example .env + else + cat > .env << 'ENVEOF' +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=sankofa +DB_USER=postgres +DB_PASSWORD= +NODE_ENV=development +PORT=4000 +ENVEOF + fi + echo "✅ .env file created" + echo "" +fi + +# Check if DB_PASSWORD is set and not placeholder +CURRENT_PASSWORD=$(grep "^DB_PASSWORD=" .env | cut -d'=' -f2- | tr -d '"' | tr -d "'") + +if [ -z "$CURRENT_PASSWORD" ] || [ "$CURRENT_PASSWORD" = "your_secure_password_here" ] || [ "$CURRENT_PASSWORD" = "YOUR_ACTUAL_DATABASE_PASSWORD_HERE" ]; then + echo "⚠ Database password not set or using placeholder." + echo "" + echo "Please enter your PostgreSQL database password:" + echo "(For development: minimum 8 characters)" + read -s DB_PASS + echo "" + + if [ -z "$DB_PASS" ]; then + echo "❌ Password cannot be empty" + exit 1 + fi + + # Update .env with actual password + if grep -q "^DB_PASSWORD=" .env; then + sed -i "s|^DB_PASSWORD=.*|DB_PASSWORD=$DB_PASS|" .env + else + echo "DB_PASSWORD=$DB_PASS" >> .env + fi + + echo "✅ Password updated in .env" + echo "" +fi + +# Ensure NODE_ENV is set to development +if ! grep -q "^NODE_ENV=" .env; then + echo "NODE_ENV=development" >> .env +elif ! grep -q "^NODE_ENV=development" .env; then + sed -i 's/^NODE_ENV=.*/NODE_ENV=development/' .env +fi + +# Step 1: Run migrations +echo "Step 1: Running database migrations..." +echo "----------------------------------------" +if pnpm db:migrate:up; then + echo "✅ Migrations completed" +else + echo "❌ Migration failed." + echo "" + echo "Common issues:" + echo " 1. Database password is incorrect" + echo " 2. PostgreSQL is not running" + echo " 3. Database 'sankofa' does not exist" + echo "" + echo "To fix:" + echo " 1. Verify PostgreSQL is running: sudo systemctl status postgresql" + echo " 2. Create database if needed: createdb sankofa" + echo " 3. Update .env with correct password" + exit 1 +fi +echo "" + +# Step 2: Seed Sovereign Stack services +echo "Step 2: Seeding Sovereign Stack services..." +echo "----------------------------------------" +if pnpm db:seed:sovereign-stack; then + echo "✅ Services seeded" +else + echo "❌ Seeding failed. Please check the error above." + exit 1 +fi +echo "" + +# Step 3: Verify setup +echo "Step 3: Verifying setup..." +echo "----------------------------------------" +if pnpm verify:sovereign-stack; then + echo "" + echo "==========================================" + echo "✅ Sovereign Stack setup complete!" + echo "==========================================" + echo "" + echo "Next steps:" + echo "1. Access the marketplace at: https://portal.sankofa.nexus/marketplace" + echo "2. Browse Phoenix Cloud Services offerings" + echo "3. Subscribe to services as needed" + echo "" +else + echo "⚠ Verification found issues. Please review the output above." + exit 1 +fi diff --git a/api/scripts/verify-sovereign-stack.ts b/api/scripts/verify-sovereign-stack.ts new file mode 100644 index 0000000..06d8c5b --- /dev/null +++ b/api/scripts/verify-sovereign-stack.ts @@ -0,0 +1,141 @@ +/** + * Verification script for Sovereign Stack marketplace services + * Verifies that all services are properly registered in the marketplace + */ + +import 'dotenv/config' +import { getDb } from '../src/db/index.js' +import { logger } from '../src/lib/logger.js' + +async function verifySovereignStackServices() { + const db = getDb() + + try { + logger.info('Verifying Sovereign Stack marketplace services...') + + // 1. Verify Phoenix publisher exists + const publisherResult = await db.query( + `SELECT * FROM publishers WHERE name = 'phoenix-cloud-services'` + ) + + if (publisherResult.rows.length === 0) { + throw new Error('Phoenix publisher not found. Please run migration 025 first.') + } + + const publisher = publisherResult.rows[0] + logger.info(`✓ Phoenix publisher found: ${publisher.display_name} (${publisher.id})`) + logger.info(` Verified: ${publisher.verified}`) + logger.info(` Website: ${publisher.website_url || 'N/A'}`) + + // 2. Verify all 9 services exist + const expectedServices = [ + 'phoenix-ledger-service', + 'phoenix-identity-service', + 'phoenix-wallet-registry', + 'phoenix-tx-orchestrator', + 'phoenix-messaging-orchestrator', + 'phoenix-voice-orchestrator', + 'phoenix-event-bus', + 'phoenix-audit-service', + 'phoenix-observability' + ] + + const servicesResult = await db.query( + `SELECT p.*, pub.display_name as publisher_name + FROM products p + JOIN publishers pub ON p.publisher_id = pub.id + WHERE pub.name = 'phoenix-cloud-services' + ORDER BY p.name` + ) + + const foundServices = servicesResult.rows.map(row => row.slug) + logger.info(`\n✓ Found ${servicesResult.rows.length} Phoenix services:`) + + for (const service of servicesResult.rows) { + logger.info(` - ${service.name} (${service.slug})`) + logger.info(` Category: ${service.category}`) + logger.info(` Status: ${service.status}`) + logger.info(` Featured: ${service.featured}`) + } + + // Check for missing services + const missingServices = expectedServices.filter(slug => !foundServices.includes(slug)) + if (missingServices.length > 0) { + logger.warn(`\n⚠ Missing services: ${missingServices.join(', ')}`) + logger.warn('Please run: pnpm db:seed:sovereign-stack') + } else { + logger.info(`\n✓ All ${expectedServices.length} expected services found!`) + } + + // 3. Verify categories are available + const categoriesResult = await db.query( + `SELECT DISTINCT category FROM products WHERE publisher_id = $1`, + [publisher.id] + ) + + const categories = categoriesResult.rows.map(row => row.category) + logger.info(`\n✓ Services span ${categories.length} categories:`) + categories.forEach(cat => logger.info(` - ${cat}`)) + + // 4. Verify product versions exist + const versionsResult = await db.query( + `SELECT COUNT(*) as count + FROM product_versions pv + JOIN products p ON pv.product_id = p.id + JOIN publishers pub ON p.publisher_id = pub.id + WHERE pub.name = 'phoenix-cloud-services'` + ) + + const versionCount = parseInt(versionsResult.rows[0].count) + logger.info(`\n✓ Found ${versionCount} product versions`) + + // 5. Verify pricing models exist + const pricingResult = await db.query( + `SELECT COUNT(*) as count + FROM pricing_models pm + JOIN products p ON pm.product_id = p.id + JOIN publishers pub ON p.publisher_id = pub.id + WHERE pub.name = 'phoenix-cloud-services'` + ) + + const pricingCount = parseInt(pricingResult.rows[0].count) + logger.info(`✓ Found ${pricingCount} pricing models`) + + // 6. Summary + logger.info('\n' + '='.repeat(60)) + logger.info('VERIFICATION SUMMARY') + logger.info('='.repeat(60)) + logger.info(`Publisher: ${publisher.display_name} (${publisher.verified ? '✓ Verified' : '✗ Not verified'})`) + logger.info(`Services: ${foundServices.length}/${expectedServices.length}`) + logger.info(`Categories: ${categories.length}`) + logger.info(`Versions: ${versionCount}`) + logger.info(`Pricing Models: ${pricingCount}`) + + if (missingServices.length === 0 && versionCount >= expectedServices.length && pricingCount >= expectedServices.length) { + logger.info('\n✅ All Sovereign Stack services verified successfully!') + return true + } else { + logger.warn('\n⚠ Some services may need to be seeded. Run: pnpm db:seed:sovereign-stack') + return false + } + } catch (error) { + logger.error('Verification error', { error }) + throw error + } finally { + await db.end() + } +} + +// Run if called directly +if (import.meta.url === `file://${process.argv[1]}`) { + verifySovereignStackServices() + .then(success => { + process.exit(success ? 0 : 1) + }) + .catch((error) => { + logger.error('Failed to verify Sovereign Stack services', { error }) + process.exit(1) + }) +} + +export { verifySovereignStackServices } diff --git a/api/setup-db-commands.txt b/api/setup-db-commands.txt new file mode 100644 index 0000000..cb2bcf6 --- /dev/null +++ b/api/setup-db-commands.txt @@ -0,0 +1,19 @@ +# Run these commands to set up the database: + +# Option 1: If you have sudo access +sudo -u postgres psql << EOSQL +CREATE DATABASE sankofa; +ALTER USER postgres PASSWORD 'dev_sankofa_2024_secure'; +\q +EOSQL + +# Option 2: If you can connect as postgres user directly +psql -U postgres << EOSQL +CREATE DATABASE sankofa; +ALTER USER postgres PASSWORD 'dev_sankofa_2024_secure'; +\q +EOSQL + +# Option 3: Using createdb command +createdb -U postgres sankofa +psql -U postgres -c "ALTER USER postgres PASSWORD 'dev_sankofa_2024_secure';" diff --git a/api/src/__tests__/integration/phoenix-railing.test.ts b/api/src/__tests__/integration/phoenix-railing.test.ts new file mode 100644 index 0000000..8f177e7 --- /dev/null +++ b/api/src/__tests__/integration/phoenix-railing.test.ts @@ -0,0 +1,40 @@ +/** + * Integration tests for Phoenix API Railing routes: + * /api/v1/tenants/me/resources, /api/v1/tenants/me/health (tenant-scoped). + */ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest' +import Fastify from 'fastify' +import { registerPhoenixRailingRoutes } from '../../routes/phoenix-railing.js' + +describe('Phoenix Railing routes', () => { + let fastify: Awaited> + + beforeAll(async () => { + fastify = Fastify({ logger: false }) + fastify.decorateRequest('tenantContext', null) + await registerPhoenixRailingRoutes(fastify) + }) + + afterAll(async () => { + await fastify.close() + }) + + describe('GET /api/v1/tenants/me/resources', () => { + it('returns 401 when no tenant context', async () => { + const res = await fastify.inject({ method: 'GET', url: '/api/v1/tenants/me/resources' }) + expect(res.statusCode).toBe(401) + const body = JSON.parse(res.payload) + expect(body.error).toMatch(/tenant|required/i) + }) + }) + + describe('GET /api/v1/tenants/me/health', () => { + it('returns 401 when no tenant context', async () => { + const res = await fastify.inject({ method: 'GET', url: '/api/v1/tenants/me/health' }) + expect(res.statusCode).toBe(401) + const body = JSON.parse(res.payload) + expect(body.error).toBeDefined() + }) + }) +}) diff --git a/api/src/db/migrations/001_initial_schema.ts b/api/src/db/migrations/001_initial_schema.ts index e244775..3044008 100644 --- a/api/src/db/migrations/001_initial_schema.ts +++ b/api/src/db/migrations/001_initial_schema.ts @@ -61,15 +61,24 @@ export const up: Migration['up'] = async (db) => { await db.query(`CREATE INDEX IF NOT EXISTS idx_resources_status ON resources(status)`) await db.query(`CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)`) - // Triggers + // Triggers (use DROP IF EXISTS to avoid conflicts) + await db.query(` + DROP TRIGGER IF EXISTS update_users_updated_at ON users + `) await db.query(` CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column() `) + await db.query(` + DROP TRIGGER IF EXISTS update_sites_updated_at ON sites + `) await db.query(` CREATE TRIGGER update_sites_updated_at BEFORE UPDATE ON sites FOR EACH ROW EXECUTE FUNCTION update_updated_at_column() `) + await db.query(` + DROP TRIGGER IF EXISTS update_resources_updated_at ON resources + `) await db.query(` CREATE TRIGGER update_resources_updated_at BEFORE UPDATE ON resources FOR EACH ROW EXECUTE FUNCTION update_updated_at_column() diff --git a/api/src/db/migrations/023_industry_controls_and_waf.ts b/api/src/db/migrations/023_industry_controls_and_waf.ts index 22d74ae..2325708 100644 --- a/api/src/db/migrations/023_industry_controls_and_waf.ts +++ b/api/src/db/migrations/023_industry_controls_and_waf.ts @@ -58,15 +58,18 @@ export const up: Migration['up'] = async (db) => { await db.query(` INSERT INTO industry_controls (industry, pillar, control_code, name, description, compliance_frameworks, requirements) VALUES - ('FINANCIAL', 'SECURITY', 'PCI-DSS-1', 'PCI-DSS Compliance', 'Payment card industry data security', ARRAY['PCI-DSS'], ARRAY['Encrypt cardholder data', 'Restrict access']), - ('FINANCIAL', 'SECURITY', 'SOX-1', 'SOX Financial Controls', 'Sarbanes-Oxley financial reporting controls', ARRAY['SOX'], ARRAY['Financial audit trail', 'Access controls']), - ('FINANCIAL', 'RELIABILITY', 'FIN-REL-1', 'Financial System Availability', 'High availability for financial systems', ARRAY[], ARRAY['99.99% uptime', 'Disaster recovery']), - ('TELECOMMUNICATIONS', 'SECURITY', 'CALEA-1', 'CALEA Compliance', 'Lawful intercept capabilities', ARRAY['CALEA'], ARRAY['Intercept capability', 'Audit logging']), - ('TELECOMMUNICATIONS', 'RELIABILITY', 'TEL-REL-1', 'Network Availability', 'Telecom network reliability', ARRAY[], ARRAY['99.999% uptime', 'Redundancy']) + ('FINANCIAL', 'SECURITY', 'PCI-DSS-1', 'PCI-DSS Compliance', 'Payment card industry data security', ARRAY['PCI-DSS']::TEXT[], ARRAY['Encrypt cardholder data', 'Restrict access']::TEXT[]), + ('FINANCIAL', 'SECURITY', 'SOX-1', 'SOX Financial Controls', 'Sarbanes-Oxley financial reporting controls', ARRAY['SOX']::TEXT[], ARRAY['Financial audit trail', 'Access controls']::TEXT[]), + ('FINANCIAL', 'RELIABILITY', 'FIN-REL-1', 'Financial System Availability', 'High availability for financial systems', ARRAY[]::TEXT[], ARRAY['99.99% uptime', 'Disaster recovery']::TEXT[]), + ('TELECOMMUNICATIONS', 'SECURITY', 'CALEA-1', 'CALEA Compliance', 'Lawful intercept capabilities', ARRAY['CALEA']::TEXT[], ARRAY['Intercept capability', 'Audit logging']::TEXT[]), + ('TELECOMMUNICATIONS', 'RELIABILITY', 'TEL-REL-1', 'Network Availability', 'Telecom network reliability', ARRAY[]::TEXT[], ARRAY['99.999% uptime', 'Redundancy']::TEXT[]) ON CONFLICT (industry, pillar, control_code) DO NOTHING `) // Update triggers + await db.query(` + DROP TRIGGER IF EXISTS update_industry_controls_updated_at ON industry_controls + `) await db.query(` CREATE TRIGGER update_industry_controls_updated_at BEFORE UPDATE ON industry_controls @@ -74,6 +77,9 @@ export const up: Migration['up'] = async (db) => { EXECUTE FUNCTION update_updated_at_column() `) + await db.query(` + DROP TRIGGER IF EXISTS update_waf_assessments_updated_at ON waf_assessments + `) await db.query(` CREATE TRIGGER update_waf_assessments_updated_at BEFORE UPDATE ON waf_assessments diff --git a/api/src/db/migrations/025_sovereign_stack_marketplace.ts b/api/src/db/migrations/025_sovereign_stack_marketplace.ts new file mode 100644 index 0000000..3a60a19 --- /dev/null +++ b/api/src/db/migrations/025_sovereign_stack_marketplace.ts @@ -0,0 +1,88 @@ +import { Migration } from '../migrate.js' + +export const up: Migration['up'] = async (db) => { + // Add new product categories for Sovereign Stack services + // We need to drop and recreate the constraint to add new categories + await db.query(` + ALTER TABLE products + DROP CONSTRAINT IF EXISTS products_category_check + `) + + await db.query(` + ALTER TABLE products + ADD CONSTRAINT products_category_check + CHECK (category IN ( + 'COMPUTE', + 'NETWORK_INFRA', + 'BLOCKCHAIN_STACK', + 'BLOCKCHAIN_TOOLS', + 'FINANCIAL_MESSAGING', + 'INTERNET_REGISTRY', + 'AI_LLM_AGENT', + 'LEDGER_SERVICES', + 'IDENTITY_SERVICES', + 'WALLET_SERVICES', + 'ORCHESTRATION_SERVICES', + 'PLATFORM_SERVICES' + )) + `) + + // Create Phoenix Cloud Services publisher if it doesn't exist + await db.query(` + INSERT INTO publishers ( + name, + display_name, + description, + website_url, + logo_url, + verified, + metadata, + created_at, + updated_at + ) + VALUES ( + 'phoenix-cloud-services', + 'Phoenix Cloud Services', + 'Sovereign cloud infrastructure provider powering the Sankofa ecosystem. Phoenix delivers world-class cloud services with multi-tenancy, sovereign identity, and advanced billing capabilities.', + 'https://phoenix.sankofa.nexus', + 'https://cdn.sankofa.nexus/phoenix-logo.svg', + true, + '{"provider": "phoenix", "tier": "sovereign", "regions": 325, "sovereign_identity": true}'::jsonb, + NOW(), + NOW() + ) + ON CONFLICT (name) DO UPDATE SET + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + website_url = EXCLUDED.website_url, + logo_url = EXCLUDED.logo_url, + verified = true, + metadata = EXCLUDED.metadata, + updated_at = NOW() + `) +} + +export const down: Migration['down'] = async (db) => { + // Remove new categories, reverting to original set + await db.query(` + ALTER TABLE products + DROP CONSTRAINT IF EXISTS products_category_check + `) + + await db.query(` + ALTER TABLE products + ADD CONSTRAINT products_category_check + CHECK (category IN ( + 'COMPUTE', + 'NETWORK_INFRA', + 'BLOCKCHAIN_STACK', + 'BLOCKCHAIN_TOOLS', + 'FINANCIAL_MESSAGING', + 'INTERNET_REGISTRY', + 'AI_LLM_AGENT' + )) + `) + + // Note: We don't delete the Phoenix publisher in down migration + // as it may have been created manually or have dependencies +} diff --git a/api/src/db/migrations/026_api_keys.ts b/api/src/db/migrations/026_api_keys.ts new file mode 100644 index 0000000..3c67eee --- /dev/null +++ b/api/src/db/migrations/026_api_keys.ts @@ -0,0 +1,45 @@ +import { Migration } from '../migrate.js' + +/** + * API keys table for client/partner API access (key hash, tenant_id, scopes). + * Used by X-API-Key auth for /api/v1/* and Phoenix API Railing. + */ +export const up: Migration['up'] = async (db) => { + await db.query(` + CREATE TABLE IF NOT EXISTS api_keys ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(255) NOT NULL, + key_prefix VARCHAR(20) NOT NULL, + key_hash VARCHAR(255) NOT NULL UNIQUE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + tenant_id UUID REFERENCES tenants(id) ON DELETE SET NULL, + permissions JSONB DEFAULT '["read", "write"]'::jsonb, + last_used_at TIMESTAMP WITH TIME ZONE, + expires_at TIMESTAMP WITH TIME ZONE, + revoked BOOLEAN NOT NULL DEFAULT false, + revoked_at TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ) + `) + await db.query(`CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id)`) + await db.query(`CREATE INDEX IF NOT EXISTS idx_api_keys_tenant_id ON api_keys(tenant_id)`) + await db.query(`CREATE INDEX IF NOT EXISTS idx_api_keys_key_hash ON api_keys(key_hash)`) + await db.query(`CREATE INDEX IF NOT EXISTS idx_api_keys_revoked ON api_keys(revoked) WHERE revoked = false`) + await db.query(` + DROP TRIGGER IF EXISTS update_api_keys_updated_at ON api_keys + `) + await db.query(` + CREATE TRIGGER update_api_keys_updated_at BEFORE UPDATE ON api_keys + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column() + `) +} + +export const down: Migration['down'] = async (db) => { + await db.query(`DROP TRIGGER IF EXISTS update_api_keys_updated_at ON api_keys`) + await db.query(`DROP INDEX IF EXISTS idx_api_keys_revoked`) + await db.query(`DROP INDEX IF EXISTS idx_api_keys_key_hash`) + await db.query(`DROP INDEX IF EXISTS idx_api_keys_tenant_id`) + await db.query(`DROP INDEX IF EXISTS idx_api_keys_user_id`) + await db.query(`DROP TABLE IF EXISTS api_keys`) +} diff --git a/api/src/db/migrations/index.ts b/api/src/db/migrations/index.ts index 192e201..567585b 100644 --- a/api/src/db/migrations/index.ts +++ b/api/src/db/migrations/index.ts @@ -17,4 +17,13 @@ export { up as up013, down as down013 } from './013_mfa_and_rbac.js' export { up as up014, down as down014 } from './014_audit_logging.js' export { up as up015, down as down015 } from './015_incident_response_and_classification.js' export { up as up016, down as down016 } from './016_resource_sharing.js' - +export { up as up017, down as down017 } from './017_marketplace_catalog.js' +export { up as up018, down as down018 } from './018_templates.js' +export { up as up019, down as down019 } from './019_deployments.js' +export { up as up020, down as down020 } from './020_blockchain_networks.js' +export { up as up021, down as down021 } from './021_workflows.js' +export { up as up022, down as down022 } from './022_pop_mappings_and_federation.js' +export { up as up023, down as down023 } from './023_industry_controls_and_waf.js' +export { up as up024, down as down024 } from './024_compliance_audit.js' +export { up as up025, down as down025 } from './025_sovereign_stack_marketplace.js' +export { up as up026, down as down026 } from './026_api_keys.js' diff --git a/api/src/db/seed.ts b/api/src/db/seed.ts index aec62b3..70cc2a2 100644 --- a/api/src/db/seed.ts +++ b/api/src/db/seed.ts @@ -1,5 +1,6 @@ import 'dotenv/config' import { getDb } from './index.js' +import { logger } from '../lib/logger.js' import bcrypt from 'bcryptjs' async function seed() { diff --git a/api/src/db/seeds/sovereign_stack_services.ts b/api/src/db/seeds/sovereign_stack_services.ts new file mode 100644 index 0000000..e637e69 --- /dev/null +++ b/api/src/db/seeds/sovereign_stack_services.ts @@ -0,0 +1,625 @@ +import 'dotenv/config' +import { getDb } from '../index.js' +import { logger } from '../../lib/logger.js' + +interface ServiceDefinition { + name: string + slug: string + category: string + description: string + shortDescription: string + tags: string[] + featured: boolean + iconUrl?: string + documentationUrl?: string + supportUrl?: string + metadata: Record + pricingType: string + pricingConfig: { + basePrice?: number + currency?: string + billingPeriod?: string + usageRates?: Record + freeTier?: { + requestsPerMonth?: number + features?: string[] + } + } +} + +const services: ServiceDefinition[] = [ + { + name: 'Phoenix Ledger Service', + slug: 'phoenix-ledger-service', + category: 'LEDGER_SERVICES', + description: `A sovereign-grade double-entry ledger system with virtual accounts, holds, and multi-asset support. + Replaces reliance on external platforms (e.g., Tatum Virtual Accounts) with owned core primitives. + Features include journal entries, subaccounts, holds/reserves, reconciliation, and full audit trail. + Every transaction is a balanced journal entry with idempotency via correlation_id. + Supports multi-asset operations (fiat, stablecoins, tokens) with state machine-based settlement.`, + shortDescription: 'Double-entry ledger with virtual accounts, holds, and multi-asset support', + tags: ['ledger', 'double-entry', 'virtual-accounts', 'financial', 'sovereign', 'audit'], + featured: true, + iconUrl: 'https://cdn.sankofa.nexus/services/ledger.svg', + documentationUrl: 'https://docs.sankofa.nexus/services/ledger', + supportUrl: 'https://support.sankofa.nexus/ledger', + metadata: { + apiEndpoints: [ + 'POST /ledger/entries', + 'POST /ledger/holds', + 'POST /ledger/transfers', + 'GET /ledger/balances' + ], + features: [ + 'Double-entry accounting', + 'Virtual account abstraction', + 'Holds and reserves', + 'Multi-asset support', + 'Reconciliation engine', + 'Immutable audit trail', + 'Idempotent operations', + 'State machine settlement' + ], + compliance: ['SOC 2', 'PCI DSS', 'GDPR'], + providerAdapters: [], + sla: { + uptime: '99.9%', + latency: '<100ms p95' + }, + architecture: 'docs/marketplace/sovereign-stack/ledger-service.md' + }, + pricingType: 'USAGE_BASED', + pricingConfig: { + currency: 'USD', + usageRates: { + journalEntry: 0.001, + holdOperation: 0.0005, + transfer: 0.002 + }, + freeTier: { + requestsPerMonth: 10000, + features: ['Basic ledger operations', 'Up to 100 virtual accounts'] + } + } + }, + { + name: 'Phoenix Identity Service', + slug: 'phoenix-identity-service', + category: 'IDENTITY_SERVICES', + description: `Comprehensive identity, authentication, and authorization service with support for users, organizations, + roles, and permissions. Features device binding, passkeys support, OAuth/OpenID Connect for integrations, + session management, and risk scoring. Centralizes identity management with no provider dependencies. + Supports multi-tenant identity with fine-grained RBAC and sovereign identity principles.`, + shortDescription: 'Users, orgs, roles, permissions, device binding, passkeys, OAuth/OIDC', + tags: ['identity', 'auth', 'rbac', 'oauth', 'oidc', 'passkeys', 'sovereign'], + featured: true, + iconUrl: 'https://cdn.sankofa.nexus/services/identity.svg', + documentationUrl: 'https://docs.sankofa.nexus/services/identity', + supportUrl: 'https://support.sankofa.nexus/identity', + metadata: { + apiEndpoints: [ + 'POST /identity/users', + 'POST /identity/orgs', + 'GET /identity/sessions', + 'POST /identity/auth/token' + ], + features: [ + 'Multi-tenant identity', + 'RBAC with fine-grained permissions', + 'Device binding', + 'Passkeys support', + 'OAuth 2.0 / OIDC', + 'Session management', + 'Risk scoring', + 'SCIM support' + ], + compliance: ['SOC 2', 'GDPR', 'HIPAA'], + providerAdapters: [], + sla: { + uptime: '99.95%', + latency: '<50ms p95' + }, + architecture: 'docs/marketplace/sovereign-stack/identity-service.md' + }, + pricingType: 'SUBSCRIPTION', + pricingConfig: { + basePrice: 99, + currency: 'USD', + billingPeriod: 'MONTHLY', + usageRates: { + perUser: 2.50, + perOrg: 50.00 + } + } + }, + { + name: 'Phoenix Wallet Registry', + slug: 'phoenix-wallet-registry', + category: 'WALLET_SERVICES', + description: `Wallet mapping and signing policy service with chain support matrix and policy engine. + Manages wallet mapping (user/org ↔ wallet addresses), chain support, policy engine for signing limits and approvals, + and recovery policies. Supports MPC (preferred for production custody), HSM-backed keys for service wallets, + and passkeys + account abstraction for end-users. Features transaction simulation and ERC-4337 smart accounts.`, + shortDescription: 'Wallet mapping, chain support, policy engine, recovery, MPC, HSM', + tags: ['wallet', 'blockchain', 'mpc', 'hsm', 'erc4337', 'custody', 'signing'], + featured: true, + iconUrl: 'https://cdn.sankofa.nexus/services/wallet.svg', + documentationUrl: 'https://docs.sankofa.nexus/services/wallet', + supportUrl: 'https://support.sankofa.nexus/wallet', + metadata: { + apiEndpoints: [ + 'POST /wallets/register', + 'POST /wallets/tx/build', + 'POST /wallets/tx/simulate', + 'POST /wallets/tx/submit' + ], + features: [ + 'Wallet mapping and registry', + 'Multi-chain support', + 'MPC custody', + 'HSM-backed keys', + 'Transaction simulation', + 'ERC-4337 smart accounts', + 'Policy engine', + 'Recovery policies' + ], + compliance: ['SOC 2', 'ISO 27001'], + providerAdapters: ['Thirdweb (optional)'], + sla: { + uptime: '99.9%', + latency: '<200ms p95' + }, + architecture: 'docs/marketplace/sovereign-stack/wallet-registry.md' + }, + pricingType: 'HYBRID', + pricingConfig: { + basePrice: 199, + currency: 'USD', + billingPeriod: 'MONTHLY', + usageRates: { + perWallet: 5.00, + perTransaction: 0.01 + } + } + }, + { + name: 'Phoenix Transaction Orchestrator', + slug: 'phoenix-tx-orchestrator', + category: 'ORCHESTRATION_SERVICES', + description: `On-chain and off-chain workflow orchestration service with retries, compensations, + provider routing, and fallback. Implements state machines for workflow management, enforces idempotency + and exactly-once semantics (logical), and provides provider routing with automatic failover. + Supports both on-chain blockchain transactions and off-chain operations with unified orchestration.`, + shortDescription: 'On-chain/off-chain workflow orchestration with retries and compensations', + tags: ['orchestration', 'workflow', 'blockchain', 'state-machine', 'idempotency', 'transactions'], + featured: true, + iconUrl: 'https://cdn.sankofa.nexus/services/tx-orchestrator.svg', + documentationUrl: 'https://docs.sankofa.nexus/services/tx-orchestrator', + supportUrl: 'https://support.sankofa.nexus/tx-orchestrator', + metadata: { + apiEndpoints: [ + 'POST /orchestrator/workflows', + 'GET /orchestrator/workflows/{id}', + 'POST /orchestrator/workflows/{id}/retry' + ], + features: [ + 'Workflow state machines', + 'Retries and compensations', + 'Provider routing and fallback', + 'Idempotency enforcement', + 'Exactly-once semantics', + 'On-chain and off-chain support', + 'Correlation ID tracking' + ], + compliance: ['SOC 2'], + providerAdapters: ['Alchemy', 'Infura', 'Self-hosted nodes'], + sla: { + uptime: '99.9%', + latency: '<500ms p95' + }, + architecture: 'docs/marketplace/sovereign-stack/tx-orchestrator.md' + }, + pricingType: 'USAGE_BASED', + pricingConfig: { + currency: 'USD', + usageRates: { + perTransaction: 0.05, + perWorkflow: 0.10 + }, + freeTier: { + requestsPerMonth: 1000, + features: ['Basic orchestration', 'Up to 10 concurrent workflows'] + } + } + }, + { + name: 'Phoenix Messaging Orchestrator', + slug: 'phoenix-messaging-orchestrator', + category: 'ORCHESTRATION_SERVICES', + description: `Multi-provider messaging orchestration service with failover for SMS, voice, email, and push notifications. + Features provider selection rules based on cost, deliverability, region, and user preference. + Includes delivery receipts, retries, suppression lists, and compliance features. + Replaces reliance on Twilio with owned core primitives while retaining optional provider integrations via adapters.`, + shortDescription: 'Multi-provider messaging (SMS/voice/email/push) with failover', + tags: ['messaging', 'sms', 'email', 'push', 'notifications', 'orchestration', 'failover'], + featured: true, + iconUrl: 'https://cdn.sankofa.nexus/services/messaging.svg', + documentationUrl: 'https://docs.sankofa.nexus/services/messaging', + supportUrl: 'https://support.sankofa.nexus/messaging', + metadata: { + apiEndpoints: [ + 'POST /messages/send', + 'GET /messages/status/{id}', + 'GET /messages/delivery/{id}' + ], + features: [ + 'Multi-provider routing', + 'Automatic failover', + 'Delivery receipts', + 'Retry logic', + 'Suppression lists', + 'Template management', + 'Compliance features', + 'Cost optimization' + ], + compliance: ['SOC 2', 'GDPR', 'TCPA'], + providerAdapters: ['Twilio', 'AWS SNS', 'Vonage', 'MessageBird'], + sla: { + uptime: '99.9%', + latency: '<200ms p95' + }, + architecture: 'docs/marketplace/sovereign-stack/messaging-orchestrator.md' + }, + pricingType: 'USAGE_BASED', + pricingConfig: { + currency: 'USD', + usageRates: { + perSMS: 0.01, + perEmail: 0.001, + perPush: 0.0005, + perVoice: 0.02 + }, + freeTier: { + requestsPerMonth: 1000, + features: ['Basic messaging', 'Single provider'] + } + } + }, + { + name: 'Phoenix Voice Orchestrator', + slug: 'phoenix-voice-orchestrator', + category: 'ORCHESTRATION_SERVICES', + description: `Text-to-speech and speech-to-text orchestration service with audio caching, + multi-provider routing, and moderation. Features deterministic caching (hash-based) for cost and latency optimization, + PII scrubbing, multi-model routing for high quality vs low-latency scenarios, and OSS fallback path for baseline TTS. + Replaces reliance on ElevenLabs with owned core primitives while retaining optional provider integrations.`, + shortDescription: 'TTS/STT with caching, multi-provider routing, moderation', + tags: ['voice', 'tts', 'stt', 'audio', 'media', 'orchestration', 'ai'], + featured: true, + iconUrl: 'https://cdn.sankofa.nexus/services/voice.svg', + documentationUrl: 'https://docs.sankofa.nexus/services/voice', + supportUrl: 'https://support.sankofa.nexus/voice', + metadata: { + apiEndpoints: [ + 'POST /voice/synthesize', + 'GET /voice/audio/{hash}', + 'POST /voice/transcribe' + ], + features: [ + 'Audio caching', + 'Multi-provider routing', + 'PII scrubbing', + 'Moderation', + 'Multi-model support', + 'OSS fallback', + 'CDN delivery' + ], + compliance: ['SOC 2', 'GDPR'], + providerAdapters: ['ElevenLabs', 'OpenAI', 'Azure TTS', 'OSS TTS'], + sla: { + uptime: '99.9%', + latency: '<500ms p95' + }, + architecture: 'docs/marketplace/sovereign-stack/voice-orchestrator.md' + }, + pricingType: 'USAGE_BASED', + pricingConfig: { + currency: 'USD', + usageRates: { + perSynthesis: 0.02, + perMinute: 0.10, + perTranscription: 0.05 + }, + freeTier: { + requestsPerMonth: 100, + features: ['Basic TTS/STT', 'Standard quality'] + } + } + }, + { + name: 'Phoenix Event Bus', + slug: 'phoenix-event-bus', + category: 'PLATFORM_SERVICES', + description: `Durable event bus service with replay, versioning, and consumer idempotency. + Implements DB Outbox pattern for atomic state + event writes. Supports Kafka, Redpanda, and NATS backends. + Features event versioning, consumer offset tracking, and processed correlation ID tracking for exactly-once delivery.`, + shortDescription: 'Durable events, replay, versioning, consumer idempotency', + tags: ['events', 'messaging', 'kafka', 'outbox', 'event-sourcing', 'platform'], + featured: false, + iconUrl: 'https://cdn.sankofa.nexus/services/event-bus.svg', + documentationUrl: 'https://docs.sankofa.nexus/services/event-bus', + supportUrl: 'https://support.sankofa.nexus/event-bus', + metadata: { + apiEndpoints: [ + 'POST /events/publish', + 'GET /events/consume', + 'POST /events/replay' + ], + features: [ + 'DB Outbox pattern', + 'Event versioning', + 'Consumer idempotency', + 'Replay support', + 'Multiple backends (Kafka/Redpanda/NATS)', + 'Offset tracking', + 'Correlation ID support' + ], + compliance: ['SOC 2'], + providerAdapters: ['Kafka', 'Redpanda', 'NATS'], + sla: { + uptime: '99.95%', + latency: '<100ms p95' + }, + architecture: 'docs/marketplace/sovereign-stack/event-bus.md' + }, + pricingType: 'SUBSCRIPTION', + pricingConfig: { + basePrice: 149, + currency: 'USD', + billingPeriod: 'MONTHLY', + usageRates: { + perGBStorage: 0.10, + perMillionEvents: 5.00 + } + } + }, + { + name: 'Phoenix Audit Service', + slug: 'phoenix-audit-service', + category: 'PLATFORM_SERVICES', + description: `Immutable audit logging service with WORM (Write Once Read Many) archive for compliance. + Features immutable audit logs with who-did-what-when tracking, PII boundaries and retention policies, + and separate operational DB from analytics store. Uses CDC to stream into warehouse for compliance reporting.`, + shortDescription: 'Immutable audit logs, WORM archive, PII boundaries, compliance', + tags: ['audit', 'logging', 'compliance', 'worm', 'immutable', 'platform'], + featured: false, + iconUrl: 'https://cdn.sankofa.nexus/services/audit.svg', + documentationUrl: 'https://docs.sankofa.nexus/services/audit', + supportUrl: 'https://support.sankofa.nexus/audit', + metadata: { + apiEndpoints: [ + 'POST /audit/log', + 'GET /audit/query', + 'GET /audit/export' + ], + features: [ + 'Immutable logs', + 'WORM archive', + 'PII boundaries', + 'Retention policies', + 'Compliance reporting', + 'Access trails', + 'CDC to warehouse' + ], + compliance: ['SOC 2', 'GDPR', 'HIPAA', 'PCI DSS'], + providerAdapters: [], + sla: { + uptime: '99.9%', + latency: '<50ms p95' + }, + architecture: 'docs/marketplace/sovereign-stack/audit-service.md' + }, + pricingType: 'USAGE_BASED', + pricingConfig: { + currency: 'USD', + usageRates: { + perGBStorage: 0.15, + perMillionLogs: 10.00 + }, + freeTier: { + requestsPerMonth: 100000, + features: ['Basic audit logging', '30-day retention'] + } + } + }, + { + name: 'Phoenix Observability Stack', + slug: 'phoenix-observability', + category: 'PLATFORM_SERVICES', + description: `Comprehensive observability service with distributed tracing, structured logs with correlation IDs, + and SLO monitoring. Features OpenTelemetry integration, distributed tracing across services, + SLOs for ledger posting, message delivery, and transaction settlement. Provides structured logging + with correlation IDs for end-to-end request tracking.`, + shortDescription: 'Distributed tracing, structured logs, SLOs, correlation IDs', + tags: ['observability', 'monitoring', 'tracing', 'logging', 'slo', 'opentelemetry', 'platform'], + featured: false, + iconUrl: 'https://cdn.sankofa.nexus/services/observability.svg', + documentationUrl: 'https://docs.sankofa.nexus/services/observability', + supportUrl: 'https://support.sankofa.nexus/observability', + metadata: { + apiEndpoints: [ + 'GET /observability/traces', + 'GET /observability/metrics', + 'GET /observability/logs', + 'GET /observability/slos' + ], + features: [ + 'Distributed tracing', + 'OpenTelemetry integration', + 'Structured logging', + 'Correlation IDs', + 'SLO monitoring', + 'Metrics collection', + 'Alerting' + ], + compliance: ['SOC 2'], + providerAdapters: [], + sla: { + uptime: '99.9%', + latency: '<100ms p95' + }, + architecture: 'docs/marketplace/sovereign-stack/observability.md' + }, + pricingType: 'USAGE_BASED', + pricingConfig: { + currency: 'USD', + usageRates: { + perMetric: 0.0001, + perLog: 0.00005, + perTrace: 0.001 + }, + freeTier: { + requestsPerMonth: 1000000, + features: ['Basic observability', '7-day retention'] + } + } + } +] + +async function seedSovereignStackServices() { + const db = getDb() + + try { + logger.info('Seeding Sovereign Stack services...') + + // Get or create Phoenix publisher + const publisherResult = await db.query( + `SELECT id FROM publishers WHERE name = 'phoenix-cloud-services'` + ) + + if (publisherResult.rows.length === 0) { + throw new Error('Phoenix publisher not found. Please run migration 025 first.') + } + + const publisherId = publisherResult.rows[0].id + logger.info(`✓ Found Phoenix publisher: ${publisherId}`) + + // Seed each service + for (const service of services) { + // Create product + const productResult = await db.query( + `INSERT INTO products ( + name, slug, category, description, short_description, publisher_id, + status, featured, icon_url, documentation_url, support_url, metadata, tags + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) + ON CONFLICT (slug) DO UPDATE SET + name = EXCLUDED.name, + description = EXCLUDED.description, + short_description = EXCLUDED.description, + category = EXCLUDED.category, + featured = EXCLUDED.featured, + icon_url = EXCLUDED.icon_url, + documentation_url = EXCLUDED.documentation_url, + support_url = EXCLUDED.support_url, + metadata = EXCLUDED.metadata, + tags = EXCLUDED.tags, + updated_at = NOW() + RETURNING id`, + [ + service.name, + service.slug, + service.category, + service.description, + service.shortDescription, + publisherId, + 'PUBLISHED', + service.featured, + service.iconUrl || null, + service.documentationUrl || null, + service.supportUrl || null, + JSON.stringify(service.metadata), + service.tags + ] + ) + + const productId = productResult.rows[0].id + logger.info(`✓ Created/updated product: ${service.name} (${productId})`) + + // Create product version (v1.0.0) + const versionResult = await db.query( + `INSERT INTO product_versions ( + product_id, version, status, is_latest, released_at, metadata + ) VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT (product_id, version) DO UPDATE SET + status = EXCLUDED.status, + is_latest = EXCLUDED.is_latest, + released_at = EXCLUDED.released_at, + updated_at = NOW() + RETURNING id`, + [ + productId, + '1.0.0', + 'PUBLISHED', + true, + new Date(), + JSON.stringify({ initialRelease: true }) + ] + ) + + const versionId = versionResult.rows[0].id + + // Unmark other versions as latest + await db.query( + `UPDATE product_versions + SET is_latest = FALSE + WHERE product_id = $1 AND id != $2`, + [productId, versionId] + ) + + // Create pricing model + await db.query( + `INSERT INTO pricing_models ( + product_id, product_version_id, pricing_type, base_price, currency, + billing_period, usage_rates, metadata + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + ON CONFLICT DO NOTHING`, + [ + productId, + versionId, + service.pricingType, + service.pricingConfig.basePrice || null, + service.pricingConfig.currency || 'USD', + service.pricingConfig.billingPeriod || null, + JSON.stringify(service.pricingConfig.usageRates || {}), + JSON.stringify({ + freeTier: service.pricingConfig.freeTier || null, + ...service.pricingConfig + }) + ] + ) + + logger.info(`✓ Created pricing model for: ${service.name}`) + } + + logger.info(`✓ Successfully seeded ${services.length} Sovereign Stack services!`) + } catch (error) { + logger.error('Seeding error', { error }) + throw error + } finally { + await db.end() + } +} + +// Run if called directly (check if this is the main module) +const isMainModule = import.meta.url === `file://${process.argv[1]}` || + process.argv[1]?.includes('sovereign_stack_services') || + process.argv[1]?.endsWith('sovereign_stack_services.ts') + +if (isMainModule) { + seedSovereignStackServices().catch((error) => { + logger.error('Failed to seed Sovereign Stack services', { error }) + process.exit(1) + }) +} + +export { seedSovereignStackServices, services } diff --git a/api/src/lib/secret-validation.ts b/api/src/lib/secret-validation.ts index ac943b4..300ccbd 100644 --- a/api/src/lib/secret-validation.ts +++ b/api/src/lib/secret-validation.ts @@ -213,15 +213,51 @@ export function requireJWTSecret(): string { /** * Validates database password specifically + * Relaxed requirements for development mode */ export function requireDatabasePassword(): string { - return requireProductionSecret( - process.env.DB_PASSWORD, - 'DB_PASSWORD', - { - minLength: 32, + const isProduction = process.env.NODE_ENV === 'production' || + process.env.ENVIRONMENT === 'production' || + process.env.PRODUCTION === 'true' + + if (isProduction) { + return requireProductionSecret( + process.env.DB_PASSWORD, + 'DB_PASSWORD', + { + minLength: 32, + } + ) + } else { + // Development mode: relaxed requirements + // Still validate but allow shorter passwords for local development + const password = process.env.DB_PASSWORD + if (!password) { + throw new SecretValidationError( + 'DB_PASSWORD is required but not provided. Please set it in your .env file.', + 'MISSING_SECRET', + { minLength: 8, requireUppercase: false, requireLowercase: false, requireNumbers: false, requireSpecialChars: false } + ) } - ) + + // Basic validation for dev (just check it's not empty and not insecure) + if (password.length < 8) { + throw new SecretValidationError( + 'DB_PASSWORD must be at least 8 characters long for development', + 'INSUFFICIENT_LENGTH', + { minLength: 8 } + ) + } + + if (INSECURE_SECRETS.includes(password.toLowerCase().trim())) { + throw new SecretValidationError( + 'DB_PASSWORD uses an insecure default value', + 'INSECURE_DEFAULT' + ) + } + + return password + } } /** diff --git a/api/src/middleware/tenant-auth.ts b/api/src/middleware/tenant-auth.ts index d4c3ce6..0efb09f 100644 --- a/api/src/middleware/tenant-auth.ts +++ b/api/src/middleware/tenant-auth.ts @@ -26,13 +26,44 @@ declare module 'fastify' { } /** - * Extract tenant context from request + * Resolve tenant context from X-API-Key (for /api/v1/* client and partner API access). + * Uses api_keys table: key hash, tenant_id, permissions. + */ +async function extractTenantContextFromApiKey( + request: FastifyRequest +): Promise { + const apiKey = (request.headers['x-api-key'] as string) || (request.headers['X-API-Key'] as string) + if (!apiKey?.trim()) return null + const { verifyApiKey } = await import('../services/api-key.js') + const result = await verifyApiKey(apiKey.trim()) + if (!result) return null + return { + tenantId: result.tenantId ?? undefined, + userId: result.userId, + email: '', + role: 'API_KEY', + permissions: { scopes: result.permissions }, + isSystemAdmin: false, + } +} + +/** + * Extract tenant context from request (JWT or X-API-Key for /api/v1/*) */ export async function extractTenantContext( request: FastifyRequest ): Promise { - // Get token from Authorization header const authHeader = request.headers.authorization + const isRailingPath = typeof request.url === 'string' && request.url.startsWith('/api/v1') + + // For /api/v1/*, allow X-API-Key when no Bearer token + if (isRailingPath && (!authHeader || !authHeader.startsWith('Bearer '))) { + const apiKeyContext = await extractTenantContextFromApiKey(request) + if (apiKeyContext) return apiKeyContext + return null + } + + // JWT path if (!authHeader || !authHeader.startsWith('Bearer ')) { return null } diff --git a/api/src/routes/phoenix-railing.ts b/api/src/routes/phoenix-railing.ts new file mode 100644 index 0000000..f89c0b0 --- /dev/null +++ b/api/src/routes/phoenix-railing.ts @@ -0,0 +1,98 @@ +/** + * Phoenix API Railing — REST routes for Infra/VE/Health proxy and tenant-scoped Client API. + * When PHOENIX_RAILING_URL is set, /api/v1/infra/*, /api/v1/ve/*, /api/v1/health/* proxy to it. + * /api/v1/tenants/me/* are tenant-scoped (from tenantContext). + */ + +import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify' + +const RAILING_URL = (process.env.PHOENIX_RAILING_URL || '').replace(/\/$/, '') +const RAILING_API_KEY = process.env.PHOENIX_RAILING_API_KEY || '' + +async function proxyToRailing( + request: FastifyRequest<{ Params: Record; Querystring: Record }>, + reply: FastifyReply, + path: string +) { + if (!RAILING_URL) { + return reply.status(503).send({ + error: 'Phoenix railing not configured', + message: 'Set PHOENIX_RAILING_URL to the Phoenix Deploy API or Phoenix API base URL', + }) + } + const qs = new URLSearchParams(request.query as Record).toString() + const url = `${RAILING_URL}${path}${qs ? `?${qs}` : ''}` + const headers: Record = { + 'Content-Type': 'application/json', + ...(request.headers['content-type'] && { 'Content-Type': request.headers['content-type'] }), + } + if (RAILING_API_KEY) headers['Authorization'] = `Bearer ${RAILING_API_KEY}` + else if (request.headers.authorization) headers['Authorization'] = request.headers.authorization + try { + const res = await fetch(url, { + method: request.method, + headers, + body: request.method !== 'GET' && request.body ? JSON.stringify(request.body) : undefined, + }) + const data = await res.json().catch(() => ({})) + return reply.status(res.status).send(data) + } catch (err: any) { + return reply.status(502).send({ error: err?.message || 'Railing proxy failed' }) + } +} + +export async function registerPhoenixRailingRoutes(fastify: FastifyInstance) { + if (RAILING_URL) { + fastify.get('/api/v1/infra/nodes', async (request, reply) => proxyToRailing(request, reply, '/api/v1/infra/nodes')) + fastify.get('/api/v1/infra/storage', async (request, reply) => proxyToRailing(request, reply, '/api/v1/infra/storage')) + fastify.get('/api/v1/ve/vms', async (request, reply) => proxyToRailing(request, reply, '/api/v1/ve/vms')) + fastify.get('/api/v1/ve/vms/:node/:vmid/status', async (request, reply) => { + const { node, vmid } = (request as any).params + return proxyToRailing(request, reply, `/api/v1/ve/vms/${node}/${vmid}/status`) + }) + fastify.get('/api/v1/health/metrics', async (request, reply) => proxyToRailing(request, reply, '/api/v1/health/metrics')) + fastify.get('/api/v1/health/alerts', async (request, reply) => proxyToRailing(request, reply, '/api/v1/health/alerts')) + fastify.get('/api/v1/health/summary', async (request, reply) => proxyToRailing(request, reply, '/api/v1/health/summary')) + fastify.post('/api/v1/ve/vms/:node/:vmid/start', async (request, reply) => { + const { node, vmid } = (request as any).params + return proxyToRailing(request, reply, `/api/v1/ve/vms/${node}/${vmid}/start`) + }) + fastify.post('/api/v1/ve/vms/:node/:vmid/stop', async (request, reply) => { + const { node, vmid } = (request as any).params + return proxyToRailing(request, reply, `/api/v1/ve/vms/${node}/${vmid}/stop`) + }) + fastify.post('/api/v1/ve/vms/:node/:vmid/reboot', async (request, reply) => { + const { node, vmid } = (request as any).params + return proxyToRailing(request, reply, `/api/v1/ve/vms/${node}/${vmid}/reboot`) + }) + } + + fastify.get('/api/v1/tenants/me/resources', async (request, reply) => { + const tenantContext = (request as any).tenantContext + if (!tenantContext?.tenantId) { + return reply.status(401).send({ error: 'Tenant context required', message: 'Use API key or JWT with tenant scope' }) + } + const db = (await import('../db/index.js')).getDb() + const result = await db.query( + 'SELECT id, name, resource_type, provider, provider_id, site_id, metadata, created_at FROM resource_inventory WHERE tenant_id = $1 ORDER BY created_at DESC', + [tenantContext.tenantId] + ) + return reply.send({ resources: result.rows, tenantId: tenantContext.tenantId }) + }) + + fastify.get('/api/v1/tenants/me/health', async (request, reply) => { + const tenantContext = (request as any).tenantContext + if (!tenantContext?.tenantId) { + return reply.status(401).send({ error: 'Tenant context required' }) + } + if (RAILING_URL) { + return proxyToRailing(request, reply, '/api/v1/health/summary') + } + return reply.send({ + tenantId: tenantContext.tenantId, + status: 'unknown', + updated_at: new Date().toISOString(), + message: 'Set PHOENIX_RAILING_URL for full health summary', + }) + }) +} diff --git a/api/src/schema/typeDefs.ts b/api/src/schema/typeDefs.ts index 5c400a6..4cc7d33 100644 --- a/api/src/schema/typeDefs.ts +++ b/api/src/schema/typeDefs.ts @@ -1282,6 +1282,11 @@ export const typeDefs = gql` FINANCIAL_MESSAGING INTERNET_REGISTRY AI_LLM_AGENT + LEDGER_SERVICES + IDENTITY_SERVICES + WALLET_SERVICES + ORCHESTRATION_SERVICES + PLATFORM_SERVICES } enum ProductStatus { diff --git a/api/src/server.ts b/api/src/server.ts index 5c5ed85..a30afbc 100644 --- a/api/src/server.ts +++ b/api/src/server.ts @@ -17,6 +17,8 @@ import { logger } from './lib/logger' import { validateAllSecrets } from './lib/secret-validation' import { initializeFIPS } from './lib/crypto' import { getFastifyTLSOptions } from './lib/tls-config' +import { registerPhoenixRailingRoutes } from './routes/phoenix-railing.js' +import { printSchema } from 'graphql' // Get TLS configuration (empty if certificates not available) const tlsOptions = getFastifyTLSOptions() @@ -111,6 +113,39 @@ async function startServer() { return { status: 'ok', timestamp: new Date().toISOString() } }) + // GraphQL schema export (SDL) for docs and codegen + fastify.get('/graphql/schema', async (_request, reply) => { + reply.type('text/plain').send(printSchema(schema)) + }) + + // GraphQL Playground (interactive docs) — redirect to Apollo Sandbox or show schema link + fastify.get('/graphql-playground', async (_request, reply) => { + const base = process.env.PUBLIC_URL || 'http://localhost:4000' + reply.type('text/html').send(` + + + + + Phoenix API — GraphQL + + + +

Phoenix API — GraphQL

+

Endpoint: ${base}/graphql

+

Schema (SDL)

+

Use Apollo Sandbox or any GraphQL client with the endpoint above.

+ + + `) + }) + + // Phoenix API Railing: /api/v1/infra/*, /api/v1/ve/*, /api/v1/health/* proxy + /api/v1/tenants/me/* + await registerPhoenixRailingRoutes(fastify) + // Start Fastify server const port = parseInt(process.env.PORT || '4000', 10) const host = process.env.HOST || '0.0.0.0' diff --git a/api/src/services/sovereign-stack/audit-service.ts b/api/src/services/sovereign-stack/audit-service.ts new file mode 100644 index 0000000..198ab9e --- /dev/null +++ b/api/src/services/sovereign-stack/audit-service.ts @@ -0,0 +1,191 @@ +/** + * Phoenix Audit Service + * Immutable audit logs, WORM archive, PII boundaries, compliance + */ + +import { getDb } from '../../db/index.js' +import { logger } from '../../lib/logger.js' + +export interface AuditLog { + logId: string + userId: string | null + action: string + resourceType: string + resourceId: string + details: Record + timestamp: Date + ipAddress?: string + userAgent?: string +} + +export interface AuditQuery { + userId?: string + action?: string + resourceType?: string + resourceId?: string + startDate?: Date + endDate?: Date + limit?: number +} + +class AuditService { + /** + * Create immutable audit log + */ + async log( + action: string, + resourceType: string, + resourceId: string, + details: Record, + userId?: string, + ipAddress?: string, + userAgent?: string + ): Promise { + const db = getDb() + + // Scrub PII from details + const scrubbedDetails = this.scrubPII(details) + + const result = await db.query( + `INSERT INTO audit_logs ( + user_id, action, resource_type, resource_id, details, ip_address, user_agent, timestamp + ) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW()) + RETURNING *`, + [ + userId || null, + action, + resourceType, + resourceId, + JSON.stringify(scrubbedDetails), + ipAddress || null, + userAgent || null + ] + ) + + logger.info('Audit log created', { logId: result.rows[0].id, action }) + + // Archive to WORM storage if needed + await this.archiveToWORM(result.rows[0]) + + return this.mapAuditLog(result.rows[0]) + } + + /** + * Query audit logs + */ + async query(query: AuditQuery): Promise { + const db = getDb() + + const conditions: string[] = [] + const params: any[] = [] + let paramIndex = 1 + + if (query.userId) { + conditions.push(`user_id = $${paramIndex++}`) + params.push(query.userId) + } + + if (query.action) { + conditions.push(`action = $${paramIndex++}`) + params.push(query.action) + } + + if (query.resourceType) { + conditions.push(`resource_type = $${paramIndex++}`) + params.push(query.resourceType) + } + + if (query.resourceId) { + conditions.push(`resource_id = $${paramIndex++}`) + params.push(query.resourceId) + } + + if (query.startDate) { + conditions.push(`timestamp >= $${paramIndex++}`) + params.push(query.startDate) + } + + if (query.endDate) { + conditions.push(`timestamp <= $${paramIndex++}`) + params.push(query.endDate) + } + + const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '' + const limit = query.limit || 1000 + + params.push(limit) + const result = await db.query( + `SELECT * FROM audit_logs + ${whereClause} + ORDER BY timestamp DESC + LIMIT $${paramIndex}`, + params + ) + + return result.rows.map(this.mapAuditLog) + } + + /** + * Export audit logs for compliance + */ + async exportForCompliance( + startDate: Date, + endDate: Date, + format: 'JSON' | 'CSV' = 'JSON' + ): Promise { + const logs = await this.query({ startDate, endDate, limit: 1000000 }) + + if (format === 'JSON') { + return JSON.stringify(logs, null, 2) + } else { + // CSV format + const headers = ['logId', 'userId', 'action', 'resourceType', 'resourceId', 'timestamp'] + const rows = logs.map(log => [ + log.logId, + log.userId || '', + log.action, + log.resourceType, + log.resourceId, + log.timestamp.toISOString() + ]) + + return [headers.join(','), ...rows.map(row => row.join(','))].join('\n') + } + } + + private scrubPII(data: Record): Record { + // Placeholder - would implement actual PII scrubbing + // Remove SSNs, credit cards, etc. based on PII boundaries + const scrubbed = { ...data } + + // Example: remove credit card numbers + if (scrubbed.cardNumber) { + scrubbed.cardNumber = '***REDACTED***' + } + + return scrubbed + } + + private async archiveToWORM(log: any): Promise { + // Archive to WORM (Write Once Read Many) storage for compliance + // This would write to immutable storage (S3 with object lock, etc.) + logger.info('Archiving to WORM storage', { logId: log.id }) + // Placeholder - would implement actual WORM archiving + } + + private mapAuditLog(row: any): AuditLog { + return { + logId: row.id, + userId: row.user_id, + action: row.action, + resourceType: row.resource_type, + resourceId: row.resource_id, + details: typeof row.details === 'string' ? JSON.parse(row.details) : row.details, + timestamp: row.timestamp, + ipAddress: row.ip_address, + userAgent: row.user_agent + } + } +} + +export const auditService = new AuditService() diff --git a/api/src/services/sovereign-stack/event-bus-service.ts b/api/src/services/sovereign-stack/event-bus-service.ts new file mode 100644 index 0000000..a396d4d --- /dev/null +++ b/api/src/services/sovereign-stack/event-bus-service.ts @@ -0,0 +1,188 @@ +/** + * Phoenix Event Bus Service + * Durable events, replay, versioning, consumer idempotency + */ + +import { getDb } from '../../db/index.js' +import { logger } from '../../lib/logger.js' + +export interface Event { + eventId: string + eventType: string + aggregateId: string + version: number + payload: Record + metadata: Record + timestamp: Date + correlationId: string +} + +export interface ConsumerOffset { + consumerId: string + eventId: string + processedAt: Date +} + +class EventBusService { + /** + * Publish an event (via outbox pattern) + */ + async publishEvent( + eventType: string, + aggregateId: string, + payload: Record, + correlationId: string, + metadata: Record = {} + ): Promise { + const db = getDb() + + // Get next version for this aggregate + const versionResult = await db.query( + `SELECT COALESCE(MAX(version), 0) + 1 as next_version + FROM events + WHERE aggregate_id = $1 AND event_type = $2`, + [aggregateId, eventType] + ) + const version = parseInt(versionResult.rows[0].next_version) + + // Insert into outbox (atomic with business logic) + const result = await db.query( + `INSERT INTO event_outbox ( + event_type, aggregate_id, version, payload, metadata, correlation_id, status + ) VALUES ($1, $2, $3, $4, $5, $6, 'PENDING') + RETURNING *`, + [ + eventType, + aggregateId, + version, + JSON.stringify(payload), + JSON.stringify(metadata), + correlationId + ] + ) + + logger.info('Event published to outbox', { + eventId: result.rows[0].id, + eventType, + correlationId + }) + + // Process outbox (would be done by background worker) + await this.processOutbox() + + return this.mapEvent(result.rows[0]) + } + + /** + * Process outbox (typically run by background worker) + */ + async processOutbox(): Promise { + const db = getDb() + + // Get pending events + const pending = await db.query( + `SELECT * FROM event_outbox WHERE status = 'PENDING' ORDER BY created_at LIMIT 100` + ) + + for (const event of pending.rows) { + try { + // Publish to actual event bus (Kafka/Redpanda/NATS) + await this.publishToBus(event) + + // Mark as published + await db.query( + `UPDATE event_outbox SET status = 'PUBLISHED', published_at = NOW() WHERE id = $1`, + [event.id] + ) + + // Insert into events table + await db.query( + `INSERT INTO events ( + event_id, event_type, aggregate_id, version, payload, metadata, correlation_id, timestamp + ) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW()) + ON CONFLICT (event_id) DO NOTHING`, + [ + event.id, + event.event_type, + event.aggregate_id, + event.version, + event.payload, + event.metadata, + event.correlation_id + ] + ) + + logger.info('Event processed from outbox', { eventId: event.id }) + } catch (error) { + logger.error('Failed to process event from outbox', { eventId: event.id, error }) + // Would implement retry logic here + } + } + } + + /** + * Consume events with idempotency + */ + async consumeEvents( + consumerId: string, + eventType: string, + limit: number = 100 + ): Promise { + const db = getDb() + + // Get last processed event + const lastOffset = await db.query( + `SELECT event_id FROM consumer_offsets + WHERE consumer_id = $1 AND event_type = $2 + ORDER BY processed_at DESC LIMIT 1`, + [consumerId, eventType] + ) + + const lastEventId = lastOffset.rows[0]?.event_id || null + + // Get events after last processed + const query = lastEventId + ? `SELECT * FROM events + WHERE event_type = $1 AND id > $2 + ORDER BY timestamp ASC LIMIT $3` + : `SELECT * FROM events + WHERE event_type = $1 + ORDER BY timestamp ASC LIMIT $2` + + const params = lastEventId ? [eventType, lastEventId, limit] : [eventType, limit] + const result = await db.query(query, params) + + // Record offsets + for (const event of result.rows) { + await db.query( + `INSERT INTO consumer_offsets (consumer_id, event_id, event_type, processed_at) + VALUES ($1, $2, $3, NOW()) + ON CONFLICT (consumer_id, event_id) DO NOTHING`, + [consumerId, event.id, eventType] + ) + } + + return result.rows.map(this.mapEvent) + } + + private async publishToBus(event: any): Promise { + // This would publish to Kafka/Redpanda/NATS + logger.info('Publishing to event bus', { eventId: event.id }) + // Placeholder - would implement actual bus publishing + } + + private mapEvent(row: any): Event { + return { + eventId: row.id || row.event_id, + eventType: row.event_type, + aggregateId: row.aggregate_id, + version: row.version, + payload: typeof row.payload === 'string' ? JSON.parse(row.payload) : row.payload, + metadata: typeof row.metadata === 'string' ? JSON.parse(row.metadata) : row.metadata, + timestamp: row.timestamp || row.created_at, + correlationId: row.correlation_id + } + } +} + +export const eventBusService = new EventBusService() diff --git a/api/src/services/sovereign-stack/identity-service.ts b/api/src/services/sovereign-stack/identity-service.ts new file mode 100644 index 0000000..da36c08 --- /dev/null +++ b/api/src/services/sovereign-stack/identity-service.ts @@ -0,0 +1,182 @@ +/** + * Phoenix Identity Service (Sovereign Stack) + * Extends the base identity service with marketplace-specific features + * Users, orgs, roles, permissions, device binding, passkeys, OAuth/OIDC + */ + +import { getDb } from '../../db/index.js' +import { logger } from '../../lib/logger.js' +import { identityService } from '../identity.js' + +export interface User { + userId: string + email: string + name: string + roles: string[] + permissions: Record + orgId: string | null +} + +export interface Organization { + orgId: string + name: string + domain: string | null + status: 'ACTIVE' | 'SUSPENDED' +} + +export interface DeviceBinding { + deviceId: string + userId: string + deviceType: string + fingerprint: string + lastUsed: Date +} + +class SovereignIdentityService { + /** + * Create user + */ + async createUser( + email: string, + name: string, + orgId?: string + ): Promise { + const db = getDb() + + // Use base identity service for Keycloak integration + const keycloakUser = await identityService.createUser(email, name) + + // Store in local DB for marketplace features + const result = await db.query( + `INSERT INTO marketplace_users (user_id, email, name, org_id) + VALUES ($1, $2, $3, $4) + ON CONFLICT (user_id) DO UPDATE SET + email = EXCLUDED.email, + name = EXCLUDED.name, + org_id = EXCLUDED.org_id + RETURNING *`, + [keycloakUser.id, email, name, orgId || null] + ) + + return this.mapUser(result.rows[0]) + } + + /** + * Create organization + */ + async createOrganization( + name: string, + domain?: string + ): Promise { + const db = getDb() + + const result = await db.query( + `INSERT INTO organizations (name, domain, status) + VALUES ($1, $2, 'ACTIVE') + RETURNING *`, + [name, domain || null] + ) + + logger.info('Organization created', { orgId: result.rows[0].id }) + return this.mapOrganization(result.rows[0]) + } + + /** + * Bind device to user + */ + async bindDevice( + userId: string, + deviceType: string, + fingerprint: string + ): Promise { + const db = getDb() + + const result = await db.query( + `INSERT INTO device_bindings (user_id, device_type, fingerprint, last_used) + VALUES ($1, $2, $3, NOW()) + ON CONFLICT (user_id, fingerprint) DO UPDATE SET + last_used = NOW() + RETURNING *`, + [userId, deviceType, fingerprint] + ) + + logger.info('Device bound', { deviceId: result.rows[0].id, userId }) + return this.mapDeviceBinding(result.rows[0]) + } + + /** + * Get user with roles and permissions + */ + async getUser(userId: string): Promise { + const db = getDb() + + const result = await db.query( + `SELECT + u.*, + o.org_id, + ARRAY_AGG(DISTINCT r.role_name) as roles, + jsonb_object_agg(DISTINCT p.permission_key, p.permission_value) as permissions + FROM marketplace_users u + LEFT JOIN organizations o ON u.org_id = o.id + LEFT JOIN user_roles r ON u.user_id = r.user_id + LEFT JOIN user_permissions p ON u.user_id = p.user_id + WHERE u.user_id = $1 + GROUP BY u.user_id, o.org_id`, + [userId] + ) + + if (result.rows.length === 0) { + return null + } + + return this.mapUser(result.rows[0]) + } + + /** + * Assign role to user + */ + async assignRole(userId: string, roleName: string): Promise { + const db = getDb() + + await db.query( + `INSERT INTO user_roles (user_id, role_name) + VALUES ($1, $2) + ON CONFLICT (user_id, role_name) DO NOTHING`, + [userId, roleName] + ) + + logger.info('Role assigned', { userId, roleName }) + } + + private mapUser(row: any): User { + return { + userId: row.user_id, + email: row.email, + name: row.name, + roles: row.roles || [], + permissions: row.permissions || {}, + orgId: row.org_id + } + } + + private mapOrganization(row: any): Organization { + return { + orgId: row.id, + name: row.name, + domain: row.domain, + status: row.status + } + } + + private mapDeviceBinding(row: any): DeviceBinding { + return { + deviceId: row.id, + userId: row.user_id, + deviceType: row.device_type, + fingerprint: row.fingerprint, + lastUsed: row.last_used + } + } +} + +export const sovereignIdentityService = new SovereignIdentityService() diff --git a/api/src/services/sovereign-stack/ledger-service.ts b/api/src/services/sovereign-stack/ledger-service.ts new file mode 100644 index 0000000..a535635 --- /dev/null +++ b/api/src/services/sovereign-stack/ledger-service.ts @@ -0,0 +1,175 @@ +/** + * Phoenix Ledger Service + * Double-entry ledger with virtual accounts, holds, and multi-asset support + */ + +import { getDb } from '../../db/index.js' +import { logger } from '../../lib/logger.js' + +export interface JournalEntry { + entryId: string + timestamp: Date + description: string + correlationId: string + lines: JournalLine[] +} + +export interface JournalLine { + accountRef: string + debit: number + credit: number + asset: string +} + +export interface VirtualAccount { + subaccountId: string + accountId: string + currency: string + asset: string + labels: Record +} + +export interface Hold { + holdId: string + amount: number + asset: string + expiry: Date | null + status: 'ACTIVE' | 'RELEASED' | 'EXPIRED' +} + +export interface Balance { + accountId: string + subaccountId: string | null + asset: string + balance: number +} + +class LedgerService { + /** + * Create a journal entry (idempotent via correlation_id) + */ + async createJournalEntry( + correlationId: string, + description: string, + lines: JournalLine[] + ): Promise { + const db = getDb() + + // Check idempotency + const existing = await db.query( + `SELECT * FROM journal_entries WHERE correlation_id = $1`, + [correlationId] + ) + + if (existing.rows.length > 0) { + logger.info('Journal entry already exists', { correlationId }) + return this.mapJournalEntry(existing.rows[0]) + } + + // Validate double-entry balance + const totalDebits = lines.reduce((sum, line) => sum + line.debit, 0) + const totalCredits = lines.reduce((sum, line) => sum + line.credit, 0) + + if (Math.abs(totalDebits - totalCredits) > 0.01) { + throw new Error('Journal entry is not balanced') + } + + // Create entry + const result = await db.query( + `INSERT INTO journal_entries (correlation_id, description, timestamp) + VALUES ($1, $2, NOW()) + RETURNING *`, + [correlationId, description] + ) + + const entryId = result.rows[0].id + + // Create journal lines + for (const line of lines) { + await db.query( + `INSERT INTO journal_lines (entry_id, account_ref, debit, credit, asset) + VALUES ($1, $2, $3, $4, $5)`, + [entryId, line.accountRef, line.debit, line.credit, line.asset] + ) + } + + logger.info('Journal entry created', { entryId, correlationId }) + return this.mapJournalEntry(result.rows[0]) + } + + /** + * Create a hold (reserve) + */ + async createHold( + accountId: string, + amount: number, + asset: string, + expiry: Date | null = null + ): Promise { + const db = getDb() + + const result = await db.query( + `INSERT INTO holds (account_id, amount, asset, expiry, status) + VALUES ($1, $2, $3, $4, 'ACTIVE') + RETURNING *`, + [accountId, amount, asset, expiry] + ) + + logger.info('Hold created', { holdId: result.rows[0].id }) + return this.mapHold(result.rows[0]) + } + + /** + * Get balance for account/subaccount + */ + async getBalance(accountId: string, subaccountId?: string, asset?: string): Promise { + const db = getDb() + + // This would query a materialized view or compute from journal_lines + const query = ` + SELECT + account_ref as account_id, + asset, + SUM(debit - credit) as balance + FROM journal_lines + WHERE account_ref = $1 + ${subaccountId ? 'AND account_ref LIKE $2' : ''} + ${asset ? 'AND asset = $3' : ''} + GROUP BY account_ref, asset + ` + + const params: any[] = [accountId] + if (subaccountId) params.push(`${accountId}:${subaccountId}`) + if (asset) params.push(asset) + + const result = await db.query(query, params) + return result.rows.map(row => ({ + accountId: row.account_id, + subaccountId: subaccountId || null, + asset: row.asset, + balance: parseFloat(row.balance) + })) + } + + private mapJournalEntry(row: any): JournalEntry { + return { + entryId: row.id, + timestamp: row.timestamp, + description: row.description, + correlationId: row.correlation_id, + lines: [] // Would be loaded separately + } + } + + private mapHold(row: any): Hold { + return { + holdId: row.id, + amount: parseFloat(row.amount), + asset: row.asset, + expiry: row.expiry, + status: row.status + } + } +} + +export const ledgerService = new LedgerService() diff --git a/api/src/services/sovereign-stack/messaging-orchestrator-service.ts b/api/src/services/sovereign-stack/messaging-orchestrator-service.ts new file mode 100644 index 0000000..9c97cce --- /dev/null +++ b/api/src/services/sovereign-stack/messaging-orchestrator-service.ts @@ -0,0 +1,144 @@ +/** + * Phoenix Messaging Orchestrator Service + * Multi-provider messaging (SMS/voice/email/push) with failover + */ + +import { getDb } from '../../db/index.js' +import { logger } from '../../lib/logger.js' + +export interface MessageRequest { + channel: 'SMS' | 'EMAIL' | 'VOICE' | 'PUSH' + to: string + template: string + params: Record + priority: 'LOW' | 'NORMAL' | 'HIGH' +} + +export interface MessageStatus { + messageId: string + status: 'PENDING' | 'SENT' | 'DELIVERED' | 'FAILED' + provider: string + deliveryReceipt?: any + retryCount: number +} + +class MessagingOrchestratorService { + /** + * Send a message with provider routing and failover + */ + async sendMessage(request: MessageRequest): Promise { + const db = getDb() + + // Select provider based on rules (cost, deliverability, region, user preference) + const provider = await this.selectProvider(request) + + const result = await db.query( + `INSERT INTO messages (channel, recipient, template, params, priority, provider, status) + VALUES ($1, $2, $3, $4, $5, $6, 'PENDING') + RETURNING *`, + [ + request.channel, + request.to, + request.template, + JSON.stringify(request.params), + request.priority, + provider + ] + ) + + const messageId = result.rows[0].id + + try { + // Send via provider adapter + await this.sendViaProvider(provider, request) + + await db.query( + `UPDATE messages SET status = 'SENT' WHERE id = $1`, + [messageId] + ) + + logger.info('Message sent', { messageId, provider }) + return { + messageId, + status: 'SENT', + provider, + retryCount: 0 + } + } catch (error) { + // Try failover provider + const failoverProvider = await this.selectFailoverProvider(request, provider) + + if (failoverProvider) { + logger.info('Retrying with failover provider', { messageId, failoverProvider }) + return this.sendMessage({ ...request, priority: 'HIGH' }) + } + + await db.query( + `UPDATE messages SET status = 'FAILED' WHERE id = $1`, + [messageId] + ) + + throw error + } + } + + /** + * Get message status + */ + async getMessageStatus(messageId: string): Promise { + const db = getDb() + + const result = await db.query( + `SELECT * FROM messages WHERE id = $1`, + [messageId] + ) + + if (result.rows.length === 0) { + throw new Error('Message not found') + } + + const row = result.rows[0] + return { + messageId: row.id, + status: row.status, + provider: row.provider, + deliveryReceipt: row.delivery_receipt, + retryCount: row.retry_count || 0 + } + } + + private async selectProvider(request: MessageRequest): Promise { + // Provider selection logic based on cost, deliverability, region, user preference + // Placeholder - would implement actual routing rules + const providers: Record = { + SMS: ['twilio', 'aws-sns', 'vonage'], + EMAIL: ['aws-ses', 'sendgrid'], + VOICE: ['twilio', 'vonage'], + PUSH: ['fcm', 'apns'] + } + + return providers[request.channel]?.[0] || 'twilio' + } + + private async selectFailoverProvider(request: MessageRequest, failedProvider: string): Promise { + // Select next provider in failover chain + const providers: Record = { + SMS: ['twilio', 'aws-sns', 'vonage'], + EMAIL: ['aws-ses', 'sendgrid'], + VOICE: ['twilio', 'vonage'], + PUSH: ['fcm', 'apns'] + } + + const chain = providers[request.channel] || [] + const index = chain.indexOf(failedProvider) + return index >= 0 && index < chain.length - 1 ? chain[index + 1] : null + } + + private async sendViaProvider(provider: string, request: MessageRequest): Promise { + // This would call the appropriate provider adapter + logger.info('Sending via provider', { provider, request }) + // Placeholder - would implement actual provider calls + } +} + +export const messagingOrchestratorService = new MessagingOrchestratorService() diff --git a/api/src/services/sovereign-stack/observability-service.ts b/api/src/services/sovereign-stack/observability-service.ts new file mode 100644 index 0000000..83c2ec6 --- /dev/null +++ b/api/src/services/sovereign-stack/observability-service.ts @@ -0,0 +1,218 @@ +/** + * Phoenix Observability Stack Service + * Distributed tracing, structured logs, SLOs, correlation IDs + */ + +import { getDb } from '../../db/index.js' +import { logger } from '../../lib/logger.js' + +export interface Trace { + traceId: string + correlationId: string + spans: Span[] + startTime: Date + endTime: Date + duration: number +} + +export interface Span { + spanId: string + traceId: string + parentSpanId: string | null + serviceName: string + operationName: string + startTime: Date + endTime: Date + duration: number + tags: Record + logs: LogEntry[] +} + +export interface LogEntry { + timestamp: Date + level: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' + message: string + correlationId: string + serviceName: string + metadata: Record +} + +export interface SLO { + sloId: string + serviceName: string + metricName: string + target: number + window: string + currentValue: number + status: 'HEALTHY' | 'WARNING' | 'BREACHED' +} + +class ObservabilityService { + /** + * Create a trace + */ + async createTrace(correlationId: string): Promise { + const db = getDb() + + const traceId = this.generateTraceId() + + const result = await db.query( + `INSERT INTO traces (trace_id, correlation_id, start_time) + VALUES ($1, $2, NOW()) + RETURNING *`, + [traceId, correlationId] + ) + + logger.info('Trace created', { traceId, correlationId }) + return { + traceId, + correlationId, + spans: [], + startTime: result.rows[0].start_time, + endTime: result.rows[0].start_time, + duration: 0 + } + } + + /** + * Add span to trace + */ + async addSpan( + traceId: string, + serviceName: string, + operationName: string, + parentSpanId: string | null, + tags: Record = {} + ): Promise { + const db = getDb() + + const spanId = this.generateSpanId() + const startTime = new Date() + + const result = await db.query( + `INSERT INTO spans ( + span_id, trace_id, parent_span_id, service_name, operation_name, start_time, tags + ) VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING *`, + [ + spanId, + traceId, + parentSpanId, + serviceName, + operationName, + startTime, + JSON.stringify(tags) + ] + ) + + return { + spanId, + traceId, + parentSpanId, + serviceName, + operationName, + startTime, + endTime: startTime, + duration: 0, + tags, + logs: [] + } + } + + /** + * Complete a span + */ + async completeSpan(spanId: string, endTime?: Date): Promise { + const db = getDb() + + const span = await db.query( + `SELECT * FROM spans WHERE span_id = $1`, + [spanId] + ) + + if (span.rows.length === 0) { + throw new Error('Span not found') + } + + const finishTime = endTime || new Date() + const duration = finishTime.getTime() - span.rows[0].start_time.getTime() + + await db.query( + `UPDATE spans SET end_time = $1, duration = $2 WHERE span_id = $3`, + [finishTime, duration, spanId] + ) + } + + /** + * Log with correlation ID + */ + async log( + level: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR', + message: string, + correlationId: string, + serviceName: string, + metadata: Record = {} + ): Promise { + const db = getDb() + + await db.query( + `INSERT INTO structured_logs ( + level, message, correlation_id, service_name, metadata, timestamp + ) VALUES ($1, $2, $3, $4, $5, NOW())`, + [level, message, correlationId, serviceName, JSON.stringify(metadata)] + ) + + logger[level.toLowerCase()](message, { correlationId, serviceName, ...metadata }) + } + + /** + * Get SLO status + */ + async getSLOStatus(serviceName: string, metricName: string): Promise { + const db = getDb() + + const result = await db.query( + `SELECT * FROM slos WHERE service_name = $1 AND metric_name = $2`, + [serviceName, metricName] + ) + + if (result.rows.length === 0) { + return null + } + + const row = result.rows[0] + const currentValue = await this.getCurrentMetricValue(serviceName, metricName) + const status = this.calculateSLOStatus(row.target, currentValue) + + return { + sloId: row.id, + serviceName: row.service_name, + metricName: row.metric_name, + target: parseFloat(row.target), + window: row.window, + currentValue, + status + } + } + + private async getCurrentMetricValue(serviceName: string, metricName: string): Promise { + // Placeholder - would query actual metrics + return 0.99 // Example: 99% uptime + } + + private calculateSLOStatus(target: number, current: number): 'HEALTHY' | 'WARNING' | 'BREACHED' { + if (current >= target) return 'HEALTHY' + if (current >= target * 0.95) return 'WARNING' + return 'BREACHED' + } + + private generateTraceId(): string { + return `trace_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + } + + private generateSpanId(): string { + return `span_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + } +} + +export const observabilityService = new ObservabilityService() diff --git a/api/src/services/sovereign-stack/tx-orchestrator-service.ts b/api/src/services/sovereign-stack/tx-orchestrator-service.ts new file mode 100644 index 0000000..15c1e61 --- /dev/null +++ b/api/src/services/sovereign-stack/tx-orchestrator-service.ts @@ -0,0 +1,127 @@ +/** + * Phoenix Transaction Orchestrator Service + * On-chain/off-chain workflow orchestration with retries and compensations + */ + +import { getDb } from '../../db/index.js' +import { logger } from '../../lib/logger.js' + +export interface Workflow { + workflowId: string + correlationId: string + state: 'INITIATED' | 'AUTHORIZED' | 'CAPTURED' | 'SETTLED' | 'REVERSED' | 'FAILED' + steps: WorkflowStep[] + retryCount: number + maxRetries: number +} + +export interface WorkflowStep { + stepId: string + type: 'ON_CHAIN' | 'OFF_CHAIN' + action: string + status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'FAILED' + retryCount: number + compensation?: string +} + +class TransactionOrchestratorService { + /** + * Create a workflow + */ + async createWorkflow( + correlationId: string, + steps: Omit[] + ): Promise { + const db = getDb() + + const result = await db.query( + `INSERT INTO workflows (correlation_id, state, max_retries, metadata) + VALUES ($1, 'INITIATED', 3, $2) + RETURNING *`, + [correlationId, JSON.stringify({ steps })] + ) + + const workflowId = result.rows[0].id + + // Create workflow steps + for (const step of steps) { + await db.query( + `INSERT INTO workflow_steps (workflow_id, type, action, status, compensation) + VALUES ($1, $2, $3, 'PENDING', $4)`, + [workflowId, step.type, step.action, step.compensation || null] + ) + } + + logger.info('Workflow created', { workflowId, correlationId }) + return this.mapWorkflow(result.rows[0]) + } + + /** + * Execute workflow step + */ + async executeStep(workflowId: string, stepId: string): Promise { + const db = getDb() + + // Update step status + await db.query( + `UPDATE workflow_steps SET status = 'IN_PROGRESS' WHERE id = $1`, + [stepId] + ) + + try { + // Execute step logic here + // This would route to appropriate provider adapter + + await db.query( + `UPDATE workflow_steps SET status = 'COMPLETED' WHERE id = $1`, + [stepId] + ) + + logger.info('Workflow step completed', { workflowId, stepId }) + } catch (error) { + await db.query( + `UPDATE workflow_steps SET status = 'FAILED' WHERE id = $1`, + [stepId] + ) + throw error + } + } + + /** + * Retry failed step + */ + async retryStep(workflowId: string, stepId: string): Promise { + const db = getDb() + + const step = await db.query( + `SELECT * FROM workflow_steps WHERE id = $1`, + [stepId] + ) + + if (step.rows[0].retry_count >= 3) { + throw new Error('Max retries exceeded') + } + + await db.query( + `UPDATE workflow_steps + SET status = 'PENDING', retry_count = retry_count + 1 + WHERE id = $1`, + [stepId] + ) + + await this.executeStep(workflowId, stepId) + } + + private mapWorkflow(row: any): Workflow { + return { + workflowId: row.id, + correlationId: row.correlation_id, + state: row.state, + steps: [], + retryCount: row.retry_count || 0, + maxRetries: row.max_retries || 3 + } + } +} + +export const txOrchestratorService = new TransactionOrchestratorService() diff --git a/api/src/services/sovereign-stack/voice-orchestrator-service.ts b/api/src/services/sovereign-stack/voice-orchestrator-service.ts new file mode 100644 index 0000000..99160a1 --- /dev/null +++ b/api/src/services/sovereign-stack/voice-orchestrator-service.ts @@ -0,0 +1,141 @@ +/** + * Phoenix Voice Orchestrator Service + * TTS/STT with caching, multi-provider routing, moderation + */ + +import { getDb } from '../../db/index.js' +import { logger } from '../../lib/logger.js' +import crypto from 'crypto' + +export interface VoiceSynthesisRequest { + text: string + voiceProfile: string + format: 'mp3' | 'wav' | 'ogg' + latencyClass: 'LOW' | 'STANDARD' | 'HIGH_QUALITY' +} + +export interface VoiceSynthesisResult { + audioHash: string + audioUrl: string + duration: number + provider: string + cached: boolean +} + +class VoiceOrchestratorService { + /** + * Synthesize voice with caching + */ + async synthesizeVoice(request: VoiceSynthesisRequest): Promise { + const db = getDb() + + // Generate deterministic cache key + const cacheKey = this.generateCacheKey(request.text, request.voiceProfile, request.format) + + // Check cache + const cached = await db.query( + `SELECT * FROM voice_cache WHERE cache_key = $1`, + [cacheKey] + ) + + if (cached.rows.length > 0) { + logger.info('Voice synthesis cache hit', { cacheKey }) + return { + audioHash: cached.rows[0].audio_hash, + audioUrl: cached.rows[0].audio_url, + duration: cached.rows[0].duration, + provider: cached.rows[0].provider, + cached: true + } + } + + // Select provider based on latency class + const provider = this.selectProvider(request.latencyClass) + + // Scrub PII from text + const scrubbedText = this.scrubPII(request.text) + + // Synthesize via provider + const synthesis = await this.synthesizeViaProvider(provider, { + ...request, + text: scrubbedText + }) + + // Store in cache + await db.query( + `INSERT INTO voice_cache (cache_key, audio_hash, audio_url, duration, provider, text_hash) + VALUES ($1, $2, $3, $4, $5, $6)`, + [cacheKey, synthesis.audioHash, synthesis.audioUrl, synthesis.duration, provider, cacheKey] + ) + + logger.info('Voice synthesized', { cacheKey, provider }) + return { + ...synthesis, + provider, + cached: false + } + } + + /** + * Get cached audio by hash + */ + async getAudioByHash(hash: string): Promise { + const db = getDb() + + const result = await db.query( + `SELECT * FROM voice_cache WHERE audio_hash = $1`, + [hash] + ) + + if (result.rows.length === 0) { + return null + } + + const row = result.rows[0] + return { + audioHash: row.audio_hash, + audioUrl: row.audio_url, + duration: row.duration, + provider: row.provider, + cached: true + } + } + + private generateCacheKey(text: string, voiceProfile: string, format: string): string { + const hash = crypto.createHash('sha256') + hash.update(`${text}:${voiceProfile}:${format}`) + return hash.digest('hex') + } + + private scrubPII(text: string): string { + // Placeholder - would implement actual PII scrubbing + // Remove emails, phone numbers, SSNs, etc. + return text + } + + private selectProvider(latencyClass: string): string { + const providers: Record = { + LOW: 'elevenlabs', + STANDARD: 'openai', + HIGH_QUALITY: 'elevenlabs' + } + return providers[latencyClass] || 'elevenlabs' + } + + private async synthesizeViaProvider( + provider: string, + request: VoiceSynthesisRequest + ): Promise> { + // This would call the appropriate provider adapter + logger.info('Synthesizing via provider', { provider, request }) + + // Placeholder - would implement actual provider calls + return { + audioHash: crypto.randomBytes(32).toString('hex'), + audioUrl: `https://cdn.sankofa.nexus/voice/${crypto.randomBytes(16).toString('hex')}.${request.format}`, + duration: 0 + } + } +} + +export const voiceOrchestratorService = new VoiceOrchestratorService() diff --git a/api/src/services/sovereign-stack/wallet-registry-service.ts b/api/src/services/sovereign-stack/wallet-registry-service.ts new file mode 100644 index 0000000..34cd32c --- /dev/null +++ b/api/src/services/sovereign-stack/wallet-registry-service.ts @@ -0,0 +1,112 @@ +/** + * Phoenix Wallet Registry Service + * Wallet mapping, chain support, policy engine, and recovery + */ + +import { getDb } from '../../db/index.js' +import { logger } from '../../lib/logger.js' + +export interface Wallet { + walletId: string + userId: string + orgId: string | null + address: string + chainId: number + custodyType: 'USER' | 'SHARED' | 'PLATFORM' + status: 'ACTIVE' | 'SUSPENDED' | 'RECOVERED' +} + +export interface TransactionRequest { + from: string + to: string + value: string + data?: string + chainId: number +} + +export interface TransactionSimulation { + success: boolean + gasEstimate: string + error?: string + warnings?: string[] +} + +class WalletRegistryService { + /** + * Register a wallet + */ + async registerWallet( + userId: string, + address: string, + chainId: number, + custodyType: 'USER' | 'SHARED' | 'PLATFORM', + orgId?: string + ): Promise { + const db = getDb() + + const result = await db.query( + `INSERT INTO wallets (user_id, org_id, address, chain_id, custody_type, status) + VALUES ($1, $2, $3, $4, $5, 'ACTIVE') + RETURNING *`, + [userId, orgId || null, address, chainId, custodyType] + ) + + logger.info('Wallet registered', { walletId: result.rows[0].id, address }) + return this.mapWallet(result.rows[0]) + } + + /** + * Build a transaction + */ + async buildTransaction(request: TransactionRequest): Promise { + // This would use a transaction builder service with deterministic encoding + logger.info('Building transaction', { request }) + + // Placeholder - would integrate with actual transaction builder + return '0x' // Serialized transaction + } + + /** + * Simulate a transaction + */ + async simulateTransaction(request: TransactionRequest): Promise { + logger.info('Simulating transaction', { request }) + + // Placeholder - would call chain RPC for simulation + return { + success: true, + gasEstimate: '21000', + warnings: [] + } + } + + /** + * Get wallets for user + */ + async getWalletsForUser(userId: string, chainId?: number): Promise { + const db = getDb() + + const query = chainId + ? `SELECT * FROM wallets WHERE user_id = $1 AND chain_id = $2` + : `SELECT * FROM wallets WHERE user_id = $1` + + const params = chainId ? [userId, chainId] : [userId] + const result = await db.query(query, params) + + return result.rows.map(this.mapWallet) + } + + private mapWallet(row: any): Wallet { + return { + walletId: row.id, + userId: row.user_id, + orgId: row.org_id, + address: row.address, + chainId: row.chain_id, + custodyType: row.custody_type, + status: row.status + } + } +} + +export const walletRegistryService = new WalletRegistryService() diff --git a/docs/api/API_VERSIONING.md b/docs/api/API_VERSIONING.md index 160bb06..ad61909 100644 --- a/docs/api/API_VERSIONING.md +++ b/docs/api/API_VERSIONING.md @@ -10,7 +10,7 @@ This document describes the API versioning strategy for the Sankofa Phoenix API. ### URL-Based Versioning -The API uses URL-based versioning for REST endpoints: +The API uses URL-based versioning for REST endpoints. The Phoenix API Railing (Infra, VE, Health, tenant-scoped) uses `/api/v1/` and aligns with this strategy. ``` /api/v1/resource diff --git a/docs/phoenix/PORTAL_RAILING_WIRING.md b/docs/phoenix/PORTAL_RAILING_WIRING.md new file mode 100644 index 0000000..167c802 --- /dev/null +++ b/docs/phoenix/PORTAL_RAILING_WIRING.md @@ -0,0 +1,73 @@ +# Phoenix Portal — API Railing Wiring + +**Purpose:** How the Phoenix Portal (UX/UI) calls the Phoenix API Railing for infrastructure, VMs, and health. +**Related:** Phoenix API Railing Spec (proxmox repo: `docs/02-architecture/PHOENIX_API_RAILING_SPEC.md`) + +--- + +## 1. Base URL + +Portal should call the **Phoenix API** (GraphQL + REST). When running locally: `http://localhost:4000`. In production: `https://api.phoenix.sankofa.nexus` (or the configured API URL). All REST railing routes are under `/api/v1/`. + +--- + +## 2. Infrastructure Overview (Portal) + +| Portal area | REST endpoint | Notes | +|-------------|---------------|--------| +| Cluster nodes | `GET /api/v1/infra/nodes` | Returns `{ nodes: [...] }`; each node has `node`, `status`, `cpu`, `mem`, etc. | +| Storage pools | `GET /api/v1/infra/storage` | Returns `{ storage: [...] }`. | + +**Auth:** Use the same session/token as for GraphQL (Keycloak OIDC). The Phoenix API forwards these to the railing (phoenix-deploy-api or internal) when `PHOENIX_RAILING_URL` is set. + +--- + +## 3. VM/CT List and Actions (Portal) + +| Portal area | REST endpoint | Notes | +|-------------|---------------|--------| +| List VMs/CTs | `GET /api/v1/ve/vms?node=` | Optional `node` query to filter by Proxmox node. | +| VM/CT status | `GET /api/v1/ve/vms/:node/:vmid/status?type=lxc|qemu` | `type=lxc` for containers. | +| Start | `POST /api/v1/ve/vms/:node/:vmid/start?type=lxc|qemu` | | +| Stop | `POST /api/v1/ve/vms/:node/:vmid/stop?type=lxc|qemu` | | +| Reboot | `POST /api/v1/ve/vms/:node/:vmid/reboot?type=lxc|qemu` | | + +**Auth:** Required. Gate destructive actions (start/stop/reboot) by role in the API; Portal should only show actions when the user has permission. + +--- + +## 4. Health / Dashboards (Portal) + +| Portal area | REST endpoint | Notes | +|-------------|---------------|--------| +| Health summary | `GET /api/v1/health/summary` | Returns `{ status, hosts, alerts, updated_at }`. | +| Metrics (PromQL) | `GET /api/v1/health/metrics?query=` | Proxy to Prometheus; use for custom dashboards. | +| Active alerts | `GET /api/v1/health/alerts` | Returns `{ alerts: [...] }`. | + +--- + +## 5. Tenant-Scoped (Client API) + +For **tenant** users (API key or JWT with tenant scope): + +| Endpoint | Description | +|----------|-------------| +| `GET /api/v1/tenants/me/resources` | Resources for the current tenant (from `tenantContext`). | +| `GET /api/v1/tenants/me/health` | Health summary (proxied to railing when configured). | + +**Auth:** Require `Authorization: Bearer ` with tenant claim or API key with tenant scope. + +--- + +## 6. Keycloak Integration + +Portal authenticates via Keycloak (OIDC). Backend (Portal server or BFF) should obtain a token and call the Phoenix API with `Authorization: Bearer `. The Phoenix API validates the token and sets `tenantContext` from the token claims; railing proxy and tenant me routes use that context. + +--- + +## 7. Implementation Checklist + +- [ ] **3.1** Portal: Infrastructure overview page — fetch `GET /api/v1/infra/nodes` and `GET /api/v1/infra/storage`, display hosts and storage. +- [ ] **3.2** Portal: VM/CT list — fetch `GET /api/v1/ve/vms`, display table; buttons for start/stop/reboot call the POST endpoints. +- [ ] **3.3** Portal: Health/dashboards — fetch `GET /api/v1/health/summary` and optionally `GET /api/v1/health/alerts`; render status and alerts. +- [ ] **3.4** Keycloak: Ensure Portal backend or BFF uses server-side token for API calls; token includes tenant when applicable.