Skill: teamtelnyx/telnyx-network (version 1.0.0)

--- name: telnyx-network description: Private mesh networking and public IP exposure via Telnyx WireGuard infrastructure. Connect nodes securely or expose services to the internet. metadata: {"openclaw":{"emoji":"🌐","requires":{"bins":["wg"],"env":["TELNYX_API_KEY"]},"primaryEnv":"TELNYX_API_KEY"}} --- # Telnyx Network Private mesh networking and public IP exposure via Telnyx WireGuard infrastructure. ## Requirements - **Telnyx API Key** — [Get one free](https://portal.telnyx.com/#/app/api-keys) - **WireGuard** installed on your machine ## Agent Use (OpenClaw) WireGuard requires elevated permissions to create network interfaces. For OpenClaw to manage your mesh autonomously, run this **once**: ```bash sudo ./setup-sudoers.sh ``` This adds a sudoers rule allowing WireGuard commands without password prompts. After setup, your agent can: ```bash # Agent can now do all of this without password prompts: ./setup.sh --region ashburn-va ./join.sh --name "my-node" --apply ./register.sh --name "my-node" ./teardown.sh ``` **What it does:** - Adds `/etc/sudoers.d/wireguard-<username>` - Only allows `wg` and `wg-quick` commands (not blanket sudo) - Can be removed anytime: `sudo rm /etc/sudoers.d/wireguard-*` **Without this setup**, the agent can still create networks and generate configs, but you'll need to manually run `sudo wg-quick up <config>` to connect. ## Two Modes ### Mesh Mode (Private) Connect multiple machines in a private network. Like Tailscale, but on Telnyx infrastructure. ```bash ./setup.sh --region ashburn-va ./join.sh --name "laptop" ./join.sh --name "server" # run on server # Now laptop and server can talk via 172.27.0.x ``` **Cost: $10/month** (WireGuard Gateway) ### Expose Mode (Public) Get a public IP and expose services to the internet. ```bash ./setup.sh --region ashburn-va ./join.sh --name "server" --apply ./add-public-ip.sh ./expose.sh 443 # Now https://64.16.x.x:443 reaches your server ``` **Cost: $60/month** (WireGuard Gateway + Internet Gateway) ## Commands | Command | Description | |---------|-------------| | `sudo ./setup-sudoers.sh` | Enable passwordless sudo for WireGuard (one-time, for agent use) | | `./setup.sh --region <code>` | Create network + WireGuard gateway | | `./join.sh --name <name>` | Add this machine to the mesh | | `./peers.sh` | List all connected peers | | `./add-public-ip.sh` | Add internet gateway (public IP) | | `./expose.sh <port>` | Open a port | | `./unexpose.sh <port>` | Close a port | | `./status.sh` | Show full status | | `./teardown.sh` | Delete everything | | `./register.sh --name <name>` | Register node in mesh registry | | `./discover.sh` | Discover other nodes on mesh | | `./unregister.sh --name <name>` | Remove node from registry | ## Node Discovery Nodes on the mesh can find each other using a registry stored in Telnyx Storage. This enables OpenClaw instances to automatically discover and communicate with each other. ### Register This Node After joining the mesh, register your node so others can find it: ```bash ./register.sh --name "home-server" ``` ### Discover Other Nodes Find all registered nodes on the mesh: ```bash ./discover.sh # Output: # NAME IP HOSTNAME REGISTERED # home-server 172.27.0.1 macbook.local 2026-01-31 ✅ # work-laptop 172.27.0.2 thinkpad 2026-01-31 ✅ # JSON output for scripts ./discover.sh --json ``` ### Unregister Remove a node from the registry: ```bash ./unregister.sh --name "old-server" ``` ### Use Case: Multi-OpenClaw Communication ```bash # On OpenClaw A ./join.sh --name "openclaw-a" --apply ./register.sh --name "openclaw-a" # On OpenClaw B ./join.sh --name "openclaw-b" --apply ./register.sh --name "openclaw-b" # Either can now discover the other ./discover.sh # → Shows both openclaw-a and openclaw-b with their mesh IPs # Direct communication works via mesh IPs curl http://172.27.0.2:18789/health # OpenClaw B's gateway ``` This completes the "host-to-local node sessions" and "direct comms between OpenClaws" use cases. ## Regions | Region | Code | Location | |--------|------|----------| | US East | `ashburn-va` | Ashburn, VA | | US Central | `chicago-il` | Chicago, IL | | EU | `frankfurt-de` | Frankfurt, DE | | EU | `amsterdam-nl` | Amsterdam, NL | Get full list: ```bash ./setup.sh --region help ``` ## Safety ### Blocked Ports (need --force) - 22 (SSH) - 23 (Telnet) - 3306 (MySQL) - 5432 (PostgreSQL) - 6379 (Redis) - 27017 (MongoDB) ### Firewall Only explicitly exposed ports accept traffic on the WireGuard interface. All other ports are blocked by default. ## Configuration All state is stored in `config.json`: ```json { "network_id": "...", "region": "ashburn-va", "wireguard_gateway": { "id": "...", "endpoint": "64.16.x.x:5107", "subnet": "172.27.0.1/24" }, "internet_gateway": { "id": "...", "public_ip": "64.16.x.x" }, "peers": [...], "exposed_ports": [443, 80] } ``` ## Use Cases ### 1. Connect OpenClaw Instances ```bash # On main server ./setup.sh --region ashburn-va ./join.sh --name "openclaw-main" --apply # On secondary server ./join.sh --name "openclaw-backup" --apply # Now they can communicate securely ``` ### 2. Expose Webhook Endpoint ```bash ./add-public-ip.sh ./expose.sh 443 # Configure your webhook URL as https://64.16.x.x/webhook ``` ### 3. Multi-Region Mesh ```bash ./setup.sh --region ashburn-va ./join.sh --name "us-east-server" # Same network, different region gateway ./setup.sh --region frankfurt-de --name same-network ./join.sh --name "eu-server" ``` ## Pricing | Component | Monthly Cost | |-----------|--------------| | WireGuard Gateway | $10 | | Internet Gateway | $50 | | Peers | Free | | Traffic | Free (beta) | ## Troubleshooting ### "Gateway still provisioning" Wait 5-10 minutes after setup for the gateway to be ready. ### "Connection refused" - Check WireGuard is running: `sudo wg show` - Check port is exposed: `./status.sh` - Check firewall: `sudo iptables -L -n` ### "Permission denied" WireGuard requires root. Run with `sudo` or use `--apply` flag. ## License MIT

path: add-public-ip.sh
content: #!/bin/bash
#
# Telnyx Network - Add Public IP
# Creates an Internet Gateway for public exposure
#
# Usage: ./add-public-ip.sh [--yes]
#

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="$SCRIPT_DIR/config.json"
API_BASE="https://api.telnyx.com/v2"

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

