Skip to content

fix(ghostkey): warn before a donor closes the only copy of their key - #102

Merged
sanity merged 1 commit into
mainfrom
ghostkey-one-shot
Aug 3, 2026
Merged

fix(ghostkey): warn before a donor closes the only copy of their key#102
sanity merged 1 commit into
mainfrom
ghostkey-one-shot

Conversation

@sanity

@sanity sanity commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

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_signed into the PaymentIntent metadata and refuses any second attempt (CertificateAlreadySigned). And generateAndSignCertificate mints 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

  • A prominent notice as soon as the key renders: 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 user interaction — the confirmation step is the point, not the sentence.
  • Both stand down once the key is saved, replaced by a short confirmation. Download, copy, and import all count. Import is treated optimistically, since the vault is cross-origin and cannot report back, so the wording still tells the donor to keep a file.

Also deletes a dead template

layouts/partials/donation-success.html is 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, beforeunload is 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_signed mark 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]

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
sanity merged commit 908a09f into main Aug 3, 2026
3 checks passed
@sanity
sanity deleted the ghostkey-one-shot branch August 3, 2026 20:02
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.
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