gilesb 5eb525d107 Add l2_server claim to JWT tokens for L1 verification
L1 needs to know which L2 server issued the token so it can verify
the token with the correct server. Now tokens include:
- l2_server: The L2 server URL (e.g., https://artdag.rose-ash.com)
- username: Also include username for compatibility (in addition to sub)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 17:10:33 +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 ARTDAG_L1=http://localhost:8100

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

API Endpoints

Server Info

Method Path Description
GET / Server info

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

Registry

Method Path Description
GET /registry Full registry
GET /registry/{name} Get asset by name
POST /registry Register new asset
POST /registry/record-run Record L1 run as owned asset

Example Usage

Register an asset

curl -X POST http://localhost:8200/registry \
  -H "Content-Type: application/json" \
  -d '{
    "name": "my-video",
    "content_hash": "abc123...",
    "asset_type": "video",
    "tags": ["art", "generated"]
  }'

Record an L1 run

curl -X POST http://localhost:8200/registry/record-run \
  -H "Content-Type: application/json" \
  -d '{
    "run_id": "uuid-from-l1",
    "output_name": "my-rendered-video"
  }'

Discover actor (WebFinger)

curl "http://localhost:8200/.well-known/webfinger?resource=acct:giles@artdag.rose-ash.com"

Get actor profile

curl -H "Accept: application/activity+json" http://localhost:8200/users/giles

Data Storage

Data stored in ~/.artdag/l2/:

  • registry.json - Asset registry
  • activities.json - Signed activities
  • actor.json - Actor profile
  • followers.json - Followers list

Architecture

L2 Server (port 8200)
    │
    ├── POST /registry → Register asset → Create activity → Sign
    │
    ├── POST /registry/record-run → 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%