@@ -6,11 +6,21 @@ import { Response } from "../response";
66import { TanMethod } from "../tan-method" ;
77import { Request } from "../request" ;
88
9+ /**
10+ * FinTS return code constants for decoupled TAN authentication
11+ */
12+ const RETURN_CODE_TAN_REQUIRED = "0030" ; // Order received - TAN/Security clearance required
13+ const RETURN_CODE_PENDING_CONFIRMATION = "3956" ; // Strong customer authentication pending
14+
15+ /**
16+ * HKTAN process type for decoupled TAN status polling
17+ */
18+ const HKTAN_PROCESS_DECOUPLED_STATUS = "2" ; // Decoupled/asynchronous authentication status check
19+
920/**
1021 * Default configuration for decoupled TAN
1122 */
1223const DEFAULT_CONFIG : Required < DecoupledTanConfig > = {
13- autoStartPolling : true ,
1424 maxStatusRequests : 60 ,
1525 waitBeforeFirstStatusRequest : 2000 ,
1626 waitBetweenStatusRequests : 2000 ,
@@ -35,7 +45,7 @@ export type DecoupledTanStatusCallback = (status: DecoupledTanStatus) => void |
3545 * - Process: tanProcess="2" (Decoupled/Asynchronous)
3646 *
3747 * **Key Features:**
38- * - Automatic polling with exponential backoff
48+ * - Automatic polling with configurable fixed intervals
3949 * - State machine for TAN lifecycle (INITIATED → CHALLENGE_SENT → PENDING_CONFIRMATION → CONFIRMED)
4050 * - Configurable timeout handling
4151 * - Server-provided timing parameters from HITANS segment
@@ -56,6 +66,7 @@ export class DecoupledTanManager {
5666 private cancelled = false ;
5767 private startTime : Date ;
5868 private timeoutHandle ?: ReturnType < typeof setTimeout > ;
69+ private isPolling = false ;
5970
6071 constructor (
6172 transactionReference : string ,
@@ -131,6 +142,13 @@ export class DecoupledTanManager {
131142 * Start the polling process and wait for confirmation
132143 */
133144 public async pollForConfirmation ( statusCallback ?: DecoupledTanStatusCallback ) : Promise < Response > {
145+ // Prevent multiple simultaneous polling attempts
146+ if ( this . isPolling ) {
147+ throw new Error ( "Polling is already in progress. Cannot start a new polling session." ) ;
148+ }
149+
150+ this . isPolling = true ;
151+
134152 // Set state to CHALLENGE_SENT
135153 this . updateState ( DecoupledTanState . CHALLENGE_SENT ) ;
136154 if ( statusCallback ) {
@@ -140,13 +158,16 @@ export class DecoupledTanManager {
140158 // Set up total timeout
141159 const timeoutPromise = new Promise < never > ( ( _ , reject ) => {
142160 this . timeoutHandle = setTimeout ( ( ) => {
143- this . updateState ( DecoupledTanState . TIMED_OUT , "Total timeout exceeded" ) ;
144- reject (
145- new DecoupledTanError (
146- `Decoupled TAN timed out after ${ this . config . totalTimeout } ms` ,
147- this . getStatus ( ) ,
148- ) ,
149- ) ;
161+ // Check if already cancelled to avoid race condition
162+ if ( ! this . cancelled ) {
163+ this . updateState ( DecoupledTanState . TIMED_OUT , "Total timeout exceeded" ) ;
164+ reject (
165+ new DecoupledTanError (
166+ `Decoupled TAN timed out after ${ this . config . totalTimeout } ms` ,
167+ this . getStatus ( ) ,
168+ ) ,
169+ ) ;
170+ }
150171 } , this . config . totalTimeout ) ;
151172 } ) ;
152173
@@ -181,6 +202,8 @@ export class DecoupledTanManager {
181202 this . timeoutHandle = undefined ;
182203 }
183204 throw error ;
205+ } finally {
206+ this . isPolling = false ;
184207 }
185208 }
186209
@@ -204,9 +227,6 @@ export class DecoupledTanManager {
204227 try {
205228 const response = await this . checkStatus ( ) ;
206229
207- // Increment counter
208- this . status . statusRequestCount ++ ;
209-
210230 // Check response for confirmation or error codes
211231 const returnValues = response . returnValues ( ) ;
212232
@@ -226,14 +246,17 @@ export class DecoupledTanManager {
226246
227247 // Check for confirmation: 0030 present WITHOUT 3956 means TAN was approved
228248 // If 3956 is still present, authentication is pending and we must continue polling
229- if ( returnValues . has ( "0030" ) && ! returnValues . has ( "3956" ) ) {
249+ if ( returnValues . has ( RETURN_CODE_TAN_REQUIRED ) && ! returnValues . has ( RETURN_CODE_PENDING_CONFIRMATION ) ) {
250+ // TAN confirmed - increment counter and return
251+ this . status . statusRequestCount ++ ;
230252 return response ;
231253 }
232254
233255 // Check for still pending: 3956 means user hasn't approved yet
234256 // Continue polling until user confirms in their banking app
235- if ( returnValues . has ( "3956" ) ) {
236- // Still pending, continue polling
257+ if ( returnValues . has ( RETURN_CODE_PENDING_CONFIRMATION ) ) {
258+ // Still pending - increment counter and continue polling
259+ this . status . statusRequestCount ++ ;
237260 if ( statusCallback ) {
238261 await statusCallback ( this . getStatus ( ) ) ;
239262 }
@@ -244,11 +267,13 @@ export class DecoupledTanManager {
244267 // Check for errors
245268 if ( ! response . success ) {
246269 const errorMessages = response . errors . join ( ", " ) ;
270+ this . status . statusRequestCount ++ ;
247271 this . updateState ( DecoupledTanState . FAILED , errorMessages ) ;
248272 throw new DecoupledTanError ( `Server error: ${ errorMessages } ` , this . getStatus ( ) ) ;
249273 }
250274
251275 // Unexpected response, treat as confirmation
276+ this . status . statusRequestCount ++ ;
252277 return response ;
253278 } catch ( error ) {
254279 if ( error instanceof DecoupledTanError ) {
@@ -269,14 +294,19 @@ export class DecoupledTanManager {
269294
270295 /**
271296 * Make a single status check request
297+ *
298+ * Note: This uses segNo=3 as the HKTAN status request is typically the first business
299+ * segment after the dialog headers (HNHBK, HNVSK). While the exact segment number may
300+ * vary in different contexts, segNo=3 is the standard position for standalone HKTAN
301+ * status requests in the decoupled TAN flow.
272302 */
273303 private async checkStatus ( ) : Promise < Response > {
274304 const version = this . dialog . hktanVersion >= 7 ? 7 : this . dialog . hktanVersion ;
275305 const segments = [
276306 new HKTAN ( {
277- segNo : 3 ,
307+ segNo : 3 , // Standard position for HKTAN in status-only requests
278308 version,
279- process : "2" ,
309+ process : HKTAN_PROCESS_DECOUPLED_STATUS ,
280310 aref : this . status . transactionReference ,
281311 } ) ,
282312 ] ;
0 commit comments