All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 13m49s
- Add -pix_fmt yuv420p to multi_res_output.py libx264 path so browsers can decode CPU-encoded segments (was producing yuv444p / High 4:4:4). - Switch silent auth check and coop fragment middlewares from opt-out blocklists to opt-in: only run for GET requests with Accept: text/html. Prevents unnecessary nav-tree/auth-menu HTTP calls on every HLS segment, IPFS proxy, and API request. - Add opaque grant token verification to L1/L2 dependencies. - Migrate client CLI to device authorization flow. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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
- User visits
/renderersand clicks "Attach" - L2 creates a scoped token bound to the specific L1
- User redirected to L1's
/auth?auth_token=... - L1 calls L2's
/auth/verifyto validate - L2 checks token scope matches requesting L1
- 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/verifyendpoint - Federated logout: L2 revokes tokens on all attached L1s
OpenTimestamps Anchoring
Cryptographic proof of existence using Bitcoin blockchain.
How It Works
- Activities are collected into merkle trees
- Merkle root submitted to Bitcoin via OpenTimestamps
- Pending proofs upgraded when Bitcoin confirms
- 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