Files
rose-ash/l2
giles a5717ec4d4 Fall back to username for auth-menu email param
Existing sessions have email=None since the field was just added.
Username IS the email in Art-DAG (OAuth returns user.email as username).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 23:39:19 +00:00
..
2026-02-24 23:07:31 +00:00
2026-02-24 23:07:31 +00:00
2026-02-24 23:07:31 +00:00
2026-02-24 23:07:31 +00:00
2026-02-24 23:07:31 +00:00
2026-02-24 23:07:31 +00:00
2026-02-24 23:07:31 +00:00
2026-02-24 23:07:31 +00:00
2026-02-24 23:07:31 +00:00
2026-02-24 23:07:31 +00:00
2026-02-24 23:07:31 +00:00
2026-02-24 23:07:31 +00:00

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

# 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

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.

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

# 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

# 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

# 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

{
  "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

{
  "@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

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

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

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

# Webfinger lookup
curl "https://artdag.example.com/.well-known/webfinger?resource=acct:giles@artdag.example.com"

Actor Profile

curl -H "Accept: application/activity+json" \
  https://artdag.example.com/users/giles

Outbox

curl -H "Accept: application/activity+json" \
  https://artdag.example.com/users/giles/outbox