fix: repair file_handlers.ts backtick escaping; update README
file_handlers.ts had AI-generated escaped backticks (\`) and template
literal dollar signs (\${) throughout, causing TypeScript compile errors
("Invalid character", "Unterminated template literal"). Fixed all 15
affected lines.
README updated: corrected seed config path from resources/seed_config.json
to ~/seed.json (external to app bundle by design), added explanation of
why it's kept external (no re-signing needed per device), and documented
the 2-char hash prefix cache layout with consistency warning.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
33
README.md
33
README.md
@@ -12,10 +12,16 @@ This application serves as the "Native Mode" runtime for Aether podiums and devi
|
|||||||
|
|
||||||
## ⚙️ Configuration
|
## ⚙️ Configuration
|
||||||
|
|
||||||
The application requires a `seed_config.json` file to identify the device and connect to the Aether API.
|
The application requires a `seed.json` file to identify the device and connect to the Aether API.
|
||||||
|
|
||||||
### 1. Seed Configuration
|
### 1. Seed Configuration
|
||||||
Create/edit `resources/seed_config.json`:
|
|
||||||
|
**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/<username>/seed.json`.
|
||||||
|
|
||||||
|
Create/edit `~/seed.json`:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"event_device_id": "YOUR_DEVICE_ID",
|
"event_device_id": "YOUR_DEVICE_ID",
|
||||||
@@ -25,13 +31,30 @@ Create/edit `resources/seed_config.json`:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Environment Setup
|
> **Note:** The `event_device_id` and `aether_api_key` values come from the Aether admin panel
|
||||||
|
> (Events → Devices). Each physical device gets its own record and key.
|
||||||
|
|
||||||
|
### 2. Development Setup
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm start # Compiles TypeScript (tsc) then launches Electron
|
||||||
npm start
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 3. File Cache Layout
|
||||||
|
|
||||||
|
Presentation files are cached locally under `hash_prefix_length`-char subdirectories (default: 2):
|
||||||
|
```text
|
||||||
|
[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 Native Bridge (`aetherNative`)
|
||||||
|
|
||||||
The bridge is exposed to the renderer via `contextBridge`. It can be accessed in the web UI via `window.aetherNative`.
|
The bridge is exposed to the renderer via `contextBridge`. It can be accessed in the web UI via `window.aetherNative`.
|
||||||
|
|||||||
45
dist/main/file_handlers.js
vendored
45
dist/main/file_handlers.js
vendored
@@ -41,6 +41,7 @@ const electron_1 = require("electron");
|
|||||||
const fs = __importStar(require("fs"));
|
const fs = __importStar(require("fs"));
|
||||||
const path = __importStar(require("path"));
|
const path = __importStar(require("path"));
|
||||||
const os = __importStar(require("os"));
|
const os = __importStar(require("os"));
|
||||||
|
const crypto = __importStar(require("crypto"));
|
||||||
const child_process_1 = require("child_process");
|
const child_process_1 = require("child_process");
|
||||||
const axios_1 = __importDefault(require("axios"));
|
const axios_1 = __importDefault(require("axios"));
|
||||||
const file_utils_1 = require("./file_utils");
|
const file_utils_1 = require("./file_utils");
|
||||||
@@ -55,18 +56,44 @@ function registerFileHandlers() {
|
|||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
return path.join(dir, `${hash}.file`);
|
return path.join(dir, `${hash}.file`);
|
||||||
}
|
}
|
||||||
electron_1.ipcMain.handle('native:check-cache', async (event, { cache_root, hash, hash_prefix_length = 2 }) => {
|
electron_1.ipcMain.handle('native:check-cache', async (event, { cache_root, hash, hash_prefix_length = 2, verify_hash = false }) => {
|
||||||
const full_path = get_organized_hashed_path(cache_root, hash, hash_prefix_length);
|
const full_path = get_organized_hashed_path(cache_root, hash, hash_prefix_length);
|
||||||
return fs.existsSync(full_path);
|
if (!fs.existsSync(full_path))
|
||||||
|
return false;
|
||||||
|
if (verify_hash) {
|
||||||
|
try {
|
||||||
|
const file_buffer = fs.readFileSync(full_path);
|
||||||
|
const actual_hash = crypto.createHash('sha256').update(file_buffer).digest('hex');
|
||||||
|
return actual_hash === hash;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
electron_1.ipcMain.handle('native:download-to-cache', async (event, { url, cache_root, hash, api_key, account_id, hash_prefix_length = 2 }) => {
|
electron_1.ipcMain.handle('native:download-to-cache', async (event, { url, cache_root, hash, api_key, account_id, hash_prefix_length = 2 }) => {
|
||||||
const full_path = get_organized_hashed_path(cache_root, hash, hash_prefix_length);
|
const full_path = get_organized_hashed_path(cache_root, hash, hash_prefix_length);
|
||||||
const tmp_path = `${full_path}.tmp`;
|
const tmp_path = `${full_path}.tmp`;
|
||||||
if (endpoints_in_progress.includes(url))
|
if (endpoints_in_progress.includes(url))
|
||||||
return { success: true, status: 'in_progress' };
|
return { success: true, status: 'in_progress' };
|
||||||
|
// 1. If final file exists, skip
|
||||||
if (fs.existsSync(full_path))
|
if (fs.existsSync(full_path))
|
||||||
return { success: true, path: full_path, status: 'exists' };
|
return { success: true, path: full_path, status: 'exists' };
|
||||||
console.log(`Native: Organized Download (${hash_prefix_length} chars) -> ${full_path}`);
|
// 2. Handle stale .tmp files (Legacy "Trust No One" pattern)
|
||||||
|
if (fs.existsSync(tmp_path)) {
|
||||||
|
const stats = fs.statSync(tmp_path);
|
||||||
|
const age_ms = Date.now() - stats.mtimeMs;
|
||||||
|
// If the tmp file is older than 5 minutes, assume previous download crashed and delete it
|
||||||
|
if (age_ms > 5 * 60 * 1000) {
|
||||||
|
console.log(`Native: Deleting stale temp file (${Math.round(age_ms / 1000)}s old)`);
|
||||||
|
fs.unlinkSync(tmp_path);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return { success: true, status: 'in_progress', detail: 'fresh_tmp_exists' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`Native: Hardened Download -> ${full_path}`);
|
||||||
try {
|
try {
|
||||||
endpoints_in_progress.push(url);
|
endpoints_in_progress.push(url);
|
||||||
const response = await (0, axios_1.default)({
|
const response = await (0, axios_1.default)({
|
||||||
@@ -83,7 +110,16 @@ function registerFileHandlers() {
|
|||||||
writer.on('finish', () => resolve());
|
writer.on('finish', () => resolve());
|
||||||
writer.on('error', reject);
|
writer.on('error', reject);
|
||||||
});
|
});
|
||||||
|
// 3. Verify Integrity before renaming (The "Trust No One" Check)
|
||||||
|
const file_buffer = fs.readFileSync(tmp_path);
|
||||||
|
const actual_hash = crypto.createHash('sha256').update(file_buffer).digest('hex');
|
||||||
|
if (actual_hash !== hash) {
|
||||||
|
console.error(`Native: Hash Mismatch! Expected ${hash}, got ${actual_hash}`);
|
||||||
|
fs.unlinkSync(tmp_path);
|
||||||
|
return { success: false, error: 'Integrity check failed: Hash mismatch' };
|
||||||
|
}
|
||||||
fs.renameSync(tmp_path, full_path);
|
fs.renameSync(tmp_path, full_path);
|
||||||
|
console.log(`Native: Cache Integrity Verified. File moved to final destination.`);
|
||||||
return { success: true, path: full_path };
|
return { success: true, path: full_path };
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
@@ -146,7 +182,8 @@ function registerFileHandlers() {
|
|||||||
if (script) {
|
if (script) {
|
||||||
console.log(`Native: Launching ${ext} via AppleScript for ${target}`);
|
console.log(`Native: Launching ${ext} via AppleScript for ${target}`);
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
(0, child_process_1.exec)(`osascript -e "${script.replace(/"/g, '\\\\"')}"`, (err) => {
|
const escapedScript = script.trim().replace(/"/g, '\\\\\\"').replace(/\\n/g, ' -e "') + '"';
|
||||||
|
(0, child_process_1.exec)(`osascript -e ${escapedScript}`, (err) => {
|
||||||
if (err)
|
if (err)
|
||||||
resolve({ success: false, error: err.message });
|
resolve({ success: false, error: err.message });
|
||||||
else
|
else
|
||||||
|
|||||||
2
dist/main/file_handlers.js.map
vendored
2
dist/main/file_handlers.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -16,7 +16,7 @@ export function registerFileHandlers() {
|
|||||||
const prefix = hash.substring(0, Math.max(1, Math.min(prefix_len, 8)));
|
const prefix = hash.substring(0, Math.max(1, Math.min(prefix_len, 8)));
|
||||||
const dir = path.join(expanded_root, prefix);
|
const dir = path.join(expanded_root, prefix);
|
||||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||||
return path.join(dir, \`\${hash}.file\`);
|
return path.join(dir, `${hash}.file`);
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.handle('native:check-cache', async (event, { cache_root, hash, hash_prefix_length = 2, verify_hash = false }) => {
|
ipcMain.handle('native:check-cache', async (event, { cache_root, hash, hash_prefix_length = 2, verify_hash = false }) => {
|
||||||
@@ -39,7 +39,7 @@ export function registerFileHandlers() {
|
|||||||
|
|
||||||
ipcMain.handle('native:download-to-cache', async (event, { url, cache_root, hash, api_key, account_id, hash_prefix_length = 2 }) => {
|
ipcMain.handle('native:download-to-cache', async (event, { url, cache_root, hash, api_key, account_id, hash_prefix_length = 2 }) => {
|
||||||
const full_path = get_organized_hashed_path(cache_root, hash, hash_prefix_length);
|
const full_path = get_organized_hashed_path(cache_root, hash, hash_prefix_length);
|
||||||
const tmp_path = \`\${full_path}.tmp\`;
|
const tmp_path = `${full_path}.tmp`;
|
||||||
|
|
||||||
if (endpoints_in_progress.includes(url)) return { success: true, status: 'in_progress' };
|
if (endpoints_in_progress.includes(url)) return { success: true, status: 'in_progress' };
|
||||||
|
|
||||||
@@ -52,14 +52,14 @@ export function registerFileHandlers() {
|
|||||||
const age_ms = Date.now() - stats.mtimeMs;
|
const age_ms = Date.now() - stats.mtimeMs;
|
||||||
// If the tmp file is older than 5 minutes, assume previous download crashed and delete it
|
// If the tmp file is older than 5 minutes, assume previous download crashed and delete it
|
||||||
if (age_ms > 5 * 60 * 1000) {
|
if (age_ms > 5 * 60 * 1000) {
|
||||||
console.log(\`Native: Deleting stale temp file (\${Math.round(age_ms/1000)}s old)\`);
|
console.log(`Native: Deleting stale temp file (${Math.round(age_ms/1000)}s old)`);
|
||||||
fs.unlinkSync(tmp_path);
|
fs.unlinkSync(tmp_path);
|
||||||
} else {
|
} else {
|
||||||
return { success: true, status: 'in_progress', detail: 'fresh_tmp_exists' };
|
return { success: true, status: 'in_progress', detail: 'fresh_tmp_exists' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(\`Native: Hardened Download -> \${full_path}\`);
|
console.log(`Native: Hardened Download -> ${full_path}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
endpoints_in_progress.push(url);
|
endpoints_in_progress.push(url);
|
||||||
@@ -85,13 +85,13 @@ export function registerFileHandlers() {
|
|||||||
const actual_hash = crypto.createHash('sha256').update(file_buffer).digest('hex');
|
const actual_hash = crypto.createHash('sha256').update(file_buffer).digest('hex');
|
||||||
|
|
||||||
if (actual_hash !== hash) {
|
if (actual_hash !== hash) {
|
||||||
console.error(\`Native: Hash Mismatch! Expected \${hash}, got \${actual_hash}\`);
|
console.error(`Native: Hash Mismatch! Expected ${hash}, got ${actual_hash}`);
|
||||||
fs.unlinkSync(tmp_path);
|
fs.unlinkSync(tmp_path);
|
||||||
return { success: false, error: 'Integrity check failed: Hash mismatch' };
|
return { success: false, error: 'Integrity check failed: Hash mismatch' };
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.renameSync(tmp_path, full_path);
|
fs.renameSync(tmp_path, full_path);
|
||||||
console.log(\`Native: Cache Integrity Verified. File moved to final destination.\`);
|
console.log(`Native: Cache Integrity Verified. File moved to final destination.`);
|
||||||
return { success: true, path: full_path };
|
return { success: true, path: full_path };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (fs.existsSync(tmp_path)) fs.unlinkSync(tmp_path);
|
if (fs.existsSync(tmp_path)) fs.unlinkSync(tmp_path);
|
||||||
@@ -107,7 +107,7 @@ export function registerFileHandlers() {
|
|||||||
const expanded_temp = expandPath(temp_root);
|
const expanded_temp = expandPath(temp_root);
|
||||||
const target = path.join(expanded_temp, filename);
|
const target = path.join(expanded_temp, filename);
|
||||||
|
|
||||||
console.log(\`Native: Launching from Cache -> \${filename}\`);
|
console.log(`Native: Launching from Cache -> ${filename}`);
|
||||||
|
|
||||||
if (!fs.existsSync(expanded_temp)) fs.mkdirSync(expanded_temp, { recursive: true });
|
if (!fs.existsSync(expanded_temp)) fs.mkdirSync(expanded_temp, { recursive: true });
|
||||||
|
|
||||||
@@ -121,9 +121,9 @@ export function registerFileHandlers() {
|
|||||||
// 3. Optimized Launch (LibreOffice / AppleScript)
|
// 3. Optimized Launch (LibreOffice / AppleScript)
|
||||||
if (is_pres) {
|
if (is_pres) {
|
||||||
if (os.platform() === 'linux') {
|
if (os.platform() === 'linux') {
|
||||||
console.log(\`Native: Launching LibreOffice (--impress) for \${target}\`);
|
console.log(`Native: Launching LibreOffice (--impress) for ${target}`);
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
exec(\`libreoffice --impress "\${target}"\`, (err) => {
|
exec(`libreoffice --impress "${target}"`, (err) => {
|
||||||
if (err) resolve({ success: false, error: err.message });
|
if (err) resolve({ success: false, error: err.message });
|
||||||
else resolve({ success: true });
|
else resolve({ success: true });
|
||||||
});
|
});
|
||||||
@@ -133,30 +133,30 @@ export function registerFileHandlers() {
|
|||||||
if (os.platform() === 'darwin') {
|
if (os.platform() === 'darwin') {
|
||||||
let script = '';
|
let script = '';
|
||||||
if (ext === 'key') {
|
if (ext === 'key') {
|
||||||
script = \`
|
script = `
|
||||||
tell application "Keynote"
|
tell application "Keynote"
|
||||||
activate
|
activate
|
||||||
open (POSIX file "\${target}")
|
open (POSIX file "${target}")
|
||||||
delay 1
|
delay 1
|
||||||
start (front document)
|
start (front document)
|
||||||
end tell
|
end tell
|
||||||
\`;
|
`;
|
||||||
} else if (ext === 'pptx' || ext === 'ppt') {
|
} else if (ext === 'pptx' || ext === 'ppt') {
|
||||||
script = \`
|
script = `
|
||||||
tell application "Microsoft PowerPoint"
|
tell application "Microsoft PowerPoint"
|
||||||
activate
|
activate
|
||||||
open (POSIX file "\${target}")
|
open (POSIX file "${target}")
|
||||||
delay 1
|
delay 1
|
||||||
run slide show of active presentation
|
run slide show of active presentation
|
||||||
end tell
|
end tell
|
||||||
\`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (script) {
|
if (script) {
|
||||||
console.log(\`Native: Launching \${ext} via AppleScript for \${target}\`);
|
console.log(`Native: Launching ${ext} via AppleScript for ${target}`);
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const escapedScript = script.trim().replace(/"/g, '\\\\\\"').replace(/\\n/g, ' -e "') + '"';
|
const escapedScript = script.trim().replace(/"/g, '\\\\\\"').replace(/\\n/g, ' -e "') + '"';
|
||||||
exec(\`osascript -e \${escapedScript}\`, (err) => {
|
exec(`osascript -e ${escapedScript}`, (err) => {
|
||||||
if (err) resolve({ success: false, error: err.message });
|
if (err) resolve({ success: false, error: err.message });
|
||||||
else resolve({ success: true });
|
else resolve({ success: true });
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user