mirror of
https://github.com/yaklang/yaklang-chrome-extension.git
synced 2026-09-22 03:10:43 +08:00
feat: advance browser agent integration workflows
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import {mkdtemp, readFile, rm} from 'node:fs/promises'
|
||||
import {tmpdir} from 'node:os'
|
||||
import {join, resolve} from 'node:path'
|
||||
import {chromium} from 'playwright-core'
|
||||
import {resolveChromiumPath} from './resolve-chromium.mjs'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
|
||||
export async function extensionRequest(page, action, payload = {}) {
|
||||
return page.evaluate(async ({requestAction, requestPayload}) => {
|
||||
const response = await chrome.runtime.sendMessage({action: requestAction, payload: requestPayload})
|
||||
if (!response?.ok) throw new Error(response?.error?.message || response?.error || requestAction)
|
||||
return response.data
|
||||
}, {requestAction: action, requestPayload: payload})
|
||||
}
|
||||
|
||||
export async function waitFor(page, action, payload, predicate, timeoutMs = 15_000) {
|
||||
const deadline = Date.now() + timeoutMs
|
||||
let value
|
||||
while (Date.now() < deadline) {
|
||||
value = await extensionRequest(page, action, payload)
|
||||
if (predicate(value)) return value
|
||||
await page.waitForTimeout(150)
|
||||
}
|
||||
throw new Error(`Timed out waiting for ${action}: ${JSON.stringify(value)}`)
|
||||
}
|
||||
|
||||
export async function launchBrowserAgentContractHarness({
|
||||
profilePrefix,
|
||||
targetURL,
|
||||
extensionPath = resolve(root, process.env.EXTENSION_PATH || '.output/chrome-mv3-enterprise'),
|
||||
}) {
|
||||
const manifest = JSON.parse(await readFile(resolve(extensionPath, 'manifest.json'), 'utf8'))
|
||||
const executablePath = await resolveChromiumPath()
|
||||
const userDataDir = await mkdtemp(join(tmpdir(), profilePrefix))
|
||||
const context = await chromium.launchPersistentContext(userDataDir, {
|
||||
executablePath,
|
||||
headless: true,
|
||||
viewport: {width: 1280, height: 760},
|
||||
args: [
|
||||
`--disable-extensions-except=${extensionPath}`,
|
||||
`--load-extension=${extensionPath}`,
|
||||
'--no-first-run',
|
||||
'--no-default-browser-check',
|
||||
],
|
||||
})
|
||||
try {
|
||||
let serviceWorker = context.serviceWorkers()[0]
|
||||
if (!serviceWorker) serviceWorker = await context.waitForEvent('serviceworker', {timeout: 15_000})
|
||||
const extensionId = new URL(serviceWorker.url()).host
|
||||
|
||||
if (manifest.permissions?.includes('userScripts')) {
|
||||
const extensionsPage = await context.newPage()
|
||||
await extensionsPage.goto(`chrome://extensions/?id=${extensionId}`)
|
||||
const toggle = extensionsPage.locator('#allow-user-scripts cr-toggle')
|
||||
await toggle.waitFor({state: 'visible', timeout: 10_000})
|
||||
if (!await toggle.evaluate((element) => Boolean(element.checked))) await toggle.click()
|
||||
await extensionsPage.close()
|
||||
}
|
||||
|
||||
const targetPage = await context.newPage()
|
||||
targetPage.on('dialog', (dialog) => void dialog.dismiss())
|
||||
await targetPage.goto(targetURL)
|
||||
|
||||
const controlPage = await context.newPage()
|
||||
await controlPage.goto(`chrome-extension://${extensionId}/options.html`)
|
||||
const tabId = await controlPage.evaluate(async (url) => {
|
||||
const tabs = await chrome.tabs.query({})
|
||||
return tabs.find((tab) => tab.url === url)?.id
|
||||
}, targetPage.url())
|
||||
if (!tabId) throw new Error(`Could not resolve target tab ${targetPage.url()}`)
|
||||
|
||||
return {
|
||||
context,
|
||||
controlPage,
|
||||
extensionId,
|
||||
tabId,
|
||||
targetPage,
|
||||
async close() {
|
||||
await context.close().catch(() => undefined)
|
||||
await rm(userDataDir, {recursive: true, force: true})
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
await context.close().catch(() => undefined)
|
||||
await rm(userDataDir, {recursive: true, force: true})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export function transformedFetchOptions(execution, originalHeaders) {
|
||||
const headers = new Map(originalHeaders.map((header) => [header.name.toLowerCase(), {
|
||||
name: header.name,
|
||||
value: header.value,
|
||||
}]))
|
||||
for (const name of execution.removeHeaders || []) headers.delete(name.toLowerCase())
|
||||
for (const header of execution.setHeaders || []) {
|
||||
headers.set(header.name.toLowerCase(), {name: header.name, value: header.value})
|
||||
}
|
||||
return {
|
||||
method: 'POST',
|
||||
headers: Object.fromEntries([...headers.values()].map((header) => [header.name, header.value])),
|
||||
body: Buffer.from(execution.bodyBase64, 'base64'),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import {
|
||||
extensionRequest,
|
||||
launchBrowserAgentContractHarness,
|
||||
transformedFetchOptions,
|
||||
} from './browser-agent-contract-harness.mjs'
|
||||
|
||||
const targetURL = process.env.AES_TARGET || 'http://127.0.0.1:82/'
|
||||
const plaintext = {username: 'admin', password: '123456'}
|
||||
let harness
|
||||
|
||||
try {
|
||||
harness = await launchBrowserAgentContractHarness({
|
||||
profilePrefix: 'yakit-aes-contract-',
|
||||
targetURL,
|
||||
})
|
||||
const {controlPage, tabId, targetPage} = harness
|
||||
let browserRequestCount = 0
|
||||
targetPage.on('request', (request) => {
|
||||
if (new URL(request.url()).pathname === '/encrypt/aes.php') browserRequestCount += 1
|
||||
})
|
||||
|
||||
await extensionRequest(controlPage, 'recording.start', {
|
||||
tabId,
|
||||
frameId: 0,
|
||||
captureValues: true,
|
||||
maxEntries: 120,
|
||||
maxValueBytes: 8_192,
|
||||
})
|
||||
await targetPage.locator('#username').fill('admin')
|
||||
await targetPage.locator('#password').fill('wrong-password')
|
||||
await targetPage.getByRole('button', {name: '登录', exact: true}).click()
|
||||
await targetPage.getByRole('button', {name: 'AES固定Key', exact: true}).click()
|
||||
await targetPage.waitForTimeout(500)
|
||||
if (browserRequestCount !== 1) {
|
||||
throw new Error(`Initial AES recording expected one real request, received ${browserRequestCount}`)
|
||||
}
|
||||
|
||||
const snapshot = await extensionRequest(
|
||||
controlPage,
|
||||
'recording.get',
|
||||
{tabId, frameId: 0, limit: 120},
|
||||
)
|
||||
const candidate = snapshot.profileCandidates?.find((item) => (
|
||||
item.status === 'ready'
|
||||
&& new URL(item.request?.url, targetURL).pathname === '/encrypt/aes.php'
|
||||
&& item.source?.callHandleId
|
||||
))
|
||||
if (!candidate) {
|
||||
throw new Error(`Single-call AES candidate was not ready after one recording: ${JSON.stringify(snapshot)}`)
|
||||
}
|
||||
|
||||
const callable = await extensionRequest(controlPage, 'callable.create', {
|
||||
tabId,
|
||||
frameId: 0,
|
||||
source: 'recording',
|
||||
callHandleId: candidate.source.callHandleId,
|
||||
name: 'CryptoJS AES recorded call',
|
||||
})
|
||||
if (callable.kind !== 'recorded-call') {
|
||||
throw new Error(`AES callable was not retained from the recording: ${JSON.stringify(callable)}`)
|
||||
}
|
||||
|
||||
const plainPacket = {
|
||||
method: 'POST',
|
||||
url: new URL('/encrypt/aes.php', targetURL).toString(),
|
||||
headers: [{name: 'Content-Type', value: 'application/json'}],
|
||||
bodyBase64: Buffer.from(JSON.stringify(plaintext)).toString('base64'),
|
||||
}
|
||||
const proposal = await extensionRequest(controlPage, 'analysis.profile.propose', {
|
||||
tabId,
|
||||
frameId: 0,
|
||||
candidateId: candidate.id,
|
||||
callableId: callable.id,
|
||||
inputPaths: ['body'],
|
||||
name: 'AES deterministic contract',
|
||||
})
|
||||
if (proposal?.proposal?.compiler !== 'browser-transform-guided-v1') {
|
||||
throw new Error(`AES Profile was not deterministically compiled: ${JSON.stringify(proposal)}`)
|
||||
}
|
||||
|
||||
const validation = await extensionRequest(controlPage, 'analysis.profile.validate', {
|
||||
tabId,
|
||||
frameId: 0,
|
||||
candidateId: candidate.id,
|
||||
callableId: callable.id,
|
||||
inputPaths: ['body'],
|
||||
name: 'AES deterministic contract',
|
||||
packet: plainPacket,
|
||||
comparisonMode: 'structure',
|
||||
})
|
||||
if (!validation?.valid || !validation?.saveEligible
|
||||
|| validation.proofLevel !== 'structure'
|
||||
|| validation.validationDraft?.contractVersion !== 1) {
|
||||
throw new Error(`AES Profile validation failed: ${JSON.stringify(validation)}`)
|
||||
}
|
||||
if (browserRequestCount !== 1) {
|
||||
throw new Error(`AES Profile validation leaked a real browser request; observed ${browserRequestCount}`)
|
||||
}
|
||||
|
||||
const validationDraft = await extensionRequest(
|
||||
controlPage,
|
||||
'analysis.profile.validation.latest',
|
||||
{tabId, frameId: 0},
|
||||
)
|
||||
if (!validationDraft || validationDraft.id !== validation.validationDraft.id
|
||||
|| validationDraft.contractVersion !== 1 || validationDraft.profile?.id) {
|
||||
throw new Error(`AES Yakit handoff draft is invalid: ${JSON.stringify(validationDraft)}`)
|
||||
}
|
||||
const savedProfile = await extensionRequest(
|
||||
controlPage,
|
||||
'transform.profile.save',
|
||||
validationDraft.profile,
|
||||
)
|
||||
const execution = await extensionRequest(controlPage, 'transform.execute', {
|
||||
profileId: savedProfile.id,
|
||||
direction: 'request',
|
||||
packet: plainPacket,
|
||||
})
|
||||
const wireBody = Buffer.from(execution.bodyBase64, 'base64').toString('utf8')
|
||||
const form = new URLSearchParams(wireBody)
|
||||
const encryptedData = form.get('encryptedData')
|
||||
if (!encryptedData || encryptedData.startsWith('{') || form.size !== 1) {
|
||||
throw new Error(`AES Profile produced an invalid or nested form envelope: ${wireBody}`)
|
||||
}
|
||||
if (browserRequestCount !== 1) {
|
||||
throw new Error(`Saved AES Profile leaked a real browser request; observed ${browserRequestCount}`)
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
execution.url,
|
||||
transformedFetchOptions(execution, plainPacket.headers),
|
||||
)
|
||||
const result = await response.json()
|
||||
if (!result.success) throw new Error(`Target server rejected the AES Profile output: ${JSON.stringify(result)}`)
|
||||
process.stdout.write(
|
||||
'AES Agent contract verified: one recording produced a callable, deterministic Profile, Yakit confirmation draft, and server-accepted wire request.\n',
|
||||
)
|
||||
} finally {
|
||||
await harness?.close()
|
||||
}
|
||||
@@ -1,76 +1,23 @@
|
||||
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { chromium } from 'playwright-core'
|
||||
import { resolveChromiumPath } from './resolve-chromium.mjs'
|
||||
import {
|
||||
extensionRequest,
|
||||
launchBrowserAgentContractHarness,
|
||||
transformedFetchOptions,
|
||||
waitFor,
|
||||
} from './browser-agent-contract-harness.mjs'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const extensionPath = resolve(root, process.env.EXTENSION_PATH || '.output/chrome-mv3-enterprise')
|
||||
const targetURL = process.env.AESRSA_TARGET || 'http://127.0.0.1:82/'
|
||||
const manifest = JSON.parse(await readFile(resolve(extensionPath, 'manifest.json'), 'utf8'))
|
||||
const executablePath = await resolveChromiumPath()
|
||||
const userDataDir = await mkdtemp(join(tmpdir(), 'yakit-aesrsa-'))
|
||||
let context
|
||||
|
||||
async function extensionRequest(page, action, payload = {}) {
|
||||
return page.evaluate(async ({ requestAction, requestPayload }) => {
|
||||
const response = await chrome.runtime.sendMessage({ action: requestAction, payload: requestPayload })
|
||||
if (!response?.ok) throw new Error(response?.error?.message || response?.error || requestAction)
|
||||
return response.data
|
||||
}, { requestAction: action, requestPayload: payload })
|
||||
}
|
||||
|
||||
async function waitFor(page, action, payload, predicate, timeoutMs = 15_000) {
|
||||
const deadline = Date.now() + timeoutMs
|
||||
let value
|
||||
while (Date.now() < deadline) {
|
||||
value = await extensionRequest(page, action, payload)
|
||||
if (predicate(value)) return value
|
||||
await page.waitForTimeout(150)
|
||||
}
|
||||
throw new Error(`Timed out waiting for ${action}: ${JSON.stringify(value)}`)
|
||||
}
|
||||
let harness
|
||||
|
||||
try {
|
||||
context = await chromium.launchPersistentContext(userDataDir, {
|
||||
executablePath,
|
||||
headless: true,
|
||||
viewport: { width: 1280, height: 760 },
|
||||
args: [
|
||||
`--disable-extensions-except=${extensionPath}`,
|
||||
`--load-extension=${extensionPath}`,
|
||||
'--no-first-run',
|
||||
'--no-default-browser-check',
|
||||
],
|
||||
harness = await launchBrowserAgentContractHarness({
|
||||
profilePrefix: 'yakit-aesrsa-',
|
||||
targetURL,
|
||||
})
|
||||
let serviceWorker = context.serviceWorkers()[0]
|
||||
if (!serviceWorker) serviceWorker = await context.waitForEvent('serviceworker', { timeout: 15_000 })
|
||||
const extensionId = new URL(serviceWorker.url()).host
|
||||
|
||||
if (manifest.permissions?.includes('userScripts')) {
|
||||
const extensionsPage = await context.newPage()
|
||||
await extensionsPage.goto(`chrome://extensions/?id=${extensionId}`)
|
||||
const toggle = extensionsPage.locator('#allow-user-scripts cr-toggle')
|
||||
await toggle.waitFor({ state: 'visible', timeout: 10_000 })
|
||||
if (!await toggle.evaluate((element) => Boolean(element.checked))) await toggle.click()
|
||||
await extensionsPage.close()
|
||||
}
|
||||
|
||||
const targetPage = await context.newPage()
|
||||
targetPage.on('dialog', (dialog) => void dialog.dismiss())
|
||||
const {controlPage, tabId, targetPage} = harness
|
||||
let browserRequestCount = 0
|
||||
targetPage.on('request', (request) => {
|
||||
if (new URL(request.url()).pathname === '/encrypt/aesrsa.php') browserRequestCount += 1
|
||||
})
|
||||
await targetPage.goto(targetURL)
|
||||
|
||||
const controlPage = await context.newPage()
|
||||
await controlPage.goto(`chrome-extension://${extensionId}/options.html`)
|
||||
const tabId = await controlPage.evaluate(async (url) => {
|
||||
const tabs = await chrome.tabs.query({})
|
||||
return tabs.find((tab) => tab.url === url)?.id
|
||||
}, targetPage.url())
|
||||
if (!tabId) throw new Error('Could not resolve the AES+RSA target tab')
|
||||
|
||||
await extensionRequest(controlPage, 'recording.start', {
|
||||
tabId, frameId: 0, captureValues: true, maxEntries: 120, maxValueBytes: 8_192,
|
||||
@@ -130,15 +77,7 @@ try {
|
||||
strategy: 'request-transaction',
|
||||
callFrameId: frame.id,
|
||||
name: 'sendDataAesRsa 请求事务',
|
||||
transaction: {
|
||||
request: {
|
||||
method: candidate.request.method,
|
||||
url: candidate.request.url,
|
||||
expectedDestinations: candidate.sources.map((source) => source.destination).filter(Boolean),
|
||||
},
|
||||
inputMode: 'auto',
|
||||
boundaries: ['fetch', 'xhr', 'beacon', 'form'],
|
||||
},
|
||||
candidateId: candidate.id,
|
||||
})
|
||||
if (callable.kind !== 'request-transaction' || callable.inputSlots?.[0]?.name !== 'body') {
|
||||
throw new Error(`Captured callable is not a request transaction: ${JSON.stringify(callable)}`)
|
||||
@@ -150,13 +89,14 @@ try {
|
||||
throw new Error(`Deep-capture replay leaked a real request; observed ${browserRequestCount}`)
|
||||
}
|
||||
|
||||
const plaintext = { username: 'admin', password: '123456' }
|
||||
let execution
|
||||
try {
|
||||
execution = await extensionRequest(controlPage, 'callable.execute', {
|
||||
tabId,
|
||||
frameId: 0,
|
||||
callableId: callable.id,
|
||||
args: [{ username: 'admin', password: '123456' }],
|
||||
args: [plaintext],
|
||||
})
|
||||
} catch (reason) {
|
||||
const diagnostics = await controlPage.evaluate(async ({ targetTabId, callableId }) => {
|
||||
@@ -209,16 +149,182 @@ try {
|
||||
if (browserRequestCount !== 1) {
|
||||
throw new Error(`Transaction execution leaked a real browser request; observed ${browserRequestCount}`)
|
||||
}
|
||||
try {
|
||||
await extensionRequest(controlPage, 'callable.execute', {
|
||||
tabId,
|
||||
frameId: 0,
|
||||
callableId: callable.id,
|
||||
args: [plaintext],
|
||||
})
|
||||
} catch (reason) {
|
||||
throw new Error(
|
||||
`Captured request transaction is not repeatable: ${reason instanceof Error ? reason.message : String(reason)}`,
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch(new URL('/encrypt/aesrsa.php', targetURL), {
|
||||
const plainPacket = {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(envelope),
|
||||
url: new URL('/encrypt/aesrsa.php', targetURL).toString(),
|
||||
headers: [{ name: 'Content-Type', value: 'application/json' }],
|
||||
bodyBase64: Buffer.from(JSON.stringify(plaintext)).toString('base64'),
|
||||
}
|
||||
const proposal = await extensionRequest(controlPage, 'analysis.profile.propose', {
|
||||
tabId,
|
||||
frameId: 0,
|
||||
candidateId: candidate.id,
|
||||
callableId: callable.id,
|
||||
inputPaths: ['body'],
|
||||
name: 'AES+RSA deterministic contract',
|
||||
})
|
||||
if (proposal?.proposal?.compiler !== 'browser-transform-guided-v1'
|
||||
|| proposal?.profile?.request?.enabled !== true) {
|
||||
throw new Error(`Deterministic profile proposal was not compiled: ${JSON.stringify(proposal)}`)
|
||||
}
|
||||
|
||||
let validation
|
||||
try {
|
||||
validation = await extensionRequest(controlPage, 'analysis.profile.validate', {
|
||||
tabId,
|
||||
frameId: 0,
|
||||
candidateId: candidate.id,
|
||||
callableId: callable.id,
|
||||
inputPaths: ['body'],
|
||||
name: 'AES+RSA deterministic contract',
|
||||
packet: plainPacket,
|
||||
comparisonMode: 'structure',
|
||||
})
|
||||
} catch (reason) {
|
||||
throw new Error(
|
||||
`Deterministic profile validation could not execute: ${reason instanceof Error ? reason.message : String(reason)}; `
|
||||
+ `packet=${JSON.stringify(plainPacket)}; profile=${JSON.stringify(proposal.profile)}`,
|
||||
)
|
||||
}
|
||||
if (!validation?.valid || !validation?.saveEligible
|
||||
|| validation.proofLevel !== 'structure'
|
||||
|| validation.validationDraft?.contractVersion !== 1
|
||||
|| !validation.validationDraft?.id) {
|
||||
throw new Error(`Deterministic profile validation failed: ${JSON.stringify(validation)}`)
|
||||
}
|
||||
if (browserRequestCount !== 1) {
|
||||
throw new Error(`Profile validation leaked a real browser request; observed ${browserRequestCount}`)
|
||||
}
|
||||
|
||||
const validationDraft = await extensionRequest(
|
||||
controlPage,
|
||||
'analysis.profile.validation.latest',
|
||||
{ tabId, frameId: 0 },
|
||||
)
|
||||
if (!validationDraft || validationDraft.contractVersion !== 1
|
||||
|| validationDraft.id !== validation.validationDraft.id
|
||||
|| validationDraft.profile?.id) {
|
||||
throw new Error(`Yakit handoff draft is missing or already persisted: ${JSON.stringify(validationDraft)}`)
|
||||
}
|
||||
|
||||
const savedProfile = await extensionRequest(
|
||||
controlPage,
|
||||
'transform.profile.save',
|
||||
validationDraft.profile,
|
||||
)
|
||||
const profiles = await extensionRequest(controlPage, 'transform.profile.list', { tabId, frameId: 0 })
|
||||
if (!savedProfile?.id || !profiles.some((profile) => profile.id === savedProfile.id)) {
|
||||
throw new Error(`Confirmed profile was not persisted: ${JSON.stringify({ savedProfile, profiles })}`)
|
||||
}
|
||||
|
||||
const profileExecution = await extensionRequest(controlPage, 'transform.execute', {
|
||||
profileId: savedProfile.id,
|
||||
direction: 'request',
|
||||
packet: plainPacket,
|
||||
})
|
||||
const transformedEnvelope = JSON.parse(
|
||||
Buffer.from(profileExecution.bodyBase64, 'base64').toString('utf8'),
|
||||
)
|
||||
for (const field of ['encryptedData', 'encryptedKey', 'encryptedIv']) {
|
||||
if (typeof transformedEnvelope?.[field] !== 'string' || !transformedEnvelope[field]) {
|
||||
throw new Error(`Saved profile output is missing ${field}: ${JSON.stringify(profileExecution)}`)
|
||||
}
|
||||
}
|
||||
if (browserRequestCount !== 1) {
|
||||
throw new Error(`Saved profile execution leaked a real browser request; observed ${browserRequestCount}`)
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
profileExecution.url,
|
||||
transformedFetchOptions(profileExecution, plainPacket.headers),
|
||||
)
|
||||
const result = await response.json()
|
||||
if (!result.success) throw new Error(`Target server rejected the transaction envelope: ${JSON.stringify(result)}`)
|
||||
process.stdout.write('AES+RSA request transaction verified: no browser request leaked and the target server accepted the captured envelope.\n')
|
||||
if (!result.success) throw new Error(`Target server rejected the saved Profile output: ${JSON.stringify(result)}`)
|
||||
|
||||
await targetPage.reload()
|
||||
const staleProfile = await waitFor(
|
||||
controlPage,
|
||||
'transform.profile.list',
|
||||
{ tabId, frameId: 0 },
|
||||
(items) => items?.find((item) => item.id === savedProfile.id)?.recovery?.state === 'stale',
|
||||
10_000,
|
||||
).then((items) => items.find((item) => item.id === savedProfile.id))
|
||||
if (staleProfile.enabled || staleProfile.recovery?.capture?.automatic !== true) {
|
||||
throw new Error(`Reloaded Profile did not fail closed with an automatic Recovery Plan: ${JSON.stringify(staleProfile)}`)
|
||||
}
|
||||
|
||||
await extensionRequest(controlPage, 'transform.recovery.start', { id: savedProfile.id })
|
||||
await targetPage.locator('#username').fill('admin')
|
||||
await targetPage.locator('#password').fill('wrong-password')
|
||||
await targetPage.getByRole('button', { name: '登录', exact: true }).click()
|
||||
let recoveryClickFailure
|
||||
const recoveryClick = targetPage.getByRole('button', { name: 'AES+Rsa加密', exact: true })
|
||||
.click({ noWaitAfter: true, timeout: 20_000 })
|
||||
.catch((reason) => { recoveryClickFailure = reason })
|
||||
const recoveryPause = await waitFor(controlPage, 'deep.capture.status', {
|
||||
tabId, frameId: 0,
|
||||
}, (value) => value?.state === 'paused' && value.pause?.collecting !== true, 20_000)
|
||||
const recoveryAutomatic = recoveryPause.pause?.automaticCapture
|
||||
if (recoveryAutomatic?.state !== 'ready' || !recoveryAutomatic.frameId
|
||||
|| recoveryAutomatic.strategy !== 'request-transaction') {
|
||||
throw new Error(`Recovery Plan did not locate the request transaction: ${JSON.stringify(recoveryPause)}`)
|
||||
}
|
||||
const recovery = await extensionRequest(controlPage, 'transform.recovery.capture', {
|
||||
id: savedProfile.id,
|
||||
...recoveryPause.target,
|
||||
callFrameId: recoveryAutomatic.frameId,
|
||||
strategy: recoveryAutomatic.strategy,
|
||||
})
|
||||
await recoveryClick
|
||||
if (recoveryClickFailure) throw recoveryClickFailure
|
||||
if (recovery.state !== 'validation-required' || !recovery.pending?.callableId) {
|
||||
throw new Error(`Recovery capture was not staged for validation: ${JSON.stringify(recovery)}`)
|
||||
}
|
||||
const recoveryValidation = await extensionRequest(controlPage, 'transform.recovery.validate', {
|
||||
id: savedProfile.id,
|
||||
packet: plainPacket,
|
||||
})
|
||||
if (recoveryValidation.recovery?.state !== 'confirmation-required'
|
||||
|| !recoveryValidation.recovery.validation?.id) {
|
||||
throw new Error(`Recovered Profile did not require explicit confirmation: ${JSON.stringify(recoveryValidation)}`)
|
||||
}
|
||||
const recoveredProfile = await extensionRequest(controlPage, 'transform.recovery.confirm', {
|
||||
id: savedProfile.id,
|
||||
validationId: recoveryValidation.recovery.validation.id,
|
||||
})
|
||||
if (recoveredProfile.id !== savedProfile.id || recoveredProfile.recovery?.state !== 'ready'
|
||||
|| recoveredProfile.target.documentId === savedProfile.target.documentId) {
|
||||
throw new Error(`Recovery confirmation did not atomically replace the document binding: ${JSON.stringify(recoveredProfile)}`)
|
||||
}
|
||||
const recoveredExecution = await extensionRequest(controlPage, 'transform.execute', {
|
||||
profileId: recoveredProfile.id,
|
||||
direction: 'request',
|
||||
packet: plainPacket,
|
||||
})
|
||||
const recoveredResponse = await fetch(
|
||||
recoveredExecution.url,
|
||||
transformedFetchOptions(recoveredExecution, plainPacket.headers),
|
||||
)
|
||||
const recoveredResult = await recoveredResponse.json()
|
||||
if (!recoveredResult.success) {
|
||||
throw new Error(`Target server rejected the recovered Profile output: ${JSON.stringify(recoveredResult)}`)
|
||||
}
|
||||
process.stdout.write(
|
||||
'AES+RSA Agent contract verified: evidence compiled, validated, saved, reloaded stale, recovered through one request-boundary capture, revalidated, explicitly confirmed, and accepted by the target server.\n',
|
||||
)
|
||||
} finally {
|
||||
await context?.close().catch(() => undefined)
|
||||
await rm(userDataDir, { recursive: true, force: true })
|
||||
await harness?.close()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
import {
|
||||
extensionRequest,
|
||||
launchBrowserAgentContractHarness,
|
||||
transformedFetchOptions,
|
||||
waitFor,
|
||||
} from './browser-agent-contract-harness.mjs'
|
||||
|
||||
const targetURL = process.env.AESSERVER_TARGET || 'http://127.0.0.1:82/'
|
||||
const keyPath = '/encrypt/server_generate_key.php'
|
||||
const requestPath = '/encrypt/aesserver.php'
|
||||
const plaintext = {username: 'admin', password: '123456'}
|
||||
let harness
|
||||
|
||||
function pathOf(url) {
|
||||
return new URL(url, targetURL).pathname
|
||||
}
|
||||
|
||||
function snapshotSummary(snapshot) {
|
||||
return {
|
||||
events: snapshot.events?.map((event) => ({
|
||||
sequence: event.sequence,
|
||||
kind: event.kind,
|
||||
operation: event.operation,
|
||||
url: event.url ? pathOf(event.url) : undefined,
|
||||
})),
|
||||
candidates: snapshot.profileCandidates?.map((candidate) => ({
|
||||
status: candidate.status,
|
||||
request: pathOf(candidate.request?.url || ''),
|
||||
prerequisites: candidate.capturePlan?.transaction?.prerequisites?.map((step) => pathOf(step.url)),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
function dependencySummary(snapshot, candidate) {
|
||||
const eventIds = new Set(candidate.evidence?.flatMap((item) => item.eventIds || []))
|
||||
return {
|
||||
evidence: candidate.evidence?.filter((item) => item.kind === 'response-boundary'),
|
||||
events: snapshot.events?.filter((event) => eventIds.has(event.id)).map((event) => ({
|
||||
id: event.id,
|
||||
sequence: event.sequence,
|
||||
kind: event.kind,
|
||||
operation: event.operation,
|
||||
inputs: event.inputs?.map((item) => item.path),
|
||||
outputs: event.outputs?.map((item) => item.path),
|
||||
})),
|
||||
links: snapshot.links?.filter((link) => eventIds.has(link.fromEventId) || eventIds.has(link.toEventId))
|
||||
.map((link) => ({kind: link.kind, fromPath: link.fromPath, toPath: link.toPath})),
|
||||
}
|
||||
}
|
||||
|
||||
async function performLogin(targetPage, password = 'wrong-password') {
|
||||
await targetPage.locator('#username').fill('admin')
|
||||
await targetPage.locator('#password').fill(password)
|
||||
await targetPage.getByRole('button', {name: '登录', exact: true}).click()
|
||||
return targetPage.getByRole('button', {name: 'AES服务端获取Key', exact: true})
|
||||
}
|
||||
|
||||
try {
|
||||
harness = await launchBrowserAgentContractHarness({
|
||||
profilePrefix: 'yakit-aesserver-',
|
||||
targetURL,
|
||||
})
|
||||
const {controlPage, tabId, targetPage} = harness
|
||||
const observed = {key: 0, terminal: 0}
|
||||
targetPage.on('request', (request) => {
|
||||
const path = pathOf(request.url())
|
||||
if (path === keyPath) observed.key += 1
|
||||
if (path === requestPath) observed.terminal += 1
|
||||
})
|
||||
|
||||
await extensionRequest(controlPage, 'recording.start', {
|
||||
tabId,
|
||||
frameId: 0,
|
||||
captureValues: true,
|
||||
maxEntries: 160,
|
||||
maxValueBytes: 8_192,
|
||||
})
|
||||
await (await performLogin(targetPage)).click()
|
||||
await targetPage.waitForTimeout(600)
|
||||
if (observed.key !== 1 || observed.terminal !== 1) {
|
||||
throw new Error(`Initial operation did not produce the expected two-request flow: ${JSON.stringify(observed)}`)
|
||||
}
|
||||
|
||||
const snapshot = await extensionRequest(
|
||||
controlPage,
|
||||
'recording.get',
|
||||
{tabId, frameId: 0, limit: 160},
|
||||
)
|
||||
const candidate = snapshot.profileCandidates?.find((item) => (
|
||||
item.status === 'capture-required'
|
||||
&& pathOf(item.request?.url || '') === requestPath
|
||||
&& item.capturePlan?.transaction?.prerequisites?.some((step) => pathOf(step.url) === keyPath)
|
||||
))
|
||||
if (!candidate) {
|
||||
throw new Error(`Recording did not infer the online key dependency: ${JSON.stringify(snapshotSummary(snapshot))}`)
|
||||
}
|
||||
const transaction = candidate.capturePlan.transaction
|
||||
if (transaction.version !== 2
|
||||
|| transaction.prerequisites.length !== 1
|
||||
|| transaction.prerequisites[0].boundary !== 'fetch'
|
||||
|| transaction.prerequisites[0].response.bodyFormat !== 'json'
|
||||
|| !transaction.prerequisites[0].response.requiredPaths.includes('body.aes_key')
|
||||
|| !transaction.prerequisites[0].response.requiredPaths.includes('body.aes_iv')
|
||||
|| transaction.request.boundary !== 'fetch'
|
||||
|| pathOf(transaction.request.url) !== requestPath) {
|
||||
throw new Error(`Inferred request transaction is not evidence-complete: ${JSON.stringify({
|
||||
transaction,
|
||||
dependency: dependencySummary(snapshot, candidate),
|
||||
})}`)
|
||||
}
|
||||
|
||||
const matcherEvent = snapshot.events?.find((event) => event.id === candidate.capturePlan.matcherEventId)
|
||||
if (!matcherEvent?.crypto?.adapterId || !matcherEvent.wrapperHandleId) {
|
||||
throw new Error(`Online-key candidate has no deep-capture matcher: ${JSON.stringify(candidate)}`)
|
||||
}
|
||||
await extensionRequest(controlPage, 'deep.capture.start', {
|
||||
tabId,
|
||||
frameId: 0,
|
||||
matcher: {
|
||||
kind: 'crypto',
|
||||
adapterId: matcherEvent.crypto.adapterId,
|
||||
operation: matcherEvent.crypto.operation,
|
||||
wrapperHandleId: matcherEvent.wrapperHandleId,
|
||||
scriptUrl: matcherEvent.scriptUrl,
|
||||
frameHints: candidate.capturePlan.frameHints,
|
||||
},
|
||||
})
|
||||
|
||||
const replayButton = await performLogin(targetPage)
|
||||
let replayFailure
|
||||
const replay = replayButton.click({noWaitAfter: true, timeout: 20_000})
|
||||
.catch((reason) => { replayFailure = reason })
|
||||
const paused = await waitFor(
|
||||
controlPage,
|
||||
'deep.capture.status',
|
||||
{tabId, frameId: 0},
|
||||
(value) => value?.state === 'paused' && value.pause?.collecting !== true,
|
||||
20_000,
|
||||
)
|
||||
const automatic = paused.pause?.automaticCapture
|
||||
const frame = paused.pause?.frames?.find((item) => item.id === automatic?.frameId)
|
||||
if (automatic?.state !== 'ready'
|
||||
|| automatic.strategy !== 'request-transaction'
|
||||
|| frame?.functionName !== 'fetchAndSendDataAes') {
|
||||
throw new Error(`Deep capture selected an invalid strategy: ${JSON.stringify({automatic, functionName: frame?.functionName})}`)
|
||||
}
|
||||
const callable = await extensionRequest(controlPage, 'callable.create', {
|
||||
tabId,
|
||||
frameId: 0,
|
||||
source: 'deep-capture',
|
||||
strategy: 'request-transaction',
|
||||
callFrameId: frame.id,
|
||||
name: '在线取钥请求事务',
|
||||
candidateId: candidate.id,
|
||||
})
|
||||
await replay
|
||||
if (replayFailure) throw replayFailure
|
||||
await targetPage.waitForTimeout(300)
|
||||
if (observed.key !== 2 || observed.terminal !== 1) {
|
||||
throw new Error(`Deep capture did not preserve the prerequisite/terminal boundary: ${JSON.stringify(observed)}`)
|
||||
}
|
||||
|
||||
const beforeCallable = {...observed}
|
||||
const callableExecution = await extensionRequest(controlPage, 'callable.execute', {
|
||||
tabId,
|
||||
frameId: 0,
|
||||
callableId: callable.id,
|
||||
args: [plaintext],
|
||||
})
|
||||
if (typeof callableExecution.value?.encryptedData !== 'string') {
|
||||
throw new Error(`Request transaction did not return the terminal envelope: ${JSON.stringify(callableExecution)}`)
|
||||
}
|
||||
if (observed.key !== beforeCallable.key + 1 || observed.terminal !== beforeCallable.terminal) {
|
||||
throw new Error(`Callable execution leaked or skipped a request: ${JSON.stringify({beforeCallable, observed})}`)
|
||||
}
|
||||
|
||||
const plainPacket = {
|
||||
method: 'POST',
|
||||
url: new URL(requestPath, targetURL).toString(),
|
||||
headers: [{name: 'Content-Type', value: 'application/json'}],
|
||||
bodyBase64: Buffer.from(JSON.stringify(plaintext)).toString('base64'),
|
||||
}
|
||||
const validation = await extensionRequest(controlPage, 'analysis.profile.validate', {
|
||||
tabId,
|
||||
frameId: 0,
|
||||
candidateId: candidate.id,
|
||||
callableId: callable.id,
|
||||
inputPaths: ['body'],
|
||||
name: '在线取钥明文网关',
|
||||
packet: plainPacket,
|
||||
comparisonMode: 'structure',
|
||||
})
|
||||
if (!validation?.valid || !validation?.saveEligible || !validation.validationDraft?.id) {
|
||||
throw new Error(`Online-key Profile validation failed: ${JSON.stringify({
|
||||
valid: validation?.valid,
|
||||
saveEligible: validation?.saveEligible,
|
||||
proofLevel: validation?.proofLevel,
|
||||
})}`)
|
||||
}
|
||||
const validationDraft = await extensionRequest(
|
||||
controlPage,
|
||||
'analysis.profile.validation.latest',
|
||||
{tabId, frameId: 0},
|
||||
)
|
||||
if (!validationDraft?.profile || validationDraft.id !== validation.validationDraft.id) {
|
||||
throw new Error('Validated online-key Profile draft was not available for confirmation')
|
||||
}
|
||||
const savedProfile = await extensionRequest(
|
||||
controlPage,
|
||||
'transform.profile.save',
|
||||
validationDraft.profile,
|
||||
)
|
||||
if (
|
||||
savedProfile.requestTransaction?.callableId !== callable.id
|
||||
|| savedProfile.requestTransaction?.transaction?.version !== 2
|
||||
) {
|
||||
throw new Error('Saved Profile did not retain its trusted request-transaction binding')
|
||||
}
|
||||
|
||||
const beforeProfile = {...observed}
|
||||
const execution = await extensionRequest(controlPage, 'transform.execute', {
|
||||
profileId: savedProfile.id,
|
||||
direction: 'request',
|
||||
packet: plainPacket,
|
||||
})
|
||||
if (observed.key !== beforeProfile.key + 1 || observed.terminal !== beforeProfile.terminal) {
|
||||
throw new Error(`Saved Profile leaked or skipped a request: ${JSON.stringify({beforeProfile, observed})}`)
|
||||
}
|
||||
const sessionHeader = execution.setHeaders?.find((header) => header.name.toLowerCase() === 'cookie')
|
||||
if (!sessionHeader?.value.includes('PHPSESSID=')) {
|
||||
throw new Error(`Saved Profile did not bind the browser session to the outgoing packet: ${JSON.stringify(execution)}`)
|
||||
}
|
||||
const wireBody = JSON.parse(Buffer.from(execution.bodyBase64, 'base64').toString('utf8'))
|
||||
if (typeof wireBody.encryptedData !== 'string' || Object.keys(wireBody).length !== 1) {
|
||||
throw new Error(`Saved Profile produced an invalid terminal envelope: ${JSON.stringify(wireBody)}`)
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
execution.url,
|
||||
transformedFetchOptions(execution, plainPacket.headers),
|
||||
)
|
||||
const result = await response.json()
|
||||
if (!result.success) {
|
||||
throw new Error(`Target server rejected the session-bound Profile output: ${JSON.stringify(result)}`)
|
||||
}
|
||||
process.stdout.write(
|
||||
'Online-key transaction verified: evidence inferred one bounded prerequisite, the browser sent no terminal request during replay, the saved Profile exported its browser session, and the target server accepted the final packet.\n',
|
||||
)
|
||||
} finally {
|
||||
await harness?.close()
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import {
|
||||
extensionRequest,
|
||||
launchBrowserAgentContractHarness,
|
||||
transformedFetchOptions,
|
||||
waitFor,
|
||||
} from './browser-agent-contract-harness.mjs'
|
||||
|
||||
const targetURL = process.env.DES_TARGET || 'http://127.0.0.1:82/'
|
||||
const requestPath = '/encrypt/des.php'
|
||||
const plaintext = {username: 'admin', password: '123456'}
|
||||
let harness
|
||||
|
||||
function pathOf(url) {
|
||||
return new URL(url, targetURL).pathname
|
||||
}
|
||||
|
||||
async function performLogin(targetPage, password = 'wrong-password') {
|
||||
await targetPage.locator('#username').fill('admin')
|
||||
await targetPage.locator('#password').fill(password)
|
||||
await targetPage.getByRole('button', {name: '登录', exact: true}).click()
|
||||
return targetPage.getByRole('button', {name: 'Des规律Key', exact: true})
|
||||
}
|
||||
|
||||
try {
|
||||
harness = await launchBrowserAgentContractHarness({
|
||||
profilePrefix: 'yakit-des-',
|
||||
targetURL,
|
||||
})
|
||||
const {controlPage, tabId, targetPage} = harness
|
||||
let browserRequestCount = 0
|
||||
targetPage.on('request', (request) => {
|
||||
if (pathOf(request.url()) === requestPath) browserRequestCount += 1
|
||||
})
|
||||
|
||||
await extensionRequest(controlPage, 'recording.start', {
|
||||
tabId, frameId: 0, captureValues: true, maxEntries: 120, maxValueBytes: 8_192,
|
||||
})
|
||||
await (await performLogin(targetPage)).click()
|
||||
await targetPage.waitForTimeout(500)
|
||||
if (browserRequestCount !== 1) throw new Error(`Initial DES recording expected one request, received ${browserRequestCount}`)
|
||||
|
||||
const snapshot = await extensionRequest(controlPage, 'recording.get', {tabId, frameId: 0, limit: 120})
|
||||
const candidate = snapshot.profileCandidates?.find((item) => pathOf(item.request?.url || '') === requestPath)
|
||||
if (!candidate || candidate.status !== 'capture-required') {
|
||||
throw new Error(`Structured DES output was not routed through business-envelope capture: ${JSON.stringify(candidate)}`)
|
||||
}
|
||||
const matcherEvent = snapshot.events?.find((event) => event.id === candidate.capturePlan?.matcherEventId)
|
||||
if (matcherEvent?.crypto?.family !== 'symmetric'
|
||||
|| !matcherEvent.crypto.operation.toLowerCase().includes('des')
|
||||
|| !matcherEvent.wrapperHandleId) {
|
||||
throw new Error(`DES candidate has no reusable crypto matcher: ${JSON.stringify(matcherEvent)}`)
|
||||
}
|
||||
|
||||
await extensionRequest(controlPage, 'deep.capture.start', {
|
||||
tabId,
|
||||
frameId: 0,
|
||||
matcher: {
|
||||
kind: 'crypto',
|
||||
adapterId: matcherEvent.crypto.adapterId,
|
||||
operation: matcherEvent.crypto.operation,
|
||||
wrapperHandleId: matcherEvent.wrapperHandleId,
|
||||
scriptUrl: matcherEvent.scriptUrl,
|
||||
frameHints: candidate.capturePlan.frameHints,
|
||||
},
|
||||
})
|
||||
const replayButton = await performLogin(targetPage)
|
||||
let replayFailure
|
||||
const replay = replayButton.click({noWaitAfter: true, timeout: 20_000})
|
||||
.catch((reason) => { replayFailure = reason })
|
||||
const paused = await waitFor(controlPage, 'deep.capture.status', {tabId, frameId: 0}, (value) => (
|
||||
value?.state === 'paused' && value.pause?.collecting !== true
|
||||
), 20_000)
|
||||
const automatic = paused.pause?.automaticCapture
|
||||
const frame = paused.pause?.frames?.find((item) => item.id === automatic?.frameId)
|
||||
if (automatic?.state !== 'ready' || automatic.strategy !== 'request-transaction'
|
||||
|| frame?.functionName !== 'encryptAndSendDataDES') {
|
||||
throw new Error(`DES deep capture did not select its business request envelope: ${JSON.stringify({automatic, frame})}`)
|
||||
}
|
||||
const callable = await extensionRequest(controlPage, 'callable.create', {
|
||||
tabId,
|
||||
frameId: 0,
|
||||
source: 'deep-capture',
|
||||
strategy: 'request-transaction',
|
||||
callFrameId: frame.id,
|
||||
name: 'DES 请求事务',
|
||||
candidateId: candidate.id,
|
||||
})
|
||||
await replay
|
||||
if (replayFailure) throw replayFailure
|
||||
await targetPage.waitForTimeout(250)
|
||||
if (browserRequestCount !== 1) throw new Error(`DES capture leaked a terminal request; observed ${browserRequestCount}`)
|
||||
|
||||
const callableExecution = await extensionRequest(controlPage, 'callable.execute', {
|
||||
tabId, frameId: 0, callableId: callable.id, args: [plaintext],
|
||||
})
|
||||
if (callableExecution.value?.username !== plaintext.username
|
||||
|| !/^[a-f0-9]+$/i.test(callableExecution.value?.password || '')) {
|
||||
throw new Error(`DES request transaction did not preserve the Hex envelope: ${JSON.stringify(callableExecution)}`)
|
||||
}
|
||||
|
||||
const plainPacket = {
|
||||
method: 'POST',
|
||||
url: new URL(requestPath, targetURL).toString(),
|
||||
headers: [{name: 'Content-Type', value: 'application/json'}],
|
||||
bodyBase64: Buffer.from(JSON.stringify(plaintext)).toString('base64'),
|
||||
}
|
||||
const validation = await extensionRequest(controlPage, 'analysis.profile.validate', {
|
||||
tabId,
|
||||
frameId: 0,
|
||||
candidateId: candidate.id,
|
||||
callableId: callable.id,
|
||||
inputPaths: ['body'],
|
||||
name: 'DES 明文网关',
|
||||
packet: plainPacket,
|
||||
comparisonMode: 'structure',
|
||||
})
|
||||
if (!validation?.valid || !validation?.saveEligible || !validation.validationDraft?.id) {
|
||||
throw new Error(`DES Profile validation failed: ${JSON.stringify(validation)}`)
|
||||
}
|
||||
const validationDraft = await extensionRequest(controlPage, 'analysis.profile.validation.latest', {tabId, frameId: 0})
|
||||
const savedProfile = await extensionRequest(controlPage, 'transform.profile.save', validationDraft.profile)
|
||||
const explanationText = JSON.stringify(savedProfile.explanation)
|
||||
if (!explanationText.includes('DES') || explanationText.includes(plaintext.password)) {
|
||||
throw new Error(`DES semantic explanation is missing or persisted plaintext: ${explanationText}`)
|
||||
}
|
||||
|
||||
const execution = await extensionRequest(controlPage, 'transform.execute', {
|
||||
profileId: savedProfile.id,
|
||||
direction: 'request',
|
||||
packet: plainPacket,
|
||||
})
|
||||
const wireBody = JSON.parse(Buffer.from(execution.bodyBase64, 'base64').toString('utf8'))
|
||||
if (wireBody.username !== plaintext.username || !/^[a-f0-9]+$/i.test(wireBody.password || '')) {
|
||||
throw new Error(`Saved DES Profile produced an invalid terminal body: ${JSON.stringify(wireBody)}`)
|
||||
}
|
||||
if (!execution.nodeTrace?.length || !execution.fieldChanges?.some((change) => change.path === 'body.password')) {
|
||||
throw new Error(`Saved DES Profile did not return an explainable runtime trace: ${JSON.stringify(execution)}`)
|
||||
}
|
||||
if (browserRequestCount !== 1) throw new Error(`DES Profile execution leaked a browser request; observed ${browserRequestCount}`)
|
||||
|
||||
const response = await fetch(execution.url, transformedFetchOptions(execution, plainPacket.headers))
|
||||
const result = await response.json()
|
||||
if (!result.success) throw new Error(`Target server rejected the saved DES Profile output: ${JSON.stringify(result)}`)
|
||||
process.stdout.write('DES transaction verified: the structured CipherParams result required business-envelope capture, replay preserved Hex serialization, runtime evidence stayed value-free, and the target accepted the final packet.\n')
|
||||
} finally {
|
||||
await harness?.close()
|
||||
}
|
||||
+374
-78
@@ -52,6 +52,7 @@ const closureFunctionName = `build_${closureHoldoutSeed}`;
|
||||
const closureSenderName = `send_${closureHoldoutSeed}`;
|
||||
const closureInitialMarker = `module-recording-${closureHoldoutSeed}`;
|
||||
const closureReplayMarker = `module-replay-${closureHoldoutSeed}`;
|
||||
const closureProfileMarker = `module-profile-${closureHoldoutSeed}`;
|
||||
const WASM_XOR_MASK = 23;
|
||||
|
||||
function decryptRSALabValue(value) {
|
||||
@@ -441,6 +442,7 @@ function clientAuthPayload(origin, challenge, auth) {
|
||||
return [
|
||||
'yak-browser-bridge-v3', 'client-auth', origin, engineIdentityId, engineInstanceId, challenge,
|
||||
auth.installationId || '', auth.client || '', auth.version || '', [...(auth.capabilities || [])].sort().join(','),
|
||||
String(auth.capabilityCatalog?.version || ''), auth.capabilityCatalog?.hash || '',
|
||||
auth.taskId || '', auth.grantId || '', auth.resumeSessionId || '',
|
||||
].join('\n');
|
||||
}
|
||||
@@ -597,9 +599,35 @@ async function callBridge(socket, id, method, params) {
|
||||
return await response;
|
||||
}
|
||||
|
||||
function recordingDiagnostic(response) {
|
||||
const result = response?.result || response?.data;
|
||||
return JSON.stringify({
|
||||
errorCode: response?.error?.code,
|
||||
status: result?.status ? {
|
||||
active: result.status.active,
|
||||
documentAvailable: result.status.documentAvailable,
|
||||
count: result.status.count,
|
||||
droppedCount: result.status.droppedCount,
|
||||
} : undefined,
|
||||
eventKinds: result?.events?.slice(0, 64).map((event) => ({
|
||||
kind: event.kind,
|
||||
operation: event.operation,
|
||||
adapterId: event.crypto?.adapterId || event.transform?.adapterId,
|
||||
})),
|
||||
candidates: result?.profileCandidates?.slice(0, 24).map((candidate) => ({
|
||||
id: candidate.id,
|
||||
status: candidate.status,
|
||||
destination: candidate.request?.destination,
|
||||
serialization: candidate.request?.serialization,
|
||||
adapterId: candidate.source?.crypto?.adapterId,
|
||||
missing: candidate.missing?.map((item) => item.kind),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
const browserErrors = [];
|
||||
let context;
|
||||
try {
|
||||
agentContractRun: try {
|
||||
context = await chromium.launchPersistentContext(userDataDir, {
|
||||
executablePath,
|
||||
headless: true,
|
||||
@@ -656,7 +684,41 @@ try {
|
||||
const popup = await context.newPage();
|
||||
await popup.setViewportSize({ width: 390, height: 600 });
|
||||
await popup.goto(`chrome-extension://${extensionId}/popup.html`);
|
||||
await popup.locator('.popup-shell').waitFor();
|
||||
try {
|
||||
await popup.locator('.popup-shell').waitFor({ timeout: 8_000 });
|
||||
} catch {
|
||||
const popupText = await popup.locator('body').innerText().catch(() => '');
|
||||
const actionStatus = await popup.evaluate(async () => {
|
||||
const inspect = async (action) => Promise.race([
|
||||
chrome.runtime.sendMessage({ action }).then(
|
||||
(response) => response?.ok ? 'ok' : `error:${response?.error || 'unknown'}`,
|
||||
(error) => `rejected:${error instanceof Error ? error.message : String(error)}`,
|
||||
),
|
||||
new Promise((resolveStatus) => setTimeout(() => resolveStatus('timeout'), 1_000)),
|
||||
]);
|
||||
return Object.fromEntries(await Promise.all(
|
||||
['state.get', 'tab.active', 'bridge.status'].map(async (action) => [action, await inspect(action)]),
|
||||
));
|
||||
});
|
||||
const workerDiagnostic = await serviceWorker.evaluate(async () => ({
|
||||
metrics: (await chrome.storage.local.get('runtime.metrics.v1'))['runtime.metrics.v1'] || null,
|
||||
manifestVersion: chrome.runtime.getManifest().version,
|
||||
})).catch((error) => ({ error: error instanceof Error ? error.message : String(error) }));
|
||||
const diagnosticsPage = await context.newPage();
|
||||
await diagnosticsPage.goto(`chrome://extensions/?id=${extensionId}`);
|
||||
const extensionDiagnostic = await diagnosticsPage.evaluate(() => {
|
||||
const collect = (root) => {
|
||||
let text = root.textContent || '';
|
||||
for (const element of root.querySelectorAll('*')) {
|
||||
if (element.shadowRoot) text += ` ${collect(element.shadowRoot)}`;
|
||||
}
|
||||
return text;
|
||||
};
|
||||
return collect(document).replace(/\s+/g, ' ').trim().slice(0, 1_200);
|
||||
});
|
||||
await diagnosticsPage.close();
|
||||
throw new Error(`Popup did not finish loading: ${popupText.slice(0, 200)}; ${JSON.stringify(actionStatus)}; ${JSON.stringify(workerDiagnostic)}; ${extensionDiagnostic}; ${browserErrors.slice(-3).join(' | ')}`);
|
||||
}
|
||||
await popup.getByText('Authenticated Security Console', { exact: true }).waitFor();
|
||||
const popupBrandsLoaded = await popup.locator('.yak-mark, .yakit-mark').evaluateAll((images) => images.every((image) => image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0));
|
||||
if (!popupBrandsLoaded) throw new Error('Popup brand assets did not load');
|
||||
@@ -874,26 +936,26 @@ try {
|
||||
await testCookie.click();
|
||||
if (await options.locator('.rule-editor input').first().inputValue() !== 'yakit_e2e_session') throw new Error('Cookie Editor did not load the selected HttpOnly cookie');
|
||||
if (await options.getByText('HttpOnly', { exact: true }).count() === 0) throw new Error('Cookie Editor did not expose HttpOnly metadata');
|
||||
const cookieTransferChecks = await options.evaluate(async ({ url }) => {
|
||||
const cookieTransferChecks = await options.evaluate(async ({ url, tabId }) => {
|
||||
const send = async (action, payload) => {
|
||||
const response = await chrome.runtime.sendMessage({ action, payload });
|
||||
if (!response?.ok) throw new Error(response?.error || action);
|
||||
return response.data;
|
||||
};
|
||||
const redacted = await send('cookie.export', { url, format: 'set-cookie', includeValues: false });
|
||||
const sensitive = await send('cookie.export', { url, format: 'json', includeValues: true });
|
||||
const redacted = await send('cookie.export', { url, tabId, format: 'set-cookie', includeValues: false });
|
||||
const sensitive = await send('cookie.export', { url, tabId, format: 'json', includeValues: true });
|
||||
const imports = [];
|
||||
imports.push(await send('cookie.import', { url, format: 'json', text: JSON.stringify([{ name: 'json_import', value: 'json-value', path: '/' }]) }));
|
||||
imports.push(await send('cookie.import', { url, format: 'netscape', text: '127.0.0.1\tFALSE\t/\tFALSE\t0\tnetscape_import\tnetscape-value\n' }));
|
||||
imports.push(await send('cookie.import', { url, format: 'set-cookie', text: 'Set-Cookie: raw_import=raw-value; Path=/; HttpOnly; SameSite=Lax; Priority=High' }));
|
||||
const listed = await send('cookie.list', { url });
|
||||
imports.push(await send('cookie.import', { url, tabId, format: 'json', text: JSON.stringify([{ name: 'json_import', value: 'json-value', path: '/' }]) }));
|
||||
imports.push(await send('cookie.import', { url, tabId, format: 'netscape', text: '127.0.0.1\tFALSE\t/\tFALSE\t0\tnetscape_import\tnetscape-value\n' }));
|
||||
imports.push(await send('cookie.import', { url, tabId, format: 'set-cookie', text: 'Set-Cookie: raw_import=raw-value; Path=/; HttpOnly; SameSite=Lax; Priority=High' }));
|
||||
const listed = await send('cookie.list', { url, tabId });
|
||||
const imported = listed.filter((cookie) => ['json_import', 'netscape_import', 'raw_import'].includes(cookie.name));
|
||||
const removed = await send('cookie.removeMany', { cookies: imported.map((cookie) => ({
|
||||
url: `${cookie.secure ? 'https' : 'http'}://${cookie.domain.replace(/^\./, '')}${cookie.path}`,
|
||||
name: cookie.name, storeId: cookie.storeId, partitionKey: cookie.partitionKey,
|
||||
})) });
|
||||
return { redacted, sensitive, imports, imported: imported.map((cookie) => cookie.name), removed };
|
||||
}, { url: testUrl });
|
||||
}, { url: testUrl, tabId: targetTab.id });
|
||||
if (!cookieTransferChecks.redacted.includes('[REDACTED]') || cookieTransferChecks.redacted.includes('authenticated')) throw new Error(`Cookie export was not redacted: ${cookieTransferChecks.redacted}`);
|
||||
if (!cookieTransferChecks.sensitive.includes('authenticated')) throw new Error('Explicit Cookie value export omitted values');
|
||||
if (cookieTransferChecks.imported.length !== 3 || cookieTransferChecks.removed.removed !== 3 || cookieTransferChecks.imports[2].warnings.length === 0) {
|
||||
@@ -994,10 +1056,6 @@ try {
|
||||
if (!optionsTab?.id || !floatingFrame) throw new Error('Could not resolve floating frame sender boundary');
|
||||
await floatingFrame.locator('.floating-panel--embedded .floating-panel__body').waitFor({ state: 'visible', timeout: 10_000 });
|
||||
await floatingFrame.getByText('快速切换', { exact: true }).waitFor({ state: 'visible' });
|
||||
const floatingBrandLoaded = await floatingFrame.locator('.floating-panel__brand img').evaluate((image) => (
|
||||
image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0
|
||||
));
|
||||
if (!floatingBrandLoaded) throw new Error('Expanded floating panel Yak asset did not load');
|
||||
await floatingFrame.evaluate(async () => {
|
||||
await new Promise((resolveFrame) => requestAnimationFrame(() => requestAnimationFrame(resolveFrame)));
|
||||
});
|
||||
@@ -1061,7 +1119,9 @@ try {
|
||||
});
|
||||
}, { endpoint: bridgeEndpoint, tabId: targetTab.id });
|
||||
const { socket: bridgeSocket, hello } = await bridgeConnection;
|
||||
if (hello.type !== 'auth' || hello.client !== 'yakit-browser-extension' || hello.protocolVersion !== 3 || !hello.installationId || !hello.signature || !hello.capabilities?.includes('browser.eval')) {
|
||||
if (hello.type !== 'auth' || hello.client !== 'yakit-browser-extension' || hello.protocolVersion !== 3
|
||||
|| !hello.installationId || !hello.signature || !hello.capabilities?.includes('browser.eval')
|
||||
|| hello.capabilityCatalog?.version !== 1 || !hello.capabilityCatalog?.hash) {
|
||||
throw new Error(`Unexpected Bridge hello: ${JSON.stringify(hello)}`);
|
||||
}
|
||||
await options.getByRole('button', { name: '引擎连接' }).click();
|
||||
@@ -1305,7 +1365,7 @@ try {
|
||||
const recordingSnapshot = await callBridge(bridgeSocket, 'verify-recording-get', 'browser.recording.get', { tabId: targetTab.id, limit: 200 });
|
||||
const observedKinds = new Set(recordingSnapshot.result?.events?.map((item) => item.kind));
|
||||
for (const kind of ['fetch', 'xhr', 'form', 'beacon', 'worker', 'message', 'websocket', 'crypto']) {
|
||||
if (!observedKinds.has(kind)) throw new Error(`Browser recording missed ${kind}: ${JSON.stringify(recordingSnapshot)}`);
|
||||
if (!observedKinds.has(kind)) throw new Error(`Browser recording missed ${kind}: ${recordingDiagnostic(recordingSnapshot)}`);
|
||||
}
|
||||
const workerSendEvent = recordingSnapshot.result?.events?.find((item) => item.kind === 'worker' && item.operation === 'worker.postMessage');
|
||||
const workerReceiveEvent = recordingSnapshot.result?.events?.find((item) => item.kind === 'worker' && item.operation === 'worker.message');
|
||||
@@ -1313,38 +1373,38 @@ try {
|
||||
if (!workerSendEvent?.wrapperHandleId || !workerReceiveEvent || workerSendEvent.channelId !== workerReceiveEvent.channelId
|
||||
|| workerSendEvent.traceId !== workerReceiveEvent.traceId
|
||||
|| !recordingSnapshot.result?.links?.some((item) => item.kind === 'channel' && item.fromEventId === workerSendEvent.id && item.toEventId === workerReceiveEvent.id)) {
|
||||
throw new Error(`Worker boundary did not retain its exact handle and async Trace correlation: ${JSON.stringify(recordingSnapshot)}`);
|
||||
throw new Error(`Worker boundary did not retain its exact handle and async Trace correlation: ${recordingDiagnostic(recordingSnapshot)}`);
|
||||
}
|
||||
const unknownBoundaryCandidate = recordingSnapshot.result?.profileCandidates?.find((candidate) => (
|
||||
candidate.request.eventId === workerRequestEvent?.id && candidate.source.operation === 'unknown-business-envelope'
|
||||
));
|
||||
if (!unknownBoundaryCandidate || unknownBoundaryCandidate.status !== 'capture-required') {
|
||||
throw new Error(`Unknown Worker/ESM boundary did not remain actionable: ${JSON.stringify(recordingSnapshot)}`);
|
||||
throw new Error(`Unknown Worker/ESM boundary did not remain actionable: ${recordingDiagnostic(recordingSnapshot)}`);
|
||||
}
|
||||
const observedCryptoProviders = new Set(recordingSnapshot.result?.events
|
||||
?.filter((item) => item.kind === 'crypto')
|
||||
.map((item) => item.crypto?.adapterId));
|
||||
for (const provider of ['webcrypto', 'cryptojs', 'jsencrypt', 'sm-crypto', 'node-forge']) {
|
||||
if (!observedCryptoProviders.has(provider)) {
|
||||
throw new Error(`Browser recording missed the ${provider} crypto adapter: ${JSON.stringify(recordingSnapshot)}`);
|
||||
throw new Error(`Browser recording missed the ${provider} crypto adapter: ${recordingDiagnostic(recordingSnapshot)}`);
|
||||
}
|
||||
}
|
||||
const smOperations = new Set(recordingSnapshot.result?.events
|
||||
?.filter((item) => item.kind === 'crypto' && item.crypto?.adapterId === 'sm-crypto')
|
||||
.map((item) => item.crypto?.operation));
|
||||
for (const operation of ['sm2.encrypt', 'sm2.decrypt', 'sm2.sign', 'sm2.verify', 'sm3.digest', 'sm4.encrypt', 'sm4.decrypt']) {
|
||||
if (!smOperations.has(operation)) throw new Error(`Real sm-crypto fixture missed ${operation}: ${JSON.stringify(recordingSnapshot)}`);
|
||||
if (!smOperations.has(operation)) throw new Error(`Real sm-crypto fixture missed ${operation}: ${recordingDiagnostic(recordingSnapshot)}`);
|
||||
}
|
||||
const forgeEvents = recordingSnapshot.result?.events
|
||||
?.filter((item) => item.kind === 'crypto' && item.crypto?.adapterId === 'node-forge') || [];
|
||||
for (const phase of ['create', 'init', 'update', 'final']) {
|
||||
if (!forgeEvents.some((item) => item.crypto?.state?.phase === phase && item.crypto?.state?.correlationId)) {
|
||||
throw new Error(`Real node-forge fixture missed correlated ${phase} state: ${JSON.stringify(recordingSnapshot)}`);
|
||||
throw new Error(`Real node-forge fixture missed correlated ${phase} state: ${recordingDiagnostic(recordingSnapshot)}`);
|
||||
}
|
||||
}
|
||||
if (!forgeEvents.some((item) => item.crypto?.operation === 'rsa.encrypt' && item.callHandleId && item.callableCapable)
|
||||
|| !forgeEvents.some((item) => item.crypto?.operation === 'cipher.encrypt.output.toHex')) {
|
||||
throw new Error(`Real node-forge fixture missed RSA callable or cipher output boundary: ${JSON.stringify(recordingSnapshot)}`);
|
||||
throw new Error(`Real node-forge fixture missed RSA callable or cipher output boundary: ${recordingDiagnostic(recordingSnapshot)}`);
|
||||
}
|
||||
const linkedCryptoEvent = recordingSnapshot.result?.events?.find((item) => (
|
||||
item.kind === 'crypto' && item.crypto?.adapterId === 'cryptojs' && item.crypto?.operation === 'SHA256'
|
||||
@@ -1360,6 +1420,9 @@ try {
|
||||
const closureRequestEvent = recordingSnapshot.result?.events?.find((item) => (
|
||||
item.kind === 'fetch' && item.url?.includes(closureSubmitPath)
|
||||
));
|
||||
const closureCandidate = recordingSnapshot.result?.profileCandidates?.find((candidate) => (
|
||||
candidate.request?.eventId === closureRequestEvent?.id
|
||||
));
|
||||
const webCryptoDigestEvents = recordingSnapshot.result?.events?.filter((item) => (
|
||||
item.kind === 'crypto' && item.crypto?.adapterId === 'webcrypto'
|
||||
&& item.crypto?.operation === 'digest' && item.wrapperHandleId
|
||||
@@ -1398,21 +1461,33 @@ try {
|
||||
.map((item) => `${item.crypto?.adapterId}:${item.crypto?.operation}:${Boolean(item.wrapperHandleId)}`),
|
||||
})}`);
|
||||
}
|
||||
if (!closureRequestEvent || !closureDigestLink) {
|
||||
if (!closureRequestEvent || !closureDigestLink || !closureCandidate) {
|
||||
throw new Error(`Randomized ESM/WASM holdout did not use the generic evidence graph: ${JSON.stringify({
|
||||
closureSubmitPath,
|
||||
closureDigestField,
|
||||
request: closureRequestEvent,
|
||||
candidate: closureCandidate,
|
||||
digests: webCryptoDigestEvents,
|
||||
links: recordingSnapshot.result?.links?.filter((item) => item.toEventId === closureRequestEvent?.id),
|
||||
})}`);
|
||||
}
|
||||
const registeredTraces = await callBridge(
|
||||
bridgeSocket,
|
||||
'verify-closure-agent-trace-list',
|
||||
'browser.recording.trace.list',
|
||||
{ tabId: targetTab.id, limit: 100 },
|
||||
);
|
||||
if (registeredTraces.error || !registeredTraces.result?.some((trace) => (
|
||||
trace.candidates?.some((candidate) => candidate.id === closureCandidate.id)
|
||||
))) {
|
||||
throw new Error(`Randomized ESM/WASM candidate was not registered by the Agent trace contract: ${JSON.stringify(registeredTraces)}`);
|
||||
}
|
||||
const linkedFetchEvent = recordingSnapshot.result?.events?.find((item) => item.kind === 'fetch' && item.url?.includes('recorder-linked-fetch'));
|
||||
if (!linkedCryptoEvent || !linkedFetchEvent || !recordingSnapshot.result?.links?.some((link) => link.fromEventId === linkedCryptoEvent.id && link.toEventId === linkedFetchEvent.id)) {
|
||||
throw new Error(`Recording did not link CryptoJS output to Fetch input: ${JSON.stringify(recordingSnapshot)}`);
|
||||
throw new Error(`Recording did not link CryptoJS output to Fetch input: ${recordingDiagnostic(recordingSnapshot)}`);
|
||||
}
|
||||
if (!recordingSnapshot.result?.traces?.some((trace) => trace.eventIds.includes(linkedCryptoEvent.id) && trace.eventIds.includes(linkedFetchEvent.id))) {
|
||||
throw new Error(`Linked events were not grouped into one business Trace: ${JSON.stringify(recordingSnapshot)}`);
|
||||
throw new Error(`Linked events were not grouped into one business Trace: ${recordingDiagnostic(recordingSnapshot)}`);
|
||||
}
|
||||
const inferredProfile = recordingSnapshot.result?.profileCandidates?.find((candidate) => (
|
||||
candidate.source?.eventId === linkedCryptoEvent.id && candidate.request?.eventId === linkedFetchEvent.id
|
||||
@@ -1423,19 +1498,30 @@ try {
|
||||
|| inferredProfile.confidence?.level !== 'high'
|
||||
|| inferredProfile.source?.arguments?.[0]?.role !== 'data'
|
||||
|| inferredProfile.aiContext?.valuePolicy !== 'metadata-only') {
|
||||
throw new Error(`Recording did not infer a safe high-confidence Profile candidate: ${JSON.stringify(recordingSnapshot)}`);
|
||||
throw new Error(`Recording did not infer a safe high-confidence Profile candidate: ${recordingDiagnostic(recordingSnapshot)}`);
|
||||
}
|
||||
const formLinkedFetchEvent = recordingSnapshot.result?.events?.find((item) => item.kind === 'fetch' && item.url?.includes('recorder-form-linked-fetch'));
|
||||
const formFieldLink = recordingSnapshot.result?.links?.find((link) => (
|
||||
link.toEventId === formLinkedFetchEvent?.id && link.toPath === '$body:form.encryptedData'
|
||||
));
|
||||
const formLinkedCandidate = recordingSnapshot.result?.profileCandidates?.find((candidate) => (
|
||||
candidate.source?.eventId === formFieldLink?.fromEventId && candidate.request?.eventId === formLinkedFetchEvent?.id
|
||||
candidate.request?.eventId === formLinkedFetchEvent?.id
|
||||
&& candidate.request?.destination === 'body.encryptedData'
|
||||
));
|
||||
if (!formLinkedFetchEvent || !formFieldLink || formLinkedCandidate?.request?.destination !== 'body.encryptedData'
|
||||
|| formLinkedCandidate?.request?.serialization !== 'form-field'
|
||||
|| formLinkedCandidate?.status !== 'ready' || formLinkedCandidate?.confidence?.level !== 'high') {
|
||||
throw new Error(`Recording did not preserve the generic form field value chain: ${JSON.stringify(recordingSnapshot)}`);
|
||||
|| formLinkedCandidate?.status !== 'capture-required' || formLinkedCandidate?.confidence?.level !== 'high') {
|
||||
throw new Error(`Recording did not preserve the generic form field value chain: ${JSON.stringify({
|
||||
formLinkedFetchEvent,
|
||||
formFieldLink,
|
||||
formLinkedCandidate,
|
||||
relatedCandidates: recordingSnapshot.result?.profileCandidates?.filter((candidate) => (
|
||||
candidate.request?.eventId === formLinkedFetchEvent?.id
|
||||
)),
|
||||
relatedLinks: recordingSnapshot.result?.links?.filter((link) => (
|
||||
link.toEventId === formLinkedFetchEvent?.id
|
||||
)),
|
||||
})}`);
|
||||
}
|
||||
const rsaCryptoEvent = recordingSnapshot.result?.events?.find((item) => (
|
||||
item.kind === 'crypto' && item.crypto?.adapterId === 'jsencrypt' && item.crypto?.operation === 'encrypt'
|
||||
@@ -1444,8 +1530,7 @@ try {
|
||||
item.kind === 'fetch' && item.url?.includes('/encrypt/rsa.php?source=recorder-rsa-profile')
|
||||
));
|
||||
const rsaFieldLink = recordingSnapshot.result?.links?.find((link) => (
|
||||
link.fromEventId === rsaCryptoEvent?.id
|
||||
&& link.toEventId === rsaRequestEvent?.id
|
||||
link.toEventId === rsaRequestEvent?.id
|
||||
&& link.toPath === '$body:form.data'
|
||||
));
|
||||
const rsaCandidate = recordingSnapshot.result?.profileCandidates?.find((candidate) => (
|
||||
@@ -1457,10 +1542,15 @@ try {
|
||||
|| rsaCryptoEvent.crypto?.key?.kind !== 'public'
|
||||
|| rsaCryptoEvent.crypto?.key?.bits !== 1024
|
||||
|| !rsaCryptoEvent.crypto?.key?.fingerprint
|
||||
|| rsaCandidate?.status !== 'ready'
|
||||
|| rsaCandidate?.status !== 'capture-required'
|
||||
|| rsaCandidate.request?.destination !== 'body.data'
|
||||
|| rsaCandidate.request?.serialization !== 'form-field') {
|
||||
throw new Error(`JSEncrypt RSA was not inferred as an executable form-field Profile: ${JSON.stringify(recordingSnapshot)}`);
|
||||
throw new Error(`JSEncrypt RSA did not preserve its form-field evidence through serialization: ${JSON.stringify({
|
||||
rsaCryptoEvent,
|
||||
rsaRequestEvent,
|
||||
rsaFieldLink,
|
||||
rsaCandidate,
|
||||
})}`);
|
||||
}
|
||||
const rsaAIContext = JSON.stringify(rsaCandidate.aiContext);
|
||||
if (rsaAIContext.includes('BEGIN PUBLIC KEY') || rsaAIContext.includes(rsaLabPublicModulusHex)) {
|
||||
@@ -1478,7 +1568,7 @@ try {
|
||||
captureValues: true,
|
||||
});
|
||||
if (!deniedSensitiveRecording.error?.message?.includes('browser.recording.sensitive.read')) {
|
||||
throw new Error(`Recording value capture did not require its sensitive scope: ${JSON.stringify(deniedSensitiveRecording)}`);
|
||||
throw new Error(`Recording value capture did not require its sensitive scope: ${recordingDiagnostic(deniedSensitiveRecording)}`);
|
||||
}
|
||||
await callBridge(bridgeSocket, 'verify-recording-stop-metadata', 'browser.recording.stop', { tabId: targetTab.id });
|
||||
await options.evaluate(async ({ tabId, frameIds }) => {
|
||||
@@ -1493,6 +1583,7 @@ try {
|
||||
'browser.recording.read', 'browser.recording.control', 'browser.recording.sensitive.read', 'browser.callable.execute',
|
||||
'browser.debugger.read', 'browser.debugger.control',
|
||||
'browser.transform.read', 'browser.transform.manage', 'browser.transform.execute',
|
||||
'browser.isolation.read',
|
||||
'browser.proxy.read', 'browser.proxy.write',
|
||||
],
|
||||
durationMinutes: 5,
|
||||
@@ -1517,7 +1608,7 @@ try {
|
||||
});
|
||||
const sensitiveRecording = await callBridge(bridgeSocket, 'verify-recording-sensitive-get', 'browser.recording.get', { tabId: targetTab.id });
|
||||
if (!JSON.stringify(sensitiveRecording.result).includes('recorder-sensitive-preview-686')) {
|
||||
throw new Error(`Explicit recording value capture did not return its bounded preview: ${JSON.stringify(sensitiveRecording)}`);
|
||||
throw new Error(`Explicit recording value capture did not return its bounded preview: ${recordingDiagnostic(sensitiveRecording)}`);
|
||||
}
|
||||
const callableSource = sensitiveRecording.result?.events?.find((item) => (
|
||||
item.kind === 'crypto'
|
||||
@@ -1526,7 +1617,7 @@ try {
|
||||
&& item.callHandleId
|
||||
&& item.callableCapable
|
||||
));
|
||||
if (!callableSource) throw new Error(`Sensitive recording did not retain an executable JSEncrypt call handle: ${JSON.stringify(sensitiveRecording)}`);
|
||||
if (!callableSource) throw new Error(`Sensitive recording did not retain an executable JSEncrypt call handle: ${recordingDiagnostic(sensitiveRecording)}`);
|
||||
const createdCallable = await callBridge(bridgeSocket, 'verify-callable-create', 'browser.callable.create', {
|
||||
tabId: targetTab.id,
|
||||
source: 'recording',
|
||||
@@ -1656,24 +1747,135 @@ try {
|
||||
throw new Error(`Randomized ESM/WASM callable could not be retained: ${JSON.stringify(closureCallable)}`);
|
||||
}
|
||||
await webPage.locator('#opaque-module-result').getByText(closureReplayMarker, { exact: true }).waitFor();
|
||||
const closureReplay = await callBridge(bridgeSocket, 'verify-closure-callable-execute', 'browser.callable.execute', {
|
||||
const closureReplay = await callBridge(bridgeSocket, 'verify-closure-callable-replay', 'browser.callable.replay', {
|
||||
tabId: targetTab.id,
|
||||
callableId: closureCallable.result.id,
|
||||
args: [{ marker: closureReplayMarker, nested: { seed: closureHoldoutSeed } }],
|
||||
});
|
||||
if (closureReplay.error || !closureReplay.result?.value || typeof closureReplay.result.value !== 'object') {
|
||||
if (closureReplay.error || !closureReplay.result?.execution?.value
|
||||
|| typeof closureReplay.result.execution.value !== 'object') {
|
||||
throw new Error(`Randomized ESM/WASM callable replay failed: ${JSON.stringify(closureReplay)}`);
|
||||
}
|
||||
const closureServerResponse = await fetch(new URL(closureSubmitPath, testUrl), {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(closureReplay.result.value),
|
||||
body: JSON.stringify(closureReplay.result.execution.value),
|
||||
});
|
||||
const closureServerResult = await closureServerResponse.json();
|
||||
if (!closureServerResponse.ok || !closureServerResult.ok || closureServerResult.marker !== closureReplayMarker) {
|
||||
throw new Error(`Independent server rejected the retained ESM/WASM callable: ${JSON.stringify(closureServerResult)}`);
|
||||
}
|
||||
|
||||
const closurePlainPacket = {
|
||||
method: 'POST',
|
||||
url: new URL(closureSubmitPath, testUrl).toString(),
|
||||
headers: [{ name: 'Content-Type', value: 'application/json' }],
|
||||
bodyBase64: Buffer.from(JSON.stringify({
|
||||
marker: closureProfileMarker,
|
||||
nested: { seed: closureHoldoutSeed },
|
||||
})).toString('base64'),
|
||||
};
|
||||
const closureProposal = await callBridge(
|
||||
bridgeSocket,
|
||||
'verify-closure-agent-profile-propose',
|
||||
'browser.profile.propose',
|
||||
{
|
||||
tabId: targetTab.id,
|
||||
candidateId: closureCandidate.id,
|
||||
callableId: closureCallable.result.id,
|
||||
inputPaths: ['body'],
|
||||
name: 'Opaque ESM/WASM Agent contract',
|
||||
},
|
||||
);
|
||||
if (closureProposal.error
|
||||
|| closureProposal.result?.proposal?.compiler !== 'browser-transform-guided-v1') {
|
||||
throw new Error(`Randomized ESM/WASM Profile proposal failed: ${JSON.stringify(closureProposal)}`);
|
||||
}
|
||||
const closureValidation = await callBridge(
|
||||
bridgeSocket,
|
||||
'verify-closure-agent-profile-validate',
|
||||
'browser.profile.validate',
|
||||
{
|
||||
tabId: targetTab.id,
|
||||
candidateId: closureCandidate.id,
|
||||
callableId: closureCallable.result.id,
|
||||
inputPaths: ['body'],
|
||||
name: 'Opaque ESM/WASM Agent contract',
|
||||
packet: closurePlainPacket,
|
||||
comparisonMode: 'structure',
|
||||
},
|
||||
);
|
||||
if (closureValidation.error || !closureValidation.result?.valid
|
||||
|| closureValidation.result?.validationDraft?.contractVersion !== 1) {
|
||||
throw new Error(`Randomized ESM/WASM Profile validation failed: ${JSON.stringify(closureValidation)}`);
|
||||
}
|
||||
const closureValidationDraft = await callBridge(
|
||||
bridgeSocket,
|
||||
'verify-closure-agent-validation-latest',
|
||||
'browser.profile.validation.latest',
|
||||
{ tabId: targetTab.id },
|
||||
);
|
||||
if (closureValidationDraft.error
|
||||
|| closureValidationDraft.result?.id !== closureValidation.result.validationDraft.id
|
||||
|| closureValidationDraft.result?.contractVersion !== 1
|
||||
|| closureValidationDraft.result?.profile?.id) {
|
||||
throw new Error(`Randomized ESM/WASM Yakit handoff draft is invalid: ${JSON.stringify(closureValidationDraft)}`);
|
||||
}
|
||||
const closureProfile = await callBridge(
|
||||
bridgeSocket,
|
||||
'verify-closure-agent-profile-save',
|
||||
'browser.transform.profile.save',
|
||||
closureValidationDraft.result.profile,
|
||||
);
|
||||
if (closureProfile.error || !closureProfile.result?.id) {
|
||||
throw new Error(`Randomized ESM/WASM confirmed Profile was not saved: ${JSON.stringify(closureProfile)}`);
|
||||
}
|
||||
const closureProfileExecution = await callBridge(
|
||||
bridgeSocket,
|
||||
'verify-closure-agent-profile-execute',
|
||||
'browser.transform.execute',
|
||||
{
|
||||
profileId: closureProfile.result.id,
|
||||
direction: 'request',
|
||||
packet: closurePlainPacket,
|
||||
},
|
||||
);
|
||||
if (closureProfileExecution.error || !closureProfileExecution.result?.bodyBase64) {
|
||||
throw new Error(`Randomized ESM/WASM saved Profile did not execute: ${JSON.stringify(closureProfileExecution)}`);
|
||||
}
|
||||
const closureProfileServerResponse = await fetch(closureProfileExecution.result.url, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: Buffer.from(closureProfileExecution.result.bodyBase64, 'base64'),
|
||||
});
|
||||
const closureProfileServerResult = await closureProfileServerResponse.json();
|
||||
if (!closureProfileServerResponse.ok || !closureProfileServerResult.ok
|
||||
|| closureProfileServerResult.marker !== closureProfileMarker) {
|
||||
throw new Error(`Independent server rejected the ESM/WASM Profile output: ${JSON.stringify(closureProfileServerResult)}`);
|
||||
}
|
||||
const removedClosureProfile = await callBridge(
|
||||
bridgeSocket,
|
||||
'verify-closure-agent-profile-delete',
|
||||
'browser.transform.profile.delete',
|
||||
{ id: closureProfile.result.id },
|
||||
);
|
||||
if (removedClosureProfile.error
|
||||
|| removedClosureProfile.result?.some((profile) => profile.id === closureProfile.result.id)) {
|
||||
throw new Error(`Randomized ESM/WASM Agent contract Profile was not removed: ${JSON.stringify(removedClosureProfile)}`);
|
||||
}
|
||||
await callBridge(bridgeSocket, 'verify-closure-capture-detach', 'browser.deep_capture.detach', { tabId: targetTab.id });
|
||||
if (process.env.AGENT_CONTRACT_HOLDOUT_ONLY === '1') {
|
||||
console.log(JSON.stringify({
|
||||
verified: 'unknown-esm-wasm-agent-contract',
|
||||
contractVersion: closureValidationDraft.result.contractVersion,
|
||||
candidateId: closureCandidate.id,
|
||||
callableKind: closureCallable.result.kind,
|
||||
compiler: closureProposal.result.proposal.compiler,
|
||||
serializationSource: closureProposal.result.proposal.serializationSource,
|
||||
serverAccepted: closureProfileServerResult.ok,
|
||||
}, null, 2));
|
||||
break agentContractRun;
|
||||
}
|
||||
|
||||
await webPage.locator('#crypto-lab[data-ready="true"]').waitFor();
|
||||
await webPage.locator('#crypto-password').fill('deep-capture-first-901');
|
||||
@@ -2198,10 +2400,10 @@ try {
|
||||
const insecureRecording = await options.evaluate(async (tabId) => {
|
||||
return await chrome.runtime.sendMessage({ action: 'recording.get', payload: { tabId, limit: 100 } });
|
||||
}, insecureTab.id);
|
||||
if (!insecureRecording?.ok) throw new Error(`Could not read insecure HTTP recording: ${JSON.stringify(insecureRecording)}`);
|
||||
if (!insecureRecording?.ok) throw new Error(`Could not read insecure HTTP recording: ${recordingDiagnostic(insecureRecording)}`);
|
||||
const insecureKinds = new Set(insecureRecording.data.events.map((item) => item.kind));
|
||||
for (const kind of ['fetch', 'xhr', 'websocket', 'crypto']) {
|
||||
if (!insecureKinds.has(kind)) throw new Error(`Insecure HTTP recording missed ${kind}: ${JSON.stringify(insecureRecording)}`);
|
||||
if (!insecureKinds.has(kind)) throw new Error(`Insecure HTTP recording missed ${kind}: ${recordingDiagnostic(insecureRecording)}`);
|
||||
}
|
||||
const insecureRecordingStop = await options.evaluate(async (tabId) => {
|
||||
return await chrome.runtime.sendMessage({ action: 'recording.stop', payload: { tabId } });
|
||||
@@ -2395,7 +2597,7 @@ try {
|
||||
{ tabId: targetTab.id, captureValues: true, maxEntries: 80 },
|
||||
);
|
||||
if (automaticCaptureRecording.error) {
|
||||
throw new Error(`Could not start the automatic business capture recording: ${JSON.stringify(automaticCaptureRecording)}`);
|
||||
throw new Error(`Could not start the automatic business capture recording: ${recordingDiagnostic(automaticCaptureRecording)}`);
|
||||
}
|
||||
await webPage.locator('#crypto-password').fill('automatic-capture-seed-951');
|
||||
await webPage.locator('#crypto-submit').click();
|
||||
@@ -2431,6 +2633,7 @@ try {
|
||||
await webPage.locator('#crypto-password').fill('automatic-capture-replay-952');
|
||||
await webPage.locator('#crypto-submit').click();
|
||||
await options.locator('#gateway-mode-tab[aria-selected="true"]').waitFor({ timeout: 30_000 });
|
||||
await options.getByRole('button', { name: '配置', exact: true }).click();
|
||||
const automaticGateway = options.locator('.transform-guide');
|
||||
await automaticGateway.waitFor();
|
||||
if (await automaticGateway.getByLabel('输出形态').inputValue() !== 'body') {
|
||||
@@ -2468,11 +2671,16 @@ try {
|
||||
|| automaticCallable.operation !== 'buildLoginEnvelope') {
|
||||
throw new Error(`One-click capture did not retain the complete business callable: ${JSON.stringify(automaticCallables)}`);
|
||||
}
|
||||
await options.locator('.transform-editor-actions').getByRole('button', { name: '保存', exact: true }).click();
|
||||
const saveAutomaticPipeline = options.locator('.transform-editor-actions').getByRole('button', { name: '保存', exact: true });
|
||||
if (await saveAutomaticPipeline.isEnabled()) await saveAutomaticPipeline.click();
|
||||
const executeAutomaticPipeline = options.getByRole('button', { name: '执行 Pipeline', exact: true });
|
||||
await executeAutomaticPipeline.waitFor({ state: 'visible' });
|
||||
if (!await executeAutomaticPipeline.isEnabled()) {
|
||||
throw new Error('Automatic business gateway was neither persisted nor ready for local replay');
|
||||
}
|
||||
await executeAutomaticPipeline.click();
|
||||
await options.getByText('转换完成', { exact: true }).waitFor();
|
||||
await options.locator('.transform-test-debug').getByText('调试输出', { exact: true }).click();
|
||||
const automaticLogicalOutput = JSON.parse(await options.locator('.transform-test-result pre').textContent());
|
||||
if (!automaticLogicalOutput.body?.ciphertext || !automaticLogicalOutput.body?.signature || !automaticLogicalOutput.body?.iv) {
|
||||
throw new Error(`Automatic business gateway did not execute the complete page closure: ${JSON.stringify(automaticLogicalOutput)}`);
|
||||
@@ -2530,20 +2738,24 @@ try {
|
||||
limit: 40,
|
||||
});
|
||||
const guidedFormCandidate = guidedFormRecording.result?.profileCandidates?.find((candidate) => (
|
||||
candidate.status === 'ready'
|
||||
candidate.status === 'capture-required'
|
||||
&& candidate.request?.serialization === 'form-field'
|
||||
&& candidate.request?.destination === 'body.data'
|
||||
&& candidate.source?.crypto?.adapterId === 'jsencrypt'
|
||||
));
|
||||
if (!guidedFormCandidate?.source?.eventId) {
|
||||
throw new Error(`Dedicated recording did not produce a guided form candidate: ${JSON.stringify(guidedFormRecording)}`);
|
||||
throw new Error(`Dedicated recording did not produce a capture-ready form candidate: ${recordingDiagnostic(guidedFormRecording)}`);
|
||||
}
|
||||
|
||||
await options.getByRole('button', { name: '网络活动' }).click();
|
||||
await options.locator('#gateway-mode-tab').click();
|
||||
const transformWorkbench = options.locator('.transform-workbench');
|
||||
await transformWorkbench.waitFor();
|
||||
await options.getByText('E2E login plaintext gateway', { exact: true }).waitFor();
|
||||
const e2eTransformProfile = options.getByText('E2E login plaintext gateway', { exact: true });
|
||||
await e2eTransformProfile.waitFor();
|
||||
await e2eTransformProfile.click();
|
||||
await options.locator('.transform-data-flow').waitFor();
|
||||
await options.getByText('明文如何成为线上请求', { exact: true }).waitFor();
|
||||
const transformBounds = await transformWorkbench.evaluate((element) => ({
|
||||
clientWidth: element.clientWidth,
|
||||
scrollWidth: element.scrollWidth,
|
||||
@@ -2597,7 +2809,7 @@ try {
|
||||
if (guidedCallableForDelete.crypto?.adapterId !== 'jsencrypt'
|
||||
|| guidedCallableForDelete.crypto?.key?.bits !== 1024
|
||||
|| guidedCallableForDelete.crypto?.padding !== 'PKCS1-v1_5') {
|
||||
throw new Error(`The UI-created callable lost its RSA adapter metadata: ${JSON.stringify(guidedCallableForDelete)}`);
|
||||
throw new Error('The UI-created callable lost its RSA adapter metadata');
|
||||
}
|
||||
const guidedRsaReplay = await callBridge(bridgeSocket, 'verify-guided-rsa-replay', 'browser.callable.execute', {
|
||||
tabId: targetTab.id,
|
||||
@@ -2611,36 +2823,26 @@ try {
|
||||
if (guidedRsaReplay.error
|
||||
|| guidedRsaReplayPayload?.username !== 'guided-rsa-admin'
|
||||
|| guidedRsaReplayPayload?.password !== 'guided-rsa-password') {
|
||||
throw new Error(`The UI-created RSA callable could not replay through its retained receiver: ${JSON.stringify({ guidedRsaReplay, guidedRsaReplayPlaintext, guidedRsaReplayPayload })}`);
|
||||
throw new Error(`The UI-created RSA callable replay failed: ${guidedRsaReplay.error?.code || 'result-mismatch'}`);
|
||||
}
|
||||
await formInferencePanel.getByRole('button', { name: '生成明文网关', exact: true }).click();
|
||||
const guidedGateway = options.locator('.transform-guide');
|
||||
await guidedGateway.waitFor();
|
||||
if (await guidedGateway.getByLabel('输出形态').inputValue() !== 'form-field'
|
||||
|| await guidedGateway.getByLabel('表单字段名').inputValue() !== 'data') {
|
||||
throw new Error('Form evidence was not compiled into a guided form-field gateway');
|
||||
await formInferencePanel.getByRole(
|
||||
'button',
|
||||
{ name: '自动捕获完整加密流程', exact: true },
|
||||
).waitFor();
|
||||
if (await formInferencePanel.getByRole(
|
||||
'button',
|
||||
{ name: '生成明文网关', exact: true },
|
||||
).count()) {
|
||||
throw new Error('A low-level RSA callable bypassed the required business-capture step');
|
||||
}
|
||||
if (await options.getByLabel('URL 模式').inputValue() !== '*/encrypt/rsa.php'
|
||||
|| !await options.getByLabel('回放请求 URL').inputValue().then((value) => value.startsWith(`${new URL(testUrl).origin}/encrypt/rsa.php?`))) {
|
||||
throw new Error('Relative request evidence was not resolved against the current page');
|
||||
}
|
||||
await guidedGateway.getByText('自动设置表单 Content-Type', { exact: true }).waitFor();
|
||||
if (await options.locator('.transform-node-list').isVisible()) {
|
||||
throw new Error('Guided gateway leaked the advanced DAG editor');
|
||||
}
|
||||
const replayBody = await options.getByLabel('回放 Body').inputValue();
|
||||
if (JSON.stringify(JSON.parse(replayBody)) !== JSON.stringify({ username: 'admin', password: '123456' })) {
|
||||
throw new Error(`Recorded short sample was not carried into local replay: ${replayBody}`);
|
||||
}
|
||||
await options.locator('.transform-test-field-label').getByText('短时样本', { exact: true }).waitFor();
|
||||
await options.screenshot({ path: resolve(artifacts, 'options-browser-transform-guided-rsa.png') });
|
||||
await options.locator('.transform-callable-menu > summary').click();
|
||||
const disposableCallable = options.locator('.transform-callable-list section').filter({ hasText: guidedCallableForDelete.name });
|
||||
await disposableCallable.getByRole('button', { name: `删除 ${guidedCallableForDelete.name}`, exact: true }).click();
|
||||
await disposableCallable.getByRole('button', { name: '确认删除', exact: true }).click();
|
||||
await disposableCallable.waitFor({ state: 'detached' });
|
||||
await options.screenshot({ path: resolve(artifacts, 'options-browser-transform-callables.png') });
|
||||
await options.locator('#recording-mode-tab').click();
|
||||
await options.screenshot({ path: resolve(artifacts, 'options-browser-recording-capture-required.png') });
|
||||
await options.evaluate(async ({ tabId, callableId }) => {
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
action: 'callable.delete',
|
||||
payload: { tabId, callableId },
|
||||
});
|
||||
if (!response?.ok) throw new Error(response?.error || 'callable.delete');
|
||||
}, { tabId: targetTab.id, callableId: guidedCallableForDelete.id });
|
||||
await options.locator('#deep-mode-tab').click();
|
||||
const deepCaptureWorkspace = options.locator('.deep-capture');
|
||||
await deepCaptureWorkspace.waitFor();
|
||||
@@ -2681,6 +2883,54 @@ try {
|
||||
throw new Error(`Agent network capture did not include the explicitly granted request: ${JSON.stringify(bridgeNetworkList)}`);
|
||||
}
|
||||
|
||||
const authorizationAttestation = await callBridge(
|
||||
bridgeSocket,
|
||||
'verify-authorization-attestation',
|
||||
'browser.authorization.context.attest',
|
||||
{ tabId: targetTab.id, frameId: 0 },
|
||||
);
|
||||
if (authorizationAttestation.error || !authorizationAttestation.result?.id) {
|
||||
throw new Error(`Authorization attestation failed: ${JSON.stringify(authorizationAttestation)}`);
|
||||
}
|
||||
const authorizationCandidates = await callBridge(
|
||||
bridgeSocket,
|
||||
'verify-authorization-candidates',
|
||||
'browser.authorization.baseline.candidates',
|
||||
{
|
||||
tabId: targetTab.id,
|
||||
frameId: 0,
|
||||
authContextKind: 'attestation',
|
||||
authContextId: authorizationAttestation.result.id,
|
||||
limit: 20,
|
||||
},
|
||||
);
|
||||
const authorizationCandidate = authorizationCandidates.result?.find(
|
||||
(candidate) => candidate.url?.includes('/api/session') && candidate.eligible,
|
||||
);
|
||||
if (authorizationCandidates.error || !authorizationCandidate?.id) {
|
||||
throw new Error(`Authorization request candidate was unavailable: ${JSON.stringify(authorizationCandidates)}`);
|
||||
}
|
||||
const authorizationBaseline = await callBridge(
|
||||
bridgeSocket,
|
||||
'verify-authorization-baseline',
|
||||
'browser.authorization.baseline.capture',
|
||||
{
|
||||
tabId: targetTab.id,
|
||||
frameId: 0,
|
||||
authContextKind: 'attestation',
|
||||
authContextId: authorizationAttestation.result.id,
|
||||
networkRequestId: authorizationCandidate.id,
|
||||
comparisonKey: randomBytes(32).toString('base64url'),
|
||||
},
|
||||
);
|
||||
const authorizationBaselineJSON = JSON.stringify(authorizationBaseline.result || {});
|
||||
if (authorizationBaseline.error || !authorizationBaseline.result?.id
|
||||
|| authorizationBaselineJSON.includes('network-sensitive-e2e-771')
|
||||
|| authorizationBaselineJSON.includes('request-header-value')
|
||||
|| authorizationBaselineJSON.includes('yakit_e2e_session=authenticated')) {
|
||||
throw new Error('Authorization baseline capture failed its redaction contract');
|
||||
}
|
||||
|
||||
await options.getByRole('button', { name: 'Yakit' }).click();
|
||||
const fuzzerOpenMessage = await webFuzzerOpenRequest;
|
||||
const fuzzerPacket = Buffer.from(fuzzerOpenMessage.params?.rawRequestBase64 || '', 'base64').toString('utf8');
|
||||
@@ -2769,7 +3019,8 @@ try {
|
||||
await mobileOptions.locator('#gateway-mode-tab').click();
|
||||
const mobileTransformWorkbench = mobileOptions.locator('.transform-workbench');
|
||||
await mobileTransformWorkbench.waitFor();
|
||||
await mobileOptions.getByText('E2E login plaintext gateway', { exact: true }).waitFor();
|
||||
await mobileOptions.locator('.transform-profile-list').getByText('E2E login plaintext gateway', { exact: true }).waitFor();
|
||||
await mobileOptions.locator('.transform-data-flow').waitFor();
|
||||
await mobileOptions.waitForTimeout(250);
|
||||
const mobileTransformBounds = await mobileTransformWorkbench.evaluate((element) => ({
|
||||
clientWidth: element.clientWidth,
|
||||
@@ -2843,6 +3094,7 @@ try {
|
||||
'browser.dom.write',
|
||||
'browser.tab.activate', 'browser.page.invoke', 'browser.page.eval.expression', 'browser.human.takeover',
|
||||
'browser.network.read', 'browser.network.capture', 'browser.network.sensitive.read',
|
||||
'browser.isolation.read',
|
||||
'browser.proxy.read', 'browser.proxy.write',
|
||||
],
|
||||
durationMinutes: 5,
|
||||
@@ -2868,6 +3120,7 @@ try {
|
||||
'browser.dom.write',
|
||||
'browser.tab.activate', 'browser.page.invoke', 'browser.page.eval.expression', 'browser.human.takeover',
|
||||
'browser.network.read', 'browser.network.capture', 'browser.network.sensitive.read',
|
||||
'browser.isolation.read',
|
||||
'browser.proxy.read', 'browser.proxy.write',
|
||||
],
|
||||
durationMinutes: 5,
|
||||
@@ -2875,12 +3128,24 @@ try {
|
||||
});
|
||||
if (!response?.ok) throw new Error(response?.error || 'grant.create after reload');
|
||||
}, { tabId: targetTab.id });
|
||||
const staleAuthorizationBaseline = await callBridge(
|
||||
bridgeSocket,
|
||||
'verify-authorization-baseline-stale',
|
||||
'browser.authorization.baseline.get',
|
||||
{ id: authorizationBaseline.result.id },
|
||||
);
|
||||
if (staleAuthorizationBaseline.error?.code !== 'authorization_baseline_stale') {
|
||||
throw new Error('Authorization baseline survived a document and grant replacement');
|
||||
}
|
||||
|
||||
await options.evaluate(async () => await chrome.runtime.sendMessage({ action: 'panel.update', payload: { enabled: false } }));
|
||||
await webPage.locator('.floating-panel__brand').waitFor({ state: 'hidden' });
|
||||
await options.evaluate(async () => await chrome.runtime.sendMessage({ action: 'panel.update', payload: { enabled: true } }));
|
||||
await webPage.locator('.floating-panel__brand').waitFor({ state: 'visible' });
|
||||
await webPage.evaluate(() => dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyY', altKey: true, shiftKey: true, bubbles: true })));
|
||||
await webPage.evaluate(() => {
|
||||
if (document.activeElement instanceof HTMLElement) document.activeElement.blur();
|
||||
});
|
||||
await webPage.keyboard.press('Alt+Shift+Y');
|
||||
await webPage.locator('.floating-panel.is-expanded').waitFor();
|
||||
await webPage.locator('.floating-panel__brand').click();
|
||||
const currentOrigin = new URL(testUrl).origin;
|
||||
@@ -3069,7 +3334,38 @@ try {
|
||||
}
|
||||
if (browserErrors.length > 0) throw new Error(`Browser page errors:\n${browserErrors.join('\n')}`);
|
||||
|
||||
console.log(JSON.stringify({ extensionId, testUrl, evalResult: evalResponse.data, timeoutError: timeoutResponse.error, bridgeEval: bridgeEval.result, cancelledBridgeError: cancelledBridgeEval.error, handoffEvent, auditEventCount: networkAudit.length, capturedNetworkUrl: capturedRequest.url, fuzzerPageId: 'e2e-fuzzer-page', protocolChecks, staleDocumentError: staleDocumentEval.error, staleOriginError: staleOriginEval.error?.message, serviceWorkerRestart: { beforeRestart, afterRestart }, unpairedIdentity, recorderPerformance: { idleCallsMs: idleRecorderDurationMs, ...recorderPerformance }, rightMetrics, panelMetrics, narrowPanel, artifacts }, null, 2));
|
||||
console.log(JSON.stringify({
|
||||
status: 'passed',
|
||||
extensionId,
|
||||
testOrigin: new URL(testUrl).origin,
|
||||
checks: {
|
||||
expressionEval: evalResponse.data?.value?.answer === 42,
|
||||
timeout: Boolean(timeoutResponse.error),
|
||||
bridgeEval: bridgeEval.result?.value?.answer === 42,
|
||||
cancellation: cancelledBridgeEval.error?.code === 'cancelled',
|
||||
handoff: handoffEvent.params?.state === 'completed',
|
||||
authorizationBaseline: Boolean(authorizationBaseline.result?.id),
|
||||
authorizationRecovery: staleAuthorizationBaseline.error?.code,
|
||||
staleDocument: staleDocumentEval.error?.code,
|
||||
staleOrigin: Boolean(staleOriginEval.error),
|
||||
serviceWorkerRestart: beforeRestart.grantId === afterRestart.grantId,
|
||||
stableInstallationIdentity: (
|
||||
unpairedIdentity.beforeInstallationId === unpairedIdentity.afterInstallationId
|
||||
),
|
||||
},
|
||||
auditEventCount: networkAudit.length,
|
||||
capturedNetworkPath: new URL(capturedRequest.url).pathname,
|
||||
recorderPerformance: {
|
||||
idleCallsMs: idleRecorderDurationMs,
|
||||
...recorderPerformance,
|
||||
},
|
||||
layout: {
|
||||
floatingWidth: rightMetrics.width,
|
||||
expandedHeight: panelMetrics.height,
|
||||
narrowWidth: narrowPanel.width,
|
||||
},
|
||||
artifacts,
|
||||
}, null, 2));
|
||||
} finally {
|
||||
await context?.close();
|
||||
server.close();
|
||||
|
||||
Reference in New Issue
Block a user