feat: add paired Chrome bridge authentication
This commit is contained in:
+167
-115
@@ -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';
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user