feat(website): fetch cryptify's upload limits instead of baking them - #271
Merged
Conversation
`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>
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
approved these changes
Aug 31, 2026
rubenhensen
approved these changes
Sep 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #267.
apps/websiteheld its own copy of two numbers cryptify enforces (VITE_MAX_UPLOAD_SIZE,VITE_ROLLING_LIMIT), plus a third inlocalUsage.ts'sWINDOW_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 fromGET /limits, which merged as postguard#392 on 2026-08-31.What changed
src/lib/limits.ts. A store carryingloading/failed/ready, loaded byuploadLimits.load(FILEHOST_URL)from the compose screen'sonMount.window_daysis converted towindowMson the way in; the two byte figures are used as served.VITE_MAX_UPLOAD_SIZEandVITE_ROLLING_LIMITare gone from.env,.env.dev,src/lib/env.tsand the README table.localUsage.tstakes the window and the rolling limit from its callers instead of holding copies.usage.ts'sparseLimitExceededBody()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.SendButtongatescanEncrypton 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.FileInputhides 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:258computes the sameoverLimitTextvalue the same way, so both are decimal now.maxFileSizeMBcannot become decimal. Item 5 asks forMAX_UPLOAD_SIZE / (1024 * 1024)to be made decimal, but that number is handed to Dropzone, which checksfile.size > maxFilesize * 1048576(@deltablot/dropzone/dist/dropzone.js:1479). MiB is its unit, not a display choice. Dividing by1e6would 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/limitsserving 503, an otherwise valid message keepsaria-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 reads4.00 GB.yivi-disclosure-recoveryandscan-instruction-yivi-logonow mockGET /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:
pnpm -r test,pnpm -r typecheck,pnpm --filter postguard-website lint,lint:cssandtest:e2e(44 passed) all pass locally. I also ran the built site undervite previewagainst a stubbed/limitsand eyeballed both states.Out of scope
effectiveLimitstill subtracts the per-browserlocalStoragefigure, 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, websitelint,lint:cssand 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 (loadingandfailedboth fall through!limitsKnown), now split by a newvalidation.limitsLoadingstring in both locales; and the six new locale keys were indented 12 spaces where their siblings use 16, whichpnpm lintcannot catch because.prettierignorelistssrc/lib/locales/.Attention:
apps/website/src/lib/components/filesharing/SendButton.svelte:90—canEncryptgates onperUploadBytesalone whilegetValidationErrors()gates oneffectiveLimitBytes(), so once the rolling window is exhausted the send button still rendersaria-disabled="false"and only the click-time modal blocks it; pre-existing onmain(same mismatch at 456d8ce) and left unfixed because closing it changes send-button behaviour this PR is not about.