From 7ea123386554544bf64c8daba1b718e755d2be29 Mon Sep 17 00:00:00 2001 From: Rinse Date: Mon, 23 Feb 2026 02:24:52 +0000 Subject: [PATCH 1/2] feat: auto-submit iframe challenge answers via postMessage When a url/iframe challenge (e.g. OAuth, CAPTCHA) completes inside an iframe, the iframe can now signal completion via postMessage and the client auto-submits the challenge answers without manual user action. --- src/hooks/actions/actions.ts | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/hooks/actions/actions.ts b/src/hooks/actions/actions.ts index 23ffad50..2dd168a9 100644 --- a/src/hooks/actions/actions.ts +++ b/src/hooks/actions/actions.ts @@ -1,4 +1,4 @@ -import {useMemo, useState} from 'react' +import {useEffect, useMemo, useState} from 'react' import useAccountsStore from '../../stores/accounts' import Logger from '@plebbit/plebbit-logger' const log = Logger('plebbit-react-hooks:actions:hooks') @@ -36,6 +36,28 @@ const publishChallengeAnswersNotReady: PublishChallengeAnswers = async (challeng throw Error(`can't call publishChallengeAnswers() before result.challenge is defined (before the challenge message is received)`) } +function useIframeChallengeAutoAnswer(challenge: Challenge | undefined, publishChallengeAnswers: PublishChallengeAnswers | undefined) { + const iframeChallengeUrl = challenge?.challenges?.find((c) => c.type === 'url/iframe')?.challenge + + useEffect(() => { + if (!iframeChallengeUrl || !publishChallengeAnswers) return + + let iframeOrigin: string | undefined + try { + iframeOrigin = new URL(iframeChallengeUrl).origin + } catch (e) {} + + const handleMessage = (event: MessageEvent) => { + if (iframeOrigin && event.origin !== iframeOrigin) return + if (event.data?.type === 'challengeAnswer' && Array.isArray(event.data.challengeAnswers)) { + publishChallengeAnswers(event.data.challengeAnswers) + } + } + window.addEventListener('message', handleMessage) + return () => window.removeEventListener('message', handleMessage) + }, [iframeChallengeUrl, publishChallengeAnswers]) +} + export function useSubscribe(options?: UseSubscribeOptions): UseSubscribeResult { assert(!options || typeof options === 'object', `useSubscribe options argument '${options}' not an object`) const {subplebbitAddress, accountName, onError} = options || {} @@ -155,6 +177,7 @@ export function usePublishComment(options?: UsePublishCommentOptions): UsePublis const [challenge, setChallenge] = useState() const [challengeVerification, setChallengeVerification] = useState() const [publishChallengeAnswers, setPublishChallengeAnswers] = useState() + useIframeChallengeAutoAnswer(challenge, publishChallengeAnswers) let initialState = 'initializing' // before the accountId and options is defined, nothing can happen @@ -228,6 +251,7 @@ export function usePublishVote(options?: UsePublishVoteOptions): UsePublishVoteR const [challenge, setChallenge] = useState() const [challengeVerification, setChallengeVerification] = useState() const [publishChallengeAnswers, setPublishChallengeAnswers] = useState() + useIframeChallengeAutoAnswer(challenge, publishChallengeAnswers) let initialState = 'initializing' // before the accountId and options is defined, nothing can happen @@ -299,6 +323,7 @@ export function usePublishCommentEdit(options?: UsePublishCommentEditOptions): U const [challenge, setChallenge] = useState() const [challengeVerification, setChallengeVerification] = useState() const [publishChallengeAnswers, setPublishChallengeAnswers] = useState() + useIframeChallengeAutoAnswer(challenge, publishChallengeAnswers) let initialState = 'initializing' // before the accountId and options is defined, nothing can happen @@ -370,6 +395,7 @@ export function usePublishCommentModeration(options?: UsePublishCommentModeratio const [challenge, setChallenge] = useState() const [challengeVerification, setChallengeVerification] = useState() const [publishChallengeAnswers, setPublishChallengeAnswers] = useState() + useIframeChallengeAutoAnswer(challenge, publishChallengeAnswers) let initialState = 'initializing' // before the accountId and options is defined, nothing can happen @@ -441,6 +467,7 @@ export function usePublishSubplebbitEdit(options?: UsePublishSubplebbitEditOptio const [challenge, setChallenge] = useState() const [challengeVerification, setChallengeVerification] = useState() const [publishChallengeAnswers, setPublishChallengeAnswers] = useState() + useIframeChallengeAutoAnswer(challenge, publishChallengeAnswers) let initialState = 'initializing' // before the accountId and options is defined, nothing can happen From 73371c03c27866fbcd02493e03be9f05dfd7c1dc Mon Sep 17 00:00:00 2001 From: Rinse Date: Wed, 25 Feb 2026 04:25:37 +0000 Subject: [PATCH 2/2] fix: multi-challenge support and TypeScript build for iframe auto-answer - Fix TypeScript implicit any error by adding type annotations - Only auto-submit when all challenges are url/iframe type - Collect answers positionally from each iframe via postMessage - Use 'challengeanswer' (lowercase) as message type --- dist/hooks/actions/actions.js | 43 ++++++++++++++++++++++++++++++++++- src/hooks/actions/actions.ts | 36 +++++++++++++++++++++-------- 2 files changed, 68 insertions(+), 11 deletions(-) diff --git a/dist/hooks/actions/actions.js b/dist/hooks/actions/actions.js index 95705160..8797a23a 100644 --- a/dist/hooks/actions/actions.js +++ b/dist/hooks/actions/actions.js @@ -18,7 +18,7 @@ var __rest = (this && this.__rest) || function (s, e) { } return t; }; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import useAccountsStore from '../../stores/accounts'; import Logger from '@plebbit/plebbit-logger'; const log = Logger('plebbit-react-hooks:actions:hooks'); @@ -27,6 +27,42 @@ import { useAccount, useAccountId } from '../accounts'; const publishChallengeAnswersNotReady = (challengeAnswers) => __awaiter(void 0, void 0, void 0, function* () { throw Error(`can't call publishChallengeAnswers() before result.challenge is defined (before the challenge message is received)`); }); +function useIframeChallengeAutoAnswer(challenge, publishChallengeAnswers) { + const challenges = challenge === null || challenge === void 0 ? void 0 : challenge.challenges; + const allIframe = (challenges === null || challenges === void 0 ? void 0 : challenges.length) ? challenges.every((c) => c.type === 'url/iframe') : false; + useEffect(() => { + if (!allIframe || !challenges || !publishChallengeAnswers) + return; + const origins = challenges.map((c) => { + try { + return new URL(c.challenge).origin; + } + catch (e) { + return undefined; + } + }); + const answers = new Array(challenges.length).fill(undefined); + let submitted = false; + const handleMessage = (event) => { + var _a; + if (submitted) + return; + if (((_a = event.data) === null || _a === void 0 ? void 0 : _a.type) !== 'challengeanswer' || typeof event.data.challengeAnswer !== 'string') + return; + // find which iframe sent this by matching origin to first unfilled slot + const index = origins.findIndex((origin, i) => answers[i] === undefined && (!origin || origin === event.origin)); + if (index === -1) + return; + answers[index] = event.data.challengeAnswer; + if (answers.every((a) => a !== undefined)) { + submitted = true; + publishChallengeAnswers(answers); + } + }; + window.addEventListener('message', handleMessage); + return () => window.removeEventListener('message', handleMessage); + }, [allIframe, challenges, publishChallengeAnswers]); +} export function useSubscribe(options) { var _a; assert(!options || typeof options === 'object', `useSubscribe options argument '${options}' not an object`); @@ -137,6 +173,7 @@ export function usePublishComment(options) { const [challenge, setChallenge] = useState(); const [challengeVerification, setChallengeVerification] = useState(); const [publishChallengeAnswers, setPublishChallengeAnswers] = useState(); + useIframeChallengeAutoAnswer(challenge, publishChallengeAnswers); let initialState = 'initializing'; // before the accountId and options is defined, nothing can happen if (accountId && options) { @@ -201,6 +238,7 @@ export function usePublishVote(options) { const [challenge, setChallenge] = useState(); const [challengeVerification, setChallengeVerification] = useState(); const [publishChallengeAnswers, setPublishChallengeAnswers] = useState(); + useIframeChallengeAutoAnswer(challenge, publishChallengeAnswers); let initialState = 'initializing'; // before the accountId and options is defined, nothing can happen if (accountId && options) { @@ -263,6 +301,7 @@ export function usePublishCommentEdit(options) { const [challenge, setChallenge] = useState(); const [challengeVerification, setChallengeVerification] = useState(); const [publishChallengeAnswers, setPublishChallengeAnswers] = useState(); + useIframeChallengeAutoAnswer(challenge, publishChallengeAnswers); let initialState = 'initializing'; // before the accountId and options is defined, nothing can happen if (accountId && options) { @@ -325,6 +364,7 @@ export function usePublishCommentModeration(options) { const [challenge, setChallenge] = useState(); const [challengeVerification, setChallengeVerification] = useState(); const [publishChallengeAnswers, setPublishChallengeAnswers] = useState(); + useIframeChallengeAutoAnswer(challenge, publishChallengeAnswers); let initialState = 'initializing'; // before the accountId and options is defined, nothing can happen if (accountId && options) { @@ -387,6 +427,7 @@ export function usePublishSubplebbitEdit(options) { const [challenge, setChallenge] = useState(); const [challengeVerification, setChallengeVerification] = useState(); const [publishChallengeAnswers, setPublishChallengeAnswers] = useState(); + useIframeChallengeAutoAnswer(challenge, publishChallengeAnswers); let initialState = 'initializing'; // before the accountId and options is defined, nothing can happen if (accountId && subplebbitAddress) { diff --git a/src/hooks/actions/actions.ts b/src/hooks/actions/actions.ts index 2dd168a9..8f47fa73 100644 --- a/src/hooks/actions/actions.ts +++ b/src/hooks/actions/actions.ts @@ -37,25 +37,41 @@ const publishChallengeAnswersNotReady: PublishChallengeAnswers = async (challeng } function useIframeChallengeAutoAnswer(challenge: Challenge | undefined, publishChallengeAnswers: PublishChallengeAnswers | undefined) { - const iframeChallengeUrl = challenge?.challenges?.find((c) => c.type === 'url/iframe')?.challenge + const challenges: {type: string; challenge: string}[] | undefined = challenge?.challenges + const allIframe = challenges?.length ? challenges.every((c: {type: string; challenge: string}) => c.type === 'url/iframe') : false useEffect(() => { - if (!iframeChallengeUrl || !publishChallengeAnswers) return + if (!allIframe || !challenges || !publishChallengeAnswers) return - let iframeOrigin: string | undefined - try { - iframeOrigin = new URL(iframeChallengeUrl).origin - } catch (e) {} + const origins = challenges.map((c: {type: string; challenge: string}) => { + try { + return new URL(c.challenge).origin + } catch (e) { + return undefined + } + }) + + const answers: (string | undefined)[] = new Array(challenges.length).fill(undefined) + let submitted = false const handleMessage = (event: MessageEvent) => { - if (iframeOrigin && event.origin !== iframeOrigin) return - if (event.data?.type === 'challengeAnswer' && Array.isArray(event.data.challengeAnswers)) { - publishChallengeAnswers(event.data.challengeAnswers) + if (submitted) return + if (event.data?.type !== 'challengeanswer' || typeof event.data.challengeAnswer !== 'string') return + + // find which iframe sent this by matching origin to first unfilled slot + const index = origins.findIndex((origin, i) => answers[i] === undefined && (!origin || origin === event.origin)) + if (index === -1) return + + answers[index] = event.data.challengeAnswer + + if (answers.every((a) => a !== undefined)) { + submitted = true + publishChallengeAnswers(answers as string[]) } } window.addEventListener('message', handleMessage) return () => window.removeEventListener('message', handleMessage) - }, [iframeChallengeUrl, publishChallengeAnswers]) + }, [allIframe, challenges, publishChallengeAnswers]) } export function useSubscribe(options?: UseSubscribeOptions): UseSubscribeResult {