SentinelChallenge
SentinelChallenge is the user-facing Cap.js captcha component rendered when the backend responds with a 403 { action: 'CAPTCHA_REQUIRED', userCaptchaId }. It is a React component exported from @traveloka/core, and is normally consumed indirectly — it is mounted globally by SentinelChallengeProvider, which any callAPI/callAPIClientSide request can trigger transparently.
Responsibilities
| Responsibility | Detail |
|---|---|
| Resolve challenge target | Determines the challengeId to solve, from props or the current user session |
| Avoid double-mounting | Guards against a second challenge instance stealing the global fetch override |
| Lazy-load the solver | Loads the actual Cap.js widget (SentinelChallengeSolver) via next/dynamic, ssr: false |
Public API
SentinelChallenge
import { SentinelChallenge } from '@traveloka/core';
A React component. In normal usage you don't render it directly — see Global wiring below.
SentinelChallengeProps
import type { SentinelChallengeProps } from '@traveloka/core';
| Prop | Type | Description |
|---|---|---|
challengeId | string | The captcha challenge id to solve. Falls back to userState.userCaptchaId if omitted |
type | 'capjs' | Challenge widget type. Currently only Cap.js is supported |
endpoints | { challenge?, redeem? } | Overrides the default challenge/redeem API endpoints |
domain | ProductDomain | Overrides the default product domain used for the challenge/redeem calls |
onSuccess | () => void | Called when the captcha is solved |
onError | () => void | Called when the captcha fails or errors |
onLoading | () => void | Called while the widget is loading |
If neither challengeId prop nor userState.userCaptchaId is available, SentinelChallenge renders null.
Global Wiring
In practice, SentinelChallenge is never rendered by application code directly. It is mounted by SentinelChallengeProvider, which itself is mounted once by SentinelManager (see sentinel.md):
SentinelManager
└─ SentinelChallengeProvider (registers window.registerSolveSentinelChallenge)
└─ SentinelChallenge (rendered only while a challenge is active)
└─ SentinelChallengeSolver (lazy-loaded Cap.js widget)
- Any API call (via
callAPIClientSide) that receives403 { action: 'CAPTCHA_REQUIRED', userCaptchaId }callswaitForSentinelChallenge({ userCaptchaId })(packages/core/api/helpers/sharedAPIHelpers.ts). waitForSentinelChallengeinvokeswindow.registerSolveSentinelChallenge(userCaptchaId, resolve), whichSentinelChallengeProviderregistered on mount.SentinelChallengeProvidersets itsactiveChallengestate, causingSentinelChallengeto render with thatchallengeId.- Once the user solves (or fails) the captcha,
SentinelChallenge'sonSuccess/onErrorresolves the pending promise back incallAPIClientSide, which retries the original request on success.
Only one challenge can be active at a time per page; a second concurrent request for a different userCaptchaId is rejected (resolved as false) while one is in progress.
window.CAP_CUSTOM_FETCH Ownership
SentinelChallengeSolver (the lazy-loaded Cap.js widget) installs a custom fetch override on window.CAP_CUSTOM_FETCH while mounted, and removes it on unmount — but only if it still owns that reference (identity-checked), so it never clobbers a handler installed by another captcha flow (e.g. app-common/captcha/Captcha.tsx). SentinelChallenge itself checks this global on mount: if a challenge is already owned elsewhere, it renders null instead of mounting a second, conflicting solver.
Safety Timeout
To prevent a challenge that never resolves (e.g. the user abandons the page, or the widget silently fails to mount) from permanently wedging the provider, SentinelChallengeProvider starts a SENTINEL_CHALLENGE_TIMEOUT_MS (30s, challenge/constants.ts) timer when a challenge becomes active. If still unresolved when it fires, the provider logs [sen.challenge] Challenge timed out waiting for resolution and resolves the challenge as failed (false), freeing it up for the next request.
Related
sentinel.md— overall Sentinel architecture andSentinelManager.packages/core/sentinel/challenge/README.md— implementation-level details and ownership contract for contributors to this package.
Last updated: 2026-08-03