2026-05-11 17:07:22 -04:00

Aether Native Launcher (Electron)

The Aether Native Launcher is a specialized Electron-based shell for the Aether Platform. It provides a secure bridge between the SvelteKit web UI and the local operating system, enabling features restricted by browser sandboxing.

🚀 Overview

This application serves as the "Native Mode" runtime for Aether podiums and devices. It handles:

  • Local File Orchestration: Managed cache for presentation files (PPTx, Keynote, PDF).
  • Automation: Specialized AppleScript handlers for PowerPoint and Keynote.
  • Hardware Telemetry: Direct access to CPU, RAM, and Network interface data.
  • Remote Control: Slide navigation and application control via WebSocket intents.

Launcher Terminology

Use these terms consistently when working on the launcher bridge:

  • Launch Profiles: the Svelte-side map keyed by file extension.
  • Launch Profile: one resolved config object selected from that map for a file.
  • Native Template: the single AppleScript or shell command string Electron executes after the file has been copied to temp. This is an implementation detail of the bridge.

In short, the profiles are the policy; the launch profile is the selected policy entry; the native template is the executable string produced by that policy.

Do not use launch_scripts as the public/config-facing term. If that wording appears in old comments or generated output, treat it as stale naming drift and update it to launch_profiles when referring to the Svelte-side map or native_template when referring to the resolved string.

🖥️ Onsite Deployment

Current hardware: MacBook Air 2018 — Intel x64. All current deployments use aether_launcher-darwin-x64. Future hardware: Apple Silicon Macs use aether_launcher-darwin-arm64. Windows and Linux builds are planned.

SSH user on all laptops: speaker ready IP pattern: 192.168.32.1XX (XX = zero-padded laptop number, e.g. 03 → .103). Find/replace this prefix for other onsite environments.

Deploy files live in deploy/:

File Purpose
deploy/deploy.sh Deploy script — handles arch detection, scp, and seed.json
deploy/devices.conf Laptop list: number, IP, event_device_id
deploy/event.env Gitignored — per-event API key and URLs (create from example)
deploy/event.env.example Template for event.env

Step 1 — Build the app (workstation)

cd ~/OSIT_dev/aether_app_native_electron
npm run package:mac
# Produces builds/aether_launcher-darwin-x64/aether_launcher.app  ← the one to deploy

Only rebuild if source code has changed. The .app bundle is identical for all Intel laptops — only ~/seed.json differs per device.

Note: The build will print WARNING: Could not find icon "..." with extension ".icon". This is cosmetic — electron-packager checks for several icon format variants and warns for each it doesn't find. The .icns file is correctly embedded as electron.icns inside the app bundle.

Step 2 — Create event.env

cp deploy/event.env.example deploy/event.env
# Edit deploy/event.env — fill in AETHER_API_KEY

Create the API key in the Aether admin panel before the show (Core → Accounts or Events → Devices API key section). All laptops share one key per event. Delete it after the show.

Step 3 — Run the deploy script

# Deploy specific laptops:
./deploy/deploy.sh 01 02 03

# Deploy all laptops in devices.conf:
./deploy/deploy.sh all

# Update seed.json only (no .app copy — e.g. when rotating the API key):
./deploy/deploy.sh --seed-only all

# Deploy and re-grant Accessibility permission in one pass:
./deploy/deploy.sh --fix-accessibility all

The script auto-detects each Mac's CPU architecture, copies the correct .app build, writes seed.json, and verifies. One SSH connection failure won't abort the batch — it logs and continues, then reports which laptops need a retry.

Step 4 — Verify and launch

After the script completes, launch the app on each laptop and confirm it connects and shows the correct device name in the Launcher UI.

macOS Accessibility Permission

The launcher sends keystrokes to PowerPoint and Keynote via AppleScript. macOS requires explicit Accessibility access for this. Every time a new .app binary is deployed, macOS invalidates the stored permission because the code signature changes — even when rsync updates in-place.

Symptom: Slide control silently fails; osascript eventually returns a permissions error.

Manual fix (GUI — always works):

  1. System Settings → Privacy & Security → Accessibility
  2. Find aether_launcher in the list → remove it ( button)
  3. Re-add it (+ button → /Applications/aether_launcher.app) and toggle it on

Automated fix — use the --fix-accessibility deploy flag:

./deploy/deploy.sh --fix-accessibility 01 02 03
./deploy/deploy.sh --build --fix-accessibility all

This runs tccutil reset (no password required) then attempts a direct TCC database grant via sudo sqlite3. The sqlite3 step requires NOPASSWD sudo on each Mac — one-time setup:

ssh "speaker ready"@192.168.32.1XX "sudo visudo -f /etc/sudoers.d/aether-tcc"

Add this line, then save:

speaker ready ALL=(ALL) NOPASSWD: /usr/bin/sqlite3 /Library/Application\ Support/com.apple.TCC/TCC.db *

If the sqlite3 grant fails (SIP enabled, sudoers not configured), the script logs a warning and falls back gracefully — a fresh permission prompt appears the first time the app uses accessibility. The --fix-accessibility flag can be combined with any other flag.

Long-term fix: Code-sign the app with an Apple Developer certificate. A stable signature means macOS never invalidates the permission on updates. Currently out of scope.

Adding SSH key to a new laptop (first time only)

ssh-copy-id "speaker ready"@192.168.32.1XX

Run once per laptop before deploying.


Manual deploy reference

The script covers the normal case. For one-off fixes or if the script isn't available:

# Detect arch
ssh "speaker ready"@192.168.32.103 "uname -m"
# x86_64 → darwin-x64  |  arm64 → darwin-arm64

# Copy .app (Intel example):
scp -r builds/aether_launcher-darwin-x64/aether_launcher.app \
    "speaker ready"@192.168.32.103:/Applications/aether_launcher.app

# Write seed.json:
ssh "speaker ready"@192.168.32.103 "cat > ~/seed.json" << 'EOF'
{
  "event_device_id": "DEVICE_ID_FOR_THIS_LAPTOP",
  "aether_api_key": "YOUR_API_KEY",
  "primary_api_base_url": "https://api.oneskyit.com",
  "backup_api_base_url": "https://bak-api.oneskyit.com",
  "onsite_api_base_url": null
}
EOF

# Verify:
ssh "speaker ready"@192.168.32.103 "cat ~/seed.json"

event_device_id values by laptop — see the Device Reference table below.


📋 Device Reference

Laptop IP Address event_device_id Notes
01 192.168.32.101 tFLL1fLQfnk
02 192.168.32.102 rpbfunVPEzw
03 192.168.32.103 1EPfPX8kfw8
04 192.168.32.104 zvgyLM5yieU
05 192.168.32.105 QOc046GoeSc
06 192.168.32.106 2o8j6eb0L6s
07 192.168.32.107 Oa1tlxPEVSQ
08 192.168.32.108 fY4yznpUZ48
09 192.168.32.109 YlgGCyjo9bY
10 192.168.32.110 GcTnFsp1mHI
11 192.168.32.111 6z88m9oEZio
12 192.168.32.112 EggJqL2kWkA
13 192.168.32.113 O11eckHFdVE
14 192.168.32.114 reI0SecUEhI
15 192.168.32.115 crozxT8mA44
16 192.168.32.116 0nP4VZsvr2Q
17 192.168.32.117 Gm2gNqPGzLA
19 192.168.32.119 6tpukvRVugU (no laptop 18)
x20 192.168.32.120 rwLYnKUNd1M old 04, spare/retired

aether_api_key: all laptops share a single key per event deployment. The key is created in the Aether admin panel before the show and deleted after. Check builds/seed.json for the current key, or create a new one in Aether (Core → Accounts or Events → Devices API key section) before each deployment.


⚙️ Configuration

The application requires a seed.json file to identify the device and connect to the Aether API.

1. Seed Configuration

