Skip to content

Give ParaSend a product page and put the buyer's questions first on ParaSign - #339

Merged
Apolloccrypt merged 4 commits into
mainfrom
feat/fe-product
Sep 2, 2026
Merged

Give ParaSend a product page and put the buyer's questions first on ParaSign#339
Apolloccrypt merged 4 commits into
mainfrom
feat/fe-product

Conversation

@Apolloccrypt

@Apolloccrypt Apolloccrypt commented Sep 2, 2026

Copy link
Copy Markdown
Owner

What this is

Three commercial pages, one shape, taken from docs/brand/messaging.md (#331):
what you can do here, who it is for, the split with its real number, who is
behind it, then the proof and the honest limits. /parasend is new; /parasign
has existed since #325 and is rebuilt around the buyer; /sign loses the last
piece of jargon in its first screenful.

Rebased on main at 6dded85. Conflicts resolved in favour of main for
everything outside these three pages, including the Community naming from #328
and the /about give-back sentence from #332.

The review points, and what each one got

Point What changed
/parasign opens on technique, not a promise First paragraph is two sentences and 160 characters about what the buyer gets. The guide's own tone rule keeps cryptography below the fold; the paragraph the guide specifies for this page broke that rule, so it now sits verbatim in the proof block at 04.
Jargon label under the H1 in the first screen Eyebrow reads ParaSign · document signing by Paramant.
eIDAS status on screen 8 of 9 Moved from 6,386px to 737px. It is the first thing a lawyer asks, so the page answers it before the tiers.
Price table before the explanation /parasign: 01 what it does, then 02 what it costs (first tier card at 2,518px). /parasend: tables at 4,220px, after what it does, how to begin, and who is behind it.
Nowhere does it say who it is for Both pages name the buyer in the first screen. /parasign uses the audience index.html already states: legal, finance and healthcare practices in the EU.
"Free forever for everyone" hides the limits /parasign: "2 signatures a month, no card, forever." /parasend: "10 transfers a month, 5 MB a file, links that last an hour and are gone after one read." Both at the promise, not four screens down, and both read from relay/lib/tiers.js.
"the tier named Free" bridge sentence Gone, and the /parasend tier card is named Community. #328 renamed the tier on /pricing, so there is nothing left to explain away.
SLA is 99.95%, not 99.9% Correct on the page, and now read off /sla by name (the Enterprise cell) so any page repeating it has to match.
The monthly prices are not pinned Fixed, see below.
/sign: "private key" in the first screenful "Your signing key stays here · it is made on your device and never reaches our servers."
/parasend "What it does" is technique Section 02 says what the buyer gets. AES-256-GCM, ML-KEM-768, ECDH P-256, ML-DSA-65 and the RAM budget are in 06, each beside the page that checks it.

Two things beyond the list, both from the same review:

  • "Post-quantum, zero-knowledge" is gone from the scope note. It described
    nothing the page can point at, and it stood next to the most honest paragraph
    on the page.
  • The /parasend founder block quoted a sentence about US subscriptions that had
    no source. Signed in, the free plan is called Community, and the page answers what a buyer asks #332 has since put the guide's sentence on /about, so the page now
    quotes it: "The Community plan is his way of giving something back to society;
    the business plans pay for it."

The two gates that were not gates

Both found by sabotage, not by reading.

The monthly price on both product pages was pinned by nothing.
relay/test/pricing-page.test.js asserted with a bare substring, so
includes('€49') was satisfied by the annual €499 excl. on the same
card, and includes('€15') by €150. Verified: €49 to
€59 on /parasign and €15 to €19 on /parasend both stayed
green. Those are the two numbers a buyer clicks. An amount now has to end where
it says it ends and sit on the card of its own plan, so the opening line
"business plans from €49 a month" cannot stand in for the tier.

A claim that has to be read first was asserted against the whole file, which
is satisfied by screen 8 of 9. The eIDAS scope note, the audience line and the
free promise with its number are now asserted against the hero section, not the
file.

Also pinned: the EU claim in the form #328 settled ("No US provider in the data
path", with the Resend exception in the same breath) on /, /parasign and
/parasend, with the broader "no US company" wording refused on the product
pages, since section 9 of the guide holds it open and it stays on /security
where the table qualifies it. And the stale string is gone: the suite pinned
"The community tier stays free", a sentence #328 removed from /pricing.

Second round: four claims contradicted by their own source

Each of these was a claim with a checkpoint attached, where the checkpoint said
something else. That is worse than no checkpoint, so the pins moved with the
claims.

1. The free number was the wrong number. Both pages said "10 uploads an
hour". The figure is real, but it is ANON_RATE_PER_HOUR on
POST /v2/anon-inbound (relay/relay.js), deprecated on 2026-05-28, answering
with Deprecation and Sunset headers, retiring 2026-12-31. It is not what a
Community account gets. relay/lib/tiers.js gives the Community row 10
transfers a month, 5 MB a file, a one hour link and one read; relay.js
enforces them with a 402 (monthly_transfer_quota_reached) and a 413
(Max 5MB). Hero and Community card now state those. ParaSend Pro gains its
real ceiling, 500 a month, in place of "No IP rate limit", the same artefact.

On /parasign the ParaSign Pro card said "Unlimited transfers". A ParaSign Pro
account derives plan_parasend: 'pro' (entitlements.js derivePlanParasend),
which is 500 a month, and that file has a hard rule that no metered tier is
unbounded: even enterprise gets a finite ceiling, never Infinity. Now 500.

frontend/pricing.html:249 and frontend/index.html:360 carry the same
upload figure. They are not touched here; #336 and a separate PR correct
them. Naming it so it is not lost.

2. ParaShare has no signature, and the register the card cites says so.
/parasend promised "ML-DSA-65 signed receipts" for ParaShare and linked to the
algorithm register in the same sentence. That register
(crypto-agility.html:429-434) lists ParaShare (webapp) with SIG n/a on the
pre-v1 hybrid wire, migrating to v1; ML-DSA-65 on the v1 wire is sdk-py
3.0.0 and sdk-js 3.0.0. The card now states what the register states. The
buyer-facing half went with it: section 02 promised the recipient "proof that
the file came from you", a signature claim on a path with no signature. It now
says what the path does deliver: the file opens on the recipient's registered
device and nowhere else, and a file altered on the way does not open at all.

3. The jurisdiction table does not carry Bunny. Both pages named Bunny DNS
(Slovenia) and then sent the reader to the Jurisdiction and privacy table on
/security. That table (security.html:351-362) lists Hetzner Nuremberg, the
legal jurisdiction, the CLOUD Act row, retention, IP logging and analytics. DNS
is not a row in it. The sentence stays exactly as index.html:382 carries it;
the pointer now goes to /privacy and ParaRule 5, which do carry it.

4. Two smaller ones. "Fixed-size ciphertext" is a ParaShare property, not a
property of every transfer (security.html:358: "approximate encrypted size (up
to 5 MB; exact 5 MB for ParaShare)"), so it is bounded to that path. And the Pro
list was missing "Email notifications via Resend" (pricing.html:267), which
names a subprocessor, so its absence quietly dropped a disclosure.

What the pins do now

The ParaSend limit lines are read from relay/lib/tiers.js instead of compared
against the wording on /pricing. Comparing two pages only proves they agree, and
that is exactly the failure here: /pricing and index.html are wrong in the same
way, so a page-to-page pin was green throughout. The uploads-per-hour figure and
"unlimited transfers" are refused on both product pages under any wording. The
algorithm register is parsed and the page held to the row it cites. The EU
sentence is pinned to index.html, where it ships, and an assertion fails if the
/security table ever starts naming Bunny, at which point the pointer may return.

Sabotage log

Each string mutated on its own, suite run, reverted.

Mutation Result
/parasign Pro monthly €49 to €59 RED
/parasign Pro annual €499 to €599 RED
/parasend Pro monthly €15 to €19 RED
/parasend SLA 99.95% to SLA 99.9% RED
/parasign audience line removed from the hero RED
/parasign "2 signatures a month" removed from the hero RED
/parasend hero limits removed RED
/parasign "No US provider in the data path" to "No US company in the chain" RED
/parasend Resend sentence removed RED
tiers.js community transfers_month 10 to 20 RED
tiers.js pro transfers_month 500 to 900 RED
tiers.js community file_mb 5 to 8 RED
tiers.js community signs_month 2 to 3 RED
/parasend Community transfers line removed RED
/parasend 5 MB line removed RED
/parasend Pro transfers line back to "No IP rate limit" RED
/parasend hero back to the anon uploads figure RED
/parasign back to "Unlimited transfers" RED
/parasend "Email notifications via Resend" shortened RED
/pricing "Email notifications via Resend" shortened RED
/parasend re-claims ML-DSA-65 receipts for the webapp RED
/parasend re-claims sender proof in section 02 RED
register row: ParaShare KEM changed RED
/security jurisdiction table gains Bunny RED
index.html EU sentence changed RED

Measured at 390px, Playwright, y from the top of the document

No horizontal overflow anywhere: scrollWidth is exactly 390 on all three.

/parasign /parasend /sign
H1 119 119 105
audience line 432 396 n/a
first CTA 511 500 763
free promise with its number 632 620 291
eIDAS / SES status 737 n/a (signing only) n/a
burn-on-read warning n/a 719 n/a
first price table 2,518 4,261 n/a
page height 7,744 (9.2 screens) 8,963 (10.6) 2,163 (2.6)

On /parasign the H1, the audience, both buttons, the free promise and the eIDAS
note all land inside the first 844px.

One layout fix, page-local

At 390px the house .sec-head keeps the section number in its own grid column,
which left the H1 about 160px to wrap in: six lines of headline on /parasign,
with the buttons and the eIDAS note pushed off the first screen. On a narrow
screen the number now stacks above the headline. The rule lives in each page's
own <style>; design-system.css is untouched, because it is shared with pages
this PR does not open.

Checks run locally

tests/links, tests/seo-contract, tests/ui-truthfulness, tests/site-claims,
tests/navigation-shell, tests/frontend-loading-contract,
relay/test/pricing-page.test.js, scripts/check-csp-inline.sh,
scripts/check-cache-bust.sh, eslint, and tests/static-sanity.sh
(10/10, including the commit-style guard). All pass.

frontend/apply-nav.py was run and changed nothing: nav and footer were already
stamped correctly. bron-seo/build_sitemap.py regenerated the sitemap, which
adds the one /parasend entry.

Not in scope

The pages are English while the audience named on them is a Dutch office. Every
reviewer raised it; it is a sitewide decision and belongs in its own round. The
SES versus AES contradiction between /about and the /pricing FAQ is section 9 of
the guide and is likewise left where it is.

@Apolloccrypt

Copy link
Copy Markdown
Owner Author

390px, no session

Playwright, Chromium, 390x844, dpr 2, served from frontend/ on a local static server. Same convention as review/homepage-koper-screenshots: the images live on the orphan branch review/fe-product-screenshots, which holds no code and is not for merging.

/parasign, first screen

parasign fold

/parasign, section 01: the split

parasign split

/parasend, first screen

parasend fold

/parasend, section 01: the split

parasend split

/sign, the copy above the tool

The three mono facts and the link to the product page are new. So is a CTA that looks like a button: .ds-btn was never defined in any stylesheet, so "Create an account" used to render as two lines of plain blue text.

sign fold

Apolloccrypt added a commit that referenced this pull request Sep 2, 2026
Review of #339 read both product pages as a buyer and found the same defect
twice: the first paragraph explains the mechanism to someone who already knows
what it is. "ML-DSA-65", "transparency log", "ciphertext", "RAM" and above all
"the relay" appear before the page has said what the reader gets. The relay is
our word for our own server and it is not defined anywhere on either page.

So both ledes now say what happens in words a reader already has, and the
mechanism keeps its place under "06 / The proof", where the /about sentences
ui-truthfulness pins are still quoted verbatim.

  /parasign  ParaSign signs your PDF on your own computer. The document and
             your signing key never leave your browser. Whoever opens the file
             later can check for themselves that nothing in it was changed,
             without asking us and without an account.
  /parasend  Your file is scrambled on your own device before anything is sent.
             We never write it to disk, and once the person you sent it to has
             downloaded it, it is gone. Servers in Germany, under EU law.

Order. Section 01 ran the full tier tables, so on a 390px screen the reader
scrolled roughly 1,850px of prices, "Business 299 euro/month" among them, before
a word about what the product does. The split stays at 01 because it is the
thing that has to be clear first, but only as the two paragraphs /pricing
already carries plus a link. The tables moved to 05, after "what it does" and
"how to begin". Measured at 390x844: tiers now start at 4,449px on /parasign and
4,315px on /parasend instead of 725px and 720px.

eIDAS, on the first screen. The h1 promises "prove it years later" and the
scope note sat at 6,386px of a 7,922px page, screen 8 of 9, while for a law or
advisory firm it is the first question. A line under the buttons now says it is
an ordinary electronic signature (SES) and not a qualified eIDAS signature, and
links to the full note. The note itself is unchanged and still pinned.

Who it is for, on the first screen: an audience line under each lede, at 487px
and 392px. And the section label a reader meets first is no longer
"post-quantum e-signing" but "document signing with proof that lasts".

Three claims that read as more than they are:

- SLA 99.9% on /parasend contradicted the published 99.95% on /pricing and
  /sla. This is the drift the PR exists to prevent, and nothing pinned it, so
  pricing-page.test.js now reads the Enterprise target out of sla.html and
  fails any page that states a different SLA figure. It reads the Enterprise
  cell by name because /sla also publishes 99.5% for Community.
- "IEC 62443 / NIS2 / NEN 7510 documentation" stood on screen 3 while the "no
  third-party certification" limit was 4,000px lower. The limit is in the
  bullet now, and pinned there.
- "Post-quantum, zero-knowledge" opened the honest paragraph on /parasign with
  a term nothing on the page supports. Replaced by what actually happens: we
  never see your document.

ParaShare was a third product name with no explanation, in a tier bullet and in
step 2. It is named as what it is, the send screen of ParaSend, in both places
and in the ParaShare card under "what it does".

Tier cards now carry the names /pricing sells (Free, Pro, Business,
Enterprise) instead of product-prefixed variants, which is what the tier-name
check from #325 asks for, and the Community wording gets its bridge sentence to
the tier /pricing calls Free.

/sign: "the private key never reaches the relay" is now "never reaches our
servers". The rest of that page was read as fine.

Rebased onto main, which now carries #325's own /parasign. Conflicts in
bron-seo/apply_seo_head.py, bron-seo/build_sitemap.py, frontend/sitemap.xml,
relay/test/pricing-page.test.js and frontend/parasign.html resolved by taking
this branch's page and keeping every assertion #325 added, including the ones
that then failed against it.

Green locally: seo-contract (9), ui-truthfulness, links, frontend-loading-
contract (7), navigation-shell (24), pricing-page (44), check-csp-inline.sh,
check-cache-bust.sh, eslint (exit 0). No horizontal overflow at 390px on any of
the three pages. index.html, apply-nav.py and js/nav-auth.js untouched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJk2nCLCLmi3F71qkCUn7N
@Apolloccrypt
Apolloccrypt force-pushed the feat/fe-product branch 2 times, most recently from 22ca883 to e990d86 Compare September 2, 2026 17:47
@Apolloccrypt Apolloccrypt changed the title Give both products a page that sells before it explains Give ParaSend a product page and put the buyer's questions first on ParaSign Sep 2, 2026
@Apolloccrypt

Copy link
Copy Markdown
Owner Author

Screenshots, 390x844, deviceScaleFactor 2

Captured with Playwright against a static server on the head of this branch.
Files are on the NUC at /tmp/claude-1000/-home-mick-vault/aaa93ca4-46a3-4e08-bb7e-b16bfb35e53c/scratchpad/shots/:

  • parasign-390-fold.png / parasign-390-full.png
  • parasend-390-fold.png / parasend-390-full.png
  • sign-390-fold.png / sign-390-full.png (the visitor block a 401 reveals is forced visible)

The y-positions those shots were measured from are in the PR body. The three the
review asked for, in pixels from the top of the document:

/parasign /parasend /sign
eIDAS / SES status 737 not applicable not applicable
audience line 432 396 not applicable
first CTA 511 500 763

On /parasign that is a move from 6,386px to 737px for the eIDAS note: from
screen 8 of 9 to the first screen, above the fold at 844px, next to the buttons.

@Apolloccrypt

Copy link
Copy Markdown
Owner Author

Round two, re-measured

The static server the earlier shots were taken against was stopped by another
build, so everything was re-measured on a fresh server (port 8842, cwd verified
against frontend/, HTTP 200 asserted per page before measuring). New shots in
/tmp/claude-1000/-home-mick-vault/aaa93ca4-46a3-4e08-bb7e-b16bfb35e53c/scratchpad/pr339-round2/shots/.

scrollWidth is still exactly 390 on all three. The three positions the review
asked about are unchanged; the two that moved are the /parasend page getting
slightly longer for the corrected tier lines.

/parasign /parasend /sign
H1 119 119 105
audience line 432 396 n/a
first CTA 511 500 763
free promise with its number 632 620 291
eIDAS / SES status 737 n/a n/a
burn-on-read warning n/a 719 n/a
first price table 2,518 4,261 n/a
page height 7,744 8,963 2,163

The /parasend hero now reads "10 transfers a month, 5 MB a file, links that last
an hour and are gone after one read", all four read from relay/lib/tiers.js by
the test rather than copied from another page.

@Apolloccrypt

Copy link
Copy Markdown
Owner Author

One more instance of the same defect, out of scope here

While checking that the new pointer is honest, the same problem turned up one
level up. frontend/pararules.html:205, directly under ParaRule 5 (EU soil, EU
law, the rule that names Bunny and Resend):

<p class="rule-verify"><a href="/security">verify: the jurisdiction table &rarr;</a></p>

frontend/index.html:382 does it too, with verify: jurisdiction pointing at
the same table. Both offer the /security Jurisdiction and privacy table as the
checkpoint for a claim that table does not carry: it lists Hetzner Nuremberg,
the legal jurisdiction, the CLOUD Act row, retention, IP logging and analytics,
and no DNS row at all.

Not touched in this PR: /pararules and index.html are outside its three pages,
and section 9 of the messaging guide already holds the /security row open for
its own PR. Flagging it so the fix lands with that one rather than getting lost.
The assertion added here fails the moment the table does start naming Bunny, so
the pointer can come back on both pages at that point.

…araSign

Three pages, one shape, taken from docs/brand/messaging.md: what you can do
here, who it is for, the split with its real number, who is behind it, then the
proof and the honest limits. The algorithm names moved to the bottom of every
page, where they are the reason a plain sentence above is true.

/parasend is new. ParaSign has had a page since #325; ParaSend had nothing, so
the send half of the product could not be found by name. It follows the
/parasign pattern the guide asks for.

What changed against the review, per page.

/parasign
- H1 is the one the guide specifies: "Sign documents in your browser, prove it
  years later." The old H1 was the product name, which does not say what the
  page is for.
- The opening paragraph says what a buyer gets, in two sentences and 160
  characters, and no longer opens on ML-DSA-65 and a transparency log. The
  guide's own tone rule keeps cryptography below the fold; the sentence it
  specifies for this page contradicted that rule, so the sentence now sits
  verbatim in the proof block at 04 instead of in the hero.
- The buyer is named in the first screen: legal, finance and healthcare
  practices in the EU, the same audience index.html states.
- The eIDAS status moved from 6,386px to 737px. A lawyer asks it first; the
  page now answers it before the tiers instead of on screen 8 of 9.
- The free promise carries its number. "Free forever" became "Free on the
  Community plan: 2 signatures a month, no card, forever."
- "Zero-knowledge" is gone from the scope note. It described nothing this page
  can point at, and it sat next to the most honest paragraph on the page.
- The proof block quotes /about word for word and carries the EU claim in the
  form #328 settled: no US provider in the data path, with Resend named in the
  same breath.

/parasend
- Section 02, the section that has to convince, said AES-256-GCM, ciphertext,
  RAM, ML-KEM-768, tenant and RAM budget. It now says what the buyer gets. The
  algorithm names are in 06, each next to the page that checks it.
- The free promise carries its limits: links that last an hour, gone after one
  read, 10 uploads an hour.
- The bridge sentence explaining that Community is listed as Free is gone, and
  the tier card is named Community. #328 renamed the tier on /pricing, so there
  is nothing left to explain away.
- The founder block quotes /about instead of composing a sentence about US
  subscriptions. #332 put that sentence on /about, so it is now sourced.

/sign
- "Private key" was the last jargon in the first screenful. Same fact, words a
  practice manager already uses.

One layout fix, page-local. At 390px the house .sec-head keeps the section
number in its own grid column, which left the H1 about 160px to wrap in: six
lines of headline on /parasign, and the buttons and the eIDAS note off the
first screen. On a narrow screen the number now stacks above the headline.
design-system.css is untouched, because it is shared with pages this PR does
not open.

Measured at 390px with Playwright, per page: no horizontal overflow anywhere
(scrollWidth 390), and on /parasign the H1, the audience, both buttons, the
free promise and the eIDAS note all land inside the first 844px.
Two gates that looked like gates and were not, both found by sabotage rather
than by reading.

1. The monthly price on both product pages was pinned by nothing.
relay/test/pricing-page.test.js asserted with a bare substring, so
includes('&euro;49') was satisfied by the annual "&euro;499 excl." standing on
the same card, and includes('&euro;15') by "&euro;150". Verified: &euro;49 to
&euro;59 on /parasign and &euro;15 to &euro;19 on /parasend both stayed green.
Those are the two numbers a buyer clicks. An amount now has to end where it
says it ends and sit on the card of the plan it belongs to; the opening line
"business plans from &euro;49 a month" can no longer stand in for the tier.
Both sabotages go red after this, and so do the annual amounts.

2. A claim that has to be read in the first screen was asserted against the
whole file, which is satisfied by screen 8 of 9. The eIDAS scope note, the
audience line and the free promise with its number are now asserted against the
hero section of the page, not against the file.

Also pinned, each one a sentence that had drifted or had no source:
- The SLA figure is read off /sla by name (the Enterprise cell, not the first
  .uptime on the page) and every page repeating it has to match. This is how
  "SLA 99.9%" reached a product page while /pricing and /sla both published
  99.95%: the quota lines were pinned, the service lines were not.
- The EU claim in the form #328 settled: "No US provider in the data path"
  with the Resend exception in the same breath, on /, /parasign and /parasend.
  The broader "no US company" wording is refused on the product pages;
  section 9 of the guide holds it open, and it stays on /security where the
  table qualifies it.
- The give-back sentence, now that #332 has put it on /about, quoted rather
  than composed.
- The free plan is called Community on /sign too, and "Free accounts" is
  refused there.

The stale string is gone as well: the suite pinned "The community tier stays
free", a sentence #328 removed from /pricing. It now pins what /pricing
actually ships.
…rints

Four corrections on /parasend and /parasign, each one a claim that was
contradicted by the source the page itself points at.

1. The free number was the wrong number. Both pages said "10 uploads an hour".
That figure is real, but it is ANON_RATE_PER_HOUR on POST /v2/anon-inbound
(relay/relay.js), an endpoint deprecated on 2026-05-28 that answers with
Deprecation and Sunset headers and retires on 2026-12-31. It is not what a
Community account gets. relay/lib/tiers.js gives the Community row 10 transfers
a month, 5 MB a file, a one hour link and one read, and relay.js enforces those
with a 402 (monthly_transfer_quota_reached) and a 413 (Max 5MB). The hero and
the Community card now state those. ParaSend Pro gains its real transfer
ceiling, 500 a month, in place of "No IP rate limit", which was the same
anon-endpoint artefact.

The same applies where /parasign quotes a ParaSend limit: the ParaSign Pro card
said "Unlimited transfers". A ParaSign Pro account derives plan_parasend 'pro'
(relay/lib/entitlements.js derivePlanParasend), which is 500 a month, and that
file has a hard rule that no metered tier is unbounded: even enterprise gets a
finite ceiling, never Infinity. The card now says 500.

/pricing and index.html carry the same upload figure. They are corrected in
their own PRs and are deliberately not touched here.

2. ParaShare has no signature, and the register the card links to says so.
/parasend promised "ML-DSA-65 signed receipts" for ParaShare and cited the
algorithm register in the same sentence. That register lists ParaShare (webapp)
with SIG n/a on the pre-v1 hybrid wire, migrating to v1; ML-DSA-65 on the v1
wire is sdk-py 3.0.0 and sdk-js 3.0.0. The card now states what the register
states. The buyer-facing half of the same claim went with it: section 02
promised the recipient "proof that the file came from you", which is a signature
claim on a path that has no signature. It now says what the path does deliver,
that the file opens on the recipient's registered device and nowhere else, and
that an altered file does not open at all.

3. The jurisdiction table does not carry Bunny. Both pages named Bunny DNS
(Slovenia) and then sent the reader to the Jurisdiction and privacy table on
/security to check it. That table lists Hetzner Nuremberg, the legal
jurisdiction, the CLOUD Act row, retention, IP logging and analytics. DNS is not
a row in it. The sentence stays exactly as index.html carries it; the pointer
now goes to /privacy and to ParaRule 5, which do carry it.

4. "Fixed-size ciphertext" is a ParaShare property, not a property of every
transfer. /security records "approximate encrypted size (up to 5 MB; exact 5 MB
for ParaShare)". Bounded to the path it holds on. And the Pro list was missing
"Email notifications via Resend", which /pricing carries and which names a
subprocessor, so its absence quietly dropped a disclosure.

The pins moved with the claims. The limit lines are read from relay/lib/tiers.js
instead of compared against the wording on /pricing: comparing two pages only
proves they agree, which is exactly the failure here, since /pricing and
index.html were wrong in the same way. The uploads-per-hour figure and
"unlimited transfers" are now refused on both product pages under any wording.
The register is parsed and the page held to the row it cites. The EU sentence is
pinned to index.html, where it ships, and an assertion fails if the /security
table ever starts naming Bunny, at which point the pointer may come back.

Seventeen sabotages, all red, including edits to relay/lib/tiers.js itself and
to the register row on /crypto-agility.
#334 tightened the head contract: the JSON-LD WebPage name has to be the page
title character for character, and every public page has to carry an
Organization node naming the company and the founder. /parasend was written
before that landed, so on the merged result tests/seo-contract.test.mjs failed
twice: the WebPage name used a hyphen where the title uses a middot, and there
was no Organization node at all.

The block comes from bron-seo/apply_seo_head.py, not from hand editing, so it
is the same graph the other 39 pages carry and it stays that way the next time
the script runs. `--check` is clean afterwards.

Found by rebasing on main and running the suite against the merged tree. A
green run on the branch head only describes the branch head, which is how this
would have reached main red.
@Apolloccrypt

Copy link
Copy Markdown
Owner Author

#354 fixed three of the four pages, and its guard does not cover the fourth

#354 landed while this PR was rebasing, and it corrects the same defect reported
here: the Community upload figure is ANON_RATE_PER_HOUR on the deprecated
/v2/anon-inbound, not a plan limit. It changed frontend/index.html,
frontend/docs.html and frontend/docs/api.md, and added a guard to
tests/site-claims.test.mjs.

frontend/pricing.html:281 still says:

<li>10 uploads per hour per IP</li>

That is the ParaSend Community card on the page that sells the plan. Verified on
the current main merged with this branch: node --test tests/site-claims.test.mjs
passes with that line in place, so the new guard does not reach /pricing.

So the state after #354 is: /, /docs and /docs/api.md say 10 transfers a month
and 5 MB per file; /parasign and /parasend say the same after this PR; /pricing
still says 10 uploads per hour per IP.

Not fixed here on purpose. /pricing is outside this PR's three pages, and the
reviewers have checked that boundary on every round. Extending the refusal in
relay/test/pricing-page.test.js to /pricing would turn this PR red on someone
else's page, which is not a gate this PR should own. Flagging it for #336 or a
follow-up, with the one-line change and the fact that the existing guard will
not catch it.

@Apolloccrypt
Apolloccrypt merged commit 3f6981e into main Sep 2, 2026
11 checks passed
Apolloccrypt added a commit that referenced this pull request Sep 2, 2026
The three pages a buyer reaches after the homepage were written for someone
who already believes the product. /about opened on the mission and put the
founder in section 03, below the cryptography. /security opened on defence in
depth and kept the one row a buyer came for, the jurisdiction table, seven
screens down. /trust never said who it was for.

They now run the order docs/brand/messaging.md fixes: what this is, who it is
for, who is behind it, a next step, then the proof including the honest
limits. Copy, hierarchy and order only. No restyle. index.html, apply-nav.py
and js/nav-auth.js are untouched.

/about is the founder page.

  The lede is plain language: sign and send documents so only you and the
  recipient can read them, so anyone can check later that the document is
  genuine, on servers in Germany under EU law. Under it, in the first phone
  screen, who it is for, then Mick Beer with the title the guide fixes, then
  two buttons. Measured at 390x844: the founder sits at y=463 and the second
  button at y=608. The section number "00" is hidden in the mobile override
  instead of landing under the H1 as a stray number.

  Two sentences are gone. "The cryptography is post-quantum, which is the
  proof that it still holds up in ten years" was not a proof and not
  checkable, on the page whose argument is that everything on it is
  checkable. The founder paragraph explained the free plan with a
  jurisdiction claim ("should not depend on a US subscription"), which the
  guide forbids beside his name. In its place stands the paragraph #332
  landed on main while this branch was in review: the Community plan is his
  way of giving something back to society, the business plans pay for it,
  that is the whole arrangement, and it is why the Community plan is not a
  trial and has no end date. Taken from main verbatim, moved with the section
  into the top half, and pinned so the two copies cannot drift.

  The tier block names the free plan Community, which is what /pricing prints
  on the card since #328.

/security answers "why would I trust you" before it answers "how it works".

  The promise carries its own scope. It read "even if our own server is
  broken into, nobody can read your documents" flat out, while ten screens
  lower the page says the Chromium and Outlook extensions take a server-side
  encryption path. For an extension user the flat version is untrue today, so
  the exception now travels with the promise, in the hero.

  It also says what that exception costs the reader, in words rather than in
  ours: the extensions encrypt on our server, which means we can read what
  you upload through them until that is changed. "Treat those uploads as
  relay-side" was the internal phrasing, and relay is exactly the word this
  branch removed from the /trust hero. Neither hero uses it now, and a test
  says so.

  The first screen also carries who is behind the page (Paramantis Solutions
  B.V., Harderwijk, KvK 42115132, and the founder line) and the next step.
  The buttons used to sit at roughly y=11400 on a phone; "See pricing" is now
  at y=656 and "Verify a document" at y=716.

  The free plan is Community here too. A first version of this branch left
  "ParaSign Free and ParaSend Free cost EUR 0" standing at r.448, because the
  only check that forbade the old name was scoped to /about. That check now
  covers all three pages, and the sitewide sweep in ui-truthfulness gained the
  shape it was missing, so any page using it fails. The page description no
  longer sells "relay architecture" either.

  The audit block says what /docs#audits actually adds up to: three external
  audits in April 2026, two by R. Zwarts and one by Ryan Williams of Smart
  Cyber Solutions, forty findings, four of them critical, with the resolving
  commits in the table. The previous round claimed "the audit reports
  themselves are not published" and pinned it. That was false:
  docs/security-audit-2026-04.md is the full Smart Cyber Solutions writeup
  and it ships in the site tree. Only the raw pentest output is missing,
  which is what that document itself calls the raw report. Both pages now say
  that, and link the report.

  The CLOUD Act row and the EU-law card move to the data-path wording, as
  section 9.2 of the guide requires: no US provider in the data path, with
  Resend named as the one exception in the same breath. The old row read "not
  applicable: no US infrastructure, no US company", which is broader than
  /privacy allows.

/trust names its reader, then gives that reader somewhere to go.

  The hero addresses organisations running their own relay and anyone
  checking a supplier, and now offers them two buttons instead of one text
  link mid-paragraph (y=416 and y=476). The first sentence under the hero no
  longer says "the operator who runs the relay": relay is not a word a
  supplier reviewer knows.

  The page called itself Trust & Verification in its title and H1 while the
  social card and the structured data still said Trust & Transparency. #334
  then rewrote every head on the site, so the literal strings belong there and
  tests/seo-contract.test.mjs pins them. What this branch pins is the
  relation: the title, og:title, twitter:title and the JSON-LD name must name
  the page the same, and that name must contain the words the H1 uses.
  Punctuation and case are not the point; Transparency versus Verification
  was. Its plan sentence named two of the three paid
  ParaSign tiers; it names all three, and both free tiers as Community.

Tests

  tests/ui-truthfulness.test.mjs pins each of the above, and each one was
  sabotaged in place to confirm it goes red: the ten-year promise, the
  give-back sentence, the US-subscription framing, the Community rename, the
  CLOUD Act row, the Resend exception dropped from the card, the finding
  counts, the "reports not published" sentence, the auditor names on
  /security as well as /trust, the two hero buttons on each page, the
  who-is-behind line, the bounded promise, the /trust og:title and JSON-LD
  name, the jargon in the first screen, and the paid tiers. Twenty-six
  sabotages, twenty-six red, no gaps.

  tests/site-claims.test.mjs block 11 reads the numbers out of the /pricing
  tier card that owns them and requires /about to repeat them; it follows the
  Community rename and now also forbids "ParaSign Free" on /about.

  node --test over the CI glob: 162 pass, 0 fail. check-csp-inline,
  check-cache-bust and eslint exit 0. At 390px all three pages have
  scrollWidth === clientWidth === 390.

Two things this branch had to touch on main's side

  tests/ui-truthfulness.test.mjs did not parse on main: #336 and #339 each
  landed a const named pricingVisible in the same module, for two different
  values. The one #339 added is renamed to pricingText, with a comment saying
  why. Without it nothing in this file runs, this branch included.

  tests/site-claims.test.mjs block 12 read the /pricing tier cards by their
  section heading. #336 renamed both headings and put ParaSign first, so the
  split now finds them by product prefix and orders them by position.

  Not touched, and still broken on main: relay/test/pricing-page.test.js r.498
  declares tiers twice, so eslint cannot parse it. It is outside these three
  pages and outside this PR.

Still open, deliberately

  The eIDAS level (SES on /about, advanced (AES) in the /pricing FAQ) is a
  real contradiction and needs a decision, not a copy edit. This branch stops
  it spreading and pins that it stays on the one page that has always carried
  it.
Apolloccrypt added a commit that referenced this pull request Sep 2, 2026
The three pages a buyer reaches after the homepage were written for someone
who already believes the product. /about opened on the mission and put the
founder in section 03, below the cryptography. /security opened on defence in
depth and kept the one row a buyer came for, the jurisdiction table, seven
screens down. /trust never said who it was for.

They now run the order docs/brand/messaging.md fixes: what this is, who it is
for, who is behind it, a next step, then the proof including the honest
limits. Copy, hierarchy and order only. No restyle. index.html, apply-nav.py
and js/nav-auth.js are untouched.

/about is the founder page.

  The lede is plain language: sign and send documents so only you and the
  recipient can read them, so anyone can check later that the document is
  genuine, on servers in Germany under EU law. Under it, in the first phone
  screen, who it is for, then Mick Beer with the title the guide fixes, then
  two buttons. Measured at 390x844: the founder sits at y=463 and the second
  button at y=608. The section number "00" is hidden in the mobile override
  instead of landing under the H1 as a stray number.

  Two sentences are gone. "The cryptography is post-quantum, which is the
  proof that it still holds up in ten years" was not a proof and not
  checkable, on the page whose argument is that everything on it is
  checkable. The founder paragraph explained the free plan with a
  jurisdiction claim ("should not depend on a US subscription"), which the
  guide forbids beside his name. In its place stands the paragraph #332
  landed on main while this branch was in review: the Community plan is his
  way of giving something back to society, the business plans pay for it,
  that is the whole arrangement, and it is why the Community plan is not a
  trial and has no end date. Taken from main verbatim, moved with the section
  into the top half, and pinned so the two copies cannot drift.

  The tier block names the free plan Community, which is what /pricing prints
  on the card since #328.

/security answers "why would I trust you" before it answers "how it works".

  The promise carries its own scope. It read "even if our own server is
  broken into, nobody can read your documents" flat out, while ten screens
  lower the page says the Chromium and Outlook extensions take a server-side
  encryption path. For an extension user the flat version is untrue today, so
  the exception now travels with the promise, in the hero.

  It also says what that exception costs the reader, in words rather than in
  ours: the extensions encrypt on our server, which means we can read what
  you upload through them until that is changed. "Treat those uploads as
  relay-side" was the internal phrasing, and relay is exactly the word this
  branch removed from the /trust hero. Neither hero uses it now, and a test
  says so.

  The first screen also carries who is behind the page (Paramantis Solutions
  B.V., Harderwijk, KvK 42115132, and the founder line) and the next step.
  The buttons used to sit at roughly y=11400 on a phone; "See pricing" is now
  at y=656 and "Verify a document" at y=716.

  The free plan is Community here too. A first version of this branch left
  "ParaSign Free and ParaSend Free cost EUR 0" standing at r.448, because the
  only check that forbade the old name was scoped to /about. That check now
  covers all three pages, and the sitewide sweep in ui-truthfulness gained the
  shape it was missing, so any page using it fails. The page description no
  longer sells "relay architecture" either.

  The audit block says what /docs#audits actually adds up to: three external
  audits in April 2026, two by R. Zwarts and one by Ryan Williams of Smart
  Cyber Solutions, forty findings, four of them critical, with the resolving
  commits in the table. The previous round claimed "the audit reports
  themselves are not published" and pinned it. That was false:
  docs/security-audit-2026-04.md is the full Smart Cyber Solutions writeup
  and it ships in the site tree. Only the raw pentest output is missing,
  which is what that document itself calls the raw report. Both pages now say
  that, and link the report.

  The CLOUD Act row and the EU-law card move to the data-path wording, as
  section 9.2 of the guide requires: no US provider in the data path, with
  Resend named as the one exception in the same breath. The old row read "not
  applicable: no US infrastructure, no US company", which is broader than
  /privacy allows.

/trust names its reader, then gives that reader somewhere to go.

  The hero addresses organisations running their own relay and anyone
  checking a supplier, and now offers them two buttons instead of one text
  link mid-paragraph (y=416 and y=476). The first sentence under the hero no
  longer says "the operator who runs the relay": relay is not a word a
  supplier reviewer knows.

  The page called itself Trust & Verification in its title and H1 while the
  social card and the structured data still said Trust & Transparency. #334
  then rewrote every head on the site, so the literal strings belong there and
  tests/seo-contract.test.mjs pins them. What this branch pins is the
  relation: the title, og:title, twitter:title and the JSON-LD name must name
  the page the same, and that name must contain the words the H1 uses.
  Punctuation and case are not the point; Transparency versus Verification
  was. Its plan sentence named two of the three paid
  ParaSign tiers; it names all three, and both free tiers as Community.

Tests

  tests/ui-truthfulness.test.mjs pins each of the above, and each one was
  sabotaged in place to confirm it goes red: the ten-year promise, the
  give-back sentence, the US-subscription framing, the Community rename, the
  CLOUD Act row, the Resend exception dropped from the card, the finding
  counts, the "reports not published" sentence, the auditor names on
  /security as well as /trust, the two hero buttons on each page, the
  who-is-behind line, the bounded promise, the /trust og:title and JSON-LD
  name, the jargon in the first screen, and the paid tiers. Twenty-six
  sabotages, twenty-six red, no gaps.

  tests/site-claims.test.mjs block 11 reads the numbers out of the /pricing
  tier card that owns them and requires /about to repeat them; it follows the
  Community rename and now also forbids "ParaSign Free" on /about.

  node --test over the CI glob: 162 pass, 0 fail. check-csp-inline,
  check-cache-bust and eslint exit 0. At 390px all three pages have
  scrollWidth === clientWidth === 390.

Three things this branch had to touch on main's side

  tests/ui-truthfulness.test.mjs did not parse on main: #336 and #339 each
  landed a const named pricingVisible in the same module, for two different
  values. The one #339 added is renamed to pricingText, with a comment saying
  why. Without it nothing in this file runs, this branch included.

  tests/site-claims.test.mjs block 12 read the /pricing tier cards by their
  section heading. #336 renamed both headings and put ParaSign first, so the
  split now finds them by product prefix and orders them by position.

  relay/test/pricing-page.test.js r.498 required ../lib/tiers a second time,
  under a name the file already binds at r.18. That is a SyntaxError, and it
  took down two CI checks on every PR: "static, every name must exist" (eslint
  cannot parse the file) and "relay - unit suite". The duplicate line is
  removed; the binding at the top of the file is the same module. This was
  outside the three pages, but no PR can go green while it stands.

Still open, deliberately

  The eIDAS level (SES on /about, advanced (AES) in the /pricing FAQ) is a
  real contradiction and needs a decision, not a copy edit. This branch stops
  it spreading and pins that it stays on the one page that has always carried
  it.
Apolloccrypt added a commit that referenced this pull request Sep 2, 2026
…ly form

/parasend had no inbound link anywhere on the site and /parasign had one, from
/sign. Both product cards on the homepage now lead with the page that explains
the product and keep the app as the second action. tests/links.test.mjs gains a
reachability check (a page nothing links to is indexed and unreachable at the
same time) and tests/navigation-shell.test.mjs pins the order per card.

outbound_per_hour has been enforced since the rate-limit finding (relay.js,
outboundRateOk on GET /v2/outbound/:hash) and appeared on no page: a Community
account scripting its own downloads hit a 429 it was never told about. It is now
on /parasend per tier and on the /parasign Pro card, pinned to tiers.js like the
other limits. It is stated as retrievals, not sends: it counts the account's own
fetches with its own key, and the browser recipient path (GET /v2/dl/:token/get)
has no rate limit at all, so a recipient never spends the sender's hour.

"Unlimited receiving" pointed at no field in tiers.js or entitlements.js. It is
true: nothing meters receiving, and being invited, opening an invitation and
fetching the document back are not counted on any plan. The line now reads "No
limit on receiving", /parasign says what it rests on and that signing what you
receive still counts, and pricing-page.test.js pins it negatively: the day a
receiving dimension appears in tiers.js or in the entitlement quotas, the test
goes red and the pages have to name the real ceiling.

The same Community limit shipped as "10 transfers a month" on / and /docs and
"10 transfers per month" on /pricing, /parasend and /parasign. The site's form
is "a month" everywhere now. The number checks accept both spellings on purpose,
so a tiers.js change fails on the number; the wording is pinned once, so a page
that drifts back fails on wording.

Two test files could not parse on main. #336 and #339 each added a
"const tiers = require('../lib/tiers')" to relay/test/pricing-page.test.js and a
"const pricingVisible" to tests/ui-truthfulness.test.mjs; both merged, both files
threw SyntaxError, and neither suite ran. Deduplicated as a one-line change per
file, so a hotfix that does the same rebases cleanly on top.

So that this cannot recur through this branch: every block added here sits in a
function scope and reaches the top level with nothing. The two helpers that are
used from two distant places are function declarations, which tolerate being
declared twice where a const throws.

Also in ui-truthfulness: the ParaShare register assertions sat inside
"if (shareSig === 'n/a')", so moving the webapp to ML-DSA-65 in the register
switched the check off instead of failing it. They now run on whatever the SIG
column holds, in both directions, and an unmapped value fails loudly.
@Apolloccrypt
Apolloccrypt deleted the feat/fe-product branch September 5, 2026 18:56
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