diff --git a/src/app/background/index.ts b/src/app/background/index.ts index bf308f9..d03ff10 100644 --- a/src/app/background/index.ts +++ b/src/app/background/index.ts @@ -164,11 +164,6 @@ async function handleRequest(request: ExtensionRequest, sender: Browser.runtime. request.payload.timeoutMs, )); } - case 'authorization.yakit.open': - return ok(await engineBridge.requestEngine( - 'yakit.browser_authorization.open', - request.payload, - )); case 'authorization.yakit.instances': return ok(await engineBridge.requestEngine('yakit.browser_authorization.instances', {})); case 'context.capture': { diff --git a/src/entrypoints/options/App.tsx b/src/entrypoints/options/App.tsx index 24619fb..4d7d8a1 100644 --- a/src/entrypoints/options/App.tsx +++ b/src/entrypoints/options/App.tsx @@ -238,7 +238,7 @@ function App() {
{section === 'overview' && } - {section === 'authorization' && } + {section === 'authorization' && } {section === 'proxies' && } {section === 'rules' && } {section === 'sources' && } diff --git a/src/features/authorization-testing/engine.ts b/src/features/authorization-testing/engine.ts index d77732e..8122c3f 100644 --- a/src/features/authorization-testing/engine.ts +++ b/src/features/authorization-testing/engine.ts @@ -62,6 +62,7 @@ export interface BrowserAuthorizationWorkspace { mode: BrowserAuthorizationMode; state: 'ready' | 'conditional' | 'blocked' | 'stale'; left: { + deviceId: string; accountLabel?: string; origin: string; target: { tabId: number; frameId: number; documentId: string }; @@ -316,6 +317,9 @@ export interface BrowserAuthorizationEvidenceValidation { export type BrowserAuthorizationTaskSchema = | 'authorization.workspace.create' | 'authorization.workspace.inspect' + | 'authorization.capture.start' + | 'authorization.capture.status' + | 'authorization.capture.stop' | 'authorization.baseline.candidates' | 'authorization.baseline.bind' | 'authorization.logical.bind' diff --git a/src/features/authorization-testing/protocol.ts b/src/features/authorization-testing/protocol.ts index 75bdce2..208ea23 100644 --- a/src/features/authorization-testing/protocol.ts +++ b/src/features/authorization-testing/protocol.ts @@ -219,6 +219,9 @@ export function normalizeBrowserAuthorizationTaskResult( schema: BrowserAuthorizationTaskSchema, value: unknown, ): T { + if (schema.startsWith('authorization.capture.')) { + return objectValue(value, schema, '$') as T; + } if (schema === 'authorization.baseline.candidates') { if (value === undefined || value === null) return [] as T; if (!Array.isArray(value)) mismatch(schema, '$', '数组或空值'); diff --git a/src/features/authorization-testing/ui/AuthorizationTestingWorkspace.tsx b/src/features/authorization-testing/ui/AuthorizationTestingWorkspace.tsx index 1d7fb6d..bcdceb4 100644 --- a/src/features/authorization-testing/ui/AuthorizationTestingWorkspace.tsx +++ b/src/features/authorization-testing/ui/AuthorizationTestingWorkspace.tsx @@ -1,15 +1,13 @@ -import { useCallback, useEffect, useMemo, useReducer, useState } from 'react'; +import { useCallback, useEffect, useReducer, useState } from 'react'; import { browser } from 'wxt/browser'; import { AlertTriangle, ArrowRight, Check, CircleCheck, ExternalLink, Fingerprint, - LockKeyhole, Play, RefreshCw, RotateCcw, ShieldAlert, Square, UserRoundPlus, + LockKeyhole, Play, RefreshCw, RotateCcw, ShieldAlert, Square, } from 'lucide-react'; import { Button } from '@/components/ui/button'; -import { authorizationShareGrantInput } from '@/features/grants/gateway-share'; import { errorMessage, request } from '@/platform/messaging/runtime'; import type { - ActiveTabInfo, BridgeStatus, BrowserAuthorizationInstance, BrowserIsolationContext, BrowserIsolationInspection, - ExtensionState, NetworkCaptureStatus, + ActiveTabInfo, BridgeStatus, BrowserAuthorizationInstance, NetworkCaptureStatus, } from '@/types/models'; import { runBrowserAuthorizationTask, @@ -19,10 +17,6 @@ import { type BrowserAuthorizationWorkspace, } from '../engine'; import './authorization-testing-workspace.css'; -import { - authorizationIdentityOptionDisabledReason, - normalizeAuthorizationIdentityTabSelection, -} from './identity-selection'; import { authorizationWorkspaceUIReducer, INITIAL_AUTHORIZATION_WORKSPACE_UI, @@ -34,15 +28,10 @@ import { } from './AuthorizationEvidenceWorkbench'; import { IdentitySlot } from './IdentitySlot'; -const SESSION_KEY = 'session.authorization-testing-workspace-ui.v1'; +const SESSION_KEY = 'session.authorization-testing-workspace-ui.v2'; interface AuthorizationTestingWorkspaceProps { - state: ExtensionState; - setState: (state: ExtensionState) => void; - tabs: ActiveTabInfo[]; - activeTab?: ActiveTabInfo; bridge: BridgeStatus; - refreshTabs: () => Promise; run: (task: () => Promise, success?: string) => Promise; busy: boolean; } @@ -63,6 +52,12 @@ function shortHost(tab?: ActiveTabInfo): string { } } +async function stopWorkspaceCapture(workspaceId: string): Promise { + await Promise.allSettled((['left', 'right'] as const).map((side) => ( + runBrowserAuthorizationTask('authorization.capture.stop', { workspaceId, side }) + ))); +} + function formatWorkspaceRemaining(expiresAt: number, now: number): string { const remainingSeconds = Math.max(0, Math.ceil((expiresAt - now) / 1_000)); if (remainingSeconds < 60) return `${remainingSeconds} 秒`; @@ -70,13 +65,6 @@ function formatWorkspaceRemaining(expiresAt: number, now: number): string { return minutes < 60 ? `${minutes} 分钟` : `${Math.floor(minutes / 60)} 小时 ${minutes % 60} 分钟`; } -function contextForTab( - inspection: BrowserIsolationInspection | undefined, - tabId: number | undefined, -): BrowserIsolationContext | undefined { - return inspection?.contexts.find((context) => tabId && context.tabIds.includes(tabId)); -} - function proofLabel(workspace?: BrowserAuthorizationWorkspace): string { if (!workspace) return '尚未验证'; if (workspace.proof.level === 'strong') return '强隔离'; @@ -212,19 +200,10 @@ function newestComparableAuthorizationPair( } export function AuthorizationTestingWorkspace({ - state, - setState, - tabs, - activeTab, bridge, - refreshTabs, run, busy, }: AuthorizationTestingWorkspaceProps) { - const eligibleTabs = useMemo( - () => tabs.filter((item) => item.url.startsWith('http://') || item.url.startsWith('https://')), - [tabs], - ); const [hydrated, setHydrated] = useState(false); const [ui, dispatch] = useReducer( authorizationWorkspaceUIReducer, @@ -232,11 +211,12 @@ export function AuthorizationTestingWorkspace({ ); const { mode, + leftDeviceId, + rightDeviceId, leftTabId, rightTabId, leftLabel, rightLabel, - inspection, workspace, candidates, selected, @@ -245,49 +225,28 @@ export function AuthorizationTestingWorkspace({ canaryPaths, } = ui; const [localError, setLocalError] = useState(''); - const [identityNotice, setIdentityNotice] = useState(''); const [clock, setClock] = useState(Date.now()); const [browserInstances, setBrowserInstances] = useState([]); - const [targetDeviceId, setTargetDeviceId] = useState(''); - - const leftTab = eligibleTabs.find((item) => item.id === leftTabId); - const rightTab = eligibleTabs.find((item) => item.id === rightTabId); - const leftContext = contextForTab(inspection, leftTabId); - const rightContext = contextForTab(inspection, rightTabId); - const leftIsolationContextId = leftContext?.contextId || leftTab?.isolationContextId; - const rightIsolationContextId = rightContext?.contextId || rightTab?.isolationContextId; - const identityContextsSeparated = Boolean( - leftIsolationContextId - && rightIsolationContextId - && leftIsolationContextId !== rightIsolationContextId, - ); + const leftInstance = browserInstances.find((instance) => instance.deviceId === leftDeviceId); + const rightInstance = browserInstances.find((instance) => instance.deviceId === rightDeviceId); + const leftTab = leftInstance?.tabs.find((item) => item.id === leftTabId); + const rightTab = rightInstance?.tabs.find((item) => item.id === rightTabId); const sameOrigin = Boolean(leftTab && rightTab && tabOrigin(leftTab) === tabOrigin(rightTab)); const capabilityReady = bridge.state === 'connected' && Boolean(bridge.capabilities?.includes('yakit.browser_authorization.task')); const instanceDiscoveryReady = bridge.state === 'connected' && Boolean(bridge.capabilities?.includes('yakit.browser_authorization.instances')); - const targetBrowserInstance = browserInstances.find((instance) => instance.deviceId === targetDeviceId); - - const refreshInspection = useCallback(async () => { - const next = await request('isolation.inspect', { - tabIds: eligibleTabs.length > 0 ? eligibleTabs.map((item) => item.id) : undefined, - }); - dispatch({ type: 'patch', value: { inspection: next } }); - }, [eligibleTabs]); const refreshBrowserInstances = useCallback(async () => { if (!instanceDiscoveryReady) { setBrowserInstances([]); - setTargetDeviceId(''); return []; } const result = await request('authorization.yakit.instances'); + if (!Array.isArray(result.instances) || result.instances.some((instance) => !Array.isArray(instance.tabs))) { + throw new Error('当前 Yak 引擎不支持插件内 A/B 越权测试,请更新引擎并重新连接两个浏览器实例'); + } setBrowserInstances(result.instances); - setTargetDeviceId((current) => ( - result.instances.some((instance) => !instance.current && instance.deviceId === current) - ? current - : result.instances.find((instance) => !instance.current)?.deviceId || '' - )); return result.instances; }, [instanceDiscoveryReady]); @@ -305,37 +264,42 @@ export function AuthorizationTestingWorkspace({ }, []); useEffect(() => { - if (!hydrated || workspace) return; - const normalized = normalizeAuthorizationIdentityTabSelection({ - eligibleTabIds: eligibleTabs.map((item) => item.id), - activeTabId: activeTab?.id, - leftTabId, - rightTabId, + if (!hydrated || workspace || !browserInstances.length) return; + const left = browserInstances.find((instance) => instance.current); + const right = browserInstances.find((instance) => ( + !instance.current && instance.deviceId === rightDeviceId + )) || browserInstances.find((instance) => !instance.current); + const nextLeftTabId = left?.tabs.find((tab) => tab.id === leftTabId)?.id + || left?.tabs.find((tab) => tab.active)?.id + || left?.tabs[0]?.id; + const nextRightTabId = right?.tabs.find((tab) => tab.id === rightTabId)?.id + || right?.tabs.find((tab) => tab.active)?.id + || right?.tabs[0]?.id; + if (leftDeviceId === (left?.deviceId || '') + && rightDeviceId === (right?.deviceId || '') + && leftTabId === nextLeftTabId + && rightTabId === nextRightTabId) return; + dispatch({ + type: 'patch', + value: { + leftDeviceId: left?.deviceId || '', + rightDeviceId: right?.deviceId || '', + leftTabId: nextLeftTabId, + rightTabId: nextRightTabId, + }, }); - if (normalized.leftTabId !== leftTabId || normalized.rightTabId !== rightTabId) { - dispatch({ - type: 'patch', - value: { - leftTabId: normalized.leftTabId, - rightTabId: normalized.rightTabId, - }, - }); - } - }, [activeTab?.id, eligibleTabs, hydrated, leftTabId, rightTabId, workspace]); + }, [browserInstances, hydrated, leftDeviceId, leftTabId, rightDeviceId, rightTabId, workspace]); useEffect(() => { if (!hydrated) return; const value = persistedAuthorizationWorkspaceUI(ui); void browser.storage.session.set({ [SESSION_KEY]: value }).catch(() => undefined); }, [ - canaryPaths, candidates, hydrated, leftLabel, leftTabId, mode, rightLabel, rightTabId, + canaryPaths, candidates, hydrated, leftDeviceId, leftLabel, leftTabId, mode, + rightDeviceId, rightLabel, rightTabId, selected, selectedPlanCandidateId, workspace, ]); - useEffect(() => { - void refreshInspection().catch((error) => setLocalError(errorMessage(error))); - }, [refreshInspection]); - useEffect(() => { void refreshBrowserInstances().catch((error) => setLocalError(errorMessage(error))); const refreshWhenVisible = () => { @@ -347,56 +311,17 @@ export function AuthorizationTestingWorkspace({ return () => document.removeEventListener('visibilitychange', refreshWhenVisible); }, [refreshBrowserInstances]); - useEffect(() => { - if (!hydrated || workspace || !leftTab || !rightTab) return; - const reason = authorizationIdentityOptionDisabledReason({ - candidateTabId: rightTab.id, - candidateIsolationContextId: rightIsolationContextId, - otherTabId: leftTab.id, - otherIsolationContextId: leftIsolationContextId, - otherLabel: '身份 A', - }); - if (!reason) return; - dispatch({ type: 'patch', value: { rightTabId: undefined } }); - setIdentityNotice( - leftTab.id === rightTab.id - ? '身份 B 已清空:同一个页面不能同时代表两个身份' - : '身份 B 已清空:该页面与身份 A 共享同一登录态', - ); - }, [ - hydrated, - leftIsolationContextId, - leftTab?.id, - rightIsolationContextId, - rightTab?.id, - workspace, - ]); - useEffect(() => { if (!workspace) return; void Promise.all((['left', 'right'] as const).map(async (side) => { - const target = workspace[side].target; - const status = await request('network.capture.status', target); + const status = await runBrowserAuthorizationTask( + 'authorization.capture.status', + { workspaceId: workspace.id, side }, + ); dispatch({ type: 'capture.update', side, status }); })).catch(() => undefined); }, [workspace?.id]); - useEffect(() => { - const listener = (message: unknown) => { - const input = message as { action?: string; payload?: { tabId?: number } }; - if (input?.action !== 'network.capture.changed') return; - const side = input.payload?.tabId === workspace?.left.target.tabId - ? 'left' - : input.payload?.tabId === workspace?.right.target.tabId ? 'right' : undefined; - if (!side || !workspace) return; - void request('network.capture.status', workspace[side].target) - .then((status) => dispatch({ type: 'capture.update', side, status })) - .catch(() => undefined); - }; - browser.runtime.onMessage.addListener(listener); - return () => browser.runtime.onMessage.removeListener(listener); - }, [workspace]); - useEffect(() => { if (!workspace) return undefined; setClock(Date.now()); @@ -405,6 +330,7 @@ export function AuthorizationTestingWorkspace({ }, [workspace?.id]); const resetWorkspace = async () => { + if (workspace) await stopWorkspaceCapture(workspace.id); dispatch({ type: 'workspace.reset' }); setLocalError(''); await browser.storage.session.remove(SESSION_KEY).catch(() => undefined); @@ -412,133 +338,72 @@ export function AuthorizationTestingWorkspace({ const assignIdentityTab = (side: BrowserAuthorizationSide, nextTabId: number | undefined) => { setLocalError(''); - setIdentityNotice(''); dispatch({ type: 'patch', value: side === 'left' ? { leftTabId: nextTabId } : { rightTabId: nextTabId }, }); }; - const openIncognitoSettings = () => run(async () => { - await browser.tabs.create({ url: `chrome://extensions/?id=${browser.runtime.id}` }); - }, '已打开扩展详情,请开启“允许在无痕模式下运行”'); - - const recheckIsolationCapability = () => run(async () => { - await refreshTabs(); - await refreshInspection(); - }, '浏览器隔离能力已重新检测'); - - const createIsolatedIdentity = () => run(async () => { - if (!leftTab) throw new Error('请先选择身份 A 的页面'); - const result = inspection?.browser === 'firefox' - ? await request('isolation.container.open', { url: leftTab.url, name: rightLabel || '账号 B' }) - : await request('isolation.incognito.open', { url: leftTab.url }); - await refreshTabs(); - dispatch({ type: 'patch', value: { rightTabId: result.tab.id } }); - await refreshInspection(); - }, inspection?.browser === 'firefox' ? '已创建独立 Container,请在新页面登录身份 B' : '已打开无痕身份页面,请在新页面登录身份 B'); + const assignRightInstance = (deviceId: string) => { + const instance = browserInstances.find((item) => item.deviceId === deviceId); + dispatch({ + type: 'patch', + value: { + rightDeviceId: deviceId, + rightTabId: instance?.tabs.find((tab) => tab.active)?.id || instance?.tabs[0]?.id, + }, + }); + }; const prepareWorkspace = () => run(async () => { setLocalError(''); - if (!leftTab || !rightTab) throw new Error('请选择身份 A 和身份 B 的页面'); - if (leftTab.id === rightTab.id) throw new Error('A/B 身份不能使用同一个标签页'); + if (!leftInstance || !rightInstance || !leftTab || !rightTab) throw new Error('请选择在线的 A/B 浏览器及页面'); + if (leftDeviceId === rightDeviceId) throw new Error('A/B 必须来自两个独立浏览器实例'); if (!sameOrigin) throw new Error('A/B 页面必须属于同一站点 Origin'); if (!capabilityReady) throw new Error('当前 Yak 引擎不支持插件授权测试任务,请更新并重新连接引擎'); - const nextState = await request('grant.create', authorizationShareGrantInput(state, [leftTab, rightTab])); - setState(nextState); const nextWorkspace = await runBrowserAuthorizationTask( 'authorization.workspace.create', { mode, - left: { tabId: leftTab.id, frameId: 0, accountLabel: leftLabel.trim() || '账号 A' }, - right: { tabId: rightTab.id, frameId: 0, accountLabel: rightLabel.trim() || '账号 B' }, + left: { deviceId: leftDeviceId, tabId: leftTab.id, frameId: 0, accountLabel: leftLabel.trim() || '账号 A' }, + right: { deviceId: rightDeviceId, tabId: rightTab.id, frameId: 0, accountLabel: rightLabel.trim() || '账号 B' }, }, ); dispatch({ type: 'workspace.initialize', workspace: nextWorkspace }); if (nextWorkspace.state === 'ready' || nextWorkspace.state === 'conditional') { - const [leftStatus, rightStatus] = await Promise.all([ - request('network.capture.start', { - ...nextWorkspace.left.target, - captureHeaders: true, - captureBody: true, - maxEntries: 200, - maxBodyBytes: 64 * 1024, - }), - request('network.capture.start', { - ...nextWorkspace.right.target, - captureHeaders: true, - captureBody: true, - maxEntries: 200, - maxBodyBytes: 64 * 1024, - }), - ]); + const leftStatus = await runBrowserAuthorizationTask( + 'authorization.capture.start', { workspaceId: nextWorkspace.id, side: 'left' }, + ); + let rightStatus: NetworkCaptureStatus; + try { + rightStatus = await runBrowserAuthorizationTask( + 'authorization.capture.start', { workspaceId: nextWorkspace.id, side: 'right' }, + ); + } catch (error) { + await stopWorkspaceCapture(nextWorkspace.id); + throw error; + } dispatch({ type: 'capture.replace', capture: { left: leftStatus, right: rightStatus } }); } }, 'A/B 身份已验证,双方请求捕获已开始'); - const openCrossBrowserWorkspace = () => run(async () => { - const sourceTab = leftTab || activeTab || eligibleTabs[0]; - if (!sourceTab) throw new Error('当前浏览器没有可用于测试的 HTTP(S) 页面'); - if (!instanceDiscoveryReady) throw new Error('当前 Yak 版本不支持读取在线实例,请更新引擎并重新连接插件'); - if (!bridge.capabilities?.includes('yakit.browser_authorization.open')) { - throw new Error('当前 Yak 引擎不支持打开跨浏览器工作区'); - } - const instances = await refreshBrowserInstances(); - const target = instances.find((instance) => ( - !instance.current && instance.deviceId === targetDeviceId - )) || instances.find((instance) => !instance.current); - if (!target) throw new Error('没有检测到另一个在线的 YTray 浏览器实例,请确认其插件已连接同一 Yak 引擎'); - setTargetDeviceId(target.deviceId); - await request('authorization.yakit.open', { - tabId: sourceTab.id, - mode, - targetDeviceId: target.deviceId, - }); - }, '已将两个浏览器实例带入 Yakit'); - const refreshWorkspaceDocuments = async (): Promise => { - if (!workspace || !leftTab || !rightTab) throw new Error('请先建立 A/B 工作区'); - const nextState = await request('grant.refresh'); - setState(nextState); - const grant = nextState.activeGrant; - const leftTarget = grant?.targets.find((target) => ( - target.tabId === workspace.left.target.tabId - && target.frameId === workspace.left.target.frameId - )); - const rightTarget = grant?.targets.find((target) => ( - target.tabId === workspace.right.target.tabId - && target.frameId === workspace.right.target.frameId - )); - if (!leftTarget || !rightTarget) { - throw new Error('当前共享会话已不再包含身份 A/B,请重新建立工作区'); - } - const documentChanged = ( - leftTarget.documentId !== workspace.left.target.documentId - || rightTarget.documentId !== workspace.right.target.documentId - ); - if (!documentChanged && workspace.expiresAt > Date.now()) return workspace; - + if (!workspace) throw new Error('请先建立 A/B 工作区'); const renewed = await runBrowserAuthorizationTask( - 'authorization.workspace.create', - { - mode: workspace.mode, - left: { - tabId: leftTab.id, - frameId: 0, - accountLabel: workspace.left.accountLabel || leftLabel.trim() || '账号 A', - }, - right: { - tabId: rightTab.id, - frameId: 0, - accountLabel: workspace.right.accountLabel || rightLabel.trim() || '账号 B', - }, - }, + 'authorization.workspace.inspect', { workspaceId: workspace.id, revalidate: true }, ); - dispatch({ type: 'workspace.initialize', workspace: renewed }); + if (renewed.state === 'stale' || renewed.state === 'blocked') { + throw new Error(renewed.recovery?.message || 'A/B 页面已变化,请新建工作区后重试'); + } + dispatch({ type: 'workspace.updated', workspace: renewed }); const [leftStatus, rightStatus] = await Promise.all([ - request('network.capture.status', renewed.left.target), - request('network.capture.status', renewed.right.target), + runBrowserAuthorizationTask( + 'authorization.capture.status', { workspaceId: renewed.id, side: 'left' }, + ), + runBrowserAuthorizationTask( + 'authorization.capture.status', { workspaceId: renewed.id, side: 'right' }, + ), ]); dispatch({ type: 'capture.replace', capture: { left: leftStatus, right: rightStatus } }); return renewed; @@ -677,10 +542,9 @@ export function AuthorizationTestingWorkspace({ const stopCapture = (side: BrowserAuthorizationSide) => run(async () => { if (!workspace) return; - const status = await request('network.capture.stop', { - tabId: workspace[side].target.tabId, - frameId: workspace[side].target.frameId, - }); + const status = await runBrowserAuthorizationTask( + 'authorization.capture.stop', { workspaceId: workspace.id, side }, + ); dispatch({ type: 'capture.update', side, status }); }, `${side === 'left' ? leftLabel : rightLabel} 的请求捕获已停止`); @@ -700,33 +564,30 @@ export function AuthorizationTestingWorkspace({ const executionCopy = workspace?.execution ? verdictCopy(workspace.execution.verdict, workspace.mode) : undefined; - const incognitoAccessDenied = inspection?.browser === 'chromium' - && inspection.capabilities.incognitoAccess === 'denied'; - const firefoxContainerUnavailable = inspection?.browser === 'firefox' - && !inspection.capabilities.containerTabs; const identityStageReady = Boolean( - leftTab && rightTab && sameOrigin && identityContextsSeparated && capabilityReady, + leftInstance && rightInstance && leftDeviceId !== rightDeviceId + && leftTab && rightTab && sameOrigin && capabilityReady, ); - const prepareHint = !leftTab - ? '先选择当前登录页作为身份 A' - : !rightTab - ? '还需要一个隔离登录的身份 B' + const prepareHint = !instanceDiscoveryReady + ? '请更新并连接支持在线实例的 Yak 引擎' + : !leftInstance + ? '当前 YTray 浏览器尚未被引擎识别' + : !rightInstance + ? '请先用 YTray 启动并连接另一个浏览器实例' + : !leftTab || !rightTab + ? '请在 A/B 浏览器中各打开一个 HTTP(S) 页面' : !sameOrigin ? 'A/B 页面必须属于同一站点' - : !leftIsolationContextId || !rightIsolationContextId - ? '正在确认两个页面的登录态边界' - : !identityContextsSeparated - ? 'A/B 页面仍然共享同一登录态' - : !capabilityReady - ? '请先连接支持授权测试的 Yak 引擎' - : '两个身份页面已就绪'; + : !capabilityReady + ? '请先连接支持授权测试的 Yak 引擎' + : '两个独立浏览器身份已就绪'; return
- Browser-native authorization testing -

授权测试工作区

-

从已经登录的两个页面建立身份隔离证明,录制双方正常请求,再由 Yak 生成并执行最小交叉矩阵。

+ 确定性授权测试 +

越权测试

+

选择两个在线 YTray 浏览器的登录页,插件监测双方正常请求并执行最小交叉矩阵。

@@ -741,16 +602,6 @@ export function AuthorizationTestingWorkspace({ {workspace && } - {workspace && bridge.capabilities?.includes('yakit.browser_authorization.open') && } @@ -764,7 +615,7 @@ export function AuthorizationTestingWorkspace({
{[ - ['1', '身份与隔离', Boolean(workspace)], + ['1', 'A/B 浏览器', Boolean(workspace)], ['2', '正常请求', Boolean(workspace?.baselines.left && workspace?.baselines.right)], ['3', '确定性计划', Boolean(workspace?.plan)], ['4', '结果证据', Boolean(workspace?.execution)], @@ -774,46 +625,6 @@ export function AuthorizationTestingWorkspace({
)}
- {!workspace &&
- -
- 选择独立浏览器实例 - {!instanceDiscoveryReady - ? '当前 Yak 版本不能读取在线实例,请更新引擎后重连。' - : browserInstances.length < 2 - ? '只检测到当前浏览器,请先用 YTray 启动并连接另一个实例。' - : '当前页面作为资源所有者,所选浏览器作为独立对照账号。'} -
- {browserInstances.map((instance) => )} - -
-
- -
} - {!workspace ?
测试类型 @@ -831,9 +642,9 @@ export function AuthorizationTestingWorkspace({
- {leftTab ? : '1'}当前登录页作为 A + {leftTab ? : '1'}浏览器 A 登录资源账号 - {rightTab ? : '2'}隔离页面登录 B + {rightTab ? : '2'}浏览器 B 登录对照账号 {identityStageReady ? : '3'}验证并开始捕获
@@ -844,66 +655,42 @@ export function AuthorizationTestingWorkspace({ title={mode === 'vertical' ? '低权限身份' : '身份 A'} label={leftLabel} setLabel={(value) => dispatch({ type: 'patch', value: { leftLabel: value } })} + instance={leftInstance} + instances={browserInstances} tabId={leftTabId} setTabId={(value) => assignIdentityTab('left', value)} - tabs={eligibleTabs} - context={leftContext} - disabledReason={(item) => authorizationIdentityOptionDisabledReason({ - candidateTabId: item.id, - candidateIsolationContextId: contextForTab(inspection, item.id)?.contextId || item.isolationContextId, - otherTabId: rightTabId, - otherIsolationContextId: rightIsolationContextId, - otherLabel: '身份 B', - })} - emptyHint="选择你现在已经登录的页面,作为基准身份 A" />
- {incognitoAccessDenied ? '需要无痕权限' : !leftTab ? '先准备身份 A' : !rightTab ? '再准备身份 B' : '浏览器隔离'} - {sameOrigin ? '已是同一站点' : leftTab ? 'B 需打开同一站点' : '选择当前登录页'} - {identityContextsSeparated ? '浏览上下文已分离' : rightTab ? '等待隔离验证' : 'A/B 不能共用登录态'} - {incognitoAccessDenied ?
- - -
: } + 刷新实例 +
dispatch({ type: 'patch', value: { rightLabel: value } })} + instance={rightInstance} + instances={browserInstances} + setInstanceId={assignRightInstance} tabId={rightTabId} setTabId={(value) => assignIdentityTab('right', value)} - tabs={eligibleTabs} - context={rightContext} - disabledReason={(item) => authorizationIdentityOptionDisabledReason({ - candidateTabId: item.id, - candidateIsolationContextId: contextForTab(inspection, item.id)?.contextId || item.isolationContextId, - otherTabId: leftTabId, - otherIsolationContextId: leftIsolationContextId, - otherLabel: '身份 A', - })} - emptyHint={identityNotice || '在中间创建隔离页面,登录另一个账号后会自动选为身份 B'} />
- 原始 Cookie、Storage 与请求值不会进入界面Yak 只接收短时上下文句柄、字段指纹和用户选择的真实请求。 + 插件在 A/B 浏览器内完成代码级监测不调用 AI;Yak 只编排短时上下文、请求基线与确定性交叉矩阵。
{prepareHint} diff --git a/src/features/authorization-testing/ui/IdentitySlot.tsx b/src/features/authorization-testing/ui/IdentitySlot.tsx index 81364fc..613dbf8 100644 --- a/src/features/authorization-testing/ui/IdentitySlot.tsx +++ b/src/features/authorization-testing/ui/IdentitySlot.tsx @@ -1,4 +1,4 @@ -import type { ActiveTabInfo, BrowserIsolationContext } from '@/types/models'; +import type { ActiveTabInfo, BrowserAuthorizationInstance } from '@/types/models'; function shortPageAddress(tab: ActiveTabInfo): string { try { @@ -9,67 +9,59 @@ function shortPageAddress(tab: ActiveTabInfo): string { } } -function contextKindLabel( - context: BrowserIsolationContext | undefined, - selectedTab: ActiveTabInfo | undefined, -): string { - if (!selectedTab) return '等待选择页面'; - switch (context?.kind) { - case 'chrome-incognito-store': return '无痕隔离上下文'; - case 'firefox-container': - return context.containerName ? `Container · ${context.containerName}` : 'Container 隔离上下文'; - case 'managed-ephemeral-profile': return '独立浏览器 Profile'; - case 'verified-tab-local': return '标签页局部上下文'; - case 'sequential-auth-snapshot': return '顺序身份快照'; - default: return selectedTab.incognito ? '无痕浏览上下文' : '普通浏览上下文'; - } -} - -function windowKindLabel(tab: ActiveTabInfo): string { - return tab.incognito ? '无痕窗口' : '普通窗口'; -} - export function IdentitySlot({ - side, title, label, setLabel, tabId, setTabId, tabs, context, disabledReason, emptyHint, + side, title, label, setLabel, instance, instances, setInstanceId, tabId, setTabId, }: { side: 'A' | 'B'; title: string; label: string; setLabel: (value: string) => void; + instance?: BrowserAuthorizationInstance; + instances: BrowserAuthorizationInstance[]; + setInstanceId?: (value: string) => void; tabId?: number; setTabId: (value: number | undefined) => void; - tabs: ActiveTabInfo[]; - context?: BrowserIsolationContext; - disabledReason: (tab: ActiveTabInfo) => string | undefined; - emptyHint: string; }) { - const selectedTab = tabs.find((item) => item.id === tabId); + const selectedTab = instance?.tabs.find((item) => item.id === tabId); return
-
{side}
{title}{contextKindLabel(context, selectedTab)}
- - +
+ {instance?.badge || side} +
+ {title} + {instance ? `YTray 浏览器 ${instance.badge} · 在线` : '等待在线浏览器'} +
+
+ + {setInstanceId && } +
- {selectedTab - ? context?.level === 'strong' - ? '强隔离上下文' - : context?.level === 'conditional' - ? '条件隔离上下文' - : '隔离待验证' - : '尚未选择页面'} - - {selectedTab ? `${windowKindLabel(selectedTab)} · ${selectedTab.url}` : emptyHint} + {instance ? '独立浏览器 Profile' : '尚未选择实例'} + + {selectedTab?.url || instance?.error || '请先在该浏览器打开并登录目标站点'}
; diff --git a/src/features/authorization-testing/ui/authorization-testing-workspace.css b/src/features/authorization-testing/ui/authorization-testing-workspace.css index c360971..b0deee0 100644 --- a/src/features/authorization-testing/ui/authorization-testing-workspace.css +++ b/src/features/authorization-testing/ui/authorization-testing-workspace.css @@ -116,105 +116,6 @@ color: var(--success); } -.authorization-cross-browser { - min-height: 64px; - padding: 10px 14px; - display: grid; - grid-template-columns: 34px minmax(0, 1fr) auto; - align-items: center; - gap: 11px; - border: 1px solid color-mix(in srgb, var(--authorization-accent) 24%, var(--border)); - border-radius: var(--radius-lg); - background: color-mix(in srgb, var(--authorization-accent) 5%, var(--surface)); -} - -.authorization-cross-browser > span { - width: 34px; - height: 34px; - display: grid; - place-items: center; - border-radius: var(--radius-md); - background: color-mix(in srgb, var(--authorization-accent) 12%, var(--surface)); - color: var(--authorization-accent); -} - -.authorization-cross-browser strong, -.authorization-cross-browser small { - display: block; -} - -.authorization-cross-browser > div { - min-width: 0; -} - -.authorization-cross-browser strong { - font-size: var(--text-sm); -} - -.authorization-cross-browser small { - margin-top: 3px; - color: var(--muted); - font-size: var(--text-xs); - line-height: 1.45; -} - -.authorization-cross-browser__instances { - margin-top: 8px; - display: flex; - align-items: center; - gap: 6px; - flex-wrap: wrap; -} - -.authorization-cross-browser__instances > button { - height: 27px; - padding: 0 8px 0 5px; - display: inline-flex; - align-items: center; - gap: 5px; - border: 1px solid var(--border); - border-radius: 999px; - background: var(--surface); - color: var(--muted-strong); - font-size: var(--text-xs); - cursor: pointer; -} - -.authorization-cross-browser__instances > button > b { - width: 18px; - height: 18px; - display: grid; - place-items: center; - border-radius: 50%; - background: var(--surface-subtle); - color: var(--foreground); - font-size: var(--text-xs); -} - -.authorization-cross-browser__instances > button.is-current { - opacity: 1; - background: var(--surface-subtle); - color: var(--muted); - cursor: default; -} - -.authorization-cross-browser__instances > button.is-selected { - border-color: color-mix(in srgb, var(--authorization-accent) 50%, var(--border)); - background: color-mix(in srgb, var(--authorization-accent) 9%, var(--surface)); - color: var(--authorization-accent); -} - -.authorization-cross-browser__instances > button.is-selected > b { - background: var(--authorization-accent); - color: white; -} - -.authorization-cross-browser__instances > button.is-refresh { - width: 27px; - padding: 0; - justify-content: center; -} - .authorization-identity-stage, .authorization-baseline-stage, .authorization-plan-stage { @@ -484,25 +385,6 @@ margin: 7px 0 8px; } -.authorization-isolation-actions { - margin: 7px 0 8px; - display: grid; - justify-items: center; - gap: 5px; -} - -.authorization-isolation-actions .ui-button { - margin: 0; -} - -.authorization-isolation-actions > button:not(.ui-button) { - padding: 2px 4px; - border: 0; - background: transparent; - color: var(--primary); - font-size: 11px; - cursor: pointer; -} .authorization-isolation-actions > button:not(.ui-button):disabled { color: var(--muted); @@ -1557,15 +1439,6 @@ } @media (max-width: 820px) { - .authorization-cross-browser { - grid-template-columns: 34px minmax(0, 1fr); - } - - .authorization-cross-browser > .ui-button { - grid-column: 2; - justify-self: start; - } - .authorization-heading, .authorization-prepare-bar, .authorization-section-heading { diff --git a/src/features/authorization-testing/ui/identity-selection.test.ts b/src/features/authorization-testing/ui/identity-selection.test.ts deleted file mode 100644 index 03da977..0000000 --- a/src/features/authorization-testing/ui/identity-selection.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { - authorizationIdentityOptionDisabledReason, - normalizeAuthorizationIdentityTabSelection, -} from './identity-selection'; - -describe('normalizeAuthorizationIdentityTabSelection', () => { - it('moves the only surviving persisted page to identity A', () => { - expect(normalizeAuthorizationIdentityTabSelection({ - eligibleTabIds: [22], - activeTabId: 22, - leftTabId: 11, - rightTabId: 22, - })).toEqual({ - leftTabId: 22, - rightTabId: undefined, - }); - }); - - it('clears stale selections without visually falling back to another page', () => { - expect(normalizeAuthorizationIdentityTabSelection({ - eligibleTabIds: [], - leftTabId: 11, - rightTabId: 22, - })).toEqual({ - leftTabId: undefined, - rightTabId: undefined, - }); - }); - - it('keeps two different valid user selections', () => { - expect(normalizeAuthorizationIdentityTabSelection({ - eligibleTabIds: [11, 22], - activeTabId: 22, - leftTabId: 11, - rightTabId: 22, - })).toEqual({ - leftTabId: 11, - rightTabId: 22, - }); - }); - - it('uses the active page for A while preserving a different B page', () => { - expect(normalizeAuthorizationIdentityTabSelection({ - eligibleTabIds: [11, 22], - activeTabId: 11, - leftTabId: 99, - rightTabId: 22, - })).toEqual({ - leftTabId: 11, - rightTabId: 22, - }); - }); - - it('does not automatically treat a second ordinary tab as identity B', () => { - expect(normalizeAuthorizationIdentityTabSelection({ - eligibleTabIds: [11, 22], - activeTabId: 11, - })).toEqual({ - leftTabId: 11, - rightTabId: undefined, - }); - }); -}); - -describe('authorizationIdentityOptionDisabledReason', () => { - it('disables the exact page already assigned to the other identity', () => { - expect(authorizationIdentityOptionDisabledReason({ - candidateTabId: 11, - candidateIsolationContextId: 'profile:normal', - otherTabId: 11, - otherIsolationContextId: 'profile:normal', - otherLabel: '身份 A', - })).toBe('已用于身份 A'); - }); - - it('disables another page that shares the other identity login context', () => { - expect(authorizationIdentityOptionDisabledReason({ - candidateTabId: 22, - candidateIsolationContextId: 'profile:normal', - otherTabId: 11, - otherIsolationContextId: 'profile:normal', - otherLabel: '身份 A', - })).toBe('与身份 A 共享登录态'); - }); - - it('keeps pages from another isolation context selectable', () => { - expect(authorizationIdentityOptionDisabledReason({ - candidateTabId: 22, - candidateIsolationContextId: 'profile:incognito', - otherTabId: 11, - otherIsolationContextId: 'profile:normal', - otherLabel: '身份 A', - })).toBeUndefined(); - }); -}); diff --git a/src/features/authorization-testing/ui/identity-selection.ts b/src/features/authorization-testing/ui/identity-selection.ts deleted file mode 100644 index 08ec988..0000000 --- a/src/features/authorization-testing/ui/identity-selection.ts +++ /dev/null @@ -1,67 +0,0 @@ -export interface AuthorizationIdentityTabSelection { - leftTabId?: number; - rightTabId?: number; -} - -export interface NormalizeAuthorizationIdentityTabSelectionInput - extends AuthorizationIdentityTabSelection { - eligibleTabIds: readonly number[]; - activeTabId?: number; -} - -export interface AuthorizationIdentityOptionConflictInput { - candidateTabId: number; - candidateIsolationContextId?: string; - otherTabId?: number; - otherIsolationContextId?: string; - otherLabel: string; -} - -export function authorizationIdentityOptionDisabledReason({ - candidateTabId, - candidateIsolationContextId, - otherTabId, - otherIsolationContextId, - otherLabel, -}: AuthorizationIdentityOptionConflictInput): string | undefined { - if (otherTabId !== undefined && candidateTabId === otherTabId) { - return `已用于${otherLabel}`; - } - if ( - candidateIsolationContextId - && otherIsolationContextId - && candidateIsolationContextId === otherIsolationContextId - ) { - return `与${otherLabel} 共享登录态`; - } - return undefined; -} - -export function normalizeAuthorizationIdentityTabSelection({ - eligibleTabIds, - activeTabId, - leftTabId, - rightTabId, -}: NormalizeAuthorizationIdentityTabSelectionInput): AuthorizationIdentityTabSelection { - const available = new Set( - eligibleTabIds.filter((tabId) => Number.isSafeInteger(tabId) && tabId > 0), - ); - const existing = (tabId?: number): number | undefined => ( - tabId !== undefined && available.has(tabId) ? tabId : undefined - ); - - let left = existing(leftTabId); - let right = existing(rightTabId); - - if (left !== undefined && left === right) right = undefined; - - if (left === undefined) { - left = existing(activeTabId) ?? right ?? eligibleTabIds.find((tabId) => available.has(tabId)); - if (left === right) right = undefined; - } - - return { - leftTabId: left, - rightTabId: right, - }; -} diff --git a/src/features/authorization-testing/ui/workspace-reducer.test.ts b/src/features/authorization-testing/ui/workspace-reducer.test.ts index f762166..8117feb 100644 --- a/src/features/authorization-testing/ui/workspace-reducer.test.ts +++ b/src/features/authorization-testing/ui/workspace-reducer.test.ts @@ -16,12 +16,14 @@ function fixtureWorkspace(): BrowserAuthorizationWorkspace { mode: 'horizontal', state: 'ready', left: { + deviceId: 'device-a', accountLabel: '账号 A', origin: 'https://example.test', target: { tabId: 11, frameId: 0, documentId: 'document-a' }, authentication: { status: 'authenticated', cookieCount: 1, storageEntryCount: 0 }, }, right: { + deviceId: 'device-b', accountLabel: '账号 B', origin: 'https://example.test', target: { tabId: 22, frameId: 0, documentId: 'document-b' }, @@ -71,6 +73,8 @@ describe('authorization workspace UI reducer', () => { it('resets workflow evidence without discarding the selected identities', () => { const previous = { ...INITIAL_AUTHORIZATION_WORKSPACE_UI, + leftDeviceId: 'device-a', + rightDeviceId: 'device-b', leftTabId: 11, rightTabId: 12, workspace: { id: 'old' } as BrowserAuthorizationWorkspace, @@ -80,6 +84,8 @@ describe('authorization workspace UI reducer', () => { expect(next.leftTabId).toBe(11); expect(next.rightTabId).toBe(12); + expect(next.leftDeviceId).toBe('device-a'); + expect(next.rightDeviceId).toBe('device-b'); expect(next.workspace).toBeUndefined(); expect(next.capture).toEqual({}); }); @@ -87,10 +93,8 @@ describe('authorization workspace UI reducer', () => { it('persists only durable workflow state', () => { const value = persistedAuthorizationWorkspaceUI({ ...INITIAL_AUTHORIZATION_WORKSPACE_UI, - inspection: { version: 1 } as never, capture: { left: { active: true } } as never, }); - expect(value).not.toHaveProperty('inspection'); expect(value).not.toHaveProperty('capture'); }); @@ -99,6 +103,8 @@ describe('authorization workspace UI reducer', () => { type: 'hydrate', value: { mode: 'vertical', + leftDeviceId: 'device-a', + rightDeviceId: 'device-b', leftTabId: 11, rightTabId: 'not-a-tab', leftLabel: '低权限账号', @@ -126,6 +132,8 @@ describe('authorization workspace UI reducer', () => { }; const normalized = normalizePersistedAuthorizationWorkspaceUI({ mode: 'horizontal', + leftDeviceId: 'device-a', + rightDeviceId: 'device-b', leftTabId: 11, rightTabId: 22, leftLabel: '账号 A', diff --git a/src/features/authorization-testing/ui/workspace-reducer.ts b/src/features/authorization-testing/ui/workspace-reducer.ts index 6093126..41baa83 100644 --- a/src/features/authorization-testing/ui/workspace-reducer.ts +++ b/src/features/authorization-testing/ui/workspace-reducer.ts @@ -1,7 +1,4 @@ -import type { - BrowserIsolationInspection, - NetworkCaptureStatus, -} from '@/types/models'; +import type { NetworkCaptureStatus } from '@/types/models'; import type { BrowserAuthorizationBaselineCandidate, BrowserAuthorizationMode, @@ -19,6 +16,8 @@ const EMPTY_SELECTION: Record = { left: '', ri export interface PersistedAuthorizationWorkspaceUI { mode: BrowserAuthorizationMode; + leftDeviceId: string; + rightDeviceId: string; leftTabId?: number; rightTabId?: number; leftLabel: string; @@ -31,12 +30,13 @@ export interface PersistedAuthorizationWorkspaceUI { } export interface AuthorizationWorkspaceUIState extends PersistedAuthorizationWorkspaceUI { - inspection?: BrowserIsolationInspection; capture: Partial>; } export const INITIAL_AUTHORIZATION_WORKSPACE_UI: AuthorizationWorkspaceUIState = { mode: 'horizontal', + leftDeviceId: '', + rightDeviceId: '', leftLabel: '账号 A', rightLabel: '账号 B', candidates: EMPTY_AUTHORIZATION_CANDIDATES, @@ -124,6 +124,7 @@ function safeWorkspaceForUI(input: unknown): BrowserAuthorizationWorkspace | und const target = record(side?.target); const authentication = record(side?.authentication); return Boolean(side && target && authentication + && typeof side.deviceId === 'string' && side.deviceId && Number.isSafeInteger(target.tabId) && Number(target.tabId) > 0 && Number.isSafeInteger(target.frameId) && Number(target.frameId) >= 0 && typeof target.documentId === 'string' && target.documentId @@ -265,6 +266,8 @@ export function normalizePersistedAuthorizationWorkspaceUI( }; return { mode: value.mode === 'vertical' ? 'vertical' : 'horizontal', + leftDeviceId: typeof value.leftDeviceId === 'string' ? value.leftDeviceId.slice(0, 320) : '', + rightDeviceId: typeof value.rightDeviceId === 'string' ? value.rightDeviceId.slice(0, 320) : '', leftTabId: Number.isSafeInteger(value.leftTabId) && Number(value.leftTabId) > 0 ? Number(value.leftTabId) : undefined, rightTabId: Number.isSafeInteger(value.rightTabId) && Number(value.rightTabId) > 0 ? Number(value.rightTabId) : undefined, leftLabel: typeof value.leftLabel === 'string' ? value.leftLabel.slice(0, 80) : '账号 A', @@ -290,6 +293,8 @@ export function authorizationWorkspaceUIReducer( return { ...state, mode: value.mode === 'vertical' ? 'vertical' : 'horizontal', + leftDeviceId: value.leftDeviceId || '', + rightDeviceId: value.rightDeviceId || '', leftTabId: value.leftTabId, rightTabId: value.rightTabId, leftLabel: value.leftLabel || '账号 A', @@ -351,6 +356,8 @@ export function persistedAuthorizationWorkspaceUI( ): PersistedAuthorizationWorkspaceUI { return { mode: state.mode, + leftDeviceId: state.leftDeviceId, + rightDeviceId: state.rightDeviceId, leftTabId: state.leftTabId, rightTabId: state.rightTabId, leftLabel: state.leftLabel, diff --git a/src/protocol/extension.test.ts b/src/protocol/extension.test.ts index df51e46..810968b 100644 --- a/src/protocol/extension.test.ts +++ b/src/protocol/extension.test.ts @@ -95,14 +95,6 @@ describe('extension request schemas', () => { action: 'authorization.engine.task', payload: { schema: 'authorization.unknown', payload: {} }, })).toThrow('schema'); - expect(parseExtensionRequest({ - action: 'authorization.yakit.open', - payload: { workspaceId: 'authorization-workspace-1' }, - }).action).toBe('authorization.yakit.open'); - expect(parseExtensionRequest({ - action: 'authorization.yakit.open', - payload: { tabId: 12, mode: 'horizontal', targetDeviceId: 'device-b' }, - }).payload).toEqual({ tabId: 12, mode: 'horizontal', targetDeviceId: 'device-b' }); expect(parseExtensionRequest({ action: 'authorization.yakit.instances' }).action) .toBe('authorization.yakit.instances'); expect(parseExtensionRequest({ diff --git a/src/protocol/extension.ts b/src/protocol/extension.ts index 2ad56ec..76fbace 100644 --- a/src/protocol/extension.ts +++ b/src/protocol/extension.ts @@ -289,6 +289,9 @@ const payloadSchemas = { schema: v.picklist([ 'authorization.workspace.create', 'authorization.workspace.inspect', + 'authorization.capture.start', + 'authorization.capture.status', + 'authorization.capture.stop', 'authorization.baseline.candidates', 'authorization.baseline.bind', 'authorization.logical.bind', @@ -302,14 +305,6 @@ const payloadSchemas = { payload: v.record(v.string(), v.unknown()), timeoutMs: v.optional(v.pipe(v.number(), v.safeInteger(), v.minValue(5_000), v.maxValue(120_000))), }), - 'authorization.yakit.open': v.union([ - v.strictObject({ workspaceId: id }), - v.strictObject({ - tabId, - mode: v.optional(v.picklist(['horizontal', 'vertical'])), - targetDeviceId: v.optional(id), - }), - ]), 'authorization.yakit.instances': noPayload, 'proxy.save': proxyProfile, 'proxy.delete': v.strictObject({ id }), diff --git a/src/types/messages.ts b/src/types/messages.ts index 72ab300..7fd4a5b 100644 --- a/src/types/messages.ts +++ b/src/types/messages.ts @@ -83,6 +83,9 @@ export interface ExtensionRequestMap { schema: | 'authorization.workspace.create' | 'authorization.workspace.inspect' + | 'authorization.capture.start' + | 'authorization.capture.status' + | 'authorization.capture.stop' | 'authorization.baseline.candidates' | 'authorization.baseline.bind' | 'authorization.logical.bind' @@ -97,14 +100,6 @@ export interface ExtensionRequestMap { }; output: unknown; }; - 'authorization.yakit.open': { - input: { workspaceId: string } | { - tabId: number; - mode?: 'horizontal' | 'vertical'; - targetDeviceId?: string; - }; - output: { workspaceId?: string; tabId?: number; opened: boolean }; - }; 'authorization.yakit.instances': { input: undefined; output: { instances: BrowserAuthorizationInstance[] }; diff --git a/src/types/models.ts b/src/types/models.ts index 744e28a..bd75bac 100644 --- a/src/types/models.ts +++ b/src/types/models.ts @@ -222,6 +222,8 @@ export interface BrowserAuthorizationInstance { deviceId: string; badge: string; current: boolean; + tabs: ActiveTabInfo[]; + error?: string; } export type BridgePairingState = 'idle' | 'requesting' | 'pending' | 'approved' | 'rejected' | 'expired' | 'error';