feat: release passwordless hierarchical APT v1.2.5
APT build checks / build-checks (push) Successful in 37s
APT build checks / build-checks (pull_request) Successful in 37s

This commit is contained in:
2026-08-22 16:20:12 +00:00
parent c08d2ad381
commit ace4568f81
27 changed files with 209 additions and 145 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ npm run build
- IPC methods are narrow and sender-validated against the main local file frame. - IPC methods are narrow and sender-validated against the main local file frame.
- Bridge is exactly `127.0.0.1:18247`, exact committed extension origin, and `X-APT-Pairing` authenticated. - Bridge is exactly `127.0.0.1:18247`, exact committed extension origin, and `X-APT-Pairing` authenticated.
- Persist only the pairing hash envelope under Electron `userData`; plaintext is shown once after first run/rotation. - Persist only the pairing hash envelope under Electron `userData`; plaintext is shown once after first run/rotation.
- Proxy helper launches directly with `shell: false`; stop only app-owned child processes. - Proxy helper launches directly with `shell: false`; passwordless launch uses the main-owned paired session only through the helper's required `-k` argument. Never expose that cookie to renderer IPC, UI, logs, files, shell commands, or tracked process metadata; stop only app-owned child processes.
- Update behavior is check-only against the exact GitPeji API. The only follow-up action opens the fixed GitPeji releases page externally. - Update behavior is check-only against the exact GitPeji API. The only follow-up action opens the fixed GitPeji releases page externally.
- Never add download/install/replace logic, arbitrary URLs, redirect following, shell scripts, broad process killing, hardcoded bridge secrets, or credential-bearing renderer state. - Never add download/install/replace logic, arbitrary URLs, redirect following, shell scripts, broad process killing, hardcoded bridge secrets, or credential-bearing renderer state.
- Synthetic tests only: never use a real tenant or Alta session. - Synthetic tests only: never use a real tenant or Alta session.
+2 -3
View File
@@ -8,7 +8,7 @@ APT is a Windows Electron desktop app that imports an existing Alta Video sessio
- The renderer receives only connection origin/state, device/site/auth responses, and owned proxy metadata. It never receives or supplies the Alta session value. - The renderer receives only connection origin/state, device/site/auth responses, and owned proxy metadata. It never receives or supplies the Alta session value.
- The bridge listens only on `127.0.0.1:18247`, accepts only the committed extension origin, requires the `X-APT-Pairing` secret, limits concurrent/body/deadline work, and validates an exact canonical Alta HTTPS origin. - The bridge listens only on `127.0.0.1:18247`, accepts only the committed extension origin, requires the `X-APT-Pairing` secret, limits concurrent/body/deadline work, and validates an exact canonical Alta HTTPS origin.
- The pairing envelope is a scrypt hash stored atomically under Electron `userData` with restrictive permissions. The plaintext secret is shown once on first run or rotation. Revoke invalidates it. - The pairing envelope is a scrypt hash stored atomically under Electron `userData` with restrictive permissions. The plaintext secret is shown once on first run or rotation. Revoke invalidates it.
- Proxy launch is a direct, detached `spawn` of the fixed helper with `shell: false`. Its exact arguments are deployment host, non-secret Alta username/email, and selected device UUID; no session bearer, password, or 2FA value reaches the command line. Stop actions can target only children owned by this app. - Proxy launch is a direct, detached `spawn` of the fixed helper with `shell: false`. APT passes the main-process-only paired Alta session through the helper's supported `-k` cookie mode so users are not prompted for Alta credentials again. The cookie never crosses renderer IPC, enters a shell, or reaches logs, files, tracked process metadata, or UI state; it is necessarily present in the helper process arguments because the external helper exposes no pipe/stdin credential interface. Stop actions can target only children owned by this app.
- Updates are **check-only**. APT checks the exact GitPeji release API and can open only `https://git.pejicorp.com/peji/Alta-Proxy-Tool/releases` in the system browser. It never downloads, replaces, or executes an update. - Updates are **check-only**. APT checks the exact GitPeji release API and can open only `https://git.pejicorp.com/peji/Alta-Proxy-Tool/releases` in the system browser. It never downloads, replaces, or executes an update.
## Requirements ## Requirements
@@ -30,8 +30,7 @@ npm start
2. Start APT. Under **Bridge Pairing**, copy the one-time secret. 2. Start APT. Under **Bridge Pairing**, copy the one-time secret.
3. Open the extension pairing settings, paste the secret, and save it. 3. Open the extension pairing settings, paste the secret, and save it.
4. Visit your Alta deployment in Chrome and use **Send to APT**. 4. Visit your Alta deployment in Chrome and use **Send to APT**.
5. Select a local camera, enter your Alta username/email, and choose **Start Proxy**. 5. Select a local camera and choose **Start Proxy**. APT reuses the paired Chrome session; no Alta username, password, or 2FA prompt is required.
6. Complete the password and 2FA prompts in the helper's visible Windows console. APT does not collect or pass those secrets.
Use **Generate / Rotate** if a pairing may have been exposed, then update the extension. Use **Revoke** to immediately disable bridge authentication. Use **Generate / Rotate** if a pairing may have been exposed, then update the extension. Use **Revoke** to immediately disable bridge authentication.
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Alta Proxy Tool Bridge", "name": "Alta Proxy Tool Bridge",
"version": "1.2.0", "version": "1.2.5",
"description": "Send Alta session cookies to a paired Alta Proxy Tool desktop app.", "description": "Send Alta session cookies to a paired Alta Proxy Tool desktop app.",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt4EZdkSgOsyiy5DRe0JkX+BpK94FpMjBU59NVIqDPO8QBDwvqNDWT/UjqHK/0aqSxzed5KibX6MdAvc495+u1sCybFdjDdXyBewEvg+PDqGiJketlZKC9dcR1RXHuPgAoM3NaNbMb3TqYcS9J4iGq0UwadxubkQrEcPiuyR6oriOkop8q9/5DWGb15wOGmiCuVmlXfUjNJIvNBm9P/ZHtgFBYDI2PuSSI5GI4j04VFpEyfNlFCrpi8GQ7bYZzezigZWXRjhhNwkx39bNHlkAWYa8XGZseCpKKvi0EaeCoBPjoYSAt161SM1dqX+/UC61/sLOU/SpDB1SYGTm5DC+7wIDAQAB", "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt4EZdkSgOsyiy5DRe0JkX+BpK94FpMjBU59NVIqDPO8QBDwvqNDWT/UjqHK/0aqSxzed5KibX6MdAvc495+u1sCybFdjDdXyBewEvg+PDqGiJketlZKC9dcR1RXHuPgAoM3NaNbMb3TqYcS9J4iGq0UwadxubkQrEcPiuyR6oriOkop8q9/5DWGb15wOGmiCuVmlXfUjNJIvNBm9P/ZHtgFBYDI2PuSSI5GI4j04VFpEyfNlFCrpi8GQ7bYZzezigZWXRjhhNwkx39bNHlkAWYa8XGZseCpKKvi0EaeCoBPjoYSAt161SM1dqX+/UC61/sLOU/SpDB1SYGTm5DC+7wIDAQAB",
"permissions": ["cookies", "activeTab", "clipboardWrite", "storage"], "permissions": ["cookies", "activeTab", "clipboardWrite", "storage"],
-6
View File
@@ -75,9 +75,3 @@ button:disabled { cursor: not-allowed; opacity: 0.45; }
padding-top: 14px; padding-top: 14px;
border-top: 1px solid #3c3c3c; border-top: 1px solid #3c3c3c;
} }
.privacy-note {
margin-top: 24px;
color: #aaa;
font-size: 12px;
}
+2 -2
View File
@@ -10,7 +10,7 @@
<body> <body>
<main class="options-card"> <main class="options-card">
<h1>Pair with Alta Proxy Tool</h1> <h1>Pair with Alta Proxy Tool</h1>
<p>In the desktop app, create a one-time pairing secret. Paste it below on this computer. Treat it like a password.</p> <p>Create a one-time pairing secret in APT, then paste it below.</p>
<form id="pairingForm"> <form id="pairingForm">
<label for="pairingSecret">One-time pairing secret</label> <label for="pairingSecret">One-time pairing secret</label>
<input id="pairingSecret" name="pairingSecret" type="password" inputmode="text" autocomplete="off" spellcheck="false" required> <input id="pairingSecret" name="pairingSecret" type="password" inputmode="text" autocomplete="off" spellcheck="false" required>
@@ -22,7 +22,7 @@
<p id="currentState">Checking...</p> <p id="currentState">Checking...</p>
<button id="forgetBtn" type="button" class="danger-btn" disabled>Forget pairing</button> <button id="forgetBtn" type="button" class="danger-btn" disabled>Forget pairing</button>
</section> </section>
<p class="privacy-note">The secret is stored only in Chrome extension local storage. It is never displayed again or copied automatically.</p>
</main> </main>
<script src="options.js"></script> <script src="options.js"></script>
</body> </body>
-8
View File
@@ -88,14 +88,6 @@ h1 {
opacity: 0.45; opacity: 0.45;
} }
.copy-warning {
margin: 10px 0 0;
padding: 8px 10px;
border-left: 3px solid #f0a33a;
background: rgba(240, 163, 58, 0.08);
color: #d7c29f;
font-size: 12px;
}
.status-msg { .status-msg {
display: none; display: none;
-1
View File
@@ -17,7 +17,6 @@
<button id="sendBtn" class="primary-btn" type="button" disabled>Send to APT</button> <button id="sendBtn" class="primary-btn" type="button" disabled>Send to APT</button>
<button id="copyBtn" class="secondary-btn" type="button" disabled>Copy VA Token</button> <button id="copyBtn" class="secondary-btn" type="button" disabled>Copy VA Token</button>
</div> </div>
<p id="copyWarning" class="copy-warning">Copying exposes the full bearer token. Clipboard history or sync may retain it. A confirmation is required.</p>
<div id="statusMsg" class="status-msg" role="status" aria-live="polite"></div> <div id="statusMsg" class="status-msg" role="status" aria-live="polite"></div>
</main> </main>
<script src="popup.js"></script> <script src="popup.js"></script>
+2 -6
View File
@@ -45,7 +45,6 @@ function createPopupController({
const pairingInfo = documentApi.getElementById('pairingInfo'); const pairingInfo = documentApi.getElementById('pairingInfo');
const sendBtn = documentApi.getElementById('sendBtn'); const sendBtn = documentApi.getElementById('sendBtn');
const copyBtn = documentApi.getElementById('copyBtn'); const copyBtn = documentApi.getElementById('copyBtn');
const copyWarning = documentApi.getElementById('copyWarning');
const statusMsg = documentApi.getElementById('statusMsg'); const statusMsg = documentApi.getElementById('statusMsg');
const openOptionsBtn = documentApi.getElementById('openOptionsBtn'); const openOptionsBtn = documentApi.getElementById('openOptionsBtn');
@@ -159,9 +158,7 @@ function createPopupController({
async function copyToken() { async function copyToken() {
if (!detectedOrigin || !pairingSecret || busy) return; if (!detectedOrigin || !pairingSecret || busy) return;
const confirmed = confirmCopy( const confirmed = confirmCopy('Copy VA token?');
'Copy the full VA bearer token? Clipboard history or sync may retain it. Continue only if you will paste it into a trusted destination.'
);
if (!confirmed) { if (!confirmed) {
showStatus('Copy cancelled. The VA token was not read.', 'info'); showStatus('Copy cancelled. The VA token was not read.', 'info');
return; return;
@@ -172,7 +169,7 @@ function createPopupController({
try { try {
cookieValue = await getVaCookieValue(); cookieValue = await getVaCookieValue();
await navigatorApi.clipboard.writeText(cookieValue); await navigatorApi.clipboard.writeText(cookieValue);
showStatus('VA token copied. Clear your clipboard after use.', 'success'); showStatus('VA token copied.', 'success');
} catch (error) { } catch (error) {
showCookieError(error, true); showCookieError(error, true);
} finally { } finally {
@@ -182,7 +179,6 @@ function createPopupController({
} }
async function init() { async function init() {
copyWarning.textContent = 'Copying exposes the full bearer token. Clipboard history or sync may retain it. A confirmation is required.';
sendBtn.addEventListener('click', sendToApt); sendBtn.addEventListener('click', sendToApt);
copyBtn.addEventListener('click', copyToken); copyBtn.addEventListener('click', copyToken);
openOptionsBtn.addEventListener('click', () => chromeApi.runtime.openOptionsPage()); openOptionsBtn.addEventListener('click', () => chromeApi.runtime.openOptionsPage());
@@ -40,7 +40,7 @@ Feature discovery begins only after Phase A passes independent security review a
- Use a fresh worktree from GitPeji `master` at `a80074ac57b7a4517837b5d95754f5e6433df3ac` or newer. - Use a fresh worktree from GitPeji `master` at `a80074ac57b7a4517837b5d95754f5e6433df3ac` or newer.
- Preserve current Tool Hub downloads and production visibility until Zac approves replacement. - 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 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. - Do not write cookies to logs, disk, command files, shell strings, crash reports, analytics, or test artifacts. The sole accepted exception is the fixed helper's required `-k` argv value for Zac's explicitly required paired-extension passwordless proxy flow; keep it main-process-only, direct-spawned with `shell: false`, redacted from errors, and absent from renderer IPC and tracked metadata.
- No production release until the exact candidate passes controller verification plus independent security and release reviews. - 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`. - Mac distribution remains approval-gated under Kanban task `t_42bad4d3`.
@@ -16,3 +16,7 @@
- Synthetic sentinel credentials only in tests. - Synthetic sentinel credentials only in tests.
- Work remains on the isolated hardening branch until review gates pass. - Work remains on the isolated hardening branch until review gates pass.
- No feature additions until Critical/Important security closure. - No feature additions until Critical/Important security closure.
## Accepted passwordless proxy exception
Zac explicitly requires the paired-extension **Connect → select camera → Start Proxy** flow without re-entering Alta credentials. The external `aware-cam-proxy.exe` helper supports cookie authentication only through `-k`; it exposes no stdin, named-pipe, environment-variable, or credential-file interface. APT therefore passes the validated main-owned session directly to the fixed helper as one argv value. This exception remains bounded by direct `shell: false` spawn, trusted-device allowlisting, no renderer/IPC credential parameter, no logs/files/shells, redacted launch errors, safe tracked metadata, and synthetic regression tests.
+4 -6
View File
@@ -51,11 +51,12 @@
</div> </div>
<div class="connection-controls"> <div class="connection-controls">
<button type="button" id="disconnectBtn" class="btn-outline" disabled>Disconnect</button> <button type="button" id="disconnectBtn" class="btn-outline" disabled>Disconnect</button>
<button type="button" id="managePairingBtn" class="btn-outline" hidden>Manage Pairing</button>
</div> </div>
<div id="connectionStatus" class="status-message"></div> <div id="connectionStatus" class="status-message"></div>
</section> </section>
<section class="content-section pairing-section"> <section id="pairingSection" class="content-section pairing-section" hidden>
<h2>Bridge Pairing</h2> <h2>Bridge Pairing</h2>
<p class="section-help">Only the committed APT Chrome extension can send a session. Pair it with a one-time secret.</p> <p class="section-help">Only the committed APT Chrome extension can send a session. Pair it with a one-time secret.</p>
<div class="status-row"> <div class="status-row">
@@ -70,6 +71,7 @@
<div class="proxy-buttons"> <div class="proxy-buttons">
<button type="button" id="rotatePairingBtn" class="btn-primary">Generate / Rotate</button> <button type="button" id="rotatePairingBtn" class="btn-primary">Generate / Rotate</button>
<button type="button" id="revokePairingBtn" class="btn-outline">Revoke</button> <button type="button" id="revokePairingBtn" class="btn-outline">Revoke</button>
<button type="button" id="hidePairingBtn" class="btn-outline" hidden>Done</button>
</div> </div>
</section> </section>
@@ -80,11 +82,7 @@
<label for="selectedDeviceId">Device UUID:</label> <label for="selectedDeviceId">Device UUID:</label>
<input type="text" id="selectedDeviceId" placeholder="Select a device from the list" readonly> <input type="text" id="selectedDeviceId" placeholder="Select a device from the list" readonly>
</div> </div>
<div class="input-row"> <p class="section-help">APT uses the paired Chrome extension session. Select a camera, then start the proxy—no Alta credentials need to be entered again.</p>
<label for="altaUsername">Alta username / email:</label>
<input type="text" id="altaUsername" maxlength="254" placeholder="name@example.com" autocomplete="username" spellcheck="false">
</div>
<p class="section-help">The proxy helper opens a Windows console and prompts for your password and 2FA. APT never sends those secrets on its command line.</p>
<div class="proxy-buttons"> <div class="proxy-buttons">
<button type="button" id="startProxyBtn" class="btn-primary" disabled>Start Proxy</button> <button type="button" id="startProxyBtn" class="btn-primary" disabled>Start Proxy</button>
<button type="button" id="stopProxyBtn" class="btn-outline" disabled>Stop Proxy</button> <button type="button" id="stopProxyBtn" class="btn-outline" disabled>Stop Proxy</button>
+1 -1
View File
@@ -81,7 +81,7 @@ function registerIpcHandlers() {
registerIpc('get-device-groups', () => runtime.getDeviceGroups()); registerIpc('get-device-groups', () => runtime.getDeviceGroups());
registerIpc('get-device-hierarchy', () => runtime.getDeviceHierarchy()); registerIpc('get-device-hierarchy', () => runtime.getDeviceHierarchy());
registerIpc('get-auth-info', () => runtime.getAuthInfo()); registerIpc('get-auth-info', () => runtime.getAuthInfo());
registerIpc('launch-proxy', (deviceId, username) => runtime.launchProxy(deviceId, username)); registerIpc('launch-proxy', (deviceId) => runtime.launchProxy(deviceId));
registerIpc('stop-proxy', async (key) => { registerIpc('stop-proxy', async (key) => {
const result = await runtime.stopProxy(key); const result = await runtime.stopProxy(key);
sendConnectionState(); sendConnectionState();
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "alta-api-client", "name": "alta-api-client",
"version": "1.2.0", "version": "1.2.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "alta-api-client", "name": "alta-api-client",
"version": "1.2.0", "version": "1.2.5",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"axios": "1.19.0" "axios": "1.19.0"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "alta-api-client", "name": "alta-api-client",
"version": "1.2.0", "version": "1.2.5",
"description": "Secure Windows Electron client for the Alta Camera Proxy", "description": "Secure Windows Electron client for the Alta Camera Proxy",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
+1 -1
View File
@@ -15,7 +15,7 @@ contextBridge.exposeInMainWorld('electronAPI', Object.freeze({
getDeviceGroups: () => ipcRenderer.invoke('get-device-groups'), getDeviceGroups: () => ipcRenderer.invoke('get-device-groups'),
getDeviceHierarchy: () => ipcRenderer.invoke('get-device-hierarchy'), getDeviceHierarchy: () => ipcRenderer.invoke('get-device-hierarchy'),
getAuthInfo: () => ipcRenderer.invoke('get-auth-info'), getAuthInfo: () => ipcRenderer.invoke('get-auth-info'),
launchProxy: (deviceId, username) => ipcRenderer.invoke('launch-proxy', deviceId, username), launchProxy: (deviceId) => ipcRenderer.invoke('launch-proxy', deviceId),
stopProxy: (key) => ipcRenderer.invoke('stop-proxy', key), stopProxy: (key) => ipcRenderer.invoke('stop-proxy', key),
disconnect: () => ipcRenderer.invoke('disconnect'), disconnect: () => ipcRenderer.invoke('disconnect'),
getConnectionState: () => ipcRenderer.invoke('get-connection-state'), getConnectionState: () => ipcRenderer.invoke('get-connection-state'),
+10 -1
View File
@@ -1,6 +1,15 @@
'use strict'; 'use strict';
(function exposeRendererController(globalScope) { (function exposeRendererController(globalScope) {
function shouldShowPairingOnboarding({
paired = false,
secretVisible = false,
connected = false,
manuallyOpen = false,
} = {}) {
return manuallyOpen || !paired || (secretVisible && !connected);
}
function createRendererController({ function createRendererController({
disconnect, disconnect,
renderConnectionState, renderConnectionState,
@@ -35,7 +44,7 @@
}); });
} }
const api = Object.freeze({ createRendererController }); const api = Object.freeze({ createRendererController, shouldShowPairingOnboarding });
if (typeof module !== 'undefined' && module.exports) module.exports = api; if (typeof module !== 'undefined' && module.exports) module.exports = api;
if (globalScope) globalScope.AptRendererController = api; if (globalScope) globalScope.AptRendererController = api;
}(typeof window !== 'undefined' ? window : undefined)); }(typeof window !== 'undefined' ? window : undefined));
+42 -9
View File
@@ -10,7 +10,6 @@ const statusIndicator = document.getElementById('statusIndicator');
const deviceSearch = document.getElementById('deviceSearch'); const deviceSearch = document.getElementById('deviceSearch');
const disconnectBtn = document.getElementById('disconnectBtn'); const disconnectBtn = document.getElementById('disconnectBtn');
const selectedDeviceId = document.getElementById('selectedDeviceId'); const selectedDeviceId = document.getElementById('selectedDeviceId');
const altaUsername = document.getElementById('altaUsername');
const startProxyBtn = document.getElementById('startProxyBtn'); const startProxyBtn = document.getElementById('startProxyBtn');
const stopProxyBtn = document.getElementById('stopProxyBtn'); const stopProxyBtn = document.getElementById('stopProxyBtn');
const checkUpdateBtn = document.getElementById('checkUpdateBtn'); const checkUpdateBtn = document.getElementById('checkUpdateBtn');
@@ -18,11 +17,14 @@ const updateNotice = document.getElementById('updateNotice');
const updateMessage = document.getElementById('updateMessage'); const updateMessage = document.getElementById('updateMessage');
const openReleasesBtn = document.getElementById('openReleasesBtn'); const openReleasesBtn = document.getElementById('openReleasesBtn');
const dismissUpdateBtn = document.getElementById('dismissUpdateBtn'); const dismissUpdateBtn = document.getElementById('dismissUpdateBtn');
const pairingSection = document.getElementById('pairingSection');
const managePairingBtn = document.getElementById('managePairingBtn');
const pairingState = document.getElementById('pairingState'); const pairingState = document.getElementById('pairingState');
const pairingSecret = document.getElementById('pairingSecret'); const pairingSecret = document.getElementById('pairingSecret');
const pairingSecretRow = document.getElementById('pairingSecretRow'); const pairingSecretRow = document.getElementById('pairingSecretRow');
const rotatePairingBtn = document.getElementById('rotatePairingBtn'); const rotatePairingBtn = document.getElementById('rotatePairingBtn');
const revokePairingBtn = document.getElementById('revokePairingBtn'); const revokePairingBtn = document.getElementById('revokePairingBtn');
const hidePairingBtn = document.getElementById('hidePairingBtn');
const ROW_HEIGHT = window.AptSidebarView.ROW_HEIGHT; const ROW_HEIGHT = window.AptSidebarView.ROW_HEIGHT;
let connection = { connected: false, origin: null, activeProxies: [] }; let connection = { connected: false, origin: null, activeProxies: [] };
@@ -32,6 +34,17 @@ let sidebarController = null;
let loadGeneration = 0; let loadGeneration = 0;
let searchTimer = null; let searchTimer = null;
let scrollFrame = null; let scrollFrame = null;
let pairingView = { paired: false, secretVisible: false, manuallyOpen: false };
function renderPairingVisibility() {
const visible = window.AptRendererController.shouldShowPairingOnboarding({
...pairingView,
connected: connection.connected,
});
pairingSection.hidden = !visible;
managePairingBtn.hidden = visible || !pairingView.paired;
hidePairingBtn.hidden = !pairingView.paired;
}
function showStatus(element, message, type) { function showStatus(element, message, type) {
element.textContent = message; element.textContent = message;
@@ -51,8 +64,7 @@ function deviceStatusFor(device) {
function updateProxyButtons() { function updateProxyButtons() {
const id = selectedDevice && selectedDevice.id; const id = selectedDevice && selectedDevice.id;
const active = id ? activeDeviceIds().has(id.toLowerCase()) : false; const active = id ? activeDeviceIds().has(id.toLowerCase()) : false;
const hasUsername = altaUsername.value.trim().length > 0; startProxyBtn.disabled = !connection.connected || !id || active;
startProxyBtn.disabled = !connection.connected || !id || !hasUsername || active;
stopProxyBtn.disabled = !id || !active; stopProxyBtn.disabled = !id || !active;
} }
@@ -67,6 +79,13 @@ function renderConnectionState(state) {
dot.className = `status-dot ${connection.connected ? 'online' : 'offline'}`; dot.className = `status-dot ${connection.connected ? 'online' : 'offline'}`;
text.textContent = connection.connected ? 'Connected' : 'Disconnected'; text.textContent = connection.connected ? 'Connected' : 'Disconnected';
disconnectBtn.disabled = !connection.connected; disconnectBtn.disabled = !connection.connected;
if (connection.connected) {
pairingView.secretVisible = false;
pairingView.manuallyOpen = false;
pairingSecret.value = '';
pairingSecretRow.style.display = 'none';
}
renderPairingVisibility();
updateProxyButtons(); updateProxyButtons();
if (sidebarController) sidebarController.refresh(); if (sidebarController) sidebarController.refresh();
} }
@@ -145,10 +164,9 @@ function installHierarchy(hierarchy) {
const count = snapshot.state.searchQuery && snapshot.state.searchCameraKeys const count = snapshot.state.searchQuery && snapshot.state.searchCameraKeys
? snapshot.state.searchCameraKeys.size ? snapshot.state.searchCameraKeys.size
: hierarchyModel.cameraCount; : hierarchyModel.cameraCount;
const hidden = snapshot.rows.hiddenSelected ? ' Selected camera is hidden by the current view.' : '';
deviceResults.textContent = snapshot.state.searchQuery deviceResults.textContent = snapshot.state.searchQuery
? `${count} exact search result${count === 1 ? '' : 's'}.${hidden}` ? `${count} exact search result${count === 1 ? '' : 's'}.`
: `${count} camera${count === 1 ? '' : 's'}.${hidden}`; : `${count} camera${count === 1 ? '' : 's'}.`;
}, },
}); });
sidebarController = window.AptSidebarController.createSidebarController({ sidebarController = window.AptSidebarController.createSidebarController({
@@ -201,7 +219,6 @@ const rendererController = window.AptRendererController.createRendererController
disconnect: () => window.electronAPI.disconnect(), disconnect: () => window.electronAPI.disconnect(),
renderConnectionState, renderConnectionState,
clearDisconnectedState: () => { clearDisconnectedState: () => {
altaUsername.value = '';
clearHierarchy(); clearHierarchy();
}, },
showConnectionStatus: (message, type) => showStatus(connectionStatus, message, type), showConnectionStatus: (message, type) => showStatus(connectionStatus, message, type),
@@ -216,7 +233,7 @@ async function handleStartProxy() {
if (!id) return; if (!id) return;
startProxyBtn.disabled = true; startProxyBtn.disabled = true;
showStatus(connectionStatus, `Starting proxy for ${selectedDevice.name || 'selected device'}...`, 'info'); showStatus(connectionStatus, `Starting proxy for ${selectedDevice.name || 'selected device'}...`, 'info');
const result = await window.electronAPI.launchProxy(id, altaUsername.value); const result = await window.electronAPI.launchProxy(id);
if (result.success) { if (result.success) {
renderConnectionState(await window.electronAPI.getConnectionState()); renderConnectionState(await window.electronAPI.getConnectionState());
showStatus(connectionStatus, 'Camera proxy started.', 'success'); showStatus(connectionStatus, 'Camera proxy started.', 'success');
@@ -259,6 +276,11 @@ async function checkForUpdates(showCurrent = true) {
} }
function renderPairingStatus(result) { function renderPairingStatus(result) {
pairingView = {
paired: result.paired === true,
secretVisible: Boolean(result.secret),
manuallyOpen: Boolean(result.secret) || result.paired !== true,
};
pairingState.textContent = result.paired ? 'Paired' : 'Revoked / not paired'; pairingState.textContent = result.paired ? 'Paired' : 'Revoked / not paired';
pairingState.className = result.paired ? 'paired' : 'unpaired'; pairingState.className = result.paired ? 'paired' : 'unpaired';
revokePairingBtn.disabled = !result.paired; revokePairingBtn.disabled = !result.paired;
@@ -270,6 +292,7 @@ function renderPairingStatus(result) {
pairingSecret.value = ''; pairingSecret.value = '';
pairingSecretRow.style.display = 'none'; pairingSecretRow.style.display = 'none';
} }
renderPairingVisibility();
} }
async function runSearch(query) { async function runSearch(query) {
@@ -340,10 +363,20 @@ deviceSearch.addEventListener('keydown', (event) => {
disconnectBtn.addEventListener('click', handleDisconnect); disconnectBtn.addEventListener('click', handleDisconnect);
startProxyBtn.addEventListener('click', handleStartProxy); startProxyBtn.addEventListener('click', handleStartProxy);
stopProxyBtn.addEventListener('click', handleStopProxy); stopProxyBtn.addEventListener('click', handleStopProxy);
altaUsername.addEventListener('input', updateProxyButtons);
checkUpdateBtn.addEventListener('click', () => checkForUpdates(true)); checkUpdateBtn.addEventListener('click', () => checkForUpdates(true));
openReleasesBtn.addEventListener('click', () => window.electronAPI.openFixedReleasesPage()); openReleasesBtn.addEventListener('click', () => window.electronAPI.openFixedReleasesPage());
dismissUpdateBtn.addEventListener('click', () => { updateNotice.style.display = 'none'; }); dismissUpdateBtn.addEventListener('click', () => { updateNotice.style.display = 'none'; });
rotatePairingBtn.addEventListener('click', async () => renderPairingStatus(await window.electronAPI.rotatePairing())); rotatePairingBtn.addEventListener('click', async () => renderPairingStatus(await window.electronAPI.rotatePairing()));
revokePairingBtn.addEventListener('click', async () => renderPairingStatus(await window.electronAPI.revokePairing())); revokePairingBtn.addEventListener('click', async () => renderPairingStatus(await window.electronAPI.revokePairing()));
managePairingBtn.addEventListener('click', () => {
pairingView.manuallyOpen = true;
renderPairingVisibility();
});
hidePairingBtn.addEventListener('click', () => {
pairingView.manuallyOpen = false;
pairingView.secretVisible = false;
pairingSecret.value = '';
pairingSecretRow.style.display = 'none';
renderPairingVisibility();
});
document.addEventListener('DOMContentLoaded', () => { initialize().catch(() => showStatus(connectionStatus, 'Application initialization failed.', 'error')); }); document.addEventListener('DOMContentLoaded', () => { initialize().catch(() => showStatus(connectionStatus, 'Application initialization failed.', 'error')); });
+8 -3
View File
@@ -37,7 +37,9 @@ function projectDevice(raw) {
type: nullableString(raw.type), type: nullableString(raw.type),
model: nullableString(raw.model), model: nullableString(raw.model),
address: nullableString(raw.address), address: nullableString(raw.address),
siteId: nullableId(raw.server_group_id), // server_group_id identifies the Alta server/cloud connector, not a device site.
// The tree infers the site through device_group_id -> deviceGroups.parent_id.
siteId: null,
deviceGroupId: nullableId(raw.device_group_id), deviceGroupId: nullableId(raw.device_group_id),
displayStatus: nullableString(raw.live && raw.live.display_status), displayStatus: nullableString(raw.live && raw.live.display_status),
localStorage: nullableBoolean(raw.capabilities && raw.capabilities.localStorage), localStorage: nullableBoolean(raw.capabilities && raw.capabilities.localStorage),
@@ -89,17 +91,20 @@ function projectDeviceHierarchy({ devices = [], sites = [], groups = [], metadat
throw new TypeError('Hierarchy projection requires arrays'); throw new TypeError('Hierarchy projection requires arrays');
} }
const deviceProjection = projectArray(devices, projectDevice); const deviceProjection = projectArray(devices, projectDevice);
const eligibleDevices = deviceProjection.projected.filter((device) => device.localStorage !== true);
const cloudNativeExcluded = deviceProjection.projected.length - eligibleDevices.length;
const siteProjection = projectArray(sites, projectSite); const siteProjection = projectArray(sites, projectSite);
const groupProjection = projectArray(groups, projectGroup); const groupProjection = projectArray(groups, projectGroup);
const payload = { const payload = {
devices: deviceProjection.projected, devices: eligibleDevices,
sites: siteProjection.projected, sites: siteProjection.projected,
groups: groupProjection.projected, groups: groupProjection.projected,
diagnostics: { diagnostics: {
devices: { devices: {
received: devices.length, received: devices.length,
eligible: deviceProjection.projected.length, eligible: eligibleDevices.length,
invalid: deviceProjection.invalid, invalid: deviceProjection.invalid,
cloudNativeExcluded,
}, },
sites: { sites: {
received: sites.length, received: sites.length,
+4 -5
View File
@@ -11,7 +11,7 @@ const {
} = require('./bridge-auth'); } = require('./bridge-auth');
const { RELEASES_PAGE_URL } = require('./update-policy'); const { RELEASES_PAGE_URL } = require('./update-policy');
const { projectDeviceHierarchy, projectGroup, projectSite } = require('./device-projection'); const { projectDeviceHierarchy, projectGroup, projectSite } = require('./device-projection');
const { validateDeviceId, validateUsername } = require('./proxy-launch'); const { validateCookie, validateDeviceId } = require('./proxy-launch');
const DEFAULT_DISCOVERY_TIMEOUT_MS = 60_000; const DEFAULT_DISCOVERY_TIMEOUT_MS = 60_000;
@@ -23,7 +23,7 @@ function safeErrorMessage(error, fallback) {
'INVALID_ALTA_RESPONSE', 'INVALID_ALTA_RESPONSE_DATA', 'ALTA_RESPONSE_TOO_LARGE', 'INVALID_ALTA_RESPONSE', 'INVALID_ALTA_RESPONSE_DATA', 'ALTA_RESPONSE_TOO_LARGE',
'ALTA_RESPONSE_TOO_MANY_OBJECTS', 'PROJECTED_PAYLOAD_TOO_LARGE', 'ALTA_RESPONSE_TOO_MANY_OBJECTS', 'PROJECTED_PAYLOAD_TOO_LARGE',
'UNSAFE_ALTA_REDIRECT', 'TOO_MANY_ALTA_REDIRECTS', 'HELPER_NOT_FOUND', 'UNSAFE_ALTA_REDIRECT', 'TOO_MANY_ALTA_REDIRECTS', 'HELPER_NOT_FOUND',
'UNSUPPORTED_PLATFORM', 'INVALID_DEVICE_ID', 'INVALID_USERNAME', 'SPAWN_FAILED', 'UNSUPPORTED_PLATFORM', 'INVALID_DEVICE_ID', 'INVALID_SESSION_COOKIE', 'SPAWN_FAILED',
]); ]);
return error && allowed.has(error.code) && typeof error.message === 'string' return error && allowed.has(error.code) && typeof error.message === 'string'
? error.message ? error.message
@@ -363,10 +363,9 @@ class AppRuntime {
} }
} }
async launchProxy(deviceId, username) { async launchProxy(deviceId) {
try { try {
const validatedId = validateDeviceId(deviceId); const validatedId = validateDeviceId(deviceId);
const validatedUsername = validateUsername(username);
this._reconcileProxies(); this._reconcileProxies();
if (!this.allowedDeviceDiscovery || !this._isCurrentDiscovery(this.allowedDeviceDiscovery) || if (!this.allowedDeviceDiscovery || !this._isCurrentDiscovery(this.allowedDeviceDiscovery) ||
!this.allowedDeviceIds.has(validatedId)) { !this.allowedDeviceIds.has(validatedId)) {
@@ -378,7 +377,7 @@ class AppRuntime {
const session = this.sessionStore.requireSession(); const session = this.sessionStore.requireSession();
const result = this.proxyManager.launchProxy({ const result = this.proxyManager.launchProxy({
deploymentHost: new URL(session.origin).hostname, deploymentHost: new URL(session.origin).hostname,
username: validatedUsername, cookie: validateCookie(session.cookie),
deviceId: validatedId, deviceId: validatedId,
}); });
this.proxyByDevice.set(validatedId, result.processId); this.proxyByDevice.set(validatedId, result.processId);
+16 -15
View File
@@ -5,7 +5,7 @@ const nodePath = require('node:path');
const { spawn: nodeSpawn } = require('node:child_process'); const { spawn: nodeSpawn } = require('node:child_process');
const HELPER_FILENAME = 'aware-cam-proxy.exe'; const HELPER_FILENAME = 'aware-cam-proxy.exe';
const MAX_USERNAME_LENGTH = 254; const MAX_COOKIE_LENGTH = 4096;
const MAX_HOST_LENGTH = 253; const MAX_HOST_LENGTH = 253;
const DEVICE_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; const DEVICE_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/; const DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
@@ -42,19 +42,20 @@ function validateDeviceId(value) {
return value.toLowerCase(); return value.toLowerCase();
} }
function validateUsername(value) { function validateCookie(value) {
if (typeof value !== 'string' || value.length === 0 || value.length > MAX_USERNAME_LENGTH || value.trim().length === 0) { if (typeof value !== 'string' || value.length === 0 || value.length > MAX_COOKIE_LENGTH) {
throw new ProxyLaunchError('Alta username is invalid.', 'INVALID_USERNAME'); throw new ProxyLaunchError('Alta session cookie is invalid.', 'INVALID_SESSION_COOKIE');
} }
if (/[\u0000-\u001f\u007f-\u009f]/.test(value)) { if (/[;\u0000-\u001f\u007f-\u009f]/.test(value)) {
throw new ProxyLaunchError('Alta username must not contain control characters.', 'INVALID_USERNAME'); throw new ProxyLaunchError('Alta session cookie must not contain separators or control characters.', 'INVALID_SESSION_COOKIE');
} }
return value; return value;
} }
function safeProcessError(error) { function safeProcessError(error, secret) {
const source = error && typeof error.message === 'string' ? error.message : 'Unknown process error'; const source = error && typeof error.message === 'string' ? error.message : 'Unknown process error';
return source.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ').slice(0, 512); const redacted = secret ? source.split(secret).join('[REDACTED]') : source;
return redacted.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ').slice(0, 512);
} }
function safeMetadata(entry, status = entry.status) { function safeMetadata(entry, status = entry.status) {
@@ -129,7 +130,7 @@ function createProxyManager({
const deploymentHost = validateDeploymentHost(request.deploymentHost); const deploymentHost = validateDeploymentHost(request.deploymentHost);
const deviceId = validateDeviceId(request.deviceId); const deviceId = validateDeviceId(request.deviceId);
const username = validateUsername(request.username); const cookie = validateCookie(request.cookie);
if (!fs.existsSync(helperPath)) { if (!fs.existsSync(helperPath)) {
throw new ProxyLaunchError('Proxy helper was not found in the approved application directory.', 'HELPER_NOT_FOUND'); throw new ProxyLaunchError('Proxy helper was not found in the approved application directory.', 'HELPER_NOT_FOUND');
@@ -139,17 +140,17 @@ function createProxyManager({
try { try {
child = spawn( child = spawn(
helperPath, helperPath,
['-a', deploymentHost, '-u', username, '-d', deviceId], ['-a', deploymentHost, '-d', deviceId, '-k', cookie],
{ {
shell: false, shell: false,
detached: true, detached: true,
stdio: 'inherit', stdio: 'ignore',
windowsHide: false windowsHide: false
} }
); );
} catch (error) { } catch (error) {
throw new ProxyLaunchError( throw new ProxyLaunchError(
`Failed to launch proxy helper: ${safeProcessError(error)}`, `Failed to launch proxy helper: ${safeProcessError(error, cookie)}`,
'SPAWN_FAILED' 'SPAWN_FAILED'
); );
} }
@@ -268,10 +269,10 @@ function createProxyManager({
module.exports = { module.exports = {
DEFAULT_STOP_TIMEOUT_MS, DEFAULT_STOP_TIMEOUT_MS,
HELPER_FILENAME, HELPER_FILENAME,
MAX_USERNAME_LENGTH, MAX_COOKIE_LENGTH,
ProxyLaunchError, ProxyLaunchError,
createProxyManager, createProxyManager,
validateCookie,
validateDeploymentHost, validateDeploymentHost,
validateDeviceId, validateDeviceId
validateUsername
}; };
+2 -2
View File
@@ -11,7 +11,7 @@ const ROOT = path.join(__dirname, '..');
const read = (name) => fs.readFileSync(path.join(ROOT, name), 'utf8'); const read = (name) => fs.readFileSync(path.join(ROOT, name), 'utf8');
const readJson = (name) => JSON.parse(read(name)); const readJson = (name) => JSON.parse(read(name));
function writeKit(root, { version = '1.2.0', legacy = false } = {}) { function writeKit(root, { version = '1.2.5', legacy = false } = {}) {
const extension = path.join(root, 'chrome-extension'); const extension = path.join(root, 'chrome-extension');
fs.mkdirSync(extension, { recursive: true }); fs.mkdirSync(extension, { recursive: true });
fs.writeFileSync(path.join(root, 'AltaCameraProxy.exe'), 'synthetic executable'); fs.writeFileSync(path.join(root, 'AltaCameraProxy.exe'), 'synthetic executable');
@@ -31,7 +31,7 @@ test('application and extension versions and supported dependencies stay coordin
const lock = readJson('package-lock.json'); const lock = readJson('package-lock.json');
const manifest = readJson('chrome-extension/manifest.json'); const manifest = readJson('chrome-extension/manifest.json');
assert.equal(pkg.version, '1.2.0'); assert.equal(pkg.version, '1.2.5');
assert.equal(manifest.version, pkg.version); assert.equal(manifest.version, pkg.version);
assert.equal(lock.version, pkg.version); assert.equal(lock.version, pkg.version);
assert.equal(lock.packages[''].version, pkg.version); assert.equal(lock.packages[''].version, pkg.version);
+52 -6
View File
@@ -6,6 +6,7 @@ const {
MAX_PROJECTED_PAYLOAD_BYTES, MAX_PROJECTED_PAYLOAD_BYTES,
projectDeviceHierarchy, projectDeviceHierarchy,
} = require('../src/device-projection'); } = require('../src/device-projection');
const { buildDeviceTree } = require('../device-tree');
function uuid(index) { function uuid(index) {
return `00000000-0000-4000-8000-${index.toString(16).padStart(12, '0')}`; return `00000000-0000-4000-8000-${index.toString(16).padStart(12, '0')}`;
@@ -18,7 +19,7 @@ function rawDevice(index, overrides = {}) {
type: 'camera', type: 'camera',
model: 'Synthetic', model: 'Synthetic',
address: `10.0.${Math.floor(index / 255)}.${index % 255}`, address: `10.0.${Math.floor(index / 255)}.${index % 255}`,
server_group_id: 'site-1', server_group_id: 'server-group-1',
device_group_id: 'group-1', device_group_id: 'group-1',
capabilities: { localStorage: index % 2 === 0, secretCapability: 'drop-me' }, capabilities: { localStorage: index % 2 === 0, secretCapability: 'drop-me' },
live: { display_status: 'online', private: 'drop-me' }, live: { display_status: 'online', private: 'drop-me' },
@@ -29,25 +30,68 @@ function rawDevice(index, overrides = {}) {
test('projects only allowlisted fields and honestly diagnoses ineligible devices', () => { test('projects only allowlisted fields and honestly diagnoses ineligible devices', () => {
const payload = projectDeviceHierarchy({ const payload = projectDeviceHierarchy({
devices: [rawDevice(1), rawDevice(2, { guid: 'not-a-uuid' })], devices: [
rawDevice(1),
rawDevice(2, { guid: 'not-a-uuid' }),
rawDevice(4),
],
sites: [{ id: 'site-1', name: 'HQ', pending_deletion_start: null, secret: 'drop-me' }], sites: [{ id: 'site-1', name: 'HQ', pending_deletion_start: null, secret: 'drop-me' }],
groups: [{ id: 'group-1', name: 'Lobby', parent_id: null, pending_deletion_start: null, secret: 'drop-me' }], groups: [{ id: 'group-1', name: 'Lobby', parent_id: null, pending_deletion_start: null, secret: 'drop-me' }],
}); });
assert.deepEqual(payload.devices, [{ assert.deepEqual(payload.devices, [{
id: uuid(1), name: 'Camera 1', type: 'camera', model: 'Synthetic', address: '10.0.0.1', id: uuid(1), name: 'Camera 1', type: 'camera', model: 'Synthetic', address: '10.0.0.1',
siteId: 'site-1', deviceGroupId: 'group-1', displayStatus: 'online', localStorage: false, siteId: null, deviceGroupId: 'group-1', displayStatus: 'online', localStorage: false,
}]); }]);
assert.deepEqual(payload.sites, [{ id: 'site-1', name: 'HQ', pendingDeletionStart: null }]); assert.deepEqual(payload.sites, [{ id: 'site-1', name: 'HQ', pendingDeletionStart: null }]);
assert.deepEqual(payload.groups, [{ id: 'group-1', name: 'Lobby', parentId: null, pendingDeletionStart: null }]); assert.deepEqual(payload.groups, [{ id: 'group-1', name: 'Lobby', parentId: null, pendingDeletionStart: null }]);
assert.deepEqual(payload.diagnostics.devices, { received: 2, eligible: 1, invalid: 1 }); assert.deepEqual(payload.diagnostics.devices, {
received: 3, eligible: 1, invalid: 1, cloudNativeExcluded: 1,
});
assert.doesNotMatch(JSON.stringify(payload), /drop-me|cookie|secretCapability|private/); assert.doesNotMatch(JSON.stringify(payload), /drop-me|cookie|secretCapability|private/);
}); });
test('infers camera sites from device-group parents instead of server groups', () => {
const payload = projectDeviceHierarchy({
devices: [rawDevice(3, {
name: 'Front Door',
server_group_id: 'cloud-connector-server-group',
device_group_id: 'entrances',
})],
sites: [{ id: 'test-site', name: 'Test Site' }],
groups: [{ id: 'entrances', name: 'Entrances', parent_id: 'test-site' }],
});
const model = buildDeviceTree(payload);
const camera = [...model.cameraByKey.values()][0];
assert.equal(payload.devices[0].siteId, null);
assert.equal(camera.site.canonicalId, 'test-site');
assert.equal(camera.group.canonicalId, 'entrances');
assert.equal(camera.hierarchyStatus, 'inferred-site');
assert.deepEqual(model.diagnostics, []);
});
test('excludes cloud-native cameras while retaining Cloud Connector and unknown-storage devices', () => {
const payload = projectDeviceHierarchy({
devices: [
rawDevice(10, { name: 'Cloud Native', capabilities: { localStorage: true } }),
rawDevice(11, { name: 'Cloud Connector', capabilities: { localStorage: false } }),
rawDevice(12, { name: 'Unknown Storage', capabilities: {} }),
],
});
assert.deepEqual(payload.devices.map((device) => device.name), ['Cloud Connector', 'Unknown Storage']);
assert.deepEqual(payload.diagnostics.devices, {
received: 3, eligible: 2, invalid: 0, cloudNativeExcluded: 1,
});
});
test('projects synthetic 1,500 and 10,000 camera deployments below the 8 MiB IPC cap', () => { test('projects synthetic 1,500 and 10,000 camera deployments below the 8 MiB IPC cap', () => {
for (const count of [1_500, 10_000]) { for (const count of [1_500, 10_000]) {
const payload = projectDeviceHierarchy({ const payload = projectDeviceHierarchy({
devices: Array.from({ length: count }, (_, index) => rawDevice(index)), devices: Array.from({ length: count }, (_, index) => rawDevice(index, {
capabilities: { localStorage: false },
})),
sites: [], sites: [],
groups: [], groups: [],
}); });
@@ -80,6 +124,7 @@ test('repairs lone UTF-16 surrogates without changing valid Unicode or hiding ca
address: 'Hall \ud800\udc00 / \udc00', address: 'Hall \ud800\udc00 / \udc00',
server_group_id: 'site-\ud800', server_group_id: 'site-\ud800',
device_group_id: 'group-\udc00', device_group_id: 'group-\udc00',
capabilities: { localStorage: false },
})], })],
sites: [{ id: 'site-\ud800', name: 'Valid \ud83d\udcf7 \udc00', pending_deletion_start: '\ud800' }], sites: [{ id: 'site-\ud800', name: 'Valid \ud83d\udcf7 \udc00', pending_deletion_start: '\ud800' }],
groups: [{ id: 'group-\udc00', name: 'Group \ud83d\udcf7 \ud800', parent_id: 'site-\ud800' }], groups: [{ id: 'group-\udc00', name: 'Group \ud83d\udcf7 \ud800', parent_id: 'site-\ud800' }],
@@ -90,7 +135,7 @@ test('repairs lone UTF-16 surrogates without changing valid Unicode or hiding ca
assert.equal(payload.devices[0].name, 'Front \ufffd camera'); assert.equal(payload.devices[0].name, 'Front \ufffd camera');
assert.equal(payload.devices[0].model, 'Model \ufffd'); assert.equal(payload.devices[0].model, 'Model \ufffd');
assert.equal(payload.devices[0].address, 'Hall \ud800\udc00 / \ufffd'); assert.equal(payload.devices[0].address, 'Hall \ud800\udc00 / \ufffd');
assert.equal(payload.devices[0].siteId, 'site-\ufffd'); assert.equal(payload.devices[0].siteId, null);
assert.equal(payload.devices[0].deviceGroupId, 'group-\ufffd'); assert.equal(payload.devices[0].deviceGroupId, 'group-\ufffd');
assert.deepEqual(payload.sites, [{ id: 'site-\ufffd', name: 'Valid \ud83d\udcf7 \ufffd', pendingDeletionStart: '\ufffd' }]); assert.deepEqual(payload.sites, [{ id: 'site-\ufffd', name: 'Valid \ud83d\udcf7 \ufffd', pendingDeletionStart: '\ufffd' }]);
assert.deepEqual(payload.groups, [{ id: 'group-\ufffd', name: 'Group \ud83d\udcf7 \ufffd', parentId: 'site-\ufffd', pendingDeletionStart: null }]); assert.deepEqual(payload.groups, [{ id: 'group-\ufffd', name: 'Group \ud83d\udcf7 \ufffd', parentId: 'site-\ufffd', pendingDeletionStart: null }]);
@@ -101,6 +146,7 @@ test('repairs lone UTF-16 surrogates without changing valid Unicode or hiding ca
test('rejects a projected hierarchy payload above 8 MiB', () => { test('rejects a projected hierarchy payload above 8 MiB', () => {
const devices = Array.from({ length: 10_000 }, (_, index) => rawDevice(index, { const devices = Array.from({ length: 10_000 }, (_, index) => rawDevice(index, {
name: `Camera ${index} ${'x'.repeat(900)}`, name: `Camera ${index} ${'x'.repeat(900)}`,
capabilities: { localStorage: false },
})); }));
assert.throws( assert.throws(
() => projectDeviceHierarchy({ devices, sites: [], groups: [] }), () => projectDeviceHierarchy({ devices, sites: [], groups: [] }),
+7 -5
View File
@@ -39,10 +39,11 @@ function makeElement() {
function makePopupHarness({ paired = true, confirmCopy = false, clipboardReject = null, validServerProof = true } = {}) { function makePopupHarness({ paired = true, confirmCopy = false, clipboardReject = null, validServerProof = true } = {}) {
const elements = Object.fromEntries( const elements = Object.fromEntries(
['tabInfo', 'pairingInfo', 'sendBtn', 'copyBtn', 'statusMsg', 'copyWarning', 'confirmCopy', 'openOptionsBtn'] ['tabInfo', 'pairingInfo', 'sendBtn', 'copyBtn', 'statusMsg', 'openOptionsBtn']
.map((id) => [id, makeElement()]) .map((id) => [id, makeElement()])
); );
const clipboardWrites = []; const clipboardWrites = [];
const confirmationMessages = [];
const fetchCalls = []; const fetchCalls = [];
let cookieReads = 0; let cookieReads = 0;
const chromeApi = { const chromeApi = {
@@ -65,7 +66,7 @@ function makePopupHarness({ paired = true, confirmCopy = false, clipboardReject
chromeApi, chromeApi,
documentApi, documentApi,
navigatorApi, navigatorApi,
confirmCopy: () => confirmCopy, confirmCopy: (message) => { confirmationMessages.push(message); return confirmCopy; },
cryptoApi: crypto.webcrypto, cryptoApi: crypto.webcrypto,
fetchImpl: async (...args) => { fetchImpl: async (...args) => {
fetchCalls.push(args); fetchCalls.push(args);
@@ -81,7 +82,7 @@ function makePopupHarness({ paired = true, confirmCopy = false, clipboardReject
return { ok: true, json: async () => ({ success: true }) }; return { ok: true, json: async () => ({ success: true }) };
} }
}); });
return { controller, elements, clipboardWrites, fetchCalls, get cookieReads() { return cookieReads; } }; return { controller, elements, clipboardWrites, confirmationMessages, fetchCalls, get cookieReads() { return cookieReads; } };
} }
test('manifest commits only a public key, stable ID, local storage, and exact loopback host access', () => { test('manifest commits only a public key, stable ID, local storage, and exact loopback host access', () => {
@@ -174,11 +175,11 @@ test('copy cancellation occurs before cookie access and never writes the token',
assert.match(harness.elements.statusMsg.textContent, /cancelled/i); assert.match(harness.elements.statusMsg.textContent, /cancelled/i);
}); });
test('confirmed copy warns about clipboard history and reports success without exposing token', async () => { test('confirmed copy uses one concise prompt and reports success without exposing token', async () => {
const harness = makePopupHarness({ paired: true, confirmCopy: true }); const harness = makePopupHarness({ paired: true, confirmCopy: true });
await harness.controller.init(); await harness.controller.init();
assert.match(harness.elements.copyWarning.textContent, /clipboard (?:history|sync)/i);
await harness.controller.copyToken(); await harness.controller.copyToken();
assert.deepEqual(harness.confirmationMessages, ['Copy VA token?']);
assert.deepEqual(harness.clipboardWrites, ['sensitive-va-token']); assert.deepEqual(harness.clipboardWrites, ['sensitive-va-token']);
assert.match(harness.elements.statusMsg.textContent, /copied/i); assert.match(harness.elements.statusMsg.textContent, /copied/i);
assert.equal(harness.elements.statusMsg.textContent.includes('sensitive-va-token'), false); assert.equal(harness.elements.statusMsg.textContent.includes('sensitive-va-token'), false);
@@ -201,6 +202,7 @@ test('options UI stores a validated pairing secret locally and can forget pairin
const js = read('options.js'); const js = read('options.js');
assert.match(html, /pairingSecret/); assert.match(html, /pairingSecret/);
assert.match(html, /type="password"/); assert.match(html, /type="password"/);
assert.doesNotMatch(html, /treat it like a password|privacy-note/i);
assert.match(js, /chrome\.storage\.local\.set/); assert.match(js, /chrome\.storage\.local\.set/);
assert.match(js, /chrome\.storage\.local\.remove/); assert.match(js, /chrome\.storage\.local\.remove/);
assert.match(js, /aptPairingSecret/); assert.match(js, /aptPairingSecret/);
+22 -22
View File
@@ -11,7 +11,7 @@ const APPROVED_DIRECTORY = 'C:\\Program Files\\Alta Proxy Tool';
const APPROVED_HELPER = 'C:\\Program Files\\Alta Proxy Tool\\aware-cam-proxy.exe'; const APPROVED_HELPER = 'C:\\Program Files\\Alta Proxy Tool\\aware-cam-proxy.exe';
const VALID_HOST = 'tenant.avasecurity.com'; const VALID_HOST = 'tenant.avasecurity.com';
const VALID_DEVICE_ID = '123e4567-e89b-42d3-a456-426614174000'; const VALID_DEVICE_ID = '123e4567-e89b-42d3-a456-426614174000';
const VALID_USERNAME = 'proxy.operator+apt@example.com'; const SYNTHETIC_COOKIE = 'HERMES_SENTINEL_SESSION_COOKIE';
function loadModule() { function loadModule() {
return require(MODULE_PATH); return require(MODULE_PATH);
@@ -67,7 +67,7 @@ function validRequest(overrides = {}) {
return { return {
deploymentHost: VALID_HOST, deploymentHost: VALID_HOST,
deviceId: VALID_DEVICE_ID, deviceId: VALID_DEVICE_ID,
username: VALID_USERNAME, cookie: SYNTHETIC_COOKIE,
...overrides ...overrides
}; };
} }
@@ -76,18 +76,18 @@ test('exports the proxy manager module', () => {
assert.doesNotThrow(() => loadModule()); assert.doesNotThrow(() => loadModule());
}); });
test('launches the approved helper directly in a visible interactive console', () => { test('launches the approved helper directly with the paired Alta session', () => {
const { manager, calls } = createHarness(); const { manager, calls } = createHarness();
const result = manager.launchProxy(validRequest()); const result = manager.launchProxy(validRequest());
assert.deepEqual(calls, [[ assert.deepEqual(calls, [[
APPROVED_HELPER, APPROVED_HELPER,
['-a', VALID_HOST, '-u', VALID_USERNAME, '-d', VALID_DEVICE_ID], ['-a', VALID_HOST, '-d', VALID_DEVICE_ID, '-k', SYNTHETIC_COOKIE],
{ {
shell: false, shell: false,
detached: true, detached: true,
stdio: 'inherit', stdio: 'ignore',
windowsHide: false windowsHide: false
} }
]]); ]]);
@@ -100,16 +100,15 @@ test('launches the approved helper directly in a visible interactive console', (
}); });
}); });
test('passes username punctuation literally in argv without invoking a shell', () => { test('passes the paired cookie literally without invoking a shell or console prompt', () => {
const username = 'proxy+apt&literal|name@example.com'; const cookie = 'abc_DEF-123.456==';
const { manager, calls } = createHarness(); const { manager, calls } = createHarness();
manager.launchProxy(validRequest({ username })); manager.launchProxy(validRequest({ cookie }));
assert.equal(calls[0][1][3], username); assert.equal(calls[0][1][5], cookie);
assert.equal(calls[0][2].shell, false); assert.equal(calls[0][2].shell, false);
assert.equal(calls[0][2].stdio, 'inherit'); assert.equal(calls[0][2].stdio, 'ignore');
assert.notEqual(calls[0][2].stdio, 'ignore');
}); });
test('rejects the CRLF calc.exe reproducer in every structured input', () => { test('rejects the CRLF calc.exe reproducer in every structured input', () => {
@@ -117,8 +116,8 @@ test('rejects the CRLF calc.exe reproducer in every structured input', () => {
const attacks = [ const attacks = [
{ deploymentHost: `${VALID_HOST}\r\ncalc.exe` }, { deploymentHost: `${VALID_HOST}\r\ncalc.exe` },
{ deviceId: `${VALID_DEVICE_ID}\r\ncalc.exe` }, { deviceId: `${VALID_DEVICE_ID}\r\ncalc.exe` },
{ username: `${VALID_USERNAME}\r\ncalc.exe` }, { cookie: `${SYNTHETIC_COOKIE}\r\ncalc.exe` },
{ username: `${VALID_USERNAME}\0calc.exe` } { cookie: `${SYNTHETIC_COOKIE}\0calc.exe` }
]; ];
for (const attack of attacks) { for (const attack of attacks) {
@@ -191,10 +190,10 @@ test('normalizes a valid Alta hostname to lowercase', () => {
assert.equal(calls[0][1][1], 'tenant.avigilon.com'); assert.equal(calls[0][1][1], 'tenant.avigilon.com');
}); });
test('rejects empty, oversized, non-string, and control-character usernames', () => { test('rejects empty, oversized, non-string, and control-character cookies', () => {
for (const username of ['', 'x'.repeat(255), 42, 'user\nname', 'user\rname', 'user\0name', 'user\u007fname']) { for (const cookie of ['', 'x'.repeat(4097), 42, 'abc\nxyz', 'abc\rxyz', 'abc\0xyz', 'abc;xyz']) {
const { manager } = createHarness(); const { manager } = createHarness();
assert.throws(() => manager.launchProxy(validRequest({ username })), /username/i); assert.throws(() => manager.launchProxy(validRequest({ cookie })), /cookie/i);
} }
}); });
@@ -218,25 +217,27 @@ test('requires an absolute approved application directory and Windows platform',
); );
}); });
test('reports a bounded spawn failure without credential redaction machinery', () => { test('redacts the cookie if spawn throws an error containing it', () => {
const { createProxyManager } = loadModule(); const { createProxyManager } = loadModule();
const manager = createProxyManager({ const manager = createProxyManager({
appDirectory: APPROVED_DIRECTORY, appDirectory: APPROVED_DIRECTORY,
fs: { existsSync: () => true }, fs: { existsSync: () => true },
platform: 'win32', platform: 'win32',
spawn: () => { throw new Error('spawn failed'); } spawn: () => { throw new Error(`spawn failed for ${SYNTHETIC_COOKIE}`); }
}); });
assert.throws( assert.throws(
() => manager.launchProxy(validRequest()), () => manager.launchProxy(validRequest()),
(error) => { (error) => {
assert.match(error.message, /spawn failed/); assert.match(error.message, /spawn failed/);
assert.match(error.message, /\[REDACTED\]/);
assert.doesNotMatch(error.message, /HERMES_SENTINEL_SESSION_COOKIE/);
return true; return true;
} }
); );
}); });
test('tracks only safe process metadata and never exposes usernames', () => { test('tracks only safe process metadata and never exposes cookies', () => {
const { manager } = createHarness(); const { manager } = createHarness();
manager.launchProxy(validRequest()); manager.launchProxy(validRequest());
@@ -247,7 +248,7 @@ test('tracks only safe process metadata and never exposes usernames', () => {
startedAt: 1_777_777_777_777, startedAt: 1_777_777_777_777,
status: 'running' status: 'running'
}]); }]);
assert.doesNotMatch(JSON.stringify(tracked), /proxy\.operator/); assert.doesNotMatch(JSON.stringify(tracked), /HERMES_SENTINEL_SESSION_COOKIE/);
}); });
test('stopping one tracked process waits for confirmed exit and leaves the other alive', async () => { test('stopping one tracked process waits for confirmed exit and leaves the other alive', async () => {
@@ -381,9 +382,8 @@ test('exit events remove only the matching owned child', () => {
assert.deepEqual(manager.listTrackedProxies().map((item) => item.processId), [4101]); assert.deepEqual(manager.listTrackedProxies().map((item) => item.processId), [4101]);
}); });
test('source contains no bearer argv, shell launchers, broad process killers, or persistence APIs', () => { test('source contains no shell launchers, broad process killers, or credential persistence APIs', () => {
const source = fs.readFileSync(path.join(__dirname, '..', 'src', 'proxy-launch.js'), 'utf8'); const source = fs.readFileSync(path.join(__dirname, '..', 'src', 'proxy-launch.js'), 'utf8');
assert.doesNotMatch(source, /cookie|bearer|token|-k/i);
assert.doesNotMatch(source, /\b(?:cmd(?:\.exe)?|powershell|taskkill|pkill|wmic)\b/i); assert.doesNotMatch(source, /\b(?:cmd(?:\.exe)?|powershell|taskkill|pkill|wmic)\b/i);
assert.doesNotMatch(source, /\.(?:bat|command)\b/i); assert.doesNotMatch(source, /\.(?:bat|command)\b/i);
assert.doesNotMatch(source, /(?:writeFile|appendFile|mkdtemp|tmpdir)/); assert.doesNotMatch(source, /(?:writeFile|appendFile|mkdtemp|tmpdir)/);
+9 -1
View File
@@ -2,7 +2,7 @@
const test = require('node:test'); const test = require('node:test');
const assert = require('node:assert/strict'); const assert = require('node:assert/strict');
const { createRendererController } = require('../renderer-controller'); const { createRendererController, shouldShowPairingOnboarding } = require('../renderer-controller');
function harness(result) { function harness(result) {
const calls = []; const calls = [];
@@ -39,3 +39,11 @@ test('successful disconnect renders disconnected state and clears device state',
['status', 'Disconnected from Alta.', 'info'], ['status', 'Disconnected from Alta.', 'info'],
]); ]);
}); });
test('pairing onboarding is temporary but remains recoverable', () => {
assert.equal(shouldShowPairingOnboarding({ paired: false }), true);
assert.equal(shouldShowPairingOnboarding({ paired: true, secretVisible: true }), true);
assert.equal(shouldShowPairingOnboarding({ paired: true }), false);
assert.equal(shouldShowPairingOnboarding({ paired: true, secretVisible: true, connected: true }), false);
assert.equal(shouldShowPairingOnboarding({ paired: true, connected: true, manuallyOpen: true }), true);
});
+1
View File
@@ -40,6 +40,7 @@ test('renderer uses one hierarchy request and no parallel flat discovery', () =>
assert.match(renderer, /electronAPI\.getDeviceHierarchy\(\)/); assert.match(renderer, /electronAPI\.getDeviceHierarchy\(\)/);
assert.doesNotMatch(renderer, /electronAPI\.getDevices\(|electronAPI\.getDeviceSites\(|Promise\.all\(\s*\[\s*window\.electronAPI\.get/); assert.doesNotMatch(renderer, /electronAPI\.getDevices\(|electronAPI\.getDeviceSites\(|Promise\.all\(\s*\[\s*window\.electronAPI\.get/);
assert.doesNotMatch(renderer, /\ballDevices\b|\ballSites\b|\bcollapsedSites\b|groupDevicesBySite/); assert.doesNotMatch(renderer, /\ballDevices\b|\ballSites\b|\bcollapsedSites\b|groupDevicesBySite/);
assert.doesNotMatch(renderer, /Selected camera is hidden|hidden by the current view/i);
assert.match(renderer, /loadGeneration/); assert.match(renderer, /loadGeneration/);
assert.match(renderer, /generation\s*!==\s*loadGeneration/); assert.match(renderer, /generation\s*!==\s*loadGeneration/);
}); });
+14 -36
View File
@@ -92,14 +92,11 @@ test('runtime keeps Alta credentials in main-owned modules and exposes only non-
localStorage: null, localStorage: null,
}], }],
}); });
const launched = await runtime.launchProxy( const launched = await runtime.launchProxy('550e8400-e29b-41d4-a716-446655440000');
'550e8400-e29b-41d4-a716-446655440000',
'proxy.operator@example.com'
);
assert.equal(launched.success, true); assert.equal(launched.success, true);
assert.deepEqual(calls[0], { assert.deepEqual(calls[0], {
deploymentHost: 'customer.avasecurity.com', deploymentHost: 'customer.avasecurity.com',
username: 'proxy.operator@example.com', cookie: 'top-secret-cookie',
deviceId: '550e8400-e29b-41d4-a716-446655440000', deviceId: '550e8400-e29b-41d4-a716-446655440000',
}); });
assert.deepEqual(runtime.getConnectionState(), { assert.deepEqual(runtime.getConnectionState(), {
@@ -189,7 +186,7 @@ test('newest hierarchy discovery owns the launch allowlist when completions arri
const freshB = runtime.getDeviceHierarchy(); const freshB = runtime.getDeviceHierarchy();
pending[1]([{ guid: deviceB }]); pending[1]([{ guid: deviceB }]);
assert.equal((await freshB).success, true); assert.equal((await freshB).success, true);
assert.equal((await runtime.launchProxy(deviceB, 'operator@example.com')).success, true); assert.equal((await runtime.launchProxy(deviceB)).success, true);
pending[0]([{ guid: deviceA }]); pending[0]([{ guid: deviceA }]);
assert.deepEqual(await staleA, { assert.deepEqual(await staleA, {
success: false, success: false,
@@ -197,7 +194,7 @@ test('newest hierarchy discovery owns the launch allowlist when completions arri
hierarchy: { devices: [], sites: [], groups: [] }, hierarchy: { devices: [], sites: [], groups: [] },
message: 'Alta device discovery result is stale', message: 'Alta device discovery result is stale',
}); });
assert.equal((await runtime.launchProxy(deviceA, 'operator@example.com')).success, false); assert.equal((await runtime.launchProxy(deviceA)).success, false);
assert.deepEqual(launches, [deviceB]); assert.deepEqual(launches, [deviceB]);
}); });
@@ -248,7 +245,7 @@ test('launch allowlist remains bound to the session origin that produced it', as
}); });
assert.equal((await runtime.getDeviceHierarchy()).success, true); assert.equal((await runtime.getDeviceHierarchy()).success, true);
sessionStore.establish('https://second.avasecurity.com', 'replacement-cookie'); sessionStore.establish('https://second.avasecurity.com', 'replacement-cookie');
assert.equal((await runtime.launchProxy(deviceId, 'operator@example.com')).success, false); assert.equal((await runtime.launchProxy(deviceId)).success, false);
assert.equal(launches, 0); assert.equal(launches, 0);
}); });
@@ -256,28 +253,6 @@ test('runtime rejects discovery deadlines above 60 seconds', () => {
assert.throws(() => new AppRuntime({ discoveryTimeoutMs: 60_001 }), /discovery timeout/i); assert.throws(() => new AppRuntime({ discoveryTimeoutMs: 60_001 }), /discovery timeout/i);
}); });
test('runtime rejects invalid usernames before calling the proxy manager', async () => {
const sessionStore = createSessionStore();
sessionStore.establish('https://customer.avasecurity.com', 'top-secret-cookie');
const deviceId = '550e8400-e29b-41d4-a716-446655440000';
let launches = 0;
const runtime = new AppRuntime({
sessionStore,
altaClient: { getDevices: async () => [{ guid: deviceId }] },
proxyManager: {
launchProxy() { launches += 1; },
listTrackedProxies() { return []; },
},
});
await runtime.getDevices();
for (const username of ['', 'x'.repeat(255), 'operator@example.com\r\n-k secret', null]) {
const result = await runtime.launchProxy(deviceId, username);
assert.equal(result.success, false);
assert.match(result.message, /username/i);
}
assert.equal(launches, 0);
});
test('bridge authenticates itself before accepting a one-time HMAC cookie request', async () => { test('bridge authenticates itself before accepting a one-time HMAC cookie request', async () => {
const protect = (value) => Buffer.from(`protected:${value}`); const protect = (value) => Buffer.from(`protected:${value}`);
@@ -403,10 +378,10 @@ test('runtime reconciles exited children and permits relaunch for the same devic
}, },
}); });
await runtime.getDevices(); await runtime.getDevices();
assert.equal((await runtime.launchProxy(deviceId, 'operator@example.com')).processId, 4101); assert.equal((await runtime.launchProxy(deviceId)).processId, 4101);
tracked.length = 0; tracked.length = 0;
assert.deepEqual(runtime.getConnectionState().activeProxies, []); assert.deepEqual(runtime.getConnectionState().activeProxies, []);
assert.equal((await runtime.launchProxy(deviceId, 'operator@example.com')).processId, 4102); assert.equal((await runtime.launchProxy(deviceId)).processId, 4102);
}); });
test('disconnect stops every owned proxy before clearing the Alta session', async () => { test('disconnect stops every owned proxy before clearing the Alta session', async () => {
@@ -474,7 +449,7 @@ test('kill request without exit keeps proxy tracked and session connected until
setTimeout(callback) { timeoutCallback = callback; return 1; }, setTimeout(callback) { timeoutCallback = callback; return 1; },
clearTimeout() {}, clearTimeout() {},
}); });
proxyManager.launchProxy({ deploymentHost: 'customer.avasecurity.com', username: 'operator@example.com', deviceId }); proxyManager.launchProxy({ deploymentHost: 'customer.avasecurity.com', cookie: 'synthetic-cookie', deviceId });
const runtime = new AppRuntime({ sessionStore, altaClient: {}, proxyManager }); const runtime = new AppRuntime({ sessionStore, altaClient: {}, proxyManager });
const firstDisconnect = runtime.disconnect(); const firstDisconnect = runtime.disconnect();
@@ -525,13 +500,16 @@ test('preload and renderer expose only narrow, credential-free contracts', () =>
]; ];
for (const method of expectedMethods) assert.match(preload, new RegExp(`\\b${method}\\b`)); for (const method of expectedMethods) assert.match(preload, new RegExp(`\\b${method}\\b`));
assert.doesNotMatch(preload, /downloadAndInstall|download-and-install|onUpdateDownloadProgress|onExtensionCookie/); assert.doesNotMatch(preload, /downloadAndInstall|download-and-install|onUpdateDownloadProgress|onExtensionCookie/);
assert.match(preload, /launchProxy:\s*\(deviceId, username\)/); assert.match(preload, /launchProxy:\s*\(deviceId\)/);
assert.doesNotMatch(preload, /launchProxy:\s*\([^)]*(?:cookie|origin)/i); assert.doesNotMatch(preload, /launchProxy:\s*\([^)]*(?:cookie|origin)/i);
assert.doesNotMatch(renderer, /cookieValue|sessionData\.cookies|cookies\s*:/); assert.doesNotMatch(renderer, /cookieValue|sessionData\.cookies|cookies\s*:/);
assert.match(renderer, /state\.connected\s*&&\s*\(!wasConnected\s*\|\|\s*state\.origin\s*!==\s*previousOrigin\)/); assert.match(renderer, /state\.connected\s*&&\s*\(!wasConnected\s*\|\|\s*state\.origin\s*!==\s*previousOrigin\)/);
assert.match(read('main.js'), /onConnectionStateChanged:\s*\(\)\s*=>\s*\{\s*runtime\.onSessionChanged\(\)/); assert.match(read('main.js'), /onConnectionStateChanged:\s*\(\)\s*=>\s*\{\s*runtime\.onSessionChanged\(\)/);
assert.doesNotMatch(html, /id="cookieKey"|updateProgress|Install Update/); assert.doesNotMatch(html, /id="cookieKey"|id="altaUsername"|type="password"|updateProgress|Install Update/);
assert.match(html, /id="altaUsername"/); assert.match(html, /paired Chrome extension session/i);
assert.match(html, /id="pairingSection"[^>]*hidden/);
assert.match(html, /id="managePairingBtn"/);
assert.match(renderer, /shouldShowPairingOnboarding/);
assert.match(renderer, /openFixedReleasesPage/); assert.match(renderer, /openFixedReleasesPage/);
assert.match(html, /Bridge Pairing/); assert.match(html, /Bridge Pairing/);
}); });