import { env } from '../../../config/env'; import { apiClient } from '../../../services/api/apiClient'; import { isCbsApiError } from '../../../services/api/cbsErrors'; import { endpoints } from '../../../services/api/endpoints'; import { deviceIdentity } from '../../../services/security/deviceIdentity'; import { mpinCrypto } from '../../../services/security/mpinCrypto'; import type { CreateMpinResult, LoginResult, SessionIdentity, StartRegistrationResult, VerifyOtpResult, } from '../../../types/cbs'; /** * `MALFORMED_REQUEST` on an MPIN call is what a stale or wrong public key looks * like from the client — the server will not say more, because saying more * would leak whether the profile exists. The key is fetched fresh on every * encryption, so a retry picks up a rotated key; try exactly once more. * A second failure is a real problem worth surfacing. */ const withKeyRetry = async (call: () => Promise): Promise => { try { return await call(); } catch (error) { if (isCbsApiError(error) && error.code === 'MALFORMED_REQUEST') { return call(); } throw error; } }; export const authService = { /** * Looks the customer up in their bank's core system, links every account it * returns and sends an OTP to the handset. */ async startRegistration(mobileNumber: string, orgId: string = env.orgId) { const response = await apiClient.post( endpoints.registration.start, { orgId, mobileNumber }, ); return response.data; }, /** * Verifies the OTP, binds the profile to this device and allocates an MMID to * every linked account. Returns the single-use, five-minute setup token that * MPIN creation requires. */ async verifyRegistrationOtp({ mobileNumber, otp, orgId = env.orgId, }: { mobileNumber: string; otp: string; orgId?: string; }) { const deviceId = await deviceIdentity.get(); const response = await apiClient.post(endpoints.registration.verifyOtp, { orgId, mobileNumber, otp, deviceId, }); return response.data; }, /** * Creates the MPIN and completes registration. There is deliberately no * orgId and no mobile number in the request — both come from the setup token, * which is what stops a caller setting someone else's MPIN. The deviceId must * match the one sent at verify-otp. */ async createMpin({ setupToken, mpin }: { setupToken: string; mpin: string }) { return withKeyRetry(async () => { const deviceId = await deviceIdentity.get(); const encryptedMpin = await mpinCrypto.encryptMpin(mpin); const response = await apiClient.post(endpoints.registration.mpin, { setupToken, deviceId, encryptedMpin, }); return response.data; }); }, /** * Verifies the MPIN and opens a session. The MPIN is encrypted exactly as it * was at setup — same key, same payload shape. */ async login({ mobileNumber, mpin, orgId = env.orgId, setupToken, }: { mobileNumber: string; mpin: string; orgId?: string; /** See `LoginRequest.setupToken` — a device re-bind, and only that. */ setupToken?: string | null; }) { return withKeyRetry(async () => { const deviceId = await deviceIdentity.get(); const encryptedMpin = await mpinCrypto.encryptMpin(mpin); const response = await apiClient.post(endpoints.auth.login, { orgId, mobileNumber, deviceId, encryptedMpin, // Spread rather than set: an ordinary sign-in must not carry the key at // all, and `setupToken: undefined` is a key once this goes through // JSON.stringify on some clients. ...(setupToken ? { setupToken } : {}), }); return response.data; }); }, /** Who the current session belongs to. Doubles as a keep-alive. */ async getIdentity() { const response = await apiClient.get(endpoints.auth.me); return response.data; }, /** * Revokes the session immediately. The endpoint always answers 200, even for * a token that was already dead — a different answer would tell a caller * whether a token they hold is live. Nothing that happens here may block the * local sign-out, so every failure is swallowed: the customer asked to be * signed out, and a network error must not leave them signed in. */ async logout() { try { await apiClient.post(endpoints.auth.logout); } catch { // Intentionally ignored. The local session is cleared regardless. } }, };