Skip to content

Site: four-intent structure, interim Supporter signing, Participate and registers, temporary Review page - #313

Merged
tnunamak merged 10 commits into
mainfrom
site/four-intent-structure
Sep 3, 2026
Merged

Site: four-intent structure, interim Supporter signing, Participate and registers, temporary Review page#313
tnunamak merged 10 commits into
mainfrom
site/four-intent-structure

Conversation

@artvana

@artvana artvana commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Rebuilds pdpp.dev around the four intents, adds the interim Supporter signing system behind a flag, ships the conformance registers, and adds a temporary Review page.

Branched off main at f1e8357d, after #267 merged. The design handoff is committed at design_handoff_pdpp_site/.

Routes

Route What it is
/ Hero with the repo's scrolling data columns, the OAuth-versus-grant comparison, the six principles, the Supporters register, and the entry to Build.
/principles Preamble and the six principles rendered from the repo-root PRINCIPLES.md, who can sign, the signing form, and the public register.
/specification spec-core.md in the reader, with a contents rail of Core's nine sections, the informative documents, and Appendix A. GOVERNANCE.md renders inline at #governance, above it the four-stage diagram.
/build The three role cards, running the reference implementation, and the community FAQs.
/participate Three levels, four verification steps, the status table, both registers, and the contact channels.
/review Temporary. What to read, how to comment, what happens to a comment. Behind reviewOpen in the nav and ticker; the page itself always resolves.
/privacy Controller, purposes, what is stored, what is published, retention, confirmation and withdrawal, contact. All from config.
/governance 308s to /specification#governance. The standalone route is gone.

Nav is the four items with a Specification dropdown holding "The specification" and, behind reviewOpen, "Review, until 1 Oct". The full-width ticker under the header is one link wrapping a CSS marquee: two identical tracks translated by exactly -50% so the loop seam is invisible, ~55s, paused rather than hidden under prefers-reduced-motion.

Theme is the repo's existing PdppThemeSwitch and ThemeProvider, already keyed to pdpp-theme. The wordmark is the repo's existing WordmarkIcon; fonts are the repo's own loading, not the export's Google Fonts.

/review and the reader's version strip are the only places the review period is named, so retiring it is the flag plus one page.

Signing data flow

form on /principles
   |  POST /api/sign — rate limit, strict schema, organisation-domain rule
   v
pending store (KV, 48h TTL) ---- expires, leaving nothing ----> gone
   |  the signatory uses the single-use confirmation link
   v
PRIVATE repo: signatories/<yyyy>/<id>.json, committed by a bot
   |  scheduled publish, five fields only
   v
PUBLIC repo: apps/site/public/principles/supporters.json
   |
   v
/principles

Three properties hold it together:

  • An unconfirmed submission leaves no residue. It lives only in a TTL store until the person who owns the address acts.
  • One direction only. The site writes to the private repo; the private repo writes to the public one. The public site holds no credential that can read the private store.
  • The publish script is an allowlist. It names the five public fields one at a time rather than deleting the private ones, so a record that grows a field cannot leak it.

Single use is enforced by GETDEL on the pending record, not by the token: an HMAC is stateless, so check-then-read-then-delete would leave a window where two clicks both write a signatory file. The withdraw link deliberately never expires — a signatory keeps the email and may act on it a year later.

Every seam fails closed with SigningUnavailableError rather than degrading. A degrading seam would accept a signature, tell the signatory it worked, and drop it.

None of the signing system has run. The KV store, the mail provider and the deploy key do not exist. Treat the request shapes as the intended contract, to be checked against each provider's documentation when provisioned.

The private store

