rea-agents 2.2.0 → 2.4.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 +57 -10
- package/dist/application/ApplicationWorkflowEvidenceResolver.js +162 -0
- package/dist/application/ArtifactExtraction.js +5 -1
- package/dist/application/ArtifactGraphConstruction.js +3 -15
- package/dist/application/ArtifactInventory/classify.js +5 -1
- package/dist/application/ArtifactInventory/reader.js +2 -0
- package/dist/application/BinarySession.js +9 -2
- package/dist/application/BinarySessionStatus.js +40 -1
- package/dist/application/CapabilityInventory.js +26 -3
- package/dist/application/CommandShimReplay.js +6 -3
- package/dist/application/CompositeProvider.js +5 -2
- package/dist/application/DirectAnalysis.js +1 -20
- package/dist/application/DirectAnalysisStatus.js +83 -0
- package/dist/application/Doctor.js +34 -25
- package/dist/application/DoctorDiagnostics.js +1 -4
- package/dist/application/DoctorProjection.js +43 -0
- package/dist/application/DoctorScope.js +102 -0
- package/dist/application/EvidenceReferenceResolver.js +26 -0
- package/dist/application/JavaScriptApplicationEvidence.js +1 -1
- package/dist/application/JavaScriptApplicationEvidenceGraph.js +10 -1
- package/dist/application/JavaScriptApplicationService.js +3 -2
- package/dist/application/JavaScriptApplicationWorkflowEvidence.js +26 -0
- package/dist/application/JavaScriptApplicationWorkflowService.js +49 -1
- package/dist/application/JavaScriptArtifactReconstruction.js +7 -0
- package/dist/application/JavaScriptModuleRelationships.js +25 -1
- package/dist/application/JavaScriptReturnShapeProjection.js +111 -0
- package/dist/application/JavaScriptSemanticGraphBuilder.js +294 -0
- package/dist/application/JavaScriptSemanticGraphConstruction.js +144 -0
- package/dist/application/JavaScriptSemanticGraphEvidence.js +86 -0
- package/dist/application/JavaScriptSemanticGraphFlowProjection.js +72 -0
- package/dist/application/JavaScriptSemanticGraphProjection.js +33 -0
- package/dist/application/JavaScriptSemanticTraceService.js +45 -0
- package/dist/application/LoopbackReplay.js +277 -133
- package/dist/application/LoopbackReplayRecorder.js +101 -0
- package/dist/application/PermissionAuthority.js +7 -3
- package/dist/application/ProcessCaptureEnvironment.js +26 -0
- package/dist/application/ProcessCaptureJournal.js +95 -0
- package/dist/application/ProcessCaptureLifecycle.js +25 -13
- package/dist/application/ProcessCheckpoints.js +35 -4
- package/dist/application/ProcessCli.js +26 -4
- package/dist/application/ProcessHarness.js +64 -83
- package/dist/application/ProcessNormalization.js +9 -2
- package/dist/application/ProcessReactiveCoordinator.js +225 -0
- package/dist/application/ProcessReactiveEffects.js +141 -0
- package/dist/application/ProcessReactiveHarness.js +85 -0
- package/dist/application/ProcessReactiveObservations.js +105 -0
- package/dist/application/ProcessSampling.js +2 -0
- package/dist/application/SessionProviderRouter.js +7 -6
- package/dist/application/Setup.js +11 -10
- package/dist/application/SetupPlan.js +1 -1
- package/dist/application/SetupRegistrationEnvironment.js +12 -0
- package/dist/application/TerminalRenderer.js +3 -1
- package/dist/browser/CdpEndpoint.js +37 -4
- package/dist/catalogIdentity.js +16 -7
- package/dist/cli/coreAnalysisCommands.js +34 -2
- package/dist/cli/javascriptApplicationAnalysis.js +19 -0
- package/dist/cli/setupCommands.js +81 -32
- package/dist/cli/utilityCommands.js +10 -2
- package/dist/cliApplicationCommands.js +79 -16
- package/dist/cliCommandNames.js +3 -0
- package/dist/cliElectronCommands.js +23 -33
- package/dist/cliObservationOptions.js +1 -1
- package/dist/cliOutput.js +56 -0
- package/dist/cliProcessCommands.js +32 -1
- package/dist/cliSetup.js +30 -21
- package/dist/contracts/applicationToolContracts.js +54 -7
- package/dist/contracts/applicationWorkflowInputContracts.js +119 -0
- package/dist/contracts/electronToolContracts.js +53 -32
- package/dist/contracts/javascriptApplicationWorkflowExamples.js +32 -9
- package/dist/contracts/processCaptureExample.js +1 -0
- package/dist/contracts/replayMachineExample.js +37 -0
- package/dist/contracts/toolContractExamples.js +2 -0
- package/dist/contracts/toolContracts.js +5 -1
- package/dist/contracts/toolEffects.js +3 -0
- package/dist/contracts/toolOutputSchemaGroups.js +2 -0
- package/dist/contracts/toolOutputSchemaPrimitives.js +43 -0
- package/dist/doctorRuntime.js +41 -0
- package/dist/domain/analysisErrorPresentation.js +5 -3
- package/dist/domain/analysisErrorProjection.js +7 -1
- package/dist/domain/analysisSnapshot.js +2 -0
- package/dist/domain/artifactGraph.js +2 -0
- package/dist/domain/binaryTarget.js +3 -6
- package/dist/domain/changedBehavior.js +5 -4
- package/dist/domain/completionLedgerGeneration.js +190 -0
- package/dist/domain/errors.js +4 -2
- package/dist/domain/evidence.js +2 -0
- package/dist/domain/evidenceCompletionLedger.js +113 -0
- package/dist/domain/hopperStartupFailure.js +9 -0
- package/dist/domain/javascriptApplicationAnalysis.js +44 -2
- package/dist/domain/javascriptExportShapeComparison.js +145 -0
- package/dist/domain/javascriptExportShapeComparisonIdentity.js +11 -0
- package/dist/domain/javascriptExportShapeComparisonSchemas.js +277 -0
- package/dist/domain/javascriptExportShapeSelection.js +132 -0
- package/dist/domain/javascriptExportShapeVariants.js +272 -0
- package/dist/domain/javascriptRuntimeReconciliationParsing.js +2 -2
- package/dist/domain/javascriptSemanticAnalysis.js +17 -36
- package/dist/domain/javascriptSemanticCallResolution.js +129 -0
- package/dist/domain/javascriptSemanticCalls.js +269 -0
- package/dist/domain/javascriptSemanticGraph.js +356 -0
- package/dist/domain/javascriptSemanticGraphSchemas.js +329 -0
- package/dist/domain/javascriptSemanticGraphSerialization.js +23 -0
- package/dist/domain/javascriptSemanticIr.js +14 -1
- package/dist/domain/javascriptSemanticPrimitives.js +36 -0
- package/dist/domain/javascriptSemanticProjection.js +36 -4
- package/dist/domain/javascriptSemanticProvenance.js +29 -0
- package/dist/domain/javascriptSemanticQuery.js +294 -0
- package/dist/domain/javascriptSemanticQueryAssessment.js +74 -0
- package/dist/domain/javascriptSemanticQueryIdentity.js +86 -0
- package/dist/domain/javascriptSemanticQueryRelations.js +15 -0
- package/dist/domain/javascriptSemanticQuerySchemas.js +188 -0
- package/dist/domain/javascriptSemanticReturns.js +187 -0
- package/dist/domain/javascriptSemanticState.js +32 -0
- package/dist/domain/javascriptSemanticTraceSchemas.js +14 -0
- package/dist/domain/javascriptSemanticValues.js +54 -70
- package/dist/domain/processCapture.js +100 -0
- package/dist/domain/processCaptureReactiveValidation.js +175 -0
- package/dist/domain/processCaptureValidation.js +103 -12
- package/dist/domain/processComparison.js +78 -25
- package/dist/domain/processObservation.js +162 -0
- package/dist/domain/processReactiveCheckpointDataflow.js +32 -0
- package/dist/domain/processReactiveMatching.js +210 -0
- package/dist/domain/processReactiveRuntime.js +120 -0
- package/dist/domain/processReactiveScenario.js +361 -0
- package/dist/domain/processReactiveScenarioPreflight.js +120 -0
- package/dist/domain/processReactiveTransition.js +161 -0
- package/dist/domain/processScenario.js +33 -0
- package/dist/domain/processTraceComparison.js +43 -0
- package/dist/domain/processTraceDimensionProjection.js +50 -0
- package/dist/domain/processTraceEvaluation.js +312 -0
- package/dist/domain/processTraceSpecification.js +373 -0
- package/dist/domain/reconstructionVerification.js +5 -4
- package/dist/domain/replayMachine.js +18 -2
- package/dist/domain/replayMachineRun.js +216 -0
- package/dist/domain/replayMachineRuntime.js +88 -9
- package/dist/domain/staticRuntimeCorrelation.js +4 -2
- package/dist/domain/zipPackageFormat.js +20 -0
- package/dist/evaluation/CodexAgentEval.js +24 -0
- package/dist/generatedPackageMetadata.js +1 -1
- package/dist/ghidra/GhidraClient.js +13 -2
- package/dist/ghidra/GhidraDiagnostics.js +1 -0
- package/dist/ghidra/GhidraLauncher.js +7 -3
- package/dist/ghidra/GhidraProvider.js +8 -1
- package/dist/hopper/BridgeLauncher.js +56 -14
- package/dist/hopper/HopperClient.js +23 -10
- package/dist/hopper/HopperDiagnostics.js +14 -0
- package/dist/hopper/HopperProcessDiagnostic.js +14 -0
- package/dist/hopper/HopperProvider.js +8 -1
- package/dist/hopper/LinuxPrivateDisplayDiagnostic.js +72 -0
- package/dist/hopper/LinuxPrivateDisplayProbe.js +217 -0
- package/dist/main/transport.js +2 -0
- package/dist/mcpDoctor.js +310 -0
- package/dist/native/NativeMacOSProvider.js +1 -0
- package/dist/process/ProcessOwnership.js +200 -53
- package/dist/process/ProviderRunLineage.js +21 -0
- package/dist/server/createServer.js +4 -1
- package/dist/server/javascriptApplicationResult.js +22 -1
- package/dist/server/registerApplicationTools/characterization.js +3 -3
- package/dist/server/registerApplicationTools/compareExportShapes.js +37 -0
- package/dist/server/registerApplicationTools/compareVersions.js +12 -6
- package/dist/server/registerApplicationTools/controlledReplay.js +2 -2
- package/dist/server/registerApplicationTools/coverage.js +3 -3
- package/dist/server/registerApplicationTools/helpers.js +37 -25
- package/dist/server/registerApplicationTools/traceFeature.js +9 -5
- package/dist/server/registerApplicationTools/traceSemantics.js +30 -0
- package/dist/server/registerApplicationTools.js +4 -0
- package/dist/server/registerEvidenceResources.js +5 -1
- package/dist/server/registerProcessComparisonTool.js +17 -7
- package/dist/server/registerReplayMachineTool.js +20 -0
- package/dist/server/registerSessionTools.js +12 -7
- package/dist/server/sessionAvailabilityPolicy.js +2 -1
- package/dist/serverIdentity.js +19 -0
- package/package.json +62 -30
- package/scripts/hopper-demo-x11.py +459 -61
- package/scripts/prepack.mjs +55 -0
- package/scripts/prepare.mjs +28 -0
- package/scripts/rea.mjs +26 -5
- package/skills/reverse-engineer-anything/SKILL.md +4 -3
- package/skills/reverse-engineer-anything/references/javascript-applications.md +10 -0
- package/skills/reverse-engineer-anything/references/native-and-artifacts.md +2 -2
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createJavaScriptSemanticGraphUnknown } from "../domain/javascriptSemanticGraph.js";
|
|
2
|
+
import { addSemanticGraphRelation, addSemanticGraphUnknown, } from "./JavaScriptSemanticGraphConstruction.js";
|
|
3
|
+
import { inferredSemanticEvidenceAt, unknownSemanticEvidence, } from "./JavaScriptSemanticGraphEvidence.js";
|
|
4
|
+
import { semanticNodesWithinRange } from "./JavaScriptSemanticGraphProjection.js";
|
|
5
|
+
/** Link direct return expressions to their retained semantic references. */
|
|
6
|
+
export const projectSemanticReturnValues = (context) => {
|
|
7
|
+
for (const callable of context.ir.callables)
|
|
8
|
+
for (const site of callable.returnSites) {
|
|
9
|
+
const returnNode = context.returnSiteNodes.get(site.returnSiteId);
|
|
10
|
+
const references = semanticNodesWithinRange(context.referenceNodes, site.location);
|
|
11
|
+
for (const reference of references)
|
|
12
|
+
addSemanticGraphRelation(context.state, {
|
|
13
|
+
source: reference,
|
|
14
|
+
target: returnNode,
|
|
15
|
+
relation: "aliases",
|
|
16
|
+
resolution: site.identityReferenceLocation !== null &&
|
|
17
|
+
rangesEqual(reference.identity.source_range, site.identityReferenceLocation)
|
|
18
|
+
? "resolved"
|
|
19
|
+
: "candidate",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const rangesEqual = (left, right) => left !== null &&
|
|
24
|
+
right !== null &&
|
|
25
|
+
left.start.line === right.start.line &&
|
|
26
|
+
left.start.column === right.start.column &&
|
|
27
|
+
left.end.line === right.end.line &&
|
|
28
|
+
left.end.column === right.end.column;
|
|
29
|
+
/** Link each captured binding to the callable at the exact reference site. */
|
|
30
|
+
export const projectSemanticClosureCaptures = (context) => {
|
|
31
|
+
for (const capture of context.ir.closureCaptures)
|
|
32
|
+
addSemanticGraphRelation(context.state, {
|
|
33
|
+
source: context.bindingNodes.get(capture.bindingId),
|
|
34
|
+
target: context.callableNodes.get(capture.callableId),
|
|
35
|
+
relation: "captures",
|
|
36
|
+
resolution: "resolved",
|
|
37
|
+
evidence: inferredSemanticEvidenceAt(context.file, capture.referenceLocation),
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
/** Retain bounded unresolved dynamic-call and dynamic-property frontiers. */
|
|
41
|
+
export const projectSemanticFrontiers = (context) => {
|
|
42
|
+
for (const frontier of context.ir.frontiers) {
|
|
43
|
+
const unknown = createJavaScriptSemanticGraphUnknown({
|
|
44
|
+
node_id: frontier.callableId === null
|
|
45
|
+
? context.moduleNode.node_id
|
|
46
|
+
: (context.callableNodes.get(frontier.callableId)?.node_id ??
|
|
47
|
+
context.moduleNode.node_id),
|
|
48
|
+
family: frontier.kind === "dynamic-call" ? "call-flow" : "object-flow",
|
|
49
|
+
relation_kinds: frontier.kind === "dynamic-call"
|
|
50
|
+
? ["calls"]
|
|
51
|
+
: ["reads-property", "writes-property"],
|
|
52
|
+
reason: frontier.kind,
|
|
53
|
+
detail: frontier.reason,
|
|
54
|
+
candidate_node_ids: [],
|
|
55
|
+
evidence: unknownSemanticEvidence(context.file, frontier.location),
|
|
56
|
+
});
|
|
57
|
+
addSemanticGraphUnknown(context.state, unknown);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
/** Publish exact semantic analyzer limits reached by any admitted source file. */
|
|
61
|
+
export const semanticRecoveryLimits = (analysis) => {
|
|
62
|
+
const limits = new Map();
|
|
63
|
+
for (const { semantic } of analysis.files) {
|
|
64
|
+
if (semantic === null)
|
|
65
|
+
continue;
|
|
66
|
+
for (const name of semantic.ir.coverage.limitsReached)
|
|
67
|
+
limits.set(`semantic.${name}`, semantic.limits[name]);
|
|
68
|
+
}
|
|
69
|
+
return [...limits]
|
|
70
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
71
|
+
.map(([name, value]) => ({ name, value, unit: "items" }));
|
|
72
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { compareCodePoints } from "../domain/javascriptApplicationGraph.js";
|
|
2
|
+
import { JAVASCRIPT_SEMANTIC_RELATION_FAMILIES } from "../domain/javascriptSemanticGraphSchemas.js";
|
|
3
|
+
/** Find the innermost callable that contains one exact source range. */
|
|
4
|
+
export const owningSemanticCallableNode = (location, ir, nodes) => {
|
|
5
|
+
if (location === null)
|
|
6
|
+
return undefined;
|
|
7
|
+
return ir.callables
|
|
8
|
+
.filter((callable) => contains(callable.location, location))
|
|
9
|
+
.sort((left, right) => compareCodePoints(positionKey(right.location), positionKey(left.location)))
|
|
10
|
+
.map(({ callableId }) => nodes.get(callableId))
|
|
11
|
+
.find((node) => node !== undefined);
|
|
12
|
+
};
|
|
13
|
+
/** Find retained semantic nodes whose exact ranges are within one expression. */
|
|
14
|
+
export const semanticNodesWithinRange = (nodes, range) => nodes.filter((node) => node.identity.source_range !== null &&
|
|
15
|
+
contains(range, node.identity.source_range));
|
|
16
|
+
/** Report extractor support without treating missing families as absence. */
|
|
17
|
+
export const semanticFamilyStatus = (family, analysis, truncated) => {
|
|
18
|
+
if (!["call-flow", "closure", "data-flow", "object-flow"].includes(family))
|
|
19
|
+
return "unsupported";
|
|
20
|
+
if (truncated)
|
|
21
|
+
return "unknown";
|
|
22
|
+
return analysis.truncated_scopes === 0 ? "partial" : "unknown";
|
|
23
|
+
};
|
|
24
|
+
/** Compare two exact semantic source ranges. */
|
|
25
|
+
export const semanticRangesEqual = (left, right) => left !== null &&
|
|
26
|
+
left.start.line === right.start.line &&
|
|
27
|
+
left.start.column === right.start.column &&
|
|
28
|
+
left.end.line === right.end.line &&
|
|
29
|
+
left.end.column === right.end.column;
|
|
30
|
+
const contains = (outer, inner) => pointCompare(outer.start, inner.start) <= 0 &&
|
|
31
|
+
pointCompare(outer.end, inner.end) >= 0;
|
|
32
|
+
const pointCompare = (left, right) => left.line - right.line || left.column - right.column;
|
|
33
|
+
const positionKey = (range) => `${String(range.start.line).padStart(12, "0")}:${String(range.start.column).padStart(12, "0")}`;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AnalysisInputError, AnalysisProtocolError, } from "../domain/errors.js";
|
|
3
|
+
import { jsonValueSchema } from "../domain/jsonValue.js";
|
|
4
|
+
import { queryJavaScriptSemanticGraph } from "../domain/javascriptSemanticQuery.js";
|
|
5
|
+
import { JavaScriptSemanticQueryCursorError } from "../domain/javascriptSemanticQueryIdentity.js";
|
|
6
|
+
import { javaScriptSemanticTraceResultSchema, traceJavaScriptSemanticsInputSchema, } from "../domain/javascriptSemanticTraceSchemas.js";
|
|
7
|
+
import { err, ok } from "../domain/result.js";
|
|
8
|
+
import { parseApplicationGraphEvidence } from "./JavaScriptApplicationEvidenceGraph.js";
|
|
9
|
+
import { createJavaScriptSemanticTraceEvidence } from "./JavaScriptApplicationWorkflowEvidence.js";
|
|
10
|
+
const OPERATION = "trace_javascript_semantics";
|
|
11
|
+
/** Trace semantic relations from input already parsed by a trusted adapter. */
|
|
12
|
+
export const traceJavaScriptSemanticsEvidenceValidated = (input) => {
|
|
13
|
+
try {
|
|
14
|
+
const source = parseApplicationGraphEvidence(input.application);
|
|
15
|
+
if (source.semanticGraph === null)
|
|
16
|
+
return err(new AnalysisInputError(OPERATION, undefined, [
|
|
17
|
+
{
|
|
18
|
+
path: ["application"],
|
|
19
|
+
reason: "invalid_value",
|
|
20
|
+
expected: "analyze_javascript_application v2 Evidence; reanalyze the artifact with the current REA version",
|
|
21
|
+
},
|
|
22
|
+
]));
|
|
23
|
+
const query = queryJavaScriptSemanticGraph(source.semanticGraph, input.query);
|
|
24
|
+
const result = javaScriptSemanticTraceResultSchema.parse({
|
|
25
|
+
...query,
|
|
26
|
+
source_evidence_id: source.evidence.evidence_id,
|
|
27
|
+
evidence_links: [source.evidence.evidence_id],
|
|
28
|
+
});
|
|
29
|
+
return ok(createJavaScriptSemanticTraceEvidence({
|
|
30
|
+
application_evidence_id: source.evidence.evidence_id,
|
|
31
|
+
query: jsonValueSchema.parse(input.query),
|
|
32
|
+
}, result));
|
|
33
|
+
}
|
|
34
|
+
catch (cause) {
|
|
35
|
+
if (cause instanceof JavaScriptSemanticQueryCursorError)
|
|
36
|
+
return err(new AnalysisInputError(OPERATION, undefined, [
|
|
37
|
+
{
|
|
38
|
+
path: ["query", "cursor"],
|
|
39
|
+
reason: "invalid_value",
|
|
40
|
+
expected: "a cursor returned by the same graph, query, and limits",
|
|
41
|
+
},
|
|
42
|
+
]));
|
|
43
|
+
return err(new AnalysisProtocolError("JavaScript semantic trace failed", { cause }));
|
|
44
|
+
}
|
|
45
|
+
};
|
|
@@ -1,113 +1,144 @@
|
|
|
1
1
|
import { createServer, } from "node:http";
|
|
2
2
|
import { WebSocketServer } from "ws";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
import {} from "../domain/replayMachineRuntime.js";
|
|
4
|
+
import { closeReplayServer, listenOnLoopback, recordMachineEvent, readReplayRequestBody, ReplayRecorder, requestHeaders, waitForReplayDelay, } from "./LoopbackReplayRecorder.js";
|
|
5
|
+
const recordHttpResponse = (recorder, request, data, outcome) => recorder.record({
|
|
6
|
+
at_ms: recorder.atMs(),
|
|
7
|
+
protocol: "http",
|
|
8
|
+
direction: "response",
|
|
9
|
+
method: request.method,
|
|
10
|
+
path: request.path,
|
|
11
|
+
data,
|
|
12
|
+
outcome,
|
|
13
|
+
});
|
|
14
|
+
const handleMachineHttp = async (recorder, request) => {
|
|
15
|
+
const machine = recorder.machine;
|
|
16
|
+
if (machine === undefined)
|
|
17
|
+
return;
|
|
18
|
+
const decision = recorder.dispatchMachine({
|
|
19
|
+
protocol: "http",
|
|
20
|
+
connection: "not_applicable",
|
|
21
|
+
at_ms: request.rawAtMs,
|
|
22
|
+
recorded_at_ms: request.recordedAtMs,
|
|
23
|
+
method: request.method,
|
|
24
|
+
path: request.path,
|
|
25
|
+
headers: requestHeaders(request.request),
|
|
26
|
+
body: request.body,
|
|
27
|
+
});
|
|
28
|
+
recordMachineEvent(recorder, {
|
|
29
|
+
at_ms: request.recordedAtMs,
|
|
30
|
+
protocol: "http",
|
|
31
|
+
direction: "request",
|
|
32
|
+
method: request.method,
|
|
33
|
+
path: request.path,
|
|
34
|
+
data: request.body,
|
|
35
|
+
}, decision);
|
|
36
|
+
if (decision.outcome !== "matched") {
|
|
37
|
+
request.response.statusCode = decision.outcome === "unmatched" ? 404 : 409;
|
|
38
|
+
request.response.end();
|
|
39
|
+
return;
|
|
10
40
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
41
|
+
for (const action of decision.actions) {
|
|
42
|
+
if (action.type === "delay")
|
|
43
|
+
await waitForReplayDelay(action.duration_ms);
|
|
44
|
+
else if (action.type === "disconnect") {
|
|
45
|
+
request.response.destroy();
|
|
46
|
+
recordHttpResponse(recorder, request, "", "disconnected");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
else if (action.type === "http_response") {
|
|
50
|
+
request.response.statusCode = action.status;
|
|
51
|
+
for (const [name, value] of Object.entries(action.headers))
|
|
52
|
+
request.response.setHeader(name, value);
|
|
53
|
+
request.response.end(action.body);
|
|
54
|
+
recordHttpResponse(recorder, request, machine.redact(action.body), "matched");
|
|
55
|
+
}
|
|
14
56
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
57
|
+
};
|
|
58
|
+
const handleStaticHttp = async (recorder, request, calls) => {
|
|
59
|
+
const index = recorder.scenario.replay.http.findIndex((route) => route.method !== request.method || route.path !== request.path
|
|
60
|
+
? false
|
|
61
|
+
: (route.request_body === undefined ||
|
|
62
|
+
route.request_body === request.body) &&
|
|
63
|
+
Object.entries(route.request_headers).every(([name, value]) => request.request.headers[name.toLowerCase()] === value));
|
|
64
|
+
const route = recorder.scenario.replay.http[index];
|
|
65
|
+
const used = index < 0 ? 0 : (calls.get(index) ?? 0);
|
|
66
|
+
const outcome = route === undefined
|
|
67
|
+
? "unmatched"
|
|
68
|
+
: used >= route.max_calls
|
|
69
|
+
? "script_exhausted"
|
|
70
|
+
: "matched";
|
|
71
|
+
recorder.record({
|
|
72
|
+
at_ms: recorder.atMs(),
|
|
73
|
+
protocol: "http",
|
|
74
|
+
direction: "request",
|
|
75
|
+
method: request.method,
|
|
76
|
+
path: request.path,
|
|
77
|
+
data: request.body.length === 0 ? "" : "<redacted-request-body>",
|
|
78
|
+
outcome,
|
|
79
|
+
});
|
|
80
|
+
if (route === undefined || outcome === "script_exhausted") {
|
|
81
|
+
request.response.statusCode = route === undefined ? 404 : 409;
|
|
82
|
+
request.response.end();
|
|
83
|
+
return;
|
|
20
84
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (bytes > limit)
|
|
29
|
-
return undefined;
|
|
30
|
-
chunks.push(value);
|
|
85
|
+
calls.set(index, used + 1);
|
|
86
|
+
if (route.delay_ms > 0)
|
|
87
|
+
await waitForReplayDelay(route.delay_ms);
|
|
88
|
+
if (route.disconnect) {
|
|
89
|
+
request.response.destroy();
|
|
90
|
+
recordHttpResponse(recorder, request, "", "disconnected");
|
|
91
|
+
return;
|
|
31
92
|
}
|
|
32
|
-
|
|
93
|
+
request.response.statusCode = route.status;
|
|
94
|
+
for (const [name, value] of Object.entries(route.response_headers))
|
|
95
|
+
request.response.setHeader(name, value);
|
|
96
|
+
request.response.end(route.body);
|
|
97
|
+
recordHttpResponse(recorder, request, route.body, "matched");
|
|
33
98
|
};
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
response.statusCode = 413;
|
|
43
|
-
response.end();
|
|
44
|
-
recorder.record({
|
|
45
|
-
at_ms: recorder.atMs(),
|
|
46
|
-
protocol: "http",
|
|
47
|
-
direction: "request",
|
|
48
|
-
method,
|
|
49
|
-
path,
|
|
50
|
-
data: "<body-over-limit>",
|
|
51
|
-
outcome: "unmatched",
|
|
52
|
-
});
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
const index = recorder.scenario.replay.http.findIndex((route) => {
|
|
56
|
-
if (route.method !== method || route.path !== path)
|
|
57
|
-
return false;
|
|
58
|
-
if (route.request_body !== undefined && route.request_body !== body)
|
|
59
|
-
return false;
|
|
60
|
-
return Object.entries(route.request_headers).every(([name, value]) => request.headers[name.toLowerCase()] === value);
|
|
61
|
-
});
|
|
62
|
-
const route = recorder.scenario.replay.http[index];
|
|
63
|
-
const used = index < 0 ? 0 : (calls.get(index) ?? 0);
|
|
64
|
-
const outcome = route === undefined
|
|
65
|
-
? "unmatched"
|
|
66
|
-
: used >= route.max_calls
|
|
67
|
-
? "script_exhausted"
|
|
68
|
-
: "matched";
|
|
99
|
+
const handleHttpRequest = async (recorder, request, response, calls) => {
|
|
100
|
+
const body = await readReplayRequestBody(request, recorder.scenario.limits.protocol_body_bytes);
|
|
101
|
+
const method = request.method ?? "GET";
|
|
102
|
+
const path = new URL(request.url ?? "/", "http://127.0.0.1").pathname;
|
|
103
|
+
if (body === undefined) {
|
|
104
|
+
recorder.truncated = true;
|
|
105
|
+
response.statusCode = 413;
|
|
106
|
+
response.end();
|
|
69
107
|
recorder.record({
|
|
70
108
|
at_ms: recorder.atMs(),
|
|
71
109
|
protocol: "http",
|
|
72
110
|
direction: "request",
|
|
73
111
|
method,
|
|
74
112
|
path,
|
|
75
|
-
data:
|
|
76
|
-
outcome,
|
|
113
|
+
data: "<body-over-limit>",
|
|
114
|
+
outcome: "unmatched",
|
|
77
115
|
});
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
at_ms: recorder.atMs(),
|
|
105
|
-
protocol: "http",
|
|
106
|
-
direction: "response",
|
|
107
|
-
method,
|
|
108
|
-
path,
|
|
109
|
-
data: route.body,
|
|
110
|
-
outcome: "matched",
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const replayRequest = {
|
|
119
|
+
request,
|
|
120
|
+
response,
|
|
121
|
+
method,
|
|
122
|
+
path,
|
|
123
|
+
body,
|
|
124
|
+
rawAtMs: recorder.rawAtMs(),
|
|
125
|
+
recordedAtMs: recorder.atMs(),
|
|
126
|
+
};
|
|
127
|
+
await (recorder.machine === undefined
|
|
128
|
+
? handleStaticHttp(recorder, replayRequest, calls)
|
|
129
|
+
: recorder.enqueueMachine(() => handleMachineHttp(recorder, replayRequest)));
|
|
130
|
+
};
|
|
131
|
+
const createHttpHandler = (recorder) => {
|
|
132
|
+
const calls = new Map();
|
|
133
|
+
return (request, response) => {
|
|
134
|
+
void handleHttpRequest(recorder, request, response, calls).catch(() => {
|
|
135
|
+
recorder.truncated = true;
|
|
136
|
+
if (response.headersSent)
|
|
137
|
+
response.destroy();
|
|
138
|
+
else {
|
|
139
|
+
response.statusCode = 500;
|
|
140
|
+
response.end();
|
|
141
|
+
}
|
|
111
142
|
});
|
|
112
143
|
};
|
|
113
144
|
};
|
|
@@ -161,6 +192,119 @@ const sendWebSocketScript = async (client, connection, recorder) => {
|
|
|
161
192
|
});
|
|
162
193
|
}
|
|
163
194
|
};
|
|
195
|
+
const runWebSocketActions = async (client, recorder, decision, path) => {
|
|
196
|
+
for (const action of decision.actions) {
|
|
197
|
+
if (action.type === "delay") {
|
|
198
|
+
await waitForReplayDelay(action.duration_ms);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (action.type === "disconnect") {
|
|
202
|
+
client.close();
|
|
203
|
+
recorder.record({
|
|
204
|
+
at_ms: recorder.atMs(),
|
|
205
|
+
protocol: "websocket",
|
|
206
|
+
direction: "sent",
|
|
207
|
+
method: null,
|
|
208
|
+
path,
|
|
209
|
+
data: "",
|
|
210
|
+
outcome: "disconnected",
|
|
211
|
+
});
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (action.type === "websocket_send" && client.readyState === 1) {
|
|
215
|
+
client.send(action.data);
|
|
216
|
+
recorder.record({
|
|
217
|
+
at_ms: recorder.atMs(),
|
|
218
|
+
protocol: "websocket",
|
|
219
|
+
direction: "sent",
|
|
220
|
+
method: null,
|
|
221
|
+
path,
|
|
222
|
+
data: recorder.machine?.redact(action.data) ?? action.data,
|
|
223
|
+
outcome: "matched",
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
const handleStaticWebSocket = (client, recorder, connection, path) => {
|
|
229
|
+
client.on("message", (value) => recorder.record({
|
|
230
|
+
at_ms: recorder.atMs(),
|
|
231
|
+
protocol: "websocket",
|
|
232
|
+
direction: "received",
|
|
233
|
+
method: null,
|
|
234
|
+
path,
|
|
235
|
+
data: value.toString(),
|
|
236
|
+
outcome: "matched",
|
|
237
|
+
}));
|
|
238
|
+
void sendWebSocketScript(client, connection, recorder);
|
|
239
|
+
};
|
|
240
|
+
const handleMachineWebSocket = (options) => {
|
|
241
|
+
const { client, request, recorder, connection, path } = options;
|
|
242
|
+
const machine = recorder.machine;
|
|
243
|
+
if (machine === undefined)
|
|
244
|
+
return;
|
|
245
|
+
void recorder
|
|
246
|
+
.enqueueMachine(async () => {
|
|
247
|
+
const connectedAt = recorder.atMs();
|
|
248
|
+
const connectionDecision = recorder.dispatchMachine({
|
|
249
|
+
protocol: "websocket_connect",
|
|
250
|
+
connection: connection === 1 ? "initial" : "reconnect",
|
|
251
|
+
at_ms: recorder.rawAtMs(),
|
|
252
|
+
recorded_at_ms: connectedAt,
|
|
253
|
+
method: null,
|
|
254
|
+
path,
|
|
255
|
+
headers: requestHeaders(request),
|
|
256
|
+
body: "",
|
|
257
|
+
});
|
|
258
|
+
recordMachineEvent(recorder, {
|
|
259
|
+
at_ms: connectedAt,
|
|
260
|
+
protocol: "websocket",
|
|
261
|
+
direction: "received",
|
|
262
|
+
method: null,
|
|
263
|
+
path,
|
|
264
|
+
data: "",
|
|
265
|
+
}, connectionDecision);
|
|
266
|
+
if (connectionDecision.outcome === "matched")
|
|
267
|
+
await runWebSocketActions(client, recorder, connectionDecision, path);
|
|
268
|
+
else
|
|
269
|
+
client.close();
|
|
270
|
+
})
|
|
271
|
+
.catch(() => {
|
|
272
|
+
recorder.truncated = true;
|
|
273
|
+
client.terminate();
|
|
274
|
+
});
|
|
275
|
+
client.on("message", (value) => {
|
|
276
|
+
const body = value.toString();
|
|
277
|
+
const rawAtMs = recorder.rawAtMs();
|
|
278
|
+
const recordedAtMs = recorder.atMs();
|
|
279
|
+
void recorder
|
|
280
|
+
.enqueueMachine(async () => {
|
|
281
|
+
const decision = recorder.dispatchMachine({
|
|
282
|
+
protocol: "websocket_message",
|
|
283
|
+
connection: connection === 1 ? "initial" : "reconnect",
|
|
284
|
+
at_ms: rawAtMs,
|
|
285
|
+
recorded_at_ms: recordedAtMs,
|
|
286
|
+
method: null,
|
|
287
|
+
path,
|
|
288
|
+
headers: {},
|
|
289
|
+
body,
|
|
290
|
+
});
|
|
291
|
+
recordMachineEvent(recorder, {
|
|
292
|
+
at_ms: recordedAtMs,
|
|
293
|
+
protocol: "websocket",
|
|
294
|
+
direction: "received",
|
|
295
|
+
method: null,
|
|
296
|
+
path,
|
|
297
|
+
data: body,
|
|
298
|
+
}, decision);
|
|
299
|
+
if (decision.outcome === "matched")
|
|
300
|
+
await runWebSocketActions(client, recorder, decision, path);
|
|
301
|
+
})
|
|
302
|
+
.catch(() => {
|
|
303
|
+
recorder.truncated = true;
|
|
304
|
+
client.terminate();
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
};
|
|
164
308
|
const createWebSocketReplay = (server, recorder) => {
|
|
165
309
|
let connections = 0;
|
|
166
310
|
const websocket = new WebSocketServer({
|
|
@@ -169,13 +313,14 @@ const createWebSocketReplay = (server, recorder) => {
|
|
|
169
313
|
clientTracking: true,
|
|
170
314
|
});
|
|
171
315
|
server.on("upgrade", (request, socket, head) => {
|
|
172
|
-
if (
|
|
316
|
+
if (recorder.machine === undefined &&
|
|
317
|
+
new URL(request.url ?? "/", "http://127.0.0.1").pathname !== "/ws") {
|
|
173
318
|
socket.destroy();
|
|
174
319
|
return;
|
|
175
320
|
}
|
|
176
321
|
websocket.handleUpgrade(request, socket, head, (client) => websocket.emit("connection", client, request));
|
|
177
322
|
});
|
|
178
|
-
websocket.on("connection", (client) => {
|
|
323
|
+
websocket.on("connection", (client, request) => {
|
|
179
324
|
connections += 1;
|
|
180
325
|
if (connections > recorder.scenario.limits.connections) {
|
|
181
326
|
recorder.truncated = true;
|
|
@@ -183,60 +328,59 @@ const createWebSocketReplay = (server, recorder) => {
|
|
|
183
328
|
return;
|
|
184
329
|
}
|
|
185
330
|
client.on("error", () => undefined);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
331
|
+
const path = new URL(request.url ?? "/", "http://127.0.0.1").pathname;
|
|
332
|
+
if (recorder.machine === undefined)
|
|
333
|
+
handleStaticWebSocket(client, recorder, connections, path);
|
|
334
|
+
else
|
|
335
|
+
handleMachineWebSocket({
|
|
336
|
+
client,
|
|
337
|
+
request,
|
|
338
|
+
recorder,
|
|
339
|
+
connection: connections,
|
|
340
|
+
path,
|
|
341
|
+
});
|
|
196
342
|
});
|
|
197
343
|
return websocket;
|
|
198
344
|
};
|
|
199
|
-
const listen = async (server) => {
|
|
200
|
-
await new Promise((resolveListen, rejectListen) => {
|
|
201
|
-
server.once("error", rejectListen);
|
|
202
|
-
server.listen(0, "127.0.0.1", () => {
|
|
203
|
-
server.off("error", rejectListen);
|
|
204
|
-
resolveListen();
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
const address = server.address();
|
|
208
|
-
if (address === null || typeof address === "string")
|
|
209
|
-
throw new Error("loopback replay did not acquire a TCP port");
|
|
210
|
-
return address.port;
|
|
211
|
-
};
|
|
212
|
-
const closeServer = (server) => new Promise((resolveClose, rejectClose) => {
|
|
213
|
-
server.close((error) => error === undefined ? resolveClose() : rejectClose(error));
|
|
214
|
-
});
|
|
215
345
|
/** Start bounded HTTP and WebSocket replay endpoints on IPv4 loopback only. */
|
|
216
|
-
export const startLoopbackReplay = async (scenario) => {
|
|
217
|
-
const recorder = new ReplayRecorder(scenario);
|
|
346
|
+
export const startLoopbackReplay = async (scenario, recordEvent = () => undefined) => {
|
|
347
|
+
const recorder = new ReplayRecorder(scenario, recordEvent);
|
|
218
348
|
const server = createServer(createHttpHandler(recorder));
|
|
219
349
|
const websocket = createWebSocketReplay(server, recorder);
|
|
350
|
+
let closePromise;
|
|
220
351
|
let port;
|
|
221
352
|
try {
|
|
222
|
-
port = await
|
|
353
|
+
port = await listenOnLoopback(server);
|
|
223
354
|
}
|
|
224
355
|
catch (cause) {
|
|
225
|
-
await
|
|
356
|
+
await closeReplayServer(server).catch(() => undefined);
|
|
226
357
|
throw cause;
|
|
227
358
|
}
|
|
228
359
|
return {
|
|
229
360
|
httpUrl: `http://127.0.0.1:${String(port)}`,
|
|
230
361
|
websocketUrl: `ws://127.0.0.1:${String(port)}/ws`,
|
|
231
|
-
events
|
|
362
|
+
get events() {
|
|
363
|
+
return recorder.events.map((event) => ({ ...event }));
|
|
364
|
+
},
|
|
365
|
+
get transitions() {
|
|
366
|
+
return recorder.transitions.map((transition) => ({
|
|
367
|
+
...transition,
|
|
368
|
+
sensitive_aliases: [...transition.sensitive_aliases],
|
|
369
|
+
}));
|
|
370
|
+
},
|
|
232
371
|
get truncated() {
|
|
233
372
|
return recorder.truncated;
|
|
234
373
|
},
|
|
235
374
|
async close() {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
375
|
+
closePromise ??= (async () => {
|
|
376
|
+
recorder.stopMachineAdmission();
|
|
377
|
+
for (const client of websocket.clients)
|
|
378
|
+
client.terminate();
|
|
379
|
+
await new Promise((resolveClose) => websocket.close(() => resolveClose()));
|
|
380
|
+
await closeReplayServer(server);
|
|
381
|
+
await recorder.drainMachine();
|
|
382
|
+
})();
|
|
383
|
+
await closePromise;
|
|
240
384
|
},
|
|
241
385
|
};
|
|
242
386
|
};
|