SKIP_CONFIRM=false

while [[ $# -gt 0 ]]; do
    case $1 in
        --yes|-y) SKIP_CONFIRM=true; shift ;;
        *) echo "Unknown option: $1"; exit 1 ;;
    esac
done

if [ ! -f "$CONFIG_FILE" ]; then
    echo "Error: No config found. Run ./setup.sh first"
    exit 1
fi

if [ -z "$TELNYX_API_KEY" ]; then
    if [ -f "$SCRIPT_DIR/.env" ]; then
        source "$SCRIPT_DIR/.env"
    fi
fi

if [ -z "$TELNYX_API_KEY" ]; then
    echo -e "${RED}Error: TELNYX_API_KEY not set${NC}"
    exit 1
fi

# Check if already has IGW
EXISTING=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c.get('internet_gateway',{}).get('id',''))" 2>/dev/null)
if [ -n "$EXISTING" ]; then
    PUBLIC_IP=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c['internet_gateway']['public_ip'])")
    echo "Internet Gateway already exists."
    echo "Public IP: $PUBLIC_IP"
    exit 0
fi

# Warning
if [ "$SKIP_CONFIRM" = false ]; then
    echo ""
    echo -e "${YELLOW}⚠️  WARNING: This will expose your network to the internet.${NC}"
    echo ""
    echo "   - Adds Internet Gateway (\$50/month)"
    echo "   - Assigns a public IP address"
    echo "   - Only explicitly exposed ports will be accessible"
    echo ""
    read -p "Continue? [y/N] " -n 1 -r
    echo
    if [[ ! $REPLY =~ ^[Yy]$ ]]; then
        echo "Cancelled."
        exit 0
    fi
fi

# Get network info
NETWORK_ID=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c['network_id'])")
REGION=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c['region'])")
NAME=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c['network_name'])")

echo ""
echo "Creating Internet Gateway..."

IGW_RESP=$(curl -s -X POST "$API_BASE/public_internet_gateways" \
    -H "Authorization: Bearer $TELNYX_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"network_id\": \"$NETWORK_ID\", \"name\": \"$NAME-igw\", \"region_code\": \"$REGION\"}")

IGW_ID=$(echo "$IGW_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('data',{}).get('id',''))" 2>/dev/null)
PUBLIC_IP=$(echo "$IGW_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('data',{}).get('public_ip',''))" 2>/dev/null)

if [ -z "$IGW_ID" ]; then
    echo -e "${RED}Failed to create Internet Gateway${NC}"
    echo "$IGW_RESP"
    exit 1
fi

# Update config
python3 << EOF
import json
with open('$CONFIG_FILE', 'r') as f:
    config = json.load(f)
config['internet_gateway'] = {
    'id': '$IGW_ID',
    'public_ip': '$PUBLIC_IP'
}
with open('$CONFIG_FILE', 'w') as f:
    json.dump(config, f, indent=2)
EOF

echo ""
echo -e "${GREEN}✅ Internet Gateway created!${NC}"
echo ""
echo "Public IP: $PUBLIC_IP"
echo ""
echo "Note: Gateway takes ~10 min to provision."
echo ""
echo "Next: ./expose.sh <port> to open ports"
echo ""
echo -e "${YELLOW}Cost: \$50/month${NC}"


path: diagram.svg
content: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 500" font-family="monospace" font-size="14">
  <defs>
    <marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
      <path d="M0,0 L0,6 L9,3 z" fill="#666"/>
    </marker>
  </defs>
  
  <!-- Background -->
  <rect width="900" height="500" fill="#1a1a2e"/>
  
  <!-- Title -->
  <text x="450" y="35" fill="#fff" font-size="20" text-anchor="middle" font-weight="bold">Telnyx Network — Mesh + Expose</text>
  
  <!-- Internet (for expose mode) -->
  <text x="80" y="90" fill="#888" font-size="12">EXPOSE MODE</text>
  <ellipse cx="80" cy="130" rx="50" ry="25" fill="none" stroke="#4a9eff" stroke-width="2"/>
  <text x="80" y="135" fill="#fff" text-anchor="middle">Internet</text>
  
  <!-- Telnyx Cloud Box -->
  <rect x="200" y="70" width="350" height="200" fill="none" stroke="#4a9eff" stroke-width="2" rx="8"/>
  <text x="375" y="100" fill="#fff" font-weight="bold" text-anchor="middle">Telnyx Cloud (ashburn-va)</text>
  
  <!-- Internet Gateway -->
  <rect x="220" y="120" width="140" height="40" fill="#2d2d44" stroke="#4a9eff" stroke-width="1" rx="4"/>
  <text x="290" y="145" fill="#4a9eff" text-anchor="middle">Internet Gateway</text>
  <text x="290" y="175" fill="#888" font-size="11" text-anchor="middle">64.16.243.172</text>
  
  <!-- WireGuard Gateway -->
  <rect x="390" y="120" width="140" height="40" fill="#2d2d44" stroke="#4a9eff" stroke-width="1" rx="4"/>
  <text x="460" y="145" fill="#4a9eff" text-anchor="middle">WireGuard GW</text>
  <text x="460" y="175" fill="#888" font-size="11" text-anchor="middle">172.27.0.1</text>
  
  <!-- Private Network label -->
  <text x="375" y="210" fill="#888" font-size="11" text-anchor="middle">Private Network: 172.27.0.0/24</text>
  
  <!-- Arrow: Internet to IGW -->
  <line x1="130" y1="130" x2="215" y2="140" stroke="#666" stroke-width="2" marker-end="url(#arrow)"/>
  
  <!-- Arrow: IGW to WGW -->
  <line x1="365" y1="140" x2="385" y2="140" stroke="#666" stroke-width="2" marker-end="url(#arrow)"/>
  
  <!-- Mesh Mode Label -->
  <text x="700" y="90" fill="#888" font-size="12">MESH MODE</text>
  
  <!-- Peers Box -->
  <rect x="600" y="280" width="270" height="180" fill="none" stroke="#4a9eff" stroke-width="2" stroke-dasharray="5,5" rx="8"/>
  <text x="735" y="310" fill="#fff" font-weight="bold" text-anchor="middle">Your Nodes</text>
  
  <!-- Peer 1 -->
  <rect x="620" y="330" width="100" height="50" fill="#2d2d44" stroke="#4a9eff" stroke-width="1" rx="4"/>
  <text x="670" y="352" fill="#fff" text-anchor="middle" font-size="12">Clawdbot 1</text>
  <text x="670" y="370" fill="#888" text-anchor="middle" font-size="11">172.27.0.2</text>
  
  <!-- Peer 2 -->
  <rect x="750" y="330" width="100" height="50" fill="#2d2d44" stroke="#4a9eff" stroke-width="1" rx="4"/>
  <text x="800" y="352" fill="#fff" text-anchor="middle" font-size="12">Clawdbot 2</text>
  <text x="800" y="370" fill="#888" text-anchor="middle" font-size="11">172.27.0.3</text>
  
  <!-- Peer 3 -->
  <rect x="685" y="400" width="100" height="50" fill="#2d2d44" stroke="#4a9eff" stroke-width="1" rx="4"/>
  <text x="735" y="422" fill="#fff" text-anchor="middle" font-size="12">Laptop</text>
  <text x="735" y="440" fill="#888" text-anchor="middle" font-size="11">172.27.0.4</text>
  
  <!-- WireGuard tunnels to peers -->
  <line x1="460" y1="165" x2="670" y2="325" stroke="#4a9eff" stroke-width="1" stroke-dasharray="3,3"/>
  <line x1="460" y1="165" x2="800" y2="325" stroke="#4a9eff" stroke-width="1" stroke-dasharray="3,3"/>
  <line x1="460" y1="165" x2="735" y2="395" stroke="#4a9eff" stroke-width="1" stroke-dasharray="3,3"/>
  
  <!-- Peer to peer mesh lines -->
  <line x1="720" y1="355" x2="750" y2="355" stroke="#666" stroke-width="1"/>
  <line x1="670" y1="380" x2="700" y2="400" stroke="#666" stroke-width="1"/>
  <line x1="800" y1="380" x2="770" y2="400" stroke="#666" stroke-width="1"/>
  
  <!-- Legend -->
  <text x="50" y="480" fill="#888" font-size="11">── WireGuard tunnel</text>
  <text x="220" y="480" fill="#888" font-size="11">── Mesh traffic</text>
  <text x="380" y="480" fill="#888" font-size="11">Mesh: $10/mo | + Public IP: $60/mo</text>