Not created here, by agreement: it holds personal data and its settings need org-level rights I do not have. Staged at ~/Desktop/claude-projects/supporters-private-staging/, outside this repo, ready to push to PDP-Connect/supporters-private:

  • README.md — what it holds, who may read it, handover and deletion.
  • scripts/publish-supporters.mjs — the allowlist publisher.
  • scripts/export-list-optins.mjs — prints opted-in addresses to stdout for a one-off manual subscribe. Sends nothing.
  • .github/workflows/publish-supporters.yml — scheduled, not signature-triggered, so a confirmation cannot make the public commit timing a side channel about when someone signed.
  • SETTINGS-CHECKLIST.md — private, collaborators, forking off, secret scanning, branch protection, deploy key, and every Vercel secret.

docs/registers.md in this repo documents all three stores, the flow, access, and the handover procedure.

Registers

/register/index.json ships []; /register/trust-registries.json holds the one recognised entry, the Data Transfer Initiative's Data Trust Registry. Both are PR-driven and hold no personal data, both render on /participate. Issue templates apply-source.md, apply-accessor.md and apply-operator.md list what to attach per GOVERNANCE.md Appendix A. The Operator button is inert with its date until operatorApplications.

Config still needing real values

Every one renders a visible bracketed placeholder until set, so nothing guessed ships as fact.

  • NEXT_PUBLIC_PDPP_CONTROLLER_NAMEdecided: the Vana Foundation, holding the register until LFDT hosting is confirmed. Shipped as a default, still env-overridable.
  • NEXT_PUBLIC_PDPP_GENERAL_CONTACTdecided: support@vanafoundation.org. The footer's General contact, never the reports address.
  • NEXT_PUBLIC_PDPP_PRIVACY_CONTACT — defaults to the general address. Split it by setting this when a dedicated mailbox exists.
  • NEXT_PUBLIC_PDPP_FORM_ENDPOINT — where the form posts
  • NEXT_PUBLIC_PDPP_EMAIL_PROVIDER — named in config; the route currently targets Resend's API
  • NEXT_PUBLIC_PDPP_DISCORD_URL
  • NEXT_PUBLIC_PDPP_MAILING_LIST_URL
  • Flags: NEXT_PUBLIC_PDPP_REVIEW_OPEN, NEXT_PUBLIC_PDPP_SIGNING_LIVE, NEXT_PUBLIC_PDPP_OPERATOR_APPLICATIONS — all default off
  • Server secrets: PDPP_SIGNING_TOKEN_SECRET, PDPP_KV_REST_API_URL, PDPP_KV_REST_API_TOKEN, PDPP_MAIL_API_KEY, PDPP_MAIL_FROM, PDPP_PRIVATE_REPO_OWNER, PDPP_PRIVATE_REPO_NAME, PDPP_PRIVATE_REPO_TOKEN
  • In the private repo: PDPP_PUBLIC_REPO_TOKEN

Where the two sources disagreed

  • Hero. The prototype puts a grant card right of the hero text; the design export and the brief's own addition put the scrolling data columns there. Columns used, which also stops the grant card appearing twice, since it carries the comparison below.
  • Section citations. /build's prototype copy is built on § citations, and the brief removes section citations from site copy outside the reader and /review. Links kept as wayfinding but named ("Source declaration" rather than "Core §5"); they land on the same anchors. A grep of the built site finds zero § on /, /principles, /build, /participate and /privacy.
  • Supporters table. The prototype shows six example rows and "[ 247 ] signatories", labelled layout-only. An empty state ships instead: supporters.json is [], and a page showing invented signatories has published a false register.
  • Governance lede. The prototype's governance section says "Both are open for comment until 1 October: how to comment". That names the review period outside /review, so the clause is dropped.

Copy deltas 1 to 5 are all applied.

Verification

pnpm spec:check passes. pnpm --dir apps/site types:check, test (202 passed, 0 failed), check and build all pass, 68 pages generated.

Served the production build: all seven routes 200, /governance 308s to /specification#governance, the rail renders the three groups, and the governance document renders inline.

Grep of the built site finds one email address, the reports mailbox, plus user@example.com inside spec-collection-profile.md's own JSON example, which is pre-existing spec text on a reserved example domain. bot@pdpp.dev appears in source as the git committer identity the GitHub contents API requires; it is not a mailbox. No private-store content is tracked in this repo.

