Vanilla 401 handler did `window.location.replace('/?next_path=<currentPath>')`.
That IS a hard nav, but the browser's HTTP cache returns the cached SPA
bundle for `/` — the SPA boots, re-fetches the same /api endpoint, gets 401
again, and loops without ever hitting Traefik at the document level. Diagnosed
2026-05-05 via HAR analysis: 9 history entries bouncing `/` ↔ `/?next_path=/`
at ~780ms intervals; zero requests to bridge or oauth2-proxy during the
loop; first bridge.binarybeach.io/handoff request only after Ctrl+Shift+R.
Trigger on the platform side: oauth2-proxy refresh fails for cross-org
gmail-federated users (separate root cause — disabled platform-wide via
OAUTH2_PROXY_OIDC_GROUPS_CLAIM=). The hard-nav fix here is the safety net
that handles that and any other future 401-causing scenario.
Replace with `window.location.replace('/sign-in/?_bb_reauth=<Date.now()>')`:
- /sign-in/ matches Plane's priority-200 plane-signin-redirect Traefik
router (matched on PathRegexp `^/(sign-in|sign-up|signin|login|register|
accounts/sign-in)(/.*)?$$`), which 302s to the bridge handoff regardless
of cookie state.
- _bb_reauth=<ts> cache-busts so even a previously-cached /sign-in/
response can't short-circuit the request.
Vanilla Plane regression-safe: /sign-in/ is also a known SPA route in
upstream that bounces to /, so non-platform deployments see the same
behavior they'd get without this patch (modulo a single extra navigation).
Also fixes BINARYBEACHIO.md frontend build instructions: Dockerfile.web
needs the monorepo root as build context (turbo prune scope), opposite of
Dockerfile.api which needs apps/api/ as context.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Marker-cookie pattern per docs/conventions/per-app-edge-identity-validation.md:
- New BbEdgeIdentityMiddleware compares `_bb_edge_sub` cookie to
`X-Auth-Request-User` header on every authenticated request. On mismatch,
flushes the Django session and replaces request.user with AnonymousUser
so DRF returns 401 / browser navigations land at the bridge handoff
redirect. Lazy-populates the cookie on legacy sessions; passes through
for anonymous requests and bearer-token-only callers.
- Trusted-JWT view sets `_bb_edge_sub` on the redirect response when
X-Auth-Request-User is present (single session-mint choke-point — the
Bucket-4 entry-point is the only path that creates Plane sessions in
this deployment).
- SignOutAuthEndpoint reads optional BB_LOGOUT_REDIRECT_URL env. When set,
the SPA's /auth/sign-out/ form-POST is 302'd to the platform bridge's
synced-logout endpoint (clears edge `_bb_oauth2` + back-channels Zitadel
end_session). Without this, the user's Zitadel session at the edge
outlives the Plane logout and silently re-logs them in via bridge handoff
→ trusted sign-in. Vanilla regression-safe: env unset → upstream behavior.
Net surface vs upstream-clean: 1 new middleware file, 1 line in MIDDLEWARE,
~20 lines added to trusted.py and signout.py. No new dependencies.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Identity-model rollout T2.4. Trusted view now derives `lookup_email = bb_mailbox or email`
and uses it for both User.objects.filter() and the new-User row's email field. WARN-log
fallback to federation email when the claim is absent (transitional safety; should never
fire once Zitadel `bb-claims` Action + bridge-side userinfo enrichment are live).
Decode-time required-claims unchanged (`bb_mailbox` stays optional) so partial deploys
aren't bricked. Pre-migration SQL rename of operator's existing User row required —
see binarybeachio docs/services/plane/migration-plan.md §9.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migrates this fork to the binarybeachio platform-architecture pivot:
oauth2-proxy at the edge enforces a Zitadel session, the auth-bridge
mints a short-lived RS256 JWT, and a NEW additive endpoint at
/auth/sign-in-trusted/ verifies the JWT, claims its jti against
shared-redis (single-use replay protection, fail-closed), find-or-creates
the User, and starts a Django session via user_login().
Net surface vs. upstream-clean: 1 new view file + 1 url path + 1
exports __init__ entry + 7 reserved error codes (6000-6099 range).
github.py and the GitHub-button rebrand patch are reverted to upstream
— sign-in entry-point UX is now driven by Traefik redirectregex on
/sign-in* in infrastructure/plane/docker-compose.yml.
Replay protection contract: jti claim minted by bridge, consumed via
Redis SETNX with ttl = exp - now + 30s. Documented at
binarybeachio/docs/architecture/bridge-jwt-replay-protection.md.
Public-key transport: BB_BRIDGE_PUBLIC_KEY_URL env points at the
in-cluster bridge's /.well-known/bb-bridge.pub.pem (avoids the
env-PEM corruption issue Coolify has with backslash-escaped keys).
Endpoint is implicitly disabled (404) when env unset — vanilla
upstream behavior preserved.
Storage patches (Patch 2) unchanged. Brand asset preserved (dormant).
Pre-migration source state preserved on branch pre-migration-2026-05-04.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restructure "What's customized" into three patch groups with full file
inventories:
1. Zitadel OIDC (repurpose GitHub OAuth)
2. Brand label + logo
3. Presigned PUT for uploads (R2/B2 don't implement PostObject)
Each patch group is independently revertable; group 3 references
binarybeachio/docs/features/storage-upload-flow.md for the decision
record + rollback procedure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three small fork tweaks bundled together; none touch upload flow:
* OIDC: pass `prompt=select_account` so Zitadel always shows its account
picker rather than silently passing through an existing session. Override
with OIDC_PROMPT env var.
* Branding: swap "with binarybeach.io" -> "with BinaryBeach.io" and replace
GitHub light/dark logo imports with our brand mark (works on both themes).
* Session: thread the binarybeachio session-lifecycle convention values
(SESSION_COOKIE_AGE, ADMIN_SESSION_COOKIE_AGE, SESSION_SAVE_EVERY_REQUEST)
through docker-compose.bb-local.yml app-env mixin and document the
cross-fork convention link in BINARYBEACHIO.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Patches the plane-backend GitHubOAuthProvider so the /auth/github/*
flow points at our self-hosted Zitadel instance when ZITADEL_DOMAIN
is set, and falls back to vanilla GitHub OAuth when unset (regression-
safe). Touch surface is one backend file plus a cosmetic frontend
label change. Full rationale, configuration steps, refresh procedure,
and AGPL compliance notes in BINARYBEACHIO.md at repo root.