Skip to content

Protected pages with shareable invite links - #36

Draft
avb-dev-docs wants to merge 2 commits into
claude/charming-clarke-7l1ricfrom
claude/page-protection-invitations-0mhmrr
Draft

avb-dev-docs wants to merge 2 commits into
claude/charming-clarke-7l1ricfrom
claude/page-protection-invitations-0mhmrr

Conversation

@avb-dev-docs

@avb-dev-docs avb-dev-docs commented Aug 23, 2026 •

Copy link
Copy Markdown
Collaborator

What changed and why

You can now mark individual docs pages as protected and let people in selectively: pages opt in with protected: true (an editor lock toggle writes it), build as a lock shell with no content in the static HTML, and unlock via a site-wide reading PIN or a shareable invite link — a little signed hash an editor mints from the new Invite… menu (7/30/90-day or non-expiring) that's appended to the page URL (?invite=inv1.<exp>.<sig>) and shared. The hash is HMAC-bound to that one page's slug + expiry, redeems into a per-page cookie so the page stays unlocked after the parameter is gone, and is stateless — rotating ADMIN_SECRET revokes every link at once.

Ports the protection system already proven in keyboard-docs, and additionally gates this repo's extra content surfaces: search-index bodies, llms.txt / llms-full.txt, the /llms.mdx markdown mirrors, the AI assistant's retrieval, and the /mcp server's search/read/list tools all exclude protected pages. New Access → "Protected pages" docs page explains the flow.

Also adds a second docs page (separate commit): Editing → "Contributing with Claude Code" — the practical contributor workflow for local Claude Code (setup, asking for pages, the /new-page and /draft-page skills, verification, and the four review paths), complementing the existing "Why docs.dev + Claude Code" page.

Security considerations

This PR is an authorization feature; the gate is enforced server-side on the Worker:

  • Protected content is only ever served by /api/reader/content, which requires a valid reader cookie (HMAC of ADMIN_SECRET + READER_PIN), a valid invite for that slug, or a signed-in editor session. The prerendered page is a lock shell — content never ships in static HTML.
  • Invite tokens: HMAC-SHA256 over invite-v1:<slug>:<exp> keyed by ADMIN_SECRET, truncated to 128 bits; constant-time comparison; expiry covered by the signature so it can't be extended; version-prefixed (inv1.) and domain-separated from the reader-cookie HMAC. Minting requires an editor session. Trade-off: tokens are stateless, so there is no per-link revocation — expiry bounds a leaked link, and rotating ADMIN_SECRET revokes everything.
  • Fail-closed: with READER_PIN/ADMIN_SECRET unset, the respective unlock paths reject; nothing falls back to open.
  • Titles/descriptions of protected pages stay public by design (sidebar, lock shell, search-by-title) — documented on the new docs page.

Verification

  • Manually verified: pnpm types:check and pnpm lint clean; 34-check e2e matrix against pnpm dev — lock shell never contains page content; anonymous content API → 401; minting requires an admin session; an invite unlocks exactly its own page (cross-page attempts → 401); tampered expiry/signature → 401; cookie redemption and PIN unlock work; admins bypass; llms.txt/llms-full.txt/llms.mdx/search/MCP read_page+search_docs+list_pages all exclude the protected test pages; public pages unaffected.

Rollback

Revert the merge commit. (Any invite links handed out stop working once reverted; nothing server-side to clean up.)

Review attestation

  • I reviewed the full diff before approving.

Port of the page-protection system (proven in keyboard-docs), extended
with per-page invitation links:

- `protected: true` frontmatter (editor lock toggle included) builds the
  page as a lock shell — title/description only; content is served solely
  by /api/reader/content after a server-side check of the reader cookie,
  an invite, or a team session.
- Invite links: editors mint a signed hash from the new Invite… menu next
  to the lock toggle (or POST /api/admin/invite) and share the page URL
  with ?invite=<hash> appended. The HMAC covers the page slug and expiry
  (7/30/90 days or none), so a link opens exactly one page and can't be
  extended; redeeming it sets a per-page cookie so navigation and
  refreshes stay unlocked. Stateless — rotating ADMIN_SECRET revokes all
  links at once.
- Site-wide READER_PIN unlock and always-on access for signed-in
  editors, as in the original design.
- Protected content is excluded everywhere it could leak: search index
  bodies, llms.txt / llms-full.txt, the /llms.mdx markdown mirrors, and
  (new in this repo) the AI assistant's retrieval and the /mcp server's
  search/read/list tools.
- Docs: new Access → "Protected pages" page; READER_PIN listed in
  configuration and wrangler.jsonc comments.

Verified: types:check + lint clean; 34-check e2e matrix against pnpm dev
(lock shell never carries content; anonymous 401; mint requires session;
invite unlocks its page only — cross-page 401; tampered expiry/signature
rejected; cookie redemption; PIN path; admin bypass; llms/search/MCP
surfaces clean; public pages unaffected).

Co-Authored-By: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01Ccsf25xk9FdqFbYJ3NWkoo
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 23, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
docsdotdev 1ab6c18 Commit Preview URL

Branch Preview URL
Aug 24 2026, 01:54 AM

A practical contributor guide alongside the existing "Why docs.dev +
Claude Code" pitch page: one-time setup (clone, pnpm install, claude),
asking for pages in plain language, the /new-page and /draft-page
skills, verifying with types:check + the dev preview, and the four
review paths (direct push, PR, shared draft, branch review from the
editor), with a conventions cheat sheet. Registered in the sidebar's
Editing group after claude-code.

Verified: pnpm types:check passes.

Co-Authored-By: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01Ccsf25xk9FdqFbYJ3NWkoo
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.

2 participants