Repository navigation
fix: renew the ALTCHA proof when its challenge expires - #5
Merged
Merged
Conversation
The widget solves its proof once, on load, and the challenge the server issues is only valid for DEFAULT_EXPIRES_SECONDS (600). When that window closes the widget moves to the `expired` state and clears the solution -- it does not re-solve on its own. On a long form that is easily reached before the visitor is done. The Snellman consumer-service forms pair a 400+ option product dropdown with two date pickers, an address block and a file upload; ten minutes is an ordinary amount of time to spend there. Submitting past that point fails validation with a form-level message that highlights no field, and the visitor loses everything they typed. Re-solve on `expired` so the payload stays fresh for as long as the form is open. Renewal is parked while the tab is hidden and resumes on visibilitychange, so backgrounded tabs don't grind proof-of-work for nothing. A MutationObserver picks up widgets that appear later, since Gravity Forms replaces the form markup after AJAX submissions and on multi-page navigation. Verified in a browser against a build with a shortened window: the widget expires, renews, and returns to `verified` with a fresh payload -- and keeps doing so on each subsequent expiry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 problem
The widget solves its proof once, on load. The challenge the server issues is valid for
Challenge::DEFAULT_EXPIRES_SECONDS(600s). When that window closes the widget moves to theexpiredstate and clears the solution — it does not re-solve on its own.On a long form, ten minutes is an ordinary amount of time. The Snellman consumer-service forms (43 fi / 45 sv) pair a 400+ option product dropdown with two date pickers, an address block and a file upload. Past the window, submitting fails validation with a form-level message that highlights no field, and the visitor loses everything they typed.
This surfaced as a bug report against the sv form that was initially attributed to the product field. It isn't — with ALTCHA bypassed, every branch of that form validates cleanly, including the one reported as broken.
The fix
Re-solve on the widget's
expiredevent.verify()re-reads thechallengeattribute (a URL), so this fetches a fresh challenge rather than re-solving the dead one.visibilitychange— a backgrounded tab can't be submitting, so there's no reason to grind proof-of-work there.MutationObserverbinds widgets that appear later, since Gravity Forms replaces the form markup after AJAX submissions and on multi-page navigation.expiredevent doesn't bubble, so listeners are bound per element.display="invisible"means none of this is visible to the visitor.Verification
Against a local build with the window shortened to 25s, watching the real event and network timeline:
verified(552-char payload)verified, payload restoredIt recovers repeatedly, not just once.
Test suite: 63/63 pass.
pint --testclean.Tradeoff
A form left open now re-solves every 10 minutes while it's on screen. At the standard cost (200k, ~4s) that's cheap, and nothing happens at all while the tab is hidden.
Note
Carries the
0.6.0version bump. #2 (feat/classify-verification-failures) should ship in the same release.🤖 Generated with Claude Code