</svg>


path: discover.sh
content: #!/bin/bash
# Discover nodes registered on the Telnyx mesh
# Usage: discover.sh [--bucket <bucket>] [--json]

set -e

# Defaults
BUCKET="${TELNYX_MESH_BUCKET:-openclaw-mesh}"
OUTPUT_JSON=false

# Parse args
while [[ $# -gt 0 ]]; do
    case $1 in
        --bucket|-b)
            BUCKET="$2"
            shift 2
            ;;
        --json|-j)
            OUTPUT_JSON=true
            shift
            ;;
        --help|-h)
            echo "Usage: discover.sh [--bucket <bucket>] [--json]"
            echo ""
            echo "Discover nodes registered on the Telnyx mesh."
            echo ""
            echo "Options:"
            echo "  --bucket, -b  Registry bucket (default: openclaw-mesh)"
            echo "  --json, -j    Output as JSON array"
            echo ""
            echo "Environment:"
            echo "  TELNYX_MESH_BUCKET  Default bucket name"
            exit 0
            ;;
        *)
            echo "Unknown option: $1"
            exit 1
            ;;
    esac
done

# Check telnyx CLI
if ! command -v telnyx &> /dev/null; then
    echo "❌ Telnyx CLI not found. Install with: npm install -g @telnyx/api-cli"
    exit 1
fi

# Check if bucket exists
if ! telnyx storage bucket list 2>/dev/null | grep -q "$BUCKET"; then
    if $OUTPUT_JSON; then
        echo "[]"
    else
        echo "⚠️  No mesh registry found (bucket: $BUCKET)"
        echo "   Register a node first: scripts/register.sh --name <name>"
    fi
    exit 0
fi

# List and fetch all node registrations
NODES=()
TEMP_DIR=$(mktemp -d)

# Get list of node files
NODE_FILES=$(telnyx storage object list "$BUCKET" --prefix "nodes/" 2>/dev/null | grep -oE 'nodes/[^[:space:]]+\.json' || echo "")

if [[ -z "$NODE_FILES" ]]; then
    if $OUTPUT_JSON; then
        echo "[]"
    else
        echo "📭 No nodes registered yet"
        echo "   Register with: scripts/register.sh --name <name>"
    fi
    rm -rf "$TEMP_DIR"
    exit 0
fi

if ! $OUTPUT_JSON; then
    echo "🔍 Discovering nodes on mesh ($BUCKET)..."
    echo ""
    printf "%-15s %-15s %-20s %s\n" "NAME" "IP" "HOSTNAME" "REGISTERED"
    printf "%-15s %-15s %-20s %s\n" "----" "--" "--------" "----------"
fi

JSON_ARRAY="["
FIRST=true

for NODE_FILE in $NODE_FILES; do
    NODE_NAME=$(basename "$NODE_FILE" .json)
    TEMP_FILE="$TEMP_DIR/$NODE_NAME.json"
    
    # Fetch node data
    if telnyx storage object get "$BUCKET" "$NODE_FILE" --output "$TEMP_FILE" 2>/dev/null; then
        if [[ -f "$TEMP_FILE" ]]; then
            NAME=$(python3 -c "import json; d=json.load(open('$TEMP_FILE')); print(d.get('name','unknown'))" 2>/dev/null)
            IP=$(python3 -c "import json; d=json.load(open('$TEMP_FILE')); print(d.get('ip','unknown'))" 2>/dev/null)
            HOSTNAME=$(python3 -c "import json; d=json.load(open('$TEMP_FILE')); print(d.get('hostname','unknown'))" 2>/dev/null)
            REGISTERED=$(python3 -c "import json; d=json.load(open('$TEMP_FILE')); print(d.get('registered_at','unknown'))" 2>/dev/null)
            
            if $OUTPUT_JSON; then
                if ! $FIRST; then
                    JSON_ARRAY+=","
                fi
                JSON_ARRAY+=$(cat "$TEMP_FILE")
                FIRST=false
            else
                # Check if node is reachable (quick ping)
                # Note: ICMP may be blocked in containers/cloud environments
                REACHABLE=""
                if [[ "$IP" != "unknown" ]] && command -v ping &>/dev/null; then
                    if ping -c 1 -W 1 "$IP" &>/dev/null 2>&1; then
                        REACHABLE=" ✅"
                    fi
                fi
                printf "%-15s %-15s %-20s %s%s\n" "$NAME" "$IP" "$HOSTNAME" "${REGISTERED:0:10}" "$REACHABLE"
            fi
        fi
    fi
done

if $OUTPUT_JSON; then
    JSON_ARRAY+="]"
    echo "$JSON_ARRAY" | python3 -m json.tool
else
    echo ""
    echo "💡 Tip: Use --json for machine-readable output"
fi

rm -rf "$TEMP_DIR"


path: expose.sh
content: #!/bin/bash
#
# Telnyx Network - Expose Ports
# Opens ports on the WireGuard interface
#
# Usage: ./expose.sh <port> [port2 port3...] [--force]
#

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="$SCRIPT_DIR/config.json"

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

# Dangerous ports that need --force
DANGEROUS_PORTS=(22 23 3306 5432 6379 27017 11211)

FORCE=false
PORTS=()

# Parse args
while [[ $# -gt 0 ]]; do
    case $1 in
        --force|-f) FORCE=true; shift ;;
        -h|--help)
            echo "Usage: ./expose.sh <port> [port2...] [--force]"
            echo ""
            echo "Examples:"
            echo "  ./expose.sh 443           # Expose HTTPS"
            echo "  ./expose.sh 80 443        # Expose HTTP and HTTPS"
            echo "  ./expose.sh 22 --force    # Expose SSH (dangerous)"
            echo ""
            echo "Blocked by default (need --force):"
            echo "  22 (SSH), 23 (Telnet), 3306 (MySQL),"
            echo "  5432 (PostgreSQL), 6379 (Redis), 27017 (MongoDB)"
            exit 0
            ;;
        *)
            if [[ $1 =~ ^[0-9]+$ ]]; then
                PORTS+=("$1")
            else
                echo "Invalid port: $1"
                exit 1
            fi
            shift
            ;;
    esac
done

if [ ${#PORTS[@]} -eq 0 ]; then
    echo "Usage: ./expose.sh <port> [port2...] [--force]"
    exit 1
fi

if [ ! -f "$CONFIG_FILE" ]; then
    echo "Error: No config found. Run ./setup.sh first"
    exit 1
fi

# Check for Internet Gateway
IGW=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c.get('internet_gateway',{}).get('id',''))" 2>/dev/null)
if [ -z "$IGW" ]; then
    echo -e "${YELLOW}Warning: No Internet Gateway configured.${NC}"
    echo "Ports will only be accessible from within the mesh network."
    echo ""
    echo "For public access, run: ./add-public-ip.sh"
    echo ""
fi

# Check for dangerous ports
for PORT in "${PORTS[@]}"; do
    for DPORT in "${DANGEROUS_PORTS[@]}"; do
        if [ "$PORT" -eq "$DPORT" ] && [ "$FORCE" = false ]; then
            echo -e "${RED}🚫 Port $PORT blocked by default.${NC}"
            echo ""
            case $PORT in
                22) echo "SSH exposure is dangerous - consider using mesh-only access." ;;
                3306|5432|6379|27017) echo "Database ports should not be publicly exposed." ;;
                *) echo "This port is considered dangerous." ;;
            esac
            echo ""
            echo "If you must expose it:"
            echo "  ./expose.sh $PORT --force"
            exit 1
        fi
    done
done

# Detect WireGuard interface
WG_IFACE=$(sudo wg show 2>/dev/null | grep "interface:" | awk '{print $2}' | head -1)
if [ -z "$WG_IFACE" ]; then
    WG_IFACE="wg0"
    echo -e "${YELLOW}Note: WireGuard not active, using default interface 'wg0'${NC}"
fi

echo ""
echo "Exposing ports: ${PORTS[*]}"
echo "Interface: $WG_IFACE"
echo ""

# Apply iptables rules
for PORT in "${PORTS[@]}"; do
    echo -n "Opening port $PORT... "
    
    # Check if rule already exists
    if sudo iptables -C INPUT -i "$WG_IFACE" -p tcp --dport "$PORT" -j ACCEPT 2>/dev/null; then
        echo "already open"
    else
        sudo iptables -A INPUT -i "$WG_IFACE" -p tcp --dport "$PORT" -j ACCEPT
        echo -e "${GREEN}OK${NC}"
    fi
done

# Update config
python3 << EOF
import json
with open('$CONFIG_FILE', 'r') as f:
    config = json.load(f)
existing = set(config.get('exposed_ports', []))
existing.update([int(p) for p in "${PORTS[*]}".split()])
config['exposed_ports'] = sorted(list(existing))
with open('$CONFIG_FILE', 'w') as f:
    json.dump(config, f, indent=2)
EOF

PUBLIC_IP=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c.get('internet_gateway',{}).get('public_ip','(mesh only)'))" 2>/dev/null)

echo ""
echo -e "${GREEN}✅ Ports exposed!${NC}"
echo ""
echo "Access via: $PUBLIC_IP"
for PORT in "${PORTS[@]}"; do
    echo "  - $PUBLIC_IP:$PORT"
done
echo ""
echo "To close: ./unexpose.sh ${PORTS[*]}"


path: join.sh
content: #!/bin/bash
#
# Telnyx Network - Join
# Add this machine to the mesh network
#
# Usage: ./join.sh --name my-machine [--apply]
#

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="$SCRIPT_DIR/config.json"
API_BASE="https://api.telnyx.com/v2"

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

# Defaults
NAME=""
APPLY=false

# Parse args
while [[ $# -gt 0 ]]; do
    case $1 in
        --name) NAME="$2"; shift 2 ;;
        --apply) APPLY=true; shift ;;
        -h|--help)
            echo "Usage: ./join.sh --name <peer-name> [--apply]"
            echo ""
            echo "Options:"
            echo "  --name     Name for this peer (required)"
            echo "  --apply    Automatically apply WireGuard config"
            exit 0
            ;;
        *) echo "Unknown option: $1"; exit 1 ;;
    esac
done

# Check config exists
if [ ! -f "$CONFIG_FILE" ]; then
    echo -e "${RED}Error: No config found. Run ./setup.sh first${NC}"
    exit 1
fi

# Check API key
if [ -z "$TELNYX_API_KEY" ]; then
    if [ -f "$SCRIPT_DIR/.env" ]; then
        source "$SCRIPT_DIR/.env"
    fi
fi

if [ -z "$TELNYX_API_KEY" ]; then
    echo -e "${RED}Error: TELNYX_API_KEY not set${NC}"
    exit 1
fi

# Check name
if [ -z "$NAME" ]; then
    echo -e "${RED}Error: --name required${NC}"
    echo "Example: ./join.sh --name my-laptop"
    exit 1
fi

# Load config
WG_ID=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c['wireguard_gateway']['id'])")
WG_ENDPOINT=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c['wireguard_gateway']['endpoint'])")
WG_PUBKEY=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c['wireguard_gateway']['public_key'])")
WG_SUBNET=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c['wireguard_gateway']['subnet'].split('/')[0].rsplit('.', 1)[0])")

echo ""
echo -e "${GREEN}🔗 Joining Telnyx Network${NC}"
echo "=========================="
echo ""
echo "Peer name: $NAME"
echo ""

