Commit Graph

106 Commits

Author SHA1 Message Date
giles
14fbd59e7b Fix activity ID domain mismatch in per-app delivery
Mastodon requires the activity ID host to match the actor host.
The stored activity_id uses federation.rose-ash.com but per-app
delivery sends actor as blog.rose-ash.com etc.  Rewrite the
activity ID host to match the delivery domain.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 20:36:56 +00:00
giles
856ba94f3b Exempt AP paths from auth state check
/.well-known/, /users/, /nodeinfo/ now skip the prompt=none
OAuth redirect so ActivityPub endpoints work for unauthenticated
remote servers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 20:19:25 +00:00
giles
1bb19c96ed Fix per-app AP delivery, NULL uniqueness, and reverse discovery
- Delivery handler now signs/delivers using the per-app domain that
  matches the follower's subscription (not always federation domain)
- app_domain is NOT NULL with default 'federation' (sentinel replaces
  NULL to avoid uniqueness constraint edge case)
- Aggregate actor advertises per-app actors via alsoKnownAs
- Migration backfills existing NULL rows to 'federation'

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 19:25:24 +00:00
giles
f2262f702b Add per-app ActivityPub actors via shared AP blueprint
Each AP-enabled app (blog, market, events, federation) now serves its
own webfinger, actor profile, inbox, outbox, and followers endpoints.
Per-app actors are virtual projections of the same ActorProfile/keypair,
scoped by APFollower.app_domain and APActivity.origin_app.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 19:02:30 +00:00
giles
001cbffd74 Add minimal home page templates — content only, no title bar
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 17:32:25 +00:00
giles
38233279a2 Rename blog.home → blog.index in not_found template
The blog index moved from / to /index; homepage now shows a Ghost page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 16:54:02 +00:00
giles
9cb8cf9e1d Add at-least-once delivery + idempotent federation handler
- EventProcessor now recovers stuck "processing" activities back to
  "pending" after 5 minutes (handles process crashes)
- New ap_delivery_log table records successful inbox deliveries
- Federation delivery handler checks the log before sending, so
  retries skip already-delivered inboxes
- Together these give at-least-once + idempotent semantics

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 16:16:55 +00:00
giles
8951a62b90 Add NOTIFY/LISTEN wake-up to event processor
emit_activity() now fires NOTIFY ap_activity_pending inside the
caller's transaction (delivered on commit).  EventProcessor maintains
a dedicated asyncpg LISTEN connection and wakes the poll loop
immediately, dropping latency from ~2 s to sub-100 ms.  The fixed-
interval poll remains as a safety-net fallback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 15:35:27 +00:00
giles
7b878a501b Add migration for device_id column on oauth_grants
The column was added to the create_table migration after it had already
been applied, so the live DB was missing it.  This new migration adds
the column and index separately.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 15:25:10 +00:00
giles
748d28e657 Set blog_did = account_did — one device identity across all apps
Callback adopts account's device_id by overwriting g.device_id,
so the factory after_request sets {app}_did cookie to account's value.
Simplifies factory check: g.device_id IS the account_did, no need
to read _account_did from session separately.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 15:11:24 +00:00
giles
cad528d732 Device-id SSO: account sets did, signals login via Redis
- Factory: set {name}_did cookie for all apps (including account)
  via before_request/after_request hooks (g.device_id always available)
- Factory: _check_auth_state checks did_auth:{account_did} in Redis
  to override stale "not logged in" cache when account login detected
- OAuth: removed _ensure_device_cookie (moved to factory), callback
  stores account_did from authorize redirect in session
- OAuth: login uses g.device_id, logout clears _account_did

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 14:57:40 +00:00
giles
c4590d1442 Replace propagation chain + check-device with prompt=none OAuth handshake
Client apps now do a silent OAuth round-trip (prompt=none) to account on
first visit. If user is logged in on account, they get silently logged in.
If not, the result is cached (5 min) to avoid repeated handshakes.

Grant verification now uses direct DB query instead of aiohttp HTTP calls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 13:40:58 +00:00
giles
38a2023ca3 Add aiohttp to requirements for internal auth checks
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 13:05:24 +00:00
giles
de93dfdc73 Device cookie + internal endpoint for auth state detection
Each client app sets a persistent first-party device cookie ({app}_did).
On each request:
- Logged in: verify grant via account internal endpoint (cached 60s)
- Not logged in + device cookie: check-device endpoint detects if user
  logged in since last grant revocation → triggers OAuth automatically
No cross-domain cookies. No propagation chain. Each app checks independently.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 12:50:43 +00:00
giles
6bb26522a1 Add OAuth grants for per-device session revocation
- OAuthGrant model tracks each client authorization, tied to the
  account session (issuer_session) that issued it
- OAuth authorize creates grant + code together
- Client apps store grant_token in session, verify via account's
  internal /auth/internal/verify-grant endpoint (Redis-cached 60s)
- Account logout revokes only grants from that device's session
- Replaces iframe-based logout with server-side grant revocation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 12:30:08 +00:00
giles
9a637c6227 sso-clear returns 204 for iframe-based logout
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 12:21:14 +00:00
giles
a93a456ac5 Remove sso_hint cookie, add sso-clear logout chain
sso_hint on .rose-ash.com was blocked by Safari ITP — the exact
problem we're solving. Replaced with redirect chain: account logout
chains through each client app's /auth/sso-clear to clear all
first-party sessions without any cross-domain cookies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 12:17:22 +00:00
giles
223491fad5 SSO revocation: clear local session when sso_hint cookie is gone
When account logs out and deletes sso_hint, client apps now detect
the missing cookie and clear their local session on next request.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 12:15:35 +00:00
giles
dfc41ada7d Make account the OAuth authorization server instead of federation
All client apps (including federation) now redirect to account for OAuth.
Factory excludes account from OAuth client blueprint registration.
SSO logout chains through account instead of federation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:53:34 +00:00
giles
60cd08adc9 Add /auth/clear endpoint to reset session cookies
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:44:47 +00:00
giles
d50f01d41f Logout: redirect through federation sso-logout to clear all sessions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:31:44 +00:00
giles
bfd8d55f27 Silent SSO via sso_hint cookie
- Federation sets sso_hint=1 on .rose-ash.com after magic link login
- Client apps: before_request checks sso_hint, triggers silent OAuth
  once per session (sso_checked flag prevents loops)
- Logout clears sso_hint cookie on all apps

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:23:26 +00:00
giles
d0a5170cd9 Fix logout: redirect to blog home to avoid re-auth loop
Account's / requires login, so redirecting there after logout
triggers silent OAuth re-authentication. Blog home is safe.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:15:14 +00:00
giles
8323c45711 Fix logout: post to local /auth/logout/ not federation
Each app has its own session and OAuth logout endpoint now.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:07:03 +00:00
giles
16df62e2c4 Sign-in button → account app, clear old shared cookie
- Nav sign-in links point to account_url('/') instead of login_url()
- After-request hook clears old blog_session cookie on .rose-ash.com
  (prevents collision with new per-app first-party cookies)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 10:56:56 +00:00