Location: ~/seed.json (user's home directory — external to the app bundle by design)

This file is intentionally kept outside the application bundle so it can be edited per-device without re-signing or repackaging the app. On macOS this is /Users/speaker ready/seed.json.

Seed file format:

{
  "event_device_id": "tFLL1fLQfnk",
  "aether_api_key": "YOUR_API_KEY",
  "primary_api_base_url": "https://api.oneskyit.com",
  "backup_api_base_url": "https://bak-api.oneskyit.com",
  "onsite_api_base_url": null
}

event_device_id is the id_random from the Aether event_device record for that physical laptop — see the Device Reference table above. aether_api_key is a shared key created per event deployment and deleted after the show.

2. Development Setup

npm install
npm start        # Compiles TypeScript (tsc) then launches Electron

3. File Cache Layout

Presentation files are cached locally under hash_prefix_length-char subdirectories (default: 2):

[local_file_cache_path]/
  4a/
    4a228ef8ac1a...sha256hash...file
  1d/
    1d720916a831...sha256hash...file

Important: hash_prefix_length must be consistent. If it changes, files in old directories become orphaned and will be re-downloaded. The default is 2 and should not be changed unless explicitly coordinated across all devices.

🌉 The Native Bridge (aetherNative)

The bridge is exposed to the renderer via contextBridge. It can be accessed in the web UI via window.aetherNative.

Design principle: The Electron app is a thin OS primitive layer. Business logic (which script runs for which file type, how to sequence operations, etc.) belongs in the SvelteKit/Svelte side where it can be changed without a rebuild and redeployment. Electron handlers should rarely need to change.

File Cache

Method Description
check_cache({cache_root, hash, hash_prefix_length?, verify_hash?}) Checks if a file exists in the hashed cache. verify_hash: true re-hashes the file to confirm integrity.
download_to_cache({url, cache_root, hash, api_key, account_id, hash_prefix_length?}) Streams a file from the API into the hashed cache. Verifies SHA-256 integrity before finalizing.
copy_from_cache_to_temp({cache_root, hash, temp_root, filename, hash_prefix_length?}) Preferred primitive. Copies cached file to temp dir with original filename. Returns { success, path }. The Svelte caller decides what to do next.
launch_from_cache({cache_root, hash, temp_root, filename, hash_prefix_length?, native_template?}) Combines copy + launch. The Svelte side resolves the Launch Profile to a single native_template string (AppleScript or shell: prefixed command). If no template is supplied, it returns an error.

Shell & OS

Method Description
run_cmd({cmd, timeout?, return_stdout?}) Async shell command execution.
run_cmd_sync({cmd}) Synchronous shell command execution.
run_osascript(script) Hardened. Runs AppleScript via temp .scpt file — handles multi-line scripts and paths with spaces/special characters correctly. macOS only.
open_folder(path) Opens a directory in Finder / system file manager.
open_local_file_v2(path) Opens a file with its default OS application.
open_external({url, app?}) Opens a URL in Chrome, Firefox, or the default browser.
kill_processes({process_name_li}) Terminates processes by name. macOS/Linux: pkill -f. Windows: taskkill /F.

Presentations (Phase 5 — legacy specialized handlers)

Method Description
launch_presentation({path, app?, os?}) Platform-aware launcher. Resolves [home]/[tmp] placeholders. Hardened (2026-05-11): AppleScript written to temp .scpt file, same as run_osascript. For new flows prefer copy_from_cache_to_temp + run_osascript for full control.
control_presentation({app, action}) Slide navigation (next/prev/start/stop) for PowerPoint or Keynote via AppleScript. macOS only.

System Management (Phase 5)

Method Description
get_device_config() Returns hydrated device config injected at startup from seed.json + API.
get_device_info() Returns OS metadata: platform, hostname, IPs, CPU count, free RAM, home/tmp paths.
window_control({action, value?}) Electron window: maximize, minimize, restore, close, fullscreen, kiosk, devtools, reload.
set_wallpaper({path?, url?, url_external?, display?, api_key?, account_id?}) Sets desktop wallpaper. Accepts a local path or downloads from url (cached to ~/Library/Caches/OSIT/wallpaper/). url_external sets a separate image on the projector/second display. display: 'all' (default) | 'primary' | 'external'. macOS only in production; Linux returns a dev-mode preview payload without applying.
power_control({action}) Shutdown, reboot, or sleep. macOS + Linux. Requires sudo for shutdown/reboot.
set_display_layout({mode, configStr?}) Mirror/extend displays. macOS only. Idempotent — returns success immediately if the displays are already in the requested state (no flicker). Primary path: bundled display_control binary (native CoreGraphics, no Homebrew). Fallback: displayplacer — used when the binary is absent or when a configStr override is set. configStr is a full displayplacer string stored in event_device.data_json for per-device tuning. Build display_control via scripts/build-display-control.sh on a Mac and commit resources/bin/display_control.
list_display_modes() Returns all online displays with every usable CGDisplayMode per display: logical size, pixel size, refresh rate, HiDPI flag, and which mode is currently active. macOS only. JSON is parsed for you — result is { success, displays: DisplayInfo[] }. Use this to populate a resolution picker in the UI.
set_display_mode({display_index, width, height, refresh_rate?, hidpi?}) Sets the resolution/refresh of one display via CGConfigureDisplayWithDisplayMode. display_index matches the index from list_display_modes. hidpi: true = force HiDPI (Retina scaling), false = force non-HiDPI, null/omit = auto (prefers HiDPI on built-in, non-HiDPI on externals). Picks the highest available refresh rate when multiple modes match. macOS only.
manage_recording({action, options?}) Screen recording via bundled aperture binary. macOS only.
update_app(args) Stub. Downloads update package but does not install. Not functional.
list_tools() Returns a self-describing manifest of all available bridge functions.

Example Usage (preferred composable pattern)

import * as native from '$lib/electron/electron_relay';

// Step 1: copy the cached file to temp and get the resolved path
const copy = await native.copy_from_cache_to_temp({
    cache_root: $ae_loc.local_file_cache_path,
    hash: event_file_obj.hash_sha256,
    temp_root: $ae_loc.host_file_temp_path,
    filename: event_file_obj.filename
});
if (!copy.success) { /* handle error */ return; }

// Step 2: run whatever script/command you want with that path
// Option A — AppleScript (macOS):
await native.run_osascript(`
    tell application "Microsoft PowerPoint"
        activate
        open (POSIX file "${copy.path}")
        delay 3
    end tell
`);

// Option B — shell command:
await native.run_cmd({ cmd: `open "${copy.path}"` });

// Option C — use a template from device config (data-driven, no rebuild needed):
const template = $ae_loc.native_device?.launch_profiles?.pptx;
if (template) {
    const script = template.replace(/\{\{path\}\}/g, copy.path);
    await native.run_osascript(script);
}

Configurable Launch Profiles (no rebuild needed)

launch_profiles is the Svelte-side map stored in event_device.data_json.launch_profiles. launch_from_cache receives the resolved native_template string, not the profile map. Keys are lowercase extensions (pptx, key, pdf, etc.); default is a catch-all. Profiles use {{path}} as the file path placeholder. AppleScript strings run via run_osascript; prefix with shell: for shell commands.

See documentation/PROJECT__AE_Events_Launcher_Native_integration.md Section 8 for full details.

🛠️ Development

  • Preload: Logic defined in src/preload/index.ts.
  • Handlers: OS-level logic in src/main/shell_handlers.ts, src/main/file_handlers.ts, and src/main/system_handlers.ts.
  • Types: Shared TypeScript interfaces in src/shared/types.ts.

One-Time: Build the display_control Binary (macOS)

The display_control binary provides native CoreGraphics display mirror/extend without any Homebrew dependency. It must be compiled on a Mac (Xcode CLT required) and committed to the repo so it is bundled into every packaged app. Produces a universal binary (x86_64 + arm64) that runs on both Intel and Apple Silicon Macs.

Rebuild only if scripts/display_control.m changes.

Option A — From the Linux workstation (preferred)

Laptop 01 (192.168.32.101) is the designated build Mac — Xcode CLT is installed there. The remote build script copies the source over SSH, compiles on that Mac, and pulls the binary back.

# From the repo root on the workstation:
./scripts/remote-build-display-control.sh

# Override the build Mac IP if needed:
./scripts/remote-build-display-control.sh 192.168.32.102

The script prints file resources/bin/display_control at the end. Confirm both arches appear:

resources/bin/display_control: Mach-O universal binary with 2 architectures: [x86_64] [arm64]

Then commit:

git add resources/bin/display_control
git commit -m "build: update display_control binary (universal)"

Option B — Directly on a Mac

If you have repo access on the Mac itself (Xcode CLT required — xcode-select --install):

./scripts/build-display-control.sh
# Expected last output line: x86_64 arm64

# Test with a second display connected:
./resources/bin/display_control status
./resources/bin/display_control extend
./resources/bin/display_control mirror
./resources/bin/display_control list-modes
./resources/bin/display_control set-mode 0 1920 1080
./resources/bin/display_control set-mode 1 1920 1080 --refresh 60 --no-hidpi

git add resources/bin/display_control
git commit -m "build: update display_control binary (universal)"

Once committed, brew install displayplacer is no longer required on any venue Mac. The displayplacer fallback remains in the handler only for configStr per-device overrides.


Build Requirements (System Dependencies)

bsdtar (libarchive) must be present on the build host. It is used by the postinstall patch to work around a hard hang in @electron/packager 20 on Node 26.

OS Install
Arch Linux sudo pacman -S libarchive
macOS brew install libarchive (or use the system bsdtar included in Xcode CLT)
Ubuntu/Debian sudo apt install libarchive-tools

Why bsdtar — Node 26 Packaging Bug

Symptom: npm run package:mac or npm run package:linux prints Packaging app for platform ... using electron v42.x then hangs forever (or exits 0 with no output).

Root cause: yauzl 2.10.0 (used by extract-zip inside @electron/packager 20) creates read streams that never emit data events under Node 26. The zip extraction call blocks indefinitely.

Fix: scripts/patch-packager-unzip.js (run automatically via postinstall) replaces the extractElectronZip function in node_modules/@electron/packager/dist/unzip.js with a child_process.execSync call to bsdtar -xf. bsdtar was chosen over 7z because 7z refuses to extract macOS .app bundles that contain symlinks chained through other symlinks (e.g. Electron Framework.framework/Libraries → Versions/Current/Libraries, where Versions/Current is itself a symlink).

This patch is re-applied automatically on every npm install via the postinstall hook. If a future release of @electron/packager or extract-zip fixes Node 26 compatibility, remove the postinstall line from package.json and delete scripts/patch-packager-unzip.js.

Description
No description provided
Readme 4.3 MiB
Languages
CSS 44.2%
JavaScript 40.4%
HTML 15.4%