1 Commits

Author SHA1 Message Date
peji f9ec212406 Publish GitHub Pages site 2026-05-23 00:50:40 +00:00
25 changed files with 326 additions and 9713 deletions
-37
View File
@@ -1,37 +0,0 @@
name: Deploy GitHub Pages
on:
push:
branches: [master]
paths:
- 'docs/**'
- '.github/workflows/deploy-pages.yml'
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure Pages
uses: actions/configure-pages@v5
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
-28
View File
@@ -1,28 +0,0 @@
# Dependencies
node_modules/
# Build output
dist/
# External executables (not bundled via npm)
aware-cam-proxy-win.exe
aware-cam-proxy.exe
# Reference documents
*.pdf
# OS files
Thumbs.db
Desktop.ini
.DS_Store
# Editor
.vscode/
*.swp
*.swo
# Claude Code
.claude/
# Windows null device artifact
nul
-112
View File
@@ -1,112 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Alta Proxy Tool (APT) — an Electron desktop app that authenticates with Avigilon Alta Video deployments via a companion Chrome extension, discovers cameras, and launches `aware-cam-proxy.exe` to establish camera connections. Authentication uses cookie import from Chrome — no username/password login flow. Windows-only due to the proxy executable.
## Repository
- **GitHub**: https://github.com/PageZ948/Alta-Proxy-Tool (private)
- **Branch**: master
- **Git identity**: Zac <zpage948@gmail.com> (repo-local config)
## Commands
```bash
npm start # Run the app
npm run dev # Run with DevTools open (--dev flag)
npm run build # Build portable Windows .exe (output: dist/)
npm run build-test # Build to directory without packaging
```
No test framework is configured. No linter is configured.
## Architecture
This is a vanilla Electron app (no React/Vue/framework). Core files:
```
main.js → Electron main process: IPC handlers, API calls (axios),
cookie proxy process spawning, local HTTP cookie server
preload.js → contextBridge exposing window.electronAPI with IPC wrappers
renderer.js → All UI logic: DOM manipulation, state management, event handlers
index.html → Static HTML shell, no inline scripts (CSP enforced)
styles.css → Dark theme using CSS custom properties
```
A companion Chrome extension lives in `chrome-extension/`:
```
chrome-extension/
manifest.json → Manifest V3, cookies + activeTab permissions
popup.html → Extension popup UI
popup.css → Dark theme matching the Electron app
popup.js → Tab detection, cookie retrieval, POST to localhost
icon*.png → Placeholder icons
```
### Authentication Flow
There is no login form or profile system. Authentication works exclusively through the Chrome extension cookie bridge:
1. User logs into Alta deployment in Chrome
2. Clicks the Chrome extension popup → "Send Cookie to APT"
3. Extension POSTs `{deploymentUrl, cookieValue}` to `http://127.0.0.1:18247/cookie` with `X-APT-Token` header
4. `main.js` HTTP server validates and forwards via IPC push to renderer
5. `renderer.js` `handleExtensionCookie()` sets session state, auto-populates cookie key, fetches devices
The extension is loaded unpacked via `chrome://extensions/` → Developer mode → Load unpacked → select `chrome-extension/`.
### IPC Communication Pattern
Most cross-process communication follows the request/response pattern:
1. `main.js` registers handler: `ipcMain.handle('channel-name', async (event, params) => { ... })`
2. `preload.js` exposes it: `channelName: (params) => ipcRenderer.invoke('channel-name', params)`
3. `renderer.js` calls it: `const result = await window.electronAPI.channelName(params)`
All handlers return `{ success: boolean, message?: string, ...data }`.
There is one **push-pattern** channel for the Chrome extension cookie bridge:
- `main.js` sends: `mainWindow.webContents.send('extension-cookie-received', data)`
- `preload.js` bridges: `ipcRenderer.on('extension-cookie-received', callback)`
- `renderer.js` listens via `window.electronAPI.onExtensionCookie(callback)`
### IPC Channels
| Channel | Purpose |
|---------|---------|
| `api-get-devices` | GET /api/v1/devices with cookie auth |
| `api-get-auth-info` | GET /api/v1/auth to verify session |
| `camera-proxy-cookie-launch` | Spawns aware-cam-proxy.exe (cookie method) |
| `camera-proxy-stop` | Kills all proxy processes via taskkill/powershell |
| `extension-cookie-received` | Push channel: cookie data from Chrome extension → renderer |
### State Management (renderer.js)
All connection state lives in the `sessionData` object (deploymentUrl, cookies, isConnected). There is no separate `isConnected` flag — always use `sessionData.isConnected`.
Active cookie proxy processes are tracked in `activeCookieProxyConnections` Map, keyed by device GUID.
### Security Model
- Context isolation enabled, nodeIntegration disabled
- CSP meta tag: `script-src 'self'` — no inline scripts or onclick handlers allowed
- Batch file inputs are sanitized via `sanitizeBatchInput()` to prevent command injection
- Local HTTP cookie server (port 18247) bound to `127.0.0.1` only
- Cookie server validates: shared token header, CORS restricted to `chrome-extension://` origins, deployment URL must be `*.avasecurity.com` or `*.avigilon.com` over HTTPS, type/length limits on all inputs, 64KB body size limit
## Key Conventions
- No inline event handlers in HTML — all use `addEventListener` in renderer.js
- All user-provided content rendered to DOM must go through `escapeHtml()` (XSS prevention)
- External processes spawned with `detached: true` + `unref()` so they survive if the app closes
- Device list filters out cloud cameras (`capabilities.localStorage === false` only)
- `clearDeviceList()` must NOT clear proxy connection Maps (proxies may still be running)
## External Executable
- `aware-cam-proxy.exe` — cookie-based auth proxy (required)
Not bundled via npm. Must be in the app root directory. Gitignored along with `*.pdf`, `node_modules/`, and `dist/`.
-186
View File
@@ -1,186 +0,0 @@
# Alta Video Camera Proxy
An Electron desktop application for managing Alta Video camera proxy connections. Authenticates via a companion Chrome extension that imports your existing Alta session cookie, discovers cameras, and launches proxy connections.
## Features
- **Chrome Extension Authentication**: Import your Alta session cookie from Chrome with one click — no manual login
- **API Integration**: Device discovery via Alta Video API using cookie auth
- **Camera Proxy Management**: Launch and manage camera proxy connections
- **Device Filtering**: Automatically filters to show only local (non-cloud) cameras
- **Device Search**: Quick search functionality to find cameras by name, ID, IP, or model
- **Real-time Status**: Live connection status and device online/offline indicators
- **Auto-Update**: Checks for updates on startup via GitHub Releases, with one-click in-place update
- **Modern Dark UI**: Professional dark-mode interface with responsive design
## Prerequisites
- **Node.js** (version 14 or higher)
- **npm** (comes with Node.js)
- **Google Chrome** (for the authentication extension)
- **Windows OS** (required for camera proxy executable)
- **aware-cam-proxy.exe** (camera proxy executable) — must be placed in the application directory
- **An active Alta Video session** in Chrome (logged in to your deployment)
## Installation
1. Clone or download this project
2. Install dependencies:
```bash
npm install
```
3. Place `aware-cam-proxy.exe` in the project root directory
### Chrome Extension Setup
1. Open Chrome and navigate to `chrome://extensions/`
2. Enable **Developer mode** (toggle in top-right)
3. Click **Load unpacked**
4. Select the `chrome-extension/` folder from this project
5. The extension icon will appear in the Chrome toolbar
## Usage
### Starting the Application
```bash
npm start
```
Or for development mode with DevTools:
```bash
npm run dev
```
### Connecting to Alta
1. **Log into your Alta deployment** in Chrome (e.g., `https://your-site.eu1.aware.avasecurity.com`)
2. **Click the extension icon** in Chrome — it will detect the Alta tab
3. **Click "Send Cookie to APT"** — the app will connect and load devices automatically
### Launching Camera Proxy
1. **Connect via Chrome extension** (above)
2. **Select a device** from the left sidebar
3. **Click "Start Camera Proxy"**
4. A command prompt window will open with the proxy connection
### Updating
The app checks for updates automatically 2 seconds after launch. If a newer version is available on GitHub Releases, the "Check for Updates" button in the header will show a green badge.
- **Manual check**: Click "Check for Updates" in the top-right corner
- **Install**: The update modal shows release notes — click "Install Update" to download and replace the current executable
- The app will quit, swap the `.exe`, and relaunch automatically
## API Endpoints Used
- **Device List**: `GET /api/v1/devices` — Retrieve all devices
- **Auth Info**: `GET /api/v1/auth` — Verify authentication status
## How It Works
```
Chrome Extension (popup click)
→ POST http://127.0.0.1:18247/cookie
→ Electron app HTTP server receives cookie
→ Sets session state, fetches devices
→ User selects device → launches aware-cam-proxy.exe
```
The Electron app runs a local HTTP server on port 18247 that only accepts requests from Chrome extensions with a shared token. The Chrome extension reads the `va` session cookie from the active Alta tab and sends it to the app.
## Security
- **Context Isolation**: Renderer process is isolated from Node.js APIs
- **Preload Script**: Secure IPC communication between main and renderer processes
- **CSP Enforced**: `script-src 'self'` — no inline scripts allowed
- **Localhost Only**: Cookie server binds to `127.0.0.1`, not accessible from network
- **CORS Restricted**: Only `chrome-extension://` origins accepted
- **Domain Validation**: Only `*.avasecurity.com` and `*.avigilon.com` URLs accepted
- **Input Sanitization**: Batch file inputs sanitized to prevent command injection
- **Size Limits**: 64KB body limit on cookie server, type/length validation on all inputs
## File Structure
```
├── main.js # Main process (IPC, API calls, proxy spawning, cookie server)
├── renderer.js # Renderer process (UI logic, state management)
├── preload.js # Secure IPC bridge (contextBridge)
├── index.html # Static HTML shell (CSP enforced)
├── styles.css # Dark theme styling
├── package.json # Dependencies and build config
├── chrome-extension/ # Chrome extension for cookie import
│ ├── manifest.json # Manifest V3
│ ├── popup.html # Extension popup UI
│ ├── popup.css # Dark theme styling
│ ├── popup.js # Tab detection, cookie retrieval
│ └── icon*.png # Extension icons
├── assets/
│ └── icon.png # Application icon
├── CLAUDE.md # Claude Code project instructions
└── README.md # This file
```
**External executable** (not included in repo):
- `aware-cam-proxy.exe` — cookie-based auth proxy (required, place in app root)
## Troubleshooting
### Connection Issues
- Ensure you are **logged into Alta in Chrome** before clicking the extension
- Verify the extension shows "Detected: [hostname]" in green
- If extension shows "Alta Proxy Tool is not running" — start the Electron app first
- If "Session cookie has expired" — log into Alta again in Chrome
- Check that the app console shows "Cookie server listening on http://127.0.0.1:18247"
### Camera Proxy Issues
- **Executable not found**: Ensure `aware-cam-proxy.exe` is in the application directory
- **Proxy won't start**: Check that you're connected and have selected a device
- **Command window closes immediately**: Check network connectivity to the deployment
### Device List Issues
- Ensure you're connected via the Chrome extension first
- Check that your user account has permissions to view devices
- **No devices shown**: You may only have cloud cameras which are filtered out
- Use the search box to find specific devices
## Building for Distribution
```bash
# Build portable Windows executable
npm run build
# Output: dist/AltaCameraProxy-1.0.0-portable.exe
```
**Important**: Copy `aware-cam-proxy.exe` to the same directory as the built executable before distribution.
## Limitations
- **Windows Only**: Camera proxy executable is Windows-specific
- **Chrome Required**: Authentication requires the Chrome extension
- **Local Cameras Only**: Automatically filters out cloud-based cameras
- **No Session Refresh**: Sessions may expire and require re-import from Chrome
- **Executable Required**: `aware-cam-proxy.exe` must be obtained separately
- **Update Requires Write Access**: Self-update needs write permission to the directory containing the `.exe`
## Development
To modify or extend this application:
1. **Main Process** ([main.js](main.js)): App lifecycle, API requests, proxy spawning, cookie server
2. **Renderer Process** ([renderer.js](renderer.js)): UI interactions and state management
3. **Preload Script** ([preload.js](preload.js)): Secure IPC bridge with context isolation
4. **Chrome Extension** ([chrome-extension/](chrome-extension/)): Cookie import from browser
5. **Styling** ([styles.css](styles.css)): Dark mode theme and responsive design
### Adding New IPC Endpoints
1. Add handler in [main.js](main.js) using `ipcMain.handle()`
2. Expose method in [preload.js](preload.js) via `contextBridge.exposeInMainWorld()`
3. Call from [renderer.js](renderer.js) using `window.electronAPI.yourMethod()`
## License
MIT License - Feel free to modify and distribute as needed.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

