Both /storage/{id}/test and DELETE /storage/{id} were using Bearer
token auth only. Now they also check cookie auth for browser sessions.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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
Docker Swarm (recommended for production)
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:
- L2 creates a scoped token that only works for that specific L1
- User is redirected to the L1's
/authendpoint with the scoped token - L1 calls back to L2's
/auth/verifyendpoint to validate the token - L2 verifies the token scope matches the requesting L1
- L1 sets its own local cookie, logging the user in
- The attachment is recorded in L2's
user_rendererstable
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:
- L2 revokes the token in its database (
revoked_tokenstable) - L2 calls
/auth/revokeon all attached L1s to revoke their local copies - 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 usersassets- Asset registryactivities- Signed activitiesfollowers- 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