Link the product pages, state the hourly ceiling, and write one monthly form - #359
Conversation
…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.
4e58af6 to
b762eec
Compare
|
Note on the parse breakage on main, since a hotfix is on its way for the same two lines. This branch hit both before the hotfix existed and carries a fix for each, deliberately kept to the smallest possible edit so it rebases cleanly on top of one:
Happy to drop both hunks and rebase instead once the hotfix is in, whichever is less work to land. Separately, nothing this PR adds can cause the same class of breakage again. Every new block sits in a function scope, and the two helpers used from two distant places are function declarations rather than consts: a duplicate |
docs/ONBOARDING.md, every step executed in a clean clone on origin/main before it was written down. What Paramant is in five sentences out of docs/brand/messaging.md, the repo map with the test or workflow that guards each directory, local setup with the commands CI runs and the counts they printed, the rules of the house with the gate that enforces each, pointers to docs/RELEASE.md and deploy/DEPLOY-3.1.md rather than a second copy of them, how the two heartbeats and signalen.py differ, where production runs and who holds the key, and the traps of the first week. The first-week list includes the one the clean run walked into. Two pull requests appended a top-level const to the end of the same long test file, each was green against the base it was cut from, and the merge of both was a SyntaxError that fails to load the file rather than failing a check. eslint carries one rule here, no-undef, so it does not catch a redeclaration either. #359 fixed the two files; the document names the three most exposed and says to rebase and rerun before asking for a merge. README and docs/PROJECT-STATUS.md point at the new document.
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-seven
sabotages, twenty-seven 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: 173 pass, 0 fail. check-csp-inline,
check-cache-bust and eslint exit 0, bron-seo/apply_seo_head.py --check
reports 0 pages, and tests/static-sanity.sh is PASS on all eleven checks,
including #362's new test-scope guard. At 390px all three pages have
scrollWidth === clientWidth === 390.
Following main through four parallel merges
#359 landed the deduplication this branch was carrying (the second const
tiers in relay/test/pricing-page.test.js, and pricingVisible renamed to
pricingText), so both local fixes are dropped in favour of main's.
This branch's own block in tests/ui-truthfulness.test.mjs now sits inside a
bare block scope and declares nothing at module level. Four PRs merged into
that file in parallel on 2 September and two of them collided on a top-level
const, which is a SyntaxError: not one assertion in the file runs, on any
branch. A block that declares nothing at module level cannot do that to the
next branch.
tests/site-claims.test.mjs block 12 stopped spelling the units itself. #336
renamed both /pricing section headings and put ParaSign first, and #359
reworded "2 signatures per month" to "2 signatures a month". The block now
finds the headings by product prefix, orders them by position, and lifts each
fact out of the card as a whole phrase, so /about repeats what /pricing says
rather than what this file guesses /pricing says. /about follows the new
wording: "2 signatures a month", "24 hour link expiry", "up to 10 reads per
link".
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.
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-seven
sabotages, twenty-seven 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: 173 pass, 0 fail. check-csp-inline,
check-cache-bust and eslint exit 0, bron-seo/apply_seo_head.py --check
reports 0 pages, and tests/static-sanity.sh is PASS on all eleven checks,
including #362's new test-scope guard. At 390px all three pages have
scrollWidth === clientWidth === 390.
Following main through four parallel merges
#359 landed the deduplication this branch was carrying (the second const
tiers in relay/test/pricing-page.test.js, and pricingVisible renamed to
pricingText), so both local fixes are dropped in favour of main's.
This branch's own block in tests/ui-truthfulness.test.mjs now sits inside a
bare block scope and declares nothing at module level. Four PRs merged into
that file in parallel on 2 September and two of them collided on a top-level
const, which is a SyntaxError: not one assertion in the file runs, on any
branch. A block that declares nothing at module level cannot do that to the
next branch.
tests/site-claims.test.mjs block 12 stopped spelling the units itself. #336
renamed both /pricing section headings and put ParaSign first, and #359
reworded "2 signatures per month" to "2 signatures a month". The block now
finds the headings by product prefix, orders them by position, and lifts each
fact out of the card as a whole phrase, so /about repeats what /pricing says
rather than what this file guesses /pricing says. /about follows the new
wording: "2 signatures a month", "24 hour link expiry", "up to 10 reads per
link".
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.
Four truthfulness items after #339 and #336 merged, plus two files that stopped parsing when they did.
1. /parasend was unreachable, /parasign nearly
/parasendhad zero inbound links anywhere on the site./parasignhad one, from/sign. Both are in the sitemap, so they were indexed and unreachable at the same time.Both homepage product cards now lead with the page that explains the product and keep the app as the second action:
/sign/parasign, then/sign/parashare/parasend, then/parasharePinned twice.
tests/links.test.mjsgains a reachability test: the existing check proves a link leads somewhere, this proves a page is arrived at.tests/navigation-shell.test.mjspins the CTA order per card in the rendered page.2. outbound_per_hour was enforced and stated nowhere
relay/lib/tiers.jscarriesoutbound_per_hour(Community 50, Pro 500, Business 2000, Enterprise unlimited) andrelay/relay.jsenforces it inoutboundRateOk, applied onGET /v2/outbound/:hash(relay.js:1627-1636, 4692). No page said so, so a Community account scripting its own downloads met a 429 it had never been told about.It is now on
/parasendper tier and on the/parasignPro card, pinned to tiers.js like transfers, file size, expiry and devices.It is stated as retrievals, not sends.
outboundRateOkcounts the account's own fetches with its own key:entry.apiKey !== apiKeyreturns 403, so the caller must be the uploader. The browser recipient path isGET /v2/dl/:token/get(relay.js:4004-4062), which has no rate limit at all, at the application layer or indeploy/nginx-paramant-public.conf. A recipient opening a link never spends the sender's hour.pricing-page.test.jsbanssends an houranduploads an houron both product pages for that reason.3. "Unlimited receiving" pointed at no field
It pointed at nothing in
tiers.jsorentitlements.js, and it is true: no per-account quota meters receiving. Being invited, opening an invitation and fetching the document capsule back are gated only by a 30/min per-IP window (envViewRateOk), and there is no index of envelopes an account was invited to, so there is nothing to cap. Putting your own signature on one does count, againstsigns_month.The line now reads No limit on receiving on
/parasign,/pricingand the homepage, and/parasignsays what it rests on and names the one thing that is still counted.Pinned negatively:
pricing-page.test.jsfails if any row intiers.TIER_LIMITSor any entitlement quota block gains a key matching/receiv|inbound/i. The day receiving gets metered, the pages have to name the real ceiling.frontend/signup.html's meta descriptions and JSON-LD still say "unlimited receiving". Head elements are left alone here; #335 is open.4. One spelling for the monthly period
The same Community limit shipped as
10 transfers a monthon/and/docs(#354) and10 transfers per monthon/pricing,/parasendand/parasign. Both true, which is what makes it a problem: a buyer comparing two pages has to work out whether that is one allowance or two.The site's form is
a month, on pages, infrontend/js/quota-upgrade.js, and indocs/brand/messaging.md.frontend/vs.htmlis untouched: that figure is WeTransfer's own plan, not ours.The two failures are kept apart on purpose. The number checks accept either spelling, so a
tiers.jschange fails on the number whatever the wording is that day. The wording is pinned once, inMONTHLY_FORM, so a page that drifts back fails on wording and not on a limit that is still correct.5. Two test files could not parse on main
#336and#339each added a declaration under the same name in the same scope, and both merged:relay/test/pricing-page.test.js: twoconst tiers = require('../lib/tiers')tests/ui-truthfulness.test.mjs: twoconst pricingVisibleBoth threw
SyntaxError: Identifier ... has already been declaredonorigin/mainand neither suite ran. Deduplicated; the secondpricingVisibleis a different value (tags stripped) and is renamedpricingTextrather than merged.6. ui-truthfulness: the register guard switched itself off
From the #339 end review. The ParaShare register assertions sat inside
if (shareSig === 'n/a'), so moving the webapp to ML-DSA-65 incrypto-agility.htmlwould stop the guard running and leave/parasendsaying "signature n/a" with nothing red.They now run on whatever the SIG column holds, in both directions: the register's value has to be what
/parasendand/pricingsay, no page may carry the wording for a value the register does not give, and a SIG the test has no wording for fails loudly instead of passing quietly.Tests
All green, and every new pin verified by sabotage:
links,seo-contract,ui-truthfulness,site-claims,pricing-page(46 checks),pricing-fold,frontend-loading-contract,navigation-shell(26 checks),csp-inline,cache-bust,eslint,static-sanity(10 green).tiers.jscommunityoutbound_per_hour50 to 60tiers.jsgainsinbound_per_hour/parasendback to "per month"/parasendsays "sends an hour"/parasigndrops the receiving line/parasigndrops the hourly line/parasendlinklinksand innavigation-shell/parasendand/pricingpricing.html,about.html,security.htmlandtrust.html: #336 has merged sopricing.htmlis edited here;about,securityandtrustare untouched for #335. No head element changed.