kiwi/app/services/ap/keys.py
pyr0ball 6e954c5c6e
Some checks failed
CI / Backend (Python) (push) Has been cancelled
CI / Frontend (Vue) (push) Has been cancelled
Mirror / mirror (push) Has been cancelled
feat(ap): issue #113 — ActivityPub federation + Mastodon OAuth
Full ActivityPub implementation wired to cf-core.activitypub module:

Endpoints (root-level, not under /api/v1):
  GET  /.well-known/webfinger  — WebFinger JRD (AP_ENABLED only)
  GET  /ap/actor               — Instance actor document
  POST /ap/actor/inbox         — Incoming Follow/Undo (dedup + Accept dispatch)
  GET  /ap/outbox              — OrderedCollection of community posts
  GET  /ap/posts/{slug}        — Individual AP Note
  GET  /ap/followers           — Follower count collection
  GET  /ap/following           — Empty following collection

Mastodon OAuth (under /api/v1/social/mastodon/):
  POST   /connect      — Dynamic app registration + OAuth flow start
  GET    /callback     — Code exchange + token storage (Fernet-encrypted)
  DELETE /disconnect   — Token revocation
  GET    /status       — Connection status

Config: AP_ENABLED, AP_HOST, AP_KEY_PATH, AP_TOKEN_ENCRYPTION_KEY
Migration 042: ap_followers, ap_deliveries, ap_received, mastodon_tokens tables
Key manager: auto-generates RSA-2048 keypair on first boot if AP_ENABLED
Delivery service: deliver_to_followers() with 3-retry exponential backoff + DB log
Post publish: background fan-out to AP followers + Mastodon when opted-in

All AP endpoints gracefully degrade (404) when AP_ENABLED=false.
2026-05-11 17:55:51 -07:00

48 lines
1.4 KiB
Python

# app/services/ap/keys.py
# MIT License
from __future__ import annotations
import logging
from pathlib import Path
from circuitforge_core.activitypub import CFActor, generate_rsa_keypair, load_actor_from_key_file
logger = logging.getLogger(__name__)
_actor: CFActor | None = None
def get_actor() -> CFActor | None:
"""Return the loaded instance actor, or None if AP is not enabled."""
return _actor
def init_actor(host: str, key_path: Path) -> CFActor:
"""Load or generate the instance RSA keypair and build the CFActor singleton.
Called once at startup when AP_ENABLED=true. Generates a new 2048-bit keypair
if the key file does not yet exist (first boot).
"""
global _actor
key_path.parent.mkdir(parents=True, exist_ok=True)
if not key_path.exists():
logger.info("AP: no key file found at %s — generating new RSA-2048 keypair", key_path)
private_pem, _pub = generate_rsa_keypair(bits=2048)
key_path.write_text(private_pem, encoding="utf-8")
key_path.chmod(0o600)
base = f"https://{host}"
actor_id = f"{base}/ap/actor"
_actor = load_actor_from_key_file(
actor_id=actor_id,
username="kiwi",
display_name="Kiwi Pantry",
private_key_path=str(key_path),
summary="Community pantry and recipe feed from a Kiwi instance.",
)
logger.info("AP: instance actor loaded — %s", actor_id)
return _actor