giles
ea35e040e7 Fix OAuth authorize URL: include /auth prefix
The federation auth blueprint is mounted at /auth, so the authorize
endpoint is /auth/oauth/authorize, not /oauth/authorize.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 10:25:46 +00:00
giles
46f44f6171 OAuth SSO infrastructure + account app support
- OAuthCode model + migration for authorization code flow
- OAuth client blueprint (auto-registered for non-federation apps)
- Per-app first-party session cookies (fixes Safari ITP)
- /oauth/authorize endpoint support in URL helpers
- account_url() helper + Jinja global
- Templates: federation_url('/auth/...') → account_url('/...')
- Widget registry: account page links use account_url

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 09:55:27 +00:00
giles
326b380135 Fix root top-bar account link to point to federation
The logged-in user links in _full_user.html (desktop + mobile)
still used blog_url — now they use federation_url to match
the account page migration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 09:07:38 +00:00
giles
ea8e7da9d4 Move account page URLs from blog to federation
Auth templates and widget nav links now point to
federation_url instead of blog_url, co-locating the
account UI with the auth system in the federation app.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 09:00:44 +00:00
giles
bd18d0befc Route auth to federation: login_url default, logout URL, federation_url global
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 08:41:18 +00:00
giles
5bed4a6c78 Update README: coop_url → blog_url
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 08:35:16 +00:00
giles
798087de9a Rename coop to blog throughout codebase
- coop_url() → blog_url(), AUTH_APP default → "blog"
- Session cookie: coop_session → blog_session
- Config keys: coop_root/coop_title → market_root/market_title
- All Jinja templates: coop_url → blog_url, coop_title → market_title
- Template blocks: coop-child-header → blog-child-header

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 08:32:46 +00:00
giles
cc22b21b18 Rename coop.rose-ash.com to blog.rose-ash.com in comment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 08:27:54 +00:00
giles
f085d4a8d0 Add search_actors to FederationService for paginated actor search
Fuzzy ILIKE search across remote actors and local profiles, with
WebFinger resolution for @user@domain queries. Supports page-based
pagination for infinite scroll.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 08:18:43 +00:00
giles
b16ba34b40 Add list_marketplaces to MarketService protocol, impl, and stub
Paginated query for market listings — supports optional container filtering
and returns (dtos, has_more) for infinite scroll.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 23:29:14 +00:00
giles
16e4d3aa57 Make upcoming_entries_for_container work without container filter
When container_type/container_id are None, returns all upcoming
confirmed entries across all calendars (for global event listings).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 23:04:55 +00:00
giles
6e438dbfdc Add upcoming_entries_for_container to CalendarService
New paginated query for upcoming confirmed entries across all calendars
belonging to a container (page). Used by the events page summary view.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 22:28:18 +00:00
giles
7316dc6eac Add 'updated' timestamp to Update activity objects for Mastodon
Mastodon requires an updated field to process post edits.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 21:41:18 +00:00
giles
a3a41dbefd Allow repeated Update activities for post edits
The dedup guard was blocking legitimate edits after the first Update.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 21:36:15 +00:00
giles
30b5a1438b Change AP_DOMAIN default to federation.rose-ash.com
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 21:12:28 +00:00
giles
0e89dbee55 Make origin_app migration idempotent
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 20:59:19 +00:00
giles
86ccfd25c5 Add origin_app to APActivity — apps only process their own activities
Each app's EventProcessor now filters by origin_app so apps don't steal
each other's pending activities. emit_activity() and publish_activity()
auto-detect the app name from Quart's current_app.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 20:57:46 +00:00
giles
b42f5d63db Add debug logging to EventProcessor and activity handler registry
Logs which handlers are registered at startup and which handlers are
found/called when processing each activity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 20:49:37 +00:00
giles
2e48760b38 Fix AP re-publish: use versioned object IDs after Delete
After Delete + re-Create, Mastodon tombstones the old object ID and
ignores new Creates with the same ID. Now appends /v2, /v3 etc. so
remote servers treat re-publishes as fresh posts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 19:59:48 +00:00
giles
9cdd2195df Restore all 33 deleted shared templates
Templates were incorrectly identified as dead code because individual
apps override them, but other apps still depend on the shared versions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 19:26:48 +00:00
giles
46f6ca4a0f Remove dead code: domain_event.py + 39 overridden templates
- Delete shared/models/domain_event.py (table dropped, model orphaned)
- Delete 39 shared templates that are overridden by app-local copies:
  - 8 blog overrides (blog/_action_buttons, post/_meta, etc.)
  - 27 events overrides (calendar/*, day/*, entry/*, post_entries/*)
  - 4 market overrides (market/index, browse/_oob_elements, etc.)

These shared copies were never served — Quart loads app-level
templates first, so the app-local versions always win.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:09:02 +00:00
giles
7de4a2e40e Remove dead shared _cart.html template
The cart app has its own override with ticket support. The shared
copy was never used and would only cause confusion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:05:17 +00:00
giles
1c1ab3576f Pass cart_sid through login URL for cross-app cart adoption
When login_url() is called from a different app (e.g. cart), the
anonymous cart_sid is in that app's session cookie. Pass it as a
query parameter so the auth app can store it and use it for adoption.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:46:08 +00:00
giles
07aa2e2be9 Fix cart sign-in button: use plain link instead of HTMX
The login URL is cross-origin (blog app), so hx-get can't load it
into the current page. Use a regular <a href> for cross-app navigation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:37:16 +00:00
giles
2e9db11925 Unify domain_events + ap_activities into AP-shaped event bus
All cross-service events now flow through ap_activities with a unified
EventProcessor. Internal events use visibility="internal"; federation
activities use visibility="public" and get delivered by a wildcard handler.

- Add processing columns to APActivity (process_state, actor_uri, etc.)
- New emit_activity() / register_activity_handler() API
- EventProcessor polls ap_activities instead of domain_events
- Rewrite all handlers to accept APActivity
- Migrate all 7 emit_event call sites to emit_activity
- publish_activity() sets process_state=pending directly (no emit_event bridge)
- Migration to drop domain_events table

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 16:19:29 +00:00