Skill: roasbeef/aperture (version 1.0.0)

--- name: aperture description: Install and run Aperture, the L402 Lightning reverse proxy from Lightning Labs. Use when creating L402 paywalls, configuring paid API endpoints, hosting paid content for other agents, or testing L402 authentication flows. --- # Aperture - L402 Lightning Reverse Proxy Aperture is a reverse proxy that implements the L402 protocol, enabling payment-gated API access via the Lightning Network. It sits in front of your backend services and requires Lightning micropayments before granting access. **Source:** `github.com/lightninglabs/aperture` ## Quick Start ```bash # 1. Install aperture skills/aperture/scripts/install.sh # 2. Generate config (connects to local lnd) skills/aperture/scripts/setup.sh # 3. Ensure invoice.macaroon exists (required for L402 invoice creation) # If not present, bake one with the macaroon-bakery skill: skills/macaroon-bakery/scripts/bake.sh --role invoice-only \ --save-to ~/.lnd/data/chain/bitcoin/mainnet/invoice.macaroon # 4. Start aperture skills/aperture/scripts/start.sh # 5. Test with lnget lnget -k --no-pay https://localhost:8081/api/test ``` ## How Aperture Works 1. Client requests a protected resource through Aperture 2. Aperture responds with HTTP 402 + `WWW-Authenticate: L402` header containing a macaroon and a Lightning invoice 3. Client pays the invoice and obtains the preimage 4. Client retries with `Authorization: L402 <macaroon>:<preimage>` 5. Aperture validates the token and proxies to the backend service ## Installation ```bash skills/aperture/scripts/install.sh ``` This will: - Verify Go is installed - Run `go install github.com/lightninglabs/aperture/cmd/aperture@latest` - Verify `aperture` is on `$PATH` To install manually: ```bash go install github.com/lightninglabs/aperture/cmd/aperture@latest ``` Or build from source: ```bash git clone https://github.com/lightninglabs/aperture.git cd aperture make install ``` ## Setup ```bash skills/aperture/scripts/setup.sh ``` This generates `~/.aperture/aperture.yaml` from the config template with sensible defaults. The setup script auto-detects the local lnd node paths. **Options:** ```bash # Custom network setup.sh --network testnet # Custom lnd paths setup.sh --lnd-host localhost:10009 \ --lnd-tls ~/.lnd/tls.cert \ --lnd-macdir ~/.lnd/data/chain/bitcoin/mainnet # Custom listen port setup.sh --port 8081 # Disable TLS (development only) setup.sh --insecure # Disable auth (no payments required) setup.sh --no-auth ``` ## Running Aperture ### Start ```bash skills/aperture/scripts/start.sh ``` Starts aperture as a background process reading `~/.aperture/aperture.yaml`. **Options:** ```bash start.sh --foreground # Run in foreground start.sh --config /path/to # Custom config path ``` ### Stop ```bash skills/aperture/scripts/stop.sh ``` ## Configuration Config file: `~/.aperture/aperture.yaml` ### Invoice Macaroon Requirement Aperture requires `invoice.macaroon` in the configured `macdir` to create Lightning invoices for L402 challenges. This is **not** the same as `admin.macaroon`. If the macaroon is missing, aperture will fail to start or will return errors when clients request paid resources. To bake an invoice macaroon with the macaroon-bakery skill: ```bash skills/macaroon-bakery/scripts/bake.sh --role invoice-only \ --save-to ~/.lnd/data/chain/bitcoin/mainnet/invoice.macaroon ``` The `setup.sh` script will warn you if `invoice.macaroon` is not found at the expected path. ### Minimal Agent Configuration This is the minimal config for an agent hosting paid endpoints with a local lnd node: ```yaml listenaddr: "localhost:8081" insecure: true debuglevel: "info" dbbackend: "sqlite" sqlite: dbfile: "~/.aperture/aperture.db" authenticator: network: "mainnet" lndhost: "localhost:10009" tlspath: "~/.lnd/tls.cert" macdir: "~/.lnd/data/chain/bitcoin/mainnet" services: - name: "my-api" hostregexp: ".*" pathregexp: "^/api/.*$" address: "127.0.0.1:8080" protocol: http price: 100 ``` ### Service Configuration Each service entry defines a backend to protect: ```yaml services: - name: "service-name" # Match requests by host (regex). hostregexp: "^api.example.com$" # Match requests by path (regex). pathregexp: "^/paid/.*$" # Backend address to proxy to. address: "127.0.0.1:8080" # Protocol: http or https. protocol: http # Static price in satoshis. price: 100 # Macaroon capabilities granted at base tier. capabilities: "read,write" # Token expiry in seconds (31557600 = 1 year). timeout: 31557600 # Paths exempt from payment. authwhitelistpaths: - "^/health$" - "^/public/.*$" # Per-endpoint rate limits (token bucket). ratelimits: - pathregexp: "^/api/query.*$" requests: 10 per: 1s burst: 20 ``` ### Authentication Backends #### Direct LND Connection ```yaml authenticator: network: "mainnet" lndhost: "localhost:10009" tlspath: "~/.lnd/tls.cert" macdir: "~/.lnd/data/chain/bitcoin/mainnet" ``` #### Lightning Node Connect (LNC) ```yaml authenticator: network: "mainnet" passphrase: "your-pairing-phrase" mailboxaddress: "mailbox.terminal.lightning.today:443" ``` #### Disable Authentication ```yaml authenticator: disable: true ``` ### Database Backends **SQLite (recommended for agents):** ```yaml dbbackend: "sqlite" sqlite: dbfile: "~/.aperture/aperture.db" ``` **PostgreSQL:** ```yaml dbbackend: "postgres" postgres: host: "localhost" port: 5432 user: "aperture" [REDACTED_SECRET]" dbname: "aperture" ``` ### TLS Configuration ```yaml # Auto Let's Encrypt certificate. autocert: true servername: "api.example.com" # Or disable TLS (development/local only). insecure: true ``` If neither is set, Aperture generates self-signed certs in `~/.aperture/`. ### Dynamic Pricing Connect to a gRPC price server instead of static prices: ```yaml services: - name: "my-api" dynamicprice: enabled: true grpcaddress: "127.0.0.1:10010" insecure: false tlscertpath: "/path/to/pricer/tls.cert" ``` ## Hosting Paid Content for Agents A common pattern is hosting information that other agents pay to access: ```bash # 1. Start a simple HTTP backend with your content mkdir -p /tmp/paid-content echo '{"data": "valuable information"}' > /tmp/paid-content/info.json cd /tmp/paid-content && python3 -m http.server 8080 & # 2. Configure aperture to protect it skills/aperture/scripts/setup.sh --insecure --port 8081 # 3. Start aperture skills/aperture/scripts/start.sh # 4. Other agents can now pay and fetch lnget --max-cost 100 https://localhost:8081/api/info.json ``` ## Integration with lnget and lnd With all three components running: ```bash # Verify lnd is running skills/lnd/scripts/lncli.sh getinfo # Start aperture (uses same lnd for invoice generation) skills/aperture/scripts/start.sh # Fetch a paid resource lnget --max-cost 1000 https://localhost:8081/api/data # Check tokens lnget tokens list ``` ## File Locations | Path | Purpose | |------|---------| | `~/.aperture/aperture.yaml` | Configuration file | | `~/.aperture/aperture.db` | SQLite database | | `~/.aperture/tls.cert` | TLS certificate | | `~/.aperture/tls.key` | TLS private key | | `~/.aperture/aperture.log` | Log file | ## Troubleshooting ### Port already in use Change `listenaddr` in config to a different port, or use `setup.sh --port`. ### LND connection refused Verify lnd is running and wallet is unlocked. Check `lndhost`, `tlspath`, and `macdir` in the config point to the correct lnd instance. ### No 402 challenge returned Check that the request path matches a service's `pathregexp` and is not in `authwhitelistpaths`. Verify `authenticator.disable` is not `true`. ### Token validation fails The client must present the exact macaroon from the challenge with the correct preimage. Verify the preimage matches the payment hash.

path: templates/docker-compose-aperture.yml
content: # Aperture L402 Reverse Proxy — Docker container on regtest.
#
# Requires a running litd + bitcoind regtest stack (docker-compose-regtest.yml).
# Aperture connects to litd's lnd gRPC for invoice creation and L402 auth.
#
# Usage:
#   docker compose -f docker-compose-aperture.yml up -d
#   docker compose -f docker-compose-aperture.yml down -v
#
# The aperture container runs under Rosetta on Apple Silicon since no
# arm64 image is published.
#
# Ports:
#   8082 — Aperture HTTP proxy (insecure mode for regtest)
#
# A simple backend HTTP server is included as a second container (busybox
# httpd serving static files from /www).

services:
  aperture:
    image: ${APERTURE_IMAGE:-lightninglabs/aperture}:${APERTURE_VERSION:-v0.4.2}
    platform: linux/amd64
    container_name: litd-aperture
    restart: unless-stopped
    depends_on:
      - backend
    entrypoint: ["/bin/sh", "-c", "cp /tmp/aperture.yaml /root/.aperture/aperture.yaml && exec aperture --configfile=/root/.aperture/aperture.yaml"]
    ports:
      - "${APERTURE_PORT:-8082}:8082"
    volumes:
      - aperture-data:/root/.aperture
      - ${APERTURE_CONF_PATH:-./aperture-regtest.yaml}:/tmp/aperture.yaml:ro
      # Mount litd's TLS cert and macaroon from the litd container volume.
      # These are copied in by the test setup after wallet creation.
      - litd-aperture-creds:/root/.lnd-creds:ro
    networks:
      - litd-regtest
      - aperture-net

  backend:
    image: busybox:latest
    container_name: litd-backend
    restart: unless-stopped
    # Serve a simple HTTP response on port 9999.
    command: ["sh", "-c", "mkdir -p /www/api && echo '{\"status\":\"ok\",\"message\":\"Hello from L402 backend\"}' > /www/api/data.json && echo '{\"status\":\"ok\"}' > /www/health && httpd -f -p 9999 -h /www"]
    networks:
      - aperture-net

