feat: expose guesses_remaining on invalid-PIN unlock failures - #15
Merged
Conversation
A wrong PIN previously surfaced only "Juicebox error: Invalid PIN" even though the core already received the remaining-attempt count from Juicebox's recover call. JuiceboxError::InvalidPin now carries guesses_remaining: Option<u16>, and Chat::unlock threads the count from RecoverResult::Failure into it (change_pin unlocks with the old PIN first, so it benefits automatically). The Display format keeps the byte-identical "Invalid PIN" prefix and appends the stable ": guesses_remaining=N" token when the count is known, matching the token the JS wrapper already emits. Because errors cross the C FFI as message strings, no FFI signatures change. Each binding gains a structured accessor that parses the stable token so callers never regex the message themselves: JVM ChatXdkException.getGuessesRemaining() (Integer, null when absent), .NET ChatXdkException.GuessesRemaining (int?), Go GuessesRemaining(err) (int, bool), and Python chat_xdk.guesses_remaining(exc) (int | None). A count of 0 means the guess budget is exhausted and the stored keys are locked. docs/API.md documents the failure shape, and parallel tests cover the core error path plus each binding's accessor. The regenerated go/chatxdk/libs/darwin_arm64 static lib is included; the other prebuilt Go libs need 'make prebuilt-all' (cargo-zigbuild was unavailable on this machine). Fixes #10
santiagomed
force-pushed
the
expose-guesses-remaining
branch
from
August 11, 2026 21:18
49e5e26 to
4ef81ad
Compare
|
|
…PIN not retryable
Review follow-ups: the JS wrapper gains guessesRemaining(err) so all five
bindings expose the invalid-PIN attempt count structurally instead of JS
callers string-parsing the error. Every binding's parser is anchored to
the full invalid-PIN message form ("Invalid PIN: guesses_remaining=N";
"reason=InvalidPin guesses_remaining=N" in JS) so a count embedded in an
unrelated pass-through message is never misread, with false-positive
cases added to each suite. is_retryable() now reports false for
InvalidPin with a zero remaining budget: the stored keys are locked and
another attempt cannot succeed.
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
Fixes #10.
Problem
A wrong PIN on
unlocksurfaced only the stringJuicebox error: Invalid PINin the JVM/.NET/Go/Python bindings, even though Juicebox'srecoverreports the remaining attempt budget and the core already received it —Chat::unlockdiscardedguesses_remaining, and the publicJuiceboxError::InvalidPinhad no payload. The JS wrapper already surfaced the count, so this was also a binding-parity gap. Callers building PIN UIs (the issue's browser PIN wizard) need the live count from Juicebox — it shares one budget with the X mobile app — to show "After {N} more attempts your messages will be locked" and to detect the locked state.Change
Core:
JuiceboxError::InvalidPinnow carriesguesses_remaining: Option<u16>, threaded fromRecoverResult::Failureinunlock(change_pinunlocks with the old PIN first, so it benefits automatically). The Display keeps the byte-identicalInvalid PINprefix — existing string matching keeps working — and appends a stable: guesses_remaining=Ntoken when the count is known, the same token the JS wrapper already emits.0means the budget is exhausted and the keys are locked; the token is absent when Juicebox reports no count.is_retryable()is unchanged.Bindings: errors cross the C FFI as message strings, so no FFI signatures change. Each binding gains a structured accessor that parses the documented token, so callers never regex the message themselves (the issue's options 1 + 2):
JuiceboxError::InvalidPin { guesses_remaining: Option<u16> }ChatXdkException.getGuessesRemaining()→Integer(nullwhen absent)ChatXdkException.GuessesRemaining→int?chatxdk.GuessesRemaining(err) (int, bool)chat_xdk.guesses_remaining(exc)→int | Nonereason=InvalidPin guesses_remaining=NDocs:
docs/API.mddocuments the failure shape, the stable token, and every accessor in the Juicebox Key Storage section.Tests (parallel across suites, driving the real binding): core tests use a
JuiceboxApistub (the existing mock pattern) to assertunlockandchange_pincarrySome(3)/None/Some(0)and pin the exact Display strings — so any drift in the token format failsmake cibefore it can break a binding parser. Each binding tests its accessor on the documented message forms (3 / 0 / absent) plus a real error produced by its own FFI path.Verification
make ci(fmt, clippy-D warnings, workspace tests) — passmake test-go— pass (regenerateddarwin_arm64prebuilt lib committed per repo policy; the Linux prebuilts needmake prebuilt-allwhere cargo-zigbuild is available)make jvm-test(JDK 21) — 58/58 passmake dotnet-test(DOTNET_ROLL_FORWARD=LatestMajor; only .NET 10 installed locally) — 71/71 passunittest discover— 60/60 pass (incl. 2 new)No wire-format, Juicebox
UserInfo, or recover-call changes; the exhausted state is signaled byguesses_remaining == 0rather than a new error variant.