feat: advance browser agent integration workflows

This commit is contained in:
go0p
2026-08-06 15:11:35 +08:00
committed by go0p
parent af5a4db694
commit 0c8e1c7b69
215 changed files with 35137 additions and 5442 deletions
@@ -0,0 +1,779 @@
import { browser } from 'wxt/browser';
import type {
BrowserAuthContextAttestation,
BrowserAuthContextHandle,
BrowserAuthorizationBaseline,
BrowserAuthorizationBaselineCandidate,
BrowserAuthorizationBaselinePacket,
BrowserAuthorizationCompiledRequest,
BrowserAuthorizationLogicalRequestBinding,
BrowserAuthorizationResourceSelector,
BrowserAuthorizationResourceValue,
BrowserAuthorizationTransformBinding,
BrowserTarget,
BrowserTransformProfile,
} from '@/types/models';
import {
exportNetworkRequest,
listNetworkRequests,
} from '@/features/network-capture/service';
import { ExtensionError } from '@/shared/errors';
import { getAuthContextHandle } from './auth-context';
import { getAuthContextAttestation } from './auth-attestation';
import {
MAX_AUTHORIZATION_BASELINE_BYTES,
MAX_AUTHORIZATION_BASELINE_FIELDS,
normalizeAuthorizationPath,
parseAuthorizationBaselineRequest,
} from './baseline-metadata';
import {
applyAuthorizationTransformExecution,
authorizationRequestToTransformPacket,
compileAuthorizationBaselineRequest,
extractAuthorizationResourceValue,
} from './baseline-execution';
import {
executeBrowserTransform,
getBrowserTransformProfile,
} from '@/features/browser-transform/service';
import { assertTransformRoute } from '@/features/browser-transform/mapping';
import { authorizationDynamicTransformDestinations } from './baseline-transform';
import {
assertAuthorizationLogicalPacketStructure,
authorizationPacketFingerprint,
buildAuthorizationLogicalRequestBinding,
decodeAndVerifyLogicalReplacement,
loadAuthorizationLogicalRequestBinding,
readAuthorizationLogicalResource,
replaceAuthorizationLogicalResource,
} from './logical-binding';
import {
browserTransformReplayDraftToPacket,
getBrowserTransformReplayDraft,
} from '@/features/browser-transform/replay-draft';
import {
readStructuredAuthorizationBodyValue,
} from './structured-body';
const MAX_BASELINES = 16;
const MAX_BASELINE_STORAGE_BYTES = 8 * 1_024 * 1_024;
const STORAGE_KEY = 'browser.authorization.baselines.v1';
function authorizationBytesToBase64(bytes: Uint8Array): string {
let binary = '';
const chunkSize = 0x8000;
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));
}
return btoa(binary);
}
interface StoredAuthorizationBaseline {
snapshot: BrowserAuthorizationBaseline;
rawRequestBase64: string;
requestUrl: string;
isHttps: boolean;
}
const baselines = new Map<string, StoredAuthorizationBaseline>();
let loaded = false;
function validAuthorizationRequestProtocol(value: {
protocol?: unknown;
operationFingerprint?: unknown;
operationNames?: unknown;
} | undefined): boolean {
if (!value) return false;
if (value.protocol === undefined) {
return value.operationFingerprint === undefined && value.operationNames === undefined;
}
return value.protocol === 'graphql'
&& /^sha256:[a-f0-9]{64}$/.test(String(value.operationFingerprint))
&& Array.isArray(value.operationNames)
&& value.operationNames.length > 0
&& value.operationNames.length <= 16
&& value.operationNames.every((name) => (
typeof name === 'string'
&& (
/^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(name)
|| /^(?:anonymous|batch-overflow)-[1-9][0-9]*$/.test(name)
)
));
}
function validLogicalRequestBinding(
value: unknown,
snapshot: Partial<BrowserAuthorizationBaseline>,
): value is BrowserAuthorizationLogicalRequestBinding {
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
const binding = value as Partial<BrowserAuthorizationLogicalRequestBinding>;
return binding.version === 1
&& binding.source === 'local-replay-draft'
&& binding.baselineId === snapshot.id
&& typeof binding.profileId === 'string'
&& binding.profileId.length > 0
&& typeof binding.profileName === 'string'
&& binding.profileName.length > 0
&& binding.isolationContextId === snapshot.isolationContextId
&& binding.cookieStoreId === snapshot.cookieStoreId
&& binding.origin === snapshot.origin
&& binding.target?.tabId === snapshot.target?.tabId
&& binding.target?.frameId === snapshot.target?.frameId
&& binding.target?.documentId === snapshot.target?.documentId
&& Boolean(binding.request)
&& validAuthorizationRequestProtocol(binding.request)
&& /^sha256:[a-f0-9]{64}$/.test(String(binding.request?.actionFingerprint))
&& Array.isArray(binding.request?.fields)
&& binding.request.fields.length <= MAX_AUTHORIZATION_BASELINE_FIELDS
&& Array.isArray(binding.outputDestinations)
&& binding.outputDestinations.length > 0
&& binding.outputDestinations.length <= 32
&& /^sha256:[a-f0-9]{64}$/.test(String(binding.bindingFingerprint))
&& typeof binding.profileUpdatedAt === 'number'
&& typeof binding.replayUpdatedAt === 'number'
&& binding.expiresAt === snapshot.expiresAt;
}
function validStoredBaseline(value: unknown): value is StoredAuthorizationBaseline {
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
const entry = value as Partial<StoredAuthorizationBaseline>;
const snapshot = entry.snapshot as Partial<BrowserAuthorizationBaseline> | undefined;
return snapshot?.version === 1
&& typeof snapshot.id === 'string'
&& snapshot.id.length > 0
&& typeof snapshot.deviceId === 'string'
&& typeof snapshot.installationId === 'string'
&& typeof snapshot.isolationContextId === 'string'
&& snapshot.isolationContextId.length > 0
&& typeof snapshot.cookieStoreId === 'string'
&& snapshot.cookieStoreId.length > 0
&& typeof snapshot.origin === 'string'
&& typeof snapshot.grantId === 'string'
&& typeof snapshot.networkRequestId === 'string'
&& Boolean(snapshot.target?.documentId)
&& ['handle', 'attestation'].includes(String(snapshot.authContextReference?.kind))
&& typeof snapshot.authContextReference?.id === 'string'
&& Boolean(snapshot.request)
&& validAuthorizationRequestProtocol(snapshot.request)
&& /^sha256:[a-f0-9]{64}$/.test(String(snapshot.request?.actionFingerprint))
&& Array.isArray(snapshot.request?.fields)
&& snapshot.request.fields.length <= MAX_AUTHORIZATION_BASELINE_FIELDS
&& typeof snapshot.createdAt === 'number'
&& typeof snapshot.expiresAt === 'number'
&& snapshot.expiresAt > snapshot.createdAt
&& typeof entry.rawRequestBase64 === 'string'
&& entry.rawRequestBase64.length <= Math.ceil(MAX_AUTHORIZATION_BASELINE_BYTES / 3) * 4 + 4
&& typeof entry.requestUrl === 'string'
&& entry.requestUrl.length <= 8_192
&& typeof entry.isHttps === 'boolean'
&& (
snapshot.logicalRequest === undefined
|| validLogicalRequestBinding(snapshot.logicalRequest, snapshot)
);
}
function purge(now = Date.now(), reserve = 0): boolean {
let changed = false;
for (const [id, baseline] of baselines) {
if (baseline.snapshot.expiresAt <= now) {
baselines.delete(id);
changed = true;
}
}
while (baselines.size > MAX_BASELINES - reserve) {
const oldest = baselines.keys().next().value as string | undefined;
if (!oldest) break;
baselines.delete(oldest);
changed = true;
}
return changed;
}
async function load(): Promise<void> {
if (loaded) return;
loaded = true;
try {
const stored = await browser.storage.session.get(STORAGE_KEY);
const values = stored[STORAGE_KEY];
if (!Array.isArray(values)) return;
for (const value of values.slice(-MAX_BASELINES)) {
if (validStoredBaseline(value)) baselines.set(value.snapshot.id, value);
}
purge();
} catch {
// The bounded in-memory registry remains available.
}
}
async function save(): Promise<void> {
try {
const retained: StoredAuthorizationBaseline[] = [];
for (const baseline of [...baselines.values()].reverse()) {
const candidate = [baseline, ...retained];
if (new TextEncoder().encode(JSON.stringify(candidate)).byteLength > MAX_BASELINE_STORAGE_BYTES) break;
retained.unshift(baseline);
}
baselines.clear();
for (const baseline of retained) baselines.set(baseline.snapshot.id, baseline);
await browser.storage.session.set({ [STORAGE_KEY]: retained });
} catch {
// The bounded in-memory registry remains available.
}
}
async function authContext(
kind: 'handle' | 'attestation',
id: string,
grantId: string,
): Promise<BrowserAuthContextHandle | BrowserAuthContextAttestation> {
return kind === 'handle'
? getAuthContextHandle(id, grantId)
: getAuthContextAttestation(id, grantId);
}
function sameTarget(
left: BrowserTarget,
right: BrowserTarget,
): boolean {
return left.tabId === right.tabId
&& left.frameId === right.frameId
&& left.documentId === right.documentId;
}
function authorizationDocumentOrigin(url: URL): string {
if (url.protocol === 'ws:') return `http://${url.host}`;
if (url.protocol === 'wss:') return `https://${url.host}`;
return url.origin;
}
export async function captureAuthorizationBaseline(input: {
target: BrowserTarget;
grantId: string;
authContextKind: 'handle' | 'attestation';
authContextId: string;
networkRequestId: string;
comparisonKey: string;
}): Promise<BrowserAuthorizationBaseline> {
await load();
const context = await authContext(input.authContextKind, input.authContextId, input.grantId);
if (!sameTarget(context.target, input.target)) {
throw new ExtensionError('target_denied', '授权基线请求与认证上下文不属于同一页面文档');
}
const exported = await exportNetworkRequest(input.target, input.networkRequestId);
const exportedURL = new URL(exported.url);
if (exportedURL.protocol === 'ws:' || exportedURL.protocol === 'wss:') {
throw new ExtensionError(
'authorization_protocol_unsupported',
'WebSocket 握手不能作为 HTTP 授权基线;请在录制中检查消息帧,当前版本不会把握手误当成可重放业务请求',
);
}
if (authorizationDocumentOrigin(exportedURL) !== context.origin) {
throw new ExtensionError('origin_changed', '授权基线请求与认证上下文来源不一致');
}
if (exported.limitations.length) {
throw new ExtensionError(
'authorization_baseline_incomplete',
`捕获请求不完整:${exported.limitations.join(';')}`,
);
}
const now = Date.now();
const snapshot: BrowserAuthorizationBaseline = {
version: 1,
id: crypto.randomUUID(),
deviceId: context.deviceId,
installationId: context.installationId,
isolationContextId: context.isolationContextId,
cookieStoreId: context.cookieStoreId,
origin: context.origin,
grantId: context.grantId,
target: context.target,
authContextReference: {
kind: input.authContextKind,
id: context.id,
},
networkRequestId: input.networkRequestId,
request: await parseAuthorizationBaselineRequest(
exported.rawRequestBase64,
exported.url,
input.comparisonKey,
),
createdAt: now,
expiresAt: context.expiresAt,
};
if (snapshot.expiresAt <= now) {
throw new ExtensionError('auth_context_stale', '认证上下文已经过期');
}
purge(now, 1);
baselines.set(snapshot.id, {
snapshot,
rawRequestBase64: exported.rawRequestBase64,
requestUrl: exported.url,
isHttps: exported.isHttps,
});
await save();
return snapshot;
}
export async function listAuthorizationBaselineCandidates(input: {
target: BrowserTarget;
grantId: string;
authContextKind: 'handle' | 'attestation';
authContextId: string;
limit: number;
}): Promise<BrowserAuthorizationBaselineCandidate[]> {
const context = await authContext(input.authContextKind, input.authContextId, input.grantId);
if (!sameTarget(context.target, input.target)) {
throw new ExtensionError('target_denied', '网络候选与认证上下文不属于同一页面文档');
}
const records = await listNetworkRequests(input.target, input.limit);
return records.flatMap((record) => {
let parsed: URL;
try {
parsed = new URL(record.url);
} catch {
return [];
}
if (authorizationDocumentOrigin(parsed) !== context.origin) return [];
const shapedPath = normalizeAuthorizationPath(parsed.pathname);
const reasons: string[] = [];
if (record.resourceType === 'websocket' || parsed.protocol === 'ws:' || parsed.protocol === 'wss:') {
reasons.push('WebSocket 当前仅保留握手与消息帧证据,不会进入 HTTP 授权矩阵');
}
if (!record.requestHeadersCaptured) reasons.push('未捕获实际请求头');
if (!['GET', 'HEAD', 'OPTIONS'].includes(record.method.toUpperCase())
&& !record.requestBody) {
reasons.push(record.requestBodyCaptured ? '浏览器未提供请求体' : '未捕获请求体');
}
if (record.requestBody?.truncated) reasons.push('请求体已截断');
if (record.requestBody?.reconstructed) reasons.push('请求体由浏览器字段重建');
if (record.error) reasons.push(`请求失败:${record.error}`);
return [{
id: record.id,
method: record.method,
url: `${parsed.origin}${shapedPath.normalized}`,
path: shapedPath.normalized,
resourceType: record.resourceType,
startedAt: record.startedAt,
completedAt: record.completedAt,
durationMs: record.durationMs,
statusCode: record.statusCode,
error: record.error,
eligible: reasons.length === 0,
reasons,
}];
});
}
async function validatedStoredBaseline(
id: string,
grantId: string,
validateLogicalBinding = true,
): Promise<StoredAuthorizationBaseline> {
await load();
if (purge()) await save();
const baseline = baselines.get(id);
if (!baseline || baseline.snapshot.grantId !== grantId) {
throw new ExtensionError('authorization_baseline_stale', '授权基线不存在、已过期或不属于当前共享会话');
}
try {
const context = await authContext(
baseline.snapshot.authContextReference.kind,
baseline.snapshot.authContextReference.id,
grantId,
);
if (!sameTarget(context.target, baseline.snapshot.target)) {
throw new ExtensionError('authorization_baseline_stale', '授权基线的认证上下文已经变化');
}
} catch (error) {
baselines.delete(id);
await save();
if (error instanceof ExtensionError && error.code === 'authorization_baseline_stale') throw error;
const message = error instanceof Error ? error.message : String(error);
throw new ExtensionError('authorization_baseline_stale', `授权基线实时复核失败:${message}`);
}
if (validateLogicalBinding && baseline.snapshot.logicalRequest) {
try {
await loadAuthorizationLogicalRequestBinding({ baseline: baseline.snapshot });
} catch {
baseline.snapshot = {
...baseline.snapshot,
logicalRequest: undefined,
};
baselines.set(id, baseline);
await save();
}
}
return baseline;
}
export async function getAuthorizationBaseline(
id: string,
grantId: string,
): Promise<BrowserAuthorizationBaseline> {
return (await validatedStoredBaseline(id, grantId)).snapshot;
}
export async function bindAuthorizationBaselineLogicalRequest(input: {
id: string;
grantId: string;
profileId: string;
comparisonKey: string;
}): Promise<BrowserAuthorizationBaseline> {
const baseline = await validatedStoredBaseline(input.id, input.grantId, false);
const profile = await getBrowserTransformProfile(input.profileId);
const draft = await getBrowserTransformReplayDraft(
profile.id,
'request',
baseline.snapshot.origin,
);
if (!draft) {
throw new ExtensionError(
'authorization_logical_missing',
'所选明文网关没有本机请求回放草稿,请先在明文网关中保存并验证回放输入',
);
}
const logicalRequest = await buildAuthorizationLogicalRequestBinding({
baseline: baseline.snapshot,
rawRequestBase64: baseline.rawRequestBase64,
profile,
draft,
comparisonKey: input.comparisonKey,
});
baseline.snapshot = {
...baseline.snapshot,
logicalRequest,
};
baselines.set(baseline.snapshot.id, baseline);
await save();
return baseline.snapshot;
}
function selectedBaselineField(
baseline: BrowserAuthorizationBaseline,
selector: BrowserAuthorizationResourceSelector,
) {
const sourceFields = selector.source === 'logical'
? baseline.logicalRequest?.request.fields
: baseline.request.fields;
const fields = (sourceFields || []).filter(
(field) => field.location === selector.location && field.path === selector.path,
);
if (fields.length !== 1) {
throw new ExtensionError(
fields.length ? 'authorization_selector_ambiguous' : 'authorization_selector_invalid',
fields.length ? '授权资源字段在基线中不唯一' : '授权资源字段不属于该请求基线',
);
}
if (!['string', 'number', 'boolean'].includes(fields[0].valueType)) {
throw new ExtensionError(
'authorization_selector_invalid',
'自动矩阵仅支持字符串、数字或布尔资源值',
);
}
return fields[0];
}
export async function readAuthorizationBaselineResource(input: {
id: string;
grantId: string;
selector: BrowserAuthorizationResourceSelector;
}): Promise<BrowserAuthorizationResourceValue> {
const baseline = await validatedStoredBaseline(input.id, input.grantId);
const selected = selectedBaselineField(baseline.snapshot, input.selector);
if (input.selector.source === 'logical') {
return readAuthorizationLogicalResource({
baseline: baseline.snapshot,
selector: input.selector,
});
}
if (input.selector.location === 'body') {
const value = readStructuredAuthorizationBodyValue(
authorizationRequestToTransformPacket(
baseline.rawRequestBase64,
baseline.snapshot.origin,
),
input.selector.path,
);
const bytes = new TextEncoder().encode(value.text);
if (bytes.byteLength > 8 * 1_024) {
throw new ExtensionError(
'authorization_value_too_large',
'授权 Body 资源值超过 8 KiB 上限',
);
}
return {
version: 1,
baselineId: baseline.snapshot.id,
source: 'wire',
location: 'body',
path: input.selector.path,
valueType: value.valueType,
byteLength: bytes.byteLength,
valueBase64: authorizationBytesToBase64(bytes),
valueFingerprint: selected.valueFingerprint,
};
}
const wireSelector = {
location: input.selector.location,
path: input.selector.path,
};
return extractAuthorizationResourceValue(
baseline.requestUrl,
baseline.rawRequestBase64,
baseline.snapshot.id,
wireSelector,
selected.valueFingerprint,
);
}
export async function compileAuthorizationBaseline(input: {
id: string;
grantId: string;
selector: BrowserAuthorizationResourceSelector;
replacement: BrowserAuthorizationResourceValue;
comparisonKey: string;
}): Promise<BrowserAuthorizationCompiledRequest> {
const baseline = await validatedStoredBaseline(input.id, input.grantId);
if (input.selector.source !== 'wire') {
throw new ExtensionError('authorization_selector_invalid', '直接编译只接受线上报文资源字段');
}
const wireSelector = {
source: 'wire' as const,
location: input.selector.location,
path: input.selector.path,
};
selectedBaselineField(baseline.snapshot, input.selector);
return compileAuthorizationBaselineRequest({
baselineId: baseline.snapshot.id,
rawRequestBase64: baseline.rawRequestBase64,
requestUrl: baseline.requestUrl,
publicUrl: baseline.snapshot.request.url,
selector: wireSelector,
replacement: input.replacement,
comparisonKey: input.comparisonKey,
isHttps: baseline.isHttps,
});
}
export async function compileAuthorizationBaselinePacket(input: {
id: string;
grantId: string;
}): Promise<BrowserAuthorizationBaselinePacket> {
const baseline = await validatedStoredBaseline(input.id, input.grantId);
return {
version: 1,
baselineId: baseline.snapshot.id,
method: baseline.snapshot.request.method,
url: baseline.snapshot.request.url,
isHttps: baseline.isHttps,
rawRequestBase64: baseline.rawRequestBase64,
packetFingerprint: await authorizationPacketFingerprint(baseline.rawRequestBase64),
};
}
async function authorizationTransformFingerprint(input: {
baselineId: string;
profileId: string;
profileUpdatedAt: number;
documentId: string;
isolationContextId: string;
cookieStoreId: string;
dynamicPaths: string[];
logicalBindingFingerprint?: string;
}): Promise<string> {
const digest = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(JSON.stringify(input)),
);
return `sha256:${[...new Uint8Array(digest)]
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('')}`;
}
async function validatedAuthorizationTransform(input: {
id: string;
grantId: string;
profileId: string;
}): Promise<{
baseline: StoredAuthorizationBaseline;
profile: BrowserTransformProfile;
binding: BrowserAuthorizationTransformBinding;
logical?: Awaited<ReturnType<typeof loadAuthorizationLogicalRequestBinding>>;
}> {
const baseline = await validatedStoredBaseline(input.id, input.grantId);
const profile = await getBrowserTransformProfile(input.profileId);
const target = baseline.snapshot.target;
if (
profile.target.tabId !== target.tabId
|| profile.target.frameId !== target.frameId
|| profile.target.documentId !== target.documentId
|| profile.origin !== baseline.snapshot.origin
|| profile.isolationContextId !== baseline.snapshot.isolationContextId
|| profile.cookieStoreId !== baseline.snapshot.cookieStoreId
) {
throw new ExtensionError(
'authorization_transform_target_mismatch',
'明文网关必须绑定授权基线所属的同一身份、Frame 与页面文档',
);
}
const logical = baseline.snapshot.logicalRequest?.profileId === profile.id
? await loadAuthorizationLogicalRequestBinding({
baseline: baseline.snapshot,
profileId: profile.id,
})
: undefined;
const packet = logical
? browserTransformReplayDraftToPacket(logical.draft)
: authorizationRequestToTransformPacket(
baseline.rawRequestBase64,
baseline.snapshot.origin,
);
assertTransformRoute(
profile.match.methods,
profile.match.urlPattern,
packet,
profile.origin,
);
const dynamicPaths = logical
? logical.binding.outputDestinations
: authorizationDynamicTransformDestinations(baseline.snapshot, profile);
const createdAt = Date.now();
const binding: BrowserAuthorizationTransformBinding = {
version: 1,
baselineId: baseline.snapshot.id,
profileId: profile.id,
profileName: profile.name,
isolationContextId: baseline.snapshot.isolationContextId,
cookieStoreId: baseline.snapshot.cookieStoreId,
target,
origin: baseline.snapshot.origin,
dynamicPaths,
bindingFingerprint: await authorizationTransformFingerprint({
baselineId: baseline.snapshot.id,
profileId: profile.id,
profileUpdatedAt: profile.updatedAt,
documentId: target.documentId,
isolationContextId: baseline.snapshot.isolationContextId,
cookieStoreId: baseline.snapshot.cookieStoreId,
dynamicPaths,
logicalBindingFingerprint: logical?.binding.bindingFingerprint,
}),
createdAt,
expiresAt: baseline.snapshot.expiresAt,
};
return { baseline, profile, binding, logical };
}
export async function inspectAuthorizationBaselineTransform(input: {
id: string;
grantId: string;
profileId: string;
}): Promise<BrowserAuthorizationTransformBinding> {
return (await validatedAuthorizationTransform(input)).binding;
}
export async function compileAuthorizationBaselineWithTransform(input: {
id: string;
grantId: string;
selector: BrowserAuthorizationResourceSelector;
replacement: BrowserAuthorizationResourceValue;
comparisonKey: string;
profileId: string;
bindingFingerprint: string;
}): Promise<BrowserAuthorizationCompiledRequest> {
const {
baseline,
profile,
binding,
logical,
} = await validatedAuthorizationTransform(input);
if (binding.bindingFingerprint !== input.bindingFingerprint) {
throw new ExtensionError(
'authorization_transform_changed',
'明文网关或页面文档已变化,请重新编译授权矩阵',
);
}
selectedBaselineField(baseline.snapshot, input.selector);
if (input.selector.source === 'logical') {
if (!logical || input.selector.location !== 'body') {
throw new ExtensionError(
'authorization_logical_missing',
'逻辑资源编译当前要求同一明文网关绑定下的 JSON/Form Body 字段',
);
}
const replacement = await decodeAndVerifyLogicalReplacement({
replacement: input.replacement,
selector: input.selector,
comparisonKey: input.comparisonKey,
});
const logicalPacket = replaceAuthorizationLogicalResource({
packet: browserTransformReplayDraftToPacket(logical.draft),
selector: input.selector,
replacement,
});
const execution = await executeBrowserTransform({
profileId: profile.id,
direction: 'request',
packet: logicalPacket,
});
const compiled: BrowserAuthorizationCompiledRequest = {
version: 1,
baselineId: baseline.snapshot.id,
selector: input.selector,
method: baseline.snapshot.request.method,
url: baseline.snapshot.request.url,
isHttps: baseline.isHttps,
rawRequestBase64: baseline.rawRequestBase64,
resourceValueFingerprint: input.replacement.valueFingerprint,
logicalBindingFingerprint: logical.binding.bindingFingerprint,
packetFingerprint: await authorizationPacketFingerprint(baseline.rawRequestBase64),
};
const compiledWithTransform = await applyAuthorizationTransformExecution({
compiled,
execution,
origin: baseline.snapshot.origin,
allowedDestinations: binding.dynamicPaths,
allowBody: true,
});
assertAuthorizationLogicalPacketStructure(
authorizationRequestToTransformPacket(
compiledWithTransform.rawRequestBase64,
baseline.snapshot.origin,
),
authorizationRequestToTransformPacket(
baseline.rawRequestBase64,
baseline.snapshot.origin,
),
);
return compiledWithTransform;
}
const wireSelector = {
source: 'wire' as const,
location: input.selector.location,
path: input.selector.path,
};
const compiled = await compileAuthorizationBaselineRequest({
baselineId: baseline.snapshot.id,
rawRequestBase64: baseline.rawRequestBase64,
requestUrl: baseline.requestUrl,
publicUrl: baseline.snapshot.request.url,
selector: wireSelector,
replacement: input.replacement,
comparisonKey: input.comparisonKey,
isHttps: baseline.isHttps,
});
const execution = await executeBrowserTransform({
profileId: profile.id,
direction: 'request',
packet: authorizationRequestToTransformPacket(
compiled.rawRequestBase64,
baseline.snapshot.origin,
),
});
return applyAuthorizationTransformExecution({
compiled,
execution,
origin: baseline.snapshot.origin,
allowedDestinations: binding.dynamicPaths,
});
}