Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 19 additions & 22 deletions docs/runbooks/atproto-auth-server-smoke-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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 = [
Expand All @@ -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' \
Expand Down Expand Up @@ -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=<urlencoded request_uri>
https://entryway.divine.video/api/oauth/authorize?request_uri=<urlencoded request_uri>
```

Expect:
Expand All @@ -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.

Expand All @@ -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")
}
Expand All @@ -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' \
Expand Down Expand Up @@ -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")
}
Expand All @@ -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' \
Expand Down
4 changes: 4 additions & 0 deletions docs/runbooks/atproto-opt-in-smoke-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.

Expand Down Expand Up @@ -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.
4 changes: 4 additions & 0 deletions docs/runbooks/launch-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Loading
Loading