/** * Verification logic for the voice call setup pipeline. * * These pure-ish functions encapsulate the decision-making for guardian code * verification and invite code redemption without mutating call-session * state. They return structured result objects that the caller * (CallSetupFlow) interprets to drive side-effects (TTS, session updates, * timer scheduling, etc.). */ import { validateAndConsumeVerification } from "../channels/gateway-verification-sessions.js"; import { composeVerificationVoice, GUARDIAN_VERIFY_TEMPLATE_KEYS, } from "../runtime/verification-templates.js"; import { redeemVoiceInviteViaGateway } from "./gateway-invite-reader.js"; import type { CallEventType } from "./types.js"; // ── parseDigitsFromSpeech ────────────────────────────────────────────── const wordToDigit: Record = { zero: "0", oh: "0", o: "0", one: "1", won: "1", two: "2", too: "2", to: "2", three: "3", four: "4", for: "4", fore: "4", five: "5", six: "6", seven: "7", eight: "8", ate: "8", nine: "9", }; /** * Extract digit characters from a speech transcript. Recognizes both * raw digit characters ("1 2 3") and spoken number words ("one two three"). */ export function parseDigitsFromSpeech(transcript: string): string { const digits: string[] = []; const lower = transcript.toLowerCase(); // Split on whitespace and non-alphanumeric boundaries const tokens = lower.split(/[\s,.\-;:!?]+/); for (const token of tokens) { if (/^\d$/.test(token)) { digits.push(token); } else if (wordToDigit[token]) { digits.push(wordToDigit[token]); } else if (/^\d+$/.test(token)) { // Multi-digit number like "123456" — split into individual digits digits.push(...token.split("")); } } return digits.join(""); } // ── Guardian code verification ───────────────────────────────────────── interface VerificationCallParams { verificationFromNumber: string; enteredCode: string; isOutbound: boolean; codeDigits: number; verificationAttempts: number; verificationMaxAttempts: number; } type VerificationCallResult = | { outcome: "success"; verificationType: "guardian" | "trusted_contact"; /** Event name to record */ eventName: CallEventType; /** For outbound success: the TTS text to play */ ttsMessage?: string; } | { outcome: "failure"; eventName: CallEventType; ttsMessage: string; attempts: number; } | { outcome: "retry"; ttsMessage: string; attempt: number; maxAttempts: number; }; /** * Core logic for validating an entered code against the pending voice * guardian challenge. The gateway validates and consumes the session and * applies role side effects (guardian binding / trusted-contact channel * upsert) in-engine. Returns a structured result describing what happened * so the caller can apply side-effects (state mutations, TTS, session * updates) without this function needing access to the relay connection. */ export async function attemptVerificationCode( params: VerificationCallParams, ): Promise { const { verificationFromNumber, enteredCode, isOutbound, codeDigits, verificationAttempts, verificationMaxAttempts, } = params; const result = await validateAndConsumeVerification( "phone", enteredCode, verificationFromNumber, verificationFromNumber, ); if (result.success) { const eventName = isOutbound ? "outbound_voice_verification_succeeded" : "voice_verification_succeeded"; let ttsMessage: string | undefined; if (isOutbound) { ttsMessage = composeVerificationVoice( GUARDIAN_VERIFY_TEMPLATE_KEYS.VOICE_SUCCESS, { codeDigits }, ); } return { outcome: "success", verificationType: result.verificationType, eventName, ttsMessage, }; } // Failure path const newAttempts = verificationAttempts + 1; if (newAttempts >= verificationMaxAttempts) { const failEventName = isOutbound ? "outbound_voice_verification_failed" : "voice_verification_failed"; const failureText = isOutbound ? composeVerificationVoice(GUARDIAN_VERIFY_TEMPLATE_KEYS.VOICE_FAILURE, { codeDigits, }) : "Verification failed. Goodbye."; return { outcome: "failure", eventName: failEventName, ttsMessage: failureText, attempts: newAttempts, }; } const retryText = isOutbound ? composeVerificationVoice(GUARDIAN_VERIFY_TEMPLATE_KEYS.VOICE_RETRY, { codeDigits, }) : "That code was incorrect. Please try again."; return { outcome: "retry", ttsMessage: retryText, attempt: newAttempts, maxAttempts: verificationMaxAttempts, }; } // ── Invite code redemption ───────────────────────────────────────────── interface InviteRedemptionParams { inviteRedemptionFromNumber: string; enteredCode: string; /** Resolved guardian label used in the failure TTS message. */ guardianLabel: string; } type InviteRedemptionResult = | { outcome: "success"; memberId: string; type: "redeemed" | "already_member"; inviteId: string; } | { outcome: "failure"; ttsMessage: string; }; /** * Redeem an entered invite code for the caller at the gateway — the single * lifecycle authority for voice invites. Fails CLOSED: any gateway failure is * the generic failure outcome with no local fallback. Returns a structured * result so the caller can handle state mutations and session updates. */ export async function attemptInviteCodeRedemption( params: InviteRedemptionParams, ): Promise { const { inviteRedemptionFromNumber, enteredCode, guardianLabel } = params; const result = await redeemVoiceInviteViaGateway( inviteRedemptionFromNumber, enteredCode, ); if (result.ok) { return { outcome: "success", memberId: result.outcome.contactId, type: result.outcome.result, inviteId: result.outcome.inviteId, }; } return { outcome: "failure", ttsMessage: `Sorry, the code you provided is incorrect or has since expired. Please ask ${guardianLabel} for a new code. Goodbye.`, }; }