Skip to content

divinevideo/divine-name-server

Repository files navigation

Divine Name Server

Cloudflare Worker that enables username-based Nostr identities at Divine.Video with NIP-05 verification and subdomain profile routing.

This is part of the public edge, not the ArgoCD-managed GKE stack. Production ATProto rollout depends on this worker being deployed alongside divine-router, pds.divine.video, and entryway.divine.video.

Features

  • Username Claiming: Users claim usernames via NIP-98 signed HTTP requests proving key ownership
  • Subdomain Profiles: https://alice.divine.video/ serves user profiles by proxying to main app
  • NIP-05 Verification: Nostr identity verification at both root and subdomain /.well-known/nostr.json endpoints
  • Admin Management: Reserve, revoke, burn, or assign usernames with status tracking
  • Relay Hints: Store and serve up to 50 relay hints per user for better discoverability
  • One Username Per Pubkey: Database constraints ensure each pubkey has only one active username
  • Recyclable Usernames: Revoked usernames can be reclaimed; burned usernames are permanent

Tech Stack

  • Hono: Lightweight web framework optimized for Cloudflare Workers
  • Cloudflare D1: SQLite-based edge database for username registry
  • Cloudflare Workers Assets: Static file serving for admin UI
  • React + Vite: Admin UI for username management
  • NIP-98: HTTP authentication via Nostr event signatures using @noble/secp256k1
  • TypeScript: Type-safe implementation with Cloudflare Workers types

Development

Prerequisites

  • Node.js 18+
  • npm or similar package manager
  • Cloudflare account with Workers and D1 enabled

Setup

# Install dependencies
npm install

# Apply database migrations locally
npx wrangler d1 migrations apply divine-name-server-db --local

Local Development

# Install admin UI dependencies (first time only)
cd admin-ui && npm install && cd ..

# Build admin UI
npm run build:admin

# Start development server
npm run dev

# Server runs at http://localhost:8787
# Admin UI accessible at http://localhost:8787/admin (no authentication required locally)

Note: Rebuild the admin UI (npm run build:admin) after making changes to admin-ui/ code.

Testing

# Run tests in watch mode
npm test

# Run tests once
npm test:once

Deployment

# Apply migrations to production database
npx wrangler d1 migrations apply divine-name-server-db --remote

# Deploy worker to Cloudflare
npx wrangler deploy

Deploy this after the GKE services are healthy, because it publishes the public read model that divine-router and ATProto handle discovery consume.

API Endpoints

POST /api/username/claim

Claim a username with NIP-98 authentication.

Authentication: NIP-98 signed HTTP request

Headers:

Authorization: Nostr <base64-encoded-event>

The NIP-98 event must be kind 27235 with:

  • method tag matching POST
  • u tag matching the full request URL
  • Timestamp within 60 seconds of current time

Request Body:

{
  "name": "alice",
  "relays": ["wss://relay.damus.io", "wss://nos.lol"]
}

