Files
narl-workspace/web/src/lib/Verify.svelte
T
nvrl e0a4c01654 backup: the Proton login runs from the workspace
:backup login <username> shows Proton's third-party disclosure (and a
warning when no session key file seals the stored session), asks for
the password, then for a two-factor code when the account has one. If
Proton answers with a human check, its own verify.proton.me page opens
in a sheet; the token it posts back resumes the login. :backup logout
forgets the session and ends it on Proton's side where Proton answers.
The mirror picks up a new session within a minute, without a restart.

The login is split into steps in backup::proton (start, second_factor,
verified), and a half-done login waits in memory for five minutes at
most. Every step goes through its own throttle, so the workspace cannot
hammer Proton into locking the account. FIDO2-only accounts are told to
add an authenticator app. The CLI runs over the same steps and points
to :backup login when Proton wants a human check. The CSP lets
verify.proton.me into a frame and nothing else.
2026-10-20 12:54:03 +02:00

74 lines
2.2 KiB
Svelte

<script lang="ts">
/*
* Proton's human check, when a login from `:backup login` runs into one:
* Proton's own page in a frame, which hands back a token once it is solved.
* Only messages from that page are listened to. Closing the sheet — Escape,
* or a click beside it — abandons the login.
*/
import { onDestroy } from 'svelte'
import { workspace } from './store.svelte'
const ORIGIN = 'https://verify.proton.me'
const asked = workspace.verify
let height = $state(560)
function onmessage(event: MessageEvent) {
if (event.origin !== ORIGIN) return
let data = event.data
if (typeof data === 'string') {
try {
data = JSON.parse(data)
} catch {
return
}
}
const payload = data?.payload
if (data?.type === 'HUMAN_VERIFICATION_SUCCESS' && typeof payload?.token === 'string') {
asked?.resolve(payload.token)
workspace.overlay = 'none'
} else if (data?.type === 'RESIZE' && typeof payload?.height === 'number') {
height = Math.min(Math.max(payload.height, 320), 900)
} else if (data?.type === 'CLOSE') {
workspace.overlay = 'none'
}
}
onDestroy(() => asked?.resolve(null))
</script>
<svelte:window {onmessage} />
<div class="sheet">
<header>
<span class="label rule"><span class="marker"></span><span class="title">proton</span> prove you are human to log in</span>
<button onclick={() => (workspace.overlay = 'none')}>esc</button>
</header>
{#if asked}
<iframe
title="Proton human verification"
src={asked.url}
style:height="{height}px"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
referrerpolicy="no-referrer"
></iframe>
{/if}
</div>
<style>
.sheet {
display: flex;
flex-direction: column;
gap: 16px;
width: min(100%, 64ch);
max-height: 100%;
overflow-y: auto;
background: color-mix(in oklab, var(--hull) 97%, transparent);
border: 1px solid var(--surface-alt);
border-radius: var(--frame-radius);
padding: 20px 24px;
}
header { display: flex; align-items: center; gap: 12px; }
header .label { flex: 1; }
iframe { width: 100%; border: 0; border-radius: 6px; background: #fff; }
</style>