From 0431e3172054e4d18691a97a98b39821db04fa1a Mon Sep 17 00:00:00 2001 From: John James Jacoby Date: Tue, 1 Sep 2026 16:57:39 -0500 Subject: [PATCH 1/2] Add verified local WebKit preview launcher --- docs/LOCAL_WEBKIT_PREVIEW.md | 16 ++++ package.json | 3 +- scripts/run-with-local-webkit.sh | 133 +++++++++++++++++++++++++++ scripts/test-local-webkit-preview.js | 31 +++++++ 4 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 docs/LOCAL_WEBKIT_PREVIEW.md create mode 100755 scripts/run-with-local-webkit.sh create mode 100644 scripts/test-local-webkit-preview.js diff --git a/docs/LOCAL_WEBKIT_PREVIEW.md b/docs/LOCAL_WEBKIT_PREVIEW.md new file mode 100644 index 00000000..0c0d5d30 --- /dev/null +++ b/docs/LOCAL_WEBKIT_PREVIEW.md @@ -0,0 +1,16 @@ +# Local WebKit preview + +Pasted can run against a locally built WebKit on macOS without changing its ordinary build, release artifacts, or dependency graph. This path is intended for WebKit development and before/after demonstrations; it is not a distributable Pasted build. + +Build WebKit, install Pasted's locked dependencies, then pass the WebKit build-products directory to the preview launcher: + +```sh +npm ci +npm run dev:local-webkit -- /absolute/path/to/WebKitBuild/Release +``` + +The launcher builds Pasted's normal debug executable, starts Vite on loopback, and launches the executable directly with framework and XPC loader paths pointed at the supplied build. It uses `vmmap` to verify that the Pasted process actually loaded that `WebKit.framework`; a successful application launch without that verification is treated as a failure. + +Quit Pasted to stop the preview and its Vite server. Set `PASTED_LOCAL_WEBKIT_SKIP_BUILD=1` to reuse the existing debug executable during repeated recording sessions. + +The launcher does not bundle WebKit, modify Pasted's release configuration, or replace the system framework. The preview still opens the ordinary local Pasted library, so use a privacy-safe screen such as Settings for recordings unless the library was prepared specifically for public demonstration. diff --git a/package.json b/package.json index 88001d1f..e7ebd874 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "dev": "vite", + "dev:local-webkit": "bash scripts/run-with-local-webkit.sh", "build": "tsc && vite build", "preview": "vite preview", "test": "cargo test --manifest-path src-tauri/Cargo.toml", @@ -29,7 +30,7 @@ "test:transformations": "node --experimental-strip-types scripts/test-manual-transform-step-model.ts && node scripts/audit-transformations.js", "test:activity": "node scripts/audit-activity-events.js", "test:backup": "node --experimental-strip-types scripts/test-backup-client-state.js && node --experimental-strip-types scripts/test-app-ui-state.ts && node scripts/audit-full-backup.js", - "test:platform": "node scripts/audit-platform-chrome.js", + "test:platform": "node scripts/test-local-webkit-preview.js && node scripts/audit-platform-chrome.js", "test:feedback": "node scripts/audit-capture-feedback.js", "test:app-lock": "node --experimental-strip-types scripts/test-app-lock-idle.ts", "bench:analysis": "cargo run --release --manifest-path src-tauri/Cargo.toml --example analysis_baseline --", diff --git a/scripts/run-with-local-webkit.sh b/scripts/run-with-local-webkit.sh new file mode 100755 index 00000000..e1e17a7c --- /dev/null +++ b/scripts/run-with-local-webkit.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: npm run dev:local-webkit -- /absolute/path/to/WebKitBuild/Release + +Builds the Pasted debug application, starts its Vite frontend, and launches the +application directly with a local WebKit build selected for both the app and its +XPC services. The script exits unless vmmap confirms that Pasted loaded that +WebKit.framework instead of the system framework. + +Set PASTED_LOCAL_WEBKIT_SKIP_BUILD=1 to reuse an existing debug application. +EOF +} + +if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then + usage + exit 0 +fi + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "A local WebKit preview can only run on macOS." >&2 + exit 1 +fi + +if [[ $# -ne 1 ]]; then + usage >&2 + exit 1 +fi + +webkit_build="$1" +if [[ "$webkit_build" != /* ]]; then + echo "Pass an absolute WebKit build directory." >&2 + exit 1 +fi + +webkit_binary="$webkit_build/WebKit.framework/Versions/A/WebKit" +if [[ ! -f "$webkit_binary" ]]; then + echo "WebKit.framework was not found at $webkit_build." >&2 + exit 1 +fi + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +app_binary="$repo_root/src-tauri/target/debug/pasted-app" +vite_pid="" +app_pid="" + +cleanup() { + local status=$? + trap - EXIT INT TERM HUP + if [[ -n "$app_pid" ]] && kill -0 "$app_pid" 2>/dev/null; then + kill "$app_pid" 2>/dev/null || true + wait "$app_pid" 2>/dev/null || true + fi + if [[ -n "$vite_pid" ]] && kill -0 "$vite_pid" 2>/dev/null; then + kill "$vite_pid" 2>/dev/null || true + wait "$vite_pid" 2>/dev/null || true + fi + exit "$status" +} +trap cleanup EXIT INT TERM HUP + +cd "$repo_root" + +if [[ "${PASTED_LOCAL_WEBKIT_SKIP_BUILD:-0}" != "1" ]]; then + if [[ ! -d node_modules ]]; then + echo "Install the locked frontend dependencies with npm ci first." >&2 + exit 1 + fi + cargo build --locked --manifest-path src-tauri/Cargo.toml --bin pasted-app +fi + +if [[ ! -x "$app_binary" ]]; then + echo "Pasted's debug application was not found at $app_binary." >&2 + exit 1 +fi + +npm run dev -- --host 127.0.0.1 & +vite_pid=$! + +vite_ready=false +for _ in {1..100}; do + if curl --silent --fail --output /dev/null http://127.0.0.1:1420/; then + vite_ready=true + break + fi + if ! kill -0 "$vite_pid" 2>/dev/null; then + wait "$vite_pid" + fi + sleep 0.1 +done + +if [[ "$vite_ready" != "true" ]]; then + echo "Vite did not become ready at http://127.0.0.1:1420/." >&2 + exit 1 +fi + +env \ + DYLD_FRAMEWORK_PATH="$webkit_build" \ + __XPC_DYLD_FRAMEWORK_PATH="$webkit_build" \ + DYLD_LIBRARY_PATH="$webkit_build" \ + __XPC_DYLD_LIBRARY_PATH="$webkit_build" \ + "$app_binary" & +app_pid=$! + +verified_path="" +alternate_build="${webkit_build#/private}" +for _ in {1..100}; do + if ! kill -0 "$app_pid" 2>/dev/null; then + wait "$app_pid" + fi + loaded_webkit="$(vmmap "$app_pid" 2>/dev/null | sed -n '/\/WebKit\.framework\/.*\/WebKit$/p' || true)" + if [[ "$loaded_webkit" == *"$webkit_build/WebKit.framework/"* \ + || "$loaded_webkit" == *"$alternate_build/WebKit.framework/"* ]]; then + verified_path="$loaded_webkit" + break + fi + sleep 0.1 +done + +if [[ -z "$verified_path" ]]; then + echo "Pasted launched, but vmmap did not confirm the requested WebKit.framework." >&2 + exit 1 +fi + +echo "Verified local WebKit.framework:" +echo "$verified_path" +echo "Quit Pasted to stop the preview and its Vite server." + +wait "$app_pid" +app_pid="" diff --git a/scripts/test-local-webkit-preview.js b/scripts/test-local-webkit-preview.js new file mode 100644 index 00000000..325971f2 --- /dev/null +++ b/scripts/test-local-webkit-preview.js @@ -0,0 +1,31 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import { spawnSync } from 'node:child_process'; + +const scriptPath = 'scripts/run-with-local-webkit.sh'; +const source = fs.readFileSync(scriptPath, 'utf8'); + +const syntax = spawnSync('bash', ['-n', scriptPath], { encoding: 'utf8' }); +assert.equal(syntax.status, 0, syntax.stderr); + +const help = spawnSync('bash', [scriptPath, '--help'], { encoding: 'utf8' }); +assert.equal(help.status, 0, help.stderr); +assert.match(help.stdout, /WebKitBuild\/Release/); +assert.match(help.stdout, /vmmap confirms/); + +for (const variable of [ + 'DYLD_FRAMEWORK_PATH', + '__XPC_DYLD_FRAMEWORK_PATH', + 'DYLD_LIBRARY_PATH', + '__XPC_DYLD_LIBRARY_PATH', +]) { + assert.match(source, new RegExp(`\\b${variable}=`), `${variable} must remain in the preview process environment`); +} + +assert.match(source, /cargo build --locked/, 'The preview must build from the reviewed Rust lockfile'); +assert.match(source, /npm run dev -- --host 127\.0\.0\.1/, 'The preview must bind Vite to loopback'); +assert.match(source, /vmmap "\$app_pid"/, 'The preview must inspect the launched Pasted process'); +assert.match(source, /kill "\$app_pid"/, 'Interrupted previews must stop the launched application'); +assert.match(source, /kill "\$vite_pid"/, 'Interrupted previews must stop the Vite server'); + +console.log('Local WebKit preview launcher checks passed.'); From 390172bec44254b6cdb9522bdb70211ea4fb1611 Mon Sep 17 00:00:00 2001 From: John James Jacoby Date: Tue, 1 Sep 2026 18:41:42 -0500 Subject: [PATCH 2/2] Isolate local WebKit preview data --- docs/LOCAL_WEBKIT_PREVIEW.md | 6 +++--- scripts/run-with-local-webkit.sh | 31 ++++++++++++++++++++++++---- scripts/test-local-webkit-preview.js | 6 ++++++ src-tauri/src/app_runtime.rs | 16 +++++++++++++- src-tauri/src/clipboard_monitor.rs | 3 ++- 5 files changed, 53 insertions(+), 9 deletions(-) diff --git a/docs/LOCAL_WEBKIT_PREVIEW.md b/docs/LOCAL_WEBKIT_PREVIEW.md index 0c0d5d30..c6446313 100644 --- a/docs/LOCAL_WEBKIT_PREVIEW.md +++ b/docs/LOCAL_WEBKIT_PREVIEW.md @@ -9,8 +9,8 @@ npm ci npm run dev:local-webkit -- /absolute/path/to/WebKitBuild/Release ``` -The launcher builds Pasted's normal debug executable, starts Vite on loopback, and launches the executable directly with framework and XPC loader paths pointed at the supplied build. It uses `vmmap` to verify that the Pasted process actually loaded that `WebKit.framework`; a successful application launch without that verification is treated as a failure. +The launcher builds Pasted's normal debug executable and CLI, starts Vite on loopback, and launches the executable directly with framework and XPC loader paths pointed at the supplied build. It uses `vmmap` to verify that the Pasted process actually loaded that `WebKit.framework`; a successful application launch without that verification is treated as a failure. -Quit Pasted to stop the preview and its Vite server. Set `PASTED_LOCAL_WEBKIT_SKIP_BUILD=1` to reuse the existing debug executable during repeated recording sessions. +For recording safety, the launcher creates and seeds a temporary demonstration database, starts the preview with clipboard capture paused, and deletes that temporary database when the preview exits. It never opens or modifies the ordinary Pasted library. Quit Pasted to stop the preview and its Vite server. Set `PASTED_LOCAL_WEBKIT_SKIP_BUILD=1` to reuse the existing debug executables during repeated recording sessions. -The launcher does not bundle WebKit, modify Pasted's release configuration, or replace the system framework. The preview still opens the ordinary local Pasted library, so use a privacy-safe screen such as Settings for recordings unless the library was prepared specifically for public demonstration. +The launcher does not bundle WebKit, modify Pasted's release configuration, replace the system framework, or alter the release build's database selection. The database override is compiled only into debug builds. diff --git a/scripts/run-with-local-webkit.sh b/scripts/run-with-local-webkit.sh index e1e17a7c..d166f5c5 100755 --- a/scripts/run-with-local-webkit.sh +++ b/scripts/run-with-local-webkit.sh @@ -9,7 +9,8 @@ Usage: npm run dev:local-webkit -- /absolute/path/to/WebKitBuild/Release Builds the Pasted debug application, starts its Vite frontend, and launches the application directly with a local WebKit build selected for both the app and its XPC services. The script exits unless vmmap confirms that Pasted loaded that -WebKit.framework instead of the system framework. +WebKit.framework instead of the system framework. It uses a temporary, seeded +database and pauses clipboard capture so private Pasted history cannot appear. Set PASTED_LOCAL_WEBKIT_SKIP_BUILD=1 to reuse an existing debug application. EOF @@ -44,8 +45,12 @@ fi repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" app_binary="$repo_root/src-tauri/target/debug/pasted-app" +cli_binary="$repo_root/src-tauri/target/debug/pasted" +preview_parent="${TMPDIR:-/tmp}" +preview_parent="${preview_parent%/}" vite_pid="" app_pid="" +preview_root="" cleanup() { local status=$? @@ -58,6 +63,9 @@ cleanup() { kill "$vite_pid" 2>/dev/null || true wait "$vite_pid" 2>/dev/null || true fi + if [[ -n "$preview_root" && "$preview_root" == "$preview_parent/pasted-local-webkit."* ]]; then + rm -rf -- "$preview_root" + fi exit "$status" } trap cleanup EXIT INT TERM HUP @@ -70,13 +78,27 @@ if [[ "${PASTED_LOCAL_WEBKIT_SKIP_BUILD:-0}" != "1" ]]; then exit 1 fi cargo build --locked --manifest-path src-tauri/Cargo.toml --bin pasted-app + cargo build --locked --manifest-path src-tauri/Cargo.toml --no-default-features --features cli --bin pasted fi -if [[ ! -x "$app_binary" ]]; then - echo "Pasted's debug application was not found at $app_binary." >&2 +if [[ ! -x "$app_binary" || ! -x "$cli_binary" ]]; then + echo "Pasted's debug application and CLI were not found in src-tauri/target/debug." >&2 exit 1 fi +preview_root="$(mktemp -d "$preview_parent/pasted-local-webkit.XXXXXX")" +preview_database="$preview_root/pasted.db" +preview_seed="$preview_root/demo-clips.csv" +printf '%s\n' \ + 'id,content_type,source,is_pinned,created_at,name,text_content' \ + '1,"text","Pasted Demo",true,"2026-09-01T18:00:00Z","Release engineering","Ship it, but make the pixels behave first."' \ + '2,"text","Pasted Demo",false,"2026-09-01T18:01:00Z","WebKit field notes","Yellow and cyan have agreed to remain perfectly still."' \ + '3,"text","Pasted Demo",false,"2026-09-01T18:02:00Z","Important research","Can a window corner outrun a layout viewport? Not anymore."' \ + '4,"text","Pasted Demo",false,"2026-09-01T18:03:00Z","Extremely official memo","This clipboard contains zero secrets and several excellent rectangles."' \ + > "$preview_seed" +PASTED_DATABASE_PATH="$preview_database" \ + "$cli_binary" clip import "$preview_seed" --format csv >/dev/null + npm run dev -- --host 127.0.0.1 & vite_pid=$! @@ -102,7 +124,8 @@ env \ __XPC_DYLD_FRAMEWORK_PATH="$webkit_build" \ DYLD_LIBRARY_PATH="$webkit_build" \ __XPC_DYLD_LIBRARY_PATH="$webkit_build" \ - "$app_binary" & + PASTED_PREVIEW_DATABASE_PATH="$preview_database" \ + "$app_binary" --skip-welcome & app_pid=$! verified_path="" diff --git a/scripts/test-local-webkit-preview.js b/scripts/test-local-webkit-preview.js index 325971f2..dfd69e9c 100644 --- a/scripts/test-local-webkit-preview.js +++ b/scripts/test-local-webkit-preview.js @@ -12,6 +12,7 @@ const help = spawnSync('bash', [scriptPath, '--help'], { encoding: 'utf8' }); assert.equal(help.status, 0, help.stderr); assert.match(help.stdout, /WebKitBuild\/Release/); assert.match(help.stdout, /vmmap confirms/); +assert.match(help.stdout, /temporary, seeded/); for (const variable of [ 'DYLD_FRAMEWORK_PATH', @@ -23,8 +24,13 @@ for (const variable of [ } assert.match(source, /cargo build --locked/, 'The preview must build from the reviewed Rust lockfile'); +assert.match(source, /--no-default-features --features cli --bin pasted/, 'The preview must build the CLI used to seed its isolated database'); assert.match(source, /npm run dev -- --host 127\.0\.0\.1/, 'The preview must bind Vite to loopback'); assert.match(source, /vmmap "\$app_pid"/, 'The preview must inspect the launched Pasted process'); +assert.match(source, /mktemp -d/, 'The preview must isolate its demonstration database in a temporary directory'); +assert.match(source, /PASTED_PREVIEW_DATABASE_PATH="\$preview_database"/, 'The GUI must use the isolated preview database'); +assert.match(source, /PASTED_DATABASE_PATH="\$preview_database"/, 'The CLI must seed the same isolated preview database'); +assert.match(source, /rm -rf -- "\$preview_root"/, 'The temporary preview database must be removed during cleanup'); assert.match(source, /kill "\$app_pid"/, 'Interrupted previews must stop the launched application'); assert.match(source, /kill "\$vite_pid"/, 'Interrupted previews must stop the Vite server'); diff --git a/src-tauri/src/app_runtime.rs b/src-tauri/src/app_runtime.rs index 13624710..250a0473 100644 --- a/src-tauri/src/app_runtime.rs +++ b/src-tauri/src/app_runtime.rs @@ -6,6 +6,16 @@ use tauri::{Emitter, Manager}; static EXIT_REQUESTED: AtomicBool = AtomicBool::new(false); +#[cfg(debug_assertions)] +fn preview_database_path() -> Option { + std::env::var_os("PASTED_PREVIEW_DATABASE_PATH").map(Into::into) +} + +#[cfg(not(debug_assertions))] +fn preview_database_path() -> Option { + None +} + pub(crate) fn exit_requested() -> bool { EXIT_REQUESTED.load(Ordering::SeqCst) } @@ -68,7 +78,10 @@ pub(crate) fn setup(app: &mut tauri::App) -> Result<(), Box Result<(), Box, seq_state: Arc, ocr_service: Arc, + initially_paused: bool, ) -> MonitorHandle { let running = Arc::new(AtomicBool::new(true)); let running_clone = running.clone(); - let is_manually_paused = Arc::new(AtomicBool::new(false)); + let is_manually_paused = Arc::new(AtomicBool::new(initially_paused)); let is_auto_paused = Arc::new(AtomicBool::new(false)); let is_manually_paused_clone = is_manually_paused.clone();