Skip to content

feat: expose guesses_remaining on invalid-PIN unlock failures - #15

Merged
santiagomed merged 4 commits into
mainfrom
expose-guesses-remaining
Aug 12, 2026
Merged

feat: expose guesses_remaining on invalid-PIN unlock failures#15
santiagomed merged 4 commits into
mainfrom
expose-guesses-remaining

Conversation

@santiagomed

Copy link
Copy Markdown
Contributor

Fixes #10.

Problem

A wrong PIN on unlock surfaced only the string Juicebox error: Invalid PIN in the JVM/.NET/Go/Python bindings, even though Juicebox's recover reports the remaining attempt budget and the core already received it — Chat::unlock discarded guesses_remaining, and the public JuiceboxError::InvalidPin had 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::InvalidPin now carries guesses_remaining: Option<u16>, threaded from RecoverResult::Failure in unlock (change_pin unlocks with the old PIN first, so it benefits automatically). The Display keeps the byte-identical Invalid PIN prefix — existing string matching keeps working — and appends a stable : guesses_remaining=N token when the count is known, the same token the JS wrapper already emits. 0 means 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):

Binding Accessor
Rust JuiceboxError::InvalidPin { guesses_remaining: Option<u16> }
JVM ChatXdkException.getGuessesRemaining()Integer (null when absent)
.NET ChatXdkException.GuessesRemainingint?
Go chatxdk.GuessesRemaining(err) (int, bool)
Python chat_xdk.guesses_remaining(exc)int | None
JS unchanged — already emits reason=InvalidPin guesses_remaining=N

Docs: docs/API.md documents 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 JuiceboxApi stub (the existing mock pattern) to assert unlock and change_pin carry Some(3) / None / Some(0) and pin the exact Display strings — so any drift in the token format fails make ci before 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) — pass
  • make test-go — pass (regenerated darwin_arm64 prebuilt lib committed per repo policy; the Linux prebuilts need make prebuilt-all where cargo-zigbuild is available)
  • make jvm-test (JDK 21) — 58/58 pass
  • make dotnet-test (DOTNET_ROLL_FORWARD=LatestMajor; only .NET 10 installed locally) — 71/71 pass
  • Python: extension built + unittest discover — 60/60 pass (incl. 2 new)

No wire-format, Juicebox UserInfo, or recover-call changes; the exhausted state is signaled by guesses_remaining == 0 rather than a new error variant.

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
santiagomed force-pushed the expose-guesses-remaining branch from 49e5e26 to 4ef81ad Compare August 11, 2026 21:18
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ santiagomed
❌ github-actions[bot]
You have signed the CLA already but the status is still pending? Let us recheck it.

github-actions Bot and others added 2 commits August 11, 2026 21:27
…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.
@santiagomed
santiagomed merged commit 3138132 into main Aug 12, 2026
17 of 18 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 12, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose guesses_remaining on Juicebox unlock failure (JVM / structured errors)

2 participants