Repository navigation
Add the human verification step with an embedded live view #40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c624272
aaa46e0
eebd23e
cc260f9
dc3ac9e
2422326
f7fd933
09a2052
87d22b1
221ea9f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@onkernel/managed-auth-react": minor | ||
| --- | ||
|
|
||
| Add the `AWAITING_HUMAN_INTERVENTION` step. When a connection has `hitl` enabled and the login hits a CAPTCHA the browser cannot solve, the component embeds the login browser's live view so the person can complete it, then resumes the login when they confirm. If the site asks for another check after the person continues, the screen shows a short note. Sites that restrict embedded content with a Content-Security-Policy need to allow the live view origin in `frame-src`. The `captcha_blocked` error screen now shows the API's `error_message`, which says whether a person was asked to verify and didn't finish in time. |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| import { afterAll, beforeAll, describe, expect, test } from "bun:test"; | ||
| import { createElement } from "react"; | ||
| import { act, create } from "react-test-renderer"; | ||
| import { AppearanceProvider } from "../appearance/context"; | ||
| import { LocalizationProvider } from "../localization/context"; | ||
| import { HumanInterventionStep } from "./HumanInterventionStep"; | ||
|
|
||
| const LIVE_VIEW = "https://live.example.com/browser/live/abc"; | ||
| let listener: ((event: MessageEvent) => void) | undefined; | ||
|
|
||
| function step(note?: string, liveViewUrl = LIVE_VIEW) { | ||
| return createElement( | ||
| AppearanceProvider, | ||
| null, | ||
| createElement( | ||
| LocalizationProvider, | ||
| null, | ||
| createElement(HumanInterventionStep, { | ||
| targetDomain: "example.com", | ||
| liveViewUrl, | ||
| onDone: () => {}, | ||
| note, | ||
| }), | ||
| ), | ||
| ); | ||
| } | ||
|
|
||
| function render(note?: string) { | ||
| let renderer!: ReturnType<typeof create>; | ||
| act(() => { | ||
| renderer = create(step(note)); | ||
| }); | ||
| return renderer; | ||
| } | ||
|
|
||
| const statusText = (renderer: ReturnType<typeof create>) => | ||
| renderer.root | ||
| .findAll((node) => node.props.role === "status" && node.type === "p") | ||
| .map((node) => node.children.join("")); | ||
|
|
||
| describe("HumanInterventionStep", () => { | ||
| // The step listens for live view connection failures on window. | ||
| const hadWindow = "window" in globalThis; | ||
| beforeAll(() => { | ||
| if (!hadWindow) { | ||
| Object.assign(globalThis, { | ||
| window: { | ||
| addEventListener: (_: string, fn: (event: MessageEvent) => void) => { | ||
| listener = fn; | ||
| }, | ||
| removeEventListener: () => { | ||
| listener = undefined; | ||
| }, | ||
| }, | ||
| }); | ||
| } | ||
| }); | ||
| afterAll(() => { | ||
| if (!hadWindow) delete (globalThis as { window?: unknown }).window; | ||
| }); | ||
|
|
||
| test("shows the note when the site asks again", () => { | ||
| const note = "example.com is asking to verify again."; | ||
| expect(statusText(render(note))).toEqual([note]); | ||
| }); | ||
|
|
||
| test("shows no note on the first pause", () => { | ||
| expect(statusText(render())).toEqual([]); | ||
| }); | ||
|
|
||
| test("starts the live view fresh when its url changes", () => { | ||
| const renderer = render(); | ||
| act(() => | ||
| listener?.({ | ||
| origin: "https://live.example.com", | ||
| data: { type: "KERNEL_CONNECTION_FAILED", reason: "unsupported" }, | ||
| } as MessageEvent), | ||
| ); | ||
| const alerts = () => | ||
| renderer.root.findAll((node) => node.props.role === "alert"); | ||
| expect(alerts()).toHaveLength(1); | ||
|
|
||
| const next = "https://live.example.com/browser/live/def"; | ||
| act(() => renderer.update(step(undefined, next))); | ||
|
|
||
| expect(alerts()).toHaveLength(0); | ||
| expect(renderer.root.findByType("iframe" as never).props.src).toBe(next); | ||
| act(() => renderer.unmount()); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import { useSlot } from "../appearance/context"; | ||
| import { useLocalization } from "../localization/context"; | ||
| import { extractDomainName } from "../lib/profile-name"; | ||
| import { Button } from "./primitives/Button"; | ||
| import { ErrorBanner } from "./ErrorBanner"; | ||
| import { LiveViewFrame } from "./LiveViewFrame"; | ||
|
|
||
| interface HumanInterventionStepProps { | ||
| targetDomain: string; | ||
| liveViewUrl?: string | null; | ||
| onDone: () => void; | ||
| isLoading?: boolean; | ||
| errorMessage?: string; | ||
| /** Shown when the site asks for a check again after the person continued. */ | ||
| note?: string | null; | ||
| } | ||
|
|
||
| export function HumanInterventionStep({ | ||
| targetDomain, | ||
| liveViewUrl, | ||
| onDone, | ||
| isLoading, | ||
| errorMessage, | ||
| note, | ||
| }: HumanInterventionStepProps) { | ||
| const slot = useSlot(); | ||
| const l = useLocalization(); | ||
|
|
||
| return ( | ||
| <div className="kma-step kma-human-intervention"> | ||
| <div className="kma-step__header kma-human-intervention__header"> | ||
| <h1 {...slot("title", "kma-title")}>{l.humanInterventionTitle}</h1> | ||
| </div> | ||
|
|
||
| {note && ( | ||
| <p | ||
| {...slot("humanInterventionNote", "kma-human-intervention__note")} | ||
| role="status" | ||
| > | ||
| {note} | ||
| </p> | ||
| )} | ||
| {errorMessage && <ErrorBanner message={errorMessage} />} | ||
|
|
||
| <LiveViewFrame key={liveViewUrl ?? ""} url={liveViewUrl} /> | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. optional: the reload/retry counters in |
||
| <p {...slot("humanInterventionMessage", "kma-human-intervention__hint")}> | ||
| {l.humanInterventionMessage(extractDomainName(targetDomain))} | ||
| </p> | ||
|
|
||
| <Button onClick={onDone} disabled={isLoading}> | ||
| {l.humanInterventionDone} | ||
| </Button> | ||
| </div> | ||
| ); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.