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>
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>
/.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>
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>
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>
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>
Introduces a widget system where domains register UI fragments into
named slots (container_nav, container_card, account_page, account_link).
Host apps iterate widgets generically without naming any domain.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
select_colours was only defined via {% set %} in the root header
template, making it undefined in market/browse nav templates. Moving
it to a Jinja global ensures aria-selected styling works everywhere.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Added app_name Jinja global and aria-selected to nav menu links.
Matches by first path segment (e.g. /market/... → "market") or by
app_name for cross-domain cases (e.g. cart app → "cart" menu item).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
MenuNode and ContainerRelation now live in shared/models/ — importing
glue.models caused SQLAlchemy to see duplicate table definitions.
Also register the two new models in shared/models/__init__.py.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add typed service contracts (Protocols + frozen DTOs) in shared/contracts/
for cross-domain communication. Each domain exposes a service interface
(BlogService, CalendarService, MarketService, CartService) backed by SQL
implementations in shared/services/. A singleton registry with has() guards
enables composable startup — apps register their own domain service and
stubs for absent domains.
Absorbs glue layer: navigation, relationships, event handlers (login,
container, order) now live in shared/ with has()-guarded service calls.
Factory gains domain_services_fn parameter for per-app service registration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move 6 model files to shared/models/ (ghost_content, order, page_config,
market, market_place, calendars) so apps import from shared, not each other
- Fix auth templates: replace url_for('auth.*') with coop_url() for
cross-app compatibility
- Fix TYPE_CHECKING import in sumup.py to use shared.models.order
- Delete dead infrastructure/cart_loader.py (inverted dependency)
- Update models/__init__.py to export all new models
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
market_url('/product/...') was missing the /<page_slug>/<market_slug>/
prefix required by the market app's route structure. New helper
market_product_url() resolves the prefix from request context
(g.post_slug/g.market_slug in market app, g.page_slug + market_place
in cart app).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Templates: item.post.X → item.X (MenuNode has label/slug/feature_image directly)
- factory.py: add glue.models to import loop + register_glue_handlers() at startup
- alembic env.py: add glue.models to import loop
- New migration: container_relations + menu_nodes tables with backfill from existing data
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cross-domain relationships like Product.order_items → OrderItem use
string references that SQLAlchemy resolves by class name lookup. All
model packages must be imported so every class is registered before
mapper configuration runs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
shared/logging/ shadows Python's stdlib logging module, causing a
circular import when any code does `import logging`. This breaks
both the entrypoint Redis flush and Hypercorn app loading.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>