Skip to content

divinevideo/keycast

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

442 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Keycast

OAuth 2.0 remote signing for Nostr. Users authenticate once, apps get signing access via REST API or NIP-46.

What is Keycast?

Keycast is a managed key custody service for Nostr apps. Users create an account (or import their nsec), and apps request signing permission via OAuth. Keys are encrypted server-side; signing happens via HTTP RPC or NIP-46 bunker URL.

For mobile apps: No browser extensions, no relay infrastructure to manage. One OAuth flow, then sign events via HTTPS or connect with the returned bunker URL.

For users: One identity across all apps. Revoke access per-app anytime. Import existing keys or let Keycast generate one.

Client Libraries

Platform Package Docs
Flutter/Dart keycast_flutter Demo + Guide
JS/TS keycast-login README

Quick Start

Flutter/Dart

import 'package:keycast_flutter/keycast_flutter.dart';

final oauth = KeycastOAuth(
  config: OAuthConfig(
    serverUrl: 'https://login.divine.video',
    clientId: 'your-app',
    redirectUri: 'https://login.divine.video/app/callback',
  ),
  storage: SecureKeycastStorage(), // Auto-saves credentials
);

// 1. Start OAuth flow (auto-includes authorization_handle for silent re-auth)
final (url, verifier) = await oauth.getAuthorizationUrl(scope: 'policy:social');
await launchUrl(Uri.parse(url));

// 2. Handle Universal Link callback, exchange code (auto-saves session)
final tokens = await oauth.exchangeCode(code: code, verifier: verifier);

// 3. Sign events via HTTP RPC
final session = await oauth.getSession(); // Load from storage
final rpc = KeycastRpc.fromSession(oauth.config, session!);
final signed = await rpc.signEvent(myEvent);

Requires Universal Links (iOS) / App Links (Android). See keycast_flutter_demo for complete setup including deep link configuration.

JavaScript/TypeScript

import { createKeycastClient } from 'keycast-login';

const client = createKeycastClient({
  serverUrl: 'https://login.divine.video',
  clientId: 'your-app',
  redirectUri: window.location.origin + '/callback',
  storage: localStorage, // Auto-saves credentials (optional, defaults to in-memory)
});

// 1. Start OAuth flow (auto-includes authorization_handle for silent re-auth)
const { url, pkce } = await client.oauth.getAuthorizationUrl({
  scopes: ['policy:social'],
});
sessionStorage.setItem('pkce_verifier', pkce.verifier);
window.location.href = url;

// 2. Handle callback (auto-saves session to storage)
const code = new URLSearchParams(location.search).get('code');
const verifier = sessionStorage.getItem('pkce_verifier');
const tokens = await client.oauth.exchangeCode(code, verifier);

// 3. Sign events via HTTP RPC
const rpc = client.createRpc(tokens);
const signed = await rpc.signEvent({
  kind: 1,
  content: 'Hello Nostr!',
  tags: [],
  created_at: Math.floor(Date.now() / 1000),
  pubkey: await rpc.getPublicKey(),
});

// On page reload, restore session from storage
const session = client.oauth.getSession();
if (session) {
  // User is already authenticated
}

See keycast-login README for full API reference including BYOK (Bring Your Own Key) flows.

HTTP API

For other languages, use the REST API directly:

# Exchange authorization code for credentials
curl -X POST https://login.divine.video/api/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "code": "<authorization_code>",
    "client_id": "your-app",
    "redirect_uri": "https://yourapp.com/callback",
    "code_verifier": "<pkce_verifier>"
  }'

# Response includes both bunker_url (NIP-46) and access_token (HTTP RPC)
# Sign an event via HTTP RPC
curl -X POST https://login.divine.video/api/nostr \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "method": "sign_event",
    "params": [{
      "kind": 1,
      "content": "Hello Nostr!",
      "created_at": 1234567890,
      "tags": []
    }]
  }'

How It Works

OAuth flow: App redirects to Keycast. User authenticates (or registers). App receives access token and bunker URL.

Two signing transports:

Transport Latency Use Case
HTTP RPC ~50ms Direct HTTPS with access token
NIP-46 ~200-500ms Standard Nostr protocol via relays

Both return the same signed events. Use HTTP RPC for lower latency; use NIP-46 bunker URL with existing nostr-tools/NDK integrations.

Key encryption: AES-256-GCM at rest in PostgreSQL. Configure key management with KMS_PROVIDER=file|gcp|aws.

API Reference

OAuth Endpoints

Endpoint Method Description
/api/oauth/authorize GET Start OAuth flow (redirect user here)
/api/oauth/token POST Exchange code for access_token + bunker_url

RPC Methods

POST to /api/nostr with Authorization: Bearer <access_token>:

Method Description
get_public_key Get user's public key (hex)
sign_event Sign an unsigned event
nip44_encrypt / nip44_decrypt NIP-44 encryption
nip04_encrypt / nip04_decrypt NIP-04 encryption

Self-Hosting

git clone https://github.com/ArcadeLabsInc/keycast.git
cd keycast
bun install

# Generate encryption key
bun run key:generate

# Configure environment
cp .env.example .env
# Edit DATABASE_URL, SERVER_NSEC, ALLOWED_ORIGINS

# Run with Docker
docker compose up -d --build

See DEVELOPMENT.md for local development setup.

Environment Variables

Required

Variable Description
DATABASE_URL PostgreSQL connection string
SERVER_NSEC Server's Nostr secret key for signing tokens
MASTER_KEY_PATH Path to encryption key file

Email (SendGrid)

Variable Default Description
SENDGRID_API_KEY (none) If set, uses SendGrid; otherwise logs emails to console
FROM_EMAIL noreply@keycast.app Sender email address
FROM_NAME Divine Sender display name
BASE_URL https://login.divine.video Base URL for email verification links
DISABLE_EMAILS (none) If set (any value), skips sending emails

Authentication & OAuth

Variable Default Description
TOKEN_EXPIRY_SECONDS 86400 (24 hours) JWT token expiry
APP_URL https://login.divine.video Fallback URL for OAuth callbacks
ALLOWED_PUBKEYS (none) Comma-separated admin pubkeys whitelist
ALLOWED_ORIGINS (none) CORS origins (comma-separated)
DPOP_REPLAY_FAIL_OPEN false DPoP replay cache outage mode (false fail-closed when Redis is unavailable/not configured, true dev-only per-instance fallback)

Multi-tenancy

Variable Default Description
BUNKER_RELAYS (required) NIP-46 relay URLs (comma-separated)
ALLOWED_TENANT_DOMAINS (none) If set, restricts auto-provisioning to these domains

Performance

Variable Default Description
HANDLER_CACHE_SIZE 1000000 Max entries in HTTP handler cache

Encryption

Variable Default Description
KMS_PROVIDER file Key manager backend: file, gcp, or aws
MASTER_KEY_PATH ./master.key Required when KMS_PROVIDER=file
GCP_PROJECT_ID (none) Required when KMS_PROVIDER=gcp
AWS_KMS_KEY_ID (none) Required when KMS_PROVIDER=aws (key ID/ARN/alias)
AWS_REGION us-east-1 AWS KMS region when KMS_PROVIDER=aws
USE_GCP_KMS (legacy) Backward compatibility only; if KMS_PROVIDER is also set, KMS_PROVIDER wins

KMS Provider Migration (USE_GCP_KMS -> KMS_PROVIDER)

  • Legacy behavior: when KMS_PROVIDER is unset, USE_GCP_KMS=true selects gcp, otherwise file.
  • Recommended behavior: set KMS_PROVIDER explicitly in every environment.
  • If both variables are set and disagree, runtime uses KMS_PROVIDER as source of truth.
  • Safe rollout:
    1. Set KMS_PROVIDER to your current effective provider.
    2. Deploy and verify startup/config validation succeeds.
    3. Remove legacy USE_GCP_KMS once validated.

AWS KMS Notes

  • Build with AWS support when using KMS_PROVIDER=aws:
    • cargo build --release --bin keycast --features aws
  • Minimum IAM permissions for the configured KMS key:
    • kms:Encrypt
    • kms:Decrypt
    • kms:DescribeKey

History & Team Key Management

Keycast started as a team-based key management system, forked from erskingardner/keycast. That original functionality—shared team keys with role-based access and custom permission policies—is still available but works via manual bunker URL distribution rather than OAuth.

See docs/TEAMS.md for team key management documentation.

License

MIT

About

No description, website, or topics provided.

Resources

License

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors