mirror of
https://github.com/yaklang/yaklang-chrome-extension.git
synced 2026-09-22 03:10:43 +08:00
fix(authorization): show connected browser instances
This commit is contained in:
@@ -169,6 +169,8 @@ async function handleRequest(request: ExtensionRequest, sender: Browser.runtime.
|
||||
'yakit.browser_authorization.open',
|
||||
request.payload,
|
||||
));
|
||||
case 'authorization.yakit.instances':
|
||||
return ok(await engineBridge.requestEngine('yakit.browser_authorization.instances', {}));
|
||||
case 'context.capture': {
|
||||
const { tabId, frameId, documentId, ...options } = request.payload;
|
||||
const target = await requiredRequestTarget({ tabId, frameId, documentId }, sender);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { authorizationShareGrantInput } from '@/features/grants/gateway-share';
|
||||
import { errorMessage, request } from '@/platform/messaging/runtime';
|
||||
import type {
|
||||
ActiveTabInfo, BridgeStatus, BrowserIsolationContext, BrowserIsolationInspection,
|
||||
ActiveTabInfo, BridgeStatus, BrowserAuthorizationInstance, BrowserIsolationContext, BrowserIsolationInspection,
|
||||
ExtensionState, NetworkCaptureStatus,
|
||||
} from '@/types/models';
|
||||
import {
|
||||
@@ -247,6 +247,8 @@ export function AuthorizationTestingWorkspace({
|
||||
const [localError, setLocalError] = useState('');
|
||||
const [identityNotice, setIdentityNotice] = useState('');
|
||||
const [clock, setClock] = useState(Date.now());
|
||||
const [browserInstances, setBrowserInstances] = useState<BrowserAuthorizationInstance[]>([]);
|
||||
const [targetDeviceId, setTargetDeviceId] = useState('');
|
||||
|
||||
const leftTab = eligibleTabs.find((item) => item.id === leftTabId);
|
||||
const rightTab = eligibleTabs.find((item) => item.id === rightTabId);
|
||||
@@ -262,6 +264,10 @@ export function AuthorizationTestingWorkspace({
|
||||
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 currentBrowserInstance = browserInstances.find((instance) => instance.current);
|
||||
const targetBrowserInstance = browserInstances.find((instance) => instance.deviceId === targetDeviceId);
|
||||
|
||||
const refreshInspection = useCallback(async () => {
|
||||
const next = await request('isolation.inspect', {
|
||||
@@ -270,6 +276,21 @@ export function AuthorizationTestingWorkspace({
|
||||
dispatch({ type: 'patch', value: { inspection: next } });
|
||||
}, [eligibleTabs]);
|
||||
|
||||
const refreshBrowserInstances = useCallback(async () => {
|
||||
if (!instanceDiscoveryReady) {
|
||||
setBrowserInstances([]);
|
||||
setTargetDeviceId('');
|
||||
return;
|
||||
}
|
||||
const result = await request('authorization.yakit.instances');
|
||||
setBrowserInstances(result.instances);
|
||||
setTargetDeviceId((current) => (
|
||||
result.instances.some((instance) => !instance.current && instance.deviceId === current)
|
||||
? current
|
||||
: result.instances.find((instance) => !instance.current)?.deviceId || ''
|
||||
));
|
||||
}, [instanceDiscoveryReady]);
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
try {
|
||||
@@ -315,6 +336,17 @@ export function AuthorizationTestingWorkspace({
|
||||
void refreshInspection().catch((error) => setLocalError(errorMessage(error)));
|
||||
}, [refreshInspection]);
|
||||
|
||||
useEffect(() => {
|
||||
void refreshBrowserInstances().catch((error) => setLocalError(errorMessage(error)));
|
||||
const refreshWhenVisible = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
void refreshBrowserInstances().catch((error) => setLocalError(errorMessage(error)));
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', refreshWhenVisible);
|
||||
return () => document.removeEventListener('visibilitychange', refreshWhenVisible);
|
||||
}, [refreshBrowserInstances]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hydrated || workspace || !leftTab || !rightTab) return;
|
||||
const reason = authorizationIdentityOptionDisabledReason({
|
||||
@@ -447,11 +479,16 @@ export function AuthorizationTestingWorkspace({
|
||||
|
||||
const openCrossBrowserWorkspace = () => run(async () => {
|
||||
if (!activeTab) throw new Error('请先选择需要测试的页面');
|
||||
if (!targetBrowserInstance) throw new Error('请选择另一个在线的 YTray 浏览器实例');
|
||||
if (!bridge.capabilities?.includes('yakit.browser_authorization.open')) {
|
||||
throw new Error('当前 Yak 引擎不支持打开跨浏览器工作区');
|
||||
}
|
||||
await request('authorization.yakit.open', { tabId: activeTab.id, mode });
|
||||
}, '已在 Yakit 打开跨浏览器越权测试,并带入当前页面');
|
||||
await request('authorization.yakit.open', {
|
||||
tabId: activeTab.id,
|
||||
mode,
|
||||
targetDeviceId: targetBrowserInstance.deviceId,
|
||||
});
|
||||
}, `已将浏览器 ${currentBrowserInstance?.badge || '当前'} 与 ${targetBrowserInstance?.badge || '对照'} 带入 Yakit`);
|
||||
|
||||
const refreshWorkspaceDocuments = async (): Promise<BrowserAuthorizationWorkspace> => {
|
||||
if (!workspace || !leftTab || !rightTab) throw new Error('请先建立 A/B 工作区');
|
||||
@@ -733,15 +770,38 @@ export function AuthorizationTestingWorkspace({
|
||||
{!workspace && <section className="authorization-cross-browser">
|
||||
<span><UserRoundPlus size={18} /></span>
|
||||
<div>
|
||||
<strong>使用 YTray 的多个浏览器实例</strong>
|
||||
<small>把当前页面作为资源身份带入 Yakit,再选择在线的浏览器 A、B 或 C 作为独立对照账号。</small>
|
||||
<strong>选择独立浏览器实例</strong>
|
||||
<small>{!instanceDiscoveryReady
|
||||
? '当前 Yak 版本不能读取在线实例,请更新引擎后重连。'
|
||||
: browserInstances.length < 2
|
||||
? '只检测到当前浏览器,请先用 YTray 启动并连接另一个实例。'
|
||||
: '当前页面作为资源所有者,所选浏览器作为独立对照账号。'}</small>
|
||||
<div className="authorization-cross-browser__instances">
|
||||
{browserInstances.map((instance) => <button
|
||||
key={instance.deviceId}
|
||||
type="button"
|
||||
disabled={instance.current}
|
||||
aria-pressed={!instance.current && instance.deviceId === targetDeviceId}
|
||||
className={instance.current ? 'is-current' : instance.deviceId === targetDeviceId ? 'is-selected' : ''}
|
||||
onClick={() => setTargetDeviceId(instance.deviceId)}
|
||||
>
|
||||
<b>{instance.badge}</b>
|
||||
<span>{instance.current ? '当前' : instance.deviceId === targetDeviceId ? '已选择' : '在线'}</span>
|
||||
</button>)}
|
||||
<button
|
||||
type="button"
|
||||
className="is-refresh"
|
||||
aria-label="刷新在线浏览器实例"
|
||||
onClick={() => void refreshBrowserInstances().catch((error) => setLocalError(errorMessage(error)))}
|
||||
><RefreshCw size={13} /></button>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
disabled={busy || !activeTab || !bridge.capabilities?.includes('yakit.browser_authorization.open')}
|
||||
disabled={busy || !activeTab || !targetBrowserInstance || !bridge.capabilities?.includes('yakit.browser_authorization.open')}
|
||||
onClick={() => void openCrossBrowserWorkspace()}
|
||||
>
|
||||
<ExternalLink size={15} />在 Yakit 选择实例
|
||||
<ExternalLink size={15} />用 {targetBrowserInstance?.badge || '—'} 在 Yakit 测试
|
||||
</Button>
|
||||
</section>}
|
||||
|
||||
|
||||
@@ -143,6 +143,10 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.authorization-cross-browser > div {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.authorization-cross-browser strong {
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
@@ -154,6 +158,63 @@
|
||||
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 {
|
||||
|
||||
@@ -101,8 +101,10 @@ describe('extension request schemas', () => {
|
||||
}).action).toBe('authorization.yakit.open');
|
||||
expect(parseExtensionRequest({
|
||||
action: 'authorization.yakit.open',
|
||||
payload: { tabId: 12, mode: 'horizontal' },
|
||||
}).payload).toEqual({ tabId: 12, mode: 'horizontal' });
|
||||
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({
|
||||
action: 'network.capture.start',
|
||||
payload: {
|
||||
|
||||
@@ -307,8 +307,10 @@ const payloadSchemas = {
|
||||
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 }),
|
||||
'proxy.switch': v.strictObject({ id }),
|
||||
|
||||
+10
-1
@@ -14,6 +14,7 @@ import type {
|
||||
BrowserFirefoxManagedContainer,
|
||||
BrowserIsolationInspection,
|
||||
BrowserIsolationProof,
|
||||
BrowserAuthorizationInstance,
|
||||
BrowserDeepCaptureMatcher,
|
||||
BrowserDeepCaptureStatus,
|
||||
CookieInput,
|
||||
@@ -97,9 +98,17 @@ export interface ExtensionRequestMap {
|
||||
output: unknown;
|
||||
};
|
||||
'authorization.yakit.open': {
|
||||
input: { workspaceId: string } | { tabId: number; mode?: 'horizontal' | 'vertical' };
|
||||
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[] };
|
||||
};
|
||||
'proxy.save': { input: ProxyProfile; output: ExtensionState };
|
||||
'proxy.delete': { input: { id: string }; output: ExtensionState };
|
||||
'proxy.switch': { input: { id: string }; output: ExtensionState };
|
||||
|
||||
@@ -218,6 +218,12 @@ export interface BridgePairedEngine {
|
||||
pairedAt: number;
|
||||
}
|
||||
|
||||
export interface BrowserAuthorizationInstance {
|
||||
deviceId: string;
|
||||
badge: string;
|
||||
current: boolean;
|
||||
}
|
||||
|
||||
export type BridgePairingState = 'idle' | 'requesting' | 'pending' | 'approved' | 'rejected' | 'expired' | 'error';
|
||||
|
||||
export interface BridgePairingStatus {
|
||||
|
||||
Reference in New Issue
Block a user