-72
View File
@@ -1,72 +0,0 @@
# build-kit.ps1 — Build the Alta Proxy Tool Kit zip for distribution
# Usage: powershell -ExecutionPolicy Bypass -File build-kit.ps1
#
# Produces: dist/AltaProxyToolKit.zip containing:
# - AltaCameraProxy-<version>-portable.exe
# - aware-cam-proxy.exe
# - chrome-extension/ (folder)
$ErrorActionPreference = "Stop"
# Read version from package.json
$pkg = Get-Content "package.json" -Raw | ConvertFrom-Json
$version = $pkg.version
Write-Host "Building Alta Proxy Tool Kit v$version" -ForegroundColor Cyan
# Step 1: Build the Electron portable exe
Write-Host "`n[1/4] Building Electron app..." -ForegroundColor Yellow
npm run build
if ($LASTEXITCODE -ne 0) {
Write-Host "Build failed!" -ForegroundColor Red
exit 1
}
$portableExe = "dist\AltaCameraProxy-$version-portable.exe"
if (-not (Test-Path $portableExe)) {
Write-Host "Expected output not found: $portableExe" -ForegroundColor Red
exit 1
}
Write-Host " Built: $portableExe" -ForegroundColor Green
# Step 2: Verify aware-cam-proxy.exe exists
Write-Host "`n[2/4] Checking for aware-cam-proxy.exe..." -ForegroundColor Yellow
if (-not (Test-Path "aware-cam-proxy.exe")) {
Write-Host "aware-cam-proxy.exe not found in project root!" -ForegroundColor Red
Write-Host "Place aware-cam-proxy.exe in the project root and try again." -ForegroundColor Red
exit 1
}
Write-Host " Found: aware-cam-proxy.exe" -ForegroundColor Green
# Step 3: Verify chrome-extension folder exists
Write-Host "`n[3/4] Checking for chrome-extension/..." -ForegroundColor Yellow
if (-not (Test-Path "chrome-extension\manifest.json")) {
Write-Host "chrome-extension/ folder not found or missing manifest.json!" -ForegroundColor Red
exit 1
}
Write-Host " Found: chrome-extension/" -ForegroundColor Green
# Step 4: Create the kit zip
Write-Host "`n[4/4] Creating AltaProxyToolKit.zip..." -ForegroundColor Yellow
$kitDir = "dist\AltaProxyToolKit"
$zipPath = "dist\AltaProxyToolKit.zip"
# Clean previous kit
if (Test-Path $kitDir) { Remove-Item $kitDir -Recurse -Force }
if (Test-Path $zipPath) { Remove-Item $zipPath -Force }
# Assemble kit contents
New-Item -ItemType Directory -Path $kitDir | Out-Null
Copy-Item $portableExe "$kitDir\AltaCameraProxy.exe"
Copy-Item "aware-cam-proxy.exe" "$kitDir\aware-cam-proxy.exe"
Copy-Item "chrome-extension" "$kitDir\chrome-extension" -Recurse
# Create zip
Compress-Archive -Path "$kitDir\*" -DestinationPath $zipPath -Force
# Clean up temp directory
Remove-Item $kitDir -Recurse -Force
$zipSize = [math]::Round((Get-Item $zipPath).Length / 1MB, 1)
Write-Host "`nKit ready: $zipPath ($zipSize MB)" -ForegroundColor Green
Write-Host "Upload this file as a release asset named 'AltaProxyToolKit.zip'" -ForegroundColor Cyan
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

-25
View File
@@ -1,25 +0,0 @@
{
"manifest_version": 3,
"name": "Alta Proxy Tool Bridge",
"version": "1.0.0",
"description": "Send Alta session cookies to the Alta Proxy Tool desktop app.",
"permissions": ["cookies", "activeTab", "clipboardWrite"],
"host_permissions": [
"https://*.avasecurity.com/*",
"https://*.avigilon.com/*",
"http://127.0.0.1:18247/*"
],
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icon16.png",
"48": "icon48.png",
"128": "icon128.png"
}
},
"icons": {
"16": "icon16.png",
"48": "icon48.png",
"128": "icon128.png"
}
}
-121
View File
@@ -1,121 +0,0 @@
/* Dark theme matching the Electron app */
body {
margin: 0;
padding: 0;
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, sans-serif;
background: #1E1E1E;
color: #E0E0E0;
font-size: 14px;
min-width: 300px;
}
.popup-container {
padding: 16px;
}
h1 {
font-size: 16px;
font-weight: 600;
color: #0E7AFE;
margin: 0 0 12px 0;
text-align: center;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.tab-info {
background: #2D2D30;
border: 1px solid #3C3C3C;
border-radius: 4px;
padding: 10px 12px;
margin-bottom: 12px;
font-size: 13px;
color: #999999;
word-break: break-all;
}
.tab-info.detected {
color: #4CAF50;
border-color: #4CAF50;
}
.tab-info.not-detected {
color: #F44336;
border-color: #F44336;
}
.action-buttons {
display: grid;
gap: 8px;
}
.primary-btn,
.secondary-btn {
display: block;
width: 100%;
padding: 10px 16px;
font-family: inherit;
font-size: 14px;
font-weight: bold;
color: white;
background: #0E7AFE;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s ease;
}
.primary-btn {
background: #0E7AFE;
}
.secondary-btn {
background: #2D2D30;
border: 1px solid #0E7AFE;
color: #E0E0E0;
}
.primary-btn:hover:not(:disabled) {
background: #0A5FD9;
}
.secondary-btn:hover:not(:disabled) {
background: #0E7AFE;
}
.primary-btn:disabled,
.secondary-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.status-msg {
margin-top: 10px;
padding: 8px 12px;
border-radius: 4px;
font-size: 13px;
font-weight: bold;
display: none;
border: 1px solid transparent;
}
.status-msg.success {
display: block;
background: rgba(76, 175, 80, 0.1);
color: #4CAF50;
border-color: #4CAF50;
}
.status-msg.error {
display: block;
background: rgba(244, 67, 54, 0.1);
color: #F44336;
border-color: #F44336;
}
.status-msg.info {
display: block;
background: rgba(14, 122, 254, 0.1);
color: #0E7AFE;
border-color: #0E7AFE;
}
-21
View File
@@ -1,21 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src http://127.0.0.1:18247;">
<title>Alta Proxy Tool Bridge</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="popup-container">
<h1>Alta Proxy Tool</h1>
<div id="tabInfo" class="tab-info">Checking tab...</div>
<div class="action-buttons">
<button id="sendBtn" class="primary-btn" disabled>Send to APT</button>
<button id="copyBtn" class="secondary-btn" disabled>Copy VA Token</button>
</div>
<div id="statusMsg" class="status-msg"></div>
</div>
<script src="popup.js"></script>
</body>
</html>
-130
View File
@@ -1,130 +0,0 @@
const APT_URL = 'http://127.0.0.1:18247/cookie';
const APT_TOKEN = 'apt-local-bridge-token';
const tabInfo = document.getElementById('tabInfo');
const sendBtn = document.getElementById('sendBtn');
const copyBtn = document.getElementById('copyBtn');
const statusMsg = document.getElementById('statusMsg');
let detectedOrigin = null;
function showStatus(message, type) {
statusMsg.textContent = message;
statusMsg.className = 'status-msg ' + type;
}
function setActionButtonsDisabled(disabled) {
sendBtn.disabled = disabled;
copyBtn.disabled = disabled;
}
async function getVaCookieValue() {
if (!detectedOrigin) {
throw new Error('No Alta deployment detected.');
}
const cookie = await chrome.cookies.get({ url: detectedOrigin, name: 'va' });
if (!cookie || !cookie.value) {
throw new Error('No "va" session cookie found. Are you logged in?');
}
if (cookie.expirationDate && cookie.expirationDate < Date.now() / 1000) {
throw new Error('Session cookie has expired. Please log in again.');
}
return cookie.value;
}
// Check the active tab on popup open
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (!tabs || tabs.length === 0) {
tabInfo.textContent = 'No active tab found.';
tabInfo.className = 'tab-info not-detected';
return;
}
const tab = tabs[0];
let url;
try {
url = new URL(tab.url);
} catch {
tabInfo.textContent = 'Cannot read this tab URL.';
tabInfo.className = 'tab-info not-detected';
return;
}
const hostname = url.hostname;
const isAlta = hostname.endsWith('.avasecurity.com') || hostname.endsWith('.avigilon.com');
if (!isAlta) {
tabInfo.textContent = 'This tab is not an Alta deployment.';
tabInfo.className = 'tab-info not-detected';
return;
}
detectedOrigin = url.origin;
tabInfo.textContent = 'Detected: ' + hostname;
tabInfo.className = 'tab-info detected';
setActionButtonsDisabled(false);
});
// Send cookie on button click
sendBtn.addEventListener('click', async () => {
if (!detectedOrigin) return;
setActionButtonsDisabled(true);
showStatus('Retrieving VA token...', 'info');
try {
const cookieValue = await getVaCookieValue();
showStatus('Sending to Alta Proxy Tool...', 'info');
const response = await fetch(APT_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-APT-Token': APT_TOKEN
},
body: JSON.stringify({
deploymentUrl: detectedOrigin,
cookieValue
})
});
const data = await response.json();
if (data.success) {
showStatus('VA token sent successfully!', 'success');
} else {
showStatus('Error: ' + (data.message || 'Unknown error'), 'error');
}
} catch (err) {
if (err.message && err.message.includes('Failed to fetch')) {
showStatus('Alta Proxy Tool is not running.', 'error');
} else {
showStatus('Error: ' + err.message, 'error');
}
} finally {
setActionButtonsDisabled(false);
}
});
// Copy VA token on button click
copyBtn.addEventListener('click', async () => {
if (!detectedOrigin) return;
setActionButtonsDisabled(true);
showStatus('Retrieving VA token...', 'info');
try {
const cookieValue = await getVaCookieValue();
await navigator.clipboard.writeText(cookieValue);
showStatus('VA token copied to clipboard!', 'success');
} catch (err) {
showStatus('Error: ' + err.message, 'error');
} finally {
setActionButtonsDisabled(false);
}
});
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

