From 74caeb9948df898083122e9f5abeaf8c72a6f85a Mon Sep 17 00:00:00 2001 From: Jacob Zweifel Date: Fri, 14 Aug 2026 16:07:08 -0400 Subject: [PATCH] Record the lexicon publication: the lexicons are live on-network MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The runbook is fully worked through: the authority account (did:plc:o3zuar7kk2mrz7d4sqxdisy2, handle mooring.page) holds all three com.atproto.lexicon.schema records, verified byte-identical to the documents in this tree; _lexicon.mooring.page and _atproto.mooring.page resolve; OAUTH_SCOPE is set to the granular scope in wrangler.jsonc and deployed. Post-flip sign-in passed on both bsky.social and Custos/ezpds, closing the stale granular-scope research item — Custos advertises include:* and blob:*/* and resolved the permission set. Co-Authored-By: Claude Fable 5 --- apps/web/wrangler.jsonc | 12 ++++----- docs/NEXT.md | 9 +++---- docs/runbooks/lexicon-publication.md | 38 +++++++++++++++++++--------- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/apps/web/wrangler.jsonc b/apps/web/wrangler.jsonc index b7cb7e0..16bdc66 100644 --- a/apps/web/wrangler.jsonc +++ b/apps/web/wrangler.jsonc @@ -19,12 +19,12 @@ // CLOUDFLARE_ZONE_ID — the mooring.page zone id (dash → Overview), // set here as a var. // Both unset = provisioning skipped; domains still verify and serve. - "CLOUDFLARE_ZONE_ID": "59558dba16a1fbf920d86f627d1d5822" - // OAUTH_SCOPE — unset requests 'atproto transition:generic'. Uncomment - // once the page.mooring.* lexicons are published on-network and - // _lexicon.mooring.page points at the authority DID; revert to roll - // back if a PDS can't resolve the permission set. - // "OAUTH_SCOPE": "atproto include:page.mooring.authSite blob:image/*" + "CLOUDFLARE_ZONE_ID": "59558dba16a1fbf920d86f627d1d5822", + // OAUTH_SCOPE — unset falls back to 'atproto transition:generic'. + // The granular scope needs the published page.mooring.* lexicons and + // the _lexicon.mooring.page TXT record to resolve; comment this out + // to roll back if a PDS can't resolve the permission set. + "OAUTH_SCOPE": "atproto include:page.mooring.authSite blob:image/*" }, // The apex and www are Workers custom domains (exact hostnames, DNS and // certs managed by Cloudflare). Everything else binds through this route: diff --git a/docs/NEXT.md b/docs/NEXT.md index 7277b81..a9ec4db 100644 --- a/docs/NEXT.md +++ b/docs/NEXT.md @@ -2,7 +2,7 @@ The flight plan. Each item carries enough context to start cold; update this file whenever an item lands (move it to "Done") or a new one is queued. Decisions made while working an item still go through `decisions/` as usual. -_Last updated: 2026-08-14 (**lexicon publication is ready on the code side and ADR 0014 is ratified**: the publication + live-record-check tools are in `packages/lexicons`, the `OAUTH_SCOPE` flip is gated behind a wrangler var, page paths are now lowercase/case-insensitive, and `docs/runbooks/lexicon-publication.md` holds Jacob's checklist — account, publish, DNS, flip)._ +_Last updated: 2026-08-14 (**the lexicons are published on-network and the granular OAuth scope is live**: the `mooring.page` authority account exists, `_lexicon.mooring.page` resolves, all three `com.atproto.lexicon.schema` records are up and byte-identical to the tree, and post-flip sign-in passed on both bsky.social and Custos/ezpds. What's left of the session's work is merging PR #16)._ ## Where things stand @@ -14,21 +14,20 @@ Feasibility is done and the verdict was **build it** (see `FEASIBILITY.md`). All Done so far: OAuth login (loopback dev client; hosted-client path ready pending a real key + deploy) with D1-backed state/session stores; lexicon convention tests; **read-only adapters** for Bluesky (profile + posts, replies filtered), standard.site (documents/publications, `pub.leaflet.document` fallback only when no standard.site docs exist), and sifa (profile/positions/education/skills, defensively parsed) — fetch-injected modules in `apps/web/src/lib/server/atmosphere/` with unit tests, plus `detectSources` (drives the ADR 0012 default section order) and a source-overview admin page; **site/page authoring** — record builders, PDS writes through the OAuth session, and the `/admin` + `/admin/pages` CRUD routes; **the professional-presence theme** — the render pipeline in `apps/web/src/lib/server/render/` and the public routes at `/s/[handle]`. Remaining, roughly in dependency order: -- **Publish the lexicons — code side done, Jacob's checklist remains.** Work through `docs/runbooks/lexicon-publication.md`: ADR 0014 is ratified (authority = a dedicated `mooring.page` service account, did:plc on bsky.social), so what's left is: create the account, `npm run publish-lexicons` (in `packages/lexicons`, app-password env auth), `_lexicon.mooring.page` DNS TXT, then uncomment `OAUTH_SCOPE` in `apps/web/wrangler.jsonc` and redeploy. Re-test sign-in on **both** bsky.social and Custos/ezpds after the flip — that's the re-verification of the stale granular-scope research; a Custos rejection is a finding to record, not a reason to silently stay on `transition:generic`. The one live record (`/about`) validates against the drafts (`npm run check-live-records -- malpercio.dev`); evolution policy is add-optional-only from the first `putRecord` (ADR 0012). -- A **bsky.social sign-in** as the second OAuth confirmation — the self-hosted case already passed, which is the harder half. -- **Merge the hosting PR** (#15, draft) — its contents are what is deployed and proven; `main` should say so. +- **Merge PR #16** (lexicon publication: ADR 0014, the tooling, the scope gate, path lowercasing) — its contents are what is published and deployed; `main` should say so. - **Thin deletion-honoring cache** over the direct PDS reads (ADR 0010 §4) — deliberately reordered below hosting on 2026-08-07: rendering reads live per request, which is correct until there is traffic to cache. Revisit once sites are actually served; when it comes up, start from Jetstream v2's snapshot+tail replay (`research/2026-08-14-bluesky-protocol-services.md`) rather than a relay consumer. - Later, when token-refresh races become real: a Durable-Object `requestLock` for the OAuth client. ## Standing / background -- **Re-verify stale research before leaning on it** — every file in `research/` is dated 2026-07-30/31; the Atmosphere moves fast. Spot-check anything older than a few months, especially: sifa lexicons (alpha, may have churned), teal.fm namespace (breaking rename expected), granular-OAuth rollout on self-hosted PDSes. +- **Re-verify stale research before leaning on it** — every file in `research/` is dated 2026-07-30/31; the Atmosphere moves fast. Spot-check anything older than a few months, especially: sifa lexicons (alpha, may have churned), teal.fm namespace (breaking rename expected). (The granular-OAuth-on-self-hosted-PDSes concern closed 2026-08-14: Custos/ezpds accepted the granular scope, permission-set resolution included.) - **Pin + changelog discipline on `@atproto/*`** — the OAuth packages are pre-1.0 and rename things (breaking change in the very week of ADR 0010). Watch for: `oauth-client-node` 1.0, Tap's "typed indexer" from the Spring 2026 roadmap (both are named revisit triggers in ADR 0010), and now `@atproto/lex` — the lexicon toolchain the Bluesky SDK rebased onto 2026-08-13; migrate our validation harness off `@atproto/lexicon` when lex hits 1.0 or lexicon deprecates (`research/2026-08-14-bluesky-protocol-services.md`). - **Watch the competition**: Leaflet (one feature-cycle from "your website"), Blento (same idea, one-page scale), Bluesky+ (if it ships bundling domain/profile-site features, revisit PD-4 pricing). A periodic landscape refresh earns a new dated research file. - **Optional runway**: Skyseed grants ($5–25K) are compatible with the indie model (PD-3) if wanted. ## Done +- 2026-08-14 — **The lexicons are published on-network; the granular OAuth scope is live.** Jacob worked `docs/runbooks/lexicon-publication.md` end to end: the authority account exists (**`did:plc:o3zuar7kk2mrz7d4sqxdisy2`**, handle `mooring.page`, hosted on bsky.social per ADR 0014), all three `page.mooring.*` documents are up as `com.atproto.lexicon.schema` records (verified byte-identical to the tree), `_lexicon.mooring.page` and `_atproto.mooring.page` TXT records resolve, and `OAUTH_SCOPE` is flipped to `atproto include:page.mooring.authSite blob:image/*` in the deployed app. **Post-flip sign-in passed on both bsky.social and Custos/ezpds** — closing the stale granular-scope research item (Custos advertises `include:*`/`blob:*/*` and resolved the permission set). This also supplied the queued "bsky.social sign-in as second OAuth confirmation". ADR 0012's add-optional-only policy is now enforced by resolving PDSes, not just by us; republication after a schema change = runbook §§3–4. - 2026-08-14 — **Page paths are lowercase; comparisons are case-insensitive** (Jacob's call on PR #16, closing the standing `normalizePath` question while exactly one — already-lowercase — record existed in the wild). `normalizePath` lowercases, so admin writes store lowercase and page lookups (which normalize the request path) match; `findPathConflict` and the renderer's path match also lowercase the stored side, so records written by other clients in mixed case still conflict and still serve. The `path` field's lexicon description now states the convention. Same session: **ADR 0014 Accepted** — Jacob ratified the lexicon-authority decision on the PR. - 2026-08-12 — **Lexicon publication readied, code side.** ADR 0014 (Proposed) picks the lexicon authority: a dedicated `mooring.page` service account, did:plc on bsky.social, app-password auth for tooling — not Jacob's personal `did:web:malpercio.dev`, whose resolution hangs off a personal domain. `packages/lexicons` gained `npm run publish-lexicons` (each `lexicons/` doc → a `com.atproto.lexicon.schema` record, rkey = NSID, `--dry-run` supported, idempotent via putRecord) and `npm run check-live-records -- ` (validates a live repo's `page.mooring.*` records against the drafts; the live `/about` record passes, and its shape is pinned as a test fixture so draft edits can't strand it). `OAUTH_SCOPE` became a wrangler var with the granular value staged in a comment — the flip and any rollback are config deploys. `docs/runbooks/lexicon-publication.md` sequences Jacob's side: sanity-check post (drafted), account, publish, `_lexicon.mooring.page` TXT, flip, dual-PDS sign-in re-test. 11 new unit tests (lexicons 26, web 153 total). - 2026-08-12 — **Custom domains proven end to end — the ADR 0008 v1 scope is complete.** Cloudflare for SaaS enabled with `origin.mooring.page` (originless proxied `AAAA 100::`, slug reserved in code) as fallback origin; the Worker route widened to `*/*` because custom-hostname traffic keeps the customer's hostname; `hosting/cloudflare.ts` provisions/deprovisions custom hostnames when a domain verifies in `/admin/hosting` (idempotent create, live TLS status on the page, dormant without `CLOUDFLARE_ZONE_ID` + `CLOUDFLARE_API_TOKEN`). Acceptance: **mooring.malpercio.dev** serves the site and its authored page over provisioned TLS, with app-only paths 404ing on the tenant origin. `docs/runbooks/first-deploy.md` is fully worked through. diff --git a/docs/runbooks/lexicon-publication.md b/docs/runbooks/lexicon-publication.md index 1998a6a..5d2e4a1 100644 --- a/docs/runbooks/lexicon-publication.md +++ b/docs/runbooks/lexicon-publication.md @@ -6,6 +6,12 @@ post — which is why it's a checklist and not code. The code side (the publication and live-record-check scripts, the `OAUTH_SCOPE` gate) is already in the tree. +**Worked through 2026-08-14.** The authority account is +`did:plc:o3zuar7kk2mrz7d4sqxdisy2` (handle `mooring.page`, hosted at +`fibercap.us-west.host.bsky.network`). Sections 3–4 are also the +republication procedure after any future add-optional schema change +(ADR 0012). + Sequence matters: the scope flip (§5) must come last, after resolution works end to end, or sign-in breaks on PDSes that enforce scopes they can't resolve. @@ -20,50 +26,58 @@ end to end, or sign-in breaks on PDSes that enforce scopes they can't resolve. ## 2. The authority account (ADR 0014) -- [ ] Create the account on bsky.social with handle **mooring.page**. +- [x] Create the account on bsky.social with handle **mooring.page**. Verifying that handle needs one of: - `_atproto.mooring.page TXT "did="`, or - the app serving `/.well-known/atproto-did` (not implemented; DNS is less code). -- [ ] Store the credentials in the password manager. -- [ ] Create an **app password** (Settings → Privacy and security → App +- [x] Store the credentials in the password manager. +- [x] Create an **app password** (Settings → Privacy and security → App passwords) for the publication script. It can be revoked after each run and re-minted for the next; the main password never touches a shell. -- [ ] Note the account's `did:plc:…` — the next two steps need it. +- [x] Note the account's `did:plc:…` — the next two steps need it. + (`did:plc:o3zuar7kk2mrz7d4sqxdisy2`.) ## 3. Publish -- [ ] From `packages/lexicons/`, sanity-check the records first: +- [x] From `packages/lexicons/`, sanity-check the records first: `npm run publish-lexicons -- --dry-run` -- [ ] Publish (env vars keep the app password out of the repo and out of +- [x] Publish (env vars keep the app password out of the repo and out of argv): ```bash cd packages/lexicons && LEXICON_AUTHORITY_IDENTIFIER=mooring.page LEXICON_AUTHORITY_APP_PASSWORD= npm run publish-lexicons ``` -- [ ] Re-running the same command republishes (putRecord overwrites) — this +- [x] Re-running the same command republishes (putRecord overwrites) — this is also the procedure after any future add-optional schema change. + Published 2026-08-14; all three records verified byte-identical to + the drafts in this tree. ## 4. DNS -- [ ] `_lexicon.mooring.page TXT "did="` on the +- [x] `_lexicon.mooring.page TXT "did="` on the mooring.page zone (the script prints the exact record on success). -- [ ] Verify resolution: `dig +short TXT _lexicon.mooring.page`, and fetch a +- [x] Verify resolution: `dig +short TXT _lexicon.mooring.page`, and fetch a record through the chain, e.g. `https://plc.directory/` → PDS → `com.atproto.repo.getRecord?repo=&collection=com.atproto.lexicon.schema&rkey=page.mooring.site`. + Verified 2026-08-14, end to end. ## 5. The scope flip -- [ ] Uncomment `OAUTH_SCOPE` in `apps/web/wrangler.jsonc` (the granular +- [x] Uncomment `OAUTH_SCOPE` in `apps/web/wrangler.jsonc` (the granular value is already there) and deploy from `apps/web/`: `npm run deploy`. -- [ ] **Re-test sign-in on both PDSes.** This is the re-verification of the +- [x] **Re-test sign-in on both PDSes.** This is the re-verification of the stale (2026-07-30) granular-scope research: - a bsky.social account — expected to work; - the in-house Custos/ezpds account — if it rejects the granular scope, that's a finding to record in `docs/research/` (and the var gets commented back out until Custos catches up), not a reason to silently stay on `transition:generic`. -- [ ] Existing sessions were granted under `transition:generic` and keep + + **Both passed, 2026-08-14.** Custos/ezpds advertises `include:*` and + `blob:*/*` in `scopes_supported` and completed a granular sign-in, + permission-set resolution included — the stale-research item is closed. +- [x] Existing sessions were granted under `transition:generic` and keep working; the granular grant applies on next sign-in. -- 2.51.2