Skip to content

feat(website): fetch cryptify's upload limits instead of baking them - #271

Merged
rubenhensen merged 2 commits into
mainfrom
feat/267-fetch-upload-limits
Sep 1, 2026
Merged

feat(website): fetch cryptify's upload limits instead of baking them#271
rubenhensen merged 2 commits into
mainfrom
feat/267-fetch-upload-limits

Conversation

@dobby-coder

@dobby-coder dobby-coder Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Closes #267.

apps/website held its own copy of two numbers cryptify enforces (VITE_MAX_UPLOAD_SIZE, VITE_ROLLING_LIMIT), plus a third in localUsage.ts's WINDOW_MS. They drove the dropzone cap, the "X GB remaining" line and the send gate, so correcting one meant a rebuild and a redeploy. All three now come from GET /limits, which merged as postguard#392 on 2026-08-31.

What changed

  • New src/lib/limits.ts. A store carrying loading / failed / ready, loaded by uploadLimits.load(FILEHOST_URL) from the compose screen's onMount. window_days is converted to windowMs on the way in; the two byte figures are used as served.
  • VITE_MAX_UPLOAD_SIZE and VITE_ROLLING_LIMIT are gone from .env, .env.dev, src/lib/env.ts and the README table.
  • localUsage.ts takes the window and the rolling limit from its callers instead of holding copies.
  • usage.ts's parseLimitExceededBody() takes the rolling limit as an argument. That fallback stays, per amendment item 2: it runs after a server response, so it cannot mask a failed fetch.
  • SendButton gates canEncrypt on the limits being known and shows a banner when the fetch failed. getValidationErrors() gained a matching entry, without which a click on the aria-disabled button fell straight through into a Yivi session that could never complete.
  • FileInput hides the size line while the limits are unknown rather than printing a number it does not have.

Fail closed

There is no default anywhere in the new path: no limits, no send. The rationale is a comment on the store. The fetch goes to the same host the upload needs, so a failure means the upload was never going to work, and a baked fallback is only ever read when the fetch failed, which is the one moment nobody can tell it is stale.

Two things the issue did not cover

A third GiB figure. Amendment item 6 names SendButton.svelte:194. FileInput.svelte:258 computes the same overLimitText value the same way, so both are decimal now.

maxFileSizeMB cannot become decimal. Item 5 asks for MAX_UPLOAD_SIZE / (1024 * 1024) to be made decimal, but that number is handed to Dropzone, which checks file.size > maxFilesize * 1048576 (@deltablot/dropzone/dist/dropzone.js:1479). MiB is its unit, not a display choice. Dividing by 1e6 would raise the cap it enforces from 5 GB to about 5.24 GB, so the dropzone would start accepting files cryptify refuses, which is the failure the issue is trying to close. The conversion now happens only at that boundary, with a comment saying why, and the cap equals the served byte figure exactly. The figures that really were in the wrong unit are the two GiB "over the limit" strings; those are decimal now, so the 7% disagreement is gone.

Tests

  • src/lib/limits.test.ts, vitest, a plain-TS module test as the issue asks. It pins the fail-closed branch: no limits before the fetch answers, and none after an unreachable host, a non-2xx, or a body missing a field. Each case asserts the resulting state has no limits in it at all, so putting a default back fails the test rather than passing it.
  • tests/upload-limits-fail-closed.test.ts, playwright. With /limits serving 503, an otherwise valid message keeps aria-disabled="true", the banner is on screen, and the validation modal names the reason. With the limits served, the same message sends and the size line reads 4.00 GB.
  • yivi-disclosure-recovery and scan-instruction-yivi-logo now mock GET /limits. Both drive the form to an enabled send button, and there is no cryptify behind the preview server.

The gate from the issue prints nothing:

grep -rn '5000000000\|MAX_UPLOAD_SIZE\|ROLLING_LIMIT' apps/website --include='*.ts' --include='*.svelte' --include='.env*'

pnpm -r test, pnpm -r typecheck, pnpm --filter postguard-website lint, lint:css and test:e2e (44 passed) all pass locally. I also ran the built site under vite preview against a stubbed /limits and eyeballed both states.

Out of scope

effectiveLimit still subtracts the per-browser localStorage figure, so the limit is authoritative but "X GB remaining" is still an estimate, exactly as the issue says.

Reviewed by dobby: 1 binding rule (code-comments) across all 19 changed files, plus the correctness/UX findings from review — pnpm -r test, pnpm -r typecheck, website lint, lint:css and 44 Playwright e2e all pass — approve. Two findings are fixed in 864769f: the validation modal reported "could not be loaded" while the fetch was still in flight (loading and failed both fall through !limitsKnown), now split by a new validation.limitsLoading string in both locales; and the six new locale keys were indented 12 spaces where their siblings use 16, which pnpm lint cannot catch because .prettierignore lists src/lib/locales/.
Attention: apps/website/src/lib/components/filesharing/SendButton.svelte:90canEncrypt gates on perUploadBytes alone while getValidationErrors() gates on effectiveLimitBytes(), so once the rolling window is exhausted the send button still renders aria-disabled="false" and only the click-time modal blocks it; pre-existing on main (same mismatch at 456d8ce) and left unfixed because closing it changes send-button behaviour this PR is not about.

`VITE_MAX_UPLOAD_SIZE` and `VITE_ROLLING_LIMIT` were source-controlled
copies of two numbers cryptify enforces, and `localUsage.ts` carried a
third in `WINDOW_MS`. All three now come from cryptify's `GET /limits`
(encryption4all/postguard#386) when the compose screen loads, so
correcting one no longer means rebuilding and redeploying the site.

The fetch fails closed. Until the limits arrive the send button stays
disabled, and a failed call surfaces a banner rather than falling back
to a default: the fetch targets the same host the upload needs, and a
stale fallback would only ever be read when the fetch failed.

Sizes are decimal throughout now, against the decimal bytes cryptify
serves. The two "over the limit" figures were computed in GiB next to a
limit shown in GB, so they disagreed by about 7%. Dropzone's own
`maxFilesize` stays in MiB because that is the unit it compares in; the
conversion happens only at that boundary, so the cap it enforces is
exactly the byte figure served.

Closes #267

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dobby-coder
dobby-coder Bot requested a review from rubenhensen August 31, 2026 12:43
The validation modal branched on `!limitsKnown(...)`, which is true for
`loading` as well as `failed`, so a click while the fetch was still in
flight reported "The upload limits could not be loaded" — a failure that
had not happened. On a slow connection that names the wrong cause. The
banner was already right: it gates on `status === 'failed'`.

Adds `validation.limitsLoading` in both locales for the in-flight case.
The six locale keys added by this branch were also indented 12 spaces
where their siblings use 16; `.prettierignore` lists `src/lib/locales/`,
so `pnpm lint` could not catch it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rubenhensen
rubenhensen merged commit bb81a3d into main Sep 1, 2026
38 checks passed
@rubenhensen
rubenhensen deleted the feat/267-fetch-upload-limits branch September 1, 2026 08:27
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.

website: fetch cryptify's upload limits instead of baking them, and fail closed when the fetch fails

1 participant