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:
Abin Toppo
2026-09-30 18:41:03 +05:30
co-authored by Claude Opus 5.5
parent f1def81eb7
commit 5fbd13ba12
24 changed files with 1571 additions and 29 deletions
@@ -1,5 +1,6 @@
import {
TPIN_LENGTH,
containsSequentialRun,
describeTpinProblem,
isAcceptableNewTpin,
isValidTpinEntry,
@@ -38,6 +39,22 @@ describe('describeTpinProblem', () => {
expect(describeTpinProblem('6789')).toMatch(/run of digits/);
});
// The policy refuses "a sequence of three digits", so a run of three inside
// the four counts as much as one spanning all of them.
it('refuses a run of three anywhere in the TPIN', () => {
expect(describeTpinProblem('1235')).toMatch(/run of digits/);
expect(describeTpinProblem('5987')).toMatch(/run of digits/);
expect(describeTpinProblem('0124')).toMatch(/run of digits/);
expect(describeTpinProblem('7210')).toMatch(/run of digits/);
});
it('allows a TPIN with no run of three', () => {
expect(describeTpinProblem('1243')).toBeUndefined();
expect(describeTpinProblem('1357')).toBeUndefined();
// No wrap-around from 9 to 0.
expect(describeTpinProblem('8901')).toBeUndefined();
});
it('refuses the same digit repeated', () => {
expect(describeTpinProblem('1111')).toMatch(/same digit/);
});
@@ -65,3 +82,16 @@ describe('isAcceptableNewTpin', () => {
expect(isAcceptableNewTpin('0101', '9300000101')).toBe(false);
});
});
describe('containsSequentialRun', () => {
it('finds a run of three at the start, the end, or spanning the whole value', () => {
expect(containsSequentialRun('1239')).toBe(true);
expect(containsSequentialRun('9321')).toBe(true);
expect(containsSequentialRun('3456')).toBe(true);
});
it('does not count a run of two', () => {
expect(containsSequentialRun('1290')).toBe(false);
expect(containsSequentialRun('12')).toBe(false);
});
});