Repository navigation
feat: record why a verification failed, not just that it did - #6
Merged
Merged
Conversation
The decision log flattened every rejection into `missing` or `invalid`, which is the least useful place to lose detail. An expired challenge and a forged one are the same line in the log, but they mean opposite things: one says our validity window is too short for the form, the other says we're being attacked. That gap cost us. A tester reported the Snellman consumer-service form rejecting a submission with no field highlighted, and there was no way to tell from the log whether the challenge had expired or the payload had been replayed -- the two candidate causes have identical symptoms. Challenge::classify() now surfaces the reason the altcha-org library already computes (expired / invalid_signature / invalid_solution) instead of discarding it, plus missing and malformed. verify() keeps its bool contract and defers to classify(), so there's a single parsing path. For expired challenges the log also records `expired_for` -- seconds past expiry. Whether a visitor was ten seconds over or twenty minutes over is the difference between tuning the window and looking elsewhere, and it shows whether the widget's renewal is holding up in the field. Context stays non-PII: a form id, a reason, a duration. 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 decision log flattened every rejection into
missingorinvalid— the least useful place to lose detail. An expired challenge and a forged one produced the same log line, but they mean opposite things: one says our validity window is too short for the form, the other says we're being attacked.That gap had a real cost. A tester reported the Snellman consumer-service form rejecting a submission with no field highlighted. The two candidate causes — an expired challenge and a replayed payload — have identical symptoms, and there was nothing in the log to tell them apart.
The change
Challenge::classify()surfaces the reason thealtcha-org/altchalibrary already computes and we were throwing away:okmissingaltchafield posted — bot, or the widget script never ranmalformedexpiredinvalid_signatureinvalid_solutionreplayIntegration)verify()keeps its bool contract and defers toclassify(), so there's a single parsing path rather than two that can drift.For expired challenges the log also records
expired_for— seconds pastexpiresAt. Ten seconds over versus twenty minutes over is the difference between tuning the window and looking somewhere else entirely, and it's how we'll tell whether the widget renewal in #5 is holding up in the field.Context stays non-PII: a form id, a reason, a duration.
Verification
Observed end-to-end through the real
Loggerhook:7 new unit tests, including the expired-vs-invalid distinction this exists for. 63/63 pass,
pint --testclean.Note
No behaviour change to what gets accepted or rejected — only to what's recorded. The add-on was already offered to Gravity Forms' logging framework; it just has to be enabled under Forms → Settings → Logging. Version bump rides along in #5.
🤖 Generated with Claude Code