# Create peer
echo -n "Creating peer... "
PEER_RESP=$(curl -s -X POST "$API_BASE/wireguard_peers" \
    -H "Authorization: Bearer $TELNYX_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"name\": \"$NAME\", \"wireguard_interface_id\": \"$WG_ID\"}")

PEER_ID=$(echo "$PEER_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('data',{}).get('id',''))" 2>/dev/null)
PEER_PRIVKEY=$(echo "$PEER_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('data',{}).get('private_key',''))" 2>/dev/null)
PEER_PUBKEY=$(echo "$PEER_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('data',{}).get('public_key',''))" 2>/dev/null)

if [ -z "$PEER_ID" ]; then
    echo -e "${RED}FAILED${NC}"
    echo "$PEER_RESP"
    exit 1
fi
echo -e "${GREEN}OK${NC}"

# Get config from API
echo -n "Fetching WireGuard config... "
CONF_RESP=$(curl -s "$API_BASE/wireguard_peers/$PEER_ID/config" \
    -H "Authorization: Bearer $TELNYX_API_KEY")

# The config needs the private key inserted
CONF_FILE="$SCRIPT_DIR/wg-$NAME.conf"
# Use awk instead of sed for macOS compatibility (private key may contain special chars)
echo "$CONF_RESP" | awk -v key="$PEER_PRIVKEY" '/^PrivateKey =/{print "PrivateKey = " key; next} {print}' > "$CONF_FILE"
echo -e "${GREEN}OK${NC}"

# Extract assigned IP
PEER_IP=$(grep "Address" "$CONF_FILE" | cut -d'=' -f2 | tr -d ' ')

# Update config.json with new peer
python3 << EOF
import json
with open('$CONFIG_FILE', 'r') as f:
    config = json.load(f)
config['peers'].append({
    'id': '$PEER_ID',
    'name': '$NAME',
    'ip': '$PEER_IP',
    'public_key': '$PEER_PUBKEY'
})
with open('$CONFIG_FILE', 'w') as f:
    json.dump(config, f, indent=2)
EOF

echo ""
echo -e "${GREEN}✅ Peer created!${NC}"
echo ""
echo "Your IP: $PEER_IP"
echo "Config:  $CONF_FILE"
echo ""

if [ "$APPLY" = true ]; then
    echo "Applying WireGuard config..."
    if command -v wg-quick &> /dev/null; then
        sudo wg-quick up "$CONF_FILE"
        echo -e "${GREEN}✅ Connected!${NC}"
    else
        echo -e "${RED}Error: wg-quick not found. Install WireGuard first.${NC}"
        exit 1
    fi
else
    echo "To connect:"
    echo "  sudo wg-quick up $CONF_FILE"
    echo ""
    echo "Or with --apply flag:"
    echo "  ./join.sh --name $NAME --apply"
fi

echo ""
echo -e "${YELLOW}⚠️  Save your private key! It's only shown once.${NC}"
echo "   Config file: $CONF_FILE"


path: peers.sh
content: #!/bin/bash
#
# Telnyx Network - List Peers
#

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="$SCRIPT_DIR/config.json"
API_BASE="https://api.telnyx.com/v2"

if [ ! -f "$CONFIG_FILE" ]; then
    echo "Error: No config found. Run ./setup.sh first"
    exit 1
fi

if [ -z "$TELNYX_API_KEY" ]; then
    if [ -f "$SCRIPT_DIR/.env" ]; then
        source "$SCRIPT_DIR/.env"
    fi
fi

WG_ID=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c['wireguard_gateway']['id'])")

echo ""
echo "🔗 Telnyx Network Peers"
echo "======================="
echo ""

# Fetch live peer data
PEERS=$(curl -s "$API_BASE/wireguard_peers?filter[wireguard_interface_id]=$WG_ID" \
    -H "Authorization: Bearer $TELNYX_API_KEY")

echo "$PEERS" | python3 << 'EOF'
import sys, json
from datetime import datetime

data = json.load(sys.stdin)
peers = data.get('data', [])

if not peers:
    print("No peers found.")
    print("")
    print("Add one with: ./join.sh --name my-machine")
else:
    print(f"{'NAME':<20} {'IP':<18} {'STATUS':<10} {'LAST SEEN'}")
    print("-" * 70)
    for p in peers:
        name = p.get('name', 'unknown')[:20]
        # Get peer IP from config endpoint
        last_seen = p.get('last_seen')
        if last_seen:
            status = "online"
            seen = last_seen[:19].replace('T', ' ')
        else:
            status = "never"
            seen = "-"
        # IP would need separate lookup, show placeholder
        print(f"{name:<20} {'(see config)':<18} {status:<10} {seen}")
EOF

echo ""


path: register.sh
content: #!/bin/bash
# Register this node on the Telnyx mesh registry
# Usage: register.sh --name <name> [--bucket <bucket>]

set -e

# Defaults
BUCKET="${TELNYX_MESH_BUCKET:-openclaw-mesh}"
NODE_NAME=""
METADATA=""

# Parse args
while [[ $# -gt 0 ]]; do
    case $1 in
        --name|-n)
            NODE_NAME="$2"
            shift 2
            ;;
        --bucket|-b)
            BUCKET="$2"
            shift 2
            ;;
        --meta|-m)
            METADATA="$2"
            shift 2
            ;;
        --help|-h)
            echo "Usage: register.sh --name <name> [--bucket <bucket>] [--meta <json>]"
            echo ""
            echo "Register this node on the Telnyx mesh registry."
            echo ""
            echo "Options:"
            echo "  --name, -n    Node name (required)"
            echo "  --bucket, -b  Registry bucket (default: openclaw-mesh)"
            echo "  --meta, -m    Additional metadata as JSON"
            echo ""
            echo "Environment:"
            echo "  TELNYX_MESH_BUCKET  Default bucket name"
            echo ""
            echo "Example:"
            echo "  register.sh --name home-server"
            echo "  register.sh --name laptop --meta '{\"owner\":\"john\"}'"
            exit 0
            ;;
        *)
            echo "Unknown option: $1"
            exit 1
            ;;
    esac
done

if [[ -z "$NODE_NAME" ]]; then
    echo "❌ --name is required"
    echo "Usage: register.sh --name <name>"
    exit 1
fi

# Check telnyx CLI
if ! command -v telnyx &> /dev/null; then
    echo "❌ Telnyx CLI not found. Install with: npm install -g @telnyx/api-cli"
    exit 1
fi

# Get WireGuard interface IP
WG_IP=""
if command -v wg &> /dev/null; then
    # Try to get IP from telnyx0 interface
    # macOS uses ifconfig, Linux uses ip
    if command -v ip &> /dev/null; then
        WG_IP=$(ip -4 addr show telnyx0 2>/dev/null | grep -oE 'inet [0-9.]+' | awk '{print $2}')
    fi
    if [[ -z "$WG_IP" ]] && command -v ifconfig &> /dev/null; then
        WG_IP=$(ifconfig telnyx0 2>/dev/null | grep -oE 'inet [0-9.]+' | awk '{print $2}')
    fi
