fetch_fragment() auto-returns "" when called inside a fragment request
(detected via X-Fragment-Request header). This prevents deadlocks when
e.g. blog fetches cart-mini from cart, and cart's context processor
fetches nav-tree from blog.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
FragmentError raised on network errors or non-200 responses when
required=True (default). Logs at ERROR level. Pass required=False
for optional fragments that should degrade gracefully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pool_size=5, max_overflow=10 per app (6 apps = 90 max connections).
Previously pool_size=0 meant unlimited connections, causing
asyncpg.TooManyConnectionsError under concurrent load.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Domain-specific templates moved from shared/browser/templates/ to
each app's templates/ dir. 38 domain-free infrastructure templates
remain (root layout, macros, oob, mobile, social, sentinel).
Removed fragment fallbacks from root header.
Deleted _widgets/ (dead code since Phase 4-5).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Account nav links and page panels (tickets, bookings) now fetched
as HTML fragments from events app instead of using widget registry.
All three widget registration functions are now no-ops.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Templates now consume container_nav_html and card_widgets_html from
fragment fetches instead of iterating widgets.container_nav / container_cards.
OOB nav template renders entry/calendar links directly from data.
Calendar and market widget registrations removed (account widgets remain).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously only 'cart' was mapped — slugs like 'federation', 'market',
'events', 'account' incorrectly linked to blog_url(). Now all app
slugs resolve to their proper cross-app URLs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The shared _header.html template now checks for pre-fetched fragment
HTML (nav_tree_html, auth_menu_html, cart_mini_html) before falling
back to local includes. This lets consumer apps render cross-app
UI via the fragment composition system.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Avoids collision with app-level url_value_preprocessor that pops
"slug" for page-level routing. All url_for() calls for product
endpoints now use product_slug= instead of slug=.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New HTTP client (fragments.py) fetches HTML fragments from other apps
over the Docker network, with Redis caching and graceful degradation.
Jinja global `fragment()` available in all templates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When account logs out it deletes did_auth:{device_id} from Redis.
If that key is gone, bypass the 60s grant cache and re-check the
DB immediately, detecting the revoked grant on the first request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Delivers rose:DeviceAuth activities to configured external inboxes
(e.g. artdag) via signed HTTP POST. Config via EXTERNAL_INBOXES env var.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously backfill_follower sent all Create activities regardless
of whether they were later Deleted or Updated. Now it excludes
deleted sources and applies the latest Update's object_data so
new followers see the current version of each post.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Mastodon requires object IDs to match the actor's domain. The
object_data stored in DB uses federation.rose-ash.com but per-app
delivery uses blog.rose-ash.com etc. Now rewrites id and
attributedTo in object_data, not just the activity-level fields.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
/.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>
- 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>
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>
- 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>
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>
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>
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>
- 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>
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>
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>
- 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>
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>
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>
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>
- 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>
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>
- 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>
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>
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>
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>