gilesb a5619208cf Support activity_id (hash) in /activities/{ref} URL
- Accept both numeric index and activity_id hash
- Look up activity by ID from database when hash provided
- Refactor ui_activity_detail to support both lookup methods

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 18:37:53 +00:00
2026-01-09 11:16:57 +00:00
2026-01-07 17:16:05 +00:00
2026-01-07 12:04:58 +00:00

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

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

# Generate a 64-character hex secret
openssl rand -hex 32
# Or with Python
python -c "import secrets; print(secrets.token_hex(32))"

Local development

export JWT_SECRET="your-generated-secret-here"
python server.py

Create a Docker secret:

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

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:

# 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 <container> 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.

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

curl -X POST http://localhost:8200/assets \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -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:

curl -X POST http://localhost:8200/assets/record-run \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -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
Description
No description provided
Readme 2.2 MiB
Languages
Python 91.3%
HTML 8.4%
Dockerfile 0.2%
Shell 0.1%