# API-owned authentication adaptor The Go API owns browser login and sessions. Deploy the retained web with the matching API edge and same-origin `/auth/oidc/*` routing. NextAuth, the Party bridge and the authentication-mode switches have been removed. Old browser sessions require a fresh AAI login; old mode settings confer no authority. Unenrolled deployments must supply the API transport and credentials before starting this version. Missing configuration fails closed. The adaptor uses `THEMELIO_AUTH_API_URL`, `THEMELIO_URL`, `THEMELIO_AUTH_ADAPTOR_KEY` and `THEMELIO_AUTH_POLICY_KEY`. Each key supports a `_FILE` binding and must contain at least 32 bytes. The two credentials have separate scopes. HTTPS is required outside explicit `THEMELIO_ENV=local`. `THEMELIO_COMPATIBILITY_URL` identifies the private retained service origin. Private routes reject a public host or a conflicting forwarded host even when the caller presents a valid service credential. Login forms submit to the API and the retained web uses `pharos_session` for shared server helpers. Middleware chooses the operation from the server route and method. Provider tokens supplied by a browser confer no authority; a token is injected only after canonical validation, exact session fence checks and current verified GitLab binding resolution. Provider credentials remain in server storage. Unsafe requests require the configured public Origin. The old invitation-acceptance GET is closed; its POST action remains subject to the same gates. The dashboard reads pending invitation summaries through the isolated Waldur actor's `/internal/compat/v1/waldur/invitations/summaries` capability. It uses the current Party's verified email and established Waldur binding, then checks the claim again before rendering. It requires no staff token or retained-profile email in the web process. Provider failures produce an unavailable message. The actor returns only invitation IDs, scope IDs, Project names and role labels; its existing ID-only list response remains unchanged. Both compact and dashed Waldur UUIDs are accepted at the provider boundary. Server-rendered resource and Project reads call the retained API over its configured internal origin, forwarding only the canonical session cookie. Custom headers cannot replace that session or supply provider authority. Unsafe server requests retain the incoming public-Origin check; canonical requests use uncached responses and reject redirects. Anonymous catalogue and canonical repository content reads use the explicit allowlist in `request-gates.ts`. These calls discard browser-supplied provider credentials and read the separate `pharos-public` namespace without a provider token. A supplied invalid canonical session still fails validation. Settings, publication commands, authoring and Run operations retain their session gates. Source Control authorises publication independently through the canonical API. The onboarding layout accepts a limited canonical session. Normal pages require current legacy AUP and GitLab readiness. AUP acceptance resolves the verified Party/GitLab association and calls the retained acceptance writer. It requires no impersonation token. The private `/internal/compat/v1/policy` reader uses its own credential, binding lookup and GitLab AUP read; it never validates a browser session. Exclude `/internal/*` from public ingress. The same private policy path supports a binding-independent GET for the current required version. It uses the enrolled policy-reader credential and private-host checks, without reading a member's binding or acceptance. SSH Access can therefore revoke keys after confirmed loss of the binding needed to prove acceptance. Logout forwards both the session cookie and complete in-flight login fence to the API's public logout operation before discarding the cookies. The API owns immediate revocation and subsequent provider cleanup. Denial, recovery, pending work and authority outages cannot trigger legacy login fallback. The status page refreshes its own authorised snapshot on typed session-readiness notifications and after connecting or reconnecting to Centrifugo. Subscriptions are supplied by the server. The explicit status link remains available; there is no browser polling. Credential work awaiting issuance remains pending, and work requiring attention remains resumable before its binding confirmation is saved. It presents GitLab setup and policy readiness separately, offers an explicit resume action when GitLab work needs attention, and opens policy acceptance as soon as its verified account binding is available. Waldur readiness does not delay the GitLab workspace. Resume sends the API's public same-origin command; the browser keeps only its command/correlation identities in session storage until the matching durable acknowledgement. An uncertain response or page reload reuses those identities, while a later login has a separate request scope. The private account/credential operations atomically record desired work, a command receipt and the original outcome in retained Valkey. Account work is unique by Party and provider scope; credential work is unique by session generation and verified binding. Restored records, their index correlations and revisions are checked before use. Public progress contains no proof or token. Accepted profile material includes `name`, verified `email` with `email_verified: true`, `preferred_username`, `given_name`, `family_name`, `schac_home_organization`, `voperson_external_affiliation` and `entitlements`. Affiliations and entitlements are optional arrays of at most 128 strings, each bounded to 2048 UTF-8 bytes. Their order and exact values survive persistence; an explicit empty array remains distinct from an omitted claim. Text rejects control characters and malformed Unicode. Names are bounded to 1024 bytes, preferred username to 256 and home organisation to 2048. These profile claims establish no permissions or account ownership. Per-field bounds also sit within the total transport limits: the API limits its signed assertion to 16 KiB, and retained work requests remain capped at 32 KiB. Profiles exceeding an envelope limit are rejected before provider work. The private evidence response carries optional top-level `voperson_id` and `site_username` separately from the profile. Fresh API v3 authentication requires `voperson_id`; historical evidence can omit it. Party Governance owns identity recovery and Site binding, and the retained worker cannot infer either from a username or email. Native GitLab requests contain only name, verified email, verification flag and preferred username. Existing stored command bytes and receipt fingerprints remain unchanged when optional claims are absent. Run the dedicated worker with `pnpm --dir compat/themelio --filter gitlab-web compatibility:worker`. It claims durable work using expiring leases and drains the active operation before closing storage. The `compatibility-worker` target in `compat/themelio/Dockerfile` compiles the same entry point and its Redis dependency into one JavaScript bundle. The API-auth overlay deploys that image separately from Next.js with a tokenless ServiceAccount, a read-only filesystem and scoped credentials. Loopback-only `/livez` and `/readyz` endpoints support exec probes; readiness closes on termination before the active operation drains. Storage failures restart the process and leave durable work available for lease recovery. Their port defaults to `9090`; isolated native fixtures may set `COMPATIBILITY_WORKER_HEALTH_PORT=0` with `THEMELIO_ENV=local`. Provide: | Setting | Purpose | | --- | --- | | `VALKEY_URL`, `VALKEY_USERNAME`, `VALKEY_PASSWORD` | Retained `themelio:*` storage; the worker has no administrator credential. | | `GITLAB_COMPAT_INSTANCE`, `THEMELIO_COMPAT_ACCOUNT_SCOPE` | Enrolled GitLab account scope; it must match the API dispatcher. | | `WALDUR_COMPAT_INSTANCE` | Optional enrolled Waldur scope. Without the complete actor configuration Waldur account work stays `attention_required / proof_unavailable`. | | `WALDUR_ACTOR_URL`, `WALDUR_ACCOUNT_ACTOR_KEY` | Private Waldur actor origin and the worker's account-association capability. The worker receives no Waldur staff credential. | | `THEMELIO_COMPAT_DISPATCHER_KEY` | API calls to retained work routes. | | `THEMELIO_COMPAT_EVIDENCE_KEY`, `THEMELIO_COMPAT_FENCE_KEY` | Separate API evidence-resolution and current-fence capabilities. | | `THEMELIO_COMPAT_BINDING_READER_KEY`, `THEMELIO_COMPAT_BINDING_WRITER_KEY` | Separate canonical binding query and signed-fact hand-off capabilities. | | `THEMELIO_COMPAT_BINDING_SIGNING_KEY`, `THEMELIO_COMPAT_BINDING_SIGNING_KEY_ID` | Worker-only Ed25519 PEM and enrolled Beto key ID. The PEM supports `_FILE` and retains its newlines. | | `GITLAB_ROOT_ACTOR_URL`, `GITLAB_ROOT_ACTOR_COMPAT_KEY` | Closed retained GitLab operations; no GitLab administrator token reaches the worker. | The root actor also needs `GITLAB_COMPAT_OIDC_ISSUER` and `GITLAB_COMPAT_OIDC_PROVIDER`. An explicitly enrolled `GITLAB_COMPAT_OIDC_PROVIDER_ALIASES` list lets the adaptor recognise an account carrying the same external subject under an earlier provider name; new accounts always use the canonical provider. Its old account creation and impersonation-token creation endpoints have been removed. The closed compatibility routes are the only signup writers. The isolated Waldur actor receives `WALDUR_API_URL`, `WALDUR_STAFF_TOKEN`, `WALDUR_COMPAT_INSTANCE`, `WALDUR_COMPAT_ISSUER`, `WALDUR_IDENTITY_SOURCE` and `WALDUR_IDENTITY_REGISTRATION_METHOD`. Its account and invitation interfaces use separate `WALDUR_ACCOUNT_ACTOR_KEY` and `WALDUR_INVITATION_ACTOR_KEY` capabilities. The retained web receives only the invitation capability for this slice. Existing retained endpoints keep their legacy provider configuration until their own port migration is qualified. GitLab acquisition queries the canonical provider and each enrolled transition alias using the exact external subject, checks every returned identity, then establishes the native account binding. Account preparation neither creates nor requires category roots, personal subgroups or Owner memberships. Source Control owns Project namespaces and their membership projection independently of login. A provider alias is accepted only when it resolves the exact subject to one GitLab user. Multiple users, a foreign identity, or a username or email collision requires attention. The worker signs an immutable ownership fact after provider confirmation and retains it across retries; account readiness waits for Party Governance's established binding. Before signing each fresh proof, the read-only native verification operation rechecks the exact current issuer/subject against the retained account ID. Identity recovery can retain an earlier provider effect; that observation alone cannot prove ownership for the replacement subject. A mismatch requires attention, and verification never creates a replacement provider account. If the work's candidate binding is absent, the worker queries Party Governance by the exact provider, instance and external account ID. It may reuse an established binding only for the same Party, after fresh native ownership and current evidence checks and a second canonical binding read. Retirement, foreign ownership and changed provider identities require attention. Unavailable authority remains retryable. The lease/revision-fenced checkpoint retains the established binding ID and discards only the superseded, uncommitted candidate proof; it changes no canonical binding, proof, command receipt or policy acceptance. An uncertain checkpoint acknowledgement is resolved from persisted work before retry. This recovery preserves the API session authority. The [GitLab users API](https://docs.gitlab.com/api/users/) supplies the exact identity query, and the [user tokens API](https://docs.gitlab.com/api/user_tokens/) supplies paginated marker reconciliation. Credential targets are persisted before issuance. A lost result is reconciled through its exact work marker before another token can be created. An unresolved outcome remains `attention_required / uncertain_effect`. Logout records a terminal fence before cleanup; late results cannot reopen it. A lost final Valkey acknowledgement is resolved before compensating so a successfully saved token remains usable. Local credential exposure always rechecks the current API fence and canonical binding. Provider expiry uses date precision; durable cleanup handles the interval after the canonical session expires. Definitive identity/proof/authority failures require explicit resume. Provider outages and uncertain effects remain retryable server work. New authentication generations retain observed provider effects and replace stale proof material. A Waldur account is the user named exactly by the verified OIDC subject. The actor reads that username before invoking Identity Bridge and accepts an existing account only when its identity source or registration method matches the enrolled source. It then calls Identity Bridge and confirms the exact username and UUID. A foreign account collision requires attention before Identity Bridge can mutate it. Only the configured issuer and subjects Waldur can store (`[a-z0-9@.+_-]`, at most 128 characters) qualify, and the email must be verified. The user's UUID is signed into the provider binding like the GitLab account. Without a configured Waldur the work reports `proof_unavailable`. GitLab and AUP progress independently of Waldur. Invitations use the Party's established Waldur binding: the POST acts as that Waldur user through Waldur's staff impersonation header (`X-Impersonated-User-UUID`), because Waldur 8.1.3 no longer returns another user's token. The staff token must therefore belong to a staff user, and the deployment must not enforce passkeys for it. The verified email only selects the invitations offered; no user is looked up by email or created, and a missing binding answers `409 provider_pending`. Run `pnpm --dir compat/themelio --filter gitlab-web test`, `type-check`, `pnpm --dir compat/themelio --filter gitlab-root-actor test`, `pnpm --dir compat/themelio --filter waldur-provider-actor test` and `python3 scripts/themelio_party_integration.py`. The isolated storage lane covers receipts, restart recovery, generation updates, lease fencing, ownership hand-offs and token/logout races. Live Keycloak/GitLab/Waldur, same-origin browser routing, private TLS/ingress denial and deployment qualification remain deployment requirements. There is no runtime switch back to NextAuth. Rollback requires a coordinated release rollback which fences API sessions and preserves Party identities, provider bindings and durable compatibility work.