fi

if [[ -z "$WG_IP" ]]; then
    echo "⚠️  Could not detect WireGuard IP. Is the mesh running?"
    echo "   Run: scripts/join.sh --name $NODE_NAME --apply"
    exit 1
fi

# Build registration JSON
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
HOSTNAME=$(hostname)

REG_JSON=$(cat <<EOF
{
  "name": "$NODE_NAME",
  "ip": "$WG_IP",
  "hostname": "$HOSTNAME",
  "registered_at": "$TIMESTAMP",
  "metadata": ${METADATA:-null}
}
EOF
)

echo "🔄 Registering node on mesh..."
echo "   Name: $NODE_NAME"
echo "   IP: $WG_IP"
echo "   Bucket: $BUCKET"

# Ensure bucket exists
if ! telnyx storage bucket list 2>/dev/null | grep -q "$BUCKET"; then
    echo "📦 Creating registry bucket: $BUCKET"
    telnyx storage bucket create "$BUCKET" > /dev/null
fi

# Write registration
TEMP_FILE=$(mktemp)
echo "$REG_JSON" > "$TEMP_FILE"
telnyx storage object put "$BUCKET" "$TEMP_FILE" -k "nodes/$NODE_NAME.json" > /dev/null
rm "$TEMP_FILE"

echo ""
echo "✅ Node registered!"
echo ""
echo "Other nodes can discover you with:"
echo "   scripts/discover.sh"
echo ""
echo "Registration:"
echo "$REG_JSON" | python3 -m json.tool 2>/dev/null || echo "$REG_JSON"


path: setup-sudoers.sh
content: #!/bin/bash
#
# setup-sudoers.sh - Enable passwordless sudo for WireGuard (one-time setup)
#
# This allows OpenClaw to manage WireGuard interfaces autonomously.
# Run once with: sudo ./setup-sudoers.sh
#

set -e

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m'

# Detect WireGuard binary locations
WG_PATHS=""
for path in /usr/bin/wg /usr/bin/wg-quick /opt/homebrew/bin/wg /opt/homebrew/bin/wg-quick /usr/local/bin/wg /usr/local/bin/wg-quick; do
    if [ -x "$path" ]; then
        if [ -n "$WG_PATHS" ]; then
            WG_PATHS="$WG_PATHS, $path"
        else
            WG_PATHS="$path"
        fi
    fi
done

if [ -z "$WG_PATHS" ]; then
    echo -e "${RED}❌ WireGuard not found${NC}"
    echo "Install WireGuard first:"
    echo "  macOS:  brew install wireguard-tools"
    echo "  Ubuntu: sudo apt install wireguard"
    exit 1
fi

# Get the actual user (not root if running with sudo)
TARGET_USER="${SUDO_USER:-$USER}"

if [ "$TARGET_USER" = "root" ]; then
    echo -e "${RED}❌ Don't run this as root${NC}"
    echo "Run as: sudo ./setup-sudoers.sh"
    exit 1
fi

# Build sudoers line
SUDOERS_LINE="$TARGET_USER ALL=(ALL) NOPASSWD: $WG_PATHS"
SUDOERS_FILE="/etc/sudoers.d/wireguard-$TARGET_USER"

echo ""
echo -e "${YELLOW}🔐 WireGuard Passwordless Sudo Setup${NC}"
echo "========================================"
echo ""
echo "This enables OpenClaw to manage WireGuard without password prompts."
echo ""
echo "User: $TARGET_USER"
echo "File: $SUDOERS_FILE"
echo ""
echo "Will add:"
echo -e "  ${GREEN}$SUDOERS_LINE${NC}"
echo ""
echo "This allows ONLY these specific commands without password:"
echo "$WG_PATHS" | tr ',' '\n' | while read -r path; do
    path=$(echo "$path" | xargs)  # trim whitespace
    [ -n "$path" ] && echo "  • $path"
done
echo ""

# Check if already configured
if [ -f "$SUDOERS_FILE" ]; then
    echo -e "${YELLOW}⚠️  Sudoers file already exists${NC}"
    echo "Current contents:"
    cat "$SUDOERS_FILE"
    echo ""
    read -p "Overwrite? [y/N] " -n 1 -r
    echo ""
    if [[ ! $REPLY =~ ^[Yy]$ ]]; then
        echo "Cancelled."
        exit 0
    fi
fi

# Check if running with sudo
if [ "$EUID" -ne 0 ]; then
    echo -e "${RED}❌ This script needs sudo${NC}"
    echo "Run as: sudo ./setup-sudoers.sh"
    exit 1
fi

read -p "Continue? [y/N] " -n 1 -r
echo ""

if [[ $REPLY =~ ^[Yy]$ ]]; then
    # Write sudoers file
    echo "$SUDOERS_LINE" > "$SUDOERS_FILE"
    chmod 440 "$SUDOERS_FILE"
    
    # Validate syntax
    if visudo -c -f "$SUDOERS_FILE" > /dev/null 2>&1; then
        echo ""
        echo -e "${GREEN}✅ Done!${NC}"
        echo ""
        echo "OpenClaw can now manage WireGuard without password prompts."
        echo ""
        echo "Test it:"
        echo "  sudo wg --version"
        echo ""
        echo "To undo later:"
        echo "  sudo rm $SUDOERS_FILE"
    else
        echo -e "${RED}❌ Invalid sudoers syntax - removing${NC}"
        rm -f "$SUDOERS_FILE"
        exit 1
    fi
else
    echo "Cancelled."
fi


path: setup.sh
content: #!/bin/bash
#
# Telnyx Network - Setup
# Creates network + WireGuard gateway
#
# Usage: ./setup.sh --region ashburn-va [--name my-network]
#

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="$SCRIPT_DIR/config.json"
API_BASE="https://api.telnyx.com/v2"

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

# Defaults
REGION=""
NAME="openclaw-network"

# Parse args
while [[ $# -gt 0 ]]; do
    case $1 in
        --region) REGION="$2"; shift 2 ;;
        --name) NAME="$2"; shift 2 ;;
        -h|--help)
            echo "Usage: ./setup.sh --region <region-code> [--name <network-name>]"
            echo ""
            echo "Regions: ashburn-va, chicago-il, frankfurt-de, amsterdam-nl, singapore-sg"
            echo ""
            echo "Creates:"
            echo "  - Network (free)"
            echo "  - WireGuard Gateway (\$10/mo)"
            exit 0
            ;;
        *) echo "Unknown option: $1"; exit 1 ;;
    esac
done