Five sandbox files are reformatted by the repo's own formatter. They are unrelated to this change and were already failing pnpm check on main (20 errors there); fixing them is what makes this branch green.

Assisted-by: AI

🤖 Generated with Claude Code

https://claude.ai/code/session_01NECjjZbYWNTzq3bu7nuiDP

artvana and others added 5 commits September 3, 2026 00:01
Foundation for the four-intent site.

- src/lib/site-config.ts holds every deployment-specific value behind a
  visible bracketed placeholder. A wrong-but-plausible default reads as fact
  and ships silently; "[controller name not set]" cannot. unsetConfigValues()
  is what the launch checklist reads. The reports mailbox is deliberately NOT
  configurable: a deployment that could repoint it could redirect a conduct or
  security report.
- Nav becomes Principles, Specification, Build, Participate, with the
  Specification dropdown carrying "The specification" and, behind reviewOpen,
  "Review, until 1 Oct". The dropdown opens on hover AND focus-within, so it
  is keyboard-reachable with no open/closed state to keep in sync.
- The review ticker is one link wrapping a CSS marquee: two identical tracks
  translated by exactly -50%, so the loop seam is invisible and the animation
  runs off the main thread. prefers-reduced-motion pauses it rather than
  hiding it, because the message still has to be readable.
- sync-spec-docs parses PRINCIPLES.md into PRINCIPLES_PREAMBLE and
  PRINCIPLES_LIST so /principles and the home page render the six principles
  without a second copy in site source. It throws unless exactly six parse.
- PdppConceptSection's sectionIndex becomes optional: the new site carries no
  ordinal numbering on section headings.
- PRINCIPLES.md gains the sentence about the relying party's uncertainty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NECjjZbYWNTzq3bu7nuiDP
Signed-off-by: Art A <129354338+artvana@users.noreply.github.com>
Four of the seven routes, carrying the prototype's copy with the deltas
applied.

/principles renders the preamble and the six principles from PRINCIPLES.md
through the generated module, never from page source. The Supporters table
ships an EMPTY state, not the prototype's six example rows and "[ 247 ]"
counter: those are labelled layout-only in the design, and a page showing
invented signatories has published a false register.

/build carries the three role cards and the FAQs. Section citations are named
rather than numbered ("Source declaration" rather than "Core §5"): the § glyph
and numbered citations do not belong in site copy outside the reader and
/review, and a section number that moves leaves every citing page wrong. The
links still land on the same anchors.

/review applies all three of its copy deltas: the working-sessions channel is
gone, the comment-handling sentences are replaced, and the "one document of
nine sections" sentence is cut from the lede. It is one of only two places
that name the review period, so retiring the period is the flag plus this
page.

/privacy states controller, purposes, what is stored, what is published,
retention, the confirmation and withdrawal mechanism and a contact, all from
config so nothing legal is guessed on someone else's behalf.

The signing form is present but renders the closed notice until signingLive.
When the flag is off the form is ABSENT, not disabled: a disabled form still
ships its field names and endpoint to every reader.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NECjjZbYWNTzq3bu7nuiDP
Signed-off-by: Art A <129354338+artvana@users.noreply.github.com>
Home carries the hero, the OAuth-versus-grant comparison, the six principles,
the Supporters register and the build entry point.

The hero's right-hand element is the repo's own scrolling data columns rather
than the grant card the prototype puts there. The design export and the build
prompt both call for the columns, and the grant card already carries the
comparison below: rendering it twice would spend the page's one strong
artifact on decoration.

The comparison is components, not an image. It is the one place the site shows
the argument rather than stating it, so it has to reflow, respect the theme,
be selectable and read aloud correctly. The two panels deliberately do not
share a card shell: they show a consent dialog and a record, and one shell
would flatten the difference the section exists to make.

