All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m3s
- Remove default password fallback from POSTGRES_PASSWORD in docker-compose.yml - Remove default password fallback from db.py and migrate.py - Update .env.example with required POSTGRES_PASSWORD - Update README to mark DATABASE_URL as required Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
390 lines
10 KiB
Markdown
390 lines
10 KiB
Markdown
# Art DAG L2 Server - ActivityPub
|
|
|
|
Ownership registry and ActivityPub federation for Art DAG. Manages asset provenance, cryptographic anchoring, and distributed identity.
|
|
|
|
## Features
|
|
|
|
- **Asset Registry**: Content-addressed assets with provenance tracking
|
|
- **ActivityPub Federation**: Standard protocol for distributed social networking
|
|
- **OpenTimestamps Anchoring**: Cryptographic proof of existence on Bitcoin blockchain
|
|
- **L1 Integration**: Record and verify L1 rendering runs
|
|
- **Storage Providers**: S3, IPFS, and local storage backends
|
|
- **Scoped Authentication**: Secure token-based auth for federated L1 servers
|
|
|
|
## Dependencies
|
|
|
|
- **PostgreSQL**: Primary data storage
|
|
- **artdag-common**: Shared templates and middleware
|
|
- **cryptography**: RSA key generation and signing
|
|
- **httpx**: Async HTTP client for federation
|
|
|
|
## Quick Start
|
|
|
|
```bash
|
|
# Install dependencies
|
|
pip install -r requirements.txt
|
|
|
|
# Configure
|
|
export ARTDAG_DOMAIN=artdag.example.com
|
|
export ARTDAG_USER=giles
|
|
export DATABASE_URL=postgresql://artdag:$POSTGRES_PASSWORD@localhost:5432/artdag
|
|
export L1_SERVERS=https://celery-artdag.example.com
|
|
|
|
# Generate signing keys (required for federation)
|
|
python setup_keys.py
|
|
|
|
# Start server
|
|
python server.py
|
|
```
|
|
|
|
## Docker Deployment
|
|
|
|
```bash
|
|
docker stack deploy -c docker-compose.yml artdag-l2
|
|
```
|
|
|
|
## Configuration
|
|
|
|
### Environment Variables
|
|
|
|
| Variable | Default | Description |
|
|
|----------|---------|-------------|
|
|
| `ARTDAG_DOMAIN` | `artdag.rose-ash.com` | Domain for ActivityPub actors |
|
|
| `ARTDAG_USER` | `giles` | Default username |
|
|
| `ARTDAG_DATA` | `~/.artdag/l2` | Data directory |
|
|
| `DATABASE_URL` | **(required)** | PostgreSQL connection |
|
|
| `L1_SERVERS` | - | Comma-separated list of L1 server URLs |
|
|
| `JWT_SECRET` | (generated) | JWT signing secret |
|
|
| `HOST` | `0.0.0.0` | Server bind address |
|
|
| `PORT` | `8200` | Server port |
|
|
|
|
### JWT Secret
|
|
|
|
The JWT secret signs authentication tokens. Without a persistent secret, tokens are invalidated on restart.
|
|
|
|
```bash
|
|
# Generate a secret
|
|
openssl rand -hex 32
|
|
|
|
# Set in environment
|
|
export JWT_SECRET="your-generated-secret"
|
|
|
|
# Or use Docker secrets (recommended for production)
|
|
echo "your-secret" | docker secret create jwt_secret -
|
|
```
|
|
|
|
### RSA Keys
|
|
|
|
ActivityPub requires RSA keys for signing activities:
|
|
|
|
```bash
|
|
# Generate keys
|
|
python setup_keys.py
|
|
|
|
# Or with custom paths
|
|
python setup_keys.py --data-dir /data/l2 --user giles
|
|
```
|
|
|
|
Keys stored in `$ARTDAG_DATA/keys/`:
|
|
- `{username}.pem` - Private key (chmod 600)
|
|
- `{username}.pub` - Public key (in actor profile)
|
|
|
|
## Web UI
|
|
|
|
| Path | Description |
|
|
|------|-------------|
|
|
| `/` | Home page with stats |
|
|
| `/login` | Login form |
|
|
| `/register` | Registration form |
|
|
| `/logout` | Log out |
|
|
| `/assets` | Browse registered assets |
|
|
| `/asset/{name}` | Asset detail page |
|
|
| `/activities` | Published activities |
|
|
| `/activity/{id}` | Activity detail |
|
|
| `/users` | Registered users |
|
|
| `/renderers` | L1 renderer connections |
|
|
| `/anchors/ui` | OpenTimestamps management |
|
|
| `/storage` | Storage provider config |
|
|
| `/download/client` | Download CLI client |
|
|
|
|
## API Reference
|
|
|
|
Interactive docs: http://localhost:8200/docs
|
|
|
|
### Authentication
|
|
|
|
| Method | Path | Description |
|
|
|--------|------|-------------|
|
|
| POST | `/auth/register` | Register new user |
|
|
| POST | `/auth/login` | Login, get JWT token |
|
|
| GET | `/auth/me` | Get current user info |
|
|
| POST | `/auth/verify` | Verify token (for L1 servers) |
|
|
|
|
### Assets
|
|
|
|
| Method | Path | Description |
|
|
|--------|------|-------------|
|
|
| GET | `/assets` | List all assets |
|
|
| GET | `/assets/{name}` | Get asset by name |
|
|
| POST | `/assets` | Register new asset |
|
|
| PATCH | `/assets/{name}` | Update asset metadata |
|
|
| POST | `/assets/record-run` | Record L1 run as asset |
|
|
| POST | `/assets/publish-cache` | Publish L1 cache item |
|
|
| GET | `/assets/by-run-id/{run_id}` | Find asset by L1 run ID |
|
|
|
|
### ActivityPub
|
|
|
|
| Method | Path | Description |
|
|
|--------|------|-------------|
|
|
| GET | `/.well-known/webfinger` | 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/{hash}` | Get object by content hash |
|
|
| GET | `/activities` | List activities (paginated) |
|
|
| GET | `/activities/{ref}` | Get activity by reference |
|
|
| GET | `/activity/{index}` | Get activity by index |
|
|
|
|
### OpenTimestamps Anchoring
|
|
|
|
| Method | Path | Description |
|
|
|--------|------|-------------|
|
|
| POST | `/anchors/create` | Create timestamp anchor |
|
|
| GET | `/anchors` | List all anchors |
|
|
| GET | `/anchors/{merkle_root}` | Get anchor details |
|
|
| GET | `/anchors/{merkle_root}/tree` | Get merkle tree |
|
|
| GET | `/anchors/verify/{activity_id}` | Verify activity timestamp |
|
|
| POST | `/anchors/{merkle_root}/upgrade` | Upgrade pending timestamp |
|
|
| GET | `/anchors/ui` | Anchor management UI |
|
|
| POST | `/anchors/test-ots` | Test OTS functionality |
|
|
|
|
### Renderers (L1 Connections)
|
|
|
|
| Method | Path | Description |
|
|
|--------|------|-------------|
|
|
| GET | `/renderers` | List attached L1 servers |
|
|
| GET | `/renderers/attach` | Initiate L1 attachment |
|
|
| POST | `/renderers/detach` | Detach from L1 server |
|
|
|
|
### Storage Providers
|
|
|
|
| Method | Path | Description |
|
|
|--------|------|-------------|
|
|
| GET | `/storage` | List storage providers |
|
|
| POST | `/storage` | Add provider (form) |
|
|
| POST | `/storage/add` | Add provider (JSON) |
|
|
| GET | `/storage/{id}` | Get provider details |
|
|
| PATCH | `/storage/{id}` | Update provider |
|
|
| DELETE | `/storage/{id}` | Delete provider |
|
|
| POST | `/storage/{id}/test` | Test connection |
|
|
| GET | `/storage/type/{type}` | Get form for provider type |
|
|
|
|
## L1 Renderer Integration
|
|
|
|
L2 coordinates with L1 rendering servers for distributed processing.
|
|
|
|
### Configuration
|
|
|
|
```bash
|
|
# Single L1 server
|
|
export L1_SERVERS=https://celery-artdag.rose-ash.com
|
|
|
|
# Multiple L1 servers
|
|
export L1_SERVERS=https://server1.example.com,https://server2.example.com
|
|
```
|
|
|
|
### Attachment Flow
|
|
|
|
1. User visits `/renderers` and clicks "Attach"
|
|
2. L2 creates a **scoped token** bound to the specific L1
|
|
3. User redirected to L1's `/auth?auth_token=...`
|
|
4. L1 calls L2's `/auth/verify` to validate
|
|
5. L2 checks token scope matches requesting L1
|
|
6. L1 sets local cookie, attachment recorded in `user_renderers`
|
|
|
|
### Security
|
|
|
|
- **Scoped tokens**: Tokens bound to specific L1; can't be used elsewhere
|
|
- **No shared secrets**: L1 verifies via L2's `/auth/verify` endpoint
|
|
- **Federated logout**: L2 revokes tokens on all attached L1s
|
|
|
|
## OpenTimestamps Anchoring
|
|
|
|
Cryptographic proof of existence using Bitcoin blockchain.
|
|
|
|
### How It Works
|
|
|
|
1. Activities are collected into merkle trees
|
|
2. Merkle root submitted to Bitcoin via OpenTimestamps
|
|
3. Pending proofs upgraded when Bitcoin confirms
|
|
4. Final proof verifiable without trusted third parties
|
|
|
|
### Verification
|
|
|
|
```bash
|
|
# Verify an activity's timestamp
|
|
curl https://artdag.example.com/anchors/verify/123
|
|
|
|
# Returns:
|
|
{
|
|
"activity_id": 123,
|
|
"merkle_root": "abc123...",
|
|
"status": "confirmed",
|
|
"bitcoin_block": 800000,
|
|
"verified_at": "2026-01-01T..."
|
|
}
|
|
```
|
|
|
|
## Data Model
|
|
|
|
### PostgreSQL Tables
|
|
|
|
| Table | Description |
|
|
|-------|-------------|
|
|
| `users` | Registered users with hashed passwords |
|
|
| `assets` | Asset registry with content hashes |
|
|
| `activities` | Signed ActivityPub activities |
|
|
| `followers` | Follower relationships |
|
|
| `anchors` | OpenTimestamps anchor records |
|
|
| `anchor_activities` | Activity-to-anchor mappings |
|
|
| `user_renderers` | L1 attachment records |
|
|
| `revoked_tokens` | Token revocation list |
|
|
| `storage_providers` | Storage configurations |
|
|
|
|
### Asset Structure
|
|
|
|
```json
|
|
{
|
|
"name": "my-video",
|
|
"content_hash": "sha3-256:abc123...",
|
|
"asset_type": "video",
|
|
"owner": "@giles@artdag.rose-ash.com",
|
|
"created_at": "2026-01-01T...",
|
|
"provenance": {
|
|
"inputs": [...],
|
|
"recipe": "beat-sync",
|
|
"l1_server": "https://celery-artdag.rose-ash.com",
|
|
"run_id": "..."
|
|
},
|
|
"tags": ["art", "generated"]
|
|
}
|
|
```
|
|
|
|
### Activity Structure
|
|
|
|
```json
|
|
{
|
|
"@context": "https://www.w3.org/ns/activitystreams",
|
|
"type": "Create",
|
|
"actor": "https://artdag.rose-ash.com/users/giles",
|
|
"object": {
|
|
"type": "Document",
|
|
"name": "my-video",
|
|
"content": "sha3-256:abc123...",
|
|
"attributedTo": "https://artdag.rose-ash.com/users/giles"
|
|
},
|
|
"published": "2026-01-01T..."
|
|
}
|
|
```
|
|
|
|
## CLI Commands
|
|
|
|
### Register Asset
|
|
|
|
```bash
|
|
curl -X POST https://artdag.example.com/assets \
|
|
-H "Authorization: Bearer <token>" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"name": "my-video",
|
|
"content_hash": "abc123...",
|
|
"asset_type": "video",
|
|
"tags": ["art", "generated"]
|
|
}'
|
|
```
|
|
|
|
### Record L1 Run
|
|
|
|
```bash
|
|
curl -X POST https://artdag.example.com/assets/record-run \
|
|
-H "Authorization: Bearer <token>" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"run_id": "uuid-from-l1",
|
|
"l1_server": "https://celery-artdag.rose-ash.com",
|
|
"output_name": "my-rendered-video"
|
|
}'
|
|
```
|
|
|
|
### Publish L1 Cache Item
|
|
|
|
```bash
|
|
curl -X POST https://artdag.example.com/assets/publish-cache \
|
|
-H "Authorization: Bearer <token>" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"content_hash": "abc123...",
|
|
"l1_server": "https://celery-artdag.rose-ash.com",
|
|
"name": "my-asset",
|
|
"asset_type": "video"
|
|
}'
|
|
```
|
|
|
|
## Architecture
|
|
|
|
```
|
|
L2 Server (FastAPI)
|
|
│
|
|
├── Web UI (Jinja2 + HTMX + Tailwind)
|
|
│
|
|
├── /assets → Asset Registry
|
|
│ │
|
|
│ └── PostgreSQL (assets table)
|
|
│
|
|
├── /users/{user}/outbox → ActivityPub
|
|
│ │
|
|
│ ├── Sign activities (RSA)
|
|
│ └── PostgreSQL (activities table)
|
|
│
|
|
├── /anchors → OpenTimestamps
|
|
│ │
|
|
│ ├── Merkle tree construction
|
|
│ └── Bitcoin anchoring
|
|
│
|
|
├── /auth/verify → L1 Token Verification
|
|
│ │
|
|
│ └── Scoped token validation
|
|
│
|
|
└── /storage → Storage Providers
|
|
│
|
|
├── S3 (boto3)
|
|
├── IPFS (ipfs_client)
|
|
└── Local filesystem
|
|
```
|
|
|
|
## Federation
|
|
|
|
L2 implements ActivityPub for federated asset sharing.
|
|
|
|
### Discovery
|
|
|
|
```bash
|
|
# Webfinger lookup
|
|
curl "https://artdag.example.com/.well-known/webfinger?resource=acct:giles@artdag.example.com"
|
|
```
|
|
|
|
### Actor Profile
|
|
|
|
```bash
|
|
curl -H "Accept: application/activity+json" \
|
|
https://artdag.example.com/users/giles
|
|
```
|
|
|
|
### Outbox
|
|
|
|
```bash
|
|
curl -H "Accept: application/activity+json" \
|
|
https://artdag.example.com/users/giles/outbox
|
|
```
|