feat: add paired Chrome bridge authentication

This commit is contained in:
2026-08-19 21:23:03 +00:00
parent b503b5777a
commit b6df6f1a66
10 changed files with 968 additions and 182 deletions
+5 -3
View File
@@ -1,14 +1,16 @@
{
"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"],
"version": "1.1.0",
"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",
"permissions": ["cookies", "activeTab", "clipboardWrite", "storage"],
"host_permissions": [
"https://*.avasecurity.com/*",
"https://*.avigilon.com/*",
"http://127.0.0.1:18247/*"
],
"options_page": "options.html",
"action": {
"default_popup": "popup.html",
"default_icon": {
+83
View File
@@ -0,0 +1,83 @@
:root {
color-scheme: dark;
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, sans-serif;
}
body {
margin: 0;
background: #1e1e1e;
color: #e0e0e0;
}
.options-card {
max-width: 560px;
margin: 48px auto;
padding: 28px;
border: 1px solid #3c3c3c;
border-radius: 8px;
background: #252526;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.25);
}
h1 { margin-top: 0; color: #75afff; }
h2 { margin-bottom: 6px; font-size: 16px; }
p { line-height: 1.5; }
label {
display: block;
margin-bottom: 6px;
font-weight: 600;
}
input {
box-sizing: border-box;
width: 100%;
margin-bottom: 12px;
padding: 11px;
border: 1px solid #555;
border-radius: 4px;
background: #1e1e1e;
color: #fff;
font: inherit;
}
input:focus {
border-color: #0e7afe;
outline: 2px solid rgba(14, 122, 254, 0.3);
}
button {
padding: 10px 16px;
border-radius: 4px;
color: #fff;
cursor: pointer;
font: inherit;
font-weight: 700;
}
.primary-btn { border: 0; background: #0e7afe; }
.danger-btn { border: 1px solid #f44336; background: transparent; color: #ff7b72; }
button:disabled { cursor: not-allowed; opacity: 0.45; }
.status {
min-height: 20px;
margin-top: 14px;
font-weight: 600;
}
.status.success,
.paired { color: #7bd67e; }
.status.error,
.unpaired { color: #ffc36d; }
.paired-controls {
margin-top: 24px;
padding-top: 14px;
border-top: 1px solid #3c3c3c;
}
.privacy-note {
margin-top: 24px;
color: #aaa;
font-size: 12px;
}
+29
View File
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
<title>Pair Alta Proxy Tool</title>
<link rel="stylesheet" href="options.css">
</head>
<body>
<main class="options-card">
<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>
<form id="pairingForm">
<label for="pairingSecret">One-time pairing secret</label>
<input id="pairingSecret" name="pairingSecret" type="password" inputmode="text" autocomplete="off" spellcheck="false" required>
<button type="submit" class="primary-btn">Pair extension</button>
</form>
<div id="pairingStatus" class="status" role="status" aria-live="polite"></div>
<section class="paired-controls">
<h2>Current pairing</h2>
<p id="currentState">Checking...</p>
<button id="forgetBtn" type="button" class="danger-btn" disabled>Forget pairing</button>
</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>
<script src="options.js"></script>
</body>
</html>
+61
View File
@@ -0,0 +1,61 @@
'use strict';
const PAIRING_STORAGE_KEY = 'aptPairingSecret';
const PAIRING_SECRET_PATTERN = /^[A-Za-z0-9_-]{43}$/;
const pairingForm = document.getElementById('pairingForm');
const pairingSecretInput = document.getElementById('pairingSecret');
const pairingStatus = document.getElementById('pairingStatus');
const currentState = document.getElementById('currentState');
const forgetBtn = document.getElementById('forgetBtn');
function setStatus(message, type) {
pairingStatus.textContent = message;
pairingStatus.className = `status ${type}`;
}
function renderPairingState(paired) {
currentState.textContent = paired ? 'Paired on this Chrome profile.' : 'Not paired.';
currentState.className = paired ? 'paired' : 'unpaired';
forgetBtn.disabled = !paired;
}
async function refreshPairingState() {
const stored = await chrome.storage.local.get(PAIRING_STORAGE_KEY);
const candidate = stored && stored[PAIRING_STORAGE_KEY];
renderPairingState(PAIRING_SECRET_PATTERN.test(typeof candidate === 'string' ? candidate : ''));
}
pairingForm.addEventListener('submit', async (event) => {
event.preventDefault();
const candidate = pairingSecretInput.value.trim();
pairingSecretInput.value = '';
if (!PAIRING_SECRET_PATTERN.test(candidate)) {
setStatus('That secret is not valid. Generate a new pairing secret in APT and paste it exactly.', 'error');
return;
}
try {
await chrome.storage.local.set({ [PAIRING_STORAGE_KEY]: candidate });
setStatus('Pairing saved. You can now use Send to APT from an Alta tab.', 'success');
renderPairingState(true);
} catch {
setStatus('Chrome could not save the pairing. Try again.', 'error');
}
});
forgetBtn.addEventListener('click', async () => {
try {
await chrome.storage.local.remove(PAIRING_STORAGE_KEY);
renderPairingState(false);
setStatus('Pairing forgotten. Revoke or rotate it in APT as well.', 'success');
} catch {
setStatus('Chrome could not forget the pairing. Try again.', 'error');
}
});
refreshPairingState().catch(() => {
renderPairingState(false);
setStatus('Chrome could not read pairing state.', 'error');
});
+70 -59
View File
@@ -2,120 +2,131 @@
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;
min-width: 320px;
background: #1e1e1e;
color: #e0e0e0;
font: 14px/1.4 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, sans-serif;
}
.popup-container {
padding: 16px;
}
.popup-container { padding: 16px; }
h1 {
margin: 0 0 12px;
color: #0e7afe;
font-size: 16px;
font-weight: 600;
color: #0E7AFE;
margin: 0 0 12px 0;
letter-spacing: 0.5px;
text-align: center;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.tab-info {
background: #2D2D30;
border: 1px solid #3C3C3C;
.tab-info,
.pairing-info {
margin-bottom: 10px;
padding: 9px 11px;
border: 1px solid #3c3c3c;
border-radius: 4px;
padding: 10px 12px;
margin-bottom: 12px;
background: #2d2d30;
color: #aaa;
font-size: 13px;
color: #999999;
word-break: break-all;
overflow-wrap: anywhere;
}
.tab-info.detected {
color: #4CAF50;
border-color: #4CAF50;
.tab-info.detected,
.pairing-info.paired {
border-color: #4caf50;
color: #7bd67e;
}
.tab-info.not-detected {
color: #F44336;
border-color: #F44336;
.tab-info.not-detected,
.pairing-info.unpaired {
border-color: #f0a33a;
color: #ffc36d;
}
.action-buttons {
display: grid;
gap: 8px;
}
.action-buttons { display: grid; gap: 8px; }
.primary-btn,
.secondary-btn {
display: block;
.secondary-btn,
.link-btn {
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;
font-family: inherit;
font-size: 14px;
font-weight: 700;
}
.primary-btn {
background: #0E7AFE;
border: 0;
background: #0e7afe;
color: #fff;
}
.secondary-btn {
background: #2D2D30;
border: 1px solid #0E7AFE;
color: #E0E0E0;
border: 1px solid #0e7afe;
background: #2d2d30;
color: #e0e0e0;
}
.primary-btn:hover:not(:disabled) {
background: #0A5FD9;
.link-btn {
margin: -2px 0 10px;
padding: 5px;
border: 0;
background: transparent;
color: #75afff;
text-decoration: underline;
}
.secondary-btn:hover:not(:disabled) {
background: #0E7AFE;
}
.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;
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 {
display: none;
margin-top: 10px;
padding: 8px 12px;
border: 1px solid transparent;
border-radius: 4px;
font-size: 13px;
font-weight: bold;
display: none;
border: 1px solid transparent;
font-weight: 700;
}
.status-msg.success,
.status-msg.error,
.status-msg.info { display: block; }
.status-msg.success {
display: block;
border-color: #4caf50;
background: rgba(76, 175, 80, 0.1);
color: #4CAF50;
border-color: #4CAF50;
color: #7bd67e;
}
.status-msg.error {
display: block;
border-color: #f44336;
background: rgba(244, 67, 54, 0.1);
color: #F44336;
border-color: #F44336;
color: #ff7b72;
}
.status-msg.info {
display: block;
border-color: #0e7afe;
background: rgba(14, 122, 254, 0.1);
color: #0E7AFE;
border-color: #0E7AFE;
color: #75afff;
}
[hidden] { display: none !important; }
+9 -5
View File
@@ -3,19 +3,23 @@
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src http://127.0.0.1:18247;">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Alta Proxy Tool Bridge</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="popup-container">
<main class="popup-container">
<h1>Alta Proxy Tool</h1>
<div id="pairingInfo" class="pairing-info">Checking pairing...</div>
<button id="openOptionsBtn" class="link-btn" type="button" hidden>Open pairing settings</button>
<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>
<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>
</div>
<div id="statusMsg" class="status-msg"></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>
</main>
<script src="popup.js"></script>
</body>
</html>
+167 -115
View File
@@ -1,130 +1,182 @@
'use strict';
const APT_URL = 'http://127.0.0.1:18247/cookie';
const APT_TOKEN = 'apt-local-bridge-token';
const PAIRING_STORAGE_KEY = 'aptPairingSecret';
const PAIRING_SECRET_PATTERN = /^[A-Za-z0-9_-]{43}$/;
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;
function isSupportedDeploymentUrl(value) {
try {
url = new URL(tab.url);
const url = new URL(value);
const host = url.hostname.toLowerCase();
return url.protocol === 'https:' &&
(host.endsWith('.avasecurity.com') || host.endsWith('.avigilon.com'));
} catch {
tabInfo.textContent = 'Cannot read this tab URL.';
tabInfo.className = 'tab-info not-detected';
return;
return false;
}
}
function createPopupController({
chromeApi,
documentApi,
navigatorApi,
fetchImpl,
confirmCopy
}) {
const tabInfo = documentApi.getElementById('tabInfo');
const pairingInfo = documentApi.getElementById('pairingInfo');
const sendBtn = documentApi.getElementById('sendBtn');
const copyBtn = documentApi.getElementById('copyBtn');
const copyWarning = documentApi.getElementById('copyWarning');
const statusMsg = documentApi.getElementById('statusMsg');
const openOptionsBtn = documentApi.getElementById('openOptionsBtn');
let detectedOrigin = null;
let pairingSecret = null;
let busy = false;
function showStatus(message, type) {
statusMsg.textContent = message;
statusMsg.className = `status-msg ${type}`;
}
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;
function updateActions() {
const disabled = busy || !detectedOrigin || !pairingSecret;
sendBtn.disabled = disabled;
copyBtn.disabled = disabled;
}
detectedOrigin = url.origin;
tabInfo.textContent = 'Detected: ' + hostname;
tabInfo.className = 'tab-info detected';
setActionButtonsDisabled(false);
});
function setBusy(value) {
busy = value;
updateActions();
}
// 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');
async function getVaCookieValue() {
if (!detectedOrigin) throw new Error('NO_DEPLOYMENT');
const cookie = await chromeApi.cookies.get({ url: detectedOrigin, name: 'va' });
if (!cookie || !cookie.value) throw new Error('MISSING_COOKIE');
if (cookie.expirationDate && cookie.expirationDate < Date.now() / 1000) {
throw new Error('EXPIRED_COOKIE');
}
} catch (err) {
if (err.message && err.message.includes('Failed to fetch')) {
showStatus('Alta Proxy Tool is not running.', 'error');
return cookie.value;
}
function showCookieError(error, copyOperation = false) {
if (error && error.message === 'MISSING_COOKIE') {
showStatus('No VA token found. Log in to Alta and try again.', 'error');
} else if (error && error.message === 'EXPIRED_COOKIE') {
showStatus('The VA token has expired. Log in to Alta again.', 'error');
} else if (copyOperation) {
showStatus('Could not copy the VA token. Check clipboard permission.', 'error');
} else {
showStatus('Error: ' + err.message, 'error');
showStatus('Could not reach Alta Proxy Tool on this computer.', '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);
async function sendToApt() {
if (!detectedOrigin || !pairingSecret || busy) return;
setBusy(true);
let cookieValue = null;
try {
showStatus('Sending to Alta Proxy Tool...', 'info');
cookieValue = await getVaCookieValue();
const response = await fetchImpl(APT_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-APT-Pairing-Secret': pairingSecret
},
body: JSON.stringify({ deploymentUrl: detectedOrigin, cookieValue })
});
const data = await response.json();
if (!response.ok || !data || data.success !== true) throw new Error('BRIDGE_REJECTED');
showStatus('VA token sent successfully.', 'success');
} catch (error) {
showCookieError(error);
} finally {
cookieValue = null;
setBusy(false);
}
}
});
async function copyToken() {
if (!detectedOrigin || !pairingSecret || busy) return;
const confirmed = confirmCopy(
'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) {
showStatus('Copy cancelled. The VA token was not read.', 'info');
return;
}
setBusy(true);
let cookieValue = null;
try {
cookieValue = await getVaCookieValue();
await navigatorApi.clipboard.writeText(cookieValue);
showStatus('VA token copied. Clear your clipboard after use.', 'success');
} catch (error) {
showCookieError(error, true);
} finally {
cookieValue = null;
setBusy(false);
}
}
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);
copyBtn.addEventListener('click', copyToken);
openOptionsBtn.addEventListener('click', () => chromeApi.runtime.openOptionsPage());
const stored = await chromeApi.storage.local.get(PAIRING_STORAGE_KEY);
const candidate = stored && stored[PAIRING_STORAGE_KEY];
if (PAIRING_SECRET_PATTERN.test(typeof candidate === 'string' ? candidate : '')) {
pairingSecret = candidate;
pairingInfo.textContent = 'Paired with Alta Proxy Tool';
pairingInfo.className = 'pairing-info paired';
openOptionsBtn.hidden = true;
} else {
pairingInfo.textContent = 'Not paired. Add the one-time secret from APT.';
pairingInfo.className = 'pairing-info unpaired';
openOptionsBtn.hidden = false;
}
const tabs = await chromeApi.tabs.query({ active: true, currentWindow: true });
const tab = tabs && tabs[0];
if (!tab || !isSupportedDeploymentUrl(tab.url)) {
tabInfo.textContent = 'This tab is not an Alta deployment.';
tabInfo.className = 'tab-info not-detected';
updateActions();
return;
}
const url = new URL(tab.url);
detectedOrigin = url.origin;
tabInfo.textContent = `Detected: ${url.hostname}`;
tabInfo.className = 'tab-info detected';
updateActions();
}
return { copyToken, init, sendToApt };
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
APT_URL,
PAIRING_STORAGE_KEY,
createPopupController,
isSupportedDeploymentUrl
};
} else {
createPopupController({
chromeApi: chrome,
documentApi: document,
navigatorApi: navigator,
fetchImpl: fetch,
confirmCopy: (message) => window.confirm(message)
}).init().catch(() => {
const status = document.getElementById('statusMsg');
status.textContent = 'Extension initialization failed. Open pairing settings and try again.';
status.className = 'status-msg error';
});
}