volumes:
  aperture-data:
  litd-aperture-creds:
    external: true

networks:
  aperture-net:
    driver: bridge
  litd-regtest:
    external: true
    name: templates_litd-regtest


path: templates/aperture-regtest.yaml
content: # Aperture L402 Reverse Proxy — Regtest Configuration
#
# Used with docker-compose-aperture.yml for integration testing.
# Connects to litd on the litd-regtest Docker network for invoice creation.

# Listen on all interfaces so Docker port mapping works.
listenaddr: "0.0.0.0:8082"

debuglevel: "debug"

# No TLS — regtest only.
insecure: true

# SQLite database for token storage.
dbbackend: "sqlite"
sqlite:
  dbfile: "/root/.aperture/aperture.db"

# L402 authenticator — connects to litd's embedded lnd.
authenticator:
  network: "regtest"
  lndhost: "litd:10009"
  tlspath: "/root/.lnd-creds/tls.cert"
  macdir: "/root/.lnd-creds"

# Backend service protected by L402.
services:
  - name: "test-api"
    hostregexp: ".*"
    pathregexp: '^/api/.*$'
    address: "litd-backend:9999"
    protocol: http
    price: 1
    timeout: 31557600
    authwhitelistpaths:
      - '^/health$'


path: templates/aperture-example.yaml
content: # Aperture L402 Reverse Proxy Configuration Template
#
# Copy to ~/.aperture/aperture.yaml and modify as needed.
# Or use: skills/aperture/scripts/setup.sh to generate automatically.
#
# Documentation: https://github.com/lightninglabs/aperture

# Listen address for incoming requests.
listenaddr: "localhost:8081"

# Log level: trace, debug, info, warn, error, critical.
debuglevel: "info"

# Disable TLS (development only). Set to false for production.
insecure: true

# Database backend: sqlite, postgres, or etcd.
dbbackend: "sqlite"
sqlite:
  dbfile: "~/.aperture/aperture.db"

