fix(ghostkey): warn before a donor closes the only copy of their key - #102
Merged
Conversation
The success page is the sole custodian of a Ghost Key and nothing said so. The key is generated in the donor's browser, and the server signs exactly one per payment -- it writes `certificate_signed` into the PaymentIntent metadata and refuses any second attempt. Reloading the page mints a DIFFERENT keypair, which the server then declines to sign. So closing the tab without saving loses a donation that cannot be reissued, silently, with no warning and no recovery. Adds: - A prominent notice, shown as soon as the key renders, saying this page is the only copy and the donation cannot be reissued. - A `beforeunload` guard so the browser asks for confirmation before the tab closes while the key is unsaved. Browsers supply their own wording and only honour this after interaction; the confirmation step is the point, not the sentence. - Both stand down once the key has been downloaded, copied, or imported, replaced by a short confirmation. Import is treated optimistically -- the vault is cross-origin and cannot report back -- so the wording still tells the donor to keep a file. Also deletes `layouts/partials/donation-success.html`. The page renders via the *shortcode* of the same name; the partial is referenced nowhere and predates the "Import to Freenet" button entirely. I edited it first by mistake and the change silently did nothing, which is exactly the trap it will set for the next person. Verified in a browser against a real build: warning shows on render, beforeunload is armed while unsaved, both clear after a download, and the guard stands down. 7/7.
sanity
added a commit
that referenced
this pull request
Aug 4, 2026
**The vault was reachable from nowhere.** Its contract id appeared in exactly one place on the whole site -- the import button's JavaScript -- while the prose named "the Ghostkey Vault" six times without ever linking it. So the only route in was to complete a purchase and click Import, and anyone who already had Ghost Keys had no route at all. Adds a link on /ghostkey/ in the section already about storage, and names the address on the success page. Both say plainly that it needs a Freenet node on the same computer and will not resolve otherwise, since the address is the user's own machine rather than a website. **The pre-purchase warning was wrong.** /ghostkey/create/ told people, immediately before paying, that the vault "can currently lose keys", citing freenet/ghostkeys#3 -- which is closed and fixed. That is inaccurate friction at the exact moment someone is deciding to spend money, and my earlier staleness sweep missed it because I checked /ghostkey/_index.md and not the create page's own copy. Replaced with what is actually true and more useful: the key is created in the browser on the next page and signed once, that page is the only copy, and the donation cannot be reissued -- so download or import before closing the tab. Same fact the success page now warns about (#102), stated before the money is spent rather than after. Verified in a browser against a real build: 6/6.
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.
The success page is the sole custodian of a Ghost Key, and nothing said so.
What actually happens today
The key is generated in the donor's browser. The server signs exactly one per payment — it writes
certificate_signedinto the PaymentIntent metadata and refuses any second attempt (CertificateAlreadySigned). AndgenerateAndSignCertificatemints a fresh keypair on every run.Put together: reloading the page produces a different key, which the server then declines to sign. So closing that tab without saving loses a donation that cannot be reissued — silently, with no warning and no recovery path.
I found this while investigating something else entirely (ghostkeys#19, the three-tab flow). It is a bigger problem than the one I was looking at.
What this adds
beforeunloadguard, so the browser asks for confirmation before the tab closes while the key is unsaved. Browsers supply their own wording and only honour this after user interaction — the confirmation step is the point, not the sentence.Also deletes a dead template
layouts/partials/donation-success.htmlis referenced nowhere — the page renders via the shortcode of the same name. The partial predates the "Import to Freenet" button entirely.I edited it first by mistake and the change silently did nothing. It set that trap for me and would set it for the next person, so it goes.
Verification
Driven in a real browser against a built site: warning appears on render,
beforeunloadis armed while unsaved, both clear after a download, and the guard stands down afterwards. 7/7.Getting there took three attempts — the first two were measuring a stale HTTP server pointed at the wrong directory, and then a build missing the WASM module. Noting that because the failures looked like bugs in the change and were not.
Related, not fixed here
The
certificate_signedmark is set just before signing, and released on failure under a claim lock — that path is handled properly. The residual is a crash between the two, or Stripe rejecting the release call (errors there are logged, not propagated), either of which leaves a donor locked out with no self-serve recovery. Narrow, pre-existing, and worth a separate look.[AI-assisted - Claude]