/specification now renders GOVERNANCE.md inline at #governance, with the
four-stage diagram built from components for the same reasons. /governance
308s there and the standalone route is gone. One page carries the protocol and
the programme that stewards it.

The contents rail becomes Core's own nine sections as in-page anchors, then
the informative documents under "Implementer guidance, informative", then
Appendix A. The sections are hand-declared rather than picked from the
fumadocs tree because they are anchors on this page, and the tree only knows
about documents.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NECjjZbYWNTzq3bu7nuiDP
Signed-off-by: Art A <129354338+artvana@users.noreply.github.com>
/participate carries the three levels, the four verification steps, the status
table, both registers and the contact channels.

Both registers are PR-driven JSON in this repo and hold no personal data.
/register/index.json ships empty; /register/trust-registries.json holds the one
recognised entry, the Data Transfer Initiative's Data Trust Registry. Both are
read at build time rather than fetched: they ship in the repo, so a network hop
in front of them would only add a way for the page to 500.

The Operator apply button is inert with its date until operatorApplications is
on. The test suite an Operator is assessed against does not exist until then,
so a live button would invite an application nobody can act on.

Copy delta 1 applied: the Level 1 channel list is "Discord, mailing list,
comments", and the section is "Channels and contacts".

Issue templates for the three roles list what to attach per GOVERNANCE.md
Appendix A, each stating that the admissions check is exhaustive and that every
return cites the item it failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NECjjZbYWNTzq3bu7nuiDP
Signed-off-by: Art A <129354338+artvana@users.noreply.github.com>
The signing system, all behind signingLive and none of it exercised: the KV
store, the mail provider and the deploy key do not exist yet. Every seam fails
closed with SigningUnavailableError rather than degrading, because a seam that
degraded would accept a signature, tell the signatory it worked, and drop it.

POST /api/sign rate-limits, validates against a strict schema, refuses an
organisation submission that is not from an address at the organisation's own
domain, writes a pending record to a TTL store and sends ONE email. Nothing is
published here. A signature becomes real only when the confirmation link is
used, which is what stops anyone signing in someone else's name.

Single use is enforced by GETDEL on the pending record, not by the signature:
an HMAC is stateless, so checking, then reading, then deleting would leave a
window where two clicks both write a signatory file.

The withdraw link deliberately does not expire. A signatory keeps the email and
may act on it a year later, and a withdrawal that needed a person to serve it
is the friction the Principles say ending should not have.

The flags gate the ROUTES, not just the UI: a form hidden in the page while the
endpoint still accepts posts is not switched off.

docs/registers.md documents the three stores, the one-way data flow, who has
access, and the handover, rotation and deletion procedure.

Also here: the sitemap drops /governance (a redirect in a sitemap is a redirect
a crawler must follow to learn it was one) and /review (temporary, and a page
due for retirement should not be advertised), and picks up the new routes.

Five sandbox files are reformatted by the repo's own formatter. They are
unrelated to this change and were already failing `pnpm check` on main; fixing
them is what makes this branch green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NECjjZbYWNTzq3bu7nuiDP
Signed-off-by: Art A <129354338+artvana@users.noreply.github.com>
@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
pdpp Ready Ready Preview Sep 3, 2026 1:54pm UTC

Request Review

@artvana

artvana commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

@tnunamak @annakaz — review context for all three open PRs, and the order to take them, is in one comment on #279: #279 (comment)

This one is independent on the branch graph but coupled in content: /specification renders the repo-root GOVERNANCE.md, so #279's text lands on the site the moment it merges. Until then that section still shows Official Source and Steward.

The Vana Foundation holds the Supporter register until LF Decentralized Trust
hosting is confirmed, and support@vanafoundation.org is the project's general
address. Both were placeholders; they are decided, so they render as
themselves.

They are defaults rather than hard-coded values: a deployment can still
override either through the environment, and the transfer to LFDT is one edit
here.

