diff --git a/apps/web/.dev.vars.example b/apps/web/.dev.vars.example
--- a/apps/web/.dev.vars.example
+++ b/apps/web/.dev.vars.example
@@ -9,3 +9,7 @@ # Set APP_HOST to serve claimed subdomains locally: with the value below,
# http://
Releasing frees the name for anyone to claim; links to it stop working.
- {:else} ++ + Releasing frees the name for anyone to claim; links to it stop working.{#if !data.mayClaimSubdomain} + While new claims are paused, you will not be able to claim another.{/if} + +
+ {:else if data.mayClaimSubdomain}Claim a subdomain to put your site on the web. One per account; first come, first served.
+ {:else} +New subdomains are paused.
{/if}None yet. A custom domain serves your site at an address you own.
+ {/if} + {#if data.mayAddDomain} + ++ + Ownership is proven with a TXT record tied to your atproto identity, not to Mooring — the + domain stays yours. + +
{:else} -None yet. A custom domain serves your site at an address you own.
++ New custom domains are paused.{#if data.domains.length > 0} + Domains already on your account keep working.{/if} +
{/if} - -- - Ownership is proven with a TXT record tied to your atproto identity, not to Mooring — the - domain stays yours. - -
diff --git a/apps/web/wrangler.jsonc b/apps/web/wrangler.jsonc --- a/apps/web/wrangler.jsonc +++ b/apps/web/wrangler.jsonc @@ -20,6 +20,11 @@ // 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", + // Who may claim a subdomain: whitespace/comma-separated DIDs, `*` for + // every account, unset for none. Open here because a subdomain costs + // nothing per account; an instance that wants them invite-only replaces + // this with a DID list. + "SUBDOMAIN_ALLOWLIST": "*", // 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 @@ -60,4 +65,8 @@ // SESSION_SECRET — HMAC key for the session cookie. // CLOUDFLARE_API_TOKEN — scoped token (Zone → SSL and Certificates → // Edit, for the mooring.page zone) for // custom-hostname provisioning. + // CUSTOM_DOMAIN_ALLOWLIST — DIDs that may add a custom domain; + // whitespace/comma separated, `*` for every + // account, unset for none. A secret rather than a + // var because the list names individual people. } diff --git a/docs/NEXT.md b/docs/NEXT.md --- a/docs/NEXT.md +++ b/docs/NEXT.md @@ -2,13 +2,17 @@ # NEXT — the queue for future sessions 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-25 (sign-off band + Atmosphere actions landed from the theme-UX tail; the `signOff` lexicon republication is queued for Jacob. Critique trend for the push: 25 → 31 → 29 → 32, snapshots in `.impeccable/critique/`)._ +_Last updated: 2026-08-25 (sign-off band + Atmosphere actions landed from the theme-UX tail; the `signOff` lexicon republication is queued for Jacob. PR #27 merged: custom domains are invite-only pending pricing — **PD-9, and it needs `CUSTOM_DOMAIN_ALLOWLIST` set on the next deploy or nobody, Jacob included, can add one** (queue item 0 below); subdomains stay open as PD-4's free tier. Critique trend for the push: 25 → 31 → 29 → 32, snapshots in `.impeccable/critique/`)._ ## Where things stand Feasibility is done and the verdict was **build it** (see `FEASIBILITY.md`). All founding decisions are recorded: PD-1..8 in `decisions/product-decisions.md`, ADRs 0001–0014 in `decisions/adr/` — all Accepted, including ADR 0008 (v1 scope), ADR 0010 (tech stack: TypeScript + SvelteKit, first-party `@atproto/*`, direct PDS reads for v1, BEAM benched for v2 services), ADR 0011 (hosting: Cloudflare Workers + Cloudflare for SaaS; DB: SQLite-family via D1 hosted / file self-host), ADR 0012 (`page.mooring.*` record schemas — draft lexicons in `lexicons/page/mooring/`), and ADR 0013 (the hosting registry — subdomain slugs and custom domains — lives in D1). The product is **Mooring** at **mooring.page** (domain secured); the repo keeps the codename Fahrenheit. **The v1 build is underway** (Jacob green-lit 2026-08-03): npm workspaces with `apps/web` (SvelteKit + adapter-cloudflare, atproto OAuth sign-in with the Workers compat layer, D1-backed stores) and `packages/lexicons` (convention tests), plus CI. ## Queued, roughly in order + +### 0. Operator step, before or with the next deploy + +`wrangler secret put CUSTOM_DOMAIN_ALLOWLIST` — a whitespace- or comma-separated DID list, starting with Jacob's `did:web:malpercio.dev` and `did:plc:o3zuar7kk2mrz7d4sqxdisy2` plus whoever is invited. Unset means **nobody** can add a custom domain, so a deploy without it locks the operator out too (domains already added keep serving either way). `*` opens it to every account — the value that lifts the lock when PD-4's paid tier ships. Subdomains need no secret: `SUBDOMAIN_ALLOWLIST` is a var in `wrangler.jsonc`, set to `*`, and deploys with the code. ### 1. Continue v1 (per ADR 0008 — the scope is ratified; don't re-scope) @@ -33,7 +37,11 @@ - **Optional runway**: Skyseed grants ($5–25K) are compatible with the indie model (PD-3) if wanted. ## Done +<<<<<<< HEAD - 2026-08-25 — **The airmail sign-off band landed — the letter signs off, with Atmosphere-native actions** (tail items "sign-off block" and "Atmosphere-native actions", resolved together). Three directions were mocked on the design canvas (letter closing / postmark band / "From" block); Jacob picked the **postmark band**: a sunk-paper band above the footer where the airmail stripe returns to bookend the page, a rotated circular postmark (handle + render-month, airmail-blue ink), the owner's name, and three actions — Follow (Bluesky profile link, where Follow/Message actually live; Bluesky has no follow/DM intent URL), Say hello (`bsky.app/intent/compose` prefilled with the @mention), Write to me (mailto, only when `contactEmail` is set). The valediction is **configurable, not default**: new optional `signOff` field on `page.mooring.site` (add-optional-only per ADR 0012; republication queued above) flowing lexicon → record builder → settings form → `SiteView` → `SiteLayout`; empty renders the name alone — Jacob was unsure "Warmly," suits every site, so it's a choice ("whimsy seasons the dish"). Band renders on every page like the footer; verified locally against live PDS data (jzweifel.dev): actions resolve, dark tokens flip, no horizontal overflow at 375px, chips ≥24px targets. +======= +- 2026-08-25 — **Costly hosting actions gated behind DID allowlists** (PD-9, PR #27). `hosting/access.ts` holds `allows(list, did)`; the two actions that take a *new* host — `claim` and `addDomain` in `/admin/hosting` — check it and 403, and the page hides their forms. On mooring.page only custom domains are actually locked: subdomains are PD-4's free tier and cost nothing per account, so `SUBDOMAIN_ALLOWLIST` ships as a `*` var while `CUSTOM_DOMAIN_ALLOWLIST` is a secret naming individuals. The subdomain gate stays in the code because a self-hosted instance may want it, and flipping it is a config change. Nothing else needed gating: `release`, `verifyDomain` and `removeDomain` already 404 on rows the account does not hold, so accounts that already have a subdomain or a domain keep them, keep serving, and can still retry TLS. Serving is untouched — `hooks.server.ts` never consults the lists. 8 new unit tests (194 in `web`). Caveat by design: a grandfathered account that *releases* its subdomain cannot reclaim one while the lock is on, and the release copy says so. +>>>>>>> origin/main - 2026-08-25 — **Critique rounds 3–4 and their fix PRs (#24, #25) merged and deployed.** Round 3 (29/40) discovered pre-existing issues all earlier reviews missed; PR #24 fixed the mechanical ones: per-site favicons (the stock Svelte logo was the P0 — sites use the record `icon` blob → avatar fallback via `/blob`, app pages get an airmail-striped icon), sifa positions/education sorted newest-first (record order was never meaningful), `yearOf`/`yearSpan` tolerant of formless date strings, the career "More" disclosure made reversible with focus retained, visible contact address in the footer, banner `srcset` + 768 blob width, absolute `og:image` via `/blob`, `color-scheme`/`theme-color`. Round 4 scored **32/40** ("an authored design with a real point of view") and caught PR #24's own regression; PR #25 fixed it: footer wraps at phone widths (the visible email had overflowed the nowrap row), the career summary leads with its full first paragraph so the quantified proof ("~1M → 5M+") is visible without clicking, Skills sublabel, "+21 more"→"Show fewer" swap, ≥24px disclosure targets, banner double-download eliminated. Working pattern that held all push: dual-assessment critique → decisions with Jacob (AskUserQuestion) → design canvas when the change is visual → PR with local verification against live PDS data → deploy → re-critique. - 2026-08-25 — **PR #22 merged: contact affordance, career clamp, blob proxy, polish.** The site record gained optional `contactEmail` (admin settings field; renders a "Write to me" mailto stamp in every hero and a filled "Say hello →" chip on the Open to row — answering the re-critique's P0). The sifa career summary clamps to two sentences behind a native disclosure; record-level curation fields stay queued. Profile images serve through `/blob/{did}/{cid}?w=` (ADR 0015, **Accepted**): DID-document-pinned source, width allowlist, immutable cache, `cf.image` transform with pass-through fallback. Sweep: own-host links suppressed in bios, single writing link, consistent new-tab externals, permalink `aria-label`s, ≥24px tap targets, dark-mode airmail token, " · " paragraph separator in meta descriptions, avatar alt text. Re-critique of the deployed overhaul scored **31/40** (from 25; dual-assessment snapshots in `.impeccable/critique/`). - 2026-08-24 — **Theme UX overhaul landed** (PR #20 merged: linkification everywhere, two CSS specificity/background bugs, sifa `startedAt`/`endedAt` date fix, writing URLs joined from publication url + doc path, skill cap, dates in `