diff --git a/docs/runbooks/atproto-auth-server-smoke-test.md b/docs/runbooks/atproto-auth-server-smoke-test.md index ad34270..d9d69b8 100644 --- a/docs/runbooks/atproto-auth-server-smoke-test.md +++ b/docs/runbooks/atproto-auth-server-smoke-test.md @@ -2,9 +2,9 @@ ## Purpose -Verify that a `ready` DiVine account can be used by an external Bluesky-compatible client through the delegated ATProto Authorization Server on `login.divine.video`. +Verify that a `ready` DiVine account can be used by an external Bluesky-compatible client through the delegated ATProto Authorization Server on `entryway.divine.video`. -This smoke test covers the Phase 2 contract that is implemented today: +This smoke test covers the live ATProto launch contract: - PDS protected-resource discovery - Authorization Server metadata discovery @@ -33,7 +33,7 @@ Current limitations to account for during testing: - `ATPROTO_OAUTH_PDS_DID` or `PDS_SERVICE_DID` - `rsky-pds` is configured with: - `PDS_SERVICE_DID` - - `PDS_ENTRYWAY_URL=https://login.divine.video` + - `PDS_ENTRYWAY_URL=https://entryway.divine.video` - `PDS_ENTRYWAY_JWT_PUBLIC_KEY_HEX` matching the public key for keycast `ATPROTO_OAUTH_JWT_PRIVATE_KEY_HEX` ## 1. Discover The PDS Protected Resource @@ -47,31 +47,28 @@ curl -sS https://pds.divine.video/.well-known/oauth-protected-resource | jq Expect: - `resource` points at the PDS public URL -- `authorization_servers` is exactly `["https://login.divine.video"]` +- `authorization_servers` is exactly `["https://entryway.divine.video"]` ## 2. Discover The Authorization Server Run: ```bash -curl -sS https://login.divine.video/.well-known/oauth-authorization-server | jq +curl -sS https://entryway.divine.video/.well-known/oauth-authorization-server | jq ``` Expect at least: -- `issuer = "https://login.divine.video"` -- `authorization_endpoint = "https://login.divine.video/api/atproto/oauth/authorize"` -- `token_endpoint = "https://login.divine.video/api/atproto/oauth/token"` -- `pushed_authorization_request_endpoint = "https://login.divine.video/api/atproto/oauth/par"` -- `scopes_supported` includes `atproto` -- `token_endpoint_auth_methods_supported` includes both `none` and `private_key_jwt` -- `client_id_metadata_document_supported = true` +- `issuer = "https://entryway.divine.video"` +- `authorization_endpoint = "https://entryway.divine.video/api/oauth/authorize"` +- `token_endpoint = "https://entryway.divine.video/api/oauth/token"` +- `pushed_authorization_request_endpoint = "https://entryway.divine.video/api/oauth/par"` +- `token_endpoint_auth_methods_supported` includes `none` - `require_pushed_authorization_requests = true` -This smoke test uses the public-client path by default. +This smoke test uses the public-client path only. - Public client: send `client_id` directly and do not include `client_assertion`. -- Confidential client: host a client metadata document at the `client_id` URL, set `token_endpoint_auth_method = private_key_jwt`, publish `jwks` or `jwks_uri`, and include `client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer` plus a signed `client_assertion` at PAR, token exchange, and refresh. The assertion uses `iss = sub = client_id` and `aud = https://login.divine.video`. ## 3. Create A PAR Request @@ -102,7 +99,7 @@ header = { typ: "dpop+jwt", alg: "ES256", jwk: JSON.parse(ENV.fetch("P256_DPOP_J payload = { jti: "par-#{SecureRandom.uuid}", htm: "POST", - htu: "https://login.divine.video/api/atproto/oauth/par", + htu: "https://entryway.divine.video/api/oauth/par", iat: Integer(ENV.fetch("PAR_IAT")) } segments = [ @@ -120,7 +117,7 @@ puts "#{segments.join(".")}.#{Base64.urlsafe_encode64(sig, padding: false)}" curl -sS \ -D /tmp/atproto-par-headers.txt \ - -X POST https://login.divine.video/api/atproto/oauth/par \ + -X POST https://entryway.divine.video/api/oauth/par \ -H 'Content-Type: application/x-www-form-urlencoded' \ -H "DPoP: $PAR_DPOP" \ --data-urlencode 'client_id=https://example-client.invalid' \ @@ -150,7 +147,7 @@ PAR_NONCE="$(awk 'BEGIN{IGNORECASE=1}/^DPoP-Nonce:/{print $2}' /tmp/atproto-par- Open: ```text -https://login.divine.video/api/atproto/oauth/authorize?request_uri= +https://entryway.divine.video/api/oauth/authorize?request_uri= ``` Expect: @@ -161,7 +158,7 @@ Expect: - the callback query includes: - `code` - `state=smoke-test-state` - - `iss=https://login.divine.video` + - `iss=https://entryway.divine.video` If the account is not `ready`, expect the authorization request to fail instead of issuing a code. @@ -175,7 +172,7 @@ header = { typ: "dpop+jwt", alg: "ES256", jwk: JSON.parse(ENV.fetch("P256_DPOP_J payload = { jti: "token-#{SecureRandom.uuid}", htm: "POST", - htu: "https://login.divine.video/api/atproto/oauth/token", + htu: "https://entryway.divine.video/api/oauth/token", iat: Integer(`date +%s`), nonce: ENV.fetch("PAR_NONCE") } @@ -194,7 +191,7 @@ puts "#{segments.join(".")}.#{Base64.urlsafe_encode64(sig, padding: false)}" curl -sS \ -D /tmp/atproto-token-headers.txt \ - -X POST https://login.divine.video/api/atproto/oauth/token \ + -X POST https://entryway.divine.video/api/oauth/token \ -H 'Content-Type: application/x-www-form-urlencoded' \ -H "DPoP: $TOKEN_DPOP" \ --data-urlencode 'grant_type=authorization_code' \ @@ -232,7 +229,7 @@ header = { typ: "dpop+jwt", alg: "ES256", jwk: JSON.parse(ENV.fetch("P256_DPOP_J payload = { jti: "refresh-#{SecureRandom.uuid}", htm: "POST", - htu: "https://login.divine.video/api/atproto/oauth/token", + htu: "https://entryway.divine.video/api/oauth/token", iat: Integer(`date +%s`), nonce: ENV.fetch("TOKEN_NONCE") } @@ -251,7 +248,7 @@ puts "#{segments.join(".")}.#{Base64.urlsafe_encode64(sig, padding: false)}" curl -sS \ -D /tmp/atproto-refresh-headers.txt \ - -X POST https://login.divine.video/api/atproto/oauth/token \ + -X POST https://entryway.divine.video/api/oauth/token \ -H 'Content-Type: application/x-www-form-urlencoded' \ -H "DPoP: $REFRESH_DPOP" \ --data-urlencode 'grant_type=refresh_token' \ diff --git a/docs/runbooks/atproto-opt-in-smoke-test.md b/docs/runbooks/atproto-opt-in-smoke-test.md index 61f66c7..49fb1a6 100644 --- a/docs/runbooks/atproto-opt-in-smoke-test.md +++ b/docs/runbooks/atproto-opt-in-smoke-test.md @@ -2,6 +2,8 @@ Use this checklist to validate the end-to-end ATProto opt-in flow across `rsky-pds`, `keycast`, `divine-sky`, `divine-name-server`, Fastly KV, and `divine-router`. +The GKE workloads are delivered through `divine-iac-coreconfig` and ArgoCD. The public edge services (`divine-name-server` on Cloudflare Workers and `divine-router` on Fastly Compute@Edge) are deployed separately and must be live before treating the smoke as production-valid. + ## Production Login Contract The production login contract is separate from the lifecycle smoke: @@ -12,6 +14,7 @@ The production login contract is separate from the lifecycle smoke: - `entryway.divine.video` is the ATProto Authorization Server Do not use `login.divine.video` as a protocol origin in production checks; the public discovery chain should go handle -> DID -> PDS -> entryway. +If any step in the edge chain is not live, the smoke is staging-only and should not be used to widen rollout. Use `scripts/smoke-divine-atproto-login.sh` to validate the handle, DID, PDS, and entryway chain before running the opt-in lifecycle checks below. @@ -110,3 +113,4 @@ For a localnet run instead of staging: - Live queue success must advance the relay cursor after enqueue, not after PDS completion. - Backfill failure must surface as `account_links.publish_backfill_state = 'failed'` with `publish_backfill_error` populated. - Client feature flags must be required to expose the ATProto controls on mobile and web. +- The smoke order must be: login-chain smoke, opt-in lifecycle smoke, then any rollout widening. diff --git a/docs/runbooks/launch-checklist.md b/docs/runbooks/launch-checklist.md index 0076c43..adcc3c7 100644 --- a/docs/runbooks/launch-checklist.md +++ b/docs/runbooks/launch-checklist.md @@ -3,15 +3,18 @@ ## Deploy Contract - Confirm `divine-iac-coreconfig` is the source of truth for staging and production manifests, secrets, and routes. +- Confirm ArgoCD covers the GKE services (`keycast`, `rsky-pds`, `divine-atbridge`, `divine-handle-gateway`) but not the Cloudflare Worker or Fastly edge. - Confirm the `sky` namespace exists for `divine-sky` workloads. - Confirm `divine-atbridge` and `divine-handle-gateway` are internal-only services. - Confirm `divine-feedgen` and `divine-labeler` are the only public services. - Confirm public hostnames are `feed.staging.dvines.org`, `feed.divine.video`, `labeler.staging.dvines.org`, and `labeler.divine.video`. +- Confirm the live ATProto contract is `login.divine.video` for the human console, `entryway.divine.video` for the Authorization Server, and `pds.divine.video` for the PDS. ## Preflight - Confirm `cargo fmt --check`, `cargo clippy --workspace --all-targets -- -D warnings`, and `bash scripts/test-workspace.sh` pass on the release candidate. - If `cargo check --workspace` is still failing because of the unrelated `divine-feedgen` baseline, record that separately and do not treat it as a bridge-scheduler regression. +- Confirm the rollout order is: host contract, PDS protected-resource metadata, keycast GitOps/runtime wiring, immutable image pinning, then public-edge deploys. - Verify keycast can claim usernames without enabling ATProto by default. - Verify a verified cookie-auth user can open `settings/security` and see the `Bluesky Account` card without unlocking private-key export first. - Verify keycast `/api/user/atproto/enable`, `/status`, and `/disable` work for an authenticated user. @@ -68,6 +71,7 @@ `SELECT nostr_event_id, job_source, lease_owner, lease_expires_at FROM publish_jobs WHERE state = 'in_progress' ORDER BY lease_expires_at ASC NULLS FIRST;` - Check for users with failed backlog planning before widening traffic: `SELECT nostr_pubkey, did, publish_backfill_error FROM account_links WHERE publish_backfill_state = 'failed' ORDER BY updated_at DESC;` +- Confirm the Cloudflare Worker (`divine-name-server`) and Fastly service (`divine-router`) are deployed separately from ArgoCD-managed GKE workloads before a production cutover. - Confirm the canonical architecture boundary is still intact: - keycast owns consent/lifecycle - divine-handle-gateway syncs ready/failed/disabled transitions back into keycast diff --git a/docs/superpowers/plans/2026-03-28-login-divine-video-atproto-auth-server.md b/docs/superpowers/plans/2026-03-28-login-divine-video-atproto-auth-server.md index dfdf606..de2ad54 100644 --- a/docs/superpowers/plans/2026-03-28-login-divine-video-atproto-auth-server.md +++ b/docs/superpowers/plans/2026-03-28-login-divine-video-atproto-auth-server.md @@ -1,5 +1,9 @@ # login.divine.video ATProto Auth Server Implementation Plan +> **Superseded by:** [entryway.divine.video Phase 2 Normalization Plan](/Users/rabble/code/divine/divine-sky/docs/superpowers/plans/2026-04-03-entryway-divine-video-phase2-normalization.md) +> +> This plan reflects the earlier `login.divine.video` auth-server assumption. Execute the entryway-normalized plan instead: `login.divine.video` is the Keycast human console, and `entryway.divine.video` is the public ATProto Authorization Server. + > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Make `login.divine.video` an ATProto Authorization Server that external Bluesky-compatible clients can use for account authentication against DiVine-hosted ATProto accounts. diff --git a/docs/superpowers/plans/2026-04-03-entryway-divine-video-phase2-normalization.md b/docs/superpowers/plans/2026-04-03-entryway-divine-video-phase2-normalization.md new file mode 100644 index 0000000..1bfe939 --- /dev/null +++ b/docs/superpowers/plans/2026-04-03-entryway-divine-video-phase2-normalization.md @@ -0,0 +1,318 @@ +# entryway.divine.video Phase 2 Normalization Plan + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Normalize Phase 2 so users can sign in to Bluesky-compatible ATProto clients with `username.divine.video` through `pds.divine.video` plus `entryway.divine.video`, while `login.divine.video` stays the Keycast human console for DiVine + Nostr auth and lifecycle control. + +**Architecture:** Treat `entryway.divine.video` as the only public ATProto Authorization Server origin. Keep Keycast as the source of truth for account sessions, consent, and ATProto lifecycle state, but make the public protocol contract host-aware: `entryway` serves ATProto authorization-server behavior, `pds` serves protected-resource behavior, and `login` remains a human-facing control plane. The implementation work is mostly contract tightening and normalization across tests, config, smoke scripts, and docs rather than inventing a new OAuth flow from scratch. + +**Tech Stack:** Rust, Axum, `keycast`, `rsky-pds`, SQLx/Postgres, ATProto OAuth metadata, PAR, DPoP, markdown runbooks, shell smoke scripts. + +--- + +## Chunk 1: Normalize The Written Contract + +### Task 1: Replace or supersede the old `login.divine.video` auth-server story + +**Files:** +- Create: `docs/superpowers/specs/2026-04-03-entryway-divine-video-atproto-login-boundary-design.md` +- Modify: `docs/superpowers/specs/2026-03-28-login-divine-video-atproto-auth-server-design.md` +- Modify: `docs/superpowers/plans/2026-03-28-login-divine-video-atproto-auth-server.md` +- Reference: `docs/runbooks/login-divine-video.md` + +- [ ] **Step 1: Add the normalized design doc** + +Write a spec that states: +- `username.divine.video` is the public handle +- `pds.divine.video` is the protected resource +- `entryway.divine.video` is the ATProto Authorization Server +- `login.divine.video` is the Keycast human console + +- [ ] **Step 2: Mark the older `login`-as-auth-server spec as superseded** + +Add a short note near the top of `2026-03-28-login-divine-video-atproto-auth-server-design.md` pointing readers to the newer `entryway` boundary design. + +- [ ] **Step 3: Mark the older `login`-as-auth-server plan as superseded** + +Add a short note near the top of `2026-03-28-login-divine-video-atproto-auth-server.md` that Phase 2 should now be executed against the `entryway`-normalized contract. + +- [ ] **Step 4: Verify the docs cross-reference cleanly** + +Run: + +```bash +cd /Users/rabble/code/divine/divine-sky +rg -n "login\\.divine\\.video.*Authorization Server|entryway\\.divine\\.video" docs/superpowers/specs docs/superpowers/plans docs/runbooks +``` + +Expected: older `login` references remain only where intentionally marked historical or superseded. + +### Task 2: Reconcile runbooks and smoke docs around `entryway.divine.video` + +**Files:** +- Modify: `docs/runbooks/login-divine-video.md` +- Modify: `docs/runbooks/atproto-auth-server-smoke-test.md` +- Modify: `docs/runbooks/launch-checklist.md` +- Modify: `scripts/smoke-divine-atproto-login.sh` + +- [ ] **Step 1: Update the smoke test hostname expectations** + +Ensure the runbook and smoke script both expect: +- `https://pds.divine.video/.well-known/oauth-protected-resource` +- `authorization_servers = ["https://entryway.divine.video"]` +- `https://entryway.divine.video/.well-known/oauth-authorization-server` + +- [ ] **Step 2: Make the login runbook explicit about Keycast’s role** + +Add a short contract summary that says Keycast owns: +- user session +- consent +- lifecycle state +- enable / disable UX + +But not the public ATProto Authorization Server hostname. + +- [ ] **Step 3: Update the launch checklist** + +Include a deploy-time check that `entryway.divine.video` is the issuer in live auth-server metadata and that `login.divine.video` is not referenced as the ATProto Authorization Server in current customer-facing docs. + +- [ ] **Step 4: Validate the docs and script surface** + +Run: + +```bash +cd /Users/rabble/code/divine/divine-sky +bash scripts/smoke-divine-atproto-login.sh --help +ruby -e 'require "yaml"; YAML.load_file("docs/runbooks/launch-checklist.md") rescue nil' +``` + +Expected: the smoke script still parses and the runbook edits are syntactically clean. + +## Chunk 2: Tighten The Keycast Host Boundary + +### Task 3: Add failing tests that prove `entryway` is the auth-server host and `login` is not + +**Files:** +- Modify: `/Users/rabble/code/divine/keycast/api/tests/atproto_oauth_metadata_test.rs` +- Modify: `/Users/rabble/code/divine/keycast/api/tests/atproto_par_test.rs` +- Modify: `/Users/rabble/code/divine/keycast/api/tests/atproto_dpop_token_test.rs` +- Reference: `/Users/rabble/code/divine/keycast/api/src/api/http/atproto_oauth.rs` + +- [ ] **Step 1: Add a failing metadata test for the login host** + +Assert that requests with: + +```text +Host: login.divine.video +``` + +do not advertise `login.divine.video` as the issuer when the public contract is `entryway.divine.video`. + +- [ ] **Step 2: Add a failing metadata test for forwarded-host handling** + +Assert that when the incoming deployment terminates on the same service but forwards: + +```text +Host: login.divine.video +X-Forwarded-Host: entryway.divine.video +``` + +the metadata still resolves to `https://entryway.divine.video`. + +- [ ] **Step 3: Add a failing PAR / token test for wrong-host rejection** + +Assert that DPoP `htu` and issuer-sensitive request handling reject `login.divine.video` when the public ATProto origin should be `entryway.divine.video`. + +- [ ] **Step 4: Run the focused keycast tests to verify they fail** + +Run: + +```bash +cd /Users/rabble/code/divine/keycast +cargo test -p keycast_api --test atproto_oauth_metadata_test --test atproto_par_test --test atproto_dpop_token_test -- --nocapture +``` + +Expected: FAIL if any host-boundary assumptions still leak `login.divine.video`. + +### Task 4: Implement or tighten host-aware auth-server behavior in Keycast + +**Files:** +- Modify: `/Users/rabble/code/divine/keycast/api/src/api/http/atproto_oauth.rs` +- Modify: `/Users/rabble/code/divine/keycast/api/src/api/http/atproto_oauth_metadata.rs` +- Modify: `/Users/rabble/code/divine/keycast/api/openapi.yaml` +- Modify: `/Users/rabble/code/divine/keycast/docs/DEPLOYMENT.md` + +- [ ] **Step 1: Centralize the entryway-origin resolution** + +Make one helper authoritative for: +- issuer +- authorization endpoint +- token endpoint +- PAR endpoint +- host matching for forwarded traffic + +- [ ] **Step 2: Fail closed on the wrong public host** + +If a request arrives on `login.divine.video` for public ATProto auth-server metadata, either: +- return `404`, or +- redirect only if the existing ATProto client behavior is proven safe + +Prefer `404` unless a failing test proves the redirect is required and safe. + +- [ ] **Step 3: Update docs and API examples** + +OpenAPI and deployment docs should reference `entryway.divine.video` for ATProto auth-server examples, while leaving `login.divine.video` examples intact for DiVine and Nostr flows. + +- [ ] **Step 4: Re-run the focused keycast tests** + +Run: + +```bash +cd /Users/rabble/code/divine/keycast +cargo test -p keycast_api --test atproto_oauth_metadata_test --test atproto_par_test --test atproto_dpop_token_test -- --nocapture +cargo clippy -p keycast_api --all-targets -- -D warnings +``` + +Expected: PASS. + +## Chunk 3: Tighten The PDS Discovery Contract + +### Task 5: Add failing tests that `pds.divine.video` only advertises `entryway.divine.video` + +**Files:** +- Modify: `/Users/rabble/code/divine/rsky/rsky-pds/tests/integration_tests.rs` +- Reference: `/Users/rabble/code/divine/rsky/rsky-pds/src/well_known.rs` +- Reference: `/Users/rabble/code/divine/rsky/rsky-pds/src/config/mod.rs` + +- [ ] **Step 1: Add a failing protected-resource metadata assertion** + +Assert: + +```json +{ + "authorization_servers": ["https://entryway.divine.video"] +} +``` + +and reject responses that still name `login.divine.video`. + +- [ ] **Step 2: Add a failing env-driven config test** + +Assert that the protected-resource metadata uses the configured entryway URL rather than a hard-coded login URL. + +- [ ] **Step 3: Run the focused PDS integration test** + +Run: + +```bash +cd /Users/rabble/code/divine/rsky +cargo test -p rsky-pds --test integration_tests -- --nocapture +``` + +Expected: FAIL if discovery still leaks the wrong hostname. + +### Task 6: Implement or tighten PDS protected-resource metadata and config + +**Files:** +- Modify: `/Users/rabble/code/divine/rsky/rsky-pds/src/well_known.rs` +- Modify: `/Users/rabble/code/divine/rsky/rsky-pds/src/config/mod.rs` +- Modify: `/Users/rabble/code/divine/rsky/rsky-pds/src/auth_verifier.rs` + +- [ ] **Step 1: Centralize entryway-origin config** + +Use a single config path for: +- protected-resource metadata `authorization_servers` +- external token issuer validation +- any ATProto auth-server docs or comments + +- [ ] **Step 2: Remove stale `login` phrasing from the code comments** + +Update comments that still describe the flow as "`login.divine.video / keycast`" if the public ATProto contract is now `entryway.divine.video`. + +- [ ] **Step 3: Re-run the focused PDS tests** + +Run: + +```bash +cd /Users/rabble/code/divine/rsky +cargo test -p rsky-pds --test integration_tests -- --nocapture +``` + +Expected: PASS. + +## Chunk 4: End-To-End Interop Verification + +### Task 7: Add a production-style interop canary for the public chain + +**Files:** +- Modify: `scripts/smoke-divine-atproto-login.sh` +- Modify: `docs/runbooks/divine-atproto-login-canary.md` +- Modify: `docs/runbooks/atproto-auth-server-smoke-test.md` + +- [ ] **Step 1: Add a public-client canary path** + +Cover: +- handle typed as `username.divine.video` +- PDS protected-resource discovery +- auth-server discovery on `entryway.divine.video` +- PAR +- browser authorization +- token exchange +- authenticated PDS request + +- [ ] **Step 2: Add explicit host assertions** + +The canary should fail if: +- `login.divine.video` is discovered as the ATProto Authorization Server +- `entryway.divine.video` serves HTML instead of JSON metadata +- `pds.divine.video` advertises anything other than `entryway.divine.video` + +- [ ] **Step 3: Run the canary in dry-run or documented local mode** + +Run: + +```bash +cd /Users/rabble/code/divine/divine-sky +bash scripts/smoke-divine-atproto-login.sh +``` + +Expected: PASS in an environment with live entryway and PDS configuration. + +### Task 8: Final verification and rollout signoff + +**Files:** +- No code changes expected + +- [ ] **Step 1: Re-run all focused contract checks** + +Run: + +```bash +cd /Users/rabble/code/divine/keycast +cargo test -p keycast_api --test atproto_oauth_metadata_test --test atproto_par_test --test atproto_dpop_token_test -- --nocapture + +cd /Users/rabble/code/divine/rsky +cargo test -p rsky-pds --test integration_tests -- --nocapture + +cd /Users/rabble/code/divine/divine-sky +bash scripts/smoke-divine-atproto-login.sh +``` + +- [ ] **Step 2: Verify the human/protocol split manually** + +Confirm: +- `login.divine.video` is still the DiVine + Nostr Keycast UI +- `entryway.divine.video` is the ATProto Authorization Server +- `pds.divine.video` is the protected resource +- `username.divine.video` is the handle users enter in clients + +- [ ] **Step 3: Commit the normalization work** + +```bash +git -C /Users/rabble/code/divine/divine-sky add docs/superpowers/specs/2026-04-03-entryway-divine-video-atproto-login-boundary-design.md docs/superpowers/specs/2026-03-28-login-divine-video-atproto-auth-server-design.md docs/superpowers/plans/2026-04-03-entryway-divine-video-phase2-normalization.md docs/superpowers/plans/2026-03-28-login-divine-video-atproto-auth-server.md docs/runbooks/login-divine-video.md docs/runbooks/atproto-auth-server-smoke-test.md docs/runbooks/launch-checklist.md docs/runbooks/divine-atproto-login-canary.md scripts/smoke-divine-atproto-login.sh +git -C /Users/rabble/code/divine/keycast add api/src/api/http/atproto_oauth.rs api/src/api/http/atproto_oauth_metadata.rs api/openapi.yaml docs/DEPLOYMENT.md api/tests/atproto_oauth_metadata_test.rs api/tests/atproto_par_test.rs api/tests/atproto_dpop_token_test.rs +git -C /Users/rabble/code/divine/rsky add rsky-pds/src/well_known.rs rsky-pds/src/config/mod.rs rsky-pds/src/auth_verifier.rs rsky-pds/tests/integration_tests.rs +git -C /Users/rabble/code/divine/divine-sky commit -m "docs: normalize phase 2 around entryway auth" +git -C /Users/rabble/code/divine/keycast commit -m "fix: make entryway the public atproto auth host" +git -C /Users/rabble/code/divine/rsky commit -m "fix: advertise entryway as pds auth server" +``` diff --git a/docs/superpowers/specs/2026-03-28-login-divine-video-atproto-auth-server-design.md b/docs/superpowers/specs/2026-03-28-login-divine-video-atproto-auth-server-design.md index 76c585d..b16f17e 100644 --- a/docs/superpowers/specs/2026-03-28-login-divine-video-atproto-auth-server-design.md +++ b/docs/superpowers/specs/2026-03-28-login-divine-video-atproto-auth-server-design.md @@ -1,5 +1,9 @@ # login.divine.video ATProto Auth Server Design +> **Superseded by:** [entryway.divine.video ATProto Login Boundary Design](/Users/rabble/code/divine/divine-sky/docs/superpowers/specs/2026-04-03-entryway-divine-video-atproto-login-boundary-design.md) +> +> This document reflects the earlier assumption that `login.divine.video` would be the public ATProto Authorization Server. The current boundary is `login.divine.video` for the Keycast human console and `entryway.divine.video` for the public ATProto Authorization Server. + ## Goal Make `login.divine.video` a valid ATProto Authorization Server so other Bluesky-compatible clients can use it for account authentication. diff --git a/docs/superpowers/specs/2026-04-03-entryway-divine-video-atproto-login-boundary-design.md b/docs/superpowers/specs/2026-04-03-entryway-divine-video-atproto-login-boundary-design.md new file mode 100644 index 0000000..a110a1b --- /dev/null +++ b/docs/superpowers/specs/2026-04-03-entryway-divine-video-atproto-login-boundary-design.md @@ -0,0 +1,126 @@ +# entryway.divine.video ATProto Login Boundary Design + +## Goal + +Let users sign in to third-party Bluesky-compatible clients with a `username.divine.video` account without turning `login.divine.video` into the public ATProto protocol origin. + +The public protocol chain should be: + +- `username.divine.video` as the handle users type into clients +- `pds.divine.video` as the protected resource / PDS host +- `entryway.divine.video` as the ATProto Authorization Server +- `login.divine.video` as the human-facing Keycast console for DiVine + Nostr auth, consent, recovery, and lifecycle state + +## Why This Supersedes Older Phase 2 Docs + +The older Phase 2 auth-server spec assumed `login.divine.video` itself should become the ATProto Authorization Server. + +That is no longer the cleanest boundary, and the repository already reflects that: + +- [login-divine-video.md](/Users/rabble/code/divine/divine-sky/docs/runbooks/login-divine-video.md) says `login.divine.video` is not the ATProto authorization server +- [divine-multi-user-atproto-login-design.md](/Users/rabble/code/divine/divine-sky/docs/superpowers/specs/2026-03-29-divine-multi-user-atproto-login-design.md) recommends `entryway.divine.video` +- [smoke-divine-atproto-login.sh](/Users/rabble/code/divine/divine-sky/scripts/smoke-divine-atproto-login.sh) already verifies `entryway.divine.video` +- `keycast` ATProto OAuth code already has an `ATPROTO_ENTRYWAY_ORIGIN` concept and defaults to `https://entryway.divine.video` + +So the remaining work is not "should we use `entryway`?" It is "normalize the contract everywhere so the code, tests, docs, and rollout all describe the same boundary." + +## Recommendation + +Use `entryway.divine.video` as the public ATProto Authorization Server. + +Keep `login.divine.video` as Keycast's human-facing surface. + +This is the right split because: + +- it keeps DiVine + Nostr product UX on the Keycast host users already know +- it preserves a clean ATProto protocol hostname for discovery and OAuth +- it matches ATProto discovery rules better than mixing human-console and protocol roles on the same public host +- it lets `entryway` be implemented either as a dedicated service or as host-aware routing on top of Keycast, without changing the public contract + +## Host Responsibilities + +### `username.divine.video` + +- Public handle host +- Used for NIP-05 / ATProto handle resolution +- Must resolve to the user's DID only when the account is active and `ready` + +### `login.divine.video` + +- Keycast human console +- DiVine + Nostr login UX +- Username claim and recovery UX +- ATProto enable / disable / status lifecycle UX +- Consent and lifecycle source of truth + +It must not be treated as the public ATProto Authorization Server in external client docs or smoke tests. + +### `entryway.divine.video` + +- Public ATProto Authorization Server +- Serves `/.well-known/oauth-authorization-server` +- Owns PAR, authorization, token, refresh, and DPoP-facing OAuth behavior +- Can reuse Keycast sessions and lifecycle state behind the scenes + +Whether `entryway` is a separate service or host-aware behavior on the same Keycast deployment is an implementation detail. Publicly, it is a distinct ATProto protocol origin. + +### `pds.divine.video` + +- Public PDS and protected resource +- Serves `/.well-known/oauth-protected-resource` +- Accepts ATProto access tokens minted by `entryway.divine.video` + +## End-To-End Login Flow + +1. A user claims `username.divine.video` in Keycast. +2. The user enables ATProto from `login.divine.video`. +3. `divine-sky` provisions the account until lifecycle is `ready`. +4. A Bluesky-compatible client receives `username.divine.video` from the user. +5. Handle resolution leads the client to the user's DID and `pds.divine.video`. +6. The client fetches `https://pds.divine.video/.well-known/oauth-protected-resource`. +7. That metadata advertises `https://entryway.divine.video` as the authorization server. +8. The client fetches `https://entryway.divine.video/.well-known/oauth-authorization-server`. +9. The client runs PAR plus browser auth plus token exchange against `entryway.divine.video`. +10. `entryway` consults Keycast-owned lifecycle state and only authorizes users whose ATProto account is `ready`. +11. The returned access token works against `pds.divine.video`. +12. Disabling from `login.divine.video` blocks new approvals and refresh immediately; existing short-lived access tokens expire naturally. + +## Product Rules + +- No external ATProto OAuth login unless lifecycle is `ready`. +- `login.divine.video` remains the place users manage consent and account state. +- External ATProto clients should never need to know about DiVine's internal service boundaries beyond `username.divine.video`, `pds.divine.video`, and `entryway.divine.video`. +- The first-party DiVine/Nostr product may continue to use `login.divine.video` directly for its own auth flows. + +## Implementation Consequences + +### Keycast + +- Keep lifecycle, consent, and user session ownership in Keycast. +- Make host-aware ATProto auth-server behavior explicit: `entryway` is allowed to serve auth-server metadata and ATProto OAuth endpoints; `login` is not the public ATProto contract. +- Tighten tests so `entryway` hostnames are the expected issuer and endpoint origins. + +### `rsky-pds` + +- Protected-resource metadata must advertise `https://entryway.divine.video`. +- Token trust config must continue to point at the entryway issuer and public key. + +### Docs and Runbooks + +- Older docs that say "`login.divine.video` is the Authorization Server" should be marked superseded or updated. +- Smoke tests and launch docs should treat `entryway.divine.video` as the protocol hostname and `login.divine.video` as the human console. + +## Non-Goals + +- Replacing `login.divine.video` for DiVine or Nostr UX +- Making `login.divine.video` a PDS +- Replacing `pds.divine.video` as the protected resource +- Collapsing all hostnames into a single public origin + +## Acceptance Criteria + +- A user can enter `username.divine.video` into a Bluesky-compatible client and be taken through a discovery chain that leads to `pds.divine.video` plus `entryway.divine.video`. +- `entryway.divine.video` is the only ATProto Authorization Server origin advertised in protected-resource metadata. +- `login.divine.video` remains the human-facing Keycast console and no longer appears as the public ATProto Authorization Server in current Phase 2 docs. +- Smoke tests, runbooks, config examples, and protocol tests all describe the same host boundary. +- The implementation still enforces `ready` lifecycle gating and refresh-cutoff-on-disable semantics. diff --git a/scripts/smoke-divine-atproto-login.sh b/scripts/smoke-divine-atproto-login.sh index 78eabba..ed38961 100755 --- a/scripts/smoke-divine-atproto-login.sh +++ b/scripts/smoke-divine-atproto-login.sh @@ -1,6 +1,17 @@ #!/usr/bin/env bash set -euo pipefail +show_help() { + cat <<'EOF' +Usage: smoke-divine-atproto-login.sh [--help] + +Validates the public Divine ATProto login contract for the configured handle. + +Environment: + HANDLE Handle to probe. Defaults to rabble.divine.video. +EOF +} + fail() { printf 'FAIL: %s\n' "$1" >&2 exit 1 @@ -10,6 +21,18 @@ require_cmd() { command -v "$1" >/dev/null 2>&1 || fail "missing required command: $1" } +case "${1:-}" in + -h|--help) + show_help + exit 0 + ;; + "") + ;; + *) + fail "unknown argument: $1" + ;; +esac + require_cmd curl require_cmd python3