diff --git a/packages/managed-auth-react/src/KernelManagedAuth.tsx b/packages/managed-auth-react/src/KernelManagedAuth.tsx
index 8be1d02..64dba1a 100644
--- a/packages/managed-auth-react/src/KernelManagedAuth.tsx
+++ b/packages/managed-auth-react/src/KernelManagedAuth.tsx
@@ -10,6 +10,7 @@ import { StepError } from "./components/StepError";
import { StepExpired } from "./components/StepExpired";
import { LoadingState } from "./components/LoadingState";
import { ExternalActionWaiting } from "./components/ExternalActionWaiting";
+import { HumanInterventionStep } from "./components/HumanInterventionStep";
import { useLocalization } from "./localization/context";
import type { Appearance } from "./appearance/types";
import type { Localization } from "./localization/types";
@@ -120,6 +121,12 @@ function KernelManagedAuthInner({
);
}
+ if (uiState === "awaiting_human_intervention") {
+ return (
+
+ );
+ }
+
if (uiState === "success") {
return ;
}
diff --git a/packages/managed-auth-react/src/components/HumanInterventionStep.tsx b/packages/managed-auth-react/src/components/HumanInterventionStep.tsx
new file mode 100644
index 0000000..aa3d5e5
--- /dev/null
+++ b/packages/managed-auth-react/src/components/HumanInterventionStep.tsx
@@ -0,0 +1,59 @@
+import { useSlot } from "../appearance/context";
+import { useLocalization } from "../localization/context";
+
+interface HumanInterventionStepProps {
+ liveViewUrl?: string | null;
+}
+
+export function HumanInterventionStep({
+ liveViewUrl,
+}: HumanInterventionStepProps) {
+ const slot = useSlot();
+ const l = useLocalization();
+
+ return (
+
+
+
{l.humanInterventionTitle}
+
+ {l.humanInterventionMessage}
+
+
+
+ {liveViewUrl ? (
+
+
+
+
+ Live
+
+
+ ) : (
+
+
+
+
+
+ )}
+
+
{l.humanInterventionWaiting}
+
+ );
+}
diff --git a/packages/managed-auth-react/src/lib/types.ts b/packages/managed-auth-react/src/lib/types.ts
index 70c851e..f75ea83 100644
--- a/packages/managed-auth-react/src/lib/types.ts
+++ b/packages/managed-auth-react/src/lib/types.ts
@@ -13,6 +13,7 @@ export type FlowStep =
| "DISCOVERING"
| "AWAITING_INPUT"
| "AWAITING_EXTERNAL_ACTION"
+ | "AWAITING_HUMAN_INTERVENTION"
| "SUBMITTING"
| "COMPLETED";
@@ -101,6 +102,7 @@ export type UIState =
| "discovering"
| "awaiting_input"
| "awaiting_external_action"
+ | "awaiting_human_intervention"
| "submitting"
| "success"
| "expired"
diff --git a/packages/managed-auth-react/src/localization/defaults.ts b/packages/managed-auth-react/src/localization/defaults.ts
index 664838e..2afea3f 100644
--- a/packages/managed-auth-react/src/localization/defaults.ts
+++ b/packages/managed-auth-react/src/localization/defaults.ts
@@ -69,4 +69,9 @@ export const DEFAULT_LOCALIZATION: Localizer = {
externalActionTitle: "Action Required",
externalActionFallbackMessage: "Complete the verification on your device",
externalActionWaiting: "Waiting for your confirmation...",
+ humanInterventionTitle: "Your Help Needed",
+ humanInterventionMessage:
+ "Please solve the captcha in the browser below to continue",
+ humanInterventionIframeTitle: "Browser live view",
+ humanInterventionWaiting: "Waiting for you to complete the challenge...",
};
diff --git a/packages/managed-auth-react/src/localization/types.ts b/packages/managed-auth-react/src/localization/types.ts
index 01a7f3d..f67a895 100644
--- a/packages/managed-auth-react/src/localization/types.ts
+++ b/packages/managed-auth-react/src/localization/types.ts
@@ -58,6 +58,11 @@ export interface Localization {
externalActionTitle?: string;
externalActionFallbackMessage?: string;
externalActionWaiting?: string;
+ /** Human intervention (HITL). */
+ humanInterventionTitle?: string;
+ humanInterventionMessage?: string;
+ humanInterventionIframeTitle?: string;
+ humanInterventionWaiting?: string;
}
export type Localizer = Required;
diff --git a/packages/managed-auth-react/src/session/useManagedAuthSession.ts b/packages/managed-auth-react/src/session/useManagedAuthSession.ts
index 1e817c7..6402913 100644
--- a/packages/managed-auth-react/src/session/useManagedAuthSession.ts
+++ b/packages/managed-auth-react/src/session/useManagedAuthSession.ts
@@ -37,6 +37,8 @@ function deriveUIState(state: ManagedAuthResponse): UIState {
return "awaiting_input";
case "AWAITING_EXTERNAL_ACTION":
return "awaiting_external_action";
+ case "AWAITING_HUMAN_INTERVENTION":
+ return "awaiting_human_intervention";
case "SUBMITTING":
return "submitting";
default:
diff --git a/packages/managed-auth-react/src/styles/styles.css b/packages/managed-auth-react/src/styles/styles.css
index be0412e..934f4c4 100644
--- a/packages/managed-auth-react/src/styles/styles.css
+++ b/packages/managed-auth-react/src/styles/styles.css
@@ -1062,6 +1062,92 @@
}
}
+/* ---------- Human intervention (HITL) ---------- */
+
+/* Animate the shell + card growing when HITL is active so the transition isn't
+ abrupt. Uses :has() to detect HITL state. */
+.kma-shell {
+ transition: max-width 0.25s ease;
+}
+
+.kma-card {
+ transition: max-width 0.25s ease;
+}
+
+.kma-shell:has(.kma-human-intervention),
+.kma-card:has(.kma-human-intervention) {
+ max-width: min(90vw, 960px);
+}
+
+.kma-human-intervention {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ gap: 1.25rem;
+ padding: 0.5rem 0;
+ text-align: center;
+}
+
+.kma-human-intervention .kma-step__header {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ align-items: center;
+}
+
+.kma-human-intervention__iframe-wrap {
+ position: relative;
+ width: 100%;
+ aspect-ratio: 4 / 3;
+ border-radius: var(--kma-radius, 0.75rem);
+ overflow: hidden;
+ border: 1px solid var(--kma-color-border, rgba(0, 0, 0, 0.08));
+ background: var(--kma-color-muted, rgba(0, 0, 0, 0.03));
+}
+
+.kma-human-intervention__iframe {
+ width: 100%;
+ height: 100%;
+ border: none;
+ display: block;
+}
+
+/* Subtle "live" indicator pinned to the iframe corner for context. */
+.kma-human-intervention__live-badge {
+ position: absolute;
+ top: 0.75rem;
+ right: 0.75rem;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.375rem;
+ padding: 0.25rem 0.625rem;
+ font-size: 0.75rem;
+ font-weight: 600;
+ letter-spacing: 0.02em;
+ background: rgba(0, 0, 0, 0.65);
+ color: #fff;
+ border-radius: 9999px;
+ pointer-events: none;
+}
+
+.kma-human-intervention__live-dot {
+ width: 0.5rem;
+ height: 0.5rem;
+ border-radius: 9999px;
+ background: oklch(0.704 0.191 22.216);
+ animation: kma-live-pulse 2s ease-in-out infinite;
+}
+
+@keyframes kma-live-pulse {
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.4;
+ }
+}
+
/* ---------- Expired step ---------- */
/* Matches the previous hosted-ui `space-y-6 text-center py-8`. */