The privacy contact defaults to the general address for now. A privacy page
that cannot name an address to write to is worse than one naming the same
address as everything else; split them by setting the variable when a
dedicated mailbox exists.

The footer gains its Contact block. Reports and General are never the same
address: reports is the LF Decentralized Trust mailbox fixed by GOVERNANCE.md's
header, general is the project's own, and routing either to the other is the
failure the separation prevents. The footer's governance link now points at
/specification#governance rather than through the /governance redirect.

Verified on the served build: no bracketed placeholder renders on /, /principles,
/privacy or /participate, and no em-dash appears in site copy on any of the
seven routes. Discord and the mailing list URL are the two values still unset.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NECjjZbYWNTzq3bu7nuiDP
Signed-off-by: Art A <129354338+artvana@users.noreply.github.com>
The gate is not "is it a list", it is "would this put personal data in the
private repository". Nothing that would collect a person's details is
reachable until the arrangement for holding them is confirmed. A form that
takes an address into a store nobody has agreed the shape of is the one thing
on this site that a later edit cannot undo.

Three surfaces claimed signing was open and no longer do:

- The signing section keeps its place on /principles and now carries a waiting
  panel. It says we are not taking signatures yet, says why, and sends the
  reader to the specification instead, because someone who arrived wanting to
  support the work can still do the useful thing this week and comment on it.
- /participate's Level 2 said "Open now". It now says signing opens once the
  register has a confirmed home, and its link goes to the Principles document
  rather than to a form that is not there.
- The Pre stage in the governance diagram said "you can sign now".

The Discord and mailing list cards no longer render an unset config value as a
live link. An href of "[mailing list URL not set]" looks live and goes nowhere,
which is worst at exactly the moment a reader expects to hand over an address;
they show "Link to follow" until the destination is real.

The mailing list opt-in needed no separate change: it lives inside the form,
which is absent while signingLive is off.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NECjjZbYWNTzq3bu7nuiDP
Signed-off-by: Art A <129354338+artvana@users.noreply.github.com>
@artvana

artvana commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 4d2c3269: every surface that would put personal data in the private repository is now held in a waiting state, pending Kat F. on the right approach.

The gate is "would this collect someone's details", not "is it a list". Three places claimed signing was open and no longer do:

  • The signing section keeps its place on /principles and carries a waiting panel: not taking signatures yet, why, and a link to the specification instead, so someone who came to support the work can still comment on it this week.
  • /participate Level 2 said "Open now"; it now says signing opens once the register has a confirmed home, and links to the Principles document rather than to a form that is not there.
  • The Pre stage in the governance diagram said "you can sign now".

Discord and the mailing list no longer render an unset config value as a live link (href="[mailing list URL not set]"); they show "Link to follow" until the destination is real. The mailing list opt-in needed no change, since it lives inside the form and the form is absent while signingLive is off.

Nothing here changes the conformance register, which is PR-driven and holds no personal data.

Verified on the served build: no page claims signing is open, and no placeholder href renders.

discord.gg/JmKcFYE4 for #pdp-connect.

It also collapses a duplicate. The footer read DISCORD_INVITE_URL from
site-facts while /participate read discordUrl from config, so the site held two
Discord constants at once. That is how a site ends up publishing two different
invites and only noticing when one of them expires. Config owns the value now
and the footer reads the same one; the site-facts constant is retired with a
note saying where it went.

The previous invite (FV4bkZBdmA) is replaced, not kept as a fallback. Two live
invites to the same channel is the state this change exists to remove.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NECjjZbYWNTzq3bu7nuiDP
Signed-off-by: Art A <129354338+artvana@users.noreply.github.com>
Signed-off-by: Art A <129354338+artvana@users.noreply.github.com>
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
@tnunamak
tnunamak merged commit 6c42c41 into main Sep 3, 2026
6 checks passed
@tnunamak
tnunamak deleted the site/four-intent-structure branch September 3, 2026 14:38
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