# LND authentication configuration.
authenticator:
  # Network: regtest, simnet, testnet, mainnet, signet.
  network: "mainnet"

  # Direct lnd connection.
  lndhost: "localhost:10009"
  tlspath: "~/.lnd/tls.cert"
  macdir: "~/.lnd/data/chain/bitcoin/mainnet"

  # Or disable auth entirely for testing.
  # disable: true

# Backend services protected by L402.
services:
  - name: "default-api"
    # Match all hosts.
    hostregexp: ".*"

    # Protect /api/ paths.
    pathregexp: '^/api/.*$'

    # Backend server address.
    address: "127.0.0.1:8080"
    protocol: http

    # Price per request in satoshis.
    price: 100

    # Token expires after 1 year (seconds).
    timeout: 31557600

    # Free paths (no payment required).
    authwhitelistpaths:
      - '^/health$'
      - '^/public/.*$'

    # Optional rate limits.
    # ratelimits:
    #   - pathregexp: '^/api/.*$'
    #     requests: 100
    #     per: 1m
    #     burst: 150


path: scripts/setup.sh
content: #!/usr/bin/env bash
# Generate aperture configuration from template.
#
# Usage:
#   setup.sh                                    # Defaults (mainnet, lnd)
#   setup.sh --network testnet --insecure       # Testnet, no TLS
#   setup.sh --no-auth                          # Disable L402 auth
#   setup.sh --port 8081                        # Custom listen port

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APERTURE_DIR="$HOME/.aperture"
CONFIG_FILE="$APERTURE_DIR/aperture.yaml"
NETWORK="mainnet"
PORT="8081"
INSECURE=false
NO_AUTH=false
LND_HOST="localhost:10009"
LND_TLS="$HOME/.lnd/tls.cert"
LND_MACDIR=""
SERVICE_PORT="8080"
SERVICE_PRICE="100"

# Parse arguments.
while [[ $# -gt 0 ]]; do
    case $1 in
        --network)
            NETWORK="$2"
            shift 2
            ;;
        --port)
            PORT="$2"
            shift 2
            ;;
        --insecure)
            INSECURE=true
            shift
            ;;
        --no-auth)
            NO_AUTH=true
            shift
            ;;
        --lnd-host)
            LND_HOST="$2"
            shift 2
            ;;
        --lnd-tls)
            LND_TLS="$2"
            shift 2
            ;;
        --lnd-macdir)
            LND_MACDIR="$2"
            shift 2
            ;;
        --service-port)
            SERVICE_PORT="$2"
            shift 2
            ;;
        --service-price)
            SERVICE_PRICE="$2"
            shift 2
            ;;
        -h|--help)
            echo "Usage: setup.sh [options]"
            echo ""
            echo "Generate aperture configuration."
            echo ""
            echo "Options:"
            echo "  --network NETWORK        Bitcoin network (default: mainnet)"
            echo "  --port PORT              Listen port (default: 8081)"
            echo "  --insecure               Disable TLS"
            echo "  --no-auth                Disable L402 authentication"
            echo "  --lnd-host HOST          lnd gRPC host (default: localhost:10009)"
            echo "  --lnd-tls PATH           lnd TLS cert path"
            echo "  --lnd-macdir PATH        lnd macaroon directory"
            echo "  --service-port PORT      Backend service port (default: 8080)"
            echo "  --service-price SATS     Price per request in sats (default: 100)"
            exit 0
            ;;
        *)
            echo "Unknown option: $1" >&2
            exit 1
            ;;
    esac
done

# Auto-detect lnd macaroon directory if not specified.
if [ -z "$LND_MACDIR" ]; then
    LND_MACDIR="$HOME/.lnd/data/chain/bitcoin/$NETWORK"
fi

echo "=== Aperture Configuration Setup ==="
echo ""
echo "Network:      $NETWORK"
echo "Listen port:  $PORT"
echo "Insecure:     $INSECURE"
echo "Auth:         $([ "$NO_AUTH" = true ] && echo "disabled" || echo "enabled")"
echo "LND host:     $LND_HOST"
echo "Config:       $CONFIG_FILE"
echo ""

# Create aperture directory.
mkdir -p "$APERTURE_DIR"

