Skip to content

fix(sharing): honest published-address visibility bridge (SHR-8/SHR-10)#131

Merged
eliotlim merged 3 commits into
mainfrom
feat/sharing-site-visibility-bridge
Jul 12, 2026
Merged

fix(sharing): honest published-address visibility bridge (SHR-8/SHR-10)#131
eliotlim merged 3 commits into
mainfrom
feat/sharing-site-visibility-bridge

Conversation

@eliotlim

Copy link
Copy Markdown
Owner

Problem

Page-visibility (set in-app) and site-visibility (the published *.book.cloud address scope, held on the account, defaulting to restricted) were disconnected. A page set "Anyone with the link" was still silently bounced at the edge for anyone not signed in, because the edge serves only a public address anonymously. So "anyone with the link" was false by default, with no in-app control over the address scope and no honest signal that the link was dead.

A first cut of the bridge landed the control but under-stated exposure for the in-between account scopes: it treated everything that wasn't public as "Private" (an authenticated site lets any signed-in stranger read; a members site lets the whole roster read), and toggling such a site would collapse it to restricted (lossy).

Epic: Sharing & publishing — simplify + make honest (IA 2026-07) (Notion). Closes SHR-8, SHR-10.

Solution

  • SDK get/setSiteVisibility over the existing account route PATCH /api/sites/:id (owner-only, whitelist-validated + ownership-enforced server-side). No new account surface.
  • A "Published address: Private/Public" control (owner's publishing device only) in the Sharing settings tab and the Share dialog.
  • An honest mismatch notice (page public but address not) with an inline "Make address Public" action — the one-flip fix, right at the pointer.
  • A read-only honest row for the authenticated/members scopes this desktop control doesn't set: accurate hint ("Anyone signed in can open this address" / "Workspace members can open this address"), a disabled toggle that can't collapse the scope, and a "Manage on the web" pointer. Never mislabeled as "Private".
  • SHR-10: dropped the broken iss#sub handle-invite form (email-only invites remain).

No account edit, no default change, no edge/relay touched. This is UI/display + one notice-condition + review nits.

Key files: packages/sdk/.../forwarding, ForwardingProvider, SiteVisibilityControl, ShareDialog, SharingPublishingSettings, i18n ×4 (en/de/ja/zh).

Review fixes applied (all pre-endorsed)

  • [Devon + Sasha, blocker] Honest read-only row for authenticated/members (disabled toggle + accurate hint + "Manage on the web"); binary toggle kept only for publicrestricted.
  • [Quinn] Mismatch notice now fires on the effective scope, so inheritpublic is covered (effectiveScope = scope === 'inherit' ? defaultVisibility : scope).
  • [Sasha] Corrected the doc comment that wrongly claimed the edge coarsens every non-public scope to "private".
  • [Devon] Workspace-global hint under the control in the Share dialog ("Applies to your whole published address, not just this page.").
  • [Devon] Notice bumped to full-contrast body (not muted) on the caution surface + inline Make address Public button.
  • [Quinn] Explicit canManage && owner guard on the control in Settings (defense-in-depth).
  • [Devon] Public/Private state appended to the switch aria-label.

Before / After

The control only renders behind a live forwarding tunnel (publishedHost), so these are faithful static renders of the shipped component markup using the app's real design tokens. Live capture is the owner's prod-verify (below).

Before After
authenticated/members sites shown as "Private" with hint "signed-out visitors are turned away" — a false under-statement; flipping the toggle collapsed the scope to restricted. Mismatch notice was muted-contrast, fired only on an explicit public scope (missed inheritpublic), and offered no inline fix. All five states, honest. States 3 & 4 are the new read-only rows (disabled toggle, accurate hint, "Manage on the web"). State 5 is the Share-dialog mismatch: full-contrast notice + inline Make address Public + workspace-global hint.

SiteVisibilityControl states

State-by-state (renders shown above):

  1. Public — binary toggle, on. Hint: "Anyone with the link can open this address…".
  2. Private (restricted) — binary toggle, off. Hint: "…signed-out visitors are turned away."
  3. Anyone signed in (authenticated) — NEW read-only row, disabled toggle, state pill "Anyone signed in", hint "Anyone signed in can open this address.", "Manage on the web" link.
  4. Members (members) — NEW read-only row, disabled toggle, state pill "Members", hint "Workspace members can open this address.", "Manage on the web" link.
  5. Share dialog mismatch — page is public but the address is Private: full-contrast amber notice + inline Make address Public button + "Applies to your whole published address, not just this page."

Test procedure

  • pnpm verify green: build:libs ✓, typecheck ✓ (6 projects), lint ✓ (6 projects), test ✓ — SDK 175, UI 968, server 645 unit tests pass.
  • SDK unit tests cover the site-visibility contract and the no-provision-to-read invariant.
  • The site-visibility UI is inert in the web e2e env (it's gated on a live tunnel / publishedHost, which the Playwright suite can't stand up), so runtime verification is the owner's prod-verify below — not a CI gate here.

Operational notes — OWNER GATE

This is edge-exposure: the owner merges and runs the prod verify (do not merge on gate-clearance alone). Prod-verify checklist:

  1. Signed/notarized build with real forwarding against account.book.cloud + prod identity keys.
  2. Enable Publish → confirm "Published address: Private" (matches the restricted default).
  3. Set a page "Anyone with the link" → confirm the honest mismatch notice appears.
  4. From a signed-out / second-account browser, open the *.book.cloud link → expect BOUNCED (proves the defect this PR surfaces).
  5. Flip Published address → Public (via the inline Make address Public or the toggle) → confirm anonymous open now SUCCEEDS and the notice clears.
  6. Flip back → anonymous bounced again.

Sasha's residual notes for prod-verify:

  • This is the first shipping caller of the site-visibility PATCH — glance at the account route's CORS/CSRF posture.
  • Confirm no other surface leaves a site in an intermediate members/authenticated scope (the read-only row handles display, but the setter here only ever writes public/restricted).

Gates cleared: security ✓ Sasha, code ✓ Quinn, design ✓ Devon (blocker fixed to their pre-endorsed shape).


Verify: green (build:libs / typecheck / lint / test — SDK 175 · UI 968 · server 645). Gates: security ✓ Sasha · code ✓ Quinn · design ✓ Devon. Owner merges (edge-exposure).

🤖 Generated with Claude Code

https://claude.ai/code/session_01KFk2T9k3p7ghCjdzMfkA5w

eliotlim and others added 2 commits July 12, 2026 16:14
…invite form

SHR-8: surface + set the account's published-site audience scope so a page set
"public" on a Private address stops silently lying about "anyone with the link".

- sdk: ForwardingClient.getSiteVisibility()/setSiteVisibility() over the EXISTING
  owner-only account route (GET/PATCH /api/sites/:id, body {visibility}); add
  SiteVisibility + SITE_VISIBILITIES mirroring @book.dev/forwarding. No default,
  edge, or account change — the account still whitelist-validates + enforces
  ownership server-side; the client only runs on the owner's device.
- ui: ForwardingProvider reads the REAL scope once online and exposes an
  owner-only setter; SiteVisibilityControl ("Published address: Private/Public")
  in the Sharing settings tab + Share dialog; Share dialog shows an actionable
  notice when a page is public but the address is restricted.

SHR-10: remove the iss#sub handle invite affordance (handle resolution stub-fails,
OB-195) — invite by email only across all four locales.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KFk2T9k3p7ghCjdzMfkA5w
…+ review nits