-341
View File
@@ -1,341 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- GitHub Pages rebuild marker: 2026-05-22 -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Alta Proxy Tool</title>
<style>
:root {
--bg-primary: #1E1E1E;
--bg-secondary: #2D2D30;
--border: #3C3C3C;
--text-primary: #E0E0E0;
--text-secondary: #999999;
--accent-primary: #0E7AFE;
--accent-primary-hover: #0A5FD9;
--card-bg: #2D2D30;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 16px;
line-height: 1.6;
min-height: 100vh;
display: flex;
flex-direction: column;
}
a {
color: var(--accent-primary);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Header */
.header {
border-bottom: 1px solid var(--border);
padding: 16px 0;
background: var(--bg-secondary);
}
.container {
max-width: 720px;
margin: 0 auto;
padding: 0 24px;
}
.header .container {
display: flex;
align-items: center;
justify-content: space-between;
}
.header-brand {
display: flex;
align-items: center;
gap: 12px;
}
.header-brand img {
width: 32px;
height: 32px;
}
.header-brand span {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.header-link {
color: var(--text-secondary);
font-size: 14px;
}
.header-link:hover {
color: var(--text-primary);
}
/* Hero */
main {
flex: 1;
}
.hero {
text-align: center;
padding: 100px 0 48px;
}
.hero-icon {
width: 96px;
height: 96px;
margin-bottom: 24px;
}
.hero h1 {
font-size: 40px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 12px;
}
.hero .tagline {
font-size: 18px;
color: var(--text-secondary);
margin-bottom: 40px;
max-width: 500px;
margin-left: auto;
margin-right: auto;
}
.btn-download {
display: inline-block;
background: var(--accent-primary);
color: #fff;
font-size: 18px;
font-weight: 600;
padding: 16px 40px;
border-radius: 6px;
transition: background 0.2s;
}
.btn-download:hover {
background: var(--accent-primary-hover);
text-decoration: none;
}
.hero-note {
margin-top: 14px;
font-size: 13px;
color: var(--text-secondary);
}
/* Kit contents */
.kit-info {
padding: 48px 0;
border-top: 1px solid var(--border);
}
.kit-info h2 {
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 16px;
text-align: center;
}
.kit-contents {
display: flex;
gap: 16px;
justify-content: center;
}
.kit-item {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 20px 28px;
text-align: center;
}
.kit-item strong {
display: block;
font-size: 14px;
margin-bottom: 4px;
}
.kit-item span {
font-size: 13px;
color: var(--text-secondary);
}
/* Setup */
.setup {
padding: 48px 0;
border-top: 1px solid var(--border);
}
.setup h2 {
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 20px;
text-align: center;
}
.setup-steps {
list-style: none;
max-width: 480px;
margin: 0 auto;
counter-reset: step;
}
.setup-steps li {
counter-increment: step;
padding: 12px 0 12px 44px;
position: relative;
font-size: 15px;
color: var(--text-secondary);
}
.setup-steps li + li {
border-top: 1px solid var(--border);
}
.setup-steps li::before {
content: counter(step);
position: absolute;
left: 0;
top: 12px;
width: 28px;
height: 28px;
background: var(--accent-primary);
color: #fff;
font-weight: 700;
font-size: 13px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.setup-steps code {
background: var(--bg-primary);
border: 1px solid var(--border);
padding: 1px 5px;
border-radius: 3px;
font-size: 13px;
color: var(--accent-primary);
}
.setup-note {
text-align: center;
margin-top: 20px;
font-size: 13px;
color: var(--text-secondary);
}
/* Footer */
.footer {
border-top: 1px solid var(--border);
padding: 24px 0;
text-align: center;
font-size: 13px;
color: var(--text-secondary);
}
.footer a {
color: var(--text-secondary);
}
.footer a:hover {
color: var(--text-primary);
}
/* Responsive */
@media (max-width: 600px) {
.hero {
padding: 60px 0 36px;
}
.hero h1 {
font-size: 28px;
}
.hero .tagline {
font-size: 16px;
}
.btn-download {
font-size: 16px;
padding: 12px 28px;
}
.kit-contents {
flex-direction: column;
align-items: center;
}
.kit-item {
width: 100%;
max-width: 280px;
}
}
</style>
</head>
<body>
<header class="header">
<div class="container">
<div class="header-brand">
<img src="icon.png" alt="Alta Proxy Tool">
<span>Alta Proxy Tool</span>
</div>
<a href="https://github.com/PageZ948/Alta-Proxy-Tool" class="header-link">GitHub</a>
</div>
</header>
<main>
<div class="container">
<div class="hero">
<img src="icon.png" alt="" class="hero-icon">
<h1>Alta Proxy Tool</h1>
<a href="https://github.com/PageZ948/Alta-Proxy-Tool/releases/latest/download/AltaProxyToolKit.zip" class="btn-download">Download Kit for Windows</a>
</div>
<section class="setup">
<h2>Setup</h2>
<ol class="setup-steps">
<li>Extract the zip to any folder</li>
<li>Load <code>chrome-extension/</code> in Chrome via <code>chrome://extensions</code> (Developer mode)</li>
<li>Log into your Alta deployment in Chrome</li>
<li>Click the extension icon and send cookies to the app</li>
<li>Run <strong>AltaCameraProxy.exe</strong> and start proxying</li>
</ol>
<p class="setup-note">Requires Windows 10+, Google Chrome, and an Avigilon Alta account.</p>
</section>
</div>
</main>
<footer class="footer">
<div class="container">
<a href="https://github.com/PageZ948/Alta-Proxy-Tool">Alta Proxy Tool on GitHub</a>
</div>
</footer>
</body>
</html>
@@ -1,533 +0,0 @@
# Alta Proxy Tool Security Foundation and Product Roadmap Implementation Plan
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.
**Goal:** Remove APTs Critical/Important security and release blockers, establish a reproducible Windows baseline, recover the Mac implementation into source control, and only then open a controlled lane for new features.
**Architecture:** Keep Electrons renderer unprivileged. Move session credentials, URL validation, proxy launching, process ownership, and update verification into small main-process modules with narrow IPC contracts. Development occurs in an isolated GitPeji worktree; existing Tool Hub downloads and production surfaces remain unchanged until a fully tested, exact-commit release is approved.
**Tech Stack:** Electron, Node.js built-in `node:test`, Chrome Manifest V3 extension APIs, Electron Builder, GitPeji/Gitea Actions, SHA-256 release manifests, Windows Authenticode when available.
---
## Executive sequencing
### Phase A — Security containment and proof
No feature work enters this phase. It must close:
1. arbitrary-code self-updater;
2. CR/LF batch command injection;
3. renderer-controlled URL/cookie exfiltration;
4. cookie leakage through logs, temporary files, command lines, and broad clipboard handling;
5. stale/vulnerable Electron, Axios, and build dependencies.
### Phase B — Product/release foundation
1. canonical GitPeji source and exact tagged builds;
2. automated tests and CI;
3. reproducible Windows kit with helper provenance;
4. recover Mac source, then separately decide Intel/Rosetta versus native Apple Silicon;
5. safe, signed/digest-bound updater only after all earlier gates pass.
### Phase C — New features
Feature discovery begins only after Phase A passes independent security review and Phase B has a reproducible development build. New functions go into a written backlog with user value, permissions, data touched, risk, and acceptance tests before implementation.
## Non-negotiable guardrails
- GitPeji is the only source of truth. Do not fetch from, push to, publish on, or update from GitHub.
- Use a fresh worktree from GitPeji `master` at `a80074ac57b7a4517837b5d95754f5e6433df3ac` or newer.
- Preserve current Tool Hub downloads and production visibility until Zac approves replacement.
- Do not use real Alta cookies in automated tests; use conspicuous synthetic sentinels.
- Do not write cookies to logs, disk, command files, shell strings, crash reports, analytics, or test artifacts.
- No production release until the exact candidate passes controller verification plus independent security and release reviews.
- Mac distribution remains approval-gated under Kanban task `t_42bad4d3`.
---
## Milestone 0: Establish an isolated hardening lane
### Task 1: Create the clean GitPeji worktree and baseline evidence
**Objective:** Start from an exact, clean canonical source without modifying the archived checkout.
**Files:**
- Create worktree: `/home/peji/worktrees/apt-security-foundation`
- Create: `docs/security/2026-08-security-baseline.md`
- Reference: `/home/peji/.hermes/reports/alta-proxy-tool-full-review-2026-08-19.md`
**Steps:**
1. Fetch only GitPeji and verify `origin/master`.
2. Create branch `hardening/security-foundation` in the isolated worktree.
3. Record full starting SHA, remote, branch, clean status, dependency audit counts, current package version, and the review report path.
4. Confirm the archived checkout and Tool Hub checkout remain clean.
5. Commit only the baseline document.
**Commands:**
```bash
git fetch origin master
git worktree add -b hardening/security-foundation /home/peji/worktrees/apt-security-foundation origin/master
git -C /home/peji/worktrees/apt-security-foundation status --short --branch
```
**Expected:** clean branch based on exact GitPeji `origin/master`; no GitHub remote used.
**Commit:** `docs: record APT security hardening baseline`
---
## Milestone 1: Add a testable security boundary
### Task 2: Introduce the smallest useful test harness
**Objective:** Make security fixes test-first instead of relying on ad-hoc probes.
**Files:**
- Modify: `package.json`
- Create: `test/url-policy.test.js`
- Create: `test/error-redaction.test.js`
- Create: `test/proxy-launch.test.js`
- Create: `test/update-policy.test.js`
- Create: `test/bridge-auth.test.js`
- Create: `src/url-policy.js`
- Create: `src/error-redaction.js`
**Steps:**
1. Add `npm test` using `node --test` and `npm run check` for syntax/tests.
2. Write failing URL-policy tests for:
- HTTPS Alta subdomains accepted;
- HTTP rejected;
- non-Alta hosts rejected;
- userinfo, fragments, unexpected paths/ports, CR/LF, and malformed URLs rejected.
3. Write failing error-redaction tests proving a synthetic `va=HERMES_SENTINEL_SECRET` never appears in formatted output.
4. Implement minimal pure helpers.
5. Run tests and syntax checks.
**Commands:**
```bash
npm test
npm run check
```
**Expected:** all tests pass; no source code needs Electron to test URL/redaction logic.
**Commit:** `test: add APT security regression harness`
---
## Milestone 2: Remove the Critical updater path
### Task 3: Disable executable self-update completely
**Objective:** Remove the current arbitrary-native-code installation surface before redesigning updates.
**Files:**
- Modify: `main.js:454-630`
- Modify: `preload.js:25-37`
- Modify: `renderer.js:24-34,57-61,602-712,756-760`
- Modify: `index.html:38-41,84-106`
- Modify: `README.md:13,68-74,166`
- Test: `test/update-policy.test.js`
**Steps:**
1. Write failing source/contract tests asserting no `download-and-install-update` IPC method is exposed and no executable replacement code remains.
2. Remove updater IPC handlers, redirect downloader, executable overwrite batch script, progress event, and install controls.
3. Replace the UI with either no update control or a safe informational link to an authenticated/internal release page; do not download or execute anything.
4. Remove startup network update checks.
5. Run tests and confirm the app still starts/builds.
**Acceptance:** no renderer-to-main path can download, overwrite, or launch an executable.
**Commit:** `fix: remove unsafe in-place updater`
---
## Milestone 3: Close command injection and process-control hazards
### Task 4: Replace `.bat` construction with direct process spawning
**Objective:** Eliminate command injection and plaintext cookie batch files.
**Files:**
- Create: `src/proxy-launch.js`
- Modify: `main.js:26-31,274-387`
- Test: `test/proxy-launch.test.js`
**Steps:**
1. Write failing tests for CR/LF, shell metacharacters, malformed UUIDs, invalid domains, oversized cookie values, and missing helper.
2. Define strict input contracts:
- deployment host comes from validated main-process session state;
- device UUID matches the exact supported identifier shape;
- cookie length is bounded and never serialised into a command string;
- executable path is resolved from an approved fixed location.
3. Spawn `aware-cam-proxy.exe` directly with an argument array and `shell: false`.
4. Remove `sanitizeBatchInput()`, temporary `.bat` files, `cmd /c start`, and cookie-bearing echo output.
5. Capture only process ID, device ID, start time, and safe status metadata.
6. Add a Windows-specific smoke harness that substitutes a harmless fixture executable/script and records argv without exposing the cookie.
**Acceptance:** the CR/LF reproducer is rejected; no batch/command file exists; cookie is not printed or persisted.
**Commit:** `fix: launch proxy without shell command files`
### Task 5: Stop only APT-owned proxy processes
**Objective:** Prevent “Stop Proxy” from killing unrelated helper processes.
**Files:**
- Modify: `src/proxy-launch.js`
- Modify: `main.js:389-452`
- Modify: `renderer.js:147-177`
- Test: `test/proxy-launch.test.js`
**Steps:**
1. Write a failing test proving only tracked child process IDs are terminated.
2. Replace `taskkill /im aware-cam-proxy.exe` with tracked-process termination.
3. Return per-device stop results and remove dead entries from the process map.
4. Handle already-exited and permission-denied states honestly.
5. Verify two fixture processes: stopping one leaves the other alive.
**Commit:** `fix: scope proxy termination to APT processes`
---
## Milestone 4: Move trust and cookies into the main process
### Task 6: Replace renderer-supplied URL/cookie IPC parameters
**Objective:** Prevent a compromised renderer from sending Alta cookies to arbitrary destinations.
**Files:**
- Create: `src/session-store.js`
- Create: `src/alta-client.js`
- Modify: `main.js:192-272`
- Modify: `preload.js:5-12`
- Modify: `renderer.js:225-304,714-743`
- Test: `test/url-policy.test.js`
- Create: `test/alta-client.test.js`
**Steps:**
1. Write failing tests showing API methods reject renderer-provided URL/cookie fields.
2. Store deployment origin and cookie only in main-process memory.
3. Expose narrow IPC calls: `getDevices()`, `getDeviceSites()`, `getAuthInfo()`, `launchProxy(deviceId)`, `disconnect()`.
4. Revalidate HTTPS Alta origin inside the main process before every request.
5. Disable ambient proxy use for cookie-bearing requests unless a separately reviewed explicit enterprise-proxy configuration is later required.
6. Disable cross-origin redirects or validate every redirect before forwarding credentials.
7. Validate `event.senderFrame` against the apps local file origin/window.
8. On disconnect and app quit, overwrite references and clear session state.
**Acceptance:** renderer messages cannot choose a destination or supply/receive the bearer cookie.
**Commit:** `fix: keep Alta session authority in the main process`
### Task 7: Redact all network and process errors
**Objective:** Ensure failures cannot print cookies or secret-bearing request configuration.
**Files:**
- Modify: `src/error-redaction.js`
- Modify: `src/alta-client.js`
- Modify: `main.js`
- Test: `test/error-redaction.test.js`
**Steps:**
1. Add synthetic Axios errors containing secrets in headers, URLs, nested config, causes, and response bodies.
2. Format only bounded fields: operation, safe error code, HTTP status, timeout flag, and a sanitised message.
3. Remove all full-object `console.error(..., error)` calls from credential-bearing paths.
4. Add a source scan test for forbidden logging patterns.
5. Run a sentinel failure probe and assert zero secret occurrences in stdout/stderr.
**Commit:** `fix: redact credentials from APT errors`
---
## Milestone 5: Replace the public hard-coded bridge secret
### Task 8: Add explicit extension pairing
**Objective:** Stop treating `apt-local-bridge-token` as authentication.
**Recommended v1 design:** keep loopback temporarily but pair the extension to the desktop app with a random per-install secret and a stable extension identity. Native Messaging is a later option if portability/setup proves acceptable.
**Files:**
- Create: `src/bridge-auth.js`
- Modify: `main.js:12-145`
- Modify: `chrome-extension/manifest.json`
- Modify: `chrome-extension/popup.js`
- Create: `chrome-extension/options.html`
- Create: `chrome-extension/options.js`
- Test: `test/bridge-auth.test.js`
- Create: `test/extension-contract.test.js`
**Steps:**
1. Write failing tests for missing/wrong secret, unknown extension origin, replay/rotation, oversized body, slow body, and malformed JSON.
2. Generate a cryptographically random pairing secret and store it with user-only permissions under Electron `userData`.
3. Add a one-time pairing flow: desktop shows code; extension stores the resulting secret in `chrome.storage.local`.
4. Give the extension a stable ID and accept only that exact extension origin.
5. Compare secrets in constant time.
6. Add request/body deadlines and bounded concurrency before body reads.
7. Rotate/revoke pairing from the desktop UI.
8. Remove `apt-local-bridge-token` from source.
**Acceptance:** a different Chrome extension cannot submit data even when it knows the source code; pairing can be revoked without reinstalling APT.
**Commit:** `feat: add paired authentication for the Chrome bridge`
### Task 9: Make clipboard token copying explicitly guarded
**Objective:** Retain Zacs requested copy function without presenting it as harmless.
**Files:**
- Modify: `chrome-extension/popup.html`
- Modify: `chrome-extension/popup.js`
- Modify: `chrome-extension/popup.css`
- Test: `test/extension-contract.test.js`
**Steps:**
1. Require a deliberate confirmation for copying the full bearer token.
2. Show a concise warning that clipboard history/synchronisation may retain it.
3. Never auto-copy.
4. Clear extension references immediately after the operation.
5. Test cancellation, permission denial, missing/expired cookie, and success messaging.
**Commit:** `fix: guard VA token clipboard export`
---
## Milestone 6: Modernise dependencies and desktop hardening
### Task 10: Upgrade Electron, Axios, and packaging dependencies
**Objective:** Remove known dependency blockers without mixing in feature changes.
**Files:**
- Modify: `package.json`
- Modify: `package-lock.json`
- Modify: `main.js:148-167`
- Remove dependency if unused: `crypto-js`
- Remove/replace: `electron-packager`
**Steps:**
1. Create a dependency-upgrade branch commit separately from behaviour changes.
2. Upgrade Electron to a currently supported major compatible with the target Windows estate.
3. Upgrade Axios and transitive packages to patched releases.
4. Remove unused `crypto-js` and deprecated `electron-packager` if source/runtime scans confirm no use.
5. Enable explicit renderer sandboxing and deny unexpected navigation/window creation.
6. Run full tests, `npm audit`, Electron smoke, and Windows package build.
7. Do not accept Critical/High runtime findings; document any build-only exception with CVE reachability evidence.
**Acceptance:** zero Critical/High production audit findings; supported Electron line; app smoke passes.
**Commit:** `chore: modernize Electron runtime and dependencies`
---
## Milestone 7: Canonical source and reproducible releases
### Task 11: Add helper provenance without committing the binary blindly
**Objective:** Make the Windows kit reproducible and auditable while respecting vendor licensing.
**Files:**
- Create: `vendor/aware-cam-proxy/README.md`
- Create: `vendor/aware-cam-proxy/checksums.json`
- Modify: `build-kit.ps1`
- Modify: `README.md`
- Create: `LICENSE`
- Create: `THIRD_PARTY_NOTICES.md`
**Steps:**
1. Confirm redistribution rights for Windows/macOS helper binaries; stop if unavailable.
2. Record approved filename, platform, architecture, version/source owner, exact size, SHA-256, and acquisition procedure.
3. Make the kit build reject any helper that does not match the approved manifest.
4. Replace `author: "Your Name"` and add accurate project/third-party licensing.
5. Add ZIP content, path-safety, extension-content, executable-presence, and checksum tests.
**Commit:** `build: verify proxy helper provenance in release kits`
### Task 12: Recover Mac implementation into GitPeji source
**Objective:** Stop treating the Tool Hub Mac ZIP as source control.
**Files:**
- Recover into: `src/proxy-launch.js`
- Modify: `package.json`
- Modify: `README.md`
- Create: `docs/platforms/macos.md`
- Test: `test/proxy-launch.test.js`
**Steps:**
1. Extract and diff the current Tool Hub Mac `app.asar` against the hardened source.
2. Port only reviewed platform logic; do not copy the stripped packaged `package.json` wholesale.
3. Reuse the direct-spawn/no-shell/session rules from Windows.
4. Add explicit x64 and arm64 packaging targets, but publish only architectures with a matching approved helper.
5. Test on Intel/Rosetta and a current Apple Silicon Mac.
6. Keep Kanban `t_42bad4d3` open until native Apple Silicon/helper support is proven or Zac explicitly accepts Rosetta-only scope.
**Commit:** `feat: restore reviewed macOS source and build targets`
### Task 13: Add GitPeji CI and exact-commit release evidence
**Objective:** Build every release from one tagged GitPeji commit with retained evidence.
**Files:**
- Create: `.gitea/workflows/ci.yml`
- Create: `.gitea/workflows/release.yml`
- Remove or retire: `.github/workflows/deploy-pages.yml`
- Modify: `docs/index.html`
- Create: `scripts/verify-kit.js`
**Steps:**
1. CI gate: `npm ci`, syntax, tests, audit policy, Windows unpacked build, kit verification, secret scan.
2. Release gate: tag/version match, clean source, approved helper checksum, exact artifact hashes, SBOM, checksums, retained logs.
3. Point docs/downloads to GitPeji or the approved Tool Hub route, never GitHub.
4. Publish no artifact automatically until the first manual release rehearsal passes.
5. Rehearse on a non-production candidate tag and verify fresh download/extraction.
**Commit:** `ci: add GitPeji build and release gates`
---
## Milestone 8: Reintroduce updates safely, if still worthwhile
### Task 14: Decide whether APT actually needs in-place updates
**Objective:** Avoid rebuilding a dangerous feature merely because it existed before.
**Decision gate:**
- Preferred simplest option: notify that a verified update exists and open the trusted Tool Hub/GitPeji release page; user installs it manually.
- In-place update is allowed only if code signing, digest verification, rollback, and platform-specific testing are available.
**Deliverable:** `docs/decisions/0001-update-strategy.md`
**Commit:** `docs: decide APT update strategy`
### Task 15: Implement a verified updater only if the decision approves it
**Objective:** Bind installation to an exact trusted release.
**Files:**
- Create: `src/update-manifest.js`
- Create: `src/updater.js`
- Modify: `main.js`
- Modify: `preload.js`
- Test: `test/update-policy.test.js`
**Required tests:** wrong host, redirect host change, wrong size, wrong SHA-256, wrong signature, downgrade, wrong platform/architecture, truncated body, oversized body, interrupted download, failed replacement, successful rollback.
**Acceptance:** only a manifest-authenticated, platform-matched artifact can reach replacement code; failure leaves the installed executable intact.
**Commit:** `feat: add manifest-verified GitPeji updates`
---
## Milestone 9: Closure and controlled release
### Task 16: Run adversarial security closure review
**Objective:** Independently prove every Critical/Important finding is closed.
**Required probes:**
- arbitrary updater URL cannot execute;
- CR/LF proxy parameters cannot become commands;
- renderer cannot choose request destination or supply cookies;
- Axios failures contain no sentinel secret;
- bridge rejects unknown extension/wrong secret/slow bodies;
- cookies never appear in files, process command strings, logs, tests, ZIP metadata, or analytics;
- stop affects only APT-owned child processes;
- dependency audit and Electron support policy pass.
**Output:** finding-to-fix closure matrix with exact tests and commit.
### Task 17: Build a DEV release candidate and preserve production
**Objective:** Give Zac a testable candidate without replacing current downloads.
**Steps:**
1. Query usage/confirm release lane before changing Tool Hub visibility.
2. Build exact tagged candidate and record artifact SHA-256.
3. Publish only to an isolated DEV/download location clearly marked pre-release.
4. Run Windows app, extension pairing, device discovery, proxy launch/stop, disconnect, error, and update-notification workflows.
5. Verify current production Tool Hub/download hashes did not change.
6. Request Zac acceptance.
### Task 18: Promote only the accepted exact artifact
**Objective:** Replace the existing release safely and reversibly.
**Steps:**
1. Preserve current Windows/Mac kits and hashes as rollback artifacts.
2. Promote the exact accepted candidate—do not rebuild.
3. Verify GitPeji tag/release, Tool Hub route, download size/hash, extension contents, Windows smoke, and analytics.
4. Keep Mac separate until its own acceptance gate passes.
5. Remove temporary DEV artifacts only after production verification and rollback proof.
---
## Feature roadmap gate
### Task 19: Capture and prioritise new functions with Zac
**Objective:** Turn feature ideas into a safe, ordered backlog after the red flags are closed.
For each feature record:
- user/problem and field-engineer value;
- exact user journey;
- read versus write operations;
- Alta endpoints/data required;
- cookie/session impact;
- local helper/process impact;
- Windows/macOS parity expectation;
- failure and offline behaviour;
- acceptance tests;
- release risk and whether it can remain DEV-only.
**Suggested buckets for the discussion:**
1. **Proxy workflow improvements:** multi-camera queue, per-camera status, retry/diagnostics, safe logs.
2. **Commissioning:** thumbnail/stream health, recording/site checks, readiness report.
3. **Troubleshooting:** streams debug, recent alerts, support bundle.
4. **Operator UX:** saved non-secret deployment aliases, clearer helper detection, version/support panel.
5. **Platform:** native Apple Silicon, managed extension packaging, enterprise deployment.
Do not implement feature buckets during the hardening branch. Create separate feature branches from the accepted security foundation.
---
## Definition of “major red flags fixed”
All must be true:
- [ ] Unsafe updater removed or cryptographically verified.
- [ ] No shell/batch construction from user or renderer data.
- [ ] Main process owns deployment origin and cookies.
- [ ] Privileged IPC validates sender and strict schemas.
- [ ] No cookie in logs, files, command strings, or automatic clipboard actions.
- [ ] Loopback bridge uses revocable pairing and exact extension identity.
- [ ] Proxy stop is process-scoped.
- [ ] Supported Electron/Axios versions; no Critical/High runtime audit findings.
- [ ] Automated tests and GitPeji CI pass.
- [ ] Windows kit is exact-commit reproducible with helper provenance.
- [ ] Mac source is canonical or Mac distribution remains explicitly withheld.
- [ ] Independent security closure review returns PASS.
- [ ] Existing production/download state remains unchanged until Zac approves promotion.
@@ -1,18 +0,0 @@
# APT security hardening baseline
- Canonical source: GitPeji `peji/Alta-Proxy-Tool`
- Starting commit: `a80074ac57b7a4517837b5d95754f5e6433df3ac`
- Branch: `hardening/security-foundation`
- Original review: `/home/peji/.hermes/reports/alta-proxy-tool-full-review-2026-08-19.md`
- Implementation plan: `docs/plans/2026-08-19-apt-security-foundation.md`
- Starting package version: `1.0.0`
- Starting audit: 25 findings (1 critical, 22 high, 2 moderate); production-only audit 3 findings (2 high, 1 moderate)
- Starting release state: GitPeji/GitHub split-brain, no application CI, unsafe unsigned updater, no automated tests
## Guardrails
- GitPeji only; GitHub remote removed.
- Production Tool Hub and existing downloads remain unchanged.
- Synthetic sentinel credentials only in tests.
- Work remains on the isolated hardening branch until review gates pass.
- No feature additions until Critical/Important security closure.
View File

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

+323 -92
View File
@@ -1,110 +1,341 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<!-- GitHub Pages rebuild marker: 2026-05-22 -->
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;"> <title>Alta Proxy Tool</title>
<title>Alta Video Camera Proxy with API</title> <style>
<link rel="stylesheet" href="styles.css"> :root {
--bg-primary: #1E1E1E;
--bg-secondary: #2D2D30;
--border: #3C3C3C;
--text-primary: #E0E0E0;
--text-secondary: #999999;
--accent-primary: #0E7AFE;
--accent-primary-hover: #0A5FD9;
--card-bg: #2D2D30;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 16px;
line-height: 1.6;
min-height: 100vh;
display: flex;
flex-direction: column;
}
a {
color: var(--accent-primary);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Header */
.header {
border-bottom: 1px solid var(--border);
padding: 16px 0;
background: var(--bg-secondary);
}
.container {
max-width: 720px;
margin: 0 auto;
padding: 0 24px;
}
.header .container {
display: flex;
align-items: center;
justify-content: space-between;
}
.header-brand {
display: flex;
align-items: center;
gap: 12px;
}
.header-brand img {
width: 32px;
height: 32px;
}
.header-brand span {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.header-link {
color: var(--text-secondary);
font-size: 14px;
}
.header-link:hover {
color: var(--text-primary);
}
/* Hero */
main {
flex: 1;
}
.hero {
text-align: center;
padding: 100px 0 48px;
}
.hero-icon {
width: 96px;
height: 96px;
margin-bottom: 24px;
}
.hero h1 {
font-size: 40px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 12px;
}
.hero .tagline {
font-size: 18px;
color: var(--text-secondary);
margin-bottom: 40px;
max-width: 500px;
margin-left: auto;
margin-right: auto;
}
.btn-download {
display: inline-block;
background: var(--accent-primary);
color: #fff;
font-size: 18px;
font-weight: 600;
padding: 16px 40px;
border-radius: 6px;
transition: background 0.2s;
}
.btn-download:hover {
background: var(--accent-primary-hover);
text-decoration: none;
}
.hero-note {
margin-top: 14px;
font-size: 13px;
color: var(--text-secondary);
}
/* Kit contents */
.kit-info {
padding: 48px 0;
border-top: 1px solid var(--border);
}
.kit-info h2 {
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 16px;
text-align: center;
}
.kit-contents {
display: flex;
gap: 16px;
justify-content: center;
}
.kit-item {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 20px 28px;
text-align: center;
}
.kit-item strong {
display: block;
font-size: 14px;
margin-bottom: 4px;
}
.kit-item span {
font-size: 13px;
color: var(--text-secondary);
}
/* Setup */
.setup {
padding: 48px 0;
border-top: 1px solid var(--border);
}
.setup h2 {
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 20px;
text-align: center;
}
.setup-steps {
list-style: none;
max-width: 480px;
margin: 0 auto;
counter-reset: step;
}
.setup-steps li {
counter-increment: step;
padding: 12px 0 12px 44px;
position: relative;
font-size: 15px;
color: var(--text-secondary);
}
.setup-steps li + li {
border-top: 1px solid var(--border);
}
.setup-steps li::before {
content: counter(step);
position: absolute;
left: 0;
top: 12px;
width: 28px;
height: 28px;
background: var(--accent-primary);
color: #fff;
font-weight: 700;
font-size: 13px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.setup-steps code {
background: var(--bg-primary);
border: 1px solid var(--border);
padding: 1px 5px;
border-radius: 3px;
font-size: 13px;
color: var(--accent-primary);
}
.setup-note {
text-align: center;
margin-top: 20px;
font-size: 13px;
color: var(--text-secondary);
}
/* Footer */
.footer {
border-top: 1px solid var(--border);
padding: 24px 0;
text-align: center;
font-size: 13px;
color: var(--text-secondary);
}
.footer a {
color: var(--text-secondary);
}
.footer a:hover {
color: var(--text-primary);
}
/* Responsive */
@media (max-width: 600px) {
.hero {
padding: 60px 0 36px;
}
.hero h1 {
font-size: 28px;
}
.hero .tagline {
font-size: 16px;
}
.btn-download {
font-size: 16px;
padding: 12px 28px;
}
.kit-contents {
flex-direction: column;
align-items: center;
}
.kit-item {
width: 100%;
max-width: 280px;
}
}
</style>
</head> </head>
<body> <body>
<div class="app-container">
<!-- Main Content Layout --> <header class="header">
<div class="main-layout"> <div class="container">
<!-- Left Sidebar - Available Devices --> <div class="header-brand">
<aside class="devices-sidebar"> <img src="icon.png" alt="Alta Proxy Tool">
<div class="sidebar-header"> <span>Alta Proxy Tool</span>
<h2>Available Devices</h2> </div>
<a href="https://github.com/PageZ948/Alta-Proxy-Tool" class="header-link">GitHub</a>
</div>
</header>
<main>
<div class="container">
<div class="hero">
<img src="icon.png" alt="" class="hero-icon">
<h1>Alta Proxy Tool</h1>
<a href="https://github.com/PageZ948/Alta-Proxy-Tool/releases/latest/download/AltaProxyToolKit.zip" class="btn-download">Download Kit for Windows</a>
</div> </div>
<div id="deviceStatus" class="status-message"></div> <section class="setup">
<h2>Setup</h2>
<!-- Device Search --> <ol class="setup-steps">
<div class="device-search-container"> <li>Extract the zip to any folder</li>
<input type="text" id="deviceSearch" placeholder="Search devices..." class="device-search-input"> <li>Load <code>chrome-extension/</code> in Chrome via <code>chrome://extensions</code> (Developer mode)</li>
</div> <li>Log into your Alta deployment in Chrome</li>
<li>Click the extension icon and send cookies to the app</li>
<div class="device-list-container"> <li>Run <strong>AltaCameraProxy.exe</strong> and start proxying</li>
<div id="deviceList" class="device-list"> </ol>
<p class="placeholder-text">Connect to API to load devices</p> <p class="setup-note">Requires Windows 10+, Google Chrome, and an Avigilon Alta account.</p>
</div>
</div>
</aside>
<!-- Main Content Area -->
<main class="main-content">
<div class="content-header">
<h1>Alta Video Camera Proxy</h1>
<button type="button" id="checkUpdateBtn" class="btn-update" title="Check for Updates">
<span class="update-icon">&#x21bb;</span>
<span class="update-text">Check for Updates</span>
</button>
</div>
<!-- API Connection Section -->
<section class="content-section">
<h2>API Connection</h2>
<div class="connection-status">
<div class="status-row">
<label>Status:</label>
<div class="status-indicator" id="statusIndicator">
<span class="status-dot offline"></span>
<span class="status-text">Disconnected</span>
</div>
</div>
</div>
<div class="connection-controls">
<button type="button" id="disconnectBtn" class="btn-outline" disabled>Disconnect</button>
</div>
<div id="connectionStatus" class="status-message"></div>
</section> </section>
<!-- Cookie-Based Camera Proxy Section -->
<section class="content-section">
<h2>Camera Proxy</h2>
<div class="proxy-controls">
<div class="input-row">
<label for="cookieDeviceUUID">Device UUID:</label>
<input type="text" id="cookieDeviceUUID" placeholder="(Auto-filled when you select a device from the list)" readonly>
</div> </div>
<div class="input-row" style="display: none;">
<label for="cookieKey">Cookie Key:</label>
<input type="text" id="cookieKey" placeholder="Paste your cookie key here">
</div>
<div class="proxy-buttons">
<button type="button" id="startCookieProxyBtn" class="btn-primary" disabled>Start Proxy</button>
<button type="button" id="stopCookieProxyBtn" class="btn-outline" disabled>Stop Proxy</button>
</div>
</div>
</section>
</main> </main>
</div>
</div>
<!-- Update Modal --> <footer class="footer">
<div id="updateModalOverlay" class="update-modal-overlay" style="display: none;"> <div class="container">
<div class="update-modal-card"> <a href="https://github.com/PageZ948/Alta-Proxy-Tool">Alta Proxy Tool on GitHub</a>
<div class="update-modal-header">
<h3>Update Available</h3>
<button type="button" id="updateModalCloseBtn" class="update-modal-close">&times;</button>
</div>
<div class="update-modal-body">
<p id="updateModalMessage" class="update-modal-message"></p>
<div id="updateModalNotes" class="update-modal-notes"></div>
<div id="updateProgressContainer" class="update-progress-container" style="display: none;">
<div class="update-progress-track">
<div id="updateProgressFill" class="update-progress-fill" style="width: 0%"></div>
</div>
<span id="updateProgressText" class="update-progress-text">0%</span>
</div>
</div>
<div class="update-modal-footer">
<button type="button" id="updateInstallBtn" class="btn-primary">Install Update</button>
<button type="button" id="updateLaterBtn" class="btn-outline">Later</button>
</div>
</div>
</div> </div>
</footer>
<script src="renderer.js"></script>
</body> </body>
</html> </html>
-631
View File
@@ -1,631 +0,0 @@
const { app, BrowserWindow, ipcMain, shell } = require('electron');
const path = require('path');
const fs = require('fs');
const os = require('os');
const axios = require('axios');
const https = require('https');
const { spawn } = require('child_process');
const http = require('http');
let mainWindow;
let activeProxyProcesses = new Map(); // Track active camera proxy processes
let cookieServer = null;
const COOKIE_SERVER_PORT = 18247;
const COOKIE_SERVER_TOKEN = 'apt-local-bridge-token';
// Get the directory where the user-facing executable resides.
// In portable builds, __dirname points to a temp extraction directory,
// so we use the actual .exe location instead.
function getAppDirectory() {
if (app.isPackaged) {
return path.dirname(process.env.PORTABLE_EXECUTABLE_FILE || app.getPath('exe'));
}
return __dirname;
}
// Sanitize strings before embedding in batch files to prevent command injection
function sanitizeBatchInput(input) {
if (typeof input !== 'string') return '';
// Remove characters that have special meaning in batch/cmd: & | < > ^ % " ` !
return input.replace(/[&|<>^%"`!]/g, '');
}
function startCookieServer() {
cookieServer = http.createServer((req, res) => {
// CORS headers — only allow Chrome extension origins
const origin = req.headers.origin || '';
if (origin.startsWith('chrome-extension://')) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-APT-Token');
// Handle preflight
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
// Only accept POST /cookie
if (req.method !== 'POST' || req.url !== '/cookie') {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, message: 'Not found' }));
return;
}
// Verify shared token
if (req.headers['x-apt-token'] !== COOKIE_SERVER_TOKEN) {
res.writeHead(403, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, message: 'Forbidden' }));
return;
}
// Read body with 64KB size limit
let body = '';
let bodySize = 0;
const MAX_BODY_SIZE = 65536;
req.on('data', (chunk) => {
bodySize += chunk.length;
if (bodySize > MAX_BODY_SIZE) {
res.writeHead(413, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, message: 'Payload too large' }));
req.destroy();
return;
}
body += chunk;
});
req.on('end', () => {
try {
const data = JSON.parse(body);
const { deploymentUrl, cookieValue } = data;
if (!deploymentUrl || !cookieValue) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, message: 'Missing deploymentUrl or cookieValue' }));
return;
}
// Validate types and lengths
if (typeof deploymentUrl !== 'string' || typeof cookieValue !== 'string' ||
deploymentUrl.length > 512 || cookieValue.length > 4096) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, message: 'Invalid parameter types or lengths' }));
return;
}
// Validate deployment URL is an Alta domain
try {
const parsed = new URL(deploymentUrl);
const isAltaDomain = parsed.hostname.endsWith('.avasecurity.com') ||
parsed.hostname.endsWith('.avigilon.com');
if (!isAltaDomain || parsed.protocol !== 'https:') {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, message: 'Invalid deployment URL domain' }));
return;
}
} catch {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, message: 'Invalid deployment URL' }));
return;
}
if (mainWindow && !mainWindow.isDestroyed()) {
const cookies = ['va=' + cookieValue];
mainWindow.webContents.send('extension-cookie-received', {
deploymentUrl: deploymentUrl.replace(/\/$/, ''),
cookies,
cookieValue
});
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, message: 'Cookie received' }));
} else {
res.writeHead(503, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, message: 'Application window not available' }));
}
} catch (e) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, message: 'Invalid JSON' }));
}
});
});
cookieServer.listen(COOKIE_SERVER_PORT, '127.0.0.1', () => {
console.log(`Cookie server listening on http://127.0.0.1:${COOKIE_SERVER_PORT}`);
});
cookieServer.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`Cookie server error: Port ${COOKIE_SERVER_PORT} is already in use`);
} else {
console.error('Cookie server error:', err.message);
}
});
}
function createWindow() {
mainWindow = new BrowserWindow({
width: 1400,
height: 900,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
},
icon: path.join(__dirname, 'assets', 'icon.png'), // Optional icon
title: 'Alta Video Camera Proxy with API'
});
mainWindow.loadFile('index.html');
// Open DevTools in development
if (process.argv.includes('--dev')) {
mainWindow.webContents.openDevTools();
}
}
app.whenReady().then(() => {
createWindow();
startCookieServer();
});
app.on('before-quit', () => {
if (cookieServer) {
cookieServer.close();
}
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// IPC handlers for API communication
ipcMain.handle('api-get-devices', async (event, { deploymentUrl, cookies }) => {
try {
const devicesUrl = `${deploymentUrl}/api/v1/devices`;
// Create axios instance with cookies
const axiosInstance = axios.create({
timeout: 10000,
headers: {
'Cookie': cookies ? cookies.join('; ') : ''
}
});
const response = await axiosInstance.get(devicesUrl);
return {
success: true,
devices: response.data,
message: `Found ${response.data.length} devices`
};
} catch (error) {
console.error('Get devices error:', error);
return {
success: false,
message: error.response?.data?.message || error.message || 'Failed to get devices'
};
}
});
ipcMain.handle('api-get-device-sites', async (event, { deploymentUrl, cookies }) => {
try {
const sitesUrl = `${deploymentUrl}/api/v1/deviceSites`;
const axiosInstance = axios.create({
timeout: 10000,
headers: {
'Cookie': cookies ? cookies.join('; ') : ''
}
});
const response = await axiosInstance.get(sitesUrl);
return {
success: true,
sites: response.data
};
} catch (error) {
console.error('Get device sites error:', error);
return {
success: false,
sites: [],
message: error.response?.data?.message || error.message || 'Failed to get device sites'
};
}
});
ipcMain.handle('api-get-auth-info', async (event, { deploymentUrl, cookies }) => {
try {
const authUrl = `${deploymentUrl}/api/v1/auth`;
const axiosInstance = axios.create({
timeout: 10000,
headers: {
'Cookie': cookies ? cookies.join('; ') : ''
}
});
const response = await axiosInstance.get(authUrl);
return {
success: true,
authInfo: response.data
};
} catch (error) {
console.error('Get auth info error:', error);
return {
success: false,
message: error.response?.data?.message || error.message || 'Failed to get auth info'
};
}
});
// Cookie-based camera proxy functionality
ipcMain.handle('camera-proxy-cookie-launch', async (event, { deploymentUrl, cookieKey, deviceUuid }) => {
try {
// Path to the cookie-based camera proxy executable
const proxyExePath = path.join(getAppDirectory(), 'aware-cam-proxy.exe');
// Check if the executable exists
if (!fs.existsSync(proxyExePath)) {
return {
success: false,
message: 'Cookie-based camera proxy executable not found. Please ensure aware-cam-proxy.exe is in the application directory.'
};
}
// Extract the domain from the deployment URL
let domain = deploymentUrl;
if (domain.startsWith('https://')) {
domain = domain.substring(8);
} else if (domain.startsWith('http://')) {
domain = domain.substring(7);
}
// Remove trailing path segments
domain = domain.split('/')[0];
// Sanitize all inputs before embedding in batch file
const safeDomain = sanitizeBatchInput(domain);
const safeDeviceUuid = sanitizeBatchInput(deviceUuid);
const safeCookieKey = sanitizeBatchInput(cookieKey);
if (!safeDomain || !safeDeviceUuid || !safeCookieKey) {
return {
success: false,
message: 'Invalid characters detected in connection parameters.'
};
}
// Create a batch file to launch the cookie-based camera proxy
const truncatedKey = safeCookieKey.length > 20 ? safeCookieKey.substring(0, 20) + '...' : safeCookieKey;
const batchContent = `@echo off
title APT-Proxy-${safeDeviceUuid}
echo Launching Alta Video Camera Proxy (Cookie Method)...
echo Domain: ${safeDomain}
echo Device UUID: ${safeDeviceUuid}
echo Cookie Key: ${truncatedKey}
echo.
"${proxyExePath}" -a "${safeDomain}" -d "${safeDeviceUuid}" -k "${safeCookieKey}"
echo.
echo Cookie-based camera proxy has finished. Press any key to close this window.
pause >nul`;
const tempDir = os.tmpdir();
const batchPath = path.join(tempDir, `cookie-proxy-${Date.now()}.bat`);
// Write the batch file
fs.writeFileSync(batchPath, batchContent);
console.log('Launching cookie-based camera proxy via batch file:', batchPath);
console.log('Command will be: aware-cam-proxy.exe -a', safeDomain, '-d', safeDeviceUuid, '-k [REDACTED]');
// Launch the batch file in a new command prompt window with unique title
const windowTitle = `APT-Proxy-${safeDeviceUuid}`;
const cmdProcess = spawn('cmd', ['/c', 'start', `"${windowTitle}"`, 'cmd', '/c', batchPath], {
detached: true,
stdio: 'ignore'
});
// Store the process information for later termination
const processInfo = {
process: cmdProcess,
batchPath: batchPath,
deviceUuid: safeDeviceUuid,
startTime: Date.now(),
cookieKey: truncatedKey,
domain: safeDomain,
windowTitle: windowTitle, // Store window title for targeted cleanup
type: 'cookie' // Mark as cookie-based proxy
};
activeProxyProcesses.set(cmdProcess.pid, processInfo);
// Clean up the batch file after a delay
setTimeout(() => {
try {
if (fs.existsSync(batchPath)) {
fs.unlinkSync(batchPath);
}
} catch (error) {
console.log('Could not clean up cookie proxy batch file:', error.message);
}
}, 60000); // Clean up after 1 minute
cmdProcess.unref(); // Allow the parent process to exit independently
// Clean up process tracking when it exits
cmdProcess.on('exit', () => {
activeProxyProcesses.delete(cmdProcess.pid);
});
return {
success: true,
message: `Cookie-based camera proxy launched for ${deviceUuid}!`,
processId: cmdProcess.pid,
deviceUuid: deviceUuid,
type: 'cookie'
};
} catch (error) {
console.error('Failed to launch cookie-based camera proxy:', error);
return {
success: false,
message: `Failed to launch cookie-based camera proxy: ${error.message}`
};
}
});
// Stop camera proxy functionality
ipcMain.handle('camera-proxy-stop', async (event, { processId }) => {
try {
console.log('Attempting to stop camera proxy processes...');
return new Promise((resolve) => {
// Kill all aware-cam-proxy.exe processes by name
const killProxy = spawn('taskkill', ['/f', '/im', 'aware-cam-proxy.exe'], {
stdio: ['ignore', 'pipe', 'pipe']
});
let proxyOutput = '';
let proxyError = '';
killProxy.stdout.on('data', (data) => {
proxyOutput += data.toString();
});
killProxy.stderr.on('data', (data) => {
proxyError += data.toString();
});
killProxy.on('close', (code) => {
// Clean up our process tracking
activeProxyProcesses.clear();
if (code === 0 || proxyOutput.includes('SUCCESS')) {
console.log('Camera proxy processes terminated successfully');
resolve({
success: true,
message: 'Camera proxy processes stopped successfully'
});
} else if (proxyError.includes('not found') || proxyError.includes('No tasks')) {
console.log('No camera proxy processes were running');
resolve({
success: true,
message: 'No camera proxy processes were running'
});
} else {
resolve({
success: true,
message: 'Attempted to stop all camera proxy processes'
});
}
});
killProxy.on('error', (error) => {
console.error('Error with taskkill by name:', error);
activeProxyProcesses.clear();
resolve({
success: false,
message: `Failed to stop camera proxy: ${error.message}`
});
});
});
} catch (error) {
console.error('Failed to stop camera proxy:', error);
return {
success: false,
message: `Failed to stop camera proxy: ${error.message}`
};
}
});
// --- Self-Update Functionality ---
// Compare semver versions: returns -1 if a < b, 0 if equal, 1 if a > b
function compareVersions(a, b) {
// Strip pre-release tags (e.g. "1.2.3-beta.1" → "1.2.3")
const cleanA = a.replace(/-.*$/, '');
const cleanB = b.replace(/-.*$/, '');
const partsA = cleanA.split('.').map(Number);
const partsB = cleanB.split('.').map(Number);
for (let i = 0; i < 3; i++) {
const numA = partsA[i] || 0;
const numB = partsB[i] || 0;
if (numA < numB) return -1;
if (numA > numB) return 1;
}
return 0;
}
// Follow HTTPS redirects and return the final response (for GitHub asset downloads)
function httpsGetFollowRedirects(url, callback, redirectCount = 0) {
if (redirectCount >= 5) {
return callback(null, new Error('Too many redirects'));
}
const parsed = new URL(url);
if (parsed.protocol !== 'https:') {
return callback(null, new Error('Only HTTPS URLs are allowed'));
}
https.get(url, { headers: { 'User-Agent': 'Alta-Proxy-Tool' } }, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
const redirectUrl = new URL(res.headers.location, url).href;
httpsGetFollowRedirects(redirectUrl, callback, redirectCount + 1);
} else {
callback(res);
}
}).on('error', (err) => {
callback(null, err);
});
}
ipcMain.handle('get-current-version', async () => {
return { success: true, version: app.getVersion() };
});
ipcMain.handle('check-for-updates', async () => {
try {
const response = await axios.get(
'https://api.github.com/repos/PageZ948/Alta-Proxy-Tool/releases/latest',
{
timeout: 10000,
headers: { 'User-Agent': 'Alta-Proxy-Tool', 'Accept': 'application/vnd.github.v3+json' }
}
);
const release = response.data;
const latestVersion = release.tag_name.replace(/^v/, '');
const currentVersion = app.getVersion();
const updateAvailable = compareVersions(currentVersion, latestVersion) < 0;
// Find the portable .exe asset
const exeAsset = release.assets.find(a => /AltaCameraProxy-.*-portable\.exe$/i.test(a.name));
const downloadUrl = exeAsset ? exeAsset.browser_download_url : null;
return {
success: true,
updateAvailable,
currentVersion,
latestVersion,
downloadUrl,
releaseNotes: release.body || '',
releaseName: release.name || `v${latestVersion}`
};
} catch (error) {
if (error.response && error.response.status === 404) {
return { success: true, updateAvailable: false, currentVersion: app.getVersion(), message: 'No releases available yet' };
}
if (error.response && error.response.status === 403) {
return { success: false, message: 'GitHub API rate limit exceeded. Try again later.' };
}
console.error('Check for updates error:', error.message);
return { success: false, message: error.message || 'Failed to check for updates' };
}
});
ipcMain.handle('download-and-install-update', async (event, { downloadUrl }) => {
try {
// Determine the path to the currently running executable
const currentExePath = process.env.PORTABLE_EXECUTABLE_FILE || app.getPath('exe');
const currentDir = getAppDirectory();
const currentExeName = path.basename(currentExePath);
// Check write permission on the app directory
try {
fs.accessSync(currentDir, fs.constants.W_OK);
} catch {
return { success: false, message: 'No write permission to the application directory. Try running as administrator.' };
}
const tempDir = os.tmpdir();
const tempExePath = path.join(tempDir, `AltaCameraProxy-update-${Date.now()}.exe`);
// Download the file with progress reporting
await new Promise((resolve, reject) => {
httpsGetFollowRedirects(downloadUrl, (res, err) => {
if (err) return reject(err);
if (res.statusCode !== 200) {
res.resume();
return reject(new Error(`Download failed with status ${res.statusCode}`));
}
const totalSize = parseInt(res.headers['content-length'], 10) || 0;
let downloadedSize = 0;
const fileStream = fs.createWriteStream(tempExePath);
res.on('data', (chunk) => {
downloadedSize += chunk.length;
if (totalSize > 0 && mainWindow && !mainWindow.isDestroyed()) {
const percent = Math.round((downloadedSize / totalSize) * 100);
mainWindow.webContents.send('update-download-progress', { percent, downloadedSize, totalSize });
}
});
res.pipe(fileStream);
fileStream.on('finish', () => {
fileStream.close();
resolve();
});
fileStream.on('error', (err) => {
fs.unlink(tempExePath, () => {});
reject(err);
});
});
});
// Verify downloaded file size (sanity check: > 10MB for an Electron portable exe)
const stats = fs.statSync(tempExePath);
if (stats.size < 10 * 1024 * 1024) {
fs.unlinkSync(tempExePath);
return { success: false, message: 'Downloaded file is too small — update may be corrupt.' };
}
// Create batch script to replace the exe after this process exits
const batchPath = path.join(tempDir, `apt-update-${Date.now()}.bat`);
const pid = process.pid;
const batchContent = `@echo off\r\ntitle APT-Updater\r\necho Waiting for Alta Proxy Tool to close...\r\n:waitloop\r\ntasklist /fi "PID eq ${pid}" 2>nul | find "${pid}" >nul\r\nif not errorlevel 1 (\r\n timeout /t 1 /nobreak >nul\r\n goto waitloop\r\n)\r\necho Applying update...\r\ncopy /y "${tempExePath}" "${path.join(currentDir, currentExeName)}"\r\nif errorlevel 1 (\r\n echo Update failed! Could not copy new version.\r\n pause\r\n del "${tempExePath}" >nul 2>&1\r\n del "%~f0" >nul 2>&1\r\n exit /b 1\r\n)\r\necho Update complete. Launching new version...\r\nstart "" "${path.join(currentDir, currentExeName)}"\r\ndel "${tempExePath}" >nul 2>&1\r\ndel "%~f0" >nul 2>&1\r\n`;
fs.writeFileSync(batchPath, batchContent);
// Spawn the updater batch script detached
let updater;
try {
updater = spawn('cmd', ['/c', batchPath], {
detached: true,
stdio: 'ignore',
windowsHide: true
});
updater.unref();
} catch (spawnError) {
console.error('Failed to spawn updater:', spawnError);
try { fs.unlinkSync(tempExePath); } catch {}
try { fs.unlinkSync(batchPath); } catch {}
return { success: false, message: 'Failed to start updater process.' };
}
// Quit the app after a delay to let the IPC response return to renderer
setTimeout(() => {
app.quit();
}, 1500);
return { success: true, message: 'Update is being installed. The app will restart shortly.' };
} catch (error) {
console.error('Download and install update error:', error);
return { success: false, message: error.message || 'Failed to download and install update' };
}
});
-5824
View File
File diff suppressed because it is too large Load Diff
-47
View File
@@ -1,47 +0,0 @@
{
"name": "alta-api-client",
"version": "1.0.0",
"description": "Electron app for connecting to Alta API",
"main": "main.js",
"scripts": {
"start": "electron .",
"dev": "electron . --dev",
"build": "electron-builder --win --publish=never",
"build-test": "electron-builder --win --dir",
"build-kit": "powershell -ExecutionPolicy Bypass -File build-kit.ps1",
"prebuild": "echo Checking build requirements..."
},
"build": {
"appId": "com.internal.alta-camera-proxy",
"productName": "Alta Camera Proxy",
"directories": {
"output": "dist"
},
"win": {
"target": "portable",
"icon": "assets/icon.png",
"signAndEditExecutable": false
},
"portable": {
"artifactName": "AltaCameraProxy-${version}-portable.exe"
},
"forceCodeSigning": false
},
"keywords": [
"electron",
"alta",
"api",
"avigilon"
],
"author": "Your Name",
"license": "MIT",
"devDependencies": {
"electron": "^28.0.0",
"electron-builder": "^26.4.0",
"electron-packager": "^17.1.2"
},
"dependencies": {
"axios": "^1.6.0",
"crypto-js": "^4.2.0"
}
}
-38
View File
@@ -1,38 +0,0 @@
const { contextBridge, ipcRenderer } = require('electron');
// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld('electronAPI', {
getDevices: (params) => ipcRenderer.invoke('api-get-devices', params),
getDeviceSites: (params) => ipcRenderer.invoke('api-get-device-sites', params),
getAuthInfo: (params) => ipcRenderer.invoke('api-get-auth-info', params),
// Camera proxy functionality
launchCookieCameraProxy: (params) => ipcRenderer.invoke('camera-proxy-cookie-launch', params),
stopCameraProxy: (processId) => ipcRenderer.invoke('camera-proxy-stop', { processId }),
// Extension cookie bridge (push from main process)
onExtensionCookie: (callback) => {
ipcRenderer.on('extension-cookie-received', (event, data) => {
try {
callback(data);
} catch (error) {
console.error('Extension cookie handler error:', error);
}
});
},
// Self-update functionality
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
downloadAndInstallUpdate: (params) => ipcRenderer.invoke('download-and-install-update', params),
getCurrentVersion: () => ipcRenderer.invoke('get-current-version'),
onUpdateDownloadProgress: (callback) => {
ipcRenderer.on('update-download-progress', (event, data) => {
try {
callback(data);
} catch (error) {
console.error('Update download progress handler error:', error);
}
});
}
});
-764
View File
@@ -1,764 +0,0 @@
// Global variables to store session data
let sessionData = {
deploymentUrl: '',
cookies: null,
isConnected: false
};
// DOM elements
const connectionStatus = document.getElementById('connectionStatus');
const deviceStatus = document.getElementById('deviceStatus');
const deviceList = document.getElementById('deviceList');
const statusIndicator = document.getElementById('statusIndicator');
const deviceSearch = document.getElementById('deviceSearch');
// Connection buttons
const disconnectBtn = document.getElementById('disconnectBtn');
// Cookie proxy elements
const cookieDeviceUUID = document.getElementById('cookieDeviceUUID');
const cookieKey = document.getElementById('cookieKey');
const startCookieProxyBtn = document.getElementById('startCookieProxyBtn');
const stopCookieProxyBtn = document.getElementById('stopCookieProxyBtn');
// Update elements
const checkUpdateBtn = document.getElementById('checkUpdateBtn');
const updateModalOverlay = document.getElementById('updateModalOverlay');
const updateModalCloseBtn = document.getElementById('updateModalCloseBtn');
const updateModalMessage = document.getElementById('updateModalMessage');
const updateModalNotes = document.getElementById('updateModalNotes');
const updateProgressContainer = document.getElementById('updateProgressContainer');
const updateProgressFill = document.getElementById('updateProgressFill');
const updateProgressText = document.getElementById('updateProgressText');
const updateInstallBtn = document.getElementById('updateInstallBtn');
const updateLaterBtn = document.getElementById('updateLaterBtn');
// Track selected device
let selectedDevice = null;
let activeCookieProxyConnections = new Map(); // Track cookie-based proxy connections
let allDevices = []; // Store all devices for search functionality
let allSites = {}; // Store site id → site name mapping
let collapsedSites = new Set(); // Track which site groups are collapsed
let pendingUpdateInfo = null; // Store update info for install action
// Event listeners
disconnectBtn.addEventListener('click', handleDisconnect);
// Cookie proxy event listeners
startCookieProxyBtn.addEventListener('click', handleStartCookieProxy);
stopCookieProxyBtn.addEventListener('click', handleStopCookieProxy);
// Cookie key input listener to update button states
cookieKey.addEventListener('input', updateCookieProxyButtonStates);
// Device search event listener
deviceSearch.addEventListener('input', handleDeviceSearch);
// Update event listeners
checkUpdateBtn.addEventListener('click', handleCheckForUpdates);
updateInstallBtn.addEventListener('click', handleInstallUpdate);
updateLaterBtn.addEventListener('click', closeUpdateModal);
updateModalCloseBtn.addEventListener('click', closeUpdateModal);
// Handle disconnect
function handleDisconnect() {
sessionData.isConnected = false;
sessionData.cookies = null;
sessionData.deploymentUrl = '';
selectedDevice = null;
activeCookieProxyConnections.clear();
allDevices = []; // Clear stored devices
allSites = {}; // Clear site data
collapsedSites.clear();
updateConnectionStatus(false);
updateButtonStates();
clearDeviceList();
cookieDeviceUUID.value = '';
cookieKey.value = '';
deviceSearch.value = ''; // Clear search input
// Clear device status message
deviceStatus.style.display = 'none';
deviceStatus.textContent = '';
showStatus(connectionStatus, 'Disconnected from API', 'info');
}
// Handle start cookie proxy
async function handleStartCookieProxy() {
if (!selectedDevice) {
showStatus(connectionStatus, 'Please select a device first', 'error');
return;
}
const cookieKeyValue = cookieKey.value.trim();
if (!cookieKeyValue) {
showStatus(connectionStatus, 'Please enter a cookie key', 'error');
return;
}
if (!sessionData.deploymentUrl) {
showStatus(connectionStatus, 'Please connect to API first to get deployment URL', 'error');
return;
}
// Check if this device already has an active cookie connection
const deviceId = selectedDevice.guid || selectedDevice.id;
if (activeCookieProxyConnections.has(deviceId)) {
showStatus(connectionStatus, `Cookie proxy already running for device ${selectedDevice.name}`, 'warning');
return;
}
// Disable button during launch
startCookieProxyBtn.disabled = true;
showStatus(connectionStatus, `Starting cookie proxy for device ${selectedDevice.name}...`, 'info');
try {
const result = await window.electronAPI.launchCookieCameraProxy({
deploymentUrl: sessionData.deploymentUrl,
cookieKey: cookieKeyValue,
deviceUuid: deviceId
});
if (result.success) {
// Track this connection
activeCookieProxyConnections.set(deviceId, {
processId: result.processId,
deviceName: selectedDevice.name,
deviceId: deviceId,
startTime: Date.now(),
type: 'cookie'
});
updateCookieProxyButtonStates();
showStatus(connectionStatus, `${result.message} (Cookie proxy active for ${selectedDevice.name})`, 'success');
} else {
showStatus(connectionStatus, `Failed to start cookie proxy: ${result.message}`, 'error');
}
} catch (error) {
console.error('Cookie proxy launch error:', error);
showStatus(connectionStatus, `Error launching cookie proxy: ${error.message}`, 'error');
} finally {
startCookieProxyBtn.disabled = false;
}
}
// Handle stop cookie proxy
async function handleStopCookieProxy() {
if (activeCookieProxyConnections.size === 0) {
showStatus(connectionStatus, 'No active cookie proxy connections found', 'warning');
return;
}
showStatus(connectionStatus, 'Stopping cookie proxy connections...', 'info');
try {
const result = await window.electronAPI.stopCameraProxy(null); // Stop all processes
if (result.success) {
activeCookieProxyConnections.clear();
showStatus(connectionStatus, 'Cookie proxy connections stopped successfully', 'success');
// Update visual indicators for all devices
const deviceItems = deviceList.querySelectorAll('.device-item');
deviceItems.forEach(item => {
item.classList.remove('cookie-proxy-active');
});
} else {
showStatus(connectionStatus, `Failed to stop cookie proxy: ${result.message}`, 'warning');
}
} catch (error) {
console.error('Stop cookie proxy error:', error);
showStatus(connectionStatus, 'Error stopping cookie proxy', 'error');
}
updateCookieProxyButtonStates();
}
// Update cookie proxy button states
function updateCookieProxyButtonStates() {
const hasSelectedDevice = selectedDevice !== null;
const hasCookieKey = cookieKey.value.trim().length > 0;
const hasActiveConnection = activeCookieProxyConnections.size > 0;
if (sessionData.isConnected && hasSelectedDevice && hasCookieKey) {
const deviceId = selectedDevice.guid || selectedDevice.id;
const isThisDeviceActive = activeCookieProxyConnections.has(deviceId);
startCookieProxyBtn.disabled = isThisDeviceActive;
stopCookieProxyBtn.disabled = !hasActiveConnection;
} else {
startCookieProxyBtn.disabled = true;
stopCookieProxyBtn.disabled = !hasActiveConnection;
}
}
// Update connection status indicator
function updateConnectionStatus(connected) {
const statusDot = statusIndicator.querySelector('.status-dot');
const statusText = statusIndicator.querySelector('.status-text');
if (connected) {
statusDot.className = 'status-dot online';
statusText.textContent = 'Connected';
} else {
statusDot.className = 'status-dot offline';
statusText.textContent = 'Disconnected';
}
}
// Update button states based on connection status
function updateButtonStates() {
if (sessionData.isConnected) {
disconnectBtn.disabled = false;
} else {
disconnectBtn.disabled = true;
startCookieProxyBtn.disabled = true;
stopCookieProxyBtn.disabled = true;
}
// Always update cookie proxy button states
updateCookieProxyButtonStates();
}
// Fetch device sites and build id → name map
async function fetchDeviceSites() {
try {
const result = await window.electronAPI.getDeviceSites({
deploymentUrl: sessionData.deploymentUrl,
cookies: sessionData.cookies
});
if (result.success && Array.isArray(result.sites)) {
allSites = {};
result.sites.forEach(site => {
if (site.id) {
allSites[site.id] = site.name || 'Unnamed Site';
}
});
}
} catch (error) {
console.log('Could not fetch device sites:', error.message);
allSites = {};
}
}
// Handle get devices (now called automatically)
async function handleGetDevices() {
if (!sessionData.isConnected) {
showStatus(deviceStatus, 'Please connect to the API first', 'error');
return;
}
showStatus(deviceStatus, 'Fetching devices...', 'info');
clearDeviceList();
try {
// Fetch devices and sites in parallel
const [devicesResult] = await Promise.all([
window.electronAPI.getDevices({
deploymentUrl: sessionData.deploymentUrl,
cookies: sessionData.cookies
}),
fetchDeviceSites()
]);
if (devicesResult.success) {
// Filter devices to only show non-cloud cameras (localStorage = false)
const filteredDevices = devicesResult.devices.filter(device => {
// Check if device has capabilities and localStorage property
if (device.capabilities && device.capabilities.localStorage !== undefined) {
// Only show devices where localStorage is false (non-cloud cameras)
return device.capabilities.localStorage === false;
}
// If no capabilities or localStorage property, include the device (fallback)
return true;
});
const totalDevices = devicesResult.devices.length;
const filteredCount = filteredDevices.length;
const cloudDevicesHidden = totalDevices - filteredCount;
const siteCount = Object.keys(allSites).length;
let statusMessage = `Found ${filteredCount} local camera${filteredCount !== 1 ? 's' : ''}`;
if (siteCount > 0) {
statusMessage += ` across ${siteCount} site${siteCount !== 1 ? 's' : ''}`;
}
if (cloudDevicesHidden > 0) {
statusMessage += ` (${cloudDevicesHidden} cloud camera${cloudDevicesHidden !== 1 ? 's' : ''} hidden)`;
}
showStatus(deviceStatus, statusMessage, 'success');
// Store all devices for search functionality
allDevices = filteredDevices;
displayDevices(filteredDevices);
} else {
showStatus(deviceStatus, `Failed to get devices: ${devicesResult.message}`, 'error');
}
} catch (error) {
console.error('Get devices error:', error);
showStatus(deviceStatus, `Error getting devices: ${error.message}`, 'error');
}
}
// Helper function to determine device status from API data
function getDeviceStatus(device) {
// Check for live.display_status first (Alta API standard)
if (device.live && device.live.display_status) {
const status = device.live.display_status.toLowerCase();
// Handle color-based status responses from Alta API
if (status === 'green') {
return {
isOnline: true,
statusText: 'Online'
};
} else if (status === 'red') {
return {
isOnline: false,
statusText: 'Offline'
};
} else if (status === 'yellow' || status === 'orange') {
return {
isOnline: false,
statusText: 'Warning'
};
}
// Handle text-based status responses
return {
isOnline: status === 'online' || status === 'live' || status === 'connected',
statusText: status === 'online' || status === 'live' || status === 'connected' ? 'Online' : 'Offline'
};
}
// Fallback to other possible status fields
if (device.online !== undefined) {
return {
isOnline: device.online,
statusText: device.online ? 'Online' : 'Offline'
};
}
if (device.status) {
const status = device.status.toLowerCase();
// Handle color-based status in other fields
if (status === 'green') {
return {
isOnline: true,
statusText: 'Online'
};
} else if (status === 'red') {
return {
isOnline: false,
statusText: 'Offline'
};
}
return {
isOnline: status === 'online' || status === 'live' || status === 'connected',
statusText: status === 'online' || status === 'live' || status === 'connected' ? 'Online' : 'Offline'
};
}
// Default to offline if no status information available
return {
isOnline: false,
statusText: 'Offline'
};
}
// Handle device search
function handleDeviceSearch() {
const searchTerm = deviceSearch.value.toLowerCase().trim();
if (!searchTerm) {
// Show all devices if search is empty
displayDevices(allDevices);
return;
}
// Filter devices based on search term (includes site name)
const filteredDevices = allDevices.filter(device => {
const deviceName = (device.name || '').toLowerCase();
const deviceId = (device.guid || device.id || '').toLowerCase();
const deviceType = (device.type || '').toLowerCase();
const deviceModel = (device.model || '').toLowerCase();
const deviceIp = (device.ipAddress || '').toLowerCase();
const siteName = (device.server_group_id && allSites[device.server_group_id] || '').toLowerCase();
return deviceName.includes(searchTerm) ||
deviceId.includes(searchTerm) ||
deviceType.includes(searchTerm) ||
deviceModel.includes(searchTerm) ||
deviceIp.includes(searchTerm) ||
siteName.includes(searchTerm);
});
displayDevices(filteredDevices);
}
// Group devices by their site using server_group_id → allSites mapping
function groupDevicesBySite(devices) {
const groups = {};
const ungrouped = [];
devices.forEach(device => {
const siteId = device.server_group_id;
const siteName = siteId && allSites[siteId] ? allSites[siteId] : null;
if (siteName) {
if (!groups[siteId]) {
groups[siteId] = { name: siteName, devices: [] };
}
groups[siteId].devices.push(device);
} else {
ungrouped.push(device);
}
});
return { groups, ungrouped };
}
// Create a device item DOM element
function createDeviceItem(device, index) {
const deviceItem = document.createElement('div');
deviceItem.className = 'device-item';
deviceItem.dataset.deviceIndex = index;
deviceItem.dataset.deviceId = device.guid || device.id;
const status = getDeviceStatus(device);
const deviceId = device.guid || device.id;
const isCookieProxyActive = activeCookieProxyConnections.has(deviceId);
if (isCookieProxyActive) {
deviceItem.classList.add('cookie-proxy-active');
}
deviceItem.innerHTML = `
<div class="device-name">${escapeHtml(device.name || 'Unnamed Device')}</div>
<div class="device-status-dot ${status.isOnline ? 'online' : 'offline'}"></div>
`;
deviceItem.addEventListener('click', () => selectDevice(device, deviceItem));
return deviceItem;
}
// Display devices in the UI, grouped by site
function displayDevices(devices) {
clearDeviceList();
if (!devices || devices.length === 0) {
const searchTerm = deviceSearch.value.toLowerCase().trim();
const message = searchTerm ? 'No devices match your search' : 'No devices found';
deviceList.innerHTML = `<p class="no-devices">${message}</p>`;
return;
}
const hasSites = Object.keys(allSites).length > 0;
// If no sites were fetched, fall back to flat list
if (!hasSites) {
devices.forEach((device, index) => {
deviceList.appendChild(createDeviceItem(device, index));
});
updateCookieProxyButtonStates();
return;
}
const { groups, ungrouped } = groupDevicesBySite(devices);
// Sort site groups alphabetically by name
const sortedSiteIds = Object.keys(groups).sort((a, b) =>
groups[a].name.localeCompare(groups[b].name)
);
let globalIndex = 0;
sortedSiteIds.forEach(siteId => {
const group = groups[siteId];
const isCollapsed = collapsedSites.has(siteId);
// Site header
const siteHeader = document.createElement('div');
siteHeader.className = 'site-group-header' + (isCollapsed ? ' collapsed' : '');
siteHeader.innerHTML = `
<span class="site-group-arrow">${isCollapsed ? '\u25B6' : '\u25BC'}</span>
<span class="site-group-name">${escapeHtml(group.name)}</span>
<span class="site-group-count">${group.devices.length}</span>
`;
siteHeader.addEventListener('click', () => {
if (collapsedSites.has(siteId)) {
collapsedSites.delete(siteId);
} else {
collapsedSites.add(siteId);
}
// Re-render with current search filter
const searchTerm = deviceSearch.value.toLowerCase().trim();
if (searchTerm) {
handleDeviceSearch();
} else {
displayDevices(allDevices);
}
});
deviceList.appendChild(siteHeader);
// Device items (hidden if collapsed)
if (!isCollapsed) {
group.devices.forEach(device => {
deviceList.appendChild(createDeviceItem(device, globalIndex++));
});
}
});
// Ungrouped devices at the bottom
if (ungrouped.length > 0) {
if (sortedSiteIds.length > 0) {
const ungroupedHeader = document.createElement('div');
ungroupedHeader.className = 'site-group-header' + (collapsedSites.has('__ungrouped') ? ' collapsed' : '');
ungroupedHeader.innerHTML = `
<span class="site-group-arrow">${collapsedSites.has('__ungrouped') ? '\u25B6' : '\u25BC'}</span>
<span class="site-group-name">Ungrouped</span>
<span class="site-group-count">${ungrouped.length}</span>
`;
ungroupedHeader.addEventListener('click', () => {
if (collapsedSites.has('__ungrouped')) {
collapsedSites.delete('__ungrouped');
} else {
collapsedSites.add('__ungrouped');
}
const searchTerm = deviceSearch.value.toLowerCase().trim();
if (searchTerm) {
handleDeviceSearch();
} else {
displayDevices(allDevices);
}
});
deviceList.appendChild(ungroupedHeader);
}
if (!collapsedSites.has('__ungrouped') || sortedSiteIds.length === 0) {
ungrouped.forEach(device => {
deviceList.appendChild(createDeviceItem(device, globalIndex++));
});
}
}
updateCookieProxyButtonStates();
}
// Handle device selection
function selectDevice(device, deviceElement) {
// Remove previous selection
const previousSelected = deviceList.querySelector('.device-item.selected');
if (previousSelected) {
previousSelected.classList.remove('selected');
}
// Select current device
deviceElement.classList.add('selected');
selectedDevice = device;
// Auto-populate cookie device UUID field
const uuid = device.guid || device.id || '';
cookieDeviceUUID.value = uuid;
// Show device selection feedback with connection status
if (uuid) {
const isCookieActive = activeCookieProxyConnections.has(uuid);
const cookieStatusText = isCookieActive ? ' (COOKIE PROXY ACTIVE)' : '';
showStatus(connectionStatus, `Selected device: ${device.name || 'Unnamed Device'} (UUID: ${uuid})${cookieStatusText}`, 'info');
}
updateCookieProxyButtonStates();
}
// Utility functions
function showStatus(element, message, type) {
element.textContent = message;
element.className = `status-message ${type}`;
element.style.display = 'block';
}
function clearDeviceList() {
deviceList.innerHTML = '<p class="placeholder-text">Connect to API to load devices</p>';
selectedDevice = null;
}
function escapeHtml(text) {
if (typeof text !== 'string') return text;
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// --- Self-Update Functions ---
async function handleCheckForUpdates() {
checkUpdateBtn.disabled = true;
try {
const result = await window.electronAPI.checkForUpdates();
if (!result.success) {
showStatus(connectionStatus, result.message || 'Failed to check for updates', 'error');
return;
}
if (result.message === 'No releases available yet') {
showStatus(connectionStatus, 'No releases available yet', 'info');
return;
}
if (result.updateAvailable) {
showUpdateModal(result);
} else {
showStatus(connectionStatus, `You're on the latest version (v${result.currentVersion})`, 'success');
}
} catch (error) {
console.error('Check for updates error:', error);
showStatus(connectionStatus, 'Could not check for updates. Check your internet connection.', 'error');
} finally {
checkUpdateBtn.disabled = false;
}
}
function showUpdateModal(updateInfo) {
pendingUpdateInfo = updateInfo;
updateModalMessage.textContent = `A new version is available: v${updateInfo.latestVersion} (current: v${updateInfo.currentVersion})`;
updateModalNotes.textContent = updateInfo.releaseNotes || '';
// Reset progress state
updateProgressContainer.style.display = 'none';
updateProgressFill.style.width = '0%';
updateProgressText.textContent = '0%';
// Reset button states
updateInstallBtn.disabled = !updateInfo.downloadUrl;
updateLaterBtn.disabled = false;
updateInstallBtn.textContent = 'Install Update';
if (!updateInfo.downloadUrl) {
updateModalMessage.textContent += '\n(No downloadable asset found for this release)';
}
updateModalOverlay.style.display = 'flex';
}
async function handleInstallUpdate() {
if (!pendingUpdateInfo || !pendingUpdateInfo.downloadUrl) return;
// Disable controls during download
updateInstallBtn.disabled = true;
updateInstallBtn.textContent = 'Downloading...';
updateLaterBtn.disabled = true;
updateModalCloseBtn.style.display = 'none';
updateProgressContainer.style.display = 'flex';
try {
const result = await window.electronAPI.downloadAndInstallUpdate({
downloadUrl: pendingUpdateInfo.downloadUrl
});
if (result.success) {
updateInstallBtn.textContent = 'Restarting...';
updateProgressFill.style.width = '100%';
updateProgressText.textContent = '100%';
} else {
showStatus(connectionStatus, `Update failed: ${result.message}`, 'error');
closeUpdateModal();
}
} catch (error) {
console.error('Install update error:', error);
showStatus(connectionStatus, 'Update failed. Please try again.', 'error');
closeUpdateModal();
}
}
function closeUpdateModal() {
updateModalOverlay.style.display = 'none';
pendingUpdateInfo = null;
updateProgressContainer.style.display = 'none';
updateProgressFill.style.width = '0%';
updateProgressText.textContent = '0%';
updateInstallBtn.textContent = 'Install Update';
updateInstallBtn.disabled = false;
updateLaterBtn.disabled = false;
updateModalCloseBtn.style.display = '';
}
async function checkForUpdatesOnStartup() {
try {
const result = await window.electronAPI.checkForUpdates();
if (result.success && result.updateAvailable) {
checkUpdateBtn.classList.add('update-available');
checkUpdateBtn.title = `Update available: v${result.latestVersion}`;
showStatus(connectionStatus, `Update available: v${result.latestVersion}`, 'info');
// Store for quick modal open
pendingUpdateInfo = result;
}
} catch (error) {
// Silent fail on startup check
console.log('Startup update check failed:', error.message);
}
}
// Handle cookie received from Chrome extension via local HTTP bridge
async function handleExtensionCookie(data) {
const { deploymentUrl, cookies, cookieValue } = data;
// If already connected, disconnect first
if (sessionData.isConnected) {
handleDisconnect();
}
// Set session state from extension cookie
sessionData.deploymentUrl = deploymentUrl;
sessionData.cookies = cookies;
sessionData.isConnected = true;
showStatus(connectionStatus, `Connected via Chrome extension to ${deploymentUrl}`, 'success');
updateConnectionStatus(true);
updateButtonStates();
// Auto-populate cookie key
cookieKey.value = cookieValue;
updateCookieProxyButtonStates();
// Fetch devices
try {
await handleGetDevices();
} catch (err) {
console.error('Failed to fetch devices after extension cookie:', err);
showStatus(deviceStatus, 'Connected, but failed to load devices.', 'warning');
}
}
// Initialize the app
document.addEventListener('DOMContentLoaded', async () => {
console.log('Alta Video Camera Proxy loaded');
// Initialize connection status
updateConnectionStatus(false);
updateButtonStates();
// Listen for cookies pushed from Chrome extension
window.electronAPI.onExtensionCookie(handleExtensionCookie);
// Listen for update download progress
window.electronAPI.onUpdateDownloadProgress((data) => {
updateProgressFill.style.width = `${data.percent}%`;
updateProgressText.textContent = `${data.percent}%`;
});
// Auto-check for updates after 2 seconds
setTimeout(checkForUpdatesOnStartup, 2000);
});
-690
View File
@@ -1,690 +0,0 @@
/* Dark Mode Professional UI - Alta Video Camera Proxy */
/* CSS Variables for Dark Mode Color Palette */
:root {
--bg-primary: #1E1E1E;
--bg-secondary: #2D2D30;
--border: #3C3C3C;
--text-primary: #E0E0E0;
--text-secondary: #999999;
--accent-primary: #0E7AFE;
--accent-primary-hover: #0A5FD9;
--success: #4CAF50;
--error: #F44336;
--warning: #FF9800;
--input-bg: #1E1E1E;
--button-outline: #555555;
--card-bg: #2D2D30;
--hover-bg: #3C3C3C;
--tab-active: #0E7AFE;
}
/* Reset and Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 14px;
line-height: 1.4;
overflow: hidden;
}
/* App Container */
.app-container {
height: 100vh;
display: flex;
flex-direction: column;
}
/* Main Layout */
.main-layout {
flex: 1;
display: flex;
overflow: hidden;
}
/* Left Sidebar - Available Devices */
.devices-sidebar {
width: 220px;
background: var(--card-bg);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidebar-header {
padding: 16px;
border-bottom: 1px solid var(--border);
background: var(--bg-secondary);
}
.sidebar-header h2 {
font-size: 16px;
font-weight: bold;
color: var(--text-primary);
margin: 0;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Device Search */
.device-search-container {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
background: var(--bg-secondary);
}
.device-search-input {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--button-outline);
border-radius: 4px;
font-size: 14px;
background: var(--input-bg);
color: var(--text-primary);
transition: border-color 0.2s ease;
}
.device-search-input::placeholder {
color: var(--text-secondary);
font-style: italic;
}
.device-search-input:focus {
outline: none;
border-color: var(--accent-primary);
}
/* Device List */
.device-list-container {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.device-list {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.device-item {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 4px;
padding: 12px;
margin-bottom: 6px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
}
.device-item:hover {
background: var(--hover-bg);
border-color: var(--accent-primary);
}
.device-item.selected {
background: var(--accent-primary);
color: white;
border-color: var(--accent-primary);
}
.device-name {
font-size: 14px;
font-weight: bold;
color: inherit;
flex: 1;
}
/* Device Status Dot */
.device-status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
margin-left: 8px;
flex-shrink: 0;
}
.device-status-dot.online {
background: var(--success);
box-shadow: 0 0 6px rgba(76, 175, 80, 0.6);
}
.device-status-dot.offline {
background: var(--error);
box-shadow: 0 0 6px rgba(244, 67, 54, 0.6);
}
/* Site Group Headers */
.site-group-header {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 10px;
margin-bottom: 4px;
cursor: pointer;
border-radius: 4px;
user-select: none;
transition: background 0.15s ease;
}
.site-group-header:hover {
background: var(--hover-bg);
}
.site-group-arrow {
font-size: 10px;
color: var(--text-secondary);
width: 12px;
flex-shrink: 0;
text-align: center;
}
.site-group-name {
font-size: 11px;
font-weight: bold;
color: var(--accent-primary);
text-transform: uppercase;
letter-spacing: 0.5px;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.site-group-count {
font-size: 10px;
color: var(--text-secondary);
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 10px;
padding: 1px 6px;
flex-shrink: 0;
}
.placeholder-text {
color: var(--text-secondary);
font-style: italic;
text-align: center;
padding: 20px;
font-size: 12px;
}
/* Main Content Area */
.main-content {
flex: 1;
padding: 24px;
overflow-y: auto;
background: var(--bg-primary);
}
.content-header {
margin-bottom: 24px;
display: flex;
align-items: center;
justify-content: space-between;
}
.content-header h1 {
font-size: 24px;
font-weight: 600;
color: var(--tab-active);
margin: 0;
}
/* Content Sections */
.content-section {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 20px;
margin-bottom: 16px;
}
.content-section h2 {
font-size: 16px;
font-weight: bold;
color: var(--tab-active);
margin: 0 0 16px 0;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Buttons */
button {
font-family: inherit;
font-size: 14px;
font-weight: bold;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
text-transform: none;
}
.btn-primary {
background: var(--accent-primary);
color: white;
}
.btn-primary:hover:not(:disabled) {
background: var(--accent-primary-hover);
}
.btn-success {
background: var(--success);
color: white;
}
.btn-success:hover:not(:disabled) {
background: #45a049;
}
.btn-outline {
background: transparent;
color: var(--text-primary);
border: 1px solid var(--button-outline);
}
.btn-outline:hover:not(:disabled) {
background: var(--hover-bg);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Connection Status */
.connection-status {
margin-bottom: 16px;
}
.status-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.status-row label {
font-size: 14px;
font-weight: bold;
color: var(--text-primary);
min-width: 50px;
}
.status-indicator {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border-radius: 12px;
background: var(--bg-secondary);
border: 1px solid var(--border);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--error);
}
.status-dot.online {
background: var(--success);
}
.status-dot.offline {
background: var(--error);
}
.status-text {
font-size: 14px;
font-weight: bold;
color: var(--text-primary);
}
/* Connection Controls */
.connection-controls {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 12px;
}
/* Proxy Controls */
.proxy-controls {
display: flex;
flex-direction: column;
gap: 12px;
}
.input-row {
display: flex;
align-items: center;
gap: 12px;
}
.input-row label {
font-size: 14px;
font-weight: bold;
color: var(--text-primary);
min-width: 80px;
}
.input-row input {
flex: 1;
padding: 6px 10px;
border: 1px solid var(--button-outline);
border-radius: 4px;
font-size: 14px;
background: var(--input-bg);
color: var(--text-secondary);
font-style: italic;
}
.input-row input:focus {
outline: none;
border-color: var(--accent-primary);
}
.proxy-buttons {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
/* Status Messages */
.status-message {
padding: 8px 12px;
border-radius: 4px;
margin: 8px 0;
font-weight: bold;
font-size: 14px;
display: none;
border: 1px solid transparent;
}
.status-message.success {
background-color: rgba(76, 175, 80, 0.1);
color: var(--success);
border-color: var(--success);
}
.status-message.error {
background-color: rgba(244, 67, 54, 0.1);
color: var(--error);
border-color: var(--error);
}
.status-message.warning {
background-color: rgba(255, 152, 0, 0.1);
color: var(--warning);
border-color: var(--warning);
}
.status-message.info {
background-color: rgba(14, 122, 254, 0.1);
color: var(--accent-primary);
border-color: var(--accent-primary);
}
/* Dark Scrollbars */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: var(--button-outline);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--hover-bg);
}
/* Loading Animation */
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.6; }
100% { opacity: 1; }
}
button:disabled {
animation: pulse 2s infinite;
}
/* Update Button */
.btn-update {
display: flex;
align-items: center;
gap: 6px;
background: transparent;
color: var(--text-secondary);
border: 1px solid var(--button-outline);
padding: 6px 12px;
font-size: 12px;
font-weight: bold;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
position: relative;
}
.btn-update:hover:not(:disabled) {
color: var(--text-primary);
border-color: var(--accent-primary);
}
.btn-update:hover:not(:disabled) .update-icon {
animation: spin 0.6s ease;
}
.btn-update:disabled .update-icon {
animation: spin 1s linear infinite;
}
.btn-update.update-available {
color: var(--success);
border-color: var(--success);
box-shadow: 0 0 8px rgba(76, 175, 80, 0.3);
}
.btn-update.update-available::after {
content: '';
position: absolute;
top: -3px;
right: -3px;
width: 8px;
height: 8px;
background: var(--success);
border-radius: 50%;
box-shadow: 0 0 6px rgba(76, 175, 80, 0.8);
}
.update-icon {
font-size: 14px;
display: inline-block;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Update Modal */
.update-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.update-modal-card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 8px;
width: 480px;
max-width: 90%;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
.update-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--border);
}
.update-modal-header h3 {
font-size: 16px;
color: var(--text-primary);
margin: 0;
}
.update-modal-close {
background: transparent;
border: none;
color: var(--text-secondary);
font-size: 20px;
cursor: pointer;
padding: 0 4px;
line-height: 1;
}
.update-modal-close:hover {
color: var(--text-primary);
}
.update-modal-body {
padding: 20px;
overflow-y: auto;
flex: 1;
}
.update-modal-message {
font-size: 14px;
color: var(--text-primary);
margin: 0 0 12px 0;
}
.update-modal-notes {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.5;
white-space: pre-wrap;
max-height: 200px;
overflow-y: auto;
padding: 12px;
background: var(--bg-primary);
border-radius: 4px;
border: 1px solid var(--border);
}
.update-modal-notes:empty {
display: none;
}
.update-progress-container {
margin-top: 16px;
display: flex;
align-items: center;
gap: 12px;
}
.update-progress-track {
flex: 1;
height: 8px;
background: var(--bg-primary);
border-radius: 4px;
overflow: hidden;
border: 1px solid var(--border);
}
.update-progress-fill {
height: 100%;
background: var(--accent-primary);
border-radius: 4px;
transition: width 0.3s ease;
}
.update-progress-text {
font-size: 12px;
font-weight: bold;
color: var(--text-secondary);
min-width: 36px;
text-align: right;
}
.update-modal-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 16px 20px;
border-top: 1px solid var(--border);
}
/* Responsive Design */
@media (max-width: 768px) {
.devices-sidebar {
width: 150px;
}
.main-content {
padding: 16px;
}
.connection-controls,
.proxy-buttons {
flex-direction: column;
gap: 8px;
}
}
/* Focus States for Accessibility */
button:focus,
input:focus,
select:focus {
outline: 2px solid var(--accent-primary);
outline-offset: 1px;
}
/* High Contrast Support */
@media (prefers-contrast: high) {
:root {
--border: #666666;
--text-secondary: #CCCCCC;
--button-outline: #777777;
}
}