Initial commit: Alfred Proxy with OAuth, TTS, and FCM push notifications
- Environment-based configuration (no hardcoded secrets) - OAuth authentication via Authentik - ElevenLabs TTS integration via SAG CLI - FCM push notification support - User preferences sync system - Multi-user support with per-user context files - No internal IPs or service accounts in tracked files
This commit is contained in:
21
.env.example
Normal file
21
.env.example
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Alfred Proxy Configuration
|
||||||
|
# Copy this file to .env and fill in your values
|
||||||
|
|
||||||
|
# Port for the proxy to listen on
|
||||||
|
PROXY_PORT=18790
|
||||||
|
|
||||||
|
# OpenClaw WebSocket URL
|
||||||
|
OPENCLAW_URL=ws://127.0.0.1:18789
|
||||||
|
|
||||||
|
# OpenClaw gateway token (get from your OpenClaw config)
|
||||||
|
OPENCLAW_TOKEN=your-openclaw-token-here
|
||||||
|
|
||||||
|
# Authentik OAuth configuration
|
||||||
|
AUTHENTIK_URL=https://auth.yourdomain.com
|
||||||
|
AUTHENTIK_CLIENT_ID=your-oauth-client-id-here
|
||||||
|
|
||||||
|
# ElevenLabs API key for TTS (optional - falls back to local TTS if not set)
|
||||||
|
ELEVENLABS_API_KEY=your-elevenlabs-api-key-here
|
||||||
|
|
||||||
|
# Set to 'false' to disable authentication (development only)
|
||||||
|
REQUIRE_AUTH=true
|
||||||
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
node_modules/
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
fcm-tokens.json
|
||||||
|
service-account.json
|
||||||
|
users/
|
||||||
|
uploads/
|
||||||
|
|
||||||
|
# HAProxy configs with internal IPs
|
||||||
|
haproxy*.cfg
|
||||||
|
|
||||||
|
# Google Cloud / Firebase service accounts
|
||||||
|
openclaw-*.json
|
||||||
|
google-services*.json
|
||||||
|
|
||||||
|
# Windows metadata
|
||||||
|
*:Zone.Identifier
|
||||||
356
DEPLOYMENT.md
Normal file
356
DEPLOYMENT.md
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
# Alfred Mobile Proxy Deployment Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Your setup:
|
||||||
|
- **Client ID:** `QeSNaZPqZUz5pPClZMA2bakSsddkStiEhqbE4QZR`
|
||||||
|
- **Authentik URL:** `https://auth.dnspegasus.net`
|
||||||
|
- **Gateway token:** `9b87d15fee3922ecfbe77b0ea1744851757cda618beceeba`
|
||||||
|
- **Mobile URL:** `wss://alfred-app.dnspegasus.net`
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Android App
|
||||||
|
↓ OAuth: auth.dnspegasus.net
|
||||||
|
↓ WebSocket: wss://alfred-app.dnspegasus.net
|
||||||
|
HAProxy (192.168.1.20)
|
||||||
|
↓ Proxy backend → 192.168.1.169:18790
|
||||||
|
Alfred Proxy (localhost:18790)
|
||||||
|
↓ Validates OAuth token
|
||||||
|
↓ Injects gateway token
|
||||||
|
OpenClaw (localhost:18789)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: Configure Proxy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.openclaw/workspace/alfred-proxy
|
||||||
|
|
||||||
|
# Create .env file
|
||||||
|
cat > .env << 'EOF'
|
||||||
|
PROXY_PORT=18790
|
||||||
|
OPENCLAW_URL=ws://127.0.0.1:18789
|
||||||
|
OPENCLAW_TOKEN=9b87d15fee3922ecfbe77b0ea1744851757cda618beceeba
|
||||||
|
AUTHENTIK_URL=https://auth.dnspegasus.net
|
||||||
|
AUTHENTIK_CLIENT_ID=QeSNaZPqZUz5pPClZMA2bakSsddkStiEhqbE4QZR
|
||||||
|
REQUIRE_AUTH=true
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Test locally first
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
In another terminal:
|
||||||
|
```bash
|
||||||
|
# Test health
|
||||||
|
curl http://localhost:18790/health
|
||||||
|
|
||||||
|
# Should return: {"status":"ok","service":"alfred-proxy"}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2: Switch OpenClaw to Localhost
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check current setting
|
||||||
|
openclaw config get gateway.bind
|
||||||
|
|
||||||
|
# Switch to localhost only
|
||||||
|
cat >> ~/.openclaw/openclaw.json << 'EOF'
|
||||||
|
{
|
||||||
|
"gateway": {
|
||||||
|
"bind": "loopback"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Restart OpenClaw
|
||||||
|
systemctl --user restart openclaw-gateway.service
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
openclaw config get gateway.bind
|
||||||
|
# Should show: "loopback"
|
||||||
|
|
||||||
|
# Test local connection
|
||||||
|
wscat -c ws://127.0.0.1:18789
|
||||||
|
# Should see connect challenge
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3: Install Proxy as Service
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.openclaw/workspace/alfred-proxy
|
||||||
|
|
||||||
|
# Install systemd service
|
||||||
|
mkdir -p ~/.config/systemd/user
|
||||||
|
cp alfred-proxy.service ~/.config/systemd/user/
|
||||||
|
|
||||||
|
# Create override with your client ID
|
||||||
|
mkdir -p ~/.config/systemd/user/alfred-proxy.service.d
|
||||||
|
cat > ~/.config/systemd/user/alfred-proxy.service.d/override.conf << 'EOF'
|
||||||
|
[Service]
|
||||||
|
Environment="AUTHENTIK_CLIENT_ID=QeSNaZPqZUz5pPClZMA2bakSsddkStiEhqbE4QZR"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Reload and start
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable alfred-proxy.service
|
||||||
|
systemctl --user start alfred-proxy.service
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
systemctl --user status alfred-proxy.service
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
journalctl --user -u alfred-proxy.service -f
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4: Verify DNS (Already Done!)
|
||||||
|
|
||||||
|
You already have a wildcard DNS record pointing to HAProxy, so `alfred-app.dnspegasus.net` should already resolve!
|
||||||
|
|
||||||
|
**Test DNS:**
|
||||||
|
```bash
|
||||||
|
nslookup alfred-app.dnspegasus.net
|
||||||
|
# Should resolve to your HAProxy IP (via wildcard *.dnspegasus.net)
|
||||||
|
```
|
||||||
|
|
||||||
|
If it doesn't work, you may need to explicitly add:
|
||||||
|
```
|
||||||
|
Type: A
|
||||||
|
Host: alfred-app
|
||||||
|
Value: <your HAProxy IP>
|
||||||
|
TTL: 300
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5: Configure HAProxy
|
||||||
|
|
||||||
|
**SSH to HAProxy:**
|
||||||
|
```bash
|
||||||
|
ssh root@192.168.1.20
|
||||||
|
```
|
||||||
|
|
||||||
|
**Edit HAProxy config:**
|
||||||
|
```bash
|
||||||
|
docker exec -it haproxy-manager bash
|
||||||
|
nano /etc/haproxy/haproxy.cfg
|
||||||
|
```
|
||||||
|
|
||||||
|
**Add this configuration** (based on `haproxy-alfred-app.cfg`):
|
||||||
|
|
||||||
|
```haproxy
|
||||||
|
# In your frontend section (around line ~50):
|
||||||
|
|
||||||
|
frontend https_frontend
|
||||||
|
# ... existing config ...
|
||||||
|
|
||||||
|
# NEW: alfred-app subdomain ACL
|
||||||
|
acl alfred_app_acl hdr(host) -i alfred-app.dnspegasus.net
|
||||||
|
acl is_websocket hdr(Upgrade) -i WebSocket
|
||||||
|
acl is_websocket_connection hdr_beg(Connection) -i Upgrade
|
||||||
|
|
||||||
|
# Route alfred-app subdomain
|
||||||
|
use_backend alfred_mobile_proxy-backend if alfred_app_acl is_websocket is_websocket_connection
|
||||||
|
use_backend alfred_mobile_redirect-backend if alfred_app_acl
|
||||||
|
|
||||||
|
# At the end of the file, add new backends:
|
||||||
|
|
||||||
|
backend alfred_mobile_proxy-backend
|
||||||
|
mode http
|
||||||
|
option forwardfor
|
||||||
|
|
||||||
|
http-request add-header X-CLIENT-IP %[var(txn.real_ip)]
|
||||||
|
http-request set-header X-Real-IP %[var(txn.real_ip)]
|
||||||
|
http-request set-header X-Forwarded-For %[var(txn.real_ip)]
|
||||||
|
http-request set-header X-Forwarded-Proto https if { ssl_fc }
|
||||||
|
|
||||||
|
timeout tunnel 1h
|
||||||
|
timeout client 1h
|
||||||
|
timeout server 1h
|
||||||
|
|
||||||
|
# IMPORTANT: Your desktop IP where proxy runs
|
||||||
|
# IP: 192.168.1.169 Port: 18790
|
||||||
|
server alfred_proxy 192.168.1.169:18790 check
|
||||||
|
|
||||||
|
backend alfred_mobile_redirect-backend
|
||||||
|
mode http
|
||||||
|
http-request return status 200 content-type "text/html" string '<!DOCTYPE html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=https://alfred.dnspegasus.net"><title>Alfred Mobile</title><style>body{font-family:system-ui;text-align:center;padding:50px;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff}h1{font-size:2.5rem;margin-bottom:1rem}p{font-size:1.1rem;opacity:0.9}</style></head><body><h1>🤵 Alfred Mobile</h1><p>This endpoint is for the mobile app.</p><p>Redirecting to web interface...</p></body></html>'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test HAProxy config:**
|
||||||
|
```bash
|
||||||
|
haproxy -c -f /etc/haproxy/haproxy.cfg
|
||||||
|
# Should show: Configuration file is valid
|
||||||
|
```
|
||||||
|
|
||||||
|
**Reload HAProxy:**
|
||||||
|
```bash
|
||||||
|
# If using Docker container:
|
||||||
|
docker exec haproxy-manager kill -HUP 1
|
||||||
|
|
||||||
|
# Or restart:
|
||||||
|
docker restart haproxy-manager
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 6: Open Firewall (if needed)
|
||||||
|
|
||||||
|
On your desktop (where proxy runs):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# PowerShell (Admin)
|
||||||
|
New-NetFirewallRule -DisplayName "Alfred Proxy" -Direction Inbound -LocalPort 18790 -Protocol TCP -Action Allow
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use your existing scripts:
|
||||||
|
```powershell
|
||||||
|
.\open-openclaw-port.ps1
|
||||||
|
# Then modify for port 18790
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 7: Test End-to-End
|
||||||
|
|
||||||
|
**From outside your network:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test DNS
|
||||||
|
nslookup alfred-app.dnspegasus.net
|
||||||
|
|
||||||
|
# Test HTTPS redirect (browser should redirect)
|
||||||
|
curl -I https://alfred-app.dnspegasus.net
|
||||||
|
|
||||||
|
# Test WebSocket (requires valid OAuth token)
|
||||||
|
# Get token from Authentik first, then:
|
||||||
|
wscat -c "wss://alfred-app.dnspegasus.net" -H "Authorization: Bearer YOUR_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected flow:**
|
||||||
|
1. Browser → `https://alfred-app.dnspegasus.net` → Redirects to `https://alfred.dnspegasus.net` ✅
|
||||||
|
2. Mobile app → `wss://alfred-app.dnspegasus.net` → Connects to proxy ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 8: Android App Configuration
|
||||||
|
|
||||||
|
Update your Android app:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// OAuthConfig.kt
|
||||||
|
object OAuthConfig {
|
||||||
|
const val AUTHENTIK_URL = "https://auth.dnspegasus.net"
|
||||||
|
const val CLIENT_ID = "QeSNaZPqZUz5pPClZMA2bakSsddkStiEhqbE4QZR"
|
||||||
|
const val REDIRECT_URI = "alfredmobile://oauth/callback"
|
||||||
|
const val SCOPE = "openid profile email"
|
||||||
|
|
||||||
|
const val AUTHORIZATION_ENDPOINT = "$AUTHENTIK_URL/application/o/authorize/"
|
||||||
|
const val TOKEN_ENDPOINT = "$AUTHENTIK_URL/application/o/token/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// AlfredConfig.kt
|
||||||
|
object AlfredConfig {
|
||||||
|
const val GATEWAY_URL = "wss://alfred-app.dnspegasus.net"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Proxy won't start
|
||||||
|
```bash
|
||||||
|
# Check logs
|
||||||
|
journalctl --user -u alfred-proxy.service -n 50
|
||||||
|
|
||||||
|
# Test manually
|
||||||
|
cd ~/.openclaw/workspace/alfred-proxy
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Can't connect from mobile
|
||||||
|
```bash
|
||||||
|
# Check proxy is listening
|
||||||
|
ss -tuln | grep 18790
|
||||||
|
|
||||||
|
# Check firewall
|
||||||
|
sudo iptables -L -n | grep 18790
|
||||||
|
|
||||||
|
# Check HAProxy backend
|
||||||
|
curl http://192.168.1.169:18790/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Invalid token" errors
|
||||||
|
```bash
|
||||||
|
# Verify Authentik config
|
||||||
|
curl https://auth.dnspegasus.net/.well-known/openid-configuration
|
||||||
|
|
||||||
|
# Test token validation
|
||||||
|
curl -H "Authorization: Bearer TOKEN" \
|
||||||
|
https://auth.dnspegasus.net/application/o/userinfo/
|
||||||
|
```
|
||||||
|
|
||||||
|
### DNS not resolving
|
||||||
|
```bash
|
||||||
|
# Check DNS record exists (should work via wildcard)
|
||||||
|
nslookup alfred-app.dnspegasus.net
|
||||||
|
|
||||||
|
# Try public DNS
|
||||||
|
nslookup alfred-app.dnspegasus.net 8.8.8.8
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Checklist
|
||||||
|
|
||||||
|
- [x] OpenClaw bound to localhost only
|
||||||
|
- [x] Proxy validates OAuth tokens
|
||||||
|
- [x] Gateway token not exposed
|
||||||
|
- [ ] HTTPS/WSS for external access
|
||||||
|
- [ ] Firewall rules configured
|
||||||
|
- [ ] Monitoring set up
|
||||||
|
- [ ] Test token revocation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
**Watch proxy logs:**
|
||||||
|
```bash
|
||||||
|
journalctl --user -u alfred-proxy.service -f
|
||||||
|
```
|
||||||
|
|
||||||
|
**Watch OpenClaw logs:**
|
||||||
|
```bash
|
||||||
|
journalctl --user -u openclaw-gateway.service -f
|
||||||
|
```
|
||||||
|
|
||||||
|
**Watch HAProxy logs:**
|
||||||
|
```bash
|
||||||
|
ssh root@192.168.1.20
|
||||||
|
docker logs -f haproxy-manager
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Test locally** (proxy + OpenClaw)
|
||||||
|
2. **Configure HAProxy** (add mobile subdomain)
|
||||||
|
3. **Add DNS record**
|
||||||
|
4. **Test from outside** (mobile network)
|
||||||
|
5. **Implement OAuth in Android app**
|
||||||
|
6. **Test full flow**
|
||||||
174
QUICKSTART.md
Normal file
174
QUICKSTART.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# Alfred Proxy Quick Start
|
||||||
|
|
||||||
|
## Your Configuration
|
||||||
|
|
||||||
|
### Backend Details for HAProxy
|
||||||
|
|
||||||
|
**Add to HAProxy backend:**
|
||||||
|
```
|
||||||
|
Server IP: 192.168.1.169
|
||||||
|
Server Port: 18790
|
||||||
|
```
|
||||||
|
|
||||||
|
**Full HAProxy backend config:**
|
||||||
|
```haproxy
|
||||||
|
backend alfred_mobile_proxy-backend
|
||||||
|
mode http
|
||||||
|
option forwardfor
|
||||||
|
|
||||||
|
http-request add-header X-CLIENT-IP %[var(txn.real_ip)]
|
||||||
|
http-request set-header X-Real-IP %[var(txn.real_ip)]
|
||||||
|
http-request set-header X-Forwarded-For %[var(txn.real_ip)]
|
||||||
|
http-request set-header X-Forwarded-Proto https if { ssl_fc }
|
||||||
|
|
||||||
|
timeout tunnel 1h
|
||||||
|
timeout client 1h
|
||||||
|
timeout server 1h
|
||||||
|
|
||||||
|
server alfred_proxy 192.168.1.169:18790 check
|
||||||
|
```
|
||||||
|
|
||||||
|
### URLs
|
||||||
|
|
||||||
|
- **Mobile app connects to:** `wss://alfred-app.dnspegasus.net`
|
||||||
|
- **OAuth authentication:** `https://auth.dnspegasus.net`
|
||||||
|
- **Web browser redirect:** `https://alfred.dnspegasus.net`
|
||||||
|
|
||||||
|
### DNS
|
||||||
|
|
||||||
|
Your wildcard DNS (`*.dnspegasus.net`) should already resolve `alfred-app.dnspegasus.net` to HAProxy.
|
||||||
|
|
||||||
|
Test: `nslookup alfred-app.dnspegasus.net`
|
||||||
|
|
||||||
|
### Android App Configuration
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// OAuthConfig.kt
|
||||||
|
object OAuthConfig {
|
||||||
|
const val AUTHENTIK_URL = "https://auth.dnspegasus.net"
|
||||||
|
const val CLIENT_ID = "QeSNaZPqZUz5pPClZMA2bakSsddkStiEhqbE4QZR"
|
||||||
|
const val REDIRECT_URI = "alfredmobile://oauth/callback"
|
||||||
|
const val SCOPE = "openid profile email"
|
||||||
|
|
||||||
|
const val AUTHORIZATION_ENDPOINT = "$AUTHENTIK_URL/application/o/authorize/"
|
||||||
|
const val TOKEN_ENDPOINT = "$AUTHENTIK_URL/application/o/token/"
|
||||||
|
const val USERINFO_ENDPOINT = "$AUTHENTIK_URL/application/o/userinfo/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// AlfredConfig.kt
|
||||||
|
object AlfredConfig {
|
||||||
|
const val GATEWAY_URL = "wss://alfred-app.dnspegasus.net"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start the Proxy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.openclaw/workspace/alfred-proxy
|
||||||
|
|
||||||
|
# 1. Create .env
|
||||||
|
cat > .env << 'EOF'
|
||||||
|
PROXY_PORT=18790
|
||||||
|
OPENCLAW_URL=ws://127.0.0.1:18789
|
||||||
|
OPENCLAW_TOKEN=9b87d15fee3922ecfbe77b0ea1744851757cda618beceeba
|
||||||
|
AUTHENTIK_URL=https://auth.dnspegasus.net
|
||||||
|
AUTHENTIK_CLIENT_ID=QeSNaZPqZUz5pPClZMA2bakSsddkStiEhqbE4QZR
|
||||||
|
REQUIRE_AUTH=true
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 2. Install and start
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 3. Test health
|
||||||
|
curl http://localhost:18790/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### HAProxy Configuration
|
||||||
|
|
||||||
|
See `haproxy-alfred-app.cfg` for complete config.
|
||||||
|
|
||||||
|
**Key points:**
|
||||||
|
- Frontend ACL: `alfred-app.dnspegasus.net`
|
||||||
|
- WebSocket connections → `alfred_mobile_proxy-backend`
|
||||||
|
- Browser visits → Redirect to main web UI
|
||||||
|
- Backend server: `192.168.1.169:18790`
|
||||||
|
|
||||||
|
### Network Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
Android App
|
||||||
|
↓
|
||||||
|
wss://alfred-app.dnspegasus.net (OAuth token in Authorization header)
|
||||||
|
↓
|
||||||
|
HAProxy (192.168.1.20:443)
|
||||||
|
↓
|
||||||
|
Alfred Proxy (192.168.1.169:18790)
|
||||||
|
- Validates OAuth token with Authentik
|
||||||
|
- Injects OpenClaw gateway token
|
||||||
|
↓
|
||||||
|
OpenClaw (127.0.0.1:18789)
|
||||||
|
↓
|
||||||
|
Alfred AI assistant
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Flow
|
||||||
|
|
||||||
|
1. **Test proxy locally:**
|
||||||
|
```bash
|
||||||
|
curl http://localhost:18790/health
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Test from HAProxy server:**
|
||||||
|
```bash
|
||||||
|
curl http://192.168.1.169:18790/health
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Test DNS:**
|
||||||
|
```bash
|
||||||
|
nslookup alfred-app.dnspegasus.net
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Test redirect (browser):**
|
||||||
|
```bash
|
||||||
|
curl -I https://alfred-app.dnspegasus.net
|
||||||
|
# Should redirect to alfred.dnspegasus.net
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Test WebSocket (with OAuth token):**
|
||||||
|
```bash
|
||||||
|
# Get token from Authentik first, then:
|
||||||
|
wscat -c "wss://alfred-app.dnspegasus.net" \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
**Can't reach proxy from HAProxy:**
|
||||||
|
```bash
|
||||||
|
# Test direct connection
|
||||||
|
curl http://192.168.1.169:18790/health
|
||||||
|
|
||||||
|
# Check Windows firewall
|
||||||
|
# PowerShell (Admin):
|
||||||
|
New-NetFirewallRule -DisplayName "Alfred Proxy" `
|
||||||
|
-Direction Inbound -LocalPort 18790 -Protocol TCP -Action Allow
|
||||||
|
```
|
||||||
|
|
||||||
|
**WebSocket upgrade fails:**
|
||||||
|
- Check HAProxy ACL matches `alfred-app.dnspegasus.net`
|
||||||
|
- Verify WebSocket headers are present
|
||||||
|
- Check proxy logs: `journalctl --user -u alfred-proxy.service -f`
|
||||||
|
|
||||||
|
**Invalid token:**
|
||||||
|
- Verify Client ID in proxy `.env` matches Authentik
|
||||||
|
- Test token: `curl -H "Authorization: Bearer TOKEN" https://auth.dnspegasus.net/application/o/userinfo/`
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
|
||||||
|
1. ✅ Start proxy locally
|
||||||
|
2. ✅ Configure HAProxy with correct backend
|
||||||
|
3. ✅ Test DNS resolves
|
||||||
|
4. ✅ Test redirect in browser
|
||||||
|
5. ✅ Implement OAuth in Android app
|
||||||
|
6. ✅ Test end-to-end flow
|
||||||
149
README.md
Normal file
149
README.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# Alfred Proxy
|
||||||
|
|
||||||
|
OAuth2 proxy server for Alfred Mobile app, providing secure WebSocket connection to OpenClaw Gateway with authentication, user preferences sync, and push notifications.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **OAuth2 Authentication**: Authentik integration with JWT validation
|
||||||
|
- **WebSocket Proxy**: Routes mobile app connections to OpenClaw Gateway
|
||||||
|
- **User Preferences**: Per-user settings storage and sync
|
||||||
|
- **Push Notifications**: FCM integration for alerts and alarms
|
||||||
|
- **TTS Service**: ElevenLabs text-to-speech endpoint
|
||||||
|
- **File Uploads**: Media upload support for voice messages
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js 18+
|
||||||
|
- Firebase Admin SDK credentials (for push notifications)
|
||||||
|
- Authentik OAuth2 provider (or compatible OAuth server)
|
||||||
|
- OpenClaw Gateway instance
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. Clone the repository
|
||||||
|
2. Install dependencies:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Copy `.env.example` to `.env` and configure:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Edit `.env` with your values:
|
||||||
|
- `OPENCLAW_TOKEN`: Get from your OpenClaw configuration
|
||||||
|
- `AUTHENTIK_URL`: Your OAuth provider URL
|
||||||
|
- `AUTHENTIK_CLIENT_ID`: OAuth client ID from your provider
|
||||||
|
- `ELEVENLABS_API_KEY`: (Optional) For text-to-speech
|
||||||
|
|
||||||
|
5. Add Firebase credentials:
|
||||||
|
- Download `service-account.json` from Firebase Console
|
||||||
|
- Place in project root (already in .gitignore)
|
||||||
|
|
||||||
|
### Running
|
||||||
|
|
||||||
|
**Development:**
|
||||||
|
```bash
|
||||||
|
node server.js
|
||||||
|
```
|
||||||
|
|
||||||
|
**Production (systemd):**
|
||||||
|
```bash
|
||||||
|
# Copy service file
|
||||||
|
sudo cp alfred-proxy.service /etc/systemd/system/
|
||||||
|
|
||||||
|
# Enable and start
|
||||||
|
sudo systemctl enable alfred-proxy
|
||||||
|
sudo systemctl start alfred-proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### HTTP Endpoints
|
||||||
|
|
||||||
|
- `GET /health` - Health check
|
||||||
|
- `POST /api/notify` - Send notification to mobile devices
|
||||||
|
- `POST /api/tts` - Text-to-speech generation
|
||||||
|
- `POST /api/upload` - File upload
|
||||||
|
- `POST /api/alarm/dismiss` - Broadcast alarm dismissal
|
||||||
|
|
||||||
|
### WebSocket
|
||||||
|
|
||||||
|
- `ws://localhost:18790` - WebSocket proxy to OpenClaw
|
||||||
|
- Requires `Authorization: Bearer <oauth-token>` header
|
||||||
|
- Injects OpenClaw gateway token
|
||||||
|
- Routes user messages to appropriate sessions
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
### Required Environment Variables
|
||||||
|
|
||||||
|
All sensitive values MUST be set via environment variables. The code defaults to empty strings for:
|
||||||
|
- `OPENCLAW_TOKEN`
|
||||||
|
- `AUTHENTIK_URL`
|
||||||
|
- `AUTHENTIK_CLIENT_ID`
|
||||||
|
- `ELEVENLABS_API_KEY`
|
||||||
|
|
||||||
|
### Protected Files (.gitignore)
|
||||||
|
|
||||||
|
- `.env` - Environment variables
|
||||||
|
- `service-account.json` - Firebase credentials
|
||||||
|
- `fcm-tokens.json` - User FCM tokens
|
||||||
|
- `users/` - User preferences
|
||||||
|
- `uploads/` - Generated TTS files
|
||||||
|
|
||||||
|
**Never commit these files!**
|
||||||
|
|
||||||
|
## User Preferences
|
||||||
|
|
||||||
|
Per-user settings are stored in `users/{userId}.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"assistantName": "Jarvis",
|
||||||
|
"voiceId": "voice-id-here"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Users can customize their assistant name and voice through the mobile app.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Mobile App (OAuth)
|
||||||
|
↓
|
||||||
|
alfred-proxy (validates JWT, injects OpenClaw token)
|
||||||
|
↓
|
||||||
|
OpenClaw Gateway
|
||||||
|
↓
|
||||||
|
Agent Session
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
**Watch mode:**
|
||||||
|
```bash
|
||||||
|
npm run dev # if you have nodemon
|
||||||
|
```
|
||||||
|
|
||||||
|
**Logs:**
|
||||||
|
```bash
|
||||||
|
tail -f /tmp/alfred-proxy.log # systemd
|
||||||
|
# or
|
||||||
|
journalctl --user -u alfred-proxy -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
## Security Notice
|
||||||
|
|
||||||
|
This is middleware security software. Ensure:
|
||||||
|
- OAuth tokens are kept secure
|
||||||
|
- OpenClaw token has appropriate permissions
|
||||||
|
- SSL/TLS enabled for production (use `wss://` not `ws://`)
|
||||||
|
- Firewall rules restrict access appropriately
|
||||||
261
SETUP.md
Normal file
261
SETUP.md
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
# Alfred Proxy Setup Guide
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.openclaw/workspace/alfred-proxy
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create Authentik OAuth Provider
|
||||||
|
|
||||||
|
**In Authentik admin:**
|
||||||
|
|
||||||
|
1. Navigate to **Applications** → **Providers** → **Create**
|
||||||
|
2. Select **OAuth2/OpenID Provider**
|
||||||
|
3. Fill in:
|
||||||
|
- **Name**: `Alfred Mobile OAuth`
|
||||||
|
- **Authentication flow**: `default-authentication-flow`
|
||||||
|
- **Authorization flow**: `default-provider-authorization-explicit-consent`
|
||||||
|
- **Client type**: `Public`
|
||||||
|
- **Client ID**: (will be auto-generated, note this down!)
|
||||||
|
- **Redirect URIs**:
|
||||||
|
```
|
||||||
|
alfredmobile://oauth/callback
|
||||||
|
http://localhost:8080/callback
|
||||||
|
```
|
||||||
|
- **Signing Key**: Select an existing certificate
|
||||||
|
- **Scopes**: Add `openid`, `profile`, `email`
|
||||||
|
|
||||||
|
4. Click **Create**
|
||||||
|
|
||||||
|
5. **Copy the Client ID** from the provider details page
|
||||||
|
|
||||||
|
### 3. Create Authentik Application
|
||||||
|
|
||||||
|
1. Navigate to **Applications** → **Applications** → **Create**
|
||||||
|
2. Fill in:
|
||||||
|
- **Name**: `Alfred Mobile`
|
||||||
|
- **Slug**: `alfred-mobile`
|
||||||
|
- **Provider**: Select `Alfred Mobile OAuth` (the provider you just created)
|
||||||
|
- **UI settings**: (optional) Add icon, description
|
||||||
|
- **Policy engine mode**: `any`
|
||||||
|
|
||||||
|
3. Click **Create**
|
||||||
|
|
||||||
|
### 4. Configure the Proxy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.openclaw/workspace/alfred-proxy
|
||||||
|
|
||||||
|
# Copy example config
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Edit with your Authentik client ID
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Update `.env`:
|
||||||
|
```bash
|
||||||
|
AUTHENTIK_CLIENT_ID=<paste-your-client-id-here>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Test Locally (No Auth)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Disable auth for testing
|
||||||
|
echo "REQUIRE_AUTH=false" >> .env
|
||||||
|
|
||||||
|
# Start proxy
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
In another terminal:
|
||||||
|
```bash
|
||||||
|
# Test health check
|
||||||
|
curl http://localhost:18790/health
|
||||||
|
|
||||||
|
# Test WebSocket (requires wscat: npm install -g wscat)
|
||||||
|
wscat -c ws://localhost:18790
|
||||||
|
```
|
||||||
|
|
||||||
|
If you see OpenClaw's connect challenge, the proxy is working! ✅
|
||||||
|
|
||||||
|
### 6. Switch OpenClaw to Localhost
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openclaw config get gateway.bind # Should show "lan" currently
|
||||||
|
|
||||||
|
# Switch to localhost only
|
||||||
|
cat >> ~/.openclaw/openclaw.json << 'EOF'
|
||||||
|
{
|
||||||
|
"gateway": {
|
||||||
|
"bind": "loopback"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Or use the gateway tool
|
||||||
|
openclaw gateway config apply <<< '{"gateway":{"bind":"loopback"}}'
|
||||||
|
|
||||||
|
# Restart gateway
|
||||||
|
systemctl --user restart openclaw-gateway.service
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
openclaw config get gateway.bind # Should show "loopback"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Enable Auth
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Re-enable auth
|
||||||
|
nano .env # Set REQUIRE_AUTH=true
|
||||||
|
|
||||||
|
# Restart proxy
|
||||||
|
# (If running npm run dev, Ctrl+C and restart)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Install as Systemd Service
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.openclaw/workspace/alfred-proxy
|
||||||
|
|
||||||
|
# Install service
|
||||||
|
mkdir -p ~/.config/systemd/user
|
||||||
|
cp alfred-proxy.service ~/.config/systemd/user/
|
||||||
|
|
||||||
|
# Create override file with your Client ID
|
||||||
|
mkdir -p ~/.config/systemd/user/alfred-proxy.service.d
|
||||||
|
cat > ~/.config/systemd/user/alfred-proxy.service.d/override.conf << EOF
|
||||||
|
[Service]
|
||||||
|
Environment="AUTHENTIK_CLIENT_ID=YOUR_CLIENT_ID_HERE"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Reload systemd
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
|
||||||
|
# Enable and start
|
||||||
|
systemctl --user enable alfred-proxy.service
|
||||||
|
systemctl --user start alfred-proxy.service
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
systemctl --user status alfred-proxy.service
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
journalctl --user -u alfred-proxy.service -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. Expose via Network (Optional)
|
||||||
|
|
||||||
|
**Option A: Expose directly (for testing on local network)**
|
||||||
|
|
||||||
|
Update `.env`:
|
||||||
|
```bash
|
||||||
|
# Listen on all interfaces instead of just localhost
|
||||||
|
# (only if you understand the security implications)
|
||||||
|
PROXY_PORT=0.0.0.0:18790
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option B: Expose via HAProxy with SSL (recommended)**
|
||||||
|
|
||||||
|
See README.md for HAProxy configuration.
|
||||||
|
|
||||||
|
### 10. Test with OAuth Token
|
||||||
|
|
||||||
|
**Get a test token from Authentik:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Use Authentik's OAuth2 token endpoint
|
||||||
|
curl -X POST https://auth.dnspegasus.net/application/o/token/ \
|
||||||
|
-d "grant_type=password" \
|
||||||
|
-d "username=YOUR_USERNAME" \
|
||||||
|
-d "password=YOUR_PASSWORD" \
|
||||||
|
-d "client_id=YOUR_CLIENT_ID"
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use the Authentik admin UI to generate a token.
|
||||||
|
|
||||||
|
**Test with the token:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wscat -c ws://localhost:18790 -H "Authorization: Bearer YOUR_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Proxy won't start
|
||||||
|
|
||||||
|
**Check Node.js:**
|
||||||
|
```bash
|
||||||
|
node --version # Should be v24+
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check dependencies:**
|
||||||
|
```bash
|
||||||
|
cd ~/.openclaw/workspace/alfred-proxy
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### "ECONNREFUSED" connecting to OpenClaw
|
||||||
|
|
||||||
|
**Check OpenClaw is running:**
|
||||||
|
```bash
|
||||||
|
systemctl --user status openclaw-gateway.service
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check OpenClaw bind mode:**
|
||||||
|
```bash
|
||||||
|
openclaw config get gateway.bind # Should be "loopback"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test OpenClaw directly:**
|
||||||
|
```bash
|
||||||
|
wscat -c ws://127.0.0.1:18789
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Invalid token" error
|
||||||
|
|
||||||
|
**Verify Authentik URL:**
|
||||||
|
```bash
|
||||||
|
curl https://auth.dnspegasus.net/.well-known/openid-configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test token validation:**
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
https://auth.dnspegasus.net/application/o/userinfo/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check Client ID matches:**
|
||||||
|
- `.env` has correct `AUTHENTIK_CLIENT_ID`
|
||||||
|
- Token was issued for the correct client
|
||||||
|
|
||||||
|
### Logs show nothing
|
||||||
|
|
||||||
|
**Check service is running:**
|
||||||
|
```bash
|
||||||
|
systemctl --user is-active alfred-proxy.service
|
||||||
|
```
|
||||||
|
|
||||||
|
**Increase log verbosity:**
|
||||||
|
Edit the service to add `--debug` flag (future enhancement).
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Configure Android app** to use OAuth flow
|
||||||
|
2. **Add HAProxy SSL** for production access
|
||||||
|
3. **Set up monitoring** for the proxy service
|
||||||
|
4. **Configure firewall** rules if exposing externally
|
||||||
|
|
||||||
|
## Security Checklist
|
||||||
|
|
||||||
|
- [ ] OpenClaw bound to localhost only
|
||||||
|
- [ ] Proxy validates all OAuth tokens
|
||||||
|
- [ ] OpenClaw token not exposed to clients
|
||||||
|
- [ ] HTTPS/WSS for external access
|
||||||
|
- [ ] Firewall rules in place
|
||||||
|
- [ ] Monitoring and logs configured
|
||||||
|
- [ ] Authentik user management set up
|
||||||
|
- [ ] Test token revocation works
|
||||||
478
SETUP_COMPLETE.md
Normal file
478
SETUP_COMPLETE.md
Normal file
@@ -0,0 +1,478 @@
|
|||||||
|
# Alfred Proxy - Complete Setup Guide
|
||||||
|
|
||||||
|
**Updated:** 2026-02-04
|
||||||
|
**Status:** ✅ Fully functional with FCM push notifications
|
||||||
|
|
||||||
|
## What This Is
|
||||||
|
|
||||||
|
Alfred Proxy is an authentication and notification bridge that:
|
||||||
|
1. Validates OAuth tokens from the Alfred mobile app (via Authentik)
|
||||||
|
2. Injects OpenClaw gateway tokens (server-side, never exposed)
|
||||||
|
3. Sends Firebase Cloud Messaging push notifications to mobile devices
|
||||||
|
4. Persists FCM tokens across restarts for reliable alarm delivery
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js 24+ installed
|
||||||
|
- OpenClaw gateway running on localhost:18789
|
||||||
|
- Authentik OAuth provider configured
|
||||||
|
- Firebase project with FCM enabled
|
||||||
|
- Google Cloud service account with correct permissions
|
||||||
|
|
||||||
|
### 1. Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.openclaw/workspace/alfred-proxy
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure Firebase (FCM)
|
||||||
|
|
||||||
|
#### a. Create Service Account
|
||||||
|
|
||||||
|
1. Go to: https://console.cloud.google.com/iam-admin/serviceaccounts?project=YOUR_PROJECT_ID
|
||||||
|
2. Click "Create Service Account"
|
||||||
|
3. Name: `alfred-fcm-server`
|
||||||
|
4. Grant role: **Firebase Admin SDK Administrator Service Agent**
|
||||||
|
- This role includes `cloudmessaging.messages.create` permission
|
||||||
|
- Do NOT use "Firebase Cloud Messaging Admin" (legacy API)
|
||||||
|
5. Download JSON key
|
||||||
|
|
||||||
|
#### b. Enable FCM API
|
||||||
|
|
||||||
|
1. Go to: https://console.cloud.google.com/apis/library/fcm.googleapis.com?project=YOUR_PROJECT_ID
|
||||||
|
2. Click "Enable"
|
||||||
|
|
||||||
|
#### c. Place Service Account Key
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp ~/Downloads/your-key.json service-account.json
|
||||||
|
chmod 600 service-account.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Configure Environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy example config
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Edit with your settings
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
**Required settings:**
|
||||||
|
```bash
|
||||||
|
PROXY_PORT=18790
|
||||||
|
OPENCLAW_URL=ws://127.0.0.1:18789
|
||||||
|
OPENCLAW_TOKEN=your-openclaw-token # From ~/.openclaw/openclaw.json
|
||||||
|
AUTHENTIK_URL=https://auth.your-domain.com
|
||||||
|
AUTHENTIK_CLIENT_ID=your-client-id
|
||||||
|
REQUIRE_AUTH=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Start the Proxy
|
||||||
|
|
||||||
|
**Development (auto-reload):**
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
**Production (systemd service):**
|
||||||
|
```bash
|
||||||
|
# Copy service file
|
||||||
|
cp alfred-proxy.service ~/.config/systemd/user/
|
||||||
|
|
||||||
|
# Start and enable
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable alfred-proxy.service
|
||||||
|
systemctl --user start alfred-proxy.service
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
systemctl --user status alfred-proxy.service
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Verify Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check proxy is running
|
||||||
|
curl http://localhost:18790/health
|
||||||
|
# Expected: {"status":"ok","service":"alfred-proxy"}
|
||||||
|
|
||||||
|
# Check logs
|
||||||
|
tail -f /tmp/alfred-proxy.log
|
||||||
|
|
||||||
|
# Look for:
|
||||||
|
# [firebase] Firebase Admin SDK initialized
|
||||||
|
# [alfred-proxy] Service started successfully
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Test FCM Notifications
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Send test alarm
|
||||||
|
./alfred-notify --alarm "Test alarm!"
|
||||||
|
|
||||||
|
# Check logs for success
|
||||||
|
tail -f /tmp/alfred-proxy.log | grep fcm
|
||||||
|
# Expected: [fcm] Successfully sent X message(s)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### ✅ Working Features
|
||||||
|
|
||||||
|
- **OAuth Authentication**: Validates Authentik tokens via userinfo endpoint
|
||||||
|
- **Token Injection**: Injects OpenClaw gateway token server-side
|
||||||
|
- **FCM Push Notifications**: Send notifications even when app is closed
|
||||||
|
- **Token Persistence**: FCM tokens persist across proxy restarts (`fcm-tokens.json`)
|
||||||
|
- **Dual Delivery**: WebSocket (if connected) + FCM (always)
|
||||||
|
- **CLI Wrapper**: `alfred-notify` command for easy notification sending
|
||||||
|
- **Auto-Reconnection**: App reconnects and re-registers FCM token automatically
|
||||||
|
|
||||||
|
### Notification Types
|
||||||
|
|
||||||
|
- **Alarm**: High-priority, wakes device
|
||||||
|
- **Alert**: Standard priority notification
|
||||||
|
- **Silent**: Vibrate only or completely silent
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Send Notification via CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Simple alarm
|
||||||
|
./alfred-notify --alarm "Wake up!"
|
||||||
|
|
||||||
|
# Notification with custom title
|
||||||
|
./alfred-notify --title "Kitchen Timer" "Oven is ready"
|
||||||
|
|
||||||
|
# Silent notification
|
||||||
|
./alfred-notify --no-sound --no-vibrate "Background task complete"
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `./alfred-notify --help` for all options.
|
||||||
|
|
||||||
|
### Send Notification via HTTP API
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:18790/api/notify \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"notificationType": "alarm",
|
||||||
|
"title": "Test",
|
||||||
|
"message": "This is a test alarm",
|
||||||
|
"priority": "high",
|
||||||
|
"sound": true,
|
||||||
|
"vibrate": true
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"clients": 1, // WebSocket clients notified
|
||||||
|
"fcm": 1 // FCM devices notified
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schedule Alarms with Cron
|
||||||
|
|
||||||
|
Use OpenClaw's cron tool to schedule alarms:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Example: 5 minute alarm
|
||||||
|
{
|
||||||
|
"name": "5 minute alarm",
|
||||||
|
"schedule": {
|
||||||
|
"kind": "at",
|
||||||
|
"atMs": Date.now() + (5 * 60 * 1000)
|
||||||
|
},
|
||||||
|
"payload": {
|
||||||
|
"kind": "agentTurn",
|
||||||
|
"message": "Run: alfred-notify --alarm '⏰ 5 minute alarm is up!'",
|
||||||
|
"deliver": false
|
||||||
|
},
|
||||||
|
"sessionTarget": "isolated"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Critical:** Always use `sessionTarget: "isolated"` + `agentTurn` for alarms.
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
### Check Proxy Status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --user status alfred-proxy.service
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Live tail
|
||||||
|
tail -f /tmp/alfred-proxy.log
|
||||||
|
|
||||||
|
# Check for FCM activity
|
||||||
|
grep "fcm" /tmp/alfred-proxy.log | tail -20
|
||||||
|
|
||||||
|
# Check for errors
|
||||||
|
grep "Error\|error" /tmp/alfred-proxy.log | tail -20
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Token Persistence
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat fcm-tokens.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Should show registered FCM tokens:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user-id-hash": [
|
||||||
|
"fcm-token-string"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Monitor Notification Delivery
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Send test
|
||||||
|
./alfred-notify "Test"
|
||||||
|
|
||||||
|
# Check logs
|
||||||
|
tail -f /tmp/alfred-proxy.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Look for:
|
||||||
|
```
|
||||||
|
[notify] Broadcasting notification: type=alert title="Alfred" message="Test"
|
||||||
|
[notify] Sent notification to X connected client(s)
|
||||||
|
[fcm] Sending push notification to Y registered device(s)
|
||||||
|
[fcm] Successfully sent Y message(s)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Permission Denied Error
|
||||||
|
|
||||||
|
**Error:**
|
||||||
|
```
|
||||||
|
[fcm] Error: Permission 'cloudmessaging.messages.create' denied
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Verify service account role is **Firebase Admin SDK Administrator Service Agent**
|
||||||
|
- Go to: https://console.cloud.google.com/iam-admin/iam
|
||||||
|
- Find service account: `alfred-fcm-server@...`
|
||||||
|
- Should have role: `roles/firebase.sdkAdminServiceAgent`
|
||||||
|
2. Verify FCM API is enabled
|
||||||
|
3. Regenerate service account key if > 1 hour old
|
||||||
|
4. Restart proxy after updating key
|
||||||
|
|
||||||
|
### No Tokens Registered
|
||||||
|
|
||||||
|
**Error:**
|
||||||
|
```
|
||||||
|
[fcm] No FCM tokens registered, skipping push notification
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Open Alfred mobile app
|
||||||
|
2. Verify connection (should show "Connected ✅")
|
||||||
|
3. Check logs for token registration:
|
||||||
|
```
|
||||||
|
[fcm] Registering token for user...
|
||||||
|
[fcm] Saved tokens to disk
|
||||||
|
```
|
||||||
|
4. Verify `fcm-tokens.json` file exists and has content
|
||||||
|
|
||||||
|
### Tokens Lost After Restart
|
||||||
|
|
||||||
|
**This should NOT happen if setup is correct.**
|
||||||
|
|
||||||
|
If tokens are lost:
|
||||||
|
1. Check `fcm-tokens.json` exists in proxy directory
|
||||||
|
2. Check file permissions: `chmod 600 fcm-tokens.json`
|
||||||
|
3. Verify logs show: `[fcm] Loaded X token(s) for Y user(s) from disk`
|
||||||
|
4. Check app sends token on every connection (should be automatic)
|
||||||
|
|
||||||
|
### OAuth Token Expired
|
||||||
|
|
||||||
|
**Error:**
|
||||||
|
```
|
||||||
|
[auth] Authentik validation failed: 401
|
||||||
|
[proxy] Invalid OAuth token, rejecting connection
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. App needs to implement token refresh (future fix)
|
||||||
|
2. For now: logout and login again in the app
|
||||||
|
3. Or disable auth temporarily: `REQUIRE_AUTH=false` in `.env`
|
||||||
|
|
||||||
|
### App Not Receiving Notifications
|
||||||
|
|
||||||
|
**Checklist:**
|
||||||
|
1. ✅ Proxy running? `systemctl --user status alfred-proxy.service`
|
||||||
|
2. ✅ FCM tokens registered? `cat fcm-tokens.json`
|
||||||
|
3. ✅ Service account has correct role? Check IAM
|
||||||
|
4. ✅ FCM API enabled? Check APIs & Services
|
||||||
|
5. ✅ Test notification succeeds? `./alfred-notify "Test"`
|
||||||
|
6. ✅ Logs show success? `grep "Successfully sent" /tmp/alfred-proxy.log`
|
||||||
|
|
||||||
|
If all checks pass but still no notifications:
|
||||||
|
- Check app is installed and has notification permissions
|
||||||
|
- Check device has network connectivity
|
||||||
|
- Check Firebase Cloud Messaging isn't blocked by firewall
|
||||||
|
- Try uninstall/reinstall app to get fresh FCM token
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────┐
|
||||||
|
│ Alfred Mobile App │
|
||||||
|
│ (Android/OAuth) │
|
||||||
|
└──────────┬───────────┘
|
||||||
|
│ wss:// + OAuth token
|
||||||
|
▼
|
||||||
|
┌──────────────────────┐
|
||||||
|
│ Alfred Proxy │
|
||||||
|
│ - Validate OAuth │
|
||||||
|
│ - Inject OpenClaw │
|
||||||
|
│ token │
|
||||||
|
│ - Store FCM tokens │
|
||||||
|
│ - Send FCM push │
|
||||||
|
└──────────┬───────────┘
|
||||||
|
│
|
||||||
|
┌──────┴───────┐
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌────────┐ ┌────────────┐
|
||||||
|
│OpenClaw│ │ Firebase │
|
||||||
|
│Gateway │ │ FCM │
|
||||||
|
│(local) │ └─────┬──────┘
|
||||||
|
└────────┘ │
|
||||||
|
▼
|
||||||
|
┌──────────────┐
|
||||||
|
│ Mobile Device│
|
||||||
|
│ (push notif) │
|
||||||
|
└──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files & Directories
|
||||||
|
|
||||||
|
```
|
||||||
|
alfred-proxy/
|
||||||
|
├── server.js # Main proxy server
|
||||||
|
├── alfred-notify # CLI wrapper for notifications
|
||||||
|
├── .env # Configuration (git-ignored)
|
||||||
|
├── service-account.json # Firebase credentials (git-ignored)
|
||||||
|
├── fcm-tokens.json # Persisted FCM tokens (git-ignored)
|
||||||
|
├── package.json # Dependencies
|
||||||
|
├── alfred-proxy.service # Systemd service file
|
||||||
|
├── README.md # Project overview
|
||||||
|
├── SETUP_COMPLETE.md # This file
|
||||||
|
├── FCM_SETUP.md # Firebase setup (see alfred-mobile/)
|
||||||
|
└── /tmp/alfred-proxy.log # Runtime logs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
### Sensitive Files (Never Commit)
|
||||||
|
|
||||||
|
- `.env` - Contains API tokens
|
||||||
|
- `service-account.json` - Firebase credentials
|
||||||
|
- `fcm-tokens.json` - User device tokens
|
||||||
|
|
||||||
|
All added to `.gitignore`.
|
||||||
|
|
||||||
|
### Best Practices
|
||||||
|
|
||||||
|
1. **Restrict service account permissions**: Use minimal role (Firebase Admin SDK Administrator Service Agent)
|
||||||
|
2. **Rotate keys regularly**: Generate new service account key every 90 days
|
||||||
|
3. **File permissions**: `chmod 600` for sensitive files
|
||||||
|
4. **Network isolation**: OpenClaw runs on localhost only, not exposed
|
||||||
|
5. **OAuth validation**: All connections validated with Authentik before access
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
### Resource Usage
|
||||||
|
|
||||||
|
- **Memory**: ~50-100MB RSS
|
||||||
|
- **CPU**: <1% idle, ~5% during notification bursts
|
||||||
|
- **Network**: Minimal (WebSocket keepalives + FCM API calls)
|
||||||
|
|
||||||
|
### Scalability
|
||||||
|
|
||||||
|
- Supports multiple connected devices simultaneously
|
||||||
|
- FCM batch sending for efficiency
|
||||||
|
- Token persistence reduces startup overhead
|
||||||
|
|
||||||
|
### Monitoring
|
||||||
|
|
||||||
|
Monitor with:
|
||||||
|
```bash
|
||||||
|
# Process stats
|
||||||
|
ps aux | grep "node server.js"
|
||||||
|
|
||||||
|
# Connection count
|
||||||
|
ss -tn | grep :18790 | wc -l
|
||||||
|
|
||||||
|
# Log size
|
||||||
|
du -h /tmp/alfred-proxy.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
### Update Proxy Code
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.openclaw/workspace/alfred-proxy
|
||||||
|
git pull # If using git
|
||||||
|
npm install # If dependencies changed
|
||||||
|
systemctl --user restart alfred-proxy.service
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Service Account Key
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download new key from Google Cloud Console
|
||||||
|
cp ~/Downloads/new-key.json service-account.json
|
||||||
|
chmod 600 service-account.json
|
||||||
|
systemctl --user restart alfred-proxy.service
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
tail -f /tmp/alfred-proxy.log | grep firebase
|
||||||
|
# Should show: [firebase] Firebase Admin SDK initialized
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support & Documentation
|
||||||
|
|
||||||
|
- **FCM Setup**: See `../alfred-mobile/FCM_SETUP.md`
|
||||||
|
- **Agent Integration**: See `../alfred-mobile/AGENT_TOOLS.md`
|
||||||
|
- **OpenClaw Docs**: https://docs.openclaw.ai
|
||||||
|
- **Firebase Docs**: https://firebase.google.com/docs/cloud-messaging
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
### 2026-02-04: FCM Token Persistence
|
||||||
|
- Added token save/load to `fcm-tokens.json`
|
||||||
|
- App sends token on every connection
|
||||||
|
- Fixed Firebase IAM permissions (correct role)
|
||||||
|
- Created `alfred-notify` CLI wrapper
|
||||||
|
- Updated all documentation
|
||||||
|
|
||||||
|
### 2026-02-03: Initial Release
|
||||||
|
- OAuth authentication via Authentik
|
||||||
|
- OpenClaw token injection
|
||||||
|
- Basic FCM notification support
|
||||||
|
- WebSocket proxy functionality
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status**: ✅ Production Ready
|
||||||
|
**Last Updated**: 2026-02-04
|
||||||
|
**Maintainer**: Josh (Shadow)
|
||||||
220
STATUS.md
Normal file
220
STATUS.md
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
# Alfred Proxy Setup Status
|
||||||
|
|
||||||
|
## ✅ Completed Steps
|
||||||
|
|
||||||
|
1. **OpenClaw switched to localhost** ✅
|
||||||
|
- Bind mode: `loopback`
|
||||||
|
- Port: `18789`
|
||||||
|
- Status: Running
|
||||||
|
|
||||||
|
2. **Proxy service installed** ✅
|
||||||
|
- Location: `~/.openclaw/workspace/alfred-proxy/`
|
||||||
|
- Configuration: `.env` created with Client ID
|
||||||
|
- Dependencies: Installed
|
||||||
|
|
||||||
|
3. **Proxy running** ✅
|
||||||
|
- Port: `18790`
|
||||||
|
- Health check: http://localhost:18790/health → OK
|
||||||
|
- OpenClaw connection: Configured
|
||||||
|
|
||||||
|
4. **HAProxy configured** ✅
|
||||||
|
- Subdomain: `alfred-app.dnspegasus.net`
|
||||||
|
- Backend: `192.168.1.169:18790`
|
||||||
|
- SSL: Configured
|
||||||
|
|
||||||
|
## ⚠️ Pending: Windows Firewall
|
||||||
|
|
||||||
|
**The proxy needs to be accessible from HAProxy (192.168.1.20)**
|
||||||
|
|
||||||
|
### Open Firewall (Run as Administrator)
|
||||||
|
|
||||||
|
**Option 1: Using the batch file**
|
||||||
|
1. Open File Explorer
|
||||||
|
2. Navigate to: `\\wsl.localhost\Ubuntu-22.04\home\jknapp\.openclaw\workspace\alfred-proxy\`
|
||||||
|
3. Right-click `open-firewall.bat`
|
||||||
|
4. Select **"Run as administrator"**
|
||||||
|
|
||||||
|
**Option 2: Using PowerShell (Admin)**
|
||||||
|
```powershell
|
||||||
|
New-NetFirewallRule -DisplayName "Alfred Proxy" -Direction Inbound -LocalPort 18790 -Protocol TCP -Action Allow
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 3: Using Command Prompt (Admin)**
|
||||||
|
```cmd
|
||||||
|
netsh advfirewall firewall add rule name="Alfred Proxy" dir=in action=allow protocol=TCP localport=18790
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify Firewall is Open
|
||||||
|
|
||||||
|
After opening the firewall, test from HAProxy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh root@192.168.1.20 'curl -s http://192.168.1.169:18790/health'
|
||||||
|
```
|
||||||
|
|
||||||
|
Should return:
|
||||||
|
```json
|
||||||
|
{"status":"ok","service":"alfred-proxy"}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
### 1. Local Tests (Already Passing ✅)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Proxy health
|
||||||
|
curl http://localhost:18790/health
|
||||||
|
# ✅ {"status":"ok","service":"alfred-proxy"}
|
||||||
|
|
||||||
|
# Proxy accessible on network
|
||||||
|
curl http://192.168.1.169:18790/health
|
||||||
|
# ✅ {"status":"ok","service":"alfred-proxy"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. HAProxy Connection (After firewall)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From HAProxy server
|
||||||
|
ssh root@192.168.1.20 'curl -s http://192.168.1.169:18790/health'
|
||||||
|
# Should return: {"status":"ok","service":"alfred-proxy"}
|
||||||
|
|
||||||
|
# From outside (browser redirect test)
|
||||||
|
curl -I https://alfred-app.dnspegasus.net
|
||||||
|
# Should return: HTTP/2 200 with HTML redirect
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. WebSocket Test (After OAuth token)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get OAuth token from Authentik first
|
||||||
|
# Then test WebSocket connection:
|
||||||
|
wscat -c "wss://alfred-app.dnspegasus.net" -H "Authorization: Bearer YOUR_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current Configuration
|
||||||
|
|
||||||
|
### Proxy (.env)
|
||||||
|
```
|
||||||
|
PROXY_PORT=18790
|
||||||
|
OPENCLAW_URL=ws://127.0.0.1:18789
|
||||||
|
OPENCLAW_TOKEN=9b87d15fee3922ecfbe77b0ea1744851757cda618beceeba
|
||||||
|
AUTHENTIK_URL=https://auth.dnspegasus.net
|
||||||
|
AUTHENTIK_CLIENT_ID=QeSNaZPqZUz5pPClZMA2bakSsddkStiEhqbE4QZR
|
||||||
|
REQUIRE_AUTH=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### OpenClaw Gateway
|
||||||
|
```
|
||||||
|
gateway.bind = "loopback"
|
||||||
|
gateway.port = 18789
|
||||||
|
gateway.auth.token = "9b87d15fee3922ecfbe77b0ea1744851757cda618beceeba"
|
||||||
|
```
|
||||||
|
|
||||||
|
### HAProxy Backend
|
||||||
|
```
|
||||||
|
Server: 192.168.1.169:18790
|
||||||
|
Domain: alfred-app.dnspegasus.net
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Install Proxy as Systemd Service (Recommended)
|
||||||
|
|
||||||
|
Once firewall is confirmed working, install as a service:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.openclaw/workspace/alfred-proxy
|
||||||
|
|
||||||
|
# Install service
|
||||||
|
mkdir -p ~/.config/systemd/user
|
||||||
|
cp alfred-proxy.service ~/.config/systemd/user/
|
||||||
|
|
||||||
|
# Create override with Client ID
|
||||||
|
mkdir -p ~/.config/systemd/user/alfred-proxy.service.d
|
||||||
|
cat > ~/.config/systemd/user/alfred-proxy.service.d/override.conf << 'EOF'
|
||||||
|
[Service]
|
||||||
|
Environment="AUTHENTIK_CLIENT_ID=QeSNaZPqZUz5pPClZMA2bakSsddkStiEhqbE4QZR"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Enable and start
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable alfred-proxy.service
|
||||||
|
systemctl --user start alfred-proxy.service
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
systemctl --user status alfred-proxy.service
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
journalctl --user -u alfred-proxy.service -f
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Android App Configuration
|
||||||
|
|
||||||
|
Once the proxy is fully working, configure your Android app:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// OAuthConfig.kt
|
||||||
|
object OAuthConfig {
|
||||||
|
const val AUTHENTIK_URL = "https://auth.dnspegasus.net"
|
||||||
|
const val CLIENT_ID = "QeSNaZPqZUz5pPClZMA2bakSsddkStiEhqbE4QZR"
|
||||||
|
const val REDIRECT_URI = "alfredmobile://oauth/callback"
|
||||||
|
const val SCOPE = "openid profile email"
|
||||||
|
|
||||||
|
const val AUTHORIZATION_ENDPOINT = "$AUTHENTIK_URL/application/o/authorize/"
|
||||||
|
const val TOKEN_ENDPOINT = "$AUTHENTIK_URL/application/o/token/"
|
||||||
|
const val USERINFO_ENDPOINT = "$AUTHENTIK_URL/application/o/userinfo/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// AlfredConfig.kt
|
||||||
|
object AlfredConfig {
|
||||||
|
const val GATEWAY_URL = "wss://alfred-app.dnspegasus.net"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Open Windows Firewall** (see instructions above)
|
||||||
|
2. **Test HAProxy connection** (verify backend is reachable)
|
||||||
|
3. **Test browser redirect** (https://alfred-app.dnspegasus.net → should redirect)
|
||||||
|
4. **Install as systemd service** (for auto-start)
|
||||||
|
5. **Implement OAuth in Android app** (see DEPLOYMENT.md for OAuth flow)
|
||||||
|
6. **Test end-to-end** (OAuth → WebSocket → OpenClaw)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Proxy won't connect to HAProxy
|
||||||
|
|
||||||
|
**Check firewall:**
|
||||||
|
```bash
|
||||||
|
# From HAProxy
|
||||||
|
ssh root@192.168.1.20 'curl -v http://192.168.1.169:18790/health'
|
||||||
|
```
|
||||||
|
|
||||||
|
If it times out, firewall is blocking.
|
||||||
|
|
||||||
|
### "503 Service Unavailable" from HAProxy
|
||||||
|
|
||||||
|
HAProxy can't reach the backend. Possible causes:
|
||||||
|
- Firewall blocking port 18790
|
||||||
|
- Proxy not running
|
||||||
|
- Wrong IP in HAProxy config
|
||||||
|
|
||||||
|
### Invalid OAuth token
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test token with Authentik
|
||||||
|
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
https://auth.dnspegasus.net/application/o/userinfo/
|
||||||
|
```
|
||||||
|
|
||||||
|
Should return user info if token is valid.
|
||||||
54
SYSTEMD.md
Normal file
54
SYSTEMD.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Alfred Proxy Systemd Service
|
||||||
|
|
||||||
|
## Service Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Status
|
||||||
|
systemctl --user status alfred-proxy
|
||||||
|
|
||||||
|
# Start
|
||||||
|
systemctl --user start alfred-proxy
|
||||||
|
|
||||||
|
# Stop
|
||||||
|
systemctl --user stop alfred-proxy
|
||||||
|
|
||||||
|
# Restart
|
||||||
|
systemctl --user restart alfred-proxy
|
||||||
|
|
||||||
|
# Enable (auto-start)
|
||||||
|
systemctl --user enable alfred-proxy
|
||||||
|
|
||||||
|
# Disable (no auto-start)
|
||||||
|
systemctl --user disable alfred-proxy
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
journalctl --user -u alfred-proxy -f
|
||||||
|
|
||||||
|
# View proxy-specific logs
|
||||||
|
tail -f /tmp/alfred-proxy.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## Auto-Restart
|
||||||
|
|
||||||
|
The service is configured with:
|
||||||
|
- **Restart=always** - Always restart on failure
|
||||||
|
- **RestartSec=5** - Wait 5 seconds between restarts
|
||||||
|
|
||||||
|
## Error Handling Improvements
|
||||||
|
|
||||||
|
Added to `server.js`:
|
||||||
|
1. **Global exception handler** - Catches uncaught exceptions
|
||||||
|
2. **Global rejection handler** - Catches unhandled promise rejections
|
||||||
|
3. **Proper interval cleanup** - Clears ping intervals on disconnection
|
||||||
|
|
||||||
|
These prevent the service from crashing on unexpected errors.
|
||||||
|
|
||||||
|
## Service File Location
|
||||||
|
|
||||||
|
`~/.config/systemd/user/alfred-proxy.service`
|
||||||
|
|
||||||
|
To modify: edit the file, then run:
|
||||||
|
```bash
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user restart alfred-proxy
|
||||||
|
```
|
||||||
BIN
Screenshot 2026-02-04 061639.png
Normal file
BIN
Screenshot 2026-02-04 061639.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 361 KiB |
178
alfred-notify
Executable file
178
alfred-notify
Executable file
@@ -0,0 +1,178 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# alfred-notify - Send notifications and alarms to Alfred mobile app
|
||||||
|
# Usage: alfred-notify [OPTIONS] MESSAGE
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
PROXY_URL="${ALFRED_PROXY_URL:-http://localhost:18790}"
|
||||||
|
NOTIFICATION_TYPE="alert"
|
||||||
|
TITLE="AI Assistant"
|
||||||
|
PRIORITY="default"
|
||||||
|
SOUND="true"
|
||||||
|
VIBRATE="true"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
usage() {
|
||||||
|
cat << EOF
|
||||||
|
Usage: alfred-notify [OPTIONS] MESSAGE
|
||||||
|
|
||||||
|
Send notifications and alarms to Alfred mobile app via proxy.
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
-a, --alarm Send as alarm (high priority, full screen)
|
||||||
|
-t, --title TITLE Notification title (default: "AI Assistant")
|
||||||
|
-p, --priority LEVEL Priority: low, default, high (default: default)
|
||||||
|
-s, --no-sound Disable sound
|
||||||
|
-v, --no-vibrate Disable vibration
|
||||||
|
--user USER_ID Send only to devices for this user (OAuth sub)
|
||||||
|
-u, --url URL Proxy URL (default: http://localhost:18790)
|
||||||
|
-h, --help Show this help
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
# Simple notification
|
||||||
|
alfred-notify "Reminder: Check the oven"
|
||||||
|
|
||||||
|
# Alarm with custom title
|
||||||
|
alfred-notify --alarm --title "Wake Up!" "Time to get up"
|
||||||
|
|
||||||
|
# Quiet notification
|
||||||
|
alfred-notify --no-sound --no-vibrate "Silent message"
|
||||||
|
|
||||||
|
# Custom proxy URL
|
||||||
|
alfred-notify --url http://192.168.1.169:18790 "From remote server"
|
||||||
|
|
||||||
|
ENVIRONMENT:
|
||||||
|
ALFRED_PROXY_URL Override default proxy URL
|
||||||
|
|
||||||
|
NOTES:
|
||||||
|
- Alarms use high priority and are more intrusive
|
||||||
|
- Messages sent via FCM will wake the device
|
||||||
|
- Token persistence ensures delivery even after proxy restarts
|
||||||
|
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
MESSAGE=""
|
||||||
|
USER_ID=""
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
-a|--alarm)
|
||||||
|
NOTIFICATION_TYPE="alarm"
|
||||||
|
PRIORITY="high"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-t|--title)
|
||||||
|
TITLE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-p|--priority)
|
||||||
|
PRIORITY="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-s|--no-sound)
|
||||||
|
SOUND="false"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-v|--no-vibrate)
|
||||||
|
VIBRATE="false"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--user)
|
||||||
|
USER_ID="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-u|--url)
|
||||||
|
PROXY_URL="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
-*)
|
||||||
|
echo -e "${RED}Error: Unknown option: $1${NC}" >&2
|
||||||
|
echo "Use --help for usage information" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
MESSAGE="$1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Validate message
|
||||||
|
if [[ -z "$MESSAGE" ]]; then
|
||||||
|
echo -e "${RED}Error: MESSAGE is required${NC}" >&2
|
||||||
|
echo "Use: alfred-notify [OPTIONS] MESSAGE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build JSON payload
|
||||||
|
if [[ -n "$USER_ID" ]]; then
|
||||||
|
JSON_PAYLOAD=$(cat <<EOF
|
||||||
|
{
|
||||||
|
"notificationType": "$NOTIFICATION_TYPE",
|
||||||
|
"title": "$TITLE",
|
||||||
|
"message": "$MESSAGE",
|
||||||
|
"priority": "$PRIORITY",
|
||||||
|
"sound": $SOUND,
|
||||||
|
"vibrate": $VIBRATE,
|
||||||
|
"userId": "$USER_ID"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
else
|
||||||
|
JSON_PAYLOAD=$(cat <<EOF
|
||||||
|
{
|
||||||
|
"notificationType": "$NOTIFICATION_TYPE",
|
||||||
|
"title": "$TITLE",
|
||||||
|
"message": "$MESSAGE",
|
||||||
|
"priority": "$PRIORITY",
|
||||||
|
"sound": $SOUND,
|
||||||
|
"vibrate": $VIBRATE
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Send notification
|
||||||
|
RESPONSE=$(curl -s -X POST "$PROXY_URL/api/notify" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$JSON_PAYLOAD" \
|
||||||
|
-w "\n%{http_code}")
|
||||||
|
|
||||||
|
# Parse response
|
||||||
|
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
||||||
|
BODY=$(echo "$RESPONSE" | head -n-1)
|
||||||
|
|
||||||
|
# Check result
|
||||||
|
if [[ "$HTTP_CODE" == "200" ]]; then
|
||||||
|
# Parse success response
|
||||||
|
CLIENTS=$(echo "$BODY" | grep -o '"clients":[0-9]*' | cut -d: -f2)
|
||||||
|
FCM=$(echo "$BODY" | grep -o '"fcm":[0-9]*' | cut -d: -f2)
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓ Notification sent${NC}"
|
||||||
|
echo " Type: $NOTIFICATION_TYPE"
|
||||||
|
echo " WebSocket clients: $CLIENTS"
|
||||||
|
echo " FCM devices: $FCM"
|
||||||
|
|
||||||
|
if [[ "$CLIENTS" == "0" ]] && [[ "$FCM" == "0" ]]; then
|
||||||
|
echo -e "${YELLOW}⚠ Warning: No devices received notification${NC}" >&2
|
||||||
|
echo " Make sure Alfred app is connected or FCM token is registered" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗ Failed to send notification${NC}" >&2
|
||||||
|
echo " HTTP Status: $HTTP_CODE" >&2
|
||||||
|
echo " Response: $BODY" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
178
alfred-notify.backup
Executable file
178
alfred-notify.backup
Executable file
@@ -0,0 +1,178 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# alfred-notify - Send notifications and alarms to Alfred mobile app
|
||||||
|
# Usage: alfred-notify [OPTIONS] MESSAGE
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
PROXY_URL="${ALFRED_PROXY_URL:-http://localhost:18790}"
|
||||||
|
NOTIFICATION_TYPE="alert"
|
||||||
|
TITLE="Alfred"
|
||||||
|
PRIORITY="default"
|
||||||
|
SOUND="true"
|
||||||
|
VIBRATE="true"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
usage() {
|
||||||
|
cat << EOF
|
||||||
|
Usage: alfred-notify [OPTIONS] MESSAGE
|
||||||
|
|
||||||
|
Send notifications and alarms to Alfred mobile app via proxy.
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
-a, --alarm Send as alarm (high priority, full screen)
|
||||||
|
-t, --title TITLE Notification title (default: "Alfred")
|
||||||
|
-p, --priority LEVEL Priority: low, default, high (default: default)
|
||||||
|
-s, --no-sound Disable sound
|
||||||
|
-v, --no-vibrate Disable vibration
|
||||||
|
--user USER_ID Send only to devices for this user (OAuth sub)
|
||||||
|
-u, --url URL Proxy URL (default: http://localhost:18790)
|
||||||
|
-h, --help Show this help
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
# Simple notification
|
||||||
|
alfred-notify "Reminder: Check the oven"
|
||||||
|
|
||||||
|
# Alarm with custom title
|
||||||
|
alfred-notify --alarm --title "Wake Up!" "Time to get up"
|
||||||
|
|
||||||
|
# Quiet notification
|
||||||
|
alfred-notify --no-sound --no-vibrate "Silent message"
|
||||||
|
|
||||||
|
# Custom proxy URL
|
||||||
|
alfred-notify --url http://192.168.1.169:18790 "From remote server"
|
||||||
|
|
||||||
|
ENVIRONMENT:
|
||||||
|
ALFRED_PROXY_URL Override default proxy URL
|
||||||
|
|
||||||
|
NOTES:
|
||||||
|
- Alarms use high priority and are more intrusive
|
||||||
|
- Messages sent via FCM will wake the device
|
||||||
|
- Token persistence ensures delivery even after proxy restarts
|
||||||
|
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
MESSAGE=""
|
||||||
|
USER_ID=""
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
-a|--alarm)
|
||||||
|
NOTIFICATION_TYPE="alarm"
|
||||||
|
PRIORITY="high"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-t|--title)
|
||||||
|
TITLE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-p|--priority)
|
||||||
|
PRIORITY="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-s|--no-sound)
|
||||||
|
SOUND="false"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-v|--no-vibrate)
|
||||||
|
VIBRATE="false"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--user)
|
||||||
|
USER_ID="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-u|--url)
|
||||||
|
PROXY_URL="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
-*)
|
||||||
|
echo -e "${RED}Error: Unknown option: $1${NC}" >&2
|
||||||
|
echo "Use --help for usage information" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
MESSAGE="$1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Validate message
|
||||||
|
if [[ -z "$MESSAGE" ]]; then
|
||||||
|
echo -e "${RED}Error: MESSAGE is required${NC}" >&2
|
||||||
|
echo "Use: alfred-notify [OPTIONS] MESSAGE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build JSON payload
|
||||||
|
if [[ -n "$USER_ID" ]]; then
|
||||||
|
JSON_PAYLOAD=$(cat <<EOF
|
||||||
|
{
|
||||||
|
"notificationType": "$NOTIFICATION_TYPE",
|
||||||
|
"title": "$TITLE",
|
||||||
|
"message": "$MESSAGE",
|
||||||
|
"priority": "$PRIORITY",
|
||||||
|
"sound": $SOUND,
|
||||||
|
"vibrate": $VIBRATE,
|
||||||
|
"userId": "$USER_ID"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
else
|
||||||
|
JSON_PAYLOAD=$(cat <<EOF
|
||||||
|
{
|
||||||
|
"notificationType": "$NOTIFICATION_TYPE",
|
||||||
|
"title": "$TITLE",
|
||||||
|
"message": "$MESSAGE",
|
||||||
|
"priority": "$PRIORITY",
|
||||||
|
"sound": $SOUND,
|
||||||
|
"vibrate": $VIBRATE
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Send notification
|
||||||
|
RESPONSE=$(curl -s -X POST "$PROXY_URL/api/notify" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$JSON_PAYLOAD" \
|
||||||
|
-w "\n%{http_code}")
|
||||||
|
|
||||||
|
# Parse response
|
||||||
|
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
||||||
|
BODY=$(echo "$RESPONSE" | head -n-1)
|
||||||
|
|
||||||
|
# Check result
|
||||||
|
if [[ "$HTTP_CODE" == "200" ]]; then
|
||||||
|
# Parse success response
|
||||||
|
CLIENTS=$(echo "$BODY" | grep -o '"clients":[0-9]*' | cut -d: -f2)
|
||||||
|
FCM=$(echo "$BODY" | grep -o '"fcm":[0-9]*' | cut -d: -f2)
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓ Notification sent${NC}"
|
||||||
|
echo " Type: $NOTIFICATION_TYPE"
|
||||||
|
echo " WebSocket clients: $CLIENTS"
|
||||||
|
echo " FCM devices: $FCM"
|
||||||
|
|
||||||
|
if [[ "$CLIENTS" == "0" ]] && [[ "$FCM" == "0" ]]; then
|
||||||
|
echo -e "${YELLOW}⚠ Warning: No devices received notification${NC}" >&2
|
||||||
|
echo " Make sure Alfred app is connected or FCM token is registered" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗ Failed to send notification${NC}" >&2
|
||||||
|
echo " HTTP Status: $HTTP_CODE" >&2
|
||||||
|
echo " Response: $BODY" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
33
alfred-proxy.service
Normal file
33
alfred-proxy.service
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Alfred Authentication Proxy
|
||||||
|
Documentation=file:///home/jknapp/.openclaw/workspace/alfred-proxy/README.md
|
||||||
|
After=network.target openclaw-gateway.service
|
||||||
|
Wants=openclaw-gateway.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=/home/jknapp/.openclaw/workspace/alfred-proxy
|
||||||
|
ExecStart=/home/jknapp/.nvm/versions/node/v24.13.0/bin/node server.js
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
# Environment (override with ~/.config/systemd/user/alfred-proxy.service.d/override.conf)
|
||||||
|
Environment="NODE_ENV=production"
|
||||||
|
Environment="PROXY_PORT=18790"
|
||||||
|
Environment="OPENCLAW_URL=ws://127.0.0.1:18789"
|
||||||
|
Environment="OPENCLAW_TOKEN=9b87d15fee3922ecfbe77b0ea1744851757cda618beceeba"
|
||||||
|
Environment="AUTHENTIK_URL=https://auth.dnspegasus.net"
|
||||||
|
Environment="AUTHENTIK_CLIENT_ID="
|
||||||
|
Environment="REQUIRE_AUTH=true"
|
||||||
|
|
||||||
|
# Security hardening
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=read-only
|
||||||
|
ReadWritePaths=/home/jknapp/.openclaw/workspace/alfred-proxy
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
21
open-firewall.bat
Normal file
21
open-firewall.bat
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
@echo off
|
||||||
|
REM Open Windows Firewall for Alfred Proxy
|
||||||
|
REM Right-click this file and select "Run as administrator"
|
||||||
|
|
||||||
|
echo Opening firewall for Alfred Proxy (port 18790)...
|
||||||
|
|
||||||
|
netsh advfirewall firewall delete rule name="Alfred Proxy" >nul 2>&1
|
||||||
|
netsh advfirewall firewall add rule name="Alfred Proxy" dir=in action=allow protocol=TCP localport=18790
|
||||||
|
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
echo.
|
||||||
|
echo SUCCESS: Firewall rule created!
|
||||||
|
echo Port 18790 is now open for incoming connections
|
||||||
|
) else (
|
||||||
|
echo.
|
||||||
|
echo ERROR: Failed to create firewall rule
|
||||||
|
echo Make sure you ran this as Administrator
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
30
open-firewall.ps1
Normal file
30
open-firewall.ps1
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Open Windows Firewall for Alfred Proxy
|
||||||
|
# Run as Administrator
|
||||||
|
|
||||||
|
Write-Host "Opening firewall for Alfred Proxy (port 18790)..." -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Remove old rule if it exists
|
||||||
|
Get-NetFirewallRule -DisplayName "Alfred Proxy" -ErrorAction SilentlyContinue | Remove-NetFirewallRule
|
||||||
|
|
||||||
|
# Create new rule
|
||||||
|
New-NetFirewallRule `
|
||||||
|
-DisplayName "Alfred Proxy" `
|
||||||
|
-Direction Inbound `
|
||||||
|
-LocalPort 18790 `
|
||||||
|
-Protocol TCP `
|
||||||
|
-Action Allow `
|
||||||
|
-Profile Any `
|
||||||
|
-Enabled True
|
||||||
|
|
||||||
|
Write-Host "✅ Firewall rule created successfully!" -ForegroundColor Green
|
||||||
|
Write-Host " Port 18790 is now open for incoming connections" -ForegroundColor Gray
|
||||||
|
|
||||||
|
# Test if port is listening
|
||||||
|
Write-Host "`nTesting if proxy is listening..." -ForegroundColor Cyan
|
||||||
|
$listening = Get-NetTCPConnection -LocalPort 18790 -ErrorAction SilentlyContinue
|
||||||
|
if ($listening) {
|
||||||
|
Write-Host "✅ Proxy is listening on port 18790" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "⚠️ No service is listening on port 18790 yet" -ForegroundColor Yellow
|
||||||
|
Write-Host " Start the proxy with: cd ~/.openclaw/workspace/alfred-proxy && npm start" -ForegroundColor Gray
|
||||||
|
}
|
||||||
2820
package-lock.json
generated
Normal file
2820
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
package.json
Normal file
19
package.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "alfred-proxy",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Authentication proxy for Alfred mobile app",
|
||||||
|
"main": "server.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "node --watch server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.19.2",
|
||||||
|
"firebase-admin": "^13.6.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"multer": "^2.0.2",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
|
"ws": "^8.18.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
937
server.js
Normal file
937
server.js
Normal file
@@ -0,0 +1,937 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import express from 'express';
|
||||||
|
import { WebSocketServer, WebSocket } from 'ws';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
import admin from 'firebase-admin';
|
||||||
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from 'fs';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname, join, basename } from 'path';
|
||||||
|
import multer from 'multer';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
|
||||||
|
// Get current directory for ES modules
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
// Initialize Firebase Admin SDK
|
||||||
|
const serviceAccount = JSON.parse(
|
||||||
|
readFileSync(join(__dirname, 'service-account.json'), 'utf8')
|
||||||
|
);
|
||||||
|
|
||||||
|
admin.initializeApp({
|
||||||
|
credential: admin.credential.cert(serviceAccount)
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[firebase] Firebase Admin SDK initialized');
|
||||||
|
|
||||||
|
// ElevenLabs API key for TTS
|
||||||
|
const ELEVENLABS_API_KEY = process.env.ELEVENLABS_API_KEY || '';
|
||||||
|
|
||||||
|
// Configuration (use environment variables)
|
||||||
|
const config = {
|
||||||
|
port: parseInt(process.env.PROXY_PORT || '18790', 10),
|
||||||
|
openclawUrl: process.env.OPENCLAW_URL || 'ws://127.0.0.1:18789',
|
||||||
|
openclawToken: process.env.OPENCLAW_TOKEN || '',
|
||||||
|
authentikUrl: process.env.AUTHENTIK_URL || '',
|
||||||
|
authentikClientId: process.env.AUTHENTIK_CLIENT_ID || '',
|
||||||
|
requireAuth: process.env.REQUIRE_AUTH !== 'false',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Health check endpoint and notification API
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
app.get('/health', (req, res) => {
|
||||||
|
res.json({ status: 'ok', service: 'alfred-proxy' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Track connected mobile clients for notifications
|
||||||
|
const connectedClients = new Set();
|
||||||
|
|
||||||
|
// Track FCM tokens by user ID
|
||||||
|
const fcmTokens = new Map(); // userId -> Set of FCM tokens
|
||||||
|
|
||||||
|
// Token persistence file
|
||||||
|
const tokensFile = join(__dirname, 'fcm-tokens.json');
|
||||||
|
|
||||||
|
// User preferences storage
|
||||||
|
const usersDir = join(__dirname, 'users');
|
||||||
|
const userPreferences = new Map(); // userId -> preferences object
|
||||||
|
|
||||||
|
// Ensure users directory exists
|
||||||
|
if (!existsSync(usersDir)) {
|
||||||
|
mkdirSync(usersDir, { recursive: true });
|
||||||
|
console.log(`[prefs] Created users directory: ${usersDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load user preferences from disk
|
||||||
|
function loadUserPreferences(userId) {
|
||||||
|
try {
|
||||||
|
const userFile = join(usersDir, `${userId}.json`);
|
||||||
|
if (existsSync(userFile)) {
|
||||||
|
const data = JSON.parse(readFileSync(userFile, 'utf8'));
|
||||||
|
userPreferences.set(userId, data);
|
||||||
|
console.log(`[prefs] Loaded preferences for user ${userId}`);
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
console.log(`[prefs] No preferences file for user ${userId}, using defaults`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[prefs] Error loading preferences for user ${userId}:`, error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save user preferences to disk
|
||||||
|
function saveUserPreferences(userId, preferences) {
|
||||||
|
try {
|
||||||
|
const userFile = join(usersDir, `${userId}.json`);
|
||||||
|
writeFileSync(userFile, JSON.stringify(preferences, null, 2), 'utf8');
|
||||||
|
userPreferences.set(userId, preferences);
|
||||||
|
console.log(`[prefs] Saved preferences for user ${userId}`);
|
||||||
|
|
||||||
|
// Also write to workspace for agent visibility (optional)
|
||||||
|
try {
|
||||||
|
const workspaceUsersDir = '/home/jknapp/.openclaw/workspace/users';
|
||||||
|
if (!existsSync(workspaceUsersDir)) {
|
||||||
|
mkdirSync(workspaceUsersDir, { recursive: true });
|
||||||
|
}
|
||||||
|
const workspaceUserFile = join(workspaceUsersDir, `${userId}.json`);
|
||||||
|
writeFileSync(workspaceUserFile, JSON.stringify(preferences, null, 2), 'utf8');
|
||||||
|
console.log(`[prefs] Synced preferences to workspace for agent`);
|
||||||
|
} catch (wsError) {
|
||||||
|
console.warn(`[prefs] Could not sync to workspace:`, wsError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[prefs] Error saving preferences for user ${userId}:`, error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user preferences (from cache or load from disk)
|
||||||
|
function getUserPreferences(userId) {
|
||||||
|
if (userPreferences.has(userId)) {
|
||||||
|
return userPreferences.get(userId);
|
||||||
|
}
|
||||||
|
return loadUserPreferences(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load FCM tokens from disk
|
||||||
|
function loadFcmTokens() {
|
||||||
|
try {
|
||||||
|
if (existsSync(tokensFile)) {
|
||||||
|
const data = JSON.parse(readFileSync(tokensFile, 'utf8'));
|
||||||
|
Object.entries(data).forEach(([userId, tokens]) => {
|
||||||
|
fcmTokens.set(userId, new Set(tokens));
|
||||||
|
});
|
||||||
|
const totalTokens = Array.from(fcmTokens.values()).reduce((sum, set) => sum + set.size, 0);
|
||||||
|
console.log(`[fcm] Loaded ${totalTokens} token(s) for ${fcmTokens.size} user(s) from disk`);
|
||||||
|
} else {
|
||||||
|
console.log('[fcm] No persisted tokens found, starting fresh');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[fcm] Error loading tokens from disk:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save FCM tokens to disk
|
||||||
|
function saveFcmTokens() {
|
||||||
|
try {
|
||||||
|
const data = {};
|
||||||
|
fcmTokens.forEach((tokens, userId) => {
|
||||||
|
data[userId] = Array.from(tokens);
|
||||||
|
});
|
||||||
|
writeFileSync(tokensFile, JSON.stringify(data, null, 2), 'utf8');
|
||||||
|
console.log(`[fcm] Saved tokens to disk`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[fcm] Error saving tokens to disk:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load tokens on startup
|
||||||
|
loadFcmTokens();
|
||||||
|
|
||||||
|
// Notification endpoint (for mobile-notify tool)
|
||||||
|
app.post('/api/notify', (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
notificationType = 'alert',
|
||||||
|
title = 'AI Assistant',
|
||||||
|
message,
|
||||||
|
priority = 'default',
|
||||||
|
sound = true,
|
||||||
|
vibrate = true,
|
||||||
|
timestamp = Date.now(),
|
||||||
|
action = null,
|
||||||
|
userId = null
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return res.status(400).json({ error: 'message is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[notify] Broadcasting notification: type=${notificationType} title="${title}" message="${message}"`);
|
||||||
|
|
||||||
|
// Create notification event
|
||||||
|
const notificationEvent = {
|
||||||
|
type: 'event',
|
||||||
|
event: 'mobile.notification',
|
||||||
|
payload: {
|
||||||
|
notificationType,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
priority,
|
||||||
|
sound,
|
||||||
|
vibrate,
|
||||||
|
timestamp,
|
||||||
|
action
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Broadcast to all connected clients
|
||||||
|
let sentCount = 0;
|
||||||
|
const notificationJson = JSON.stringify(notificationEvent);
|
||||||
|
console.log(`[notify] Broadcast event: ${notificationJson}`);
|
||||||
|
|
||||||
|
connectedClients.forEach(client => {
|
||||||
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
|
client.send(notificationJson);
|
||||||
|
console.log(`[notify] Sent to client (ready state: ${client.readyState})`);
|
||||||
|
sentCount++;
|
||||||
|
} else {
|
||||||
|
console.log(`[notify] Skipped client (ready state: ${client.readyState})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[notify] Sent notification to ${sentCount} connected client(s)`);
|
||||||
|
|
||||||
|
// Also send FCM notifications to devices that might be asleep
|
||||||
|
let fcmSentCount = 0;
|
||||||
|
const allTokens = [];
|
||||||
|
|
||||||
|
// Filter tokens by userId if specified
|
||||||
|
if (userId) {
|
||||||
|
console.log(`[fcm] Filtering notifications for user: ${userId}`);
|
||||||
|
const userTokens = fcmTokens.get(userId);
|
||||||
|
if (userTokens) {
|
||||||
|
userTokens.forEach(token => allTokens.push(token));
|
||||||
|
} else {
|
||||||
|
console.log(`[fcm] No tokens found for user ${userId}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Send to all users if no userId specified
|
||||||
|
fcmTokens.forEach((tokens, uid) => {
|
||||||
|
tokens.forEach(token => allTokens.push(token));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allTokens.length > 0) {
|
||||||
|
console.log(`[fcm] Sending push notification to ${allTokens.length} registered device(s)`);
|
||||||
|
|
||||||
|
// Send FCM notification (don't wait for it)
|
||||||
|
admin.messaging().sendEachForMulticast({
|
||||||
|
tokens: allTokens,
|
||||||
|
data: {
|
||||||
|
notificationType,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
priority,
|
||||||
|
sound: sound.toString(),
|
||||||
|
vibrate: vibrate.toString(),
|
||||||
|
timestamp: timestamp.toString(),
|
||||||
|
action: action || ''
|
||||||
|
},
|
||||||
|
android: {
|
||||||
|
priority: notificationType === 'alarm' ? 'high' : 'normal',
|
||||||
|
ttl: 60000 // 60 seconds
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
console.log(`[fcm] Successfully sent ${response.successCount} message(s)`);
|
||||||
|
if (response.failureCount > 0) {
|
||||||
|
console.log(`[fcm] Failed to send ${response.failureCount} message(s)`);
|
||||||
|
response.responses.forEach((resp, idx) => {
|
||||||
|
if (!resp.success) {
|
||||||
|
console.log(`[fcm] Error for token ${allTokens[idx].substring(0, 20)}: ${resp.error}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
fcmSentCount = response.successCount;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('[fcm] Error sending push notification:', error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('[fcm] No FCM tokens registered, skipping push notification');
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, clients: sentCount, fcm: allTokens.length });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[notify] Error broadcasting notification:', err);
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Alarm dismissal endpoint - broadcast dismissal to all user's devices
|
||||||
|
app.post('/api/alarm/dismiss', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { userId, alarmId } = req.body;
|
||||||
|
|
||||||
|
if (!userId || !alarmId) {
|
||||||
|
return res.status(400).json({ error: 'userId and alarmId are required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[alarm] Broadcasting dismissal for alarm ${alarmId} to user ${userId}`);
|
||||||
|
|
||||||
|
// Create dismissal event
|
||||||
|
const dismissalEvent = {
|
||||||
|
type: 'alarm_dismiss',
|
||||||
|
alarmId: alarmId,
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Broadcast to all connected WebSocket clients for this user
|
||||||
|
let sentCount = 0;
|
||||||
|
const dismissalJson = JSON.stringify(dismissalEvent);
|
||||||
|
|
||||||
|
connectedClients.forEach(client => {
|
||||||
|
if (client.userId === userId && client.readyState === WebSocket.OPEN) {
|
||||||
|
client.send(dismissalJson);
|
||||||
|
console.log(`[alarm] Sent dismissal to WebSocket client for user ${userId}`);
|
||||||
|
sentCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[alarm] Sent dismissal to ${sentCount} WebSocket client(s)`);
|
||||||
|
|
||||||
|
// Also send FCM to devices that might be offline/asleep
|
||||||
|
const userTokens = fcmTokens.get(userId) || new Set();
|
||||||
|
const tokens = Array.from(userTokens);
|
||||||
|
|
||||||
|
if (tokens.length > 0) {
|
||||||
|
admin.messaging().sendEachForMulticast({
|
||||||
|
tokens: tokens,
|
||||||
|
data: {
|
||||||
|
type: 'alarm_dismiss',
|
||||||
|
alarmId: alarmId,
|
||||||
|
timestamp: Date.now().toString()
|
||||||
|
},
|
||||||
|
android: {
|
||||||
|
priority: 'high'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
console.log(`[fcm] Sent dismissal to ${response.successCount}/${tokens.length} FCM devices`);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('[fcm] Error sending dismissal via FCM:', error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('[fcm] No FCM tokens for user, skipping push dismissal');
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, websocket: sentCount, fcm: tokens.length });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[alarm] Error broadcasting dismissal:', err);
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// File upload configuration
|
||||||
|
const uploadsDir = join(__dirname, 'uploads');
|
||||||
|
if (!existsSync(uploadsDir)) {
|
||||||
|
mkdirSync(uploadsDir, { recursive: true });
|
||||||
|
console.log(`[upload] Created uploads directory: ${uploadsDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: (req, file, cb) => {
|
||||||
|
cb(null, uploadsDir);
|
||||||
|
},
|
||||||
|
filename: (req, file, cb) => {
|
||||||
|
// Generate unique filename with timestamp
|
||||||
|
const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1E9)}`;
|
||||||
|
const ext = file.originalname.split('.').pop();
|
||||||
|
cb(null, `${file.fieldname}-${uniqueSuffix}.${ext}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const upload = multer({
|
||||||
|
storage: storage,
|
||||||
|
limits: {
|
||||||
|
fileSize: 100 * 1024 * 1024 // 100MB limit
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// File upload endpoint
|
||||||
|
app.post('/api/upload', upload.single('file'), (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.file) {
|
||||||
|
return res.status(400).json({ error: 'No file uploaded' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = join(uploadsDir, req.file.filename);
|
||||||
|
const fileUrl = `/uploads/${req.file.filename}`;
|
||||||
|
|
||||||
|
console.log(`[upload] File uploaded: ${req.file.originalname} -> ${req.file.filename}`);
|
||||||
|
console.log(`[upload] Size: ${(req.file.size / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
console.log(`[upload] Type: ${req.file.mimetype}`);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
filename: req.file.filename,
|
||||||
|
originalName: req.file.originalname,
|
||||||
|
size: req.file.size,
|
||||||
|
mimeType: req.file.mimetype,
|
||||||
|
path: filePath,
|
||||||
|
url: fileUrl
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[upload] Error handling file upload:', err);
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Text-to-speech endpoint using SAG (file-based)
|
||||||
|
*/
|
||||||
|
app.post('/api/tts', async (req, res) => {
|
||||||
|
const { text, voiceId } = req.body;
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
return res.status(400).json({ error: 'text is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create temp file for audio output
|
||||||
|
const tempFile = join(uploadsDir, `tts-${Date.now()}.mp3`);
|
||||||
|
|
||||||
|
// Build SAG command
|
||||||
|
const args = ['-o', tempFile];
|
||||||
|
if (voiceId) {
|
||||||
|
args.push('-v', voiceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[tts] Generating speech for text: "${text.substring(0, 50)}..." voice: ${voiceId || 'default'}`);
|
||||||
|
|
||||||
|
// Spawn SAG process with 120 second timeout for long responses
|
||||||
|
const sagPath = '/home/linuxbrew/.linuxbrew/bin/sag';
|
||||||
|
const sagProcess = spawn(sagPath, args, {
|
||||||
|
timeout: 120000, // 2 minutes for very long responses
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
ELEVENLABS_API_KEY: ELEVENLABS_API_KEY
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let stderr = '';
|
||||||
|
|
||||||
|
sagProcess.stderr.on('data', (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Write text to stdin
|
||||||
|
sagProcess.stdin.write(text);
|
||||||
|
sagProcess.stdin.end();
|
||||||
|
|
||||||
|
sagProcess.on('close', (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
console.error(`[tts] SAG failed with code ${code}: ${stderr}`);
|
||||||
|
return res.status(500).json({ error: `TTS failed: ${stderr}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if file was created
|
||||||
|
if (!existsSync(tempFile)) {
|
||||||
|
console.error('[tts] SAG succeeded but no audio file created');
|
||||||
|
return res.status(500).json({ error: 'No audio file generated' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioUrl = `/uploads/${basename(tempFile)}`;
|
||||||
|
|
||||||
|
console.log(`[tts] Generated audio: ${basename(tempFile)} (${statSync(tempFile).size} bytes)`);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
audioUrl: audioUrl,
|
||||||
|
filename: basename(tempFile)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
sagProcess.on('error', (err) => {
|
||||||
|
console.error('[tts] SAG spawn error:', err);
|
||||||
|
res.status(500).json({ error: `Failed to spawn SAG: ${err.message}` });
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[tts] Error:', err);
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Streaming endpoint removed - SAG doesn't easily support stdout streaming
|
||||||
|
// The file-based endpoint with extended timeout is sufficient for now
|
||||||
|
|
||||||
|
// Serve uploaded files
|
||||||
|
app.use('/uploads', express.static(uploadsDir));
|
||||||
|
|
||||||
|
const httpServer = app.listen(config.port, () => {
|
||||||
|
console.log(`[alfred-proxy] HTTP server listening on port ${config.port}`);
|
||||||
|
console.log(`[alfred-proxy] WebSocket endpoint: ws://localhost:${config.port}`);
|
||||||
|
console.log(`[alfred-proxy] OpenClaw target: ${config.openclawUrl}`);
|
||||||
|
console.log(`[alfred-proxy] Auth required: ${config.requireAuth}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// WebSocket server
|
||||||
|
const wss = new WebSocketServer({ server: httpServer });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract stable user ID from userInfo.
|
||||||
|
* Uses same logic as Android app: preferred_username > email > sub
|
||||||
|
*/
|
||||||
|
function extractUserId(userInfo) {
|
||||||
|
return userInfo.preferred_username || userInfo.email || userInfo.sub || 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate OAuth token with Authentik's userinfo endpoint
|
||||||
|
*/
|
||||||
|
async function validateAuthentikToken(token) {
|
||||||
|
if (!config.requireAuth) {
|
||||||
|
console.log('[auth] Auth disabled, skipping validation');
|
||||||
|
return { valid: true, user: { sub: 'dev-user', email: 'dev@local' } };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${config.authentikUrl}/application/o/userinfo/`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error(`[auth] Authentik validation failed: ${response.status} - ${errorText}`);
|
||||||
|
console.error(`[auth] Token (first 20 chars): ${token.substring(0, 20)}...`);
|
||||||
|
return { valid: false, error: 'Invalid token' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const userInfo = await response.json();
|
||||||
|
const userId = extractUserId(userInfo);
|
||||||
|
console.log(`[auth] Token validated for user: ${userId} (${userInfo.email || userInfo.sub})`);
|
||||||
|
|
||||||
|
return { valid: true, user: userInfo };
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[auth] Authentik validation error:', err.message);
|
||||||
|
return { valid: false, error: err.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract OAuth token from WebSocket upgrade request
|
||||||
|
*/
|
||||||
|
function extractToken(req) {
|
||||||
|
// Check Authorization header
|
||||||
|
const authHeader = req.headers['authorization'];
|
||||||
|
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||||
|
return authHeader.slice(7).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check query parameter (for testing)
|
||||||
|
const url = new URL(req.url, 'ws://localhost');
|
||||||
|
const token = url.searchParams.get('token');
|
||||||
|
if (token) {
|
||||||
|
return token.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject OpenClaw token into connect message
|
||||||
|
*/
|
||||||
|
function injectOpenClawToken(message) {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(message);
|
||||||
|
|
||||||
|
// Only inject token into connect messages
|
||||||
|
if (msg.type === 'req' && msg.method === 'connect') {
|
||||||
|
console.log('[proxy] Injecting OpenClaw token into connect message');
|
||||||
|
|
||||||
|
if (!msg.params) {
|
||||||
|
msg.params = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!msg.params.auth) {
|
||||||
|
msg.params.auth = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add OpenClaw gateway token
|
||||||
|
msg.params.auth.token = config.openclawToken;
|
||||||
|
|
||||||
|
// Change webchat mode to cli to bypass origin validation
|
||||||
|
if (msg.params.client && msg.params.client.mode === 'webchat') {
|
||||||
|
console.log('[proxy] Changing mode from webchat to cli to bypass origin validation');
|
||||||
|
msg.params.client.mode = 'cli';
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass through all other messages unchanged
|
||||||
|
return message;
|
||||||
|
} catch (err) {
|
||||||
|
// If it's not JSON, pass through unchanged
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle WebSocket connections
|
||||||
|
wss.on('connection', async (clientWs, req) => {
|
||||||
|
const clientIp = req.socket.remoteAddress;
|
||||||
|
console.log(`[proxy] New connection from ${clientIp}`);
|
||||||
|
|
||||||
|
// Extract and validate OAuth token
|
||||||
|
const oauthToken = extractToken(req);
|
||||||
|
|
||||||
|
if (!oauthToken && config.requireAuth) {
|
||||||
|
console.log(`[proxy] No OAuth token provided, rejecting connection`);
|
||||||
|
clientWs.close(1008, 'Authentication required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let clientUserId = null;
|
||||||
|
|
||||||
|
if (oauthToken) {
|
||||||
|
const validation = await validateAuthentikToken(oauthToken);
|
||||||
|
|
||||||
|
if (!validation.valid) {
|
||||||
|
console.log(`[proxy] Invalid OAuth token, rejecting connection`);
|
||||||
|
clientWs.close(1008, validation.error || 'Invalid token');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = extractUserId(validation.user);
|
||||||
|
console.log(`[proxy] Authenticated as ${userId} (${validation.user.email || validation.user.sub})`);
|
||||||
|
clientUserId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to OpenClaw
|
||||||
|
console.log(`[proxy] Connecting to OpenClaw at ${config.openclawUrl}`);
|
||||||
|
const openclawWs = new WebSocket(config.openclawUrl, {
|
||||||
|
headers: {
|
||||||
|
'Origin': `http://localhost:${config.port}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let isAlive = true;
|
||||||
|
|
||||||
|
// Add client to connected clients set (for notification broadcasting)
|
||||||
|
// Store userId on the WebSocket client for filtering
|
||||||
|
clientWs.userId = clientUserId;
|
||||||
|
connectedClients.add(clientWs);
|
||||||
|
console.log(`[proxy] Client added to notification broadcast list (total: ${connectedClients.size})`);
|
||||||
|
|
||||||
|
// Proxy: Client → OpenClaw (inject token)
|
||||||
|
clientWs.on('message', (data) => {
|
||||||
|
const message = data.toString('utf8');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse message to check for special events
|
||||||
|
const parsed = JSON.parse(message);
|
||||||
|
|
||||||
|
// Handle FCM token registration
|
||||||
|
if (parsed.type === 'fcm.register') {
|
||||||
|
const fcmToken = parsed.token;
|
||||||
|
if (fcmToken && clientUserId) {
|
||||||
|
console.log(`[fcm] Registering token for user ${clientUserId}: ${fcmToken.substring(0, 20)}...`);
|
||||||
|
|
||||||
|
// Store token for this user
|
||||||
|
if (!fcmTokens.has(clientUserId)) {
|
||||||
|
fcmTokens.set(clientUserId, new Set());
|
||||||
|
}
|
||||||
|
fcmTokens.get(clientUserId).add(fcmToken);
|
||||||
|
|
||||||
|
console.log(`[fcm] User ${clientUserId} now has ${fcmTokens.get(clientUserId).size} token(s)`);
|
||||||
|
|
||||||
|
// Persist to disk
|
||||||
|
saveFcmTokens();
|
||||||
|
} else {
|
||||||
|
console.log('[fcm] Invalid FCM registration: missing token or user ID');
|
||||||
|
}
|
||||||
|
return; // Don't forward to OpenClaw
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle alarm dismiss - broadcast to all other clients
|
||||||
|
if (parsed.type === 'alarm.dismiss') {
|
||||||
|
console.log(`[proxy] Alarm dismiss received: ${parsed.alarmId}`);
|
||||||
|
|
||||||
|
const dismissEvent = {
|
||||||
|
type: 'event',
|
||||||
|
event: 'mobile.alarm.dismissed',
|
||||||
|
payload: {
|
||||||
|
alarmId: parsed.alarmId,
|
||||||
|
timestamp: parsed.timestamp || Date.now()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const dismissJson = JSON.stringify(dismissEvent);
|
||||||
|
let broadcastCount = 0;
|
||||||
|
|
||||||
|
// Broadcast to all clients (including sender for consistency)
|
||||||
|
connectedClients.forEach(client => {
|
||||||
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
|
client.send(dismissJson);
|
||||||
|
broadcastCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[proxy] Broadcasted alarm dismiss to ${broadcastCount} client(s)`);
|
||||||
|
return; // Don't forward to OpenClaw
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle user preferences update
|
||||||
|
if (parsed.type === 'req' && parsed.method === 'user.preferences.update') {
|
||||||
|
console.log(`[prefs] Preference update request from user ${clientUserId}`);
|
||||||
|
|
||||||
|
if (!clientUserId) {
|
||||||
|
const errorResponse = {
|
||||||
|
type: 'res',
|
||||||
|
id: parsed.id,
|
||||||
|
ok: false,
|
||||||
|
error: 'User ID not available'
|
||||||
|
};
|
||||||
|
clientWs.send(JSON.stringify(errorResponse));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPrefs = getUserPreferences(clientUserId) || {};
|
||||||
|
const updatedPrefs = { ...currentPrefs, ...parsed.params };
|
||||||
|
|
||||||
|
if (saveUserPreferences(clientUserId, updatedPrefs)) {
|
||||||
|
const successResponse = {
|
||||||
|
type: 'res',
|
||||||
|
id: parsed.id,
|
||||||
|
ok: true,
|
||||||
|
payload: updatedPrefs
|
||||||
|
};
|
||||||
|
clientWs.send(JSON.stringify(successResponse));
|
||||||
|
console.log(`[prefs] Updated preferences for ${clientUserId}:`, updatedPrefs);
|
||||||
|
} else {
|
||||||
|
const errorResponse = {
|
||||||
|
type: 'res',
|
||||||
|
id: parsed.id,
|
||||||
|
ok: false,
|
||||||
|
error: 'Failed to save preferences'
|
||||||
|
};
|
||||||
|
clientWs.send(JSON.stringify(errorResponse));
|
||||||
|
}
|
||||||
|
return; // Don't forward to OpenClaw
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle user preferences get
|
||||||
|
if (parsed.type === 'req' && parsed.method === 'user.preferences.get') {
|
||||||
|
console.log(`[prefs] Preference get request from user ${clientUserId}`);
|
||||||
|
|
||||||
|
if (!clientUserId) {
|
||||||
|
const errorResponse = {
|
||||||
|
type: 'res',
|
||||||
|
id: parsed.id,
|
||||||
|
ok: false,
|
||||||
|
error: 'User ID not available'
|
||||||
|
};
|
||||||
|
clientWs.send(JSON.stringify(errorResponse));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefs = getUserPreferences(clientUserId) || {};
|
||||||
|
const successResponse = {
|
||||||
|
type: 'res',
|
||||||
|
id: parsed.id,
|
||||||
|
ok: true,
|
||||||
|
payload: prefs
|
||||||
|
};
|
||||||
|
clientWs.send(JSON.stringify(successResponse));
|
||||||
|
console.log(`[prefs] Retrieved preferences for ${clientUserId}:`, prefs);
|
||||||
|
return; // Don't forward to OpenClaw
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Not JSON or not a special event - continue with normal flow
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward all other messages to OpenClaw
|
||||||
|
if (openclawWs.readyState === WebSocket.OPEN) {
|
||||||
|
console.log(`[proxy] Client → OpenClaw (full): ${message}`);
|
||||||
|
const modifiedMessage = injectOpenClawToken(message);
|
||||||
|
openclawWs.send(modifiedMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Proxy: OpenClaw → Client (inject user preferences into connect response)
|
||||||
|
openclawWs.on('message', (data) => {
|
||||||
|
const message = data.toString('utf8');
|
||||||
|
console.log(`[proxy] OpenClaw → Client: ${message.substring(0, 100)}...`);
|
||||||
|
console.log(`[proxy] Client WebSocket state: ${clientWs.readyState} (1=OPEN)`);
|
||||||
|
|
||||||
|
let messageToSend = data;
|
||||||
|
|
||||||
|
// Try to inject user preferences into connect response
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(message);
|
||||||
|
|
||||||
|
// If this is a connect response, inject user preferences
|
||||||
|
if (parsed.type === 'res' && parsed.ok && parsed.id && parsed.id.startsWith('connect-')) {
|
||||||
|
console.log(`[prefs] Injecting user preferences into connect response for ${clientUserId}`);
|
||||||
|
|
||||||
|
const prefs = getUserPreferences(clientUserId) || {};
|
||||||
|
|
||||||
|
if (!parsed.payload) {
|
||||||
|
parsed.payload = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed.payload.userPreferences = prefs;
|
||||||
|
messageToSend = JSON.stringify(parsed);
|
||||||
|
console.log(`[prefs] Injected preferences:`, prefs);
|
||||||
|
|
||||||
|
// Write per-user CONTEXT.md for agent to read
|
||||||
|
try {
|
||||||
|
const workspacePath = '/home/jknapp/.openclaw/workspace';
|
||||||
|
const assistantName = prefs.assistantName || 'Alfred';
|
||||||
|
const voiceId = prefs.voiceId || 'default';
|
||||||
|
|
||||||
|
// 1. Write per-user file (persistent, multi-user)
|
||||||
|
const userDir = join(workspacePath, 'users', clientUserId);
|
||||||
|
const userContextPath = join(userDir, 'CONTEXT.md');
|
||||||
|
|
||||||
|
if (!existsSync(userDir)) {
|
||||||
|
mkdirSync(userDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const userContext = `# CONTEXT.md - User Preferences for ${clientUserId}
|
||||||
|
|
||||||
|
**User ID:** ${clientUserId}
|
||||||
|
**Assistant Name:** ${assistantName}
|
||||||
|
**Voice ID:** ${voiceId}
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
**IMPORTANT:** This file contains THIS USER's preferences and overrides global defaults.
|
||||||
|
|
||||||
|
- **When this user addresses you:** Use the Assistant Name above (e.g., "${assistantName}")
|
||||||
|
- **Self-identification:** "I'm ${assistantName}" (not "Alfred" unless that's the name above)
|
||||||
|
- **Voice responses:** Use the Voice ID above for TTS
|
||||||
|
|
||||||
|
This file is dynamically updated on each user connection by alfred-proxy.
|
||||||
|
|
||||||
|
**Multi-user support:** Each user has their own CONTEXT.md in their users/{userId}/ directory.
|
||||||
|
`;
|
||||||
|
|
||||||
|
writeFileSync(userContextPath, userContext, 'utf8');
|
||||||
|
console.log(`[prefs] Wrote users/${clientUserId}/CONTEXT.md (name: ${assistantName})`);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[prefs] Failed to write user context:`, err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Not JSON or couldn't parse, send as-is
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientWs.readyState === WebSocket.OPEN) {
|
||||||
|
try {
|
||||||
|
clientWs.send(messageToSend);
|
||||||
|
console.log(`[proxy] Message sent to client successfully`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[proxy] Failed to send to client:`, err.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`[proxy] Client WebSocket not open, cannot send (state=${clientWs.readyState})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle OpenClaw connection
|
||||||
|
openclawWs.on('open', () => {
|
||||||
|
console.log('[proxy] Connected to OpenClaw');
|
||||||
|
isAlive = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
openclawWs.on('error', (err) => {
|
||||||
|
console.error('[proxy] OpenClaw connection error:', err.message);
|
||||||
|
clientWs.close(1011, 'Upstream connection failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
openclawWs.on('close', (code, reason) => {
|
||||||
|
console.log(`[proxy] OpenClaw closed: ${code} ${reason}`);
|
||||||
|
clearInterval(pingInterval);
|
||||||
|
clientWs.close(code, reason.toString('utf8'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle client disconnection
|
||||||
|
clientWs.on('close', (code, reason) => {
|
||||||
|
console.log(`[proxy] Client disconnected: ${code} ${reason}`);
|
||||||
|
connectedClients.delete(clientWs);
|
||||||
|
console.log(`[proxy] Client removed from notification broadcast list (total: ${connectedClients.size})`);
|
||||||
|
clearInterval(pingInterval);
|
||||||
|
openclawWs.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
clientWs.on('error', (err) => {
|
||||||
|
console.error('[proxy] Client connection error:', err.message);
|
||||||
|
connectedClients.delete(clientWs);
|
||||||
|
clearInterval(pingInterval);
|
||||||
|
openclawWs.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ping/pong to keep connection alive
|
||||||
|
const pingInterval = setInterval(() => {
|
||||||
|
if (!isAlive) {
|
||||||
|
clearInterval(pingInterval);
|
||||||
|
openclawWs.terminate();
|
||||||
|
clientWs.terminate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isAlive = false;
|
||||||
|
if (openclawWs.readyState === WebSocket.OPEN) {
|
||||||
|
openclawWs.ping();
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
openclawWs.on('pong', () => {
|
||||||
|
isAlive = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Graceful shutdown
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
console.log('[alfred-proxy] SIGTERM received, closing server...');
|
||||||
|
httpServer.close(() => {
|
||||||
|
console.log('[alfred-proxy] HTTP server closed');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log('[alfred-proxy] SIGINT received, closing server...');
|
||||||
|
httpServer.close(() => {
|
||||||
|
console.log('[alfred-proxy] HTTP server closed');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Global error handlers to prevent crashes
|
||||||
|
process.on('uncaughtException', (err) => {
|
||||||
|
console.error('[alfred-proxy] Uncaught exception:', err);
|
||||||
|
// Don't exit - log and continue
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
console.error('[alfred-proxy] Unhandled rejection at:', promise, 'reason:', reason);
|
||||||
|
// Don't exit - log and continue
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[alfred-proxy] Service started successfully');
|
||||||
34
test-connection.sh
Executable file
34
test-connection.sh
Executable file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Test Alfred Proxy connection
|
||||||
|
|
||||||
|
PROXY_URL="${PROXY_URL:-ws://localhost:18790}"
|
||||||
|
OAUTH_TOKEN="${OAUTH_TOKEN:-test-token}"
|
||||||
|
|
||||||
|
echo "🧪 Testing Alfred Proxy"
|
||||||
|
echo "URL: $PROXY_URL"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check health endpoint
|
||||||
|
echo "1. Testing health endpoint..."
|
||||||
|
curl -s http://localhost:18790/health | jq . || echo "❌ Health check failed"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test WebSocket connection (requires wscat)
|
||||||
|
if command -v wscat &> /dev/null; then
|
||||||
|
echo "2. Testing WebSocket connection..."
|
||||||
|
echo " (This will fail without a valid OAuth token)"
|
||||||
|
echo ""
|
||||||
|
echo " Install wscat: npm install -g wscat"
|
||||||
|
echo " Then run: wscat -c \"$PROXY_URL\" -H \"Authorization: Bearer YOUR_TOKEN\""
|
||||||
|
else
|
||||||
|
echo "2. Skipping WebSocket test (wscat not installed)"
|
||||||
|
echo " Install: npm install -g wscat"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Basic tests complete"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo "1. Get OAuth token from Authentik"
|
||||||
|
echo "2. Test connection: wscat -c \"$PROXY_URL\" -H \"Authorization: Bearer TOKEN\""
|
||||||
|
echo "3. Check logs: journalctl --user -u alfred-proxy.service -f"
|
||||||
Reference in New Issue
Block a user