# Check API key
if [ -z "$TELNYX_API_KEY" ]; then
    # Try .env file
    if [ -f "$SCRIPT_DIR/.env" ]; then
        source "$SCRIPT_DIR/.env"
    fi
fi

if [ -z "$TELNYX_API_KEY" ]; then
    echo -e "${RED}Error: TELNYX_API_KEY not set${NC}"
    echo "Set it: export TELNYX_API_KEY=\"KEY...\""
    exit 1
fi

# Check region
if [ -z "$REGION" ]; then
    echo -e "${RED}Error: --region required${NC}"
    echo "Example: ./setup.sh --region ashburn-va"
    echo ""
    echo "Available regions:"
    curl -s "$API_BASE/network_coverage?filter[available_services][contains]=cloud_vpn" \
        -H "Authorization: Bearer $TELNYX_API_KEY" | \
        python3 -c "import sys,json; data=json.load(sys.stdin); [print(f\"  {d['location']['code']:20} {d['location']['name']}\") for d in data.get('data',[])]" 2>/dev/null || echo "  (run with API key to see regions)"
    exit 1
fi

echo ""
echo -e "${GREEN}🔧 Telnyx Network Setup${NC}"
echo "========================"
echo ""
echo "Region: $REGION"
echo "Name:   $NAME"
echo ""

# Step 1: Create network
echo -n "Creating network... "
NETWORK_RESP=$(curl -s -X POST "$API_BASE/networks" \
    -H "Authorization: Bearer $TELNYX_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"name\": \"$NAME\"}")

NETWORK_ID=$(echo "$NETWORK_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('data',{}).get('id',''))" 2>/dev/null)

if [ -z "$NETWORK_ID" ]; then
    echo -e "${RED}FAILED${NC}"
    echo "$NETWORK_RESP"
    exit 1
fi
echo -e "${GREEN}OK${NC} ($NETWORK_ID)"

# Step 2: Create WireGuard Gateway
echo -n "Creating WireGuard gateway... "
WG_RESP=$(curl -s -X POST "$API_BASE/wireguard_interfaces" \
    -H "Authorization: Bearer $TELNYX_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"network_id\": \"$NETWORK_ID\", \"name\": \"$NAME-wg\", \"region_code\": \"$REGION\"}")

WG_ID=$(echo "$WG_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('data',{}).get('id',''))" 2>/dev/null)
WG_ENDPOINT=$(echo "$WG_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('data',{}).get('endpoint',''))" 2>/dev/null)
WG_PUBKEY=$(echo "$WG_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('data',{}).get('public_key',''))" 2>/dev/null)
WG_SUBNET=$(echo "$WG_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('data',{}).get('server_ip_address',''))" 2>/dev/null)

if [ -z "$WG_ID" ]; then
    echo -e "${RED}FAILED${NC}"
    echo "$WG_RESP"
    exit 1
fi
echo -e "${GREEN}OK${NC} ($WG_ID)"

# Save config
cat > "$CONFIG_FILE" << EOF
{
  "network_id": "$NETWORK_ID",
  "network_name": "$NAME",
  "region": "$REGION",
  "wireguard_gateway": {
    "id": "$WG_ID",
    "endpoint": "$WG_ENDPOINT",
    "public_key": "$WG_PUBKEY",
    "subnet": "$WG_SUBNET"
  },
  "internet_gateway": null,
  "peers": [],
  "exposed_ports": []
}
EOF

echo ""
echo -e "${GREEN}✅ Setup complete!${NC}"
echo ""
echo "WireGuard Gateway:"
echo "  Endpoint: $WG_ENDPOINT"
echo "  Subnet:   $WG_SUBNET"
echo ""
echo "Next steps:"
echo "  1. Wait ~5 min for gateway to provision"
echo "  2. Run: ./join.sh --name \"my-machine\""
echo ""
echo "Note: WireGuard Gateway costs \$10/month"


path: status.sh
content: #!/bin/bash
#
# Telnyx Network - Status
#

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="$SCRIPT_DIR/config.json"
API_BASE="https://api.telnyx.com/v2"

GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'

if [ ! -f "$CONFIG_FILE" ]; then
    echo "Error: No config found. Run ./setup.sh first"
    exit 1
fi

if [ -z "$TELNYX_API_KEY" ]; then
    if [ -f "$SCRIPT_DIR/.env" ]; then
        source "$SCRIPT_DIR/.env"
    fi
fi

echo ""
echo "📊 Telnyx Network Status"
echo "========================"
echo ""

# Parse config
python3 << EOF
import json

with open('$CONFIG_FILE') as f:
    c = json.load(f)

print(f"Network:    {c['network_name']} ({c['network_id'][:8]}...)")
print(f"Region:     {c['region']}")
print()

wg = c['wireguard_gateway']
print("WireGuard Gateway:")
print(f"  Endpoint: {wg['endpoint']}")
print(f"  Subnet:   {wg['subnet']}")
print()

igw = c.get('internet_gateway')
if igw:
    print("Internet Gateway:")
    print(f"  Public IP: {igw['public_ip']}")
    print()
else:
    print("Internet Gateway: Not configured")
    print("  (Run ./add-public-ip.sh to get a public IP)")
    print()

peers = c.get('peers', [])
print(f"Peers: {len(peers)}")
for p in peers:
    print(f"  - {p['name']}: {p['ip']}")
print()

exposed = c.get('exposed_ports', [])
if exposed:
    print(f"Exposed Ports: {', '.join(map(str, exposed))}")
else:
    print("Exposed Ports: None")
print()
EOF

# Check local WireGuard status
echo "Local WireGuard:"
if command -v wg &> /dev/null; then
    if sudo wg show 2>/dev/null | grep -q "interface"; then
        echo -e "  Status: ${GREEN}Connected${NC}"
        sudo wg show | grep -E "interface|endpoint|latest handshake" | sed 's/^/  /'
    else
        echo -e "  Status: ${YELLOW}Not connected${NC}"
        echo "  Run: sudo wg-quick up <config-file>"
    fi
else
    echo -e "  Status: ${RED}WireGuard not installed${NC}"
fi
echo ""


path: teardown.sh
content: #!/bin/bash
#
# Telnyx Network - Teardown
# Removes all resources
#
# Usage: ./teardown.sh [--yes]
#

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="$SCRIPT_DIR/config.json"
API_BASE="https://api.telnyx.com/v2"

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

SKIP_CONFIRM=false

while [[ $# -gt 0 ]]; do
    case $1 in
        --yes|-y) SKIP_CONFIRM=true; shift ;;
        *) shift ;;
    esac
done

if [ ! -f "$CONFIG_FILE" ]; then
    echo "No config found. Nothing to tear down."
    exit 0
fi

