From e3b1775f8dab2627784066da018881f8200e6730 Mon Sep 17 00:00:00 2001 From: go0p Date: Mon, 7 Sep 2026 15:36:59 +0800 Subject: [PATCH] feat(transform): confirm agent profiles locally --- package.json | 2 +- src/app/background/handlers/transform.ts | 23 +++++++++ src/features/browser-analysis/service.test.ts | 8 ++++ src/features/browser-analysis/service.ts | 24 +++++++++- .../BrowserTransformWorkspace.tsx | 48 ++++++++++++++++++- .../browser-transform-workspace.css | 12 +++++ .../grants/capability-handlers/transform.ts | 13 ----- src/protocol/bridge.ts | 2 - src/protocol/capabilities.test.ts | 5 ++ src/protocol/capabilities.ts | 26 +++++----- src/protocol/extension.test.ts | 8 ++++ src/protocol/extension.ts | 5 ++ src/types/messages.ts | 10 ++++ 13 files changed, 152 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 86936c0..31a9e80 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "yakit-chrome-client", "description": "Yakit Browser Extension", "private": true, - "version": "0.2.3", + "version": "0.2.4", "type": "module", "packageManager": "pnpm@10.28.2", "scripts": { diff --git a/src/app/background/handlers/transform.ts b/src/app/background/handlers/transform.ts index b143443..9045e5d 100644 --- a/src/app/background/handlers/transform.ts +++ b/src/app/background/handlers/transform.ts @@ -14,6 +14,7 @@ import { validateBrowserTransformRecovery, } from '@/features/browser-transform/service'; import { + discardBrowserTransformValidation, latestBrowserTransformValidation, proposeBrowserTransformProfile, validateInferredBrowserTransformProfile, @@ -60,6 +61,28 @@ export const handleTransformRequest: BackgroundRequestHandler = async (request, await requiredRequestTarget(request.payload, sender), ), ); + case 'analysis.profile.validation.resolve': { + const input = request.payload; + const target = await requiredRequestTarget(input, sender); + const draft = await latestBrowserTransformValidation(target); + if (!draft || draft.id !== input.validationId) { + throw new Error('验证草稿不存在或已经过期,请重新生成并验证'); + } + if (input.outcome === 'discard') { + await discardBrowserTransformValidation(target, draft.id); + return ok(null); + } + const profile = await saveBrowserTransformProfile(draft.profile); + await discardBrowserTransformValidation(target, draft.id); + void appendAuditEvent({ + category: 'capability', + action: 'analysis.profile.validation.save', + outcome: 'success', + targetTabId: profile.target.tabId, + summary: profile.name, + }); + return ok(profile); + } case 'transform.profile.list': { const input = request.payload; const target = input.tabId ? await requiredRequestTarget(input, sender) : undefined; diff --git a/src/features/browser-analysis/service.test.ts b/src/features/browser-analysis/service.test.ts index 54a3623..8210d59 100644 --- a/src/features/browser-analysis/service.test.ts +++ b/src/features/browser-analysis/service.test.ts @@ -33,6 +33,7 @@ import { BROWSER_TRANSFORM_VALIDATION_DRAFT_MAX_BYTES, compareBrowserPackets, comparePacketWithInferenceCandidate, + discardBrowserTransformValidation, inspectRecordingEvidence, listRecordingTraces, promoteObservedEnvelopeCallable, @@ -75,6 +76,13 @@ function formCandidate(): BrowserProfileInferenceCandidate { } describe('browser analysis deterministic tools', () => { + it('rejects confirmation for a missing or expired validation draft', async () => { + await expect(discardBrowserTransformValidation( + { tabId: 1, frameId: 0, documentId: 'document-1' }, + 'validation-missing', + )).rejects.toThrow(/不存在或已经过期/); + }); + it('bounds validation drafts before session persistence', () => { const draft = { contractVersion: 1, diff --git a/src/features/browser-analysis/service.ts b/src/features/browser-analysis/service.ts index 50c6e7f..7d30fe8 100644 --- a/src/features/browser-analysis/service.ts +++ b/src/features/browser-analysis/service.ts @@ -478,6 +478,26 @@ export async function latestBrowserTransformValidation( return draft || memoryValidationDrafts.get(key) || null; } +export async function discardBrowserTransformValidation( + target: BrowserTarget, + validationId: string, +): Promise { + const key = validationDraftKey(target); + let discarded = false; + validationDraftStorageQueue = validationDraftStorageQueue.then(async () => { + const drafts = pruneValidationDrafts(await readStoredValidationDrafts()); + if (drafts[key]?.id === validationId) { + delete drafts[key]; + discarded = true; + } + await writeStoredValidationDrafts(drafts); + }); + await validationDraftStorageQueue; + if (!discarded) { + throw new ExtensionError('validation_draft_stale', '验证草稿不存在或已经过期,请重新生成并验证'); + } +} + function formValueType(value: string): string { if (!value) return 'empty'; const trimmed = value.trim(); @@ -1109,7 +1129,7 @@ export async function proposeBrowserTransformProfile( ? callable.transaction ? 'captured-request-transaction' : 'validated-callable-envelope' : 'recording-evidence', }, - next: '调用 profile.validate;验证成功后由用户确认保存,AI 不直接持久化配置', + next: '调用 profile.validate;验证成功后由用户在插件中确认保存,AI 不直接持久化配置', }; } @@ -1186,7 +1206,7 @@ export async function validateBrowserTransformProposal( } : undefined, next: comparison ? comparison.equivalent - ? '确定性验证通过;Yakit 已收到待用户确认的明文网关草稿' + ? '确定性验证通过;插件已生成待用户确认的明文网关草稿' : '数据包对比未通过;检查输入映射或重新选择页面函数' : 'Pipeline 已真实回放并生成待确认草稿;如需更强证明,请提供一份浏览器线上请求进行结构对比', }; diff --git a/src/features/browser-transform/BrowserTransformWorkspace.tsx b/src/features/browser-transform/BrowserTransformWorkspace.tsx index d4be4cb..5b104d3 100644 --- a/src/features/browser-transform/BrowserTransformWorkspace.tsx +++ b/src/features/browser-transform/BrowserTransformWorkspace.tsx @@ -10,7 +10,7 @@ import type { ActiveTabInfo, BrowserPageCallable, BrowserRecordingEvent, BrowserTransformBuiltinOperation, BrowserTransformDirection, BrowserTransformNodeReference, BrowserTransformPipelineNode, BrowserTransformProfile, - BrowserTransformProfileInput, BrowserProfileInferenceCandidate, + BrowserTransformProfileInput, BrowserProfileInferenceCandidate, BrowserTransformValidationDraft, } from '@/types/models'; import { callableEnvelopeDescription, compileGuidedTransform, defaultGuidedTransform, guidedOutputDescription, parseGuidedTransform, @@ -257,6 +257,7 @@ export function BrowserTransformWorkspace({ INITIAL_TRANSFORM_WORKSPACE_STATE, ); const [workspaceView, setWorkspaceView] = useState<'flow' | 'configure'>('flow'); + const [pendingValidation, setPendingValidation] = useState(null); const { profiles, callables, selectedProfileId, draft, directionName, loadError, testMethod, testUrl, testHeaders, testBody, testSample, testResult, testError, @@ -351,11 +352,28 @@ export function BrowserTransformWorkspace({ } }, [tab]); + const loadPendingValidation = useCallback(async () => { + if (!tab) { + setPendingValidation(null); + return; + } + try { + setPendingValidation(await request('analysis.profile.validation.latest', { tabId: tab.id, frameId: 0 })); + } catch { + setPendingValidation(null); + } + }, [tab]); + useEffect(() => { workspaceMounted.current = true; return () => { workspaceMounted.current = false; }; }, []); useEffect(() => { void load(); }, [load]); + useEffect(() => { + void loadPendingValidation(); + const timer = setInterval(() => void loadPendingValidation(), 2_000); + return () => clearInterval(timer); + }, [loadPendingValidation]); useEffect(() => { if (recoveryRevision > 0) void load(); }, [load, recoveryRevision]); @@ -731,6 +749,22 @@ export function BrowserTransformWorkspace({ await load(); }, '已取消本次恢复结果,旧网关继续保持停用'); + const resolvePendingValidation = (outcome: 'save' | 'discard') => run(async () => { + if (!tab || !pendingValidation) return; + const profile = await request('analysis.profile.validation.resolve', { + tabId: tab.id, + frameId: 0, + validationId: pendingValidation.id, + outcome, + }); + setPendingValidation(null); + if (!profile) return; + setProfiles((current) => [profile, ...current.filter((item) => item.id !== profile.id)]); + setSelectedProfileId(profile.id); + setDraft(toInput(profile)); + setWorkspaceView('flow'); + }, outcome === 'save' ? '明文网关已保存' : '验证草稿已放弃'); + const execute = async () => { if (!draft?.id || dirty) { setTestError('请先保存当前 Pipeline'); return; } setTestError(''); @@ -762,6 +796,18 @@ export function BrowserTransformWorkspace({ />
+ {pendingValidation &&
+ +
+ Agent 已完成本地验证 · {pendingValidation.proofLevel === 'exact' ? '报文一致' : pendingValidation.proofLevel === 'structure' ? '结构一致' : '执行通过'} + {pendingValidation.profile.name} +

{pendingValidation.profile.origin} · {pendingValidation.profile.request.enabled ? '请求加密' : '响应解密'} · {Math.max(1, Math.ceil((pendingValidation.expiresAt - Date.now()) / 60_000))} 分钟后过期

+
+
+ + +
+
} {!draft ?
建立明文与线上报文的转换链路{callables.length ? : }
: <>
{workspaceView === 'flow' && savedProfile diff --git a/src/features/browser-transform/browser-transform-workspace.css b/src/features/browser-transform/browser-transform-workspace.css index d3bb33a..2cd1897 100644 --- a/src/features/browser-transform/browser-transform-workspace.css +++ b/src/features/browser-transform/browser-transform-workspace.css @@ -63,6 +63,16 @@ .transform-callable-confirm > div { display: flex; justify-content: flex-end; gap: 6px; } .transform-editor { max-height: 820px; overflow: auto; display: grid; align-content: start; border-right: 1px solid var(--border); } +.transform-validation-pending { min-width: 0; min-height: 72px; padding: 11px 14px; display: grid; grid-template-columns: 30px minmax(0, 1fr) auto; align-items: center; gap: 10px; border-bottom: 1px solid color-mix(in srgb, var(--success) 28%, var(--border)); background: color-mix(in srgb, var(--success-soft) 68%, var(--surface)); } +.transform-validation-pending__mark { width: 30px; height: 30px; display: grid; place-items: center; border-radius: 50%; background: var(--success-soft); color: var(--success); } +.transform-validation-pending > div { min-width: 0; } +.transform-validation-pending small, +.transform-validation-pending strong, +.transform-validation-pending p { display: block; margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.transform-validation-pending small { color: var(--success); font-size: 10px; font-weight: 650; } +.transform-validation-pending strong { margin-top: 3px; font-size: var(--text-sm); } +.transform-validation-pending p { margin-top: 2px; color: var(--muted); font-size: var(--text-xs); } +.transform-validation-pending__actions { display: flex; align-items: center; gap: 6px; } .transform-editor-empty { min-height: 520px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 11px; color: var(--muted); text-align: center; } .transform-editor-empty strong { color: var(--foreground); font-size: var(--text-md); } .transform-editor-head { min-height: 64px; padding: 10px 14px; display: flex; align-items: center; justify-content: space-between; gap: 14px; border-bottom: 1px solid var(--border); } @@ -438,6 +448,8 @@ .transform-route label:last-child { grid-column: 1 / -1; } .transform-recovery { grid-template-columns: 30px minmax(0, 1fr); } .transform-recovery__actions { grid-column: 2; justify-content: flex-start; flex-wrap: wrap; } + .transform-validation-pending { grid-template-columns: 30px minmax(0, 1fr); } + .transform-validation-pending__actions { grid-column: 2; justify-content: flex-start; } .transform-step-fields { grid-template-columns: minmax(0, 1fr); } .transform-step-fields .transform-step-name { grid-column: auto; } .transform-output-list > div { grid-template-columns: minmax(0, 1fr) 14px minmax(0, 1fr) 32px; } diff --git a/src/features/grants/capability-handlers/transform.ts b/src/features/grants/capability-handlers/transform.ts index 2c47bd9..d4cf46d 100644 --- a/src/features/grants/capability-handlers/transform.ts +++ b/src/features/grants/capability-handlers/transform.ts @@ -1,9 +1,7 @@ import type { BrowserTransformExecuteInput, BrowserTransformPacket, - BrowserTransformProfileInput, } from '@/types/models'; -import { browser } from 'wxt/browser'; import type { CapabilityDomainHandler } from '../capability-context'; import { allowedTarget, requireScope } from '../capability-context'; import { @@ -15,7 +13,6 @@ import { getBrowserTransformRecovery, listBrowserTransformProfiles, resetBrowserTransformRecovery, - saveBrowserTransformProfile, startBrowserTransformRecovery, validateBrowserTransformRecovery, } from '@/features/browser-transform/service'; @@ -25,7 +22,6 @@ import { proposeBrowserTransformProfile, validateInferredBrowserTransformProfile, } from '@/features/browser-analysis/service'; -import { ExtensionError } from '@/shared/errors'; import { TRANSFORM_CAPABILITY_DOMAIN } from '../capability-domains'; export const transformCapabilityHandler: CapabilityDomainHandler = { @@ -122,15 +118,6 @@ export const transformCapabilityHandler: CapabilityDomainHandler = { })); return visible.filter(Boolean); } - if (method === 'browser.transform.profile.save') { - const profileInput = input as unknown as BrowserTransformProfileInput; - const target = await allowedTarget(grant, profileInput.target); - const frame = await browser.webNavigation.getFrame(target); - if (!frame?.url || profileInput.origin !== new URL(frame.url).origin) { - throw new ExtensionError('target_denied', '转换配置来源与当前页面不一致'); - } - return saveBrowserTransformProfile({ ...profileInput, target }); - } if (method === 'browser.transform.profile.delete') { const profile = await getBrowserTransformProfile(String(input.id || '')); await allowedTarget(grant, profile.target); diff --git a/src/protocol/bridge.ts b/src/protocol/bridge.ts index d1868dc..e7d71c8 100644 --- a/src/protocol/bridge.ts +++ b/src/protocol/bridge.ts @@ -4,7 +4,6 @@ import type { BridgePublicKey } from '@/types/models'; import { browserTransformExecuteSchema, browserTransformPacketSchema, - browserTransformProfileInputSchema, } from './transform'; export const BRIDGE_PROTOCOL_VERSION = 3; @@ -237,7 +236,6 @@ export const capabilityParams = { 'browser.deep_capture.resume': v.optional(v.strictObject(targetFields)), 'browser.deep_capture.detach': v.optional(v.strictObject(targetFields)), 'browser.transform.profile.list': v.optional(v.strictObject(targetFields)), - 'browser.transform.profile.save': browserTransformProfileInputSchema, 'browser.transform.profile.delete': v.strictObject({ id }), 'browser.transform.recovery.get': v.strictObject({ id }), 'browser.transform.recovery.start': v.strictObject({ id }), diff --git a/src/protocol/capabilities.test.ts b/src/protocol/capabilities.test.ts index 84e9775..ca73e47 100644 --- a/src/protocol/capabilities.test.ts +++ b/src/protocol/capabilities.test.ts @@ -52,6 +52,11 @@ describe('versioned Bridge capability catalog', () => { expect(capabilityVisibleToAgent('browser.handoff.resolve')).toBe(false); expect(capabilityVisibleToAgent('browser.thumbnail')).toBe(false); expect(capabilityVisibleToAgent('browser.context')).toBe(true); + expect(catalog.capabilities.find((capability) => capability.method === 'browser.transform.profile.save')).toBeUndefined(); + expect(catalog.capabilities.find((capability) => capability.method === 'proxy.switch')?.summary) + .toContain('不会生成、启用或执行 Transform Profile'); + expect(catalog.capabilities.find((capability) => capability.method === 'browser.profile.validate')?.summary) + .toContain('用户在插件本地确认保存'); expect(catalog.capabilities.find((capability) => capability.method === 'browser.transform.recovery.capture')).toMatchObject({ access: 'dangerous', scopes: ['browser.transform.manage', 'browser.debugger.control', 'browser.callable.execute'], diff --git a/src/protocol/capabilities.ts b/src/protocol/capabilities.ts index 310591c..666aa85 100644 --- a/src/protocol/capabilities.ts +++ b/src/protocol/capabilities.ts @@ -128,7 +128,7 @@ const CAPABILITY_METADATA = { scopes: ['browser.human.takeover'], targetMode: 'none', defaultTimeoutMs: READ_TIMEOUT_MS, }, 'browser.network.start': { - domain: 'network', access: 'control', summary: '启动有界网络捕获,可选采集请求头和 Body', + domain: 'network', access: 'control', summary: '启动 DevTools 网络观察,只采集页面请求;不会生成或执行明文网关', scopes: ['browser.network.capture'], conditionalScopes: [{ scope: 'browser.network.sensitive.read', when: 'captureHeaders=true or captureBody=true' }], targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS, @@ -138,7 +138,7 @@ const CAPABILITY_METADATA = { scopes: ['browser.network.read'], targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS, }, 'browser.network.list': { - domain: 'network', access: 'read', summary: '列出已捕获请求,敏感字段仍由 Agent 操作审核策略保护', + domain: 'network', access: 'read', summary: '列出 DevTools 已捕获请求,适合观察流量;需要转换加密报文时改用 transform 域', scopes: ['browser.network.read'], targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS, }, 'browser.network.clear': { @@ -162,7 +162,7 @@ const CAPABILITY_METADATA = { scopes: ['browser.network.sensitive.read'], targetMode: 'document', defaultTimeoutMs: REPLAY_TIMEOUT_MS, }, 'browser.recording.start': { - domain: 'recording', access: 'control', summary: '开始业务 Trace 录制,可选采集有界值预览', + domain: 'recording', access: 'control', summary: '开始业务 Trace 录制;生成新明文网关时先录制一次真实业务操作,再检查候选证据', scopes: ['browser.recording.control'], conditionalScopes: [{ scope: 'browser.recording.sensitive.read', when: 'captureValues=true' }], targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS, @@ -194,7 +194,7 @@ const CAPABILITY_METADATA = { targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS, }, 'browser.callable.create': { - domain: 'callable', access: 'execute', summary: '从录制句柄或深度捕获 Frame 创建页面函数', + domain: 'callable', access: 'execute', summary: '从录制句柄或深度捕获 Frame 创建页面函数;生成明文网关 Profile 前需要得到可回放函数', scopes: ['browser.callable.execute'], conditionalScopes: [{ scope: 'browser.debugger.control', when: 'source=deep-capture' }], targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS, @@ -240,13 +240,9 @@ const CAPABILITY_METADATA = { scopes: ['browser.debugger.control'], targetMode: 'document', defaultTimeoutMs: REPLAY_TIMEOUT_MS, }, 'browser.transform.profile.list': { - domain: 'transform', access: 'read', summary: '列出目标页面可见的明文网关 Profile', + domain: 'transform', access: 'read', summary: '明文网关入口:先列出目标页面已有 Profile;已有配置可直接用 transform.execute,无配置再走录制、提案和验证', scopes: ['browser.transform.read'], targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS, }, - 'browser.transform.profile.save': { - domain: 'transform', access: 'dangerous', summary: '保存或更新完整 Transform Profile', - scopes: ['browser.transform.manage'], targetMode: 'profile', defaultTimeoutMs: READ_TIMEOUT_MS, - }, 'browser.transform.profile.delete': { domain: 'transform', access: 'write', summary: '删除 Transform Profile', scopes: ['browser.transform.manage'], targetMode: 'profile', defaultTimeoutMs: READ_TIMEOUT_MS, @@ -278,7 +274,7 @@ const CAPABILITY_METADATA = { scopes: ['browser.transform.manage'], targetMode: 'profile', defaultTimeoutMs: READ_TIMEOUT_MS, }, 'browser.transform.execute': { - domain: 'transform', access: 'execute', summary: '对 HTTP 报文应用已保存的请求或响应转换', + domain: 'transform', access: 'execute', summary: '使用已保存的 Profile 对 HTTP 报文执行请求加密或响应解密;它不是网络代理切换', scopes: ['browser.transform.execute'], targetMode: 'profile', defaultTimeoutMs: REPLAY_TIMEOUT_MS, }, 'browser.packet.compare': { @@ -286,16 +282,16 @@ const CAPABILITY_METADATA = { scopes: ['browser.transform.read'], targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS, }, 'browser.profile.propose': { - domain: 'transform', access: 'read', summary: '从候选证据和页面函数确定性编译 Profile 提案', + domain: 'transform', access: 'read', summary: '从录制候选和页面函数编译未保存的 Profile 提案;下一步必须调用 profile.validate', scopes: ['browser.transform.read', 'browser.recording.read'], targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS, }, 'browser.profile.validation.latest': { - domain: 'transform', access: 'read', summary: '读取当前文档最近的短时验证草稿', + domain: 'transform', access: 'read', summary: '读取当前文档最近的短时验证草稿及本地确认状态;草稿过期后需重新验证', scopes: ['browser.transform.read'], targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS, }, 'browser.profile.validate': { - domain: 'transform', access: 'execute', summary: '重新编译并执行 Profile,再与候选或报文证据比较', + domain: 'transform', access: 'execute', summary: '确定性执行 Profile 提案并与证据比较;成功后只生成短时草稿,必须由用户在插件本地确认保存', scopes: ['browser.transform.execute', 'browser.recording.read'], targetMode: 'document', defaultTimeoutMs: REPLAY_TIMEOUT_MS, }, @@ -310,11 +306,11 @@ const CAPABILITY_METADATA = { targetMode: 'document', defaultTimeoutMs: REPLAY_TIMEOUT_MS, }, 'proxy.list': { - domain: 'proxy', access: 'read', summary: '列出扩展代理 Profile', + domain: 'proxy', access: 'read', summary: '列出 Chrome 网络代理 Profile,仅用于流量路由;不是页面加解密或明文网关', scopes: ['browser.proxy.read'], targetMode: 'none', defaultTimeoutMs: READ_TIMEOUT_MS, }, 'proxy.switch': { - domain: 'proxy', access: 'write', summary: '切换当前代理 Profile', + domain: 'proxy', access: 'write', summary: '切换 Chrome 流量代理,仅改变网络路由;不会生成、启用或执行 Transform Profile', scopes: ['browser.proxy.write'], targetMode: 'none', defaultTimeoutMs: READ_TIMEOUT_MS, }, } satisfies Record; diff --git a/src/protocol/extension.test.ts b/src/protocol/extension.test.ts index 4ee5f97..77c8b64 100644 --- a/src/protocol/extension.test.ts +++ b/src/protocol/extension.test.ts @@ -316,6 +316,14 @@ describe('extension request schemas', () => { action: 'analysis.profile.validation.latest', payload: { tabId: 12, frameId: 0 }, }).action).toBe('analysis.profile.validation.latest'); + expect(parseExtensionRequest({ + action: 'analysis.profile.validation.resolve', + payload: { tabId: 12, frameId: 0, validationId: 'validation-1', outcome: 'save' }, + }).action).toBe('analysis.profile.validation.resolve'); + expect(() => parseExtensionRequest({ + action: 'analysis.profile.validation.resolve', + payload: { tabId: 12, frameId: 0, validationId: 'validation-1', outcome: 'approve' }, + })).toThrow(); expect(parseExtensionRequest({ action: 'transform.execute', payload: { diff --git a/src/protocol/extension.ts b/src/protocol/extension.ts index 56b2bd3..b6a204a 100644 --- a/src/protocol/extension.ts +++ b/src/protocol/extension.ts @@ -440,6 +440,11 @@ const payloadSchemas = { 'analysis.profile.propose': capabilityParams['browser.profile.propose'], 'analysis.profile.validate': capabilityParams['browser.profile.validate'], 'analysis.profile.validation.latest': capabilityParams['browser.profile.validation.latest'], + 'analysis.profile.validation.resolve': v.strictObject({ + ...targetFields, + validationId: id, + outcome: v.picklist(['save', 'discard']), + }), 'transform.profile.list': v.strictObject(targetFields), 'transform.profile.save': browserTransformProfileInputSchema, 'transform.profile.delete': v.strictObject({ id }), diff --git a/src/types/messages.ts b/src/types/messages.ts index 6c0c00c..1271a30 100644 --- a/src/types/messages.ts +++ b/src/types/messages.ts @@ -202,6 +202,16 @@ export interface ExtensionRequestMap { input: { tabId?: number; frameId?: number; documentId?: string }; output: BrowserTransformValidationDraft | null; }; + 'analysis.profile.validation.resolve': { + input: { + tabId?: number; + frameId?: number; + documentId?: string; + validationId: string; + outcome: 'save' | 'discard'; + }; + output: BrowserTransformProfile | null; + }; 'transform.profile.list': { input: { tabId?: number; frameId?: number; documentId?: string }; output: BrowserTransformProfile[] }; 'transform.profile.save': { input: BrowserTransformProfileInput; output: BrowserTransformProfile }; 'transform.profile.delete': { input: { id: string }; output: BrowserTransformProfile[] };