# Build config.
cat > "$CONFIG_FILE" << YAML
# Aperture configuration generated by setup.sh
# Edit as needed: ~/.aperture/aperture.yaml

listenaddr: "localhost:$PORT"
debuglevel: "info"
dbbackend: "sqlite"

sqlite:
  dbfile: "$APERTURE_DIR/aperture.db"

insecure: $INSECURE

authenticator:
YAML

if [ "$NO_AUTH" = true ]; then
    cat >> "$CONFIG_FILE" << YAML
  disable: true
YAML
else
    cat >> "$CONFIG_FILE" << YAML
  network: "$NETWORK"
  lndhost: "$LND_HOST"
  tlspath: "$LND_TLS"
  macdir: "$LND_MACDIR"
YAML
fi

cat >> "$CONFIG_FILE" << YAML

services:
  - name: "default-api"
    hostregexp: ".*"
    pathregexp: '^/api/.*$'
    address: "127.0.0.1:$SERVICE_PORT"
    protocol: http
    price: $SERVICE_PRICE
    timeout: 31557600
    authwhitelistpaths:
      - '^/health$'
YAML

echo "Config written to $CONFIG_FILE"
echo ""

# Warn if invoice.macaroon is missing (required for aperture to create invoices).
if [ "$NO_AUTH" != true ]; then
    INVOICE_MACAROON="$LND_MACDIR/invoice.macaroon"
    if [ ! -f "$INVOICE_MACAROON" ]; then
        echo "WARNING: invoice.macaroon not found at $INVOICE_MACAROON"
        echo ""
        echo "Aperture requires invoice.macaroon in the macdir to create invoices."
        echo "Without it, aperture will fail to generate L402 challenges."
        echo ""

        BAKERY_SCRIPT="$SCRIPT_DIR/../../macaroon-bakery/scripts/bake.sh"
        if [ -x "$BAKERY_SCRIPT" ]; then
            echo "To bake one automatically:"
            echo "  $BAKERY_SCRIPT --role invoice-only --save-to $INVOICE_MACAROON"
        else
            echo "To create one, use the macaroon-bakery skill:"
            echo "  skills/macaroon-bakery/scripts/bake.sh --role invoice-only --save-to $INVOICE_MACAROON"
        fi
        echo ""
    fi
fi

echo "Next steps:"
echo "  1. Start a backend on port $SERVICE_PORT"
echo "  2. Start aperture: skills/aperture/scripts/start.sh"
echo "  3. Test: lnget -k https://localhost:$PORT/api/test"


path: scripts/install.sh
content: #!/usr/bin/env bash
# Install aperture from source.
#
# Usage:
#   install.sh                          # Install latest
#   install.sh --version v0.3.3-beta    # Specific version

set -e

VERSION=""

# Parse arguments.
while [[ $# -gt 0 ]]; do
    case $1 in
        --version)
            VERSION="@$2"
            shift 2
            ;;
        -h|--help)
            echo "Usage: install.sh [--version VERSION]"
            echo ""
            echo "Install aperture L402 reverse proxy."
            echo ""
            echo "Options:"
            echo "  --version VERSION  Go module version (e.g., v0.3.3-beta)"
            exit 0
            ;;
        *)
            echo "Unknown option: $1" >&2
            exit 1
            ;;
    esac
done

# Default to @latest when no version specified.
VERSION="${VERSION:-@latest}"

echo "=== Installing Aperture ==="
echo ""

# Verify Go is installed.
if ! command -v go &>/dev/null; then
    echo "Error: Go is not installed." >&2
    echo "Install Go from https://go.dev/dl/" >&2
    exit 1
fi

echo "Go version: $(go version | grep -oE 'go[0-9]+\.[0-9]+')"
echo ""

# Install aperture.
echo "Installing aperture..."
go install "github.com/lightninglabs/aperture/cmd/aperture${VERSION}"
echo "Done."
echo ""

# Verify installation.
if command -v aperture &>/dev/null; then
    echo "aperture installed: $(which aperture)"
else
    echo "Warning: aperture not found on PATH." >&2
    echo "Ensure \$GOPATH/bin is in your PATH." >&2
    echo "  export PATH=\$PATH:\$(go env GOPATH)/bin" >&2
fi

echo ""
echo "Installation complete."
echo ""
echo "Next steps:"
echo "  1. Setup config: skills/aperture/scripts/setup.sh"
echo "  2. Start:        skills/aperture/scripts/start.sh"


path: scripts/stop.sh
content: #!/usr/bin/env bash
# Stop aperture reverse proxy.
#
# Usage:
#   stop.sh            # Graceful stop
#   stop.sh --force    # SIGKILL

set -e

FORCE=false

# Parse arguments.
while [[ $# -gt 0 ]]; do
    case $1 in
        --force)
            FORCE=true
            shift
            ;;
        -h|--help)
            echo "Usage: stop.sh [--force]"
            exit 0
            ;;
        *)
            echo "Unknown option: $1" >&2
            exit 1
            ;;
    esac
done

APERTURE_PID=$(pgrep -x aperture 2>/dev/null || true)
if [ -z "$APERTURE_PID" ]; then
    echo "aperture is not running."
    exit 0
fi

echo "Stopping aperture (PID: $APERTURE_PID)..."

if [ "$FORCE" = true ]; then
    kill -9 "$APERTURE_PID"
    echo "Sent SIGKILL."
else
    kill "$APERTURE_PID"
    echo "Sent SIGTERM."
fi

# Wait for exit.
for i in {1..10}; do
    if ! kill -0 "$APERTURE_PID" 2>/dev/null; then
        echo "aperture stopped."
        exit 0
    fi
    sleep 1
done

echo "Warning: aperture did not exit within 10 seconds." >&2
echo "Use --force to send SIGKILL." >&2
exit 1


path: scripts/start.sh
content: #!/usr/bin/env bash
# Start aperture reverse proxy.
#
# Usage:
#   start.sh                           # Background, default config
#   start.sh --foreground              # Foreground mode
#   start.sh --config /path/to/yaml    # Custom config

set -e

APERTURE_DIR="$HOME/.aperture"
CONFIG_FILE="$APERTURE_DIR/aperture.yaml"
FOREGROUND=false

# Parse arguments.
while [[ $# -gt 0 ]]; do
    case $1 in
        --config)
            CONFIG_FILE="$2"
            shift 2
            ;;
        --foreground)
            FOREGROUND=true
            shift
            ;;
        -h|--help)
            echo "Usage: start.sh [--foreground] [--config PATH]"
            exit 0
            ;;
        *)
            echo "Unknown option: $1" >&2
            exit 1
            ;;
    esac
done

# Verify aperture is installed.
if ! command -v aperture &>/dev/null; then
    echo "Error: aperture not found. Run install.sh first." >&2
    exit 1
fi

# Verify config exists.
if [ ! -f "$CONFIG_FILE" ]; then
    echo "Error: Config not found at $CONFIG_FILE" >&2
    echo "Run setup.sh first to generate config." >&2
    exit 1
fi

# Check if aperture is already running.
if pgrep -x aperture &>/dev/null; then
    echo "aperture is already running (PID: $(pgrep -x aperture))."
    echo "Use stop.sh to stop it first."
    exit 1
fi

echo "=== Starting Aperture ==="
echo "Config: $CONFIG_FILE"
echo ""

LOG_FILE="$APERTURE_DIR/aperture-start.log"

if [ "$FOREGROUND" = true ]; then
    exec aperture --configfile="$CONFIG_FILE"
else
    nohup aperture --configfile="$CONFIG_FILE" \
        > "$LOG_FILE" 2>&1 &
    APERTURE_PID=$!
    echo "aperture started in background (PID: $APERTURE_PID)"
    echo "Log file: $LOG_FILE"

    # Wait briefly and verify it's running.
    sleep 2
    if kill -0 "$APERTURE_PID" 2>/dev/null; then
        echo "aperture is running."
    else
        echo "Error: aperture exited immediately. Check $LOG_FILE" >&2
        tail -20 "$LOG_FILE" 2>/dev/null
        exit 1
    fi
fi


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