Applies the consolidated, pre-endorsed review fixes to the site-visibility bridge:

- SiteVisibilityControl: for a site parked at authenticated/members (scopes this
  desktop control never sets, but the account/edge honor distinctly) render an
  honest read-only row with a DISABLED toggle + accurate hint + "Manage on the web"
  pointer, instead of mislabeling it "Private" and collapsing it to restricted on a
  flip. Keep the binary toggle only for the genuinely binary public<->restricted case.
- SiteVisibilityControl: correct the doc comment that wrongly claimed the edge
  coarsens every non-public scope to private.
- SiteVisibilityControl: append the Public/Private state to the switch aria-label.
- ShareDialog: fire the site-mismatch notice on the EFFECTIVE scope, so an
  inherit->public page is covered too (not just an explicit public).
- ShareDialog: bump the notice body to full contrast (not muted) and add an inline
  "Make address Public" button that flips the site scope directly; add a
  workspace-global hint under the control.
- SharingPublishingSettings: gate the control behind an explicit canManage guard
  (defense-in-depth), matching the ShareDialog gating.
- i18n (en/de/ja/zh): new forwarding.visibility.{authenticated,members,
  authenticatedHint,membersHint,manageOnWeb} + share.{makeSitePublic,siteGlobalHint}.

No SDK contract, default, edge, or account change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KFk2T9k3p7ghCjdzMfkA5w
@vercel

vercel Bot commented Jul 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
app.book.pub Ready Ready Preview, Comment Jul 12, 2026 9:17am

Request Review

@eliotlim

Copy link
Copy Markdown
Owner Author

It's too complicated and wordy, make it more concise.

Something like:

  • "Enable Public Access"
  • "Anyone with the link can see this library, even when not signed in"
  • "Only workspace members and guests can see this library"

@eliotlim eliotlim merged commit 62d13eb into main Jul 12, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant