# Art DAG L2 Server - ActivityPub Ownership registry and ActivityPub federation for Art DAG. ## What it does - **Registry**: Maintains owned assets with content hashes - **Activities**: Creates signed ownership claims (Create activities) - **Federation**: ActivityPub endpoints for follow/share - **L1 Integration**: Records completed L1 runs as owned assets - **Authentication**: User registration, login, JWT tokens ## Setup ```bash pip install -r requirements.txt # Configure (optional - defaults shown) export ARTDAG_DOMAIN=artdag.rose-ash.com export ARTDAG_USER=giles export ARTDAG_DATA=~/.artdag/l2 export DATABASE_URL=postgresql://artdag:artdag@localhost:5432/artdag export L1_SERVERS=https://celery-artdag.rose-ash.com # Generate signing keys (required for federation) python setup_keys.py # Start server python server.py ``` ## JWT Secret Configuration The JWT secret is used to sign authentication tokens. **Without a persistent secret, tokens are invalidated on server restart.** ### Generate a secret ```bash # Generate a 64-character hex secret openssl rand -hex 32 # Or with Python python -c "import secrets; print(secrets.token_hex(32))" ``` ### Local development ```bash export JWT_SECRET="your-generated-secret-here" python server.py ``` ### Docker Swarm (recommended for production) Create a Docker secret: ```bash # From a generated value openssl rand -hex 32 | docker secret create jwt_secret - # Or from a file echo "your-secret-here" > jwt_secret.txt docker secret create jwt_secret jwt_secret.txt rm jwt_secret.txt ``` Reference in docker-compose.yml: ```yaml services: l2-server: secrets: - jwt_secret secrets: jwt_secret: external: true ``` The server reads secrets from `/run/secrets/jwt_secret` automatically. ## Key Setup ActivityPub requires RSA keys for signing activities. Generate them: ```bash # Local python setup_keys.py # Or with custom paths python setup_keys.py --data-dir /data/l2 --user giles # In Docker, exec into container or mount volume docker exec -it python setup_keys.py ``` Keys are stored in `$ARTDAG_DATA/keys/`: - `{username}.pem` - Private key (chmod 600, NEVER share) - `{username}.pub` - Public key (included in actor profile) **Important**: Private keys are gitignored. Back them up securely. Losing them invalidates all your signatures. ## L1 Renderers Configuration The `L1_SERVERS` environment variable defines which L1 rendering servers are available to users. Users can attach to these servers from the Renderers page to run effects and manage media. ```bash # Single server (default) export L1_SERVERS=https://celery-artdag.rose-ash.com # Multiple servers (comma-separated) export L1_SERVERS=https://celery-artdag.rose-ash.com,https://renderer2.example.com,https://renderer3.example.com ``` When a user attaches to an L1 server: 1. L2 creates a **scoped token** that only works for that specific L1 2. User is redirected to the L1's `/auth` endpoint with the scoped token 3. L1 calls back to L2's `/auth/verify` endpoint to validate the token 4. L2 verifies the token scope matches the requesting L1 5. L1 sets its own local cookie, logging the user in 6. The attachment is recorded in L2's `user_renderers` table ### Security Features **No shared secrets**: L1 servers verify tokens by calling L2's `/auth/verify` endpoint. Any L1 provider can federate without the JWT secret. **L1 authorization**: Only servers listed in `L1_SERVERS` can verify tokens. L1 must identify itself in verify requests. **Scoped tokens**: Tokens issued for attachment contain an `l1_server` claim. A token scoped to L1-A cannot be used on L1-B, preventing malicious L1s from stealing tokens for use elsewhere. **Federated logout**: When a user logs out of L2: 1. L2 revokes the token in its database (`revoked_tokens` table) 2. L2 calls `/auth/revoke` on all attached L1s to revoke their local copies 3. All attachments are removed from `user_renderers` Even if a malicious L1 ignores the revoke call, the token will fail verification at L2 because it's in the revocation table. **Token revocation on L1**: L1 servers maintain their own Redis-based revocation list and check it on every authenticated request. Users can manage attachments at `/renderers`. ## Web UI The server provides a web interface: | Path | Description | |------|-------------| | `/` | Home page with stats and README | | `/assets` | Browse registered assets | | `/asset/{name}` | Asset detail page | | `/activities` | View published activities | | `/activity/{id}` | Activity detail page | | `/users` | List registered users | | `/renderers` | Manage L1 renderer connections | | `/anchors/ui` | OpenTimestamps anchor management | | `/login` | Login page | | `/register` | Registration page | | `/logout` | Log out | | `/download/client` | Download CLI client | ## Client Commands ### Upload Media Register a media asset (image, video, audio) with a content hash: ```bash curl -X POST http://localhost:8200/assets \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "name": "my-video", "content_hash": "abc123...", "asset_type": "video", "tags": ["art", "generated"] }' ``` ### Upload Recipe Record an L1 run as an owned asset. This fetches the run details from the L1 server and registers the output: ```bash curl -X POST http://localhost:8200/assets/record-run \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "run_id": "uuid-from-l1", "l1_server": "https://celery-artdag.rose-ash.com", "output_name": "my-rendered-video" }' ``` ## API Endpoints ### Server Info | Method | Path | Description | |--------|------|-------------| | GET | `/` | Home page with stats | ### Assets | Method | Path | Description | |--------|------|-------------| | GET | `/assets` | List all assets | | GET | `/assets/{name}` | Get asset by name | | POST | `/assets` | Upload media - register new asset | | POST | `/assets/record-run` | Upload recipe - record L1 run | ### ActivityPub | Method | Path | Description | |--------|------|-------------| | GET | `/.well-known/webfinger?resource=acct:user@domain` | Actor discovery | | GET | `/users/{username}` | Actor profile | | GET | `/users/{username}/outbox` | Published activities | | POST | `/users/{username}/inbox` | Receive activities | | GET | `/users/{username}/followers` | Followers list | | GET | `/objects/{content_hash}` | Get object by hash | | GET | `/activities/{index}` | Get activity by index | ### Authentication | Method | Path | Description | |--------|------|-------------| | POST | `/auth/register` | Register new user (API) | | POST | `/auth/login` | Login, get JWT token (API) | | GET | `/auth/me` | Get current user | | POST | `/auth/verify` | Verify token (for L1 servers) | ## Data Storage Data stored in PostgreSQL: - `users` - Registered users - `assets` - Asset registry - `activities` - Signed activities - `followers` - Followers list RSA keys stored in `$ARTDAG_DATA/keys/` (files, not database). ## Architecture ``` L2 Server (port 8200) │ ├── POST /assets (upload media) → Register asset → Create activity → Sign │ ├── POST /assets/record-run (upload recipe) → Fetch L1 run → Register output │ │ │ └── GET L1_SERVER/runs/{id} │ ├── GET /users/{user}/outbox → Return signed activities │ └── POST /users/{user}/inbox → Receive Follow requests ```