Context
PINs can be 4, 5, or 6 digits long. The unlock forms accept them as type="password" with minLength=4 maxLength=6. The submit button enables as soon as the value's length is ≥ 4 in the badge, ≥ 4 in the popup.
Problem / Observation
- extension/src/content/Badge.tsx:862 button disabled state:
disabled={mode === "pin" ? value.length < 4 : value.length === 0}. The form submits on <Enter> while the user is still typing their 5th/6th digit.
- extension/src/popup/components/UnlockScreen.tsx:214 does the same:
disabled={busy.value || pin.length < 4}.
- A user with a 6-digit PIN gets "incorrect PIN" if their first 4 digits don't coincidentally unlock another vault. The error wipes the entered digits (state reset in
submitPin).
- No debounce, no "wait for blur" — every browser autofill that delivers digits one-by-one triggers an unlock attempt at 4.
Proposed approach
Defer submission to one of:
- An explicit submit button click (don't auto-validate on
<input> debounce; honour the user's pressed Enter only).
- A short (~400ms) debounce after the value stops changing, then submit once.
- Add a "Continue" button that explicitly submits — the segmented mode picker already lives next to the field.
Track failed attempts (current code does not) and back off after N tries (already in router via OPAQUE rate-limit upstream; the local check would be defence in depth).
Acceptance criteria
Context
PINs can be 4, 5, or 6 digits long. The unlock forms accept them as
type="password"withminLength=4 maxLength=6. The submit button enables as soon as the value's length is ≥ 4 in the badge, ≥ 4 in the popup.Problem / Observation
disabled={mode === "pin" ? value.length < 4 : value.length === 0}. The form submits on<Enter>while the user is still typing their 5th/6th digit.disabled={busy.value || pin.length < 4}.submitPin).Proposed approach
Defer submission to one of:
<input>debounce; honour the user's pressed Enter only).Track failed attempts (current code does not) and back off after N tries (already in router via OPAQUE rate-limit upstream; the local check would be defence in depth).
Acceptance criteria
tests/pin.test.tsadds a case that types"123456"digit-by-digit and verifies only one unlock attempt is made.