rea-agents 2.4.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -19
- package/bridge/ghidra/ReaGhidraBridge.java +524 -4
- package/bridge/hopper_bridge.py +313 -28
- package/dist/application/AnalysisClientCleanup.js +14 -0
- package/dist/application/AnalysisContextQueries.js +130 -0
- package/dist/application/AnalysisSnapshotCache.js +4 -2
- package/dist/application/ApplicationWorkflowEvidenceResolver.js +31 -13
- package/dist/application/ArtifactInventory/scanCanonical.js +1 -1
- package/dist/application/BinarySession.js +20 -8
- package/dist/application/BinarySessionComposition.js +21 -0
- package/dist/application/BinarySessionRecords.js +79 -9
- package/dist/application/BinarySessionStatus.js +31 -1
- package/dist/application/BrowserEvidence.js +14 -0
- package/dist/application/BrowserScenarioCapturePort.js +1 -0
- package/dist/application/BrowserScenarioCaptureService.js +53 -0
- package/dist/application/BrowserScenarioEvidence.js +40 -0
- package/dist/application/CallPathTracing.js +160 -0
- package/dist/application/CapabilityInventory.js +84 -14
- package/dist/application/ClientRegistrationStatus.js +4 -1
- package/dist/application/CompositeProvider.js +10 -1
- package/dist/application/DirectAnalysis.js +13 -4
- package/dist/application/EnhancedLiteralTracing.js +118 -0
- package/dist/application/EnhancedTools.js +71 -127
- package/dist/application/EvidenceLedger.js +45 -16
- package/dist/application/JavaScriptApplicationWorkflowEvidence.js +13 -0
- package/dist/application/JavaScriptApplicationWorkflowService.js +27 -1
- package/dist/application/JavaScriptArtifactAnalysis.js +13 -4
- package/dist/application/JavaScriptRuntimeObservationEvidence.js +34 -0
- package/dist/application/JavaScriptRuntimeObservationPort.js +1 -0
- package/dist/application/JavaScriptRuntimeObservationService.js +46 -0
- package/dist/application/JavaScriptSemanticGraphAsyncProjection.js +147 -0
- package/dist/application/JavaScriptSemanticGraphBuilder.js +34 -6
- package/dist/application/JavaScriptSemanticGraphChildProcessProjection.js +156 -0
- package/dist/application/JavaScriptSemanticGraphConstruction.js +2 -2
- package/dist/application/JavaScriptSemanticGraphDataProjection.js +203 -0
- package/dist/application/JavaScriptSemanticGraphFingerprintProjection.js +25 -0
- package/dist/application/JavaScriptSemanticGraphFlowProjection.js +129 -8
- package/dist/application/JavaScriptSemanticGraphObjectProjection.js +81 -0
- package/dist/application/JavaScriptSemanticGraphProjection.js +14 -7
- package/dist/application/JavaScriptSemanticGraphResourceProjection.js +59 -0
- package/dist/application/JavaScriptSemanticGraphValueProjection.js +77 -0
- package/dist/application/LazyAnalysisProvider.js +77 -0
- package/dist/application/LoopbackReplay.js +1 -1
- package/dist/application/NativeApiInspection.js +80 -0
- package/dist/application/ProcessReactiveEffects.js +6 -0
- package/dist/application/ProcessReactiveHarness.js +16 -1
- package/dist/application/ProjectPermissionStore.js +1 -0
- package/dist/application/ReconstructionObligationAuthority.js +72 -0
- package/dist/application/ReconstructionObligationCandidates.js +265 -0
- package/dist/application/ReconstructionObligationLedgerDiagnostics.js +47 -0
- package/dist/application/ReconstructionObligationLedgerEvaluation.js +263 -0
- package/dist/application/ReconstructionObligationLedgerProjection.js +86 -0
- package/dist/application/ReconstructionObligationLedgerService.js +72 -0
- package/dist/application/ReconstructionObligationOriginalCases.js +33 -0
- package/dist/application/ReconstructionObligationPolicies.js +132 -0
- package/dist/application/ReconstructionReadinessService.js +48 -0
- package/dist/application/SessionProviderRouter.js +6 -6
- package/dist/application/SetupClientConfiguration.js +4 -2
- package/dist/application/runtime.js +26 -9
- package/dist/artifacts/ArtifactProvider.js +87 -22
- package/dist/browser/BrowserScenarioSecrets.js +53 -0
- package/dist/browser/BrowserScenarioSessionPort.js +1 -0
- package/dist/browser/CdpBrowserProvider.js +2 -2
- package/dist/browser/CdpCaptureDocuments.js +57 -0
- package/dist/browser/CdpCaptureStorage.js +71 -29
- package/dist/browser/CdpCaptureStorageFingerprints.js +328 -0
- package/dist/browser/CdpEndpoint.js +8 -6
- package/dist/browser/CdpPageCapture.js +2 -0
- package/dist/browser/JavaScriptRuntimeScope.js +38 -0
- package/dist/browser/PlaywrightBrowserScenarioProvider.js +259 -0
- package/dist/browser/PlaywrightExecutionBoundary.js +42 -0
- package/dist/browser/PlaywrightScenarioActions.js +62 -0
- package/dist/browser/PlaywrightScenarioArtifacts.js +238 -0
- package/dist/browser/PlaywrightScenarioBrowser.js +144 -0
- package/dist/browser/PlaywrightScenarioEvents.js +301 -0
- package/dist/browser/PlaywrightScenarioSession.js +300 -0
- package/dist/browser/V8InspectorCaptureProjection.js +110 -0
- package/dist/browser/V8InspectorEndpoint.js +72 -0
- package/dist/browser/V8InspectorProvider.js +267 -0
- package/dist/catalogIdentity.js +11 -3
- package/dist/cli/artifactCommands.js +44 -3
- package/dist/cli/coreAnalysisCommands.js +78 -0
- package/dist/cli.js +4 -0
- package/dist/cliApplicationCommands.js +30 -2
- package/dist/cliBrowserAdvancedCommands.js +17 -5
- package/dist/cliBrowserCommands.js +1 -0
- package/dist/cliBrowserScenarioCommands.js +43 -0
- package/dist/cliCommandNames.js +9 -0
- package/dist/cliJavaScriptRuntimeCommands.js +164 -0
- package/dist/cliObservationOptions.js +4 -0
- package/dist/cliPolicyCommands.js +1 -0
- package/dist/config/environment.js +12 -0
- package/dist/config/parseConfig.js +88 -17
- package/dist/contracts/analysisSearchContract.js +14 -0
- package/dist/contracts/applicationToolContracts.js +86 -2
- package/dist/contracts/applicationWorkflowInputContracts.js +18 -0
- package/dist/contracts/artifactToolContracts.js +29 -2
- package/dist/contracts/browserProviderToolContracts.js +7 -0
- package/dist/contracts/browserScenarioToolContracts.js +81 -0
- package/dist/contracts/browserToolContracts.js +88 -98
- package/dist/contracts/enhancedInputs.js +35 -6
- package/dist/contracts/functionInstructionContract.js +8 -0
- package/dist/contracts/functionWorkflowToolContracts.js +30 -0
- package/dist/contracts/hopperMemoryContracts.js +22 -0
- package/dist/contracts/javascriptApplicationWorkflowExamples.js +42 -0
- package/dist/contracts/javascriptRuntimeObservationToolContracts.js +58 -0
- package/dist/contracts/promptContracts.js +14 -15
- package/dist/contracts/reconstructionReadinessExample.js +436 -0
- package/dist/contracts/toolContractExamples.js +8 -0
- package/dist/contracts/toolContractTypes.js +1 -0
- package/dist/contracts/toolContracts.js +34 -25
- package/dist/contracts/toolEffects.js +25 -0
- package/dist/contracts/toolOutputSchemaGroups.js +120 -30
- package/dist/contracts/toolOutputSchemaPrimitives.js +37 -0
- package/dist/domain/analysisErrorPresentation.js +8 -2
- package/dist/domain/analysisErrorProjection.js +9 -0
- package/dist/domain/artifactInspection.js +347 -0
- package/dist/domain/browserCaptureComparison.js +82 -0
- package/dist/domain/browserObservation.js +8 -1
- package/dist/domain/browserObservationErrors.js +1 -0
- package/dist/domain/browserObservationSchemas.js +26 -0
- package/dist/domain/browserScenario.js +165 -0
- package/dist/domain/browserScenarioCapture.js +57 -0
- package/dist/domain/browserScenarioCaptureValues.js +204 -0
- package/dist/domain/browserScenarioDiff.js +324 -0
- package/dist/domain/browserScenarioDiffValues.js +157 -0
- package/dist/domain/browserScenarioNormalization.js +45 -0
- package/dist/domain/browserScenarioValues.js +372 -0
- package/dist/domain/bytecodeProvider.js +179 -0
- package/dist/domain/conformancePackage.js +169 -0
- package/dist/domain/conformanceReplay.js +104 -0
- package/dist/domain/conformanceTrustGate.js +193 -0
- package/dist/domain/customProtocolCapture.js +137 -0
- package/dist/domain/eventProcessTree.js +214 -0
- package/dist/domain/evidenceBundle.js +47 -0
- package/dist/domain/hopperStartupFailure.js +1 -1
- package/dist/domain/hopperValues.js +14 -1
- package/dist/domain/inputIssueProjection.js +7 -1
- package/dist/domain/javascriptRuntimeObservation.js +170 -0
- package/dist/domain/javascriptRuntimeReconciliationParsing.js +120 -2
- package/dist/domain/javascriptRuntimeReconciliationResult.js +3 -3
- package/dist/domain/javascriptRuntimeReconciliationRuntime.js +2 -2
- package/dist/domain/javascriptRuntimeReconciliationSchemas.js +1 -1
- package/dist/domain/javascriptSemanticAnalysis.js +13 -16
- package/dist/domain/javascriptSemanticAsyncEffects.js +253 -0
- package/dist/domain/javascriptSemanticChildProcesses.js +237 -0
- package/dist/domain/javascriptSemanticDataEffectHelpers.js +125 -0
- package/dist/domain/javascriptSemanticDataEffects.js +284 -0
- package/dist/domain/javascriptSemanticDerivedAnalysis.js +41 -0
- package/dist/domain/javascriptSemanticFingerprints.js +174 -0
- package/dist/domain/javascriptSemanticIr.js +21 -1
- package/dist/domain/javascriptSemanticObjects.js +115 -0
- package/dist/domain/javascriptSemanticPromises.js +290 -0
- package/dist/domain/javascriptSemanticResources.js +136 -0
- package/dist/domain/javascriptSourceParser.js +15 -0
- package/dist/domain/javascriptStaticAnalysis.js +12 -15
- package/dist/domain/javascriptStaticAnalysisBundler.js +38 -0
- package/dist/domain/javascriptStaticAnalysisCalls.js +23 -1
- package/dist/domain/javascriptStaticAnalysisHelpers.js +8 -0
- package/dist/domain/mobileApplicationInvestigation.js +159 -0
- package/dist/domain/nativeApiBoundary.js +130 -0
- package/dist/domain/objcSwiftMetadata.js +150 -0
- package/dist/domain/packageAnalysis.js +123 -0
- package/dist/domain/peInspection.js +196 -0
- package/dist/domain/processCapture.js +8 -2
- package/dist/domain/processReactiveScenario.js +1 -0
- package/dist/domain/processReactiveTransition.js +9 -4
- package/dist/domain/protocolCapture.js +243 -0
- package/dist/domain/providerCleanupError.js +11 -0
- package/dist/domain/reconstructionObligationLedgerSchemas.js +278 -0
- package/dist/domain/reconstructionReadiness.js +121 -0
- package/dist/domain/reconstructionReadinessFindingHelpers.js +17 -0
- package/dist/domain/reconstructionReadinessIntegrityFindings.js +118 -0
- package/dist/domain/reconstructionReadinessJourneyFindings.js +193 -0
- package/dist/domain/reconstructionReadinessSchemas.js +240 -0
- package/dist/domain/sourceToBundleComparison.js +335 -0
- package/dist/domain/sourceToBundleComparisonSchemas.js +133 -0
- package/dist/domain/sourceToBundleSignals.js +202 -0
- package/dist/domain/webCaptureDiff.js +141 -27
- package/dist/domain/webScreenshot.js +1 -1
- package/dist/dotnet/ManagedStaticProvider.js +34 -5
- package/dist/generatedMcpToolCatalog.js +37 -0
- package/dist/generatedPackageMetadata.js +4 -4
- package/dist/ghidra/GhidraDefaults.js +1 -1
- package/dist/ghidra/GhidraFunctionValues.js +40 -2
- package/dist/ghidra/GhidraProviderCapabilities.js +47 -38
- package/dist/hopper/HopperCleanup.js +158 -0
- package/dist/hopper/HopperClient.js +93 -99
- package/dist/hopper/HopperProvider.js +61 -30
- package/dist/hopper/HopperRequestQueue.js +215 -0
- package/dist/hopper/HopperResponseStream.js +46 -0
- package/dist/hopper/protocol.js +63 -20
- package/dist/main/transport.js +24 -27
- package/dist/main.js +14 -6
- package/dist/mcpDoctor.js +27 -4
- package/dist/mcpStartupPolicy.js +14 -0
- package/dist/process/ProcessCommandIdentity.js +19 -0
- package/dist/process/ProcessOwnership.js +11 -8
- package/dist/server/DynamicToolAvailability.js +70 -0
- package/dist/server/LazyToolCatalog.js +134 -0
- package/dist/server/createServer.js +92 -154
- package/dist/server/hydrateServerTools.js +191 -0
- package/dist/server/lifecycleProgress.js +10 -0
- package/dist/server/permissionFailure.js +12 -0
- package/dist/server/registerApplicationTools/compareSourceToBundle.js +35 -0
- package/dist/server/registerApplicationTools/helpers.js +11 -7
- package/dist/server/registerApplicationTools/obligationLedger.js +27 -0
- package/dist/server/registerApplicationTools/readiness.js +55 -0
- package/dist/server/registerApplicationTools.js +6 -0
- package/dist/server/registerArtifactTools.js +9 -1
- package/dist/server/registerBrowserScenarioTool.js +25 -0
- package/dist/server/registerBrowserTools.js +2 -2
- package/dist/server/registerCloseLifecycleTool.js +57 -0
- package/dist/server/registerEnhancedTools.js +28 -7
- package/dist/server/registerEvidenceResources.js +54 -101
- package/dist/server/registerEvidenceTools.js +9 -2
- package/dist/server/registerInvestigationTools.js +1 -2
- package/dist/server/registerJavaScriptApplicationGraphResource.js +0 -4
- package/dist/server/registerJavaScriptRuntimeObservationTools.js +37 -0
- package/dist/server/registerReconstructionObligationLedgerResource.js +32 -0
- package/dist/server/registerReconstructionReadinessResource.js +32 -0
- package/dist/server/registerSessionRecordTools.js +82 -12
- package/dist/server/registerSessionStatusTool.js +11 -8
- package/dist/server/registerSessionTools.js +32 -58
- package/dist/server/sessionAvailabilityPolicy.js +10 -8
- package/dist/server/toolRegistrationOptions.js +21 -6
- package/dist/server/toolResult.js +12 -0
- package/package.json +28 -11
- package/scripts/package-runner-bootstrap.mjs +72 -0
- package/scripts/rea.mjs +11 -1
- package/skills/reverse-engineer-anything/SKILL.md +10 -3
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { browserOriginSchema, sanitizeBrowserUrl, } from "../domain/browserObservation.js";
|
|
2
|
+
import { authorizedElectronFile, canonicalElectronRoots, } from "./ElectronFileScope.js";
|
|
3
|
+
/** Canonicalize exact filesystem scopes before Inspector attachment. */
|
|
4
|
+
export const canonicalRuntimeRoots = canonicalElectronRoots;
|
|
5
|
+
/** Authorize one protocol URL without retaining an out-of-scope value. */
|
|
6
|
+
export const authorizeRuntimeLocation = async (value, roots, origins) => {
|
|
7
|
+
if (value.startsWith("node:") && value.length <= 4_096)
|
|
8
|
+
return {
|
|
9
|
+
allowed: true,
|
|
10
|
+
location: { kind: "builtin", specifier: value },
|
|
11
|
+
};
|
|
12
|
+
if (value.startsWith("file:")) {
|
|
13
|
+
const filePath = await authorizedElectronFile(value, roots);
|
|
14
|
+
return filePath === undefined
|
|
15
|
+
? { allowed: false, reason: "outside_file_roots" }
|
|
16
|
+
: { allowed: true, location: { kind: "file", file_path: filePath } };
|
|
17
|
+
}
|
|
18
|
+
let url;
|
|
19
|
+
try {
|
|
20
|
+
url = new URL(value);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return { allowed: false, reason: "unsupported_location" };
|
|
24
|
+
}
|
|
25
|
+
if (!["http:", "https:"].includes(url.protocol))
|
|
26
|
+
return { allowed: false, reason: "unsupported_location" };
|
|
27
|
+
const parsedOrigin = browserOriginSchema.safeParse(url.origin);
|
|
28
|
+
if (!parsedOrigin.success || !origins.includes(parsedOrigin.data))
|
|
29
|
+
return { allowed: false, reason: "outside_origins" };
|
|
30
|
+
return {
|
|
31
|
+
allowed: true,
|
|
32
|
+
location: {
|
|
33
|
+
kind: "url",
|
|
34
|
+
origin: parsedOrigin.data,
|
|
35
|
+
sanitized_url: sanitizeBrowserUrl(value).url,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
};
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { sanitizeBrowserUrl } from "../domain/browserObservation.js";
|
|
2
|
+
import { browserScenarioCaptureSchema, browserScenarioCompletenessSchema, browserScenarioStepSchema, browserStepArtifactsSchema, } from "../domain/browserScenarioCapture.js";
|
|
3
|
+
import { AnalysisError, BrowserObservationError, ProviderAdapterError, } from "../domain/errors.js";
|
|
4
|
+
import { err, ok } from "../domain/result.js";
|
|
5
|
+
import { BrowserScenarioCaptureBudget } from "./PlaywrightScenarioArtifacts.js";
|
|
6
|
+
const OPERATION = "capture_browser_scenario";
|
|
7
|
+
let defaultFactory;
|
|
8
|
+
const lazyPlaywrightFactory = {
|
|
9
|
+
async open(scenario, options) {
|
|
10
|
+
defaultFactory ??= import("./PlaywrightScenarioSession.js").then(({ PlaywrightScenarioSessionFactory }) => new PlaywrightScenarioSessionFactory());
|
|
11
|
+
return (await defaultFactory).open(scenario, options);
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
export const PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY = Object.freeze({
|
|
15
|
+
id: "rea-playwright-browser-scenario",
|
|
16
|
+
name: "REA Playwright browser scenario capture provider",
|
|
17
|
+
version: "1",
|
|
18
|
+
});
|
|
19
|
+
const requestedForStep = (scenario, stepIndex) => new Set([
|
|
20
|
+
...scenario.capture.after_each_step,
|
|
21
|
+
...(stepIndex === scenario.actions.length ? scenario.capture.at_end : []),
|
|
22
|
+
]);
|
|
23
|
+
const unavailableArtifacts = (requested, reason) => {
|
|
24
|
+
const state = (kind) => requested.has(kind)
|
|
25
|
+
? { state: "missing", reason }
|
|
26
|
+
: { state: "not_requested" };
|
|
27
|
+
return browserStepArtifactsSchema.parse({
|
|
28
|
+
screenshot: state("screenshot"),
|
|
29
|
+
dom: state("dom"),
|
|
30
|
+
accessibility: state("accessibility"),
|
|
31
|
+
url: state("url"),
|
|
32
|
+
history: state("history"),
|
|
33
|
+
storage: state("storage"),
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
const stepCompleteness = (artifacts, status) => {
|
|
37
|
+
const missing = Object.entries(artifacts)
|
|
38
|
+
.filter(([, state]) => state.state === "missing")
|
|
39
|
+
.map(([section]) => section);
|
|
40
|
+
const truncated = Object.entries(artifacts)
|
|
41
|
+
.filter(([, state]) => state.state === "truncated")
|
|
42
|
+
.map(([section]) => section);
|
|
43
|
+
if (status !== "completed")
|
|
44
|
+
missing.push("action");
|
|
45
|
+
const condition = truncated.length > 0
|
|
46
|
+
? "truncated"
|
|
47
|
+
: missing.length > 0
|
|
48
|
+
? "incomplete"
|
|
49
|
+
: "complete";
|
|
50
|
+
return browserScenarioCompletenessSchema.parse({
|
|
51
|
+
status: condition,
|
|
52
|
+
equality_eligible: condition === "complete",
|
|
53
|
+
missing_sections: [...new Set(missing)].sort(),
|
|
54
|
+
truncated_sections: [...new Set(truncated)].sort(),
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
const createStep = (input) => browserScenarioStepSchema.parse({
|
|
58
|
+
step_index: input.stepIndex,
|
|
59
|
+
step_id: input.stepId,
|
|
60
|
+
action: input.action,
|
|
61
|
+
status: input.status,
|
|
62
|
+
elapsed_ms: input.elapsedMs,
|
|
63
|
+
before_url: sanitizeBrowserUrl(input.beforeUrl),
|
|
64
|
+
after_url: sanitizeBrowserUrl(input.afterUrl),
|
|
65
|
+
error: input.error,
|
|
66
|
+
event_sequence_start: input.eventStart,
|
|
67
|
+
event_sequence_end: input.eventEnd,
|
|
68
|
+
artifacts: input.artifacts,
|
|
69
|
+
completeness: stepCompleteness(input.artifacts, input.status),
|
|
70
|
+
});
|
|
71
|
+
const initialStep = async (input) => {
|
|
72
|
+
const { session, scenario, budget, elapsedMs, maximumTimeoutMs, signal } = input;
|
|
73
|
+
const artifacts = await session.capture(requestedForStep(scenario, 0), budget, maximumTimeoutMs, signal);
|
|
74
|
+
return createStep({
|
|
75
|
+
stepIndex: 0,
|
|
76
|
+
stepId: "scenario_start",
|
|
77
|
+
action: "goto_start",
|
|
78
|
+
status: "completed",
|
|
79
|
+
elapsedMs,
|
|
80
|
+
beforeUrl: session.initialUrl,
|
|
81
|
+
afterUrl: session.currentUrl(),
|
|
82
|
+
error: null,
|
|
83
|
+
eventStart: 1,
|
|
84
|
+
eventEnd: session.lastEventSequence(),
|
|
85
|
+
artifacts,
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
const executeStep = async (input) => {
|
|
89
|
+
const { session, scenario, budget, action, stepIndex, startedAt, priorFailure, signal, } = input;
|
|
90
|
+
const beforeUrl = session.currentUrl();
|
|
91
|
+
const eventStart = session.nextEventSequence();
|
|
92
|
+
const requested = requestedForStep(scenario, stepIndex);
|
|
93
|
+
if (priorFailure)
|
|
94
|
+
return createStep({
|
|
95
|
+
stepIndex,
|
|
96
|
+
stepId: action.step_id,
|
|
97
|
+
action: action.action,
|
|
98
|
+
status: "cancelled",
|
|
99
|
+
elapsedMs: 0,
|
|
100
|
+
beforeUrl,
|
|
101
|
+
afterUrl: beforeUrl,
|
|
102
|
+
error: "not executed after an earlier action failure",
|
|
103
|
+
eventStart,
|
|
104
|
+
eventEnd: session.lastEventSequence(),
|
|
105
|
+
artifacts: unavailableArtifacts(requested, "action was not executed after an earlier failure"),
|
|
106
|
+
});
|
|
107
|
+
const actionStartedAt = Date.now();
|
|
108
|
+
session.setStep(stepIndex);
|
|
109
|
+
const requestCancelled = () => signal?.aborted === true;
|
|
110
|
+
let status = "completed";
|
|
111
|
+
let error = null;
|
|
112
|
+
if (requestCancelled()) {
|
|
113
|
+
status = "cancelled";
|
|
114
|
+
error = "scenario request cancelled";
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
try {
|
|
118
|
+
const elapsed = Date.now() - startedAt;
|
|
119
|
+
const remaining = scenario.limits.max_duration_ms - elapsed;
|
|
120
|
+
if (remaining <= 0)
|
|
121
|
+
throw new Error("Scenario duration limit reached");
|
|
122
|
+
await session.perform(action, remaining, signal);
|
|
123
|
+
}
|
|
124
|
+
catch (cause) {
|
|
125
|
+
status = requestCancelled() ? "cancelled" : "failed";
|
|
126
|
+
error = session.redactError(cause);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const remaining = scenario.limits.max_duration_ms - (Date.now() - startedAt);
|
|
130
|
+
const artifacts = status === "cancelled" || remaining <= 0
|
|
131
|
+
? unavailableArtifacts(requested, status === "cancelled"
|
|
132
|
+
? "scenario request cancelled"
|
|
133
|
+
: "scenario duration limit reached")
|
|
134
|
+
: await session.capture(requested, budget, remaining, signal);
|
|
135
|
+
return createStep({
|
|
136
|
+
stepIndex,
|
|
137
|
+
stepId: action.step_id,
|
|
138
|
+
action: action.action,
|
|
139
|
+
status,
|
|
140
|
+
elapsedMs: Date.now() - actionStartedAt,
|
|
141
|
+
beforeUrl,
|
|
142
|
+
afterUrl: session.currentUrl(),
|
|
143
|
+
error,
|
|
144
|
+
eventStart,
|
|
145
|
+
eventEnd: session.lastEventSequence(),
|
|
146
|
+
artifacts,
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
const globalCompleteness = (scenario, session, steps) => {
|
|
150
|
+
const missing = steps.flatMap(({ completeness }) => completeness.missing_sections);
|
|
151
|
+
const truncated = steps.flatMap(({ completeness }) => completeness.truncated_sections);
|
|
152
|
+
truncated.push(...session.eventTruncationSections());
|
|
153
|
+
if (session.mode === "connect")
|
|
154
|
+
missing.push("events");
|
|
155
|
+
const status = truncated.length > 0
|
|
156
|
+
? "truncated"
|
|
157
|
+
: missing.length > 0
|
|
158
|
+
? "incomplete"
|
|
159
|
+
: "complete";
|
|
160
|
+
return browserScenarioCompletenessSchema.parse({
|
|
161
|
+
status,
|
|
162
|
+
equality_eligible: status === "complete",
|
|
163
|
+
missing_sections: [...new Set(missing)].sort(),
|
|
164
|
+
truncated_sections: [...new Set(truncated)].sort(),
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
const runScenario = async (factory, scenario, options) => {
|
|
168
|
+
if (options.signal?.aborted === true)
|
|
169
|
+
throw new BrowserObservationError(OPERATION, "cancelled");
|
|
170
|
+
const startedAt = Date.now();
|
|
171
|
+
const budget = new BrowserScenarioCaptureBudget(scenario.limits.max_screenshots, scenario.limits.max_total_metadata_bytes);
|
|
172
|
+
const session = await factory.open(scenario, {
|
|
173
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
174
|
+
budget,
|
|
175
|
+
deadlineAt: startedAt + scenario.limits.max_duration_ms,
|
|
176
|
+
});
|
|
177
|
+
const steps = [];
|
|
178
|
+
let cleanup;
|
|
179
|
+
try {
|
|
180
|
+
steps.push(await initialStep({
|
|
181
|
+
session,
|
|
182
|
+
scenario,
|
|
183
|
+
budget,
|
|
184
|
+
elapsedMs: Date.now() - startedAt,
|
|
185
|
+
maximumTimeoutMs: scenario.limits.max_duration_ms - (Date.now() - startedAt),
|
|
186
|
+
signal: options.signal,
|
|
187
|
+
}));
|
|
188
|
+
let failed = false;
|
|
189
|
+
for (const [offset, action] of scenario.actions.entries()) {
|
|
190
|
+
const step = await executeStep({
|
|
191
|
+
session,
|
|
192
|
+
scenario,
|
|
193
|
+
budget,
|
|
194
|
+
action,
|
|
195
|
+
stepIndex: offset + 1,
|
|
196
|
+
startedAt,
|
|
197
|
+
priorFailure: failed,
|
|
198
|
+
signal: options.signal,
|
|
199
|
+
});
|
|
200
|
+
steps.push(step);
|
|
201
|
+
failed ||= step.status !== "completed";
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
cleanup = await session.close();
|
|
206
|
+
}
|
|
207
|
+
const events = session.events();
|
|
208
|
+
const completeness = globalCompleteness(scenario, session, steps);
|
|
209
|
+
return browserScenarioCaptureSchema.parse({
|
|
210
|
+
schema_version: 1,
|
|
211
|
+
browser: {
|
|
212
|
+
mode: session.mode,
|
|
213
|
+
process_ownership: session.processOwnership,
|
|
214
|
+
cleanup,
|
|
215
|
+
product: session.product,
|
|
216
|
+
version: session.version,
|
|
217
|
+
},
|
|
218
|
+
scenario: {
|
|
219
|
+
start_origin: new URL(scenario.start_url.url).origin,
|
|
220
|
+
allowed_origins: scenario.allowed_origins,
|
|
221
|
+
action_count: scenario.actions.length,
|
|
222
|
+
secret_references: scenario.secrets.map(({ secret_id: id }) => id).sort(),
|
|
223
|
+
},
|
|
224
|
+
duration_ms: Date.now() - startedAt,
|
|
225
|
+
steps,
|
|
226
|
+
events,
|
|
227
|
+
completeness,
|
|
228
|
+
limitations: [
|
|
229
|
+
"Event sequence records provider receipt order; simultaneous browser causality is not inferred.",
|
|
230
|
+
"Response bodies are not retained.",
|
|
231
|
+
"Storage values are hashed only after declared-secret redaction.",
|
|
232
|
+
...(session.mode === "connect"
|
|
233
|
+
? [
|
|
234
|
+
"CDP attachment cannot recover pre-attach events or guarantee launch-time context options.",
|
|
235
|
+
]
|
|
236
|
+
: []),
|
|
237
|
+
],
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
/** Controlled Playwright/CDP scenario driver with exact process ownership. */
|
|
241
|
+
export class PlaywrightBrowserScenarioProvider {
|
|
242
|
+
factory;
|
|
243
|
+
constructor(factory = lazyPlaywrightFactory) {
|
|
244
|
+
this.factory = factory;
|
|
245
|
+
}
|
|
246
|
+
identity() {
|
|
247
|
+
return PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY;
|
|
248
|
+
}
|
|
249
|
+
async captureScenario(scenario, options = {}) {
|
|
250
|
+
try {
|
|
251
|
+
return ok(await runScenario(this.factory, scenario, options));
|
|
252
|
+
}
|
|
253
|
+
catch (cause) {
|
|
254
|
+
if (cause instanceof AnalysisError)
|
|
255
|
+
return err(cause);
|
|
256
|
+
return err(new ProviderAdapterError(PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY.id, OPERATION, { cause }));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { AnalysisCancelledError, AnalysisTimeoutError, } from "../domain/errors.js";
|
|
2
|
+
const OPERATION = "capture_browser_scenario";
|
|
3
|
+
export const withPlaywrightExecutionBoundary = (work, maximumTimeoutMs, signal) => {
|
|
4
|
+
if (maximumTimeoutMs <= 0)
|
|
5
|
+
return Promise.reject(new AnalysisTimeoutError(OPERATION, maximumTimeoutMs));
|
|
6
|
+
if (signal?.aborted === true)
|
|
7
|
+
return Promise.reject(new AnalysisCancelledError(OPERATION));
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
let settled = false;
|
|
10
|
+
let timer;
|
|
11
|
+
const cleanup = () => {
|
|
12
|
+
if (timer !== undefined)
|
|
13
|
+
clearTimeout(timer);
|
|
14
|
+
signal?.removeEventListener("abort", onAbort);
|
|
15
|
+
};
|
|
16
|
+
const succeed = (value) => {
|
|
17
|
+
if (settled)
|
|
18
|
+
return;
|
|
19
|
+
settled = true;
|
|
20
|
+
cleanup();
|
|
21
|
+
resolve(value);
|
|
22
|
+
};
|
|
23
|
+
const fail = (error) => {
|
|
24
|
+
if (settled)
|
|
25
|
+
return;
|
|
26
|
+
settled = true;
|
|
27
|
+
cleanup();
|
|
28
|
+
reject(error);
|
|
29
|
+
};
|
|
30
|
+
const onAbort = () => {
|
|
31
|
+
fail(new AnalysisCancelledError(OPERATION));
|
|
32
|
+
};
|
|
33
|
+
timer = setTimeout(() => fail(new AnalysisTimeoutError(OPERATION, maximumTimeoutMs)), maximumTimeoutMs);
|
|
34
|
+
timer.unref();
|
|
35
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
36
|
+
if (signal?.aborted === true) {
|
|
37
|
+
onAbort();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
void work().then(succeed, fail);
|
|
41
|
+
});
|
|
42
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const resolveLocator = (page, locator) => {
|
|
2
|
+
switch (locator.kind) {
|
|
3
|
+
case "test_id":
|
|
4
|
+
return page.getByTestId(locator.value);
|
|
5
|
+
case "role":
|
|
6
|
+
return page.getByRole(locator.role, {
|
|
7
|
+
name: locator.name,
|
|
8
|
+
exact: locator.exact,
|
|
9
|
+
});
|
|
10
|
+
case "css":
|
|
11
|
+
return page.locator(locator.selector);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
const resolveValue = (value, secrets) => secrets.value(value);
|
|
15
|
+
/** Execute one admitted declarative action with no caller-supplied code. */
|
|
16
|
+
export const performPlaywrightScenarioAction = async (input) => {
|
|
17
|
+
const { page, action, secrets, defaultTimeoutMs, maximumTimeoutMs } = input;
|
|
18
|
+
const timeout = "timeout_ms" in action ? action.timeout_ms : undefined;
|
|
19
|
+
const options = {
|
|
20
|
+
timeout: Math.min(timeout ?? defaultTimeoutMs, maximumTimeoutMs),
|
|
21
|
+
};
|
|
22
|
+
switch (action.action) {
|
|
23
|
+
case "goto":
|
|
24
|
+
await page.goto(secrets.url(action.destination), {
|
|
25
|
+
timeout: options.timeout,
|
|
26
|
+
waitUntil: action.wait_until,
|
|
27
|
+
});
|
|
28
|
+
return;
|
|
29
|
+
case "click":
|
|
30
|
+
await resolveLocator(page, action.locator).click({
|
|
31
|
+
...options,
|
|
32
|
+
button: action.button,
|
|
33
|
+
clickCount: action.click_count,
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
case "fill":
|
|
37
|
+
await resolveLocator(page, action.locator).fill(resolveValue(action.value, secrets), options);
|
|
38
|
+
return;
|
|
39
|
+
case "press":
|
|
40
|
+
await resolveLocator(page, action.locator).press(action.key, options);
|
|
41
|
+
return;
|
|
42
|
+
case "select_option":
|
|
43
|
+
await resolveLocator(page, action.locator).selectOption(resolveValue(action.value, secrets), options);
|
|
44
|
+
return;
|
|
45
|
+
case "check":
|
|
46
|
+
await resolveLocator(page, action.locator).check(options);
|
|
47
|
+
return;
|
|
48
|
+
case "uncheck":
|
|
49
|
+
await resolveLocator(page, action.locator).uncheck(options);
|
|
50
|
+
return;
|
|
51
|
+
case "wait_for":
|
|
52
|
+
await resolveLocator(page, action.locator).waitFor({
|
|
53
|
+
...options,
|
|
54
|
+
state: action.state,
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
case "wait_for_timeout":
|
|
58
|
+
if (action.duration_ms > maximumTimeoutMs)
|
|
59
|
+
throw new Error("Scenario duration limit reached");
|
|
60
|
+
await page.waitForTimeout(action.duration_ms);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { sanitizeBrowserUrl } from "../domain/browserObservation.js";
|
|
4
|
+
import { browserStepArtifactsSchema, } from "../domain/browserScenarioCapture.js";
|
|
5
|
+
const TEXT_ARTIFACT_MAX_BYTES = 1_048_576;
|
|
6
|
+
const historyValueSchema = z.strictObject({
|
|
7
|
+
length: z.number().int().min(0),
|
|
8
|
+
navigation_entries: z
|
|
9
|
+
.array(z.strictObject({
|
|
10
|
+
type: z.string(),
|
|
11
|
+
name: z.string(),
|
|
12
|
+
}))
|
|
13
|
+
.max(10_000),
|
|
14
|
+
});
|
|
15
|
+
const storageValueSchema = z.strictObject({
|
|
16
|
+
local_storage: z.array(z.tuple([z.string(), z.string()])).max(10_000),
|
|
17
|
+
session_storage: z.array(z.tuple([z.string(), z.string()])).max(10_000),
|
|
18
|
+
});
|
|
19
|
+
/** Shared retained-count budget for one scenario's screenshot artifacts. */
|
|
20
|
+
export class BrowserScenarioCaptureBudget {
|
|
21
|
+
maximumScreenshots;
|
|
22
|
+
maximumMetadataBytes;
|
|
23
|
+
screenshotCount = 0;
|
|
24
|
+
metadataBytes = 0;
|
|
25
|
+
constructor(maximumScreenshots, maximumMetadataBytes) {
|
|
26
|
+
this.maximumScreenshots = maximumScreenshots;
|
|
27
|
+
this.maximumMetadataBytes = maximumMetadataBytes;
|
|
28
|
+
}
|
|
29
|
+
claimScreenshot() {
|
|
30
|
+
if (this.screenshotCount >= this.maximumScreenshots)
|
|
31
|
+
return false;
|
|
32
|
+
this.screenshotCount += 1;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
claimMetadata(bytes) {
|
|
36
|
+
if (this.metadataBytes + bytes > this.maximumMetadataBytes)
|
|
37
|
+
return false;
|
|
38
|
+
this.metadataBytes += bytes;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const notRequested = () => ({ state: "not_requested" });
|
|
43
|
+
const missing = (reason) => ({ state: "missing", reason });
|
|
44
|
+
const truncated = (observed, retained, reason) => ({
|
|
45
|
+
state: "truncated",
|
|
46
|
+
observed,
|
|
47
|
+
retained,
|
|
48
|
+
reason,
|
|
49
|
+
});
|
|
50
|
+
const textArtifact = (text, maximumBytes, budget) => {
|
|
51
|
+
const bytes = Buffer.from(text);
|
|
52
|
+
if (bytes.byteLength > maximumBytes)
|
|
53
|
+
return truncated(bytes.byteLength, 0, `artifact exceeds ${maximumBytes} bytes`);
|
|
54
|
+
if (!budget.claimMetadata(bytes.byteLength))
|
|
55
|
+
return truncated(bytes.byteLength, 0, "scenario metadata byte limit reached");
|
|
56
|
+
return {
|
|
57
|
+
state: "captured",
|
|
58
|
+
value: {
|
|
59
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
60
|
+
bytes: bytes.byteLength,
|
|
61
|
+
text,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
const metadataArtifact = (value, budget) => {
|
|
66
|
+
const bytes = Buffer.byteLength(JSON.stringify(value));
|
|
67
|
+
return budget.claimMetadata(bytes)
|
|
68
|
+
? { state: "captured", value }
|
|
69
|
+
: truncated(bytes, 0, "scenario metadata byte limit reached");
|
|
70
|
+
};
|
|
71
|
+
const captureScreenshot = async (page, scenario, budget) => {
|
|
72
|
+
if (!budget.claimScreenshot())
|
|
73
|
+
return truncated(1, 0, "scenario screenshot count limit reached");
|
|
74
|
+
try {
|
|
75
|
+
const bytes = await page.screenshot({
|
|
76
|
+
type: "png",
|
|
77
|
+
fullPage: false,
|
|
78
|
+
animations: "disabled",
|
|
79
|
+
caret: "hide",
|
|
80
|
+
});
|
|
81
|
+
if (bytes.byteLength > scenario.limits.max_screenshot_bytes)
|
|
82
|
+
return truncated(bytes.byteLength, 0, "screenshot byte limit reached");
|
|
83
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
84
|
+
return {
|
|
85
|
+
state: "captured",
|
|
86
|
+
value: {
|
|
87
|
+
uri: `rea://web-screenshot/sha256/${sha256}`,
|
|
88
|
+
sha256,
|
|
89
|
+
bytes: bytes.byteLength,
|
|
90
|
+
media_type: "image/png",
|
|
91
|
+
data_base64: bytes.toString("base64"),
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return missing("screenshot capture failed");
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const captureDom = async (input) => {
|
|
100
|
+
const { page, secrets, maximumBytes, maximumNodes, budget } = input;
|
|
101
|
+
try {
|
|
102
|
+
const nodes = await page.locator("*").count();
|
|
103
|
+
if (nodes > maximumNodes)
|
|
104
|
+
return truncated(nodes, 0, "DOM node limit reached");
|
|
105
|
+
return textArtifact(secrets.redact(await page.content()), Math.min(maximumBytes, TEXT_ARTIFACT_MAX_BYTES), budget);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return missing("DOM capture failed");
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const captureAccessibility = async (input) => {
|
|
112
|
+
const { page, secrets, maximumBytes, maximumNodes, budget } = input;
|
|
113
|
+
try {
|
|
114
|
+
const text = await page.locator("html").ariaSnapshot();
|
|
115
|
+
const nodes = text.split("\n").filter((line) => line.trim() !== "").length;
|
|
116
|
+
if (nodes > maximumNodes)
|
|
117
|
+
return truncated(nodes, 0, "accessibility node limit reached");
|
|
118
|
+
return textArtifact(secrets.redact(text), Math.min(maximumBytes, TEXT_ARTIFACT_MAX_BYTES), budget);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return missing("accessibility capture failed");
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const navigationType = (value) => {
|
|
125
|
+
switch (value) {
|
|
126
|
+
case "navigate":
|
|
127
|
+
case "reload":
|
|
128
|
+
case "back_forward":
|
|
129
|
+
case "prerender":
|
|
130
|
+
return value;
|
|
131
|
+
default:
|
|
132
|
+
return "unknown";
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const captureHistory = async (page, budget) => {
|
|
136
|
+
try {
|
|
137
|
+
const raw = historyValueSchema.parse(await page.evaluate(`(() => ({
|
|
138
|
+
length: window.history.length,
|
|
139
|
+
navigation_entries: performance.getEntriesByType("navigation").map(
|
|
140
|
+
entry => ({
|
|
141
|
+
type: typeof entry.type === "string" ? entry.type : "unknown",
|
|
142
|
+
name: entry.name
|
|
143
|
+
})
|
|
144
|
+
)
|
|
145
|
+
}))()`));
|
|
146
|
+
if (raw.navigation_entries.length > 256)
|
|
147
|
+
return truncated(raw.navigation_entries.length, 0, "navigation history entry limit reached");
|
|
148
|
+
const value = {
|
|
149
|
+
length: raw.length,
|
|
150
|
+
current_url: sanitizeBrowserUrl(page.url()),
|
|
151
|
+
navigation_entries: raw.navigation_entries.map(({ type, name }) => ({
|
|
152
|
+
type: navigationType(type),
|
|
153
|
+
name: sanitizeBrowserUrl(name),
|
|
154
|
+
})),
|
|
155
|
+
};
|
|
156
|
+
return metadataArtifact(value, budget);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return missing("history capture failed");
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const captureStorage = async (input) => {
|
|
163
|
+
const { context, page, scenario, secrets, budget } = input;
|
|
164
|
+
try {
|
|
165
|
+
const cookies = await context.cookies(scenario.allowed_origins);
|
|
166
|
+
const pageStorage = storageValueSchema.parse(await page.evaluate(`(() => ({
|
|
167
|
+
local_storage: Object.entries(window.localStorage),
|
|
168
|
+
session_storage: Object.entries(window.sessionStorage)
|
|
169
|
+
}))()`));
|
|
170
|
+
const observed = cookies.length +
|
|
171
|
+
pageStorage.local_storage.length +
|
|
172
|
+
pageStorage.session_storage.length;
|
|
173
|
+
if (observed > scenario.limits.max_storage_entries)
|
|
174
|
+
return truncated(observed, 0, "storage entry limit reached");
|
|
175
|
+
const value = {
|
|
176
|
+
cookies: cookies.map((cookie) => ({
|
|
177
|
+
name: cookie.name,
|
|
178
|
+
domain: cookie.domain,
|
|
179
|
+
path: cookie.path,
|
|
180
|
+
secure: cookie.secure,
|
|
181
|
+
http_only: cookie.httpOnly,
|
|
182
|
+
same_site: cookie.sameSite,
|
|
183
|
+
...secrets.fingerprint(cookie.value),
|
|
184
|
+
})),
|
|
185
|
+
local_storage: pageStorage.local_storage.map(([name, value]) => ({
|
|
186
|
+
name,
|
|
187
|
+
...secrets.fingerprint(value),
|
|
188
|
+
})),
|
|
189
|
+
session_storage: pageStorage.session_storage.map(([name, value]) => ({
|
|
190
|
+
name,
|
|
191
|
+
...secrets.fingerprint(value),
|
|
192
|
+
})),
|
|
193
|
+
};
|
|
194
|
+
return metadataArtifact(value, budget);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return missing("storage capture failed");
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const isAllowedPage = (page, allowedOrigins) => {
|
|
201
|
+
try {
|
|
202
|
+
return allowedOrigins.has(new URL(page.url()).origin);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
/** Capture selected step artifacts without retaining data after scope loss. */
|
|
209
|
+
export const capturePlaywrightStepArtifacts = async (input) => {
|
|
210
|
+
const { context, page, scenario, secrets, requested, budget } = input;
|
|
211
|
+
const allowed = isAllowedPage(page, new Set(scenario.allowed_origins));
|
|
212
|
+
const denied = () => missing("current page is outside approved origins");
|
|
213
|
+
const state = (kind, capture) => !requested.has(kind)
|
|
214
|
+
? Promise.resolve(notRequested())
|
|
215
|
+
: allowed || kind === "url"
|
|
216
|
+
? capture()
|
|
217
|
+
: Promise.resolve(denied());
|
|
218
|
+
return browserStepArtifactsSchema.parse({
|
|
219
|
+
screenshot: await state("screenshot", () => captureScreenshot(page, scenario, budget)),
|
|
220
|
+
dom: await state("dom", () => captureDom({
|
|
221
|
+
page,
|
|
222
|
+
secrets,
|
|
223
|
+
maximumBytes: scenario.limits.max_total_metadata_bytes,
|
|
224
|
+
maximumNodes: scenario.limits.max_dom_nodes,
|
|
225
|
+
budget,
|
|
226
|
+
})),
|
|
227
|
+
accessibility: await state("accessibility", () => captureAccessibility({
|
|
228
|
+
page,
|
|
229
|
+
secrets,
|
|
230
|
+
maximumBytes: scenario.limits.max_total_metadata_bytes,
|
|
231
|
+
maximumNodes: scenario.limits.max_accessibility_nodes,
|
|
232
|
+
budget,
|
|
233
|
+
})),
|
|
234
|
+
url: await state("url", async () => metadataArtifact(sanitizeBrowserUrl(page.url()), budget)),
|
|
235
|
+
history: await state("history", () => captureHistory(page, budget)),
|
|
236
|
+
storage: await state("storage", () => captureStorage({ context, page, scenario, secrets, budget })),
|
|
237
|
+
});
|
|
238
|
+
};
|