Fields:

  • name (required): Username to claim (3-20 chars, lowercase alphanumeric)
  • relays (optional): Array of relay URLs (max 50, must be wss:// protocol)

Success Response (200):

{
  "ok": true,
  "name": "alice",
  "pubkey": "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
  "profile_url": "https://alice.divine.video/",
  "nip05": {
    "main_domain": "alice@divine.video",
    "underscore_subdomain": "_@alice.divine.video",
    "host_style": "@alice.divine.video"
  }
}

Error Responses:

  • 400: Invalid username format or relay validation failed
  • 401: Missing or invalid NIP-98 signature
  • 403: Username is reserved or burned
  • 409: Username already claimed by another pubkey
  • 500: Internal server error

GET /.well-known/nostr.json

NIP-05 identity verification endpoint. Behavior differs based on hostname.

Subdomain Request

When accessed via subdomain (e.g., https://alice.divine.video/.well-known/nostr.json):

Response (200):

{
  "names": {
    "_": "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"
  },
  "relays": {
    "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d": [
      "wss://relay.damus.io",
      "wss://nos.lol"
    ]
  }
}

Returns a single user mapping with underscore (_) name for NIP-05 subdomain verification.

Root Domain Request

When accessed via root domain (e.g., https://divine.video/.well-known/nostr.json):

Response (200):

{
  "names": {
    "alice": "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
    "bob": "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2"
  },
  "relays": {
    "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d": ["wss://relay.damus.io"],
    "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2": ["wss://relay.primal.net"]
  }
}

Returns all active username mappings for the domain.

Headers:

Cache-Control: public, max-age=60

GET https://<username>.divine.video/

Subdomain profile routing. Proxies to the main Divine.Video application's profile page.

Behavior:

  • Active username: Proxies request to https://divine.video/profile/<npub>
  • Inactive/missing username: Returns 404 with user-friendly message
  • Converts hex pubkey to npub (Bech32) format for profile URL

Example:

  • Request: https://alice.divine.video/
  • Proxies to: https://divine.video/profile/npub180c...

Admin Endpoints (Protected by Cloudflare Access)

All admin endpoints require Cloudflare Access authentication configured at the edge.

Admin UI Access

The admin interface is available at /admin and provides a web UI for username management.

Local Development: Access directly at http://localhost:8787/admin (no authentication)

Production: Protected by Cloudflare Access. To add authorized emails:

  1. Go to Cloudflare Dashboard → Zero Trust → Access → Applications
  2. Find the application protecting your admin routes
  3. Edit the policy → Add include → Select "Emails"
  4. Enter email addresses to authorize
  5. Save application

Authorized users receive a one-time code via email when accessing the admin UI.

POST /api/admin/username/reserve

Reserve a username to prevent user claims (e.g., brand protection).

Request Body:

{
  "name": "brandname",
  "reason": "Brand protection"
}

Response (200):

{
  "ok": true,
  "name": "brandname",
  "status": "reserved"
}

POST /api/admin/username/revoke

Revoke or permanently burn a username.

Request Body:

{
  "name": "badname",
  "burn": true
}

Fields:

  • name (required): Username to revoke
  • burn (optional): If true, permanently burns the name; if false, makes it recyclable

Response (200):

{
  "ok": true,
  "name": "badname",
  "status": "burned",
  "recyclable": false
}

POST /api/admin/username/assign

Directly assign a username to a pubkey, bypassing normal claim flow.

Request Body:

{
  "name": "famousviner",
  "pubkey": "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"
}

Response (200):

{
  "ok": true,
  "name": "famousviner",
  "pubkey": "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
  "status": "active"
}

POST /api/admin/username/set-atproto

Link a username to an ATProto did:plc identity for handle resolution. This enables username.divine.video/.well-known/atproto-did to return the user's DID.

Request Body:

{
  "name": "alice",
  "atproto_did": "did:plc:abc123def456",
  "atproto_state": "ready"
}
Field Type Description
name string Username to update
atproto_did string | null The user's did:plc: identifier. Must start with did:plc:. Set to null to clear.
atproto_state string | null Lifecycle state: pending, ready, failed, disabled, or null

States:

  • pending — DID assigned, awaiting PDS configuration
  • ready — Handle resolution is active (/.well-known/atproto-did returns the DID)
  • failed — PDS configuration failed
  • disabled — Handle resolution explicitly disabled (returns 404)
  • null — No ATProto identity configured

Response (200):

{
  "ok": true,
  "name": "alice",
  "atproto_did": "did:plc:abc123def456",
  "atproto_state": "ready"
}

How it works:

  1. Call this endpoint to set a user's ATProto DID and state
  2. The Fastly KV entry is updated with the ATProto fields
  3. divine-router reads the KV entry and serves /.well-known/atproto-did
  4. Only returns the DID when status=active, atproto_state=ready, and atproto_did is present

Important: The user's DID document (managed by their PDS) must also contain alsoKnownAs: ["at://username.divine.video"] for bidirectional handle verification to succeed. This endpoint does not mint or manage DIDs — they come from the ATProto control plane.

To disable resolution immediately:

{
  "name": "alice",
  "atproto_did": "did:plc:abc123def456",
  "atproto_state": "disabled"
}

Database Schema

See migrations/0001_initial_schema.sql for complete schema definition.

Tables

usernames

Primary table mapping usernames to Nostr pubkeys.

Column Type Description
id INTEGER Primary key, auto-increment
name TEXT Unique username (3-20 lowercase alphanumeric chars)
pubkey TEXT Hex-encoded Nostr public key
relays TEXT JSON array of relay URLs (max 50)
status TEXT Status: 'active', 'reserved', 'revoked', 'burned'
recyclable INTEGER Whether name can be reclaimed (0 or 1)
created_at INTEGER Unix timestamp of creation
updated_at INTEGER Unix timestamp of last update
claimed_at INTEGER Unix timestamp when claimed by user
revoked_at INTEGER Unix timestamp when revoked
reserved_reason TEXT Admin reason for reservation
admin_notes TEXT Admin notes about username
atproto_did TEXT User's ATProto did:plc: identifier (set via admin API)
atproto_state TEXT ATProto handle resolution state: pending/ready/failed/disabled

Indexes:

  • idx_usernames_pubkey_active: Unique partial index ensuring one active username per pubkey
  • idx_usernames_status: Index on status for fast filtered queries

reserved_words

Protected words that cannot be claimed as usernames.

Column Type Description
word TEXT Reserved word (primary key)
category TEXT Category: 'system', 'brand', 'protocol', 'app', 'subdomain'
reason TEXT Human-readable reason for reservation
created_at INTEGER Unix timestamp of creation

Indexes:

  • idx_reserved_words_category: Index on category for fast lookups

See migrations/0002_seed_reserved_words.sql for the initial list of 30+ reserved words.

Status Values

  • active: Currently claimed and in use
  • reserved: Admin-reserved, cannot be claimed by users
  • revoked: Freed up and reclaimable (recyclable = 1)
  • burned: Permanently unavailable (recyclable = 0)

Username Rules

Usernames must meet these requirements:

  • Length: 3-20 characters
  • Characters: Lowercase letters (a-z) and numbers (0-9) only
  • Reserved words: Cannot use system routes, brand names, or protocol terms
  • Uniqueness: Each username can only be active for one pubkey at a time
  • One per pubkey: Each pubkey can only have one active username
  • Auto-revocation: Claiming a new username automatically revokes the old one

Valid examples: alice, bob123, user2024

Invalid examples:

  • ab (too short)
  • thisusernameiswaytoolong (too long)
  • Alice (uppercase letters)
  • alice_bob (special characters)
  • api (reserved word)

Relay Validation

Relay hints are optional but must meet these requirements when provided:

  • Protocol: Must use wss:// (secure WebSocket)
  • Count: Maximum 50 relays per username
  • Length: Each relay URL must be ≤200 characters
  • Format: Must be valid URLs per URL standard

Valid examples:

  • wss://relay.damus.io
  • wss://nos.lol
  • wss://relay.primal.net

Invalid examples:

  • https://relay.com (wrong protocol)
  • ws://relay.com (insecure WebSocket)
  • not-a-url (invalid format)

Architecture Overview

The Divine Name Server is a standalone Cloudflare Worker that handles three main flows:

1. Username Claiming Flow

User → NIP-98 Signed Request → Worker
                                   ↓
                           Verify Signature
                                   ↓
                          Validate Username
                                   ↓
                          Check Reserved Words
                                   ↓
                       Query D1 for Conflicts
                                   ↓
                     Auto-revoke Old Username
                                   ↓
                      Insert/Update New Claim
                                   ↓
                      Return Profile URLs

2. Subdomain Profile Routing

User → alice.divine.video/ → Worker
                                ↓
                       Extract Subdomain
                                ↓
                        Query D1 by Name
                                ↓
                       Convert Hex to Npub
                                ↓
              Proxy to divine.video/profile/<npub>
                                ↓
                        Return Profile Page

3. NIP-05 Identity Verification

Nostr Client → /.well-known/nostr.json → Worker
                                            ↓
                                    Detect Hostname
                                            ↓
                           Subdomain? → Query Single User
                              OR
                             Root? → Query All Active Users
                                            ↓
                                  Format NIP-05 Response
                                            ↓
                              Cache for 60 seconds, Return

Key Design Decisions

  • Standalone Worker: Independent from main Divine.Video application for scalability
  • Edge Database: D1 database for low-latency username lookups
  • NIP-98 Auth: Cryptographic proof of key ownership, no session state needed
  • Proxy Pattern: Subdomain routing proxies to existing profile pages, avoiding duplication
  • Reserved Words: Pre-seeded list protects system routes and brand names
  • Status State Machine: Clear state transitions (active → revoked → recyclable)

Worker + Admin UI Architecture

The Divine Name Server is a single Cloudflare Worker that serves both the Hono API and a React-based admin UI:

How It Works:

  • The Worker handles API routes via Hono (/api/username, /api/admin, etc.)
  • Static admin UI files are served automatically via Cloudflare Workers Assets
  • Configuration in wrangler.toml specifies the assets directory:
    [assets]
    directory = "./admin-ui/dist"

Routing Priority:

  1. Hono routes match first - API endpoints and custom routes
  2. Static files - If no route matches, serve from admin-ui/dist/
  3. SPA fallback - For client-side routing, falls back to index.html

Request Examples:

  • GET / → Hono route → Returns JSON service info
  • GET /api/username/claim → Hono route → API endpoint
  • GET /admin → Static assets → Serves React SPA
  • GET /admin/settings → Static assets → Serves React SPA (client-side routing)

This architecture allows deploying the entire system (API + admin UI) as a single Worker with no separate static hosting needed.

NIP-05 Compatibility

The service provides three NIP-05 identity formats:

  1. Standard format: alice@divine.video

    • Resolved via divine.video/.well-known/nostr.json
    • Works in all NIP-05 compatible clients
  2. Subdomain format: _@alice.divine.video

    • Resolved via alice.divine.video/.well-known/nostr.json
    • NIP-05 spec compliant using underscore name
  3. Display format: @alice.divine.video

    • Clean Bluesky-style display (not directly resolvable)
    • Maps to subdomain format for verification

All formats identify the same pubkey and support optional relay hints.

Design Documentation

For complete technical design, architecture decisions, and implementation details, see:

docs/plans/2025-11-15-divine-name-server-implementation.md

This plan includes:

  • Detailed task breakdown with acceptance criteria
  • NIP-98 verification implementation
  • Database migration steps
  • API endpoint specifications
  • Testing strategies
  • Deployment procedures

Security Considerations

  • Cryptographic Authentication: All username claims require valid NIP-98 signatures proving key ownership
  • Admin Protection: Admin endpoints protected by Cloudflare Access at the edge
  • No Session State: Stateless authentication eliminates session hijacking risks
  • Namespace Protection: Reserved words prevent claiming system routes and brand names
  • Permanent Burning: Offensive or abusive names can be permanently disabled
  • No Hijacking: Database constraints prevent claiming names owned by other pubkeys
  • Time-bound Requests: NIP-98 events expire after 60 seconds to prevent replay attacks

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages