Skip to content

Link the product pages, state the hourly ceiling, and write one monthly form - #359

Merged
Apolloccrypt merged 1 commit into
mainfrom
fix/parasend-links-and-limits
Sep 2, 2026
Merged

Link the product pages, state the hourly ceiling, and write one monthly form#359
Apolloccrypt merged 1 commit into
mainfrom
fix/parasend-links-and-limits

Conversation

@Apolloccrypt

Copy link
Copy Markdown
Owner

Four truthfulness items after #339 and #336 merged, plus two files that stopped parsing when they did.

1. /parasend was unreachable, /parasign nearly

/parasend had zero inbound links anywhere on the site. /parasign had 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:

card was now
ParaSign /sign /parasign, then /sign
ParaSend /parashare /parasend, then /parashare

Pinned twice. tests/links.test.mjs gains a reachability test: the existing check proves a link leads somewhere, this proves a page is arrived at. tests/navigation-shell.test.mjs pins the CTA order per card in the rendered page.

2. outbound_per_hour was enforced and stated nowhere

relay/lib/tiers.js carries outbound_per_hour (Community 50, Pro 500, Business 2000, Enterprise unlimited) and relay/relay.js enforces it in outboundRateOk, applied on GET /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 /parasend per tier and on the /parasign Pro card, pinned to tiers.js like transfers, file size, expiry and devices.

It is stated as retrievals, not sends. outboundRateOk counts the account's own fetches with its own key: entry.apiKey !== apiKey returns 403, so the caller must be the uploader. The browser recipient path is GET /v2/dl/:token/get (relay.js:4004-4062), which has no rate limit at all, at the application layer or in deploy/nginx-paramant-public.conf. A recipient opening a link never spends the sender's hour. pricing-page.test.js bans sends an hour and uploads an hour on both product pages for that reason.

3. "Unlimited receiving" pointed at no field

It pointed at nothing in tiers.js or entitlements.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, against signs_month.

The line now reads No limit on receiving on /parasign, /pricing and the homepage, and /parasign says what it rests on and names the one thing that is still counted.

Pinned negatively: pricing-page.test.js fails if any row in tiers.TIER_LIMITS or 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 month on / and /docs (#354) and 10 transfers per month on /pricing, /parasend and /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, in frontend/js/quota-upgrade.js, and in docs/brand/messaging.md. frontend/vs.html is 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.js change fails on the number whatever the wording is that day. The wording is pinned once, in MONTHLY_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

#336 and #339 each added a declaration under the same name in the same scope, and both merged:

  • relay/test/pricing-page.test.js: two const tiers = require('../lib/tiers')
  • tests/ui-truthfulness.test.mjs: two const pricingVisible

Both threw SyntaxError: Identifier ... has already been declared on origin/main and neither suite ran. Deduplicated; the second pricingVisible is a different value (tags stripped) and is renamed pricingText rather 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 in crypto-agility.html would stop the guard running and leave /parasend saying "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 /parasend and /pricing say, 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).

sabotage result
tiers.js community outbound_per_hour 50 to 60 red on the number
tiers.js gains inbound_per_hour red on "No limit on receiving"
/parasend back to "per month" red on wording
/parasend says "sends an hour" red on the send/retrieve distinction
/parasign drops the receiving line red
/parasign drops the hourly line red
homepage drops the /parasend link red in links and in navigation-shell
register gives ParaShare ML-DSA-65 red on /parasend and /pricing
register gives ParaShare an unmapped SIG red, with what to update

pricing.html, about.html, security.html and trust.html: #336 has merged so pricing.html is edited here; about, security and trust are untouched for #335. No head element changed.

…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 force-pushed the fix/parasend-links-and-limits branch from 4e58af6 to b762eec Compare September 2, 2026 18:44
@Apolloccrypt

Copy link
Copy Markdown
Owner Author

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:

  • relay/test/pricing-page.test.js: the second const tiers = require('../lib/tiers') is deleted, nothing else on that line's neighbours. An identical deletion in the hotfix merges without a conflict.
  • tests/ui-truthfulness.test.mjs: the second const pricingVisible is renamed pricingText, because the two are genuinely different values (one is the markup with comments stripped, the other the same page as plain text) and merging them would change what the older assertions read. If the hotfix picks a different name, that is a one-line conflict.

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 function declaration is legal in a strict CommonJS script, a duplicate const throws, which is the difference that took both files down. The only new top-level name on the branch is pricingText, and that is the dedupe itself.

@Apolloccrypt
Apolloccrypt merged commit faf599e into main Sep 2, 2026
11 checks passed
Apolloccrypt added a commit that referenced this pull request Sep 2, 2026
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.
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-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.
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-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.
@Apolloccrypt
Apolloccrypt deleted the fix/parasend-links-and-limits branch September 5, 2026 18:57
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