# 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
```
