PDS 0.4.0 (@atproto/pds 0.4.0-beta.7) signs the service-auth JWT it
attaches to proxied app.bsky.* calls with iss, aud and exp only
(createServiceJwt in dist/index.js of the image). The AppView requires
the lxm claim and rejects every such call with 401
BadJwtLexiconMethod, so social-app shows errorMessageView on Following
and Notifications. 0.4.66 passes the called method as lxm into every
service JWT it signs and sends every proxied call through the
SSRF-guarded proxy agent.
0.4.66 also replaces the whole-handle limit of 0.4.0 (30 characters
for the entire handle) with a limit of 18 on the part in front of the
service domain, so kc-administrator.<domain> passes for any domain
length, including kc-administrator.infinito.nexus (31).
- meta/services.yml: pin ghcr.io/bluesky-social/pds to 0.4.66.
- vars/main.yml: the public AppView URL is always https. Onion-primary
deployments took the protocol from the local TLS setting and sent the
service JWT to api.bsky.app over plain http. services.view.enabled is
evaluated as a bool.
- templates/env.j2: drop PDS_ADMIN_EMAIL, which neither 0.4.0 nor
0.4.66 reads. PDS_CONTACT_EMAIL_ADDRESS stays unset on purpose:
describeServer would publish the administrator's address.
- Administration.md: the account database only migrates forward; a
rollback restores the bluesky_pds_data backup together with the old
pin.
- Playwright: the broker login moves into _shared.js and waits for a
landing outside /oauth2/, because the bare base URL also matches the
intermediate /oauth2/callback hop. New scenarios check that the PDS
proxies getTimeline and listNotifications to the AppView with a JWT
it accepts, for the broker session (OIDC) and for a native session
(oauth2 disabled), and that social-app renders Following and
Notifications without errorMessageView or lxm page errors. The
rendering scenario is clearnet only: social-app boots only after
loading its age-assurance config from public.api.bsky.app.
Merge note for feature/store-i18n: 19b491b014 raises the pin to 0.4.21
and renames the contact-mail line to PDS_CONTACT_EMAIL_ADDRESS. Resolve
the pin to 0.4.66 (0.4.21 still signs without lxm, and the account
database cannot migrate back) and keep the line deleted. Take the union
of both sides in _shared.js and playwright.spec.js.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Bridge the architectural gap that left bluesky without any usable login
path when services.oauth2.enabled is false. The login-broker stays the
upstream when OIDC is on (broker auto-provisions PDS accounts via the
encrypted-app-password handoff). When OIDC is off, the broker is bypassed
and the stock social-app is served directly; the PDS administrator is
provisioned by Ansible so the operator can sign in with handle+password.
Routing (tasks/01_core.yml):
- Front-proxy http_port now picks `BLUESKY_BROKER_PORT` when
services.oauth2.enabled is true, `BLUESKY_WEB_PORT` (social-app)
otherwise. Previously the broker was always upstream, so variant-1
deploys returned a `Refusing handoff` 401 on every request.
PDS provisioning (tasks/07_native_admin.yml, new):
- uri_retry waits on PDS `/xrpc/_health`, then POSTs createAccount with
`kc-administrator.<handle-domain>` (PDS rejects bare `administrator`
as a reserved handle, hence the `kc-` prefix the broker already uses
for OIDC-derived accounts).
- Idempotent: 200 = changed, 400 with "Handle already taken" = ok.
- Gated on `BLUESKY_WEB_ENABLED and not services.oauth2.enabled`.
Broker hardening (files/login-broker/server.js):
- 401 response (missing `X-Forwarded-User`) now `text/html` instead of
`text/plain`. Headless Chromium does not fire `DOMContentLoaded`
reliably on non-HTML 401 documents — Playwright `page.goto(...,
{ waitUntil: "domcontentloaded" })` hung the full 60s test timeout in
the variant-1 guest-persona test.
Playwright spec (files/playwright/):
- Split single-file playwright.spec.js (203 lines) into _shared.js +
seven test-*.js modules following the matrix/taiga/nextcloud/pixelfed
pattern. Each scenario is now individually grep-able from the
reporter output.
- Add test-native-admin.js: verifies broker bypass (landing body
contains `<title>Bluesky</title>`, NOT `Refusing handoff`) plus PDS
createSession returns 200 + accessJwt for the deployed admin handle.
Uses Playwright's APIRequestContext directly — driving the stock
social-app login UI (hosting-provider switcher + welcome modal +
React form state) was too brittle.
Vars + env (vars/main.yml, templates/playwright.env.j2):
- `BLUESKY_ADMIN_HANDLE` and `BLUESKY_PDS_INTERNAL_URL` added for the
Ansible task.
- `ADMIN_HANDLE` and `PDS_BASE_URL` exposed to Playwright env.
Verification:
- Variant 1 (live, in-container): 3 passed, 4 skipped, 0 failed.
- Variant 0 / Variant 2: spec bodies unchanged from pre-refactor,
Ansible conditional invariant on the OIDC-on branch — but not
re-deployed for live verification.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>