if [ -z "$TELNYX_API_KEY" ]; then
    if [ -f "$SCRIPT_DIR/.env" ]; then
        source "$SCRIPT_DIR/.env"
    fi
fi

if [ -z "$TELNYX_API_KEY" ]; then
    echo -e "${RED}Error: TELNYX_API_KEY not set${NC}"
    exit 1
fi

# Show what will be deleted
echo ""
echo -e "${YELLOW}⚠️  This will delete:${NC}"
echo ""
python3 << EOF
import json
with open('$CONFIG_FILE') as f:
    c = json.load(f)
print(f"  Network:          {c['network_name']}")
print(f"  WireGuard Gateway: {c['wireguard_gateway']['id'][:8]}...")
if c.get('internet_gateway'):
    print(f"  Internet Gateway:  {c['internet_gateway']['id'][:8]}...")
    print(f"  Public IP:         {c['internet_gateway']['public_ip']}")
print(f"  Peers:             {len(c.get('peers', []))}")
EOF
echo ""

if [ "$SKIP_CONFIRM" = false ]; then
    read -p "Are you sure? [y/N] " -n 1 -r
    echo
    if [[ ! $REPLY =~ ^[Yy]$ ]]; then
        echo "Cancelled."
        exit 0
    fi
fi

echo ""
echo "Tearing down..."

# Disconnect local WireGuard
for conf in "$SCRIPT_DIR"/wg-*.conf; do
    if [ -f "$conf" ]; then
        echo -n "Disconnecting $(basename "$conf")... "
        sudo wg-quick down "$conf" 2>/dev/null || true
        echo "done"
    fi
done

# Delete Internet Gateway
IGW_ID=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c.get('internet_gateway',{}).get('id',''))" 2>/dev/null)
if [ -n "$IGW_ID" ]; then
    echo -n "Deleting Internet Gateway... "
    curl -s -X DELETE "$API_BASE/public_internet_gateways/$IGW_ID" \
        -H "Authorization: Bearer $TELNYX_API_KEY" > /dev/null
    echo "done"
fi

# Delete peers
PEER_IDS=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(' '.join([p['id'] for p in c.get('peers',[])]))" 2>/dev/null)
for PEER_ID in $PEER_IDS; do
    echo -n "Deleting peer $PEER_ID... "
    curl -s -X DELETE "$API_BASE/wireguard_peers/$PEER_ID" \
        -H "Authorization: Bearer $TELNYX_API_KEY" > /dev/null
    echo "done"
done

# Delete WireGuard Gateway
WG_ID=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c['wireguard_gateway']['id'])")
echo -n "Deleting WireGuard Gateway... "
curl -s -X DELETE "$API_BASE/wireguard_interfaces/$WG_ID" \
    -H "Authorization: Bearer $TELNYX_API_KEY" > /dev/null
echo "done"

# Delete Network
NETWORK_ID=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c['network_id'])")
echo -n "Deleting Network... "
curl -s -X DELETE "$API_BASE/networks/$NETWORK_ID" \
    -H "Authorization: Bearer $TELNYX_API_KEY" > /dev/null
echo "done"

# Clean up local files
rm -f "$SCRIPT_DIR"/wg-*.conf
rm -f "$CONFIG_FILE"

echo ""
echo -e "${GREEN}✅ Teardown complete${NC}"


path: unexpose.sh
content: #!/bin/bash
#
# Telnyx Network - Unexpose Ports
#
# Usage: ./unexpose.sh <port> [port2...]
#

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="$SCRIPT_DIR/config.json"

GREEN='\033[0;32m'
NC='\033[0m'

PORTS=()

while [[ $# -gt 0 ]]; do
    if [[ $1 =~ ^[0-9]+$ ]]; then
        PORTS+=("$1")
    fi
    shift
done

if [ ${#PORTS[@]} -eq 0 ]; then
    echo "Usage: ./unexpose.sh <port> [port2...]"
    exit 1
fi

if [ ! -f "$CONFIG_FILE" ]; then
    echo "Error: No config found"
    exit 1
fi

# Detect WireGuard interface
WG_IFACE=$(sudo wg show 2>/dev/null | grep "interface:" | awk '{print $2}' | head -1)
if [ -z "$WG_IFACE" ]; then
    WG_IFACE="wg0"
fi

echo ""
echo "Closing ports: ${PORTS[*]}"
echo ""

for PORT in "${PORTS[@]}"; do
    echo -n "Closing port $PORT... "
    if sudo iptables -D INPUT -i "$WG_IFACE" -p tcp --dport "$PORT" -j ACCEPT 2>/dev/null; then
        echo -e "${GREEN}OK${NC}"
    else
        echo "not found"
    fi
done

# Update config
python3 << EOF
import json
with open('$CONFIG_FILE', 'r') as f:
    config = json.load(f)
existing = set(config.get('exposed_ports', []))
to_remove = set([int(p) for p in "${PORTS[*]}".split()])
config['exposed_ports'] = sorted(list(existing - to_remove))
with open('$CONFIG_FILE', 'w') as f:
    json.dump(config, f, indent=2)
EOF

echo ""
echo -e "${GREEN}✅ Ports closed${NC}"


path: unregister.sh
content: #!/bin/bash
# Unregister this node from the Telnyx mesh registry
# Usage: unregister.sh --name <name> [--bucket <bucket>]

set -e

# Defaults
BUCKET="${TELNYX_MESH_BUCKET:-openclaw-mesh}"
NODE_NAME=""

# Parse args
while [[ $# -gt 0 ]]; do
    case $1 in
        --name|-n)
            NODE_NAME="$2"
            shift 2
            ;;
        --bucket|-b)
            BUCKET="$2"
            shift 2
            ;;
        --help|-h)
            echo "Usage: unregister.sh --name <name> [--bucket <bucket>]"
            echo ""
            echo "Remove a node from the Telnyx mesh registry."
            echo ""
            echo "Options:"
            echo "  --name, -n    Node name (required)"
            echo "  --bucket, -b  Registry bucket (default: openclaw-mesh)"
            exit 0
            ;;
        *)
            echo "Unknown option: $1"
            exit 1
            ;;
    esac
done

if [[ -z "$NODE_NAME" ]]; then
    echo "❌ --name is required"
    echo "Usage: unregister.sh --name <name>"
    exit 1
fi

# Check telnyx CLI
if ! command -v telnyx &> /dev/null; then
    echo "❌ Telnyx CLI not found. Install with: npm install -g @telnyx/api-cli"
    exit 1
fi

echo "🔄 Unregistering node: $NODE_NAME"

# Delete registration (--force skips confirmation prompt)
if telnyx storage object delete "$BUCKET" "nodes/$NODE_NAME.json" --force 2>/dev/null; then
    echo "✅ Node unregistered"
else
    echo "⚠️  Node not found or already unregistered"
fi


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)