Merge origin/main into feat/dylib-resolution
Follow #952's Apple ownership: the Mach-O load-command reader, dylib resolution adapter and fixture move to src/artifacts/apple/, the resolver, dyld path semantics and findings to src/domain/apple/, and the dylib lane helper to scripts/verify/apple/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
e5c6fccc35
|
|
@ -311,7 +311,7 @@ jobs:
|
|||
- name: Real binary metadata and Interface Builder integration
|
||||
run: npm run verify:apple-dispatch && npm run verify:interface-builder
|
||||
- name: Native plist CLI selection regressions
|
||||
run: npm run test:focused -- tests/boundary/cli/nativePlistCli.test.ts
|
||||
run: npm run test:focused -- tests/boundary/cli/nativePlistCli.test.ts tests/boundary/cli/nativePlistPrototypeKeyCli.test.ts tests/boundary/cli/nativePlistSelectionFailureCli.test.ts
|
||||
|
||||
reconstruction-readiness:
|
||||
name: Reconstruction readiness
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ on:
|
|||
paths:
|
||||
- "website/**"
|
||||
- "scripts/verify-website.py"
|
||||
- "scripts/prepare-website.py"
|
||||
- ".github/workflows/**"
|
||||
workflow_dispatch:
|
||||
|
||||
|
|
@ -24,5 +25,7 @@ jobs:
|
|||
steps:
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Prepare the example download
|
||||
run: python3 scripts/prepare-website.py
|
||||
- name: Check website links and publishing configuration
|
||||
run: python3 scripts/verify-website.py
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ jobs:
|
|||
steps:
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Prepare the example download
|
||||
run: python3 scripts/prepare-website.py
|
||||
- name: Check website links and publishing configuration
|
||||
run: python3 scripts/verify-website.py
|
||||
- name: Configure Pages
|
||||
|
|
|
|||
|
|
@ -45,3 +45,6 @@ Thumbs.db
|
|||
|
||||
# Local reference sources and analysis artifacts
|
||||
/_reference/
|
||||
|
||||
# Generated website download
|
||||
/website/public/examples/notes-example.zip
|
||||
|
|
|
|||
|
|
@ -356,3 +356,32 @@ repository-root default. Both point to `_reference/firmware-integration/generate
|
|||
when invoked through the documented npm commands. Optional ext4/Ghidra lanes keep
|
||||
their own prerequisites. Source-fixture generation and synthetic producer tests
|
||||
do not establish real Binwalk/Unblob analysis or another host's execution support.
|
||||
|
||||
## Apple artifact capability ownership
|
||||
|
||||
`src/domain/apple/` owns Apple application anatomy/projection, asset catalog
|
||||
facts, Interface Builder graphs, keyed archive semantics and plist values.
|
||||
`src/application/apple/AppleApplicationService.ts` retains the shared workflow
|
||||
that projects authenticated IPA/macOS inventory Evidence.
|
||||
|
||||
`src/artifacts/apple/` owns the actual producer boundary: directory acquisition,
|
||||
assetutil invocation, plist/NIB decoding, encoded view-parent interpretation and
|
||||
keyed archive inspection. These helpers are used by ArtifactProvider, retain
|
||||
its existing typed analysis boundary, injected assetutil seam and original reader
|
||||
cleanup, and do not require a new port for each helper. Producer code no longer
|
||||
lives in the application layer. Guards reject application/composition/caller
|
||||
imports from this owner while admitting pure facts and shared artifact readers.
|
||||
Adapter-source tests retain their assertions in the existing forked adapter lane.
|
||||
|
||||
`scripts/verify/apple/` groups archive, asset catalog, Interface Builder,
|
||||
Objective-C/Swift dispatch and macOS bundle verification. The macOS bundle fixture
|
||||
builder lives in `scripts/fixtures/apple/`; NIB byte fixtures stay beside their
|
||||
artifact decoder. Existing format-specific conformance sources and goldens keep
|
||||
their locations, and relocated verifiers resolve them against their own file
|
||||
URLs. Public npm entrypoints and CLI/MCP contracts stay stable; generated-document
|
||||
cache inputs include the relocated runtime scripts and fixture builder.
|
||||
|
||||
Portable decoder and injected producer tests establish their format/boundary
|
||||
claims. Actual Swift/Xcode, assetutil, native NIB, bundle signing and DMG lifecycle
|
||||
claims require the real Apple lane. Native binary API/value-flow semantics and
|
||||
shared cross-domain contract aggregates keep their separate owners.
|
||||
|
|
|
|||
|
|
@ -98,6 +98,12 @@ covers `trace-dylib-resolution`:
|
|||
|
||||
It runs in macOS CI.
|
||||
|
||||
Apple artifact verifiers live in `scripts/verify/apple/`, with the macOS bundle
|
||||
builder under `scripts/fixtures/apple/` and NIB byte fixtures beside the decoder
|
||||
in `src/artifacts/apple/`. The npm entrypoints are unchanged. Format-specific
|
||||
Swift/XIB/asset sources and goldens retain their locations; real Apple workflows
|
||||
resolve them from the verifier file URL and run in the macOS CI lane.
|
||||
|
||||
MCP SDK transport tests with recording providers remain integration tests.
|
||||
They are useful for schema drift and failure projection but do not prove that
|
||||
Hopper, Ghidra or another substituted engine works. `verify:package` proves
|
||||
|
|
|
|||
10
package.json
10
package.json
|
|
@ -121,17 +121,17 @@
|
|||
"typecheck": "tsc --noEmit",
|
||||
"verify:test-temp-hygiene": "npm run build:cached && node scripts/verify-test-temp-hygiene.mjs",
|
||||
"verify:hopper": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper.mjs",
|
||||
"verify:apple-dispatch": "npm run build:cached && node scripts/verify-apple-dispatch.mjs",
|
||||
"verify:apple-dispatch": "npm run build:cached && node scripts/verify/apple/dispatch.mjs",
|
||||
"verify:native-ui": "npm run build:cached && node scripts/verify-native-ui.mjs",
|
||||
"verify:keyed-archive": "npm run build:cached && node scripts/verify-keyed-archive.mjs",
|
||||
"verify:macos-bundle": "npm run build:cached && node scripts/verify-macos-bundle.mjs",
|
||||
"verify:keyed-archive": "npm run build:cached && node scripts/verify/apple/keyed-archive.mjs",
|
||||
"verify:macos-bundle": "npm run build:cached && node scripts/verify/apple/macos-bundle.mjs",
|
||||
"verify:ghidra": "npm run build:cached && node scripts/verify-real-ghidra.mjs",
|
||||
"verify:ida": "npm run build:cached && node scripts/verify-real-ida.mjs",
|
||||
"verify:ghidra:switch": "npm run build:cached && node scripts/verify-real-ghidra-switch.mjs",
|
||||
"verify:ghidra:dos": "npm run build:cached && node scripts/verify-real-ghidra-dos.mjs",
|
||||
"verify:ghidra:com": "npm run build:cached && node scripts/verify-real-ghidra-com.mjs",
|
||||
"verify:ghidra:aarch64-jump-table": "npm run build:cached && node scripts/verify-real-ghidra.mjs --aarch64-jump-table",
|
||||
"verify:interface-builder": "npm run build:cached && node scripts/verify-interface-builder.mjs",
|
||||
"verify:interface-builder": "npm run build:cached && node scripts/verify/apple/interface-builder.mjs",
|
||||
"verify:ghidra:cross-format": "npm run build:cached && node scripts/verify-real-ghidra.mjs --cross-format",
|
||||
"verify:ghidra:windows": "npm run build:cached && npm run build:ghidra-windows-fixture && node scripts/verify-real-ghidra-windows.mjs",
|
||||
"verify:browser": "npm run build:cached && node scripts/verify-real-browser.mjs",
|
||||
|
|
@ -171,7 +171,7 @@
|
|||
"docs:error-schema": "npm run build:cached && node scripts/generate-error-schema.mjs",
|
||||
"prepack": "node scripts/prepack.mjs",
|
||||
"prepare": "node scripts/prepare.mjs",
|
||||
"verify:asset-catalog": "npm run build:cached && node scripts/verify-asset-catalog.mjs",
|
||||
"verify:asset-catalog": "npm run build:cached && node scripts/verify/apple/asset-catalog.mjs",
|
||||
"verify:native-ui:permissions": "npm run build:cached && node scripts/verify-native-ui.mjs --allow-permission-denial",
|
||||
"test:focused": "node scripts/run-development-tests.mjs focused",
|
||||
"build:windows-native": "node scripts/build-windows-native.mjs",
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ const failedBoundary = (file, target) => {
|
|||
return "provider-construction";
|
||||
if (
|
||||
(file.startsWith("src/artifacts/javascript/") ||
|
||||
file.startsWith("src/artifacts/apple/") ||
|
||||
/^src\/artifacts\/ArtifactHash\.(?:js|ts)$/u.test(file)) &&
|
||||
(["application", "composition", "server", "cli", "main"].includes(
|
||||
targetLayer,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
"""Package the public Notes example for the static website."""
|
||||
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
|
||||
|
||||
|
||||
EXAMPLE_FILES = (
|
||||
"package.json",
|
||||
"main.js",
|
||||
"preload.js",
|
||||
"renderer.js",
|
||||
"csv.js",
|
||||
"index.html",
|
||||
)
|
||||
|
||||
|
||||
def prepare_notes_example(site):
|
||||
"""Create a ZIP with public source only and reproducible metadata."""
|
||||
source = site / "examples/notes-electron"
|
||||
contents = BytesIO()
|
||||
with ZipFile(contents, "w") as archive:
|
||||
for name in EXAMPLE_FILES:
|
||||
entry = ZipInfo(f"notes-example/{name}", (1980, 1, 1, 0, 0, 0))
|
||||
entry.create_system = 3
|
||||
entry.external_attr = 0o100644 << 16
|
||||
archive.writestr(
|
||||
entry,
|
||||
(source / name).read_bytes(),
|
||||
compress_type=ZIP_DEFLATED,
|
||||
compresslevel=9,
|
||||
)
|
||||
destination = site / "examples/notes-example.zip"
|
||||
destination.write_bytes(contents.getvalue())
|
||||
print(f"Prepared examples/notes-example.zip ({len(EXAMPLE_FILES)} files).")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
root = Path(__file__).resolve().parent.parent
|
||||
prepare_notes_example(root / "website/public")
|
||||
|
|
@ -6,6 +6,7 @@ import re
|
|||
import sys
|
||||
from urllib.parse import unquote, urlsplit
|
||||
import xml.etree.ElementTree as ET
|
||||
from zipfile import BadZipFile, ZipFile
|
||||
|
||||
|
||||
class HtmlReferences(HTMLParser):
|
||||
|
|
@ -89,15 +90,39 @@ def check_publisher(root):
|
|||
return []
|
||||
|
||||
|
||||
def check_example_archive(site):
|
||||
"""Verify the downloadable ZIP contains the current six source files."""
|
||||
source = site / "examples/notes-electron"
|
||||
expected = {
|
||||
f"notes-example/{name}": source / name
|
||||
for name in ("package.json", "main.js", "preload.js", "renderer.js", "csv.js", "index.html")
|
||||
}
|
||||
path = site / "examples/notes-example.zip"
|
||||
if not path.is_file():
|
||||
return ["Notes example ZIP is missing; run python3 scripts/prepare-website.py."]
|
||||
try:
|
||||
with ZipFile(path) as archive:
|
||||
if sorted(archive.namelist()) != sorted(expected):
|
||||
return ["Notes example ZIP must contain exactly the six files in notes-example/."]
|
||||
return [
|
||||
f"Notes example ZIP has stale source: {name}"
|
||||
for name, original in expected.items()
|
||||
if archive.read(name) != original.read_bytes()
|
||||
]
|
||||
except (BadZipFile, RuntimeError) as error:
|
||||
return [f"Notes example ZIP cannot be read: {error}"]
|
||||
|
||||
|
||||
def main():
|
||||
"""Run the same checks locally, on website PRs and before publishing."""
|
||||
root = Path(__file__).resolve().parent.parent
|
||||
pages, references, errors = check_site(root / "website/public")
|
||||
errors.extend(check_example_archive(root / "website/public"))
|
||||
errors.extend(check_publisher(root))
|
||||
if errors:
|
||||
print("Website checks failed:\n" + "\n".join(errors), file=sys.stderr)
|
||||
return 1
|
||||
print(f"Verified {pages} HTML pages, {references} local references and SVG XML.")
|
||||
print(f"Verified {pages} HTML pages, {references} local references, SVG XML and the example ZIP.")
|
||||
print("website-pages.yml is the sole GitHub Pages publisher.")
|
||||
return 0
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
artifactCli,
|
||||
artifactMcpResult,
|
||||
withArtifactMcp,
|
||||
} from "./lib/artifact-e2e.mjs";
|
||||
} from "../../lib/artifact-e2e.mjs";
|
||||
|
||||
if (process.platform !== "darwin")
|
||||
throw new Error(
|
||||
|
|
@ -30,7 +30,7 @@ try {
|
|||
"clang",
|
||||
fileURLToPath(
|
||||
new URL(
|
||||
"../tests/conformance/interface-builder/fixture.c",
|
||||
"../../../tests/conformance/interface-builder/fixture.c",
|
||||
import.meta.url,
|
||||
),
|
||||
),
|
||||
|
|
@ -44,7 +44,10 @@ try {
|
|||
await exec("/usr/bin/xcrun", [
|
||||
"actool",
|
||||
fileURLToPath(
|
||||
new URL("../tests/conformance/native/Assets.xcassets", import.meta.url),
|
||||
new URL(
|
||||
"../../../tests/conformance/native/Assets.xcassets",
|
||||
import.meta.url,
|
||||
),
|
||||
),
|
||||
"--compile",
|
||||
resources,
|
||||
|
|
@ -4,8 +4,8 @@ import { tmpdir } from "node:os";
|
|||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
|
||||
import { inspectAppleDispatchMetadata } from "../dist/native/AppleDispatchMetadata.js";
|
||||
import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js";
|
||||
import { inspectAppleDispatchMetadata } from "../../../dist/native/AppleDispatchMetadata.js";
|
||||
|
||||
if (process.platform !== "darwin")
|
||||
throw new Error(
|
||||
|
|
@ -14,7 +14,7 @@ if (process.platform !== "darwin")
|
|||
const exec = promisify(execFile);
|
||||
const source = (name) =>
|
||||
fileURLToPath(
|
||||
new URL(`../tests/conformance/native/${name}`, import.meta.url),
|
||||
new URL(`../../../tests/conformance/native/${name}`, import.meta.url),
|
||||
);
|
||||
|
||||
/** Both link modes: legacy LC_DYLD_INFO binds and the default chained fixups. */
|
||||
|
|
@ -8,8 +8,8 @@ import { join } from "node:path";
|
|||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
|
||||
import { ArtifactProvider } from "../dist/artifacts/ArtifactProvider.js";
|
||||
import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js";
|
||||
import { ArtifactProvider } from "../../../dist/artifacts/ArtifactProvider.js";
|
||||
|
||||
const exec = promisify(execFile);
|
||||
const fixtureRoot = await mkdtemp(join(tmpdir(), "rea-interface-builder-"));
|
||||
|
|
@ -18,7 +18,7 @@ const resourcesPath = join(appPath, "Contents", "Resources");
|
|||
const executablePath = join(appPath, "Contents", "MacOS", "Fixture");
|
||||
const nibPath = join(resourcesPath, "Main.nib");
|
||||
const sourceRoot = fileURLToPath(
|
||||
new URL("../tests/conformance/interface-builder/", import.meta.url),
|
||||
new URL("../../../tests/conformance/interface-builder/", import.meta.url),
|
||||
);
|
||||
|
||||
try {
|
||||
|
|
@ -10,7 +10,7 @@ import {
|
|||
artifactCli,
|
||||
artifactMcpResult,
|
||||
withArtifactMcp,
|
||||
} from "./lib/artifact-e2e.mjs";
|
||||
} from "../../lib/artifact-e2e.mjs";
|
||||
|
||||
if (process.platform !== "darwin")
|
||||
throw new Error(
|
||||
|
|
@ -25,7 +25,7 @@ try {
|
|||
join(root, "modules"),
|
||||
fileURLToPath(
|
||||
new URL(
|
||||
"../tests/conformance/native/keyed-archive.swift",
|
||||
"../../../tests/conformance/native/keyed-archive.swift",
|
||||
import.meta.url,
|
||||
),
|
||||
),
|
||||
|
|
@ -66,7 +66,7 @@ try {
|
|||
const expected = JSON.parse(
|
||||
await readFile(
|
||||
new URL(
|
||||
"../tests/fixtures/golden/keyed-archive/graph.json",
|
||||
"../../../tests/fixtures/golden/keyed-archive/graph.json",
|
||||
import.meta.url,
|
||||
),
|
||||
"utf8",
|
||||
|
|
@ -124,7 +124,7 @@ try {
|
|||
join(root, "modules"),
|
||||
fileURLToPath(
|
||||
new URL(
|
||||
"../tests/conformance/native/keyed-archive-uid-root.swift",
|
||||
"../../../tests/conformance/native/keyed-archive-uid-root.swift",
|
||||
import.meta.url,
|
||||
),
|
||||
),
|
||||
|
|
@ -9,12 +9,12 @@ import {
|
|||
artifactCliEvidence,
|
||||
artifactMcpResult,
|
||||
withArtifactMcp,
|
||||
} from "./lib/artifact-e2e.mjs";
|
||||
} from "../../lib/artifact-e2e.mjs";
|
||||
import {
|
||||
buildMacosBundleFixture,
|
||||
preflightMacosBundleFixture,
|
||||
} from "./lib/macos-bundle-fixture.mjs";
|
||||
import { verifyDylibResolution } from "./lib/macos-dylib-e2e.mjs";
|
||||
} from "../../fixtures/apple/macos-bundle.mjs";
|
||||
import { verifyDylibResolution } from "./macos-dylib-e2e.mjs";
|
||||
|
||||
const exec = promisify(execFile);
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ import {
|
|||
artifactCliEvidence,
|
||||
artifactMcpResult,
|
||||
withArtifactMcp,
|
||||
} from "./artifact-e2e.mjs";
|
||||
} from "../../lib/artifact-e2e.mjs";
|
||||
|
||||
const exec = promisify(execFile);
|
||||
|
||||
|
|
@ -3,7 +3,7 @@ import { z } from "zod";
|
|||
import type { AnalysisOperationPort } from "./AnalysisProvider.js";
|
||||
import type { EnhancedResult } from "./EnhancedToolTypes.js";
|
||||
import type { enhancedInputSchemas } from "../contracts/enhancedInputs.js";
|
||||
import { interfaceBuilderAnalysisSchema } from "../domain/interfaceBuilderGraph.js";
|
||||
import { interfaceBuilderAnalysisSchema } from "../domain/apple/interfaceBuilderGraph.js";
|
||||
import {
|
||||
nativeInvestigationGraphSchema,
|
||||
nativeInvestigationTraceSchema,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import type { Evidence } from "../domain/evidence.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
import {
|
||||
appleApplicationProjectionInputSchema,
|
||||
projectAppleApplication,
|
||||
} from "../domain/appleApplication.js";
|
||||
import type { Result } from "../domain/result.js";
|
||||
import { APPLE_APPLICATION_PROVIDER } from "./InvestigationProviders.js";
|
||||
import { projectInventoryEvidence } from "./InventoryProjectionEvidence.js";
|
||||
} from "../../domain/apple/appleApplication.js";
|
||||
import type { Result } from "../../domain/result.js";
|
||||
import { APPLE_APPLICATION_PROVIDER } from "../InvestigationProviders.js";
|
||||
import { projectInventoryEvidence } from "../InventoryProjectionEvidence.js";
|
||||
|
||||
const OPERATION = "project_apple_application_graph" as const;
|
||||
|
||||
|
|
@ -7,13 +7,13 @@ import {
|
|||
type ProviderIdentity,
|
||||
type ExecutionOptions,
|
||||
} from "../application/AnalysisProvider.js";
|
||||
import { inspectBundleKeyedArchive } from "./KeyedArchiveReader.js";
|
||||
import { traceDylibResolution } from "./DylibResolutionReader.js";
|
||||
import { inspectBundleKeyedArchive } from "./apple/KeyedArchiveReader.js";
|
||||
import { traceDylibResolution } from "./apple/DylibResolutionReader.js";
|
||||
import { basename, dirname } from "node:path";
|
||||
import { inventoryArtifact } from "../application/ArtifactInventory.js";
|
||||
import { extractArtifact } from "../application/ArtifactExtraction.js";
|
||||
import { analyzeInterfaceBuilderBundle } from "../application/InterfaceBuilderAnalysis.js";
|
||||
import { analyzeAppleAssetCatalogs } from "../application/AppleAssetCatalogAnalysis.js";
|
||||
import { analyzeInterfaceBuilderBundle } from "./apple/InterfaceBuilderAnalysis.js";
|
||||
import { analyzeAppleAssetCatalogs } from "./apple/AppleAssetCatalogAnalysis.js";
|
||||
import {
|
||||
ARTIFACT_ANALYSIS_OPERATIONS,
|
||||
artifactInventoryInputSchema,
|
||||
|
|
@ -25,7 +25,7 @@ import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.
|
|||
import { ArtifactOperationError } from "../domain/artifactOperationError.js";
|
||||
import { type AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import { interfaceBuilderLimitsSchema } from "../domain/interfaceBuilderGraph.js";
|
||||
import { interfaceBuilderLimitsSchema } from "../domain/apple/interfaceBuilderGraph.js";
|
||||
import { err, ok } from "../domain/result.js";
|
||||
import { ArtifactReaderFailure } from "./ArtifactReader.js";
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { createHash } from "node:crypto";
|
||||
import { lstat } from "node:fs/promises";
|
||||
import { DirectoryArtifactReader } from "../artifacts/DirectoryArtifactReader.js";
|
||||
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
||||
import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js";
|
||||
import { ArtifactReaderFailure } from "../ArtifactReader.js";
|
||||
import { analyzeInterfaceBuilderBundle } from "./InterfaceBuilderAnalysis.js";
|
||||
import {
|
||||
appleAssetCatalogInputSchema,
|
||||
parseAppleAssetCatalogRecords,
|
||||
projectAppleAssetCatalogPage,
|
||||
} from "../domain/appleAssetCatalog.js";
|
||||
import { execFileOutput } from "../process/ExecFileOutput.js";
|
||||
} from "../../domain/apple/appleAssetCatalog.js";
|
||||
import { execFileOutput } from "../../process/ExecFileOutput.js";
|
||||
|
||||
const ASSETUTIL = "/usr/bin/assetutil";
|
||||
const MAX_CATALOG_BYTES = 64 * 1024 * 1024;
|
||||
|
|
@ -105,7 +105,7 @@ export const analyzeAppleAssetCatalogs = async (input: {
|
|||
|
||||
const inspectAssetCatalogEntry = async (input: {
|
||||
readonly reader: DirectoryArtifactReader;
|
||||
readonly entry: import("../artifacts/ArtifactReader.js").ArtifactEntry;
|
||||
readonly entry: import("../ArtifactReader.js").ArtifactEntry;
|
||||
readonly runAssetUtil: (
|
||||
path: string,
|
||||
signal?: AbortSignal,
|
||||
|
|
@ -237,7 +237,7 @@ const runAssetUtil = async (
|
|||
|
||||
const digestBounded = async (
|
||||
reader: DirectoryArtifactReader,
|
||||
entry: import("../artifacts/ArtifactReader.js").ArtifactEntry,
|
||||
entry: import("../ArtifactReader.js").ArtifactEntry,
|
||||
signal?: AbortSignal,
|
||||
): Promise<string> => {
|
||||
if (entry.declaredSize === null || entry.declaredSize > MAX_CATALOG_BYTES)
|
||||
|
|
@ -3,7 +3,7 @@ import { constants } from "node:fs";
|
|||
import { lstat, open, readlink, realpath } from "node:fs/promises";
|
||||
import { join, relative, sep } from "node:path";
|
||||
|
||||
import { resolveTreePath } from "../domain/dyldPaths.js";
|
||||
import { resolveTreePath } from "../../domain/apple/dyldPaths.js";
|
||||
import {
|
||||
dylibResolutionInputSchema,
|
||||
dylibResolutionResultSchema,
|
||||
|
|
@ -12,9 +12,9 @@ import {
|
|||
type DylibTreeEntry,
|
||||
type DylibTreeView,
|
||||
type MachoImageFacts,
|
||||
} from "../domain/dylibResolution.js";
|
||||
import { ArtifactReaderFailure } from "./ArtifactReader.js";
|
||||
import { DirectoryArtifactReader } from "./DirectoryArtifactReader.js";
|
||||
} from "../../domain/apple/dylibResolution.js";
|
||||
import { ArtifactReaderFailure } from "../ArtifactReader.js";
|
||||
import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js";
|
||||
import { hasMachoMagic, readMachoImage } from "./MachoLoadCommandReader.js";
|
||||
|
||||
const HASH_CHUNK_BYTES = 1024 * 1024;
|
||||
|
|
@ -1,28 +1,25 @@
|
|||
import { createHash } from "node:crypto";
|
||||
import { parseBinary } from "plist";
|
||||
|
||||
import { DirectoryArtifactReader } from "../artifacts/DirectoryArtifactReader.js";
|
||||
import { projectPlistValue } from "../domain/plistValue.js";
|
||||
import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js";
|
||||
import { projectPlistValue } from "../../domain/apple/plistValue.js";
|
||||
import {
|
||||
omittedPrototypeKeysLimitation,
|
||||
parseXmlPropertyList,
|
||||
} from "../domain/propertyListKeys.js";
|
||||
import {
|
||||
decodeNibArchive,
|
||||
type NibArchiveDocument,
|
||||
} from "../artifacts/NibArchive.js";
|
||||
import type { ArtifactEntry } from "../artifacts/ArtifactReader.js";
|
||||
} from "../../domain/propertyListKeys.js";
|
||||
import { decodeNibArchive, type NibArchiveDocument } from "./NibArchive.js";
|
||||
import type { ArtifactEntry } from "../ArtifactReader.js";
|
||||
import {
|
||||
buildInterfaceBuilderAnalysis,
|
||||
interfaceBuilderLimitsSchema,
|
||||
type InterfaceBuilderDocumentInput,
|
||||
} from "../domain/interfaceBuilderGraph.js";
|
||||
} from "../../domain/apple/interfaceBuilderGraph.js";
|
||||
import {
|
||||
mergeNibHierarchies,
|
||||
projectNibViewHierarchy,
|
||||
type NibHierarchyNode,
|
||||
} from "./NibViewHierarchy.js";
|
||||
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
|
||||
import { jsonValueSchema, type JsonValue } from "../../domain/jsonValue.js";
|
||||
|
||||
const MAX_DOCUMENT_BYTES = 64 * 1024 * 1024;
|
||||
|
||||
|
|
@ -2,19 +2,19 @@ import { createHash } from "node:crypto";
|
|||
import { TextDecoder } from "node:util";
|
||||
import { parseBinary } from "plist";
|
||||
import { z } from "zod";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import { projectPlistValue } from "../domain/plistValue.js";
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
import { projectPlistValue } from "../../domain/apple/plistValue.js";
|
||||
import {
|
||||
omittedPrototypeKeysLimitation,
|
||||
parseXmlPropertyList,
|
||||
} from "../domain/propertyListKeys.js";
|
||||
} from "../../domain/propertyListKeys.js";
|
||||
import {
|
||||
keyedArchiveInputSchema,
|
||||
keyedArchiveResultSchema,
|
||||
projectKeyedArchive,
|
||||
} from "../domain/keyedArchive.js";
|
||||
import { DirectoryArtifactReader } from "./DirectoryArtifactReader.js";
|
||||
import { ArtifactReaderFailure } from "./ArtifactReader.js";
|
||||
} from "../../domain/apple/keyedArchive.js";
|
||||
import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js";
|
||||
import { ArtifactReaderFailure } from "../ArtifactReader.js";
|
||||
|
||||
const MAX_BYTES = 64 * 1024 * 1024;
|
||||
|
||||
|
|
@ -2,7 +2,7 @@ import type {
|
|||
MachoDependency,
|
||||
MachoImageFacts,
|
||||
MachoSlice,
|
||||
} from "../domain/dylibResolution.js";
|
||||
} from "../../domain/apple/dylibResolution.js";
|
||||
|
||||
/** Read `length` bytes at `offset`; shorter results mean end of file. */
|
||||
export type ReadAt = (offset: number, length: number) => Promise<Uint8Array>;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
import { TextDecoder } from "node:util";
|
||||
|
||||
const MAGIC = Buffer.from("NIBArchive", "ascii");
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type { NibArchiveDocument } from "../artifacts/NibArchive.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import type { NibArchiveDocument } from "./NibArchive.js";
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
|
||||
export type NibHierarchyNode = {
|
||||
objectID: string;
|
||||
|
|
@ -2,7 +2,7 @@ import { reconstructionCoverageEvaluationInputSchema } from "../domain/reconstru
|
|||
import { Cli, z } from "incur";
|
||||
|
||||
import { projectAndroidApplicationEvidence } from "../application/android/AndroidApplicationService.js";
|
||||
import { projectAppleApplicationEvidence } from "../application/AppleApplicationService.js";
|
||||
import { projectAppleApplicationEvidence } from "../application/apple/AppleApplicationService.js";
|
||||
import {
|
||||
compareApplicationVersionsEvidenceValidated,
|
||||
compareJavaScriptExportShapesEvidenceValidated,
|
||||
|
|
@ -19,7 +19,7 @@ import { AnalysisInputError } from "../domain/analysisErrorCore.js";
|
|||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { type AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { androidApplicationProjectionInputSchema } from "../domain/android/androidApplication.js";
|
||||
import { appleApplicationProjectionInputSchema } from "../domain/appleApplication.js";
|
||||
import { appleApplicationProjectionInputSchema } from "../domain/apple/appleApplication.js";
|
||||
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
|
||||
import type { Logger } from "../logger.js";
|
||||
import { traceApplicationFeatureInputSchema } from "../domain/javascript/javascriptFeatureTraceSchemas.js";
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import {
|
|||
import {
|
||||
appleApplicationProjectionInputSchema,
|
||||
appleApplicationProjectionResultSchema,
|
||||
} from "../domain/appleApplication.js";
|
||||
} from "../domain/apple/appleApplication.js";
|
||||
import type { ToolContract } from "./toolContracts.js";
|
||||
import { toolContractMetadata } from "./toolEffects.js";
|
||||
import { evidenceResultOf } from "./toolOutputSchemas.js";
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import { artifactOutputSchemas } from "./toolOutputSchemas.js";
|
|||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import { toolContractMetadata } from "./toolEffects.js";
|
||||
import { requireOutputSchema } from "./toolOutputSchemaPrimitives.js";
|
||||
import { appleAssetCatalogInputSchema } from "../domain/appleAssetCatalog.js";
|
||||
import { dylibResolutionInputSchema } from "../domain/dylibResolution.js";
|
||||
import { keyedArchiveInputSchema } from "../domain/keyedArchive.js";
|
||||
import { appleAssetCatalogInputSchema } from "../domain/apple/appleAssetCatalog.js";
|
||||
import { dylibResolutionInputSchema } from "../domain/apple/dylibResolution.js";
|
||||
import { keyedArchiveInputSchema } from "../domain/apple/keyedArchive.js";
|
||||
/** Exact caller boundary for deterministic artifact inventory. */
|
||||
export const artifactInventoryInputSchema = z.strictObject({
|
||||
integrity_policy: z.enum(["fail", "record-and-continue"]).default("fail"),
|
||||
|
|
|
|||
|
|
@ -30,10 +30,10 @@ import {
|
|||
} from "../domain/nativeInspection.js";
|
||||
import { artifactExtractionResultSchema } from "../domain/artifactGraph.js";
|
||||
import { artifactInspectionResultSchema } from "../domain/artifactInspection.js";
|
||||
import { interfaceBuilderAnalysisSchema } from "../domain/interfaceBuilderGraph.js";
|
||||
import { dylibResolutionResultSchema } from "../domain/dylibResolution.js";
|
||||
import { keyedArchiveResultSchema } from "../domain/keyedArchive.js";
|
||||
import { appleAssetCatalogResultSchema } from "../domain/appleAssetCatalog.js";
|
||||
import { interfaceBuilderAnalysisSchema } from "../domain/apple/interfaceBuilderGraph.js";
|
||||
import { dylibResolutionResultSchema } from "../domain/apple/dylibResolution.js";
|
||||
import { keyedArchiveResultSchema } from "../domain/apple/keyedArchive.js";
|
||||
import { appleAssetCatalogResultSchema } from "../domain/apple/appleAssetCatalog.js";
|
||||
import {
|
||||
managedArtifactInspectionSchema,
|
||||
managedMemberInspectionSchema,
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import { createHash } from "node:crypto";
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { projectAppleApplication } from "./appleApplication.js";
|
||||
import { canonicalDigest } from "./comparisonSemantics.js";
|
||||
import { createEvidence, type Evidence } from "./evidence.js";
|
||||
import { jsonValueSchema } from "./jsonValue.js";
|
||||
import { canonicalDigest } from "../comparisonSemantics.js";
|
||||
import { createEvidence, type Evidence } from "../evidence.js";
|
||||
import { jsonValueSchema } from "../jsonValue.js";
|
||||
|
||||
type EntryKind = "file" | "directory" | "symlink";
|
||||
type RootFormat = "ipa" | "zip" | "dmg" | "directory" | "asar";
|
||||
|
|
@ -14,10 +14,10 @@ import {
|
|||
platformsOf,
|
||||
type AppleInventoryEntry,
|
||||
} from "./appleBundleAnatomy.js";
|
||||
import { parseArtifactInventoryEvidence } from "./artifactInventoryEvidence.js";
|
||||
import { evidenceSchema } from "./evidence.js";
|
||||
import { digestSchema } from "./../domain/digests.js";
|
||||
import { prefixedDigestSchema } from "./../domain/digests.js";
|
||||
import { parseArtifactInventoryEvidence } from "../artifactInventoryEvidence.js";
|
||||
import { evidenceSchema } from "../evidence.js";
|
||||
import { digestSchema } from "../digests.js";
|
||||
import { prefixedDigestSchema } from "../digests.js";
|
||||
|
||||
const evidenceIdSchema = prefixedDigestSchema("ev");
|
||||
const pathSchema = z.string().min(1);
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { jsonValueSchema } from "./jsonValue.js";
|
||||
import { AnalysisOutputError } from "./analysisErrorCore.js";
|
||||
import { jsonValueSchema } from "../jsonValue.js";
|
||||
import { AnalysisOutputError } from "../analysisErrorCore.js";
|
||||
|
||||
/** Caller-controlled page bounds for compiled Apple asset metadata. */
|
||||
export const appleAssetCatalogInputSchema = z.strictObject({
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { digestSchema, prefixedDigestSchema } from "./digests.js";
|
||||
import { digestSchema, prefixedDigestSchema } from "../digests.js";
|
||||
|
||||
const pathSchema = z.string().min(1);
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ import {
|
|||
resolveTreePath,
|
||||
type Expansion,
|
||||
} from "./dyldPaths.js";
|
||||
import { digestSchema } from "./digests.js";
|
||||
import { digestSchema } from "../digests.js";
|
||||
import {
|
||||
DYLIB_RESOLUTION_LIMITATIONS,
|
||||
deriveFindings,
|
||||
|
|
@ -10,7 +10,7 @@ import {
|
|||
nativeInvestigationGraphSchema,
|
||||
nativeInvestigationNodeSchema,
|
||||
nativeInvestigationTraceLimitsSchema,
|
||||
} from "./nativeInvestigationGraph.js";
|
||||
} from "../nativeInvestigationGraph.js";
|
||||
|
||||
export { parseInterfaceBuilderRecords } from "./interfaceBuilderKeyedArchive.js";
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { jsonValueSchema } from "./jsonValue.js";
|
||||
import { jsonValueSchema } from "../jsonValue.js";
|
||||
|
||||
const recordSchema = z.record(z.string(), z.unknown());
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { z } from "zod";
|
||||
import { jsonValueSchema, type JsonValue } from "./jsonValue.js";
|
||||
import { jsonValueSchema, type JsonValue } from "../jsonValue.js";
|
||||
|
||||
/** Select one archive within the active bundle and optionally one named root. */
|
||||
export const keyedArchiveInputSchema = z.strictObject({
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
import type { JsonValue } from "../jsonValue.js";
|
||||
|
||||
const MAX_PLIST_DEPTH = 128;
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ import {
|
|||
import {
|
||||
buildInterfaceBuilderAnalysis,
|
||||
interfaceBuilderLimitsSchema,
|
||||
} from "./interfaceBuilderGraph.js";
|
||||
} from "./apple/interfaceBuilderGraph.js";
|
||||
|
||||
const evidence = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import type { JsonValue } from "../../domain/jsonValue.js";
|
|||
import {
|
||||
projectPlistValue,
|
||||
type ProjectedPlistValue,
|
||||
} from "../../domain/plistValue.js";
|
||||
} from "../../domain/apple/plistValue.js";
|
||||
import {
|
||||
omitPrototypeKeys,
|
||||
omittedPrototypeKeysLimitation,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { McpServer } from "@modelcontextprotocol/server";
|
||||
|
||||
import { projectAndroidApplicationEvidence } from "../../application/android/AndroidApplicationService.js";
|
||||
import { projectAppleApplicationEvidence } from "../../application/AppleApplicationService.js";
|
||||
import { projectAppleApplicationEvidence } from "../../application/apple/AppleApplicationService.js";
|
||||
import {
|
||||
APPLICATION_TOOL_CONTRACTS,
|
||||
applicationToolContract,
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import {
|
|||
dylibCommand,
|
||||
machoImage,
|
||||
rpathCommand,
|
||||
} from "../../../src/artifacts/MachoImage.fixture.js";
|
||||
import { dylibResolutionResultSchema } from "../../../src/domain/dylibResolution.js";
|
||||
} from "../../../src/artifacts/apple/MachoImage.fixture.js";
|
||||
import { dylibResolutionResultSchema } from "../../../src/domain/apple/dylibResolution.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import { promisify } from "node:util";
|
|||
import { buildBinary } from "plist";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { analyzeInterfaceBuilderBundle } from "../../../src/application/InterfaceBuilderAnalysis.js";
|
||||
import { encodeNibArchiveFixture } from "../../../src/artifacts/NibArchive.fixture.js";
|
||||
import { analyzeInterfaceBuilderBundle } from "../../../src/artifacts/apple/InterfaceBuilderAnalysis.js";
|
||||
import { encodeNibArchiveFixture } from "../../../src/artifacts/apple/NibArchive.fixture.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
const compile = promisify(execFile);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { readFile } from "node:fs/promises";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { decodeKeyedArchiveBytes } from "../../../src/artifacts/KeyedArchiveReader.js";
|
||||
import { decodeKeyedArchiveBytes } from "../../../src/artifacts/apple/KeyedArchiveReader.js";
|
||||
import {
|
||||
parseAppleAssetCatalogRecords,
|
||||
projectAppleAssetCatalogPage,
|
||||
} from "../../../src/domain/appleAssetCatalog.js";
|
||||
} from "../../../src/domain/apple/appleAssetCatalog.js";
|
||||
|
||||
const golden = (path: string) =>
|
||||
new URL(`../../fixtures/golden/${path}`, import.meta.url);
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import { promisify } from "node:util";
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { decodeNibArchive } from "../../../src/artifacts/NibArchive.js";
|
||||
import { analyzeInterfaceBuilderBundle } from "../../../src/application/InterfaceBuilderAnalysis.js";
|
||||
import { decodeNibArchive } from "../../../src/artifacts/apple/NibArchive.js";
|
||||
import { analyzeInterfaceBuilderBundle } from "../../../src/artifacts/apple/InterfaceBuilderAnalysis.js";
|
||||
|
||||
const compile = promisify(execFile);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { expect, it } from "vitest";
|
|||
import { z } from "zod";
|
||||
import { parseConfig } from "../../../src/config.js";
|
||||
import { createBinarySession } from "../../../src/application/runtime.js";
|
||||
import { keyedArchiveResultSchema } from "../../../src/domain/keyedArchive.js";
|
||||
import { keyedArchiveResultSchema } from "../../../src/domain/apple/keyedArchive.js";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ import {
|
|||
dylibCommand,
|
||||
machoImage,
|
||||
rpathCommand,
|
||||
} from "../../../src/artifacts/MachoImage.fixture.js";
|
||||
import { dylibResolutionResultSchema } from "../../../src/domain/dylibResolution.js";
|
||||
} from "../../../src/artifacts/apple/MachoImage.fixture.js";
|
||||
import { dylibResolutionResultSchema } from "../../../src/domain/apple/dylibResolution.js";
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { join } from "node:path";
|
|||
import { buildBinary, parse } from "plist";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { inspectBundleKeyedArchive } from "../../../../src/artifacts/KeyedArchiveReader.js";
|
||||
import { inspectBundleKeyedArchive } from "../../../../src/artifacts/apple/KeyedArchiveReader.js";
|
||||
import { nativeFixture } from "../../../fixtures/nativeCommands.js";
|
||||
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ import {
|
|||
requireSuccessfulProjection,
|
||||
} from "../../support/applicationSessionFixture.js";
|
||||
|
||||
import { projectAppleApplicationEvidence } from "../../../src/application/AppleApplicationService.js";
|
||||
import { projectAppleApplicationEvidence } from "../../../src/application/apple/AppleApplicationService.js";
|
||||
import { runProviderAnalysis } from "../../../src/application/DirectAnalysis.js";
|
||||
import { appleApplicationProjectionResultSchema } from "../../../src/domain/appleApplication.js";
|
||||
import { appleApplicationProjectionResultSchema } from "../../../src/domain/apple/appleApplication.js";
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
|
||||
// Real ZIP entries cross the former component and bridge-candidate limits.
|
||||
|
|
|
|||
|
|
@ -272,3 +272,22 @@ describe("test lane import boundaries", () => {
|
|||
if (!allowed) expect(result.stdout).toContain("no-restricted-imports");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Apple artifact producer ownership", () => {
|
||||
it.each([
|
||||
["../../application/Workflow.js", false],
|
||||
["../../composition/android.js", false],
|
||||
["../../cli.js", false],
|
||||
["../../domain/apple/plistValue.js", true],
|
||||
["../DirectoryArtifactReader.js", true],
|
||||
])("checks the producer dependency %s", (dependency, allowed) => {
|
||||
const violations = inspectModuleBoundaries(
|
||||
"src/artifacts/apple/probe.ts",
|
||||
`import type { Value } from ${JSON.stringify(dependency)};`,
|
||||
process.cwd(),
|
||||
);
|
||||
|
||||
expect(violations.length === 0).toBe(allowed);
|
||||
if (!allowed) expect(violations[0]?.boundary).toBe("artifact-acquisition");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -111,6 +111,8 @@
|
|||
"scripts/lib/*.mjs",
|
||||
"scripts/verify/managed/*.mjs",
|
||||
"scripts/fixtures/managed/*.mjs",
|
||||
"scripts/verify/apple/*.mjs",
|
||||
"scripts/fixtures/apple/*.mjs",
|
||||
"src/generated*.ts",
|
||||
"src/contracts/**/*.ts",
|
||||
"docs/**",
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
# REA website
|
||||
|
||||
An English static website with explanatory figures, worked guides and a DX-Ball investigation.
|
||||
An English static website with explanatory figures, worked guides and DX-Ball and Notion investigations.
|
||||
The public files are in `website/public/`. The site uses HTML, CSS and a small
|
||||
script for copying code and following the assembly-to-C comparison; it has no
|
||||
build step or npm dependencies.
|
||||
script for copying code and following the assembly-to-C comparison. Python
|
||||
packages the downloadable example; there is no frontend bundler or npm dependency.
|
||||
|
||||
## Local preview
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
python3 scripts/prepare-website.py
|
||||
python3 -m http.server 4173 --bind 127.0.0.1 --directory website/public
|
||||
```
|
||||
|
||||
|
|
@ -17,8 +18,10 @@ Open <http://127.0.0.1:4173/>. Refresh the browser after editing a file.
|
|||
|
||||
## Pages
|
||||
|
||||
- `public/index.html`: product introduction and DX-Ball overview.
|
||||
- `public/index.html`: product introduction and case-study overviews.
|
||||
- `public/showcase/index.html`: the case-study index.
|
||||
- `public/showcase/dx-ball/index.html`: sound-pan investigation and project status.
|
||||
- `public/showcase/notion/index.html`: Notion's Electron clipboard bridge and rich clipboard format.
|
||||
- `public/get-started/index.html`: agent setup, first CLI result and provider guides.
|
||||
- `public/guides/`: a guide hub and native, JavaScript/Electron and browser examples.
|
||||
- `public/examples/`: downloadable Electron source and an interactive Notes browser app.
|
||||
|
|
@ -40,9 +43,18 @@ Electron fixture is for static analysis; Electron is not a prerequisite for
|
|||
following that example. The separate Notes browser app runs in the local preview
|
||||
and constructs a CSV download after fetching its JSON data.
|
||||
|
||||
The Electron guide offers one ZIP containing the six source files under
|
||||
`notes-example/`. `scripts/prepare-website.py` generates this download from
|
||||
an explicit file list, with fixed timestamps and permissions. The ZIP is ignored
|
||||
by Git; website checks and each manual publication regenerate it before checking
|
||||
and uploading the public directory. The verifier checks that its entries match
|
||||
the current source and contain no extra files.
|
||||
|
||||
Agent terminals show example prompts, not transcripts of previous
|
||||
investigations. A short cursor animation starts when the prompt enters view and
|
||||
respects reduced-motion preferences. All prompt text is present without JavaScript.
|
||||
investigations. All cursors blink continuously with the same CSS animation,
|
||||
respecting reduced-motion preferences. Both the prompt and animation work
|
||||
without JavaScript. The homepage and agent setup section share a copyable
|
||||
installation prompt; setup still presents its plan for approval.
|
||||
|
||||
DX-Ball figures and findings refer to the linked 7 October 2026 checkpoint,
|
||||
commit `a55dca27ec0a07018c1b2c95ae2be027f7d8c3c4`. Update those links and figures
|
||||
|
|
@ -63,6 +75,15 @@ The worked guides use semantic HTML flows that stack vertically on smaller
|
|||
screens. The Electron teaching example uses CommonJS, matching its preload
|
||||
code; the scoped lint override admits `require` only in that example directory.
|
||||
|
||||
The Notion case follows the same HTML figure style. Its short excerpts explain
|
||||
the packaged clipboard bridge; separate web-cache probes illustrate the rich
|
||||
clipboard and Markdown formats. Only selected source details and generic
|
||||
example data belong on the site. Machine paths, account identifiers, local
|
||||
configuration, complete vendor bundles and raw captured results stay outside
|
||||
the website.
|
||||
[evidence/notion-clipboard.md](evidence/notion-clipboard.md) records the REA
|
||||
package version, selected findings, source anchors and module-probe scope.
|
||||
|
||||
## GitHub Pages
|
||||
|
||||
`.github/workflows/website-pages.yml` prepares and deploys only `website/public`.
|
||||
|
|
@ -75,6 +96,7 @@ and the single Pages publisher without installing npm dependencies. The same
|
|||
checks run before each manual deployment. You can also run them locally:
|
||||
|
||||
```sh
|
||||
python3 scripts/prepare-website.py
|
||||
python3 scripts/verify-website.py
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,12 @@ rea analyze-javascript-application \
|
|||
The source was formatted before this result was recorded. Re-run analysis
|
||||
when the example files change.
|
||||
|
||||
The guide's `examples/notes-example.zip` is generated from exactly these six
|
||||
source files, under the `notes-example/` folder. Packaging changes no file
|
||||
contents. Website verification compares every ZIP entry to its source file and
|
||||
rejects extra entries. Existing source digests and static findings remain valid
|
||||
for the unzipped example.
|
||||
|
||||
## Notes browser example
|
||||
|
||||
The three-file browser app is in `website/public/examples/notes-web/`.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,133 @@
|
|||
# Notion clipboard case: sources and scope
|
||||
|
||||
The case uses Notion Desktop 7.6.1's shipped JavaScript, a fresh REA inspection
|
||||
of its tab preload, and separately recorded probes of cached web-editor modules.
|
||||
The public page contains selected result fields, small code excerpts and generic
|
||||
example data. Original packages, complete analysis results and captured local
|
||||
state are not website assets.
|
||||
|
||||
## Fresh REA result
|
||||
|
||||
The published npm REA 4.1.0 package inspected the saved tab preload on
|
||||
7 October 2026, using Node.js 24.18.0. An independent source build of public
|
||||
checkpoint [`810fe53e`](https://github.com/morluto/rea/tree/810fe53e) returned the
|
||||
same API and invoke findings. The page uses the published-package result.
|
||||
|
||||
The successful `analyze_javascript_application` request inspected the extracted
|
||||
directory `.webpack/renderer/tab_browser_view`. Its only file was `preload.js`:
|
||||
24,579 bytes, one parsed JavaScript file and no parse failures or truncated
|
||||
scopes. The result's relevant observations were:
|
||||
|
||||
| Finding | Selected fields | Source range |
|
||||
| -------------- | ------------------------------------------------------------------------------------------ | ----------------------------------------- |
|
||||
| Context bridge | `api_key: "__electronApi"`, `api_status: "dynamic"`, `members: []`, `unknown_members: 1` | `preload.js`, line 1, columns 22764–22828 |
|
||||
| Renderer IPC | `operation: "invoke"`, `channel_expression: "e"`, `channel: null`, `resolution: "dynamic"` | `preload.js`, line 1, columns 6486–6522 |
|
||||
|
||||
These are `ast-static-analysis` observations. Lines are one-based and columns
|
||||
zero-based, in the original decoded source. The page's two result panels are a
|
||||
readable projection of these fields, not a complete or replayable Evidence
|
||||
object.
|
||||
|
||||
The focused result locates the exposed bridge and IPC wrapper. Reading the
|
||||
packaged API object supplies the `clipboard.write` member and its literal
|
||||
`notion:clipboard:write` argument. Reading the main-process wrapper and receiver
|
||||
connects that channel to Electron's clipboard call. The four-step figure
|
||||
summarizes this source reading; it is not an automatically paired IPC graph or
|
||||
a captured desktop execution.
|
||||
|
||||
Additional feature traces reused the focused result:
|
||||
|
||||
- API seed `__electronApi`, exact match: one seed, partial coverage, 17 nodes,
|
||||
16 edges and no terminal paths.
|
||||
- Channel seed `notion:clipboard:write`, exact match: no graph match. Its
|
||||
propagation through the wrappers was unresolved in that graph.
|
||||
|
||||
Neither trace establishes a complete clipboard feature graph. The page therefore
|
||||
uses the directly observed API/invoke source ranges and the separately inspected
|
||||
wrapper code, rather than presenting those trace counts as the clipboard route.
|
||||
|
||||
## Packaged bridge excerpts
|
||||
|
||||
Artifact: Notion Desktop 7.6.1 Windows `app.asar`, 8,903,750 bytes.
|
||||
SHA-256: `97c41bc14fc0ea8c91cd34f502660963dfdf294d5eb35165ccac9d30963069d4`.
|
||||
|
||||
| Relative packaged file | Relevant source anchors |
|
||||
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `.webpack/renderer/tab_browser_view/preload.js` | Clipboard API member around character 8274; invoke helpers around 6105/6460; context bridge exposure around 22764 |
|
||||
| `.webpack/main/index.js` | Handler wrapper around character 313027; clipboard receiver around 726249; sender-check adapter around 750262; underlying `isNotionWebContents` around 1281553 |
|
||||
|
||||
Character anchors refer to decoded JavaScript, counted from the start of each
|
||||
file. They are distinct from REA's line/column ranges above.
|
||||
|
||||
Source digests:
|
||||
|
||||
- Tab preload: `c209b1222cd15721bf1988f88c3e57e187d11306edb5eddb13eca441feb6e337`.
|
||||
- Main bundle: `7d06e97721281403e7cf221389de22c6e40d800030a87511231b5390df1452ea`.
|
||||
|
||||
The HTML excerpts expand minified variable names and omit unrelated members.
|
||||
The API-member excerpt illustrates the relevant member; it does not reproduce
|
||||
the entire assembled API object. The sender-check expression expands the
|
||||
adapter to its underlying `isNotionWebContents` call. The meaningful operations
|
||||
are preserved: bind the literal channel, invoke it, register its receiver,
|
||||
check the sender and call `clipboard.write(data)`.
|
||||
|
||||
## Separately cached web-editor code
|
||||
|
||||
The web examples come from a 13 July 2026 cache, build
|
||||
`swv2-23.13.20260713.0259`. This is a separate artifact identity from the desktop
|
||||
ASAR. Corresponding API names help explain the two sides; no same-version live
|
||||
copy/paste session was captured for this case.
|
||||
|
||||
The clipboard examples use:
|
||||
|
||||
- Module `747001`: copy-payload construction, private MIME storage and virtual
|
||||
clipboard handling.
|
||||
- Module `58169`: marker encoding and decoding.
|
||||
- Module `736810`: desktop clipboard routing, including the
|
||||
`electronApi.clipboard.write` call.
|
||||
|
||||
The routing helper's asset,
|
||||
`clipboard-817bda3acae9bf1e__be64437d1c0e.js`, has SHA-256
|
||||
`be64437d1c0eba9c74b48f4307c5936790c59e85aa2ce845e22016a101c2ff7f`.
|
||||
|
||||
Saved Node/JSDOM probes used extracted modules, faithful lifted helpers and
|
||||
stubbed environment APIs. The marker helper is extracted code; the virtual
|
||||
clipboard reader includes a lifted implementation. A matching-ID specimen
|
||||
recovered private formats; a stale local entry retained ordinary text and HTML.
|
||||
This supports the recovered matching policy, rather than proof of OS clipboard
|
||||
hydration or fidelity of a complex block in the running app.
|
||||
|
||||
The figure's “Project notes” and `<copy-id>` are generic illustrations.
|
||||
`<copy-id>` stands for the generated marker ID; it is not a literal runtime UUID.
|
||||
|
||||
## Markdown specimen
|
||||
|
||||
The table uses the saved extracted-module probe of structured Markdown parser
|
||||
`422457`. Input:
|
||||
|
||||
```markdown
|
||||
| A | B |
|
||||
| --- | --- |
|
||||
| 1 | 2 |
|
||||
```
|
||||
|
||||
In the structured sample record, `/samples/table/raw/blocks/0` is a table with
|
||||
`headerRow: true`, two `table_row` children, and cell text `A`/`B` followed by
|
||||
`1`/`2`. The corresponding page and chat filtered outputs preserve those rows
|
||||
and cells. The public tree is an outline of these fields, not an unmodified
|
||||
JSON object or a persisted Notion page.
|
||||
|
||||
The broader historical harness included stubs and several parser variants.
|
||||
This specimen comes from the intact structured-parser result, rather than a
|
||||
legacy table-creation record affected by a synthetic ID collision. Historical
|
||||
module probes are attributed to that harness; REA's new role in this page is
|
||||
the separately verified preload analysis.
|
||||
|
||||
## SQLite follow-up
|
||||
|
||||
The short expandable follow-up is based on the same desktop package. Its
|
||||
`getSqliteMeta` path transfers `MessageChannelMain` ports between the renderer
|
||||
and a SQLite utility process. Later statement batches and results use that
|
||||
port. Relevant files are the tab preload, main bundle and `SqliteServer.js`.
|
||||
The page explains the source-level handoff without claiming a captured SQL
|
||||
session.
|
||||
|
|
@ -5,8 +5,13 @@ function initializeCopyButtons() {
|
|||
if (target === null) return;
|
||||
|
||||
const status = document.getElementById("copy-status");
|
||||
const content = target.textContent.trim();
|
||||
const text =
|
||||
target.closest(".terminal-prompt") === null
|
||||
? content
|
||||
: content.replace(/\s+/g, " ");
|
||||
try {
|
||||
await navigator.clipboard.writeText(target.textContent.trim());
|
||||
await navigator.clipboard.writeText(text);
|
||||
button.textContent = "Copied";
|
||||
if (status !== null) status.textContent = "Code copied to clipboard.";
|
||||
} catch {
|
||||
|
|
@ -55,22 +60,3 @@ function initializeStepComparisons() {
|
|||
}
|
||||
|
||||
initializeStepComparisons();
|
||||
|
||||
function initializeAgentTerminals() {
|
||||
if (!("IntersectionObserver" in window)) return;
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (!entry.isIntersecting) return;
|
||||
entry.target.classList.add("is-visible");
|
||||
observer.unobserve(entry.target);
|
||||
});
|
||||
},
|
||||
{ threshold: 0.25 },
|
||||
);
|
||||
document.querySelectorAll(".agent-terminal").forEach((terminal) => {
|
||||
observer.observe(terminal);
|
||||
});
|
||||
}
|
||||
|
||||
initializeAgentTerminals();
|
||||
|
|
|
|||
|
|
@ -766,9 +766,7 @@ pre code {
|
|||
margin-left: 0.4em;
|
||||
vertical-align: -0.12em;
|
||||
background: #9bbfe5;
|
||||
}
|
||||
.agent-terminal.is-visible .terminal-cursor {
|
||||
animation: terminal-blink 1s step-end 6;
|
||||
animation: terminal-blink 1s step-end infinite;
|
||||
}
|
||||
@keyframes terminal-blink {
|
||||
50% {
|
||||
|
|
@ -840,6 +838,61 @@ pre code {
|
|||
color: var(--ink);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.notion-bridge-flow {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
.notion-channel {
|
||||
color: var(--accent);
|
||||
background: #edf2f8;
|
||||
border-radius: 2px;
|
||||
box-decoration-break: clone;
|
||||
-webkit-box-decoration-break: clone;
|
||||
}
|
||||
.notion-clipboard-grid,
|
||||
.notion-table-example {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.notion-clipboard-grid .code-block,
|
||||
.notion-table-example .code-block {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.notion-clipboard-grid pre,
|
||||
.notion-table-example pre {
|
||||
padding: 1rem;
|
||||
font-size: 0.75rem;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.notion-mini-flow {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.6rem 1.8rem;
|
||||
margin: 1.3rem 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.notion-mini-flow li {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
}
|
||||
.notion-mini-flow li:not(:last-child)::after {
|
||||
content: "→";
|
||||
position: absolute;
|
||||
right: -1.3rem;
|
||||
color: #8191a2;
|
||||
}
|
||||
.case-preview {
|
||||
margin: 0;
|
||||
}
|
||||
.case-preview pre {
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.workflow-figure figcaption {
|
||||
margin-top: 1rem;
|
||||
color: var(--muted);
|
||||
|
|
@ -940,7 +993,8 @@ pre code {
|
|||
.rea-query-pair {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.flow-steps {
|
||||
.flow-steps,
|
||||
.notion-bridge-flow {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
|
@ -1048,6 +1102,20 @@ pre code {
|
|||
.workflow-figure {
|
||||
padding: 1rem;
|
||||
}
|
||||
.notion-clipboard-grid,
|
||||
.notion-table-example {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.notion-mini-flow {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.notion-mini-flow li:not(:last-child)::after {
|
||||
content: "↓";
|
||||
right: auto;
|
||||
left: 0.4rem;
|
||||
bottom: -1.4rem;
|
||||
}
|
||||
.step-selector {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
|
@ -1061,7 +1129,7 @@ pre code {
|
|||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.agent-terminal.is-visible .terminal-cursor {
|
||||
.terminal-cursor {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
><span class="brand-caption">Reverse Engineer Anything</span></a
|
||||
>
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="../showcase/dx-ball/">DX-Ball</a
|
||||
<a href="../showcase/">Showcases</a
|
||||
><a class="nav-start" href="./" aria-current="page">Get started</a
|
||||
><a href="../guides/">Guides</a
|
||||
><a href="https://github.com/morluto/rea">GitHub</a>
|
||||
|
|
@ -59,20 +59,48 @@
|
|||
include Claude Code, Codex, Cursor, Gemini CLI, Windsurf and other
|
||||
supported MCP clients.
|
||||
</p>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span>Terminal</span
|
||||
><button
|
||||
<div class="agent-terminal">
|
||||
<div class="terminal-header">
|
||||
<span class="terminal-title"
|
||||
><span class="terminal-dots" aria-hidden="true"
|
||||
><i></i><i></i><i></i></span
|
||||
>Your coding agent</span
|
||||
>
|
||||
<button
|
||||
class="copy-button"
|
||||
type="button"
|
||||
data-copy="agent-setup"
|
||||
aria-label="Copy agent setup command"
|
||||
data-copy="agent-install-prompt"
|
||||
aria-label="Copy REA installation prompt"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre><code id="agent-setup">npx rea-agents@latest setup</code></pre>
|
||||
<p class="terminal-prompt">
|
||||
<span class="prompt-symbol" aria-hidden="true">›</span
|
||||
><span id="agent-install-prompt"
|
||||
>Install REA and connect it to this coding agent using npx
|
||||
rea-agents@latest setup. Show me the setup plan for approval,
|
||||
then verify the installation.</span
|
||||
><span class="terminal-cursor" aria-hidden="true"></span>
|
||||
</p>
|
||||
</div>
|
||||
<details class="evidence-details">
|
||||
<summary>Or run setup yourself</summary>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span>Terminal</span
|
||||
><button
|
||||
class="copy-button"
|
||||
type="button"
|
||||
data-copy="agent-setup"
|
||||
aria-label="Copy agent setup command"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre><code id="agent-setup">npx rea-agents@latest setup</code></pre>
|
||||
</div>
|
||||
</details>
|
||||
<p>
|
||||
Choose your client and review the setup plan. REA adds its MCP
|
||||
connection and the matching investigation skill, keeping a backup
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
><span class="brand-caption">Reverse Engineer Anything</span></a
|
||||
>
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="../../showcase/dx-ball/">DX-Ball</a
|
||||
<a href="../../showcase/">Showcases</a
|
||||
><a class="nav-start" href="../../get-started/">Get started</a
|
||||
><a href="../">Guides</a
|
||||
><a href="https://github.com/morluto/rea">GitHub</a>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
><span class="brand-caption">Reverse Engineer Anything</span></a
|
||||
>
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="../showcase/dx-ball/">DX-Ball</a
|
||||
<a href="../showcase/">Showcases</a
|
||||
><a class="nav-start" href="../get-started/">Get started</a
|
||||
><a href="./" aria-current="page">Guides</a
|
||||
><a href="https://github.com/morluto/rea">GitHub</a>
|
||||
|
|
@ -71,6 +71,10 @@
|
|||
<span>IPC</span> <b aria-hidden="true">→</b> <span>CSV file</span>
|
||||
</div>
|
||||
<a class="text-link" href="javascript/">Application workflows</a>
|
||||
<p>
|
||||
For a larger packaged app, follow
|
||||
<a href="../showcase/notion/">Notion's clipboard bridge</a>.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="guide-entry">
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
><span class="brand-caption">Reverse Engineer Anything</span></a
|
||||
>
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="../../showcase/dx-ball/">DX-Ball</a
|
||||
<a href="../../showcase/">Showcases</a
|
||||
><a class="nav-start" href="../../get-started/">Get started</a
|
||||
><a href="../">Guides</a
|
||||
><a href="https://github.com/morluto/rea">GitHub</a>
|
||||
|
|
@ -203,42 +203,53 @@
|
|||
<section class="article-section" id="try">
|
||||
<h2>Analyze the example yourself</h2>
|
||||
<p>
|
||||
Save these six files together in a folder named
|
||||
<code>notes-example</code>. Static analysis needs Node.js and npm;
|
||||
you can inspect the code without installing or launching Electron.
|
||||
Download the example and unzip it. The archive contains the
|
||||
complete <code>notes-example</code> folder. Analyze it with
|
||||
Node.js and npm; Electron is not needed for this example.
|
||||
</p>
|
||||
<ul class="example-files">
|
||||
<li>
|
||||
<a href="../../examples/notes-electron/package.json" download
|
||||
>package.json</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../../examples/notes-electron/main.js" download
|
||||
>main.js</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../../examples/notes-electron/preload.js" download
|
||||
>preload.js</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../../examples/notes-electron/renderer.js" download
|
||||
>renderer.js</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../../examples/notes-electron/csv.js" download
|
||||
>csv.js</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../../examples/notes-electron/index.html" download
|
||||
>index.html</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
<a
|
||||
class="button"
|
||||
href="../../examples/notes-example.zip"
|
||||
download="notes-example.zip"
|
||||
>Download example ZIP</a
|
||||
>
|
||||
</p>
|
||||
<details class="evidence-details">
|
||||
<summary>Browse the six source files</summary>
|
||||
<ul class="example-files">
|
||||
<li>
|
||||
<a href="../../examples/notes-electron/package.json" download
|
||||
>package.json</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../../examples/notes-electron/main.js" download
|
||||
>main.js</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../../examples/notes-electron/preload.js" download
|
||||
>preload.js</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../../examples/notes-electron/renderer.js" download
|
||||
>renderer.js</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../../examples/notes-electron/csv.js" download
|
||||
>csv.js</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../../examples/notes-electron/index.html" download
|
||||
>index.html</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span>Static application analysis</span
|
||||
|
|
@ -255,7 +266,7 @@
|
|||
/absolute/path/to/notes-example --json</code></pre>
|
||||
</div>
|
||||
<p>
|
||||
Replace the path with the folder you saved. Look for
|
||||
Replace the path with the folder you unzipped. Look for
|
||||
<code>normalized_result.summary.ipc</code> and the source
|
||||
locations in <code>normalized_result.graph</code>. The example has
|
||||
one channel and one paired invocation.
|
||||
|
|
@ -281,6 +292,12 @@
|
|||
runtime observations give you another way to investigate them.
|
||||
</p>
|
||||
<ul class="reading-list">
|
||||
<li>
|
||||
<a href="../../showcase/notion/"
|
||||
>Follow Notion's clipboard bridge</a
|
||||
>
|
||||
<span>Apply the same approach to a packaged Electron app.</span>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../browser/">Observe a browser export</a
|
||||
><span
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
><span class="brand-caption">Reverse Engineer Anything</span></a
|
||||
>
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="../../showcase/dx-ball/">DX-Ball</a
|
||||
<a href="../../showcase/">Showcases</a
|
||||
><a class="nav-start" href="../../get-started/">Get started</a
|
||||
><a href="../">Guides</a
|
||||
><a href="https://github.com/morluto/rea">GitHub</a>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
><span class="brand-caption">Reverse Engineer Anything</span></a
|
||||
>
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="showcase/dx-ball/">DX-Ball</a>
|
||||
<a href="showcase/">Showcases</a>
|
||||
<a class="nav-start" href="get-started/">Get started</a>
|
||||
<a href="guides/">Guides</a>
|
||||
<a href="https://github.com/morluto/rea">GitHub</a>
|
||||
|
|
@ -34,17 +34,40 @@
|
|||
<p class="eyebrow">Software analysis · CLI & MCP</p>
|
||||
<h1>Find out how<br />software works.</h1>
|
||||
<p class="lede">
|
||||
REA gives your coding agent tools to inspect apps, binaries and
|
||||
runtime behavior. Ask a question, follow the code, and use what you
|
||||
learn in your own project.
|
||||
Use REA with your coding agent to inspect binaries, trace application
|
||||
code and observe runtime behavior.
|
||||
</p>
|
||||
<div class="actions">
|
||||
<a class="button" href="get-started/">Get started</a>
|
||||
<a class="text-link" href="showcase/dx-ball/"
|
||||
>Read the DX-Ball case study</a
|
||||
>
|
||||
<a class="text-link" href="showcase/">Explore the case studies</a>
|
||||
</div>
|
||||
<div class="hero-command">
|
||||
<div class="agent-terminal">
|
||||
<div class="terminal-header">
|
||||
<span class="terminal-title"
|
||||
><span class="terminal-dots" aria-hidden="true"
|
||||
><i></i><i></i><i></i></span
|
||||
>Your coding agent</span
|
||||
>
|
||||
<button
|
||||
class="copy-button"
|
||||
type="button"
|
||||
data-copy="home-install-prompt"
|
||||
aria-label="Copy REA installation prompt"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<p class="terminal-prompt">
|
||||
<span class="prompt-symbol" aria-hidden="true">›</span
|
||||
><span id="home-install-prompt"
|
||||
>Install REA and connect it to this coding agent using npx
|
||||
rea-agents@latest setup. Show me the setup plan for approval,
|
||||
then verify the installation.</span
|
||||
><span class="terminal-cursor" aria-hidden="true"></span>
|
||||
</p>
|
||||
</div>
|
||||
<p class="caption">Or run this command in your terminal:</p>
|
||||
<div class="code-block code-inline">
|
||||
<code id="setup-command">npx rea-agents@latest setup</code>
|
||||
<button
|
||||
|
|
@ -56,9 +79,43 @@
|
|||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<p class="caption">
|
||||
Connects REA to your agent and installs its investigation workflow.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="section-heading">
|
||||
<h2>What REA can analyze</h2>
|
||||
<p>
|
||||
Choose a guide for your binary, JavaScript app or browser session.
|
||||
</p>
|
||||
</div>
|
||||
<div class="workflows">
|
||||
<div>
|
||||
<h3>Native binaries</h3>
|
||||
<p>
|
||||
Inspect functions, strings, references and call relationships in
|
||||
executables and libraries.
|
||||
</p>
|
||||
<a href="guides/native/">Native analysis guide</a>
|
||||
</div>
|
||||
<div>
|
||||
<h3>JavaScript & Electron</h3>
|
||||
<p>
|
||||
Map modules, routes, IPC and native dependencies from an
|
||||
application folder or ASAR archive.
|
||||
</p>
|
||||
<a href="guides/javascript/">Application workflows</a>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Browser & runtime activity</h3>
|
||||
<p>
|
||||
Capture selected browser or process activity, then compare the
|
||||
results across runs.
|
||||
</p>
|
||||
<a href="guides/browser/">Browser observation guide</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
@ -93,7 +150,7 @@
|
|||
<section class="section">
|
||||
<div class="container feature-case">
|
||||
<div>
|
||||
<p class="eyebrow">Case study</p>
|
||||
<p class="eyebrow">Case study · Native</p>
|
||||
<h2>Rebuilding DX-Ball</h2>
|
||||
<p>
|
||||
DX-Ball is a 1996 Windows game. The project is reconstructing it
|
||||
|
|
@ -134,64 +191,44 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="section-heading">
|
||||
<h2>Bring a target. Ask a question.</h2>
|
||||
<p>
|
||||
Start with a local app or file, then narrow the investigation to
|
||||
the feature or behavior you want to understand.
|
||||
</p>
|
||||
</div>
|
||||
<div class="workflows">
|
||||
<div>
|
||||
<h3>Native applications</h3>
|
||||
<p>
|
||||
Inspect functions, strings, references and call relationships in
|
||||
executables and libraries.
|
||||
</p>
|
||||
<a href="guides/native/">Native analysis guide</a>
|
||||
</div>
|
||||
<div>
|
||||
<h3>JavaScript & Electron</h3>
|
||||
<p>
|
||||
Map modules, routes, IPC and native dependencies from an
|
||||
application folder or ASAR archive.
|
||||
</p>
|
||||
<a href="guides/javascript/">Application workflows</a>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Runtime behavior</h3>
|
||||
<p>
|
||||
Capture selected browser or process activity, then compare the
|
||||
results across runs.
|
||||
</p>
|
||||
<a href="guides/browser/">Browser observation guide</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container feature-case">
|
||||
<div>
|
||||
<h2>Keep the investigation in your workflow.</h2>
|
||||
<p class="eyebrow">Case study · Electron</p>
|
||||
<h2>Notion's Electron clipboard</h2>
|
||||
<p>
|
||||
Use REA from your terminal or through your agent's MCP connection.
|
||||
Analysis runs locally, and results include the source locations,
|
||||
evidence and open questions you need to continue.
|
||||
A copy operation crosses from the page to Electron's main process.
|
||||
Inspect Notion's packaged JavaScript with REA, then follow one
|
||||
channel through the wrappers to the system clipboard.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Use your analysis tools</h3>
|
||||
<p>
|
||||
Connect a local analysis provider for native binaries. Static
|
||||
JavaScript analysis runs with Node.js and npm.
|
||||
Short code excerpts show each handoff. A second figure explains
|
||||
how an HTML marker connects ordinary clipboard text to Notion's
|
||||
structured block data.
|
||||
</p>
|
||||
<a class="text-link" href="get-started/#native"
|
||||
>Choose a native provider</a
|
||||
<a class="text-link" href="showcase/notion/"
|
||||
>Follow the clipboard bridge</a
|
||||
>
|
||||
</div>
|
||||
<figure class="workflow-figure case-preview">
|
||||
<p class="figure-title">Clipboard IPC channel</p>
|
||||
<div class="code-block">
|
||||
<div class="code-header"><span>Preload · send</span></div>
|
||||
<pre><code>invokerInMain(
|
||||
<span class="notion-channel">"notion:clipboard:write"</span>
|
||||
)</code></pre>
|
||||
</div>
|
||||
<div class="code-block">
|
||||
<div class="code-header"><span>Main process · receive</span></div>
|
||||
<pre><code>addListener(
|
||||
<span class="notion-channel">"notion:clipboard:write"</span>,
|
||||
handler
|
||||
)</code></pre>
|
||||
</div>
|
||||
<figcaption>
|
||||
Matching names give the investigation a path to follow.
|
||||
</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
@ -200,7 +237,8 @@
|
|||
<div>
|
||||
<h2>What would you like to understand?</h2>
|
||||
<p>
|
||||
Connect your agent, or get your first result from the terminal.
|
||||
Connect REA to your coding agent, or run your first analysis from
|
||||
the terminal.
|
||||
</p>
|
||||
</div>
|
||||
<a class="button" href="get-started/">Get started</a>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
><span class="brand-caption">Reverse Engineer Anything</span></a
|
||||
>
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="./" aria-current="page">DX-Ball</a>
|
||||
<a href="../">Showcases</a>
|
||||
<a class="nav-start" href="../../get-started/">Get started</a>
|
||||
<a href="../../guides/">Guides</a>
|
||||
<a href="https://github.com/morluto/rea">GitHub</a>
|
||||
|
|
@ -31,7 +31,9 @@
|
|||
</header>
|
||||
<main id="main" class="container">
|
||||
<div class="page-heading">
|
||||
<p class="breadcrumb"><a href="../../">Home</a> / DX-Ball</p>
|
||||
<p class="breadcrumb">
|
||||
<a href="../../">Home</a> / <a href="../">Showcases</a> / DX-Ball
|
||||
</p>
|
||||
<p class="eyebrow">Case study · DX-Ball 1.07</p>
|
||||
<h1>Rebuilding DX-Ball<br />from its executable.</h1>
|
||||
<p class="lede">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Case studies — REA</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Follow REA investigations through code and figures: recover a native calculation in DX-Ball and trace Notion's Electron clipboard bridge."
|
||||
/>
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="../assets/styles.css" />
|
||||
<script src="../assets/main.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<a class="brand" href="../" aria-label="REA home"
|
||||
><span class="brand-name">REA</span
|
||||
><span class="brand-caption">Reverse Engineer Anything</span></a
|
||||
>
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="./" aria-current="page">Showcases</a>
|
||||
<a class="nav-start" href="../get-started/">Get started</a>
|
||||
<a href="../guides/">Guides</a>
|
||||
<a href="https://github.com/morluto/rea">GitHub</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<main id="main" class="container">
|
||||
<div class="page-heading">
|
||||
<p class="breadcrumb"><a href="../">Home</a> / Showcases</p>
|
||||
<h1>Follow the<br />investigation.</h1>
|
||||
<p class="lede">
|
||||
Start with a question about a real application. See what REA finds,
|
||||
follow the code, and understand how the pieces fit together.
|
||||
</p>
|
||||
</div>
|
||||
<div class="guide-index">
|
||||
<section class="guide-entry">
|
||||
<p class="eyebrow">Native · Reconstruction</p>
|
||||
<div>
|
||||
<h2>Rebuilding DX-Ball</h2>
|
||||
<p>
|
||||
Recover a sound-pan calculation from a 1996 Windows game. REA's
|
||||
instructions, caller references and numeric constants lead to a
|
||||
small C function, checked against the original behavior and all 63
|
||||
compiled bytes.
|
||||
</p>
|
||||
<div class="guide-route">
|
||||
<span>Executable</span> <b aria-hidden="true">→</b>
|
||||
<span>REA findings</span> <b aria-hidden="true">→</b>
|
||||
<span>Recovered C</span> <b aria-hidden="true">→</b>
|
||||
<span>Validation</span>
|
||||
</div>
|
||||
<a class="text-link" href="dx-ball/">Read the DX-Ball case study</a>
|
||||
</div>
|
||||
</section>
|
||||
<section class="guide-entry">
|
||||
<p class="eyebrow">Electron · Feature tracing</p>
|
||||
<div>
|
||||
<h2>Following a copy through Notion</h2>
|
||||
<p>
|
||||
Trace a page-facing API through Notion's packaged JavaScript.
|
||||
Follow its IPC wrappers to the system clipboard, then see how a
|
||||
small HTML marker keeps rich block data connected to a copy.
|
||||
</p>
|
||||
<div class="guide-route">
|
||||
<span>Packaged app</span> <b aria-hidden="true">→</b>
|
||||
<span>Exposed API</span> <b aria-hidden="true">→</b>
|
||||
<span>IPC wrappers</span> <b aria-hidden="true">→</b>
|
||||
<span>Clipboard</span>
|
||||
</div>
|
||||
<a class="text-link" href="notion/">Read the Notion case study</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<section class="article-section">
|
||||
<h2>Try a smaller example first.</h2>
|
||||
<p>
|
||||
The guides walk through a native calculation, an Electron export and a
|
||||
browser request. Each starts with a prompt and a concrete target.
|
||||
</p>
|
||||
<a class="text-link" href="../guides/">Explore the worked guides</a>
|
||||
</section>
|
||||
</main>
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-inner">
|
||||
<p>REA · Reverse Engineer Anything</p>
|
||||
<div class="footer-links">
|
||||
<a href="https://github.com/morluto/rea">Source</a
|
||||
><a href="https://github.com/morluto/rea/blob/main/LICENSE"
|
||||
>MIT license</a
|
||||
><a href="https://discord.gg/GkcryMnJDM">Community</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<p class="sr-only" id="copy-status" role="status" aria-live="polite"></p>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,477 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Following Notion's clipboard through Electron — REA</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Follow Notion's clipboard API through Electron's preload, IPC wrappers and main process, with short code excerpts and a look at its rich clipboard format."
|
||||
/>
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="../../assets/styles.css" />
|
||||
<script src="../../assets/main.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<a class="brand" href="../../" aria-label="REA home"
|
||||
><span class="brand-name">REA</span
|
||||
><span class="brand-caption">Reverse Engineer Anything</span></a
|
||||
>
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="../">Showcases</a>
|
||||
<a class="nav-start" href="../../get-started/">Get started</a>
|
||||
<a href="../../guides/">Guides</a>
|
||||
<a href="https://github.com/morluto/rea">GitHub</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<main id="main" class="container">
|
||||
<div class="page-heading">
|
||||
<p class="breadcrumb">
|
||||
<a href="../../">Home</a> / <a href="../">Showcases</a> / Notion
|
||||
</p>
|
||||
<p class="eyebrow">Case study · Electron</p>
|
||||
<h1>Following a copy<br />through Notion.</h1>
|
||||
<p class="lede">
|
||||
A familiar action leads through several files. Follow Notion's
|
||||
clipboard API from the page, through Electron, to the system
|
||||
clipboard.
|
||||
</p>
|
||||
<p class="page-meta">Notion Desktop 7.6.1 · JavaScript · REA</p>
|
||||
</div>
|
||||
|
||||
<figure class="workflow-figure">
|
||||
<p class="figure-title">The bridge inside Notion's desktop package</p>
|
||||
<ol class="flow-steps notion-bridge-flow">
|
||||
<li>
|
||||
<span class="flow-label">01 · Page-facing API</span>
|
||||
<span class="flow-node"><code>clipboard.write(data)</code></span>
|
||||
<span class="flow-note"
|
||||
>The preload exposes a callable method.</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="flow-label">02 · Preload wrapper</span>
|
||||
<span class="flow-node"><code>ipcRenderer.invoke</code></span>
|
||||
<span class="flow-note"
|
||||
>The wrapper sends the channel and arguments.</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="flow-label">03 · Main-process handler</span>
|
||||
<span class="flow-node"><code>ipcMain.handle</code></span>
|
||||
<span class="flow-note"
|
||||
>The receiver checks which page sent it.</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="flow-label">04 · System clipboard</span>
|
||||
<span class="flow-node"><code>clipboard.write(data)</code></span>
|
||||
<span class="flow-note"
|
||||
>Electron writes the supplied text and HTML.</span
|
||||
>
|
||||
</li>
|
||||
</ol>
|
||||
<p class="flow-result">
|
||||
One channel joins the two sides:
|
||||
<code class="notion-channel">notion:clipboard:write</code>
|
||||
</p>
|
||||
<figcaption>
|
||||
The packaged code uses the same channel in the preload and main
|
||||
process. Resolving the wrappers reveals the Electron calls behind
|
||||
those names.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
<div class="article-layout">
|
||||
<nav class="contents" aria-label="On this page">
|
||||
<p>In this case study</p>
|
||||
<ul>
|
||||
<li><a href="#clipboard">Follow the clipboard bridge</a></li>
|
||||
<li><a href="#rich-clipboard">Keeping a block's structure</a></li>
|
||||
<li><a href="#markdown">Markdown becomes blocks</a></li>
|
||||
<li><a href="#continue">Try another question</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<article class="article" aria-label="Notion Electron case study">
|
||||
<section class="article-section" id="clipboard">
|
||||
<h2>Where does the copy operation go?</h2>
|
||||
<p>
|
||||
The page sees a method called <code>clipboard.write</code>. Its
|
||||
implementation in the preload is a wrapper. The useful question is
|
||||
what that wrapper sends, and which code receives it.
|
||||
</p>
|
||||
<div class="agent-terminal">
|
||||
<div class="terminal-header">
|
||||
<span class="terminal-title"
|
||||
><span class="terminal-dots" aria-hidden="true"
|
||||
><i></i><i></i><i></i></span
|
||||
>Your coding agent</span
|
||||
><button
|
||||
class="copy-button"
|
||||
type="button"
|
||||
data-copy="notion-prompt"
|
||||
aria-label="Copy Notion analysis prompt"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<p class="terminal-prompt">
|
||||
<span class="prompt-symbol" aria-hidden="true">›</span
|
||||
><span id="notion-prompt"
|
||||
>Use REA to trace how Notion copies text and HTML to the
|
||||
system clipboard. Show the code at each step.</span
|
||||
><span class="terminal-cursor" aria-hidden="true"></span>
|
||||
</p>
|
||||
</div>
|
||||
<p class="caption">
|
||||
An example prompt with Notion's local application package
|
||||
available to your coding agent.
|
||||
</p>
|
||||
|
||||
<h3 class="small-heading">REA finds where to start reading</h3>
|
||||
<p>
|
||||
Analyzing the tab preload with REA locates the API exposed to the
|
||||
page and the call that sends an IPC request. Both findings come
|
||||
with source ranges in the minified bundle:
|
||||
</p>
|
||||
<figure class="workflow-figure">
|
||||
<p class="figure-title">REA findings → two places to open</p>
|
||||
<div class="rea-query-pair">
|
||||
<div class="code-block">
|
||||
<div class="code-header"><span>Exposed page API</span></div>
|
||||
<pre><code>api_key: "__electronApi"
|
||||
api_status: "dynamic"
|
||||
source: preload.js:1:22764–22828</code></pre>
|
||||
</div>
|
||||
<div class="code-block">
|
||||
<div class="code-header"><span>Renderer IPC call</span></div>
|
||||
<pre><code>operation: "invoke"
|
||||
channel_expression: "e"
|
||||
source: preload.js:1:6486–6522</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<figcaption>
|
||||
Selected fields from a fresh
|
||||
<code>analyze_javascript_application</code> result on Notion's
|
||||
tab preload, recorded with REA 4.1.0. Positions use the original
|
||||
bundle's line and zero-based column.
|
||||
</figcaption>
|
||||
</figure>
|
||||
<p>
|
||||
The agent opens those locations, finds the clipboard member in the
|
||||
assembled API, and reads <code>invokerInMain</code>. Its call site
|
||||
supplies
|
||||
<code class="notion-channel">notion:clipboard:write</code>. That
|
||||
name gives the agent something precise to look for in the main
|
||||
bundle. The four excerpts below show the route it connects.
|
||||
</p>
|
||||
|
||||
<h3 class="small-heading">Four short pieces of code</h3>
|
||||
<p class="caption">
|
||||
Simplified from the packaged JavaScript, with readable variable
|
||||
names. Unrelated members are omitted.
|
||||
</p>
|
||||
<ol class="rea-queries">
|
||||
<li>
|
||||
<h4>Start at the API exposed to the page</h4>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span>tab_browser_view/preload.js · API member</span
|
||||
><button
|
||||
class="copy-button"
|
||||
type="button"
|
||||
data-copy="notion-preload-source"
|
||||
aria-label="Copy Notion preload excerpt"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre><code id="notion-preload-source">const clipboardApi = {
|
||||
write: invokerInMain(<span class="notion-channel">"notion:clipboard:write"</span>)
|
||||
};
|
||||
|
||||
// Included in the API exposed through contextBridge:
|
||||
// window.__electronApi.clipboard</code></pre>
|
||||
</div>
|
||||
<p>
|
||||
This member supplies the channel name. The implementation of
|
||||
<code>invokerInMain</code> tells us how the request leaves the
|
||||
page's process.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<h4>Resolve the preload's wrapper</h4>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span>tab_browser_view/preload.js · IPC helper</span
|
||||
><button
|
||||
class="copy-button"
|
||||
type="button"
|
||||
data-copy="notion-invoke-source"
|
||||
aria-label="Copy Notion invoke wrapper excerpt"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre><code id="notion-invoke-source">function invokerInMain(channel) {
|
||||
return (...args) => invokeInMainAndReturnResult(channel, ...args);
|
||||
}
|
||||
|
||||
function invokeInMainAndReturnResult(channel, ...args) {
|
||||
return ipcRenderer.invoke(channel, ...args);
|
||||
}</code></pre>
|
||||
</div>
|
||||
<p>
|
||||
The returned function forwards the arguments through
|
||||
Electron's request-and-response IPC. We can now look for
|
||||
<code class="notion-channel">notion:clipboard:write</code>
|
||||
on the main-process side.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<h4>Check how the receiver is registered</h4>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span>main/index.js · handler helper</span
|
||||
><button
|
||||
class="copy-button"
|
||||
type="button"
|
||||
data-copy="notion-handler-source"
|
||||
aria-label="Copy Notion handler wrapper excerpt"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre><code id="notion-handler-source">const handleRequestFromRenderer = {
|
||||
addListener(channel, handler) {
|
||||
ipcMain.handle(channel, handler);
|
||||
}
|
||||
};</code></pre>
|
||||
</div>
|
||||
<p>
|
||||
The main process uses another wrapper. Its
|
||||
<code>addListener</code> method registers an
|
||||
<code>ipcMain.handle</code> receiver for that channel.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<h4>Find the write and its sender check</h4>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span>main/index.js · clipboard receiver</span
|
||||
><button
|
||||
class="copy-button"
|
||||
type="button"
|
||||
data-copy="notion-write-source"
|
||||
aria-label="Copy Notion clipboard receiver excerpt"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre><code id="notion-write-source">handleRequestFromRenderer.addListener(
|
||||
<span class="notion-channel">"notion:clipboard:write"</span>,
|
||||
(event, data) =>
|
||||
isNotionWebContents(event.sender)
|
||||
? clipboard.write(data)
|
||||
: undefined
|
||||
);</code></pre>
|
||||
</div>
|
||||
<p>
|
||||
This is the destination. After checking the sender, the
|
||||
handler passes the supplied data to Electron's clipboard API.
|
||||
The channel and the two resolved wrappers connect the exposed
|
||||
method to this call.
|
||||
</p>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="article-section" id="rich-clipboard">
|
||||
<h2>How does a copied block keep its structure?</h2>
|
||||
<p>
|
||||
The cached web editor adds a small detail to programmatic copies.
|
||||
It sends text and HTML through
|
||||
<code>electronApi.clipboard.write</code>, while keeping Notion's
|
||||
private block data in a local virtual clipboard. An ID in the HTML
|
||||
connects the two.
|
||||
</p>
|
||||
<figure class="workflow-figure">
|
||||
<p class="figure-title">One copy, two representations</p>
|
||||
<div class="notion-clipboard-grid">
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span>Sent to the system clipboard</span>
|
||||
</div>
|
||||
<pre><code>text: "Project notes"
|
||||
html: "<p>Project notes</p>"
|
||||
+ "<!-- notionvc: <span class="notion-channel"><copy-id></span> -->"</code></pre>
|
||||
</div>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span>Kept in the local virtual clipboard</span>
|
||||
</div>
|
||||
<pre><code>id: <span class="notion-channel"><copy-id></span>
|
||||
mimeTypes:
|
||||
text/_notion-blocks-v3-production
|
||||
→ structured block data</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<ol
|
||||
class="notion-mini-flow"
|
||||
aria-label="Restoring rich clipboard data on paste"
|
||||
>
|
||||
<li>Read the HTML ID</li>
|
||||
<li>Find the same local ID</li>
|
||||
<li>Restore private formats</li>
|
||||
</ol>
|
||||
<figcaption>
|
||||
Illustrative text and ID. The saved module probe recovered the
|
||||
private formats when IDs matched; a stale entry left only the
|
||||
ordinary text and HTML.
|
||||
</figcaption>
|
||||
</figure>
|
||||
<p>
|
||||
On paste, the reader looks for the marker and checks it against
|
||||
the saved copy. A matching ID reconnects the text to its richer
|
||||
representation. This explains why the HTML carries more than
|
||||
visible content.
|
||||
</p>
|
||||
<p class="caption">
|
||||
Web assets cached on July 13, 2026. The desktop-package bridge
|
||||
above and this web-side format are separate pieces of the
|
||||
investigation.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="article-section" id="markdown">
|
||||
<h2>From Markdown to structured blocks</h2>
|
||||
<p>
|
||||
The same investigation also recovered Notion's Markdown
|
||||
processing. A small table makes the internal format easy to see:
|
||||
its rows and cells become data that the block renderer can use.
|
||||
</p>
|
||||
<figure class="workflow-figure">
|
||||
<p class="figure-title">
|
||||
A table through Notion's Markdown parser
|
||||
</p>
|
||||
<div class="notion-table-example">
|
||||
<div class="code-block">
|
||||
<div class="code-header"><span>Markdown input</span></div>
|
||||
<pre><code>| A | B |
|
||||
| - | - |
|
||||
| 1 | 2 |</code></pre>
|
||||
</div>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span>Structured parser output · excerpt</span>
|
||||
</div>
|
||||
<pre><code>table · headerRow: true
|
||||
table_row
|
||||
cells: ["A"], ["B"]
|
||||
table_row
|
||||
cells: ["1"], ["2"]</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<figcaption>
|
||||
A saved extracted-module probe of web parser
|
||||
<code>422457</code>. Both page and chat filters preserve this
|
||||
table. The tree shows the recorded rows and cell text.
|
||||
</figcaption>
|
||||
</figure>
|
||||
<p>
|
||||
Finding that transformation required separating the ordinary
|
||||
paste/import path from the structured Markdown and AI/chat
|
||||
parsers. Each has its own entry point and intermediate format.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="article-section" id="continue">
|
||||
<h2>Try another question</h2>
|
||||
<p>
|
||||
Start with one feature and follow the code that crosses a process
|
||||
boundary. A named API, a literal channel and its receiver are
|
||||
useful places to begin.
|
||||
</p>
|
||||
<details class="evidence-details">
|
||||
<summary>Another path: the offline SQLite connection</summary>
|
||||
<p>
|
||||
Notion's renderer requests <code>getSqliteMeta</code> through
|
||||
IPC. The main process creates a message channel and gives one
|
||||
port to the renderer and the other to a SQLite utility process.
|
||||
Subsequent statement batches and results travel over that port.
|
||||
</p>
|
||||
<p>
|
||||
Following this handoff explains where the database work runs,
|
||||
and why tracing only the initial IPC request misses the later
|
||||
conversation.
|
||||
</p>
|
||||
</details>
|
||||
<details class="evidence-details">
|
||||
<summary>Sources and how to read the excerpts</summary>
|
||||
<p>
|
||||
The clipboard bridge comes from Notion Desktop 7.6.1's
|
||||
<code>app.asar</code>: the tab preload and
|
||||
<code>main/index.js</code>. The excerpts expand minified
|
||||
variable names and omit unrelated code; the sender-check helper
|
||||
is shown with its underlying
|
||||
<code>isNotionWebContents</code> call.
|
||||
</p>
|
||||
<p>
|
||||
The virtual clipboard and Markdown examples come from web assets
|
||||
cached on July 13, 2026, including modules <code>747001</code>,
|
||||
<code>58169</code>, <code>736810</code> and <code>422457</code>.
|
||||
Those assets have their own version. Saved probes executed
|
||||
extracted modules and small harnesses; their outputs describe
|
||||
those probes rather than a captured desktop UI session.
|
||||
</p>
|
||||
<p>
|
||||
The REA result shown here is a focused analysis of the saved tab
|
||||
preload, recorded with the published REA 4.1.0 package. It
|
||||
locates the API and invoke operation. The clipboard member and
|
||||
matching receiver are connected by reading their wrapper
|
||||
implementations in the package.
|
||||
</p>
|
||||
</details>
|
||||
<ul class="reading-list">
|
||||
<li>
|
||||
<a href="../../guides/javascript/"
|
||||
>JavaScript application guide</a
|
||||
><span
|
||||
>Follow a small Electron application's export feature.</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../dx-ball/">DX-Ball reconstruction</a
|
||||
><span
|
||||
>Follow native instructions to a checked C function.</span
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="actions">
|
||||
<a class="button" href="../../get-started/"
|
||||
>Start with your own target</a
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-inner">
|
||||
<p>REA · Reverse Engineer Anything</p>
|
||||
<div class="footer-links">
|
||||
<a href="https://github.com/morluto/rea">Source</a
|
||||
><a href="https://github.com/morluto/rea/blob/main/LICENSE"
|
||||
>MIT license</a
|
||||
><a href="https://discord.gg/GkcryMnJDM">Community</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<p class="sr-only" id="copy-status" role="status" aria-live="polite"></p>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue