Add TPIN reset via OTP, with server-clock countdowns and redacted API logs
- Reset TPIN: empty-body initiate -> OTP -> encrypted new TPIN via /tpin/reset/*, from a Services tile, search, and "Forgot your TPIN?" on the transfer's TPIN sheet (returns to the same transfer review) - Handle every documented code: SESSION_INVALID signs out, TPIN_NOT_SET offers Create TPIN, CBS customer/account/org refusals stop the flow, outages retry from the start, expired or spent OTP/token restarts, policy VALIDATION_ERROR stays on the new-TPIN step - Secrets live in screen state only and are dropped as each step ends - Countdowns use the envelope timestamp, kept aside by the interceptor, so a wrong device clock cannot skew OTP or token expiry - New TPINs refuse any three-digit run (1235, 5987), for create too - Dev API logger redacts the session token, OTPs, PINs, encrypted payloads and one-time tokens - Tests: request bodies, RSA-OAEP SHA-256 payload decrypted with Node, fresh nonce on key retry, server-clock deadlines, log redaction Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f1def81eb7
commit
5fbd13ba12
@@ -3,6 +3,26 @@ import { isRepeatedDigits, isSequentialDigits } from '../../../utils/validation'
|
||||
/** A TPIN is always exactly four digits. */
|
||||
export const TPIN_LENGTH = 4;
|
||||
|
||||
/** The shortest run up or down the backend's TPIN policy refuses. */
|
||||
const SEQUENCE_RUN_LENGTH = 3;
|
||||
|
||||
/**
|
||||
* True when any three neighbouring digits run straight up or down — 1235 (123),
|
||||
* 5987 (987) and 1234 alike. The TPIN policy refuses "a sequence of three
|
||||
* digits" and answers "TPIN cannot contain sequential digits", so a run
|
||||
* anywhere counts, not only one spanning all four. No wrap-around: 8901 is not
|
||||
* a run.
|
||||
*/
|
||||
export const containsSequentialRun = (value: string) => {
|
||||
for (let start = 0; start + SEQUENCE_RUN_LENGTH <= value.length; start += 1) {
|
||||
if (isSequentialDigits(value.slice(start, start + SEQUENCE_RUN_LENGTH))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Any four digits — for ENTERING the TPIN on a transfer. Deliberately loose:
|
||||
* whether they are the right four is the server's to say, with INVALID_TPIN.
|
||||
@@ -19,8 +39,9 @@ export const isValidTpinEntry = (value: string) =>
|
||||
* than after one.
|
||||
*
|
||||
* The backend stays the final authority. It also refuses a TPIN matching the
|
||||
* customer's date of birth, which the app never holds — so that one can only
|
||||
* arrive as a VALIDATION_ERROR on confirm.
|
||||
* customer's date of birth, which the app never holds, and — on a reset — the
|
||||
* current TPIN or any of the previous three, which only the server's history
|
||||
* knows. Those can only arrive as a VALIDATION_ERROR on confirm.
|
||||
*/
|
||||
export const describeTpinProblem = (value: string, mobileNumber?: string): string | undefined => {
|
||||
if (value.length === 0) {
|
||||
@@ -39,8 +60,8 @@ export const describeTpinProblem = (value: string, mobileNumber?: string): strin
|
||||
return 'Pick a TPIN that is not the same digit repeated';
|
||||
}
|
||||
|
||||
if (isSequentialDigits(value)) {
|
||||
return 'Pick a TPIN that is not a run of digits like 1234 or 4321';
|
||||
if (containsSequentialRun(value)) {
|
||||
return 'Pick a TPIN without a run of digits like 123 or 987';
|
||||
}
|
||||
|
||||
const mobileDigits = mobileNumber?.replace(/\D/g, '') ?? '';
|
||||
|
||||
Reference in New Issue
Block a user