fix: keep Alta bearer out of proxy command lines
This commit is contained in:
@@ -1,37 +0,0 @@
|
|||||||
name: Deploy GitHub Pages
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
paths:
|
|
||||||
- 'docs/**'
|
|
||||||
- '.github/workflows/deploy-pages.yml'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pages: write
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: pages
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
environment:
|
|
||||||
name: github-pages
|
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Configure Pages
|
|
||||||
uses: actions/configure-pages@v5
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-pages-artifact@v3
|
|
||||||
with:
|
|
||||||
path: docs
|
|
||||||
- name: Deploy to GitHub Pages
|
|
||||||
id: deployment
|
|
||||||
uses: actions/deploy-pages@v4
|
|
||||||
@@ -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 `spawn` of the fixed helper with `shell: false`; stop actions can target only children owned by this app.
|
- 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.
|
||||||
- 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,7 +30,8 @@ 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 and choose **Start Proxy**.
|
5. Select a local camera, enter your Alta username/email, and choose **Start Proxy**.
|
||||||
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ Feature discovery begins only after Phase A passes independent security review a
|
|||||||
|
|
||||||
## Non-negotiable guardrails
|
## Non-negotiable guardrails
|
||||||
|
|
||||||
- GitPeji is the only source of truth. Do not fetch from, push to, publish on, or update from GitHub.
|
- GitPeji is the only source of truth. Do not fetch from, push to, publish on, or update from alternate source hosts.
|
||||||
- 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.
|
||||||
@@ -73,7 +73,7 @@ git worktree add -b hardening/security-foundation /home/peji/worktrees/apt-secur
|
|||||||
git -C /home/peji/worktrees/apt-security-foundation status --short --branch
|
git -C /home/peji/worktrees/apt-security-foundation status --short --branch
|
||||||
```
|
```
|
||||||
|
|
||||||
**Expected:** clean branch based on exact GitPeji `origin/master`; no GitHub remote used.
|
**Expected:** clean branch based on exact GitPeji `origin/master`; no alternate-host remote used.
|
||||||
|
|
||||||
**Commit:** `docs: record APT security hardening baseline`
|
**Commit:** `docs: record APT security hardening baseline`
|
||||||
|
|
||||||
@@ -387,7 +387,7 @@ npm run check
|
|||||||
**Files:**
|
**Files:**
|
||||||
- Create: `.gitea/workflows/ci.yml`
|
- Create: `.gitea/workflows/ci.yml`
|
||||||
- Create: `.gitea/workflows/release.yml`
|
- Create: `.gitea/workflows/release.yml`
|
||||||
- Remove or retire: `.github/workflows/deploy-pages.yml`
|
- Remove or retire the legacy alternate-host Pages workflow.
|
||||||
- Modify: `docs/index.html`
|
- Modify: `docs/index.html`
|
||||||
- Create: `scripts/verify-kit.js`
|
- Create: `scripts/verify-kit.js`
|
||||||
|
|
||||||
@@ -395,7 +395,7 @@ npm run check
|
|||||||
|
|
||||||
1. CI gate: `npm ci`, syntax, tests, audit policy, Windows unpacked build, kit verification, secret scan.
|
1. CI gate: `npm ci`, syntax, tests, audit policy, Windows unpacked build, kit verification, secret scan.
|
||||||
2. Release gate: tag/version match, clean source, approved helper checksum, exact artifact hashes, SBOM, checksums, retained logs.
|
2. Release gate: tag/version match, clean source, approved helper checksum, exact artifact hashes, SBOM, checksums, retained logs.
|
||||||
3. Point docs/downloads to GitPeji or the approved Tool Hub route, never GitHub.
|
3. Point docs/downloads to GitPeji or the approved Tool Hub route, never an alternate source host.
|
||||||
4. Publish no artifact automatically until the first manual release rehearsal passes.
|
4. Publish no artifact automatically until the first manual release rehearsal passes.
|
||||||
5. Rehearse on a non-production candidate tag and verify fresh download/extraction.
|
5. Rehearse on a non-production candidate tag and verify fresh download/extraction.
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
- Implementation plan: `docs/plans/2026-08-19-apt-security-foundation.md`
|
- Implementation plan: `docs/plans/2026-08-19-apt-security-foundation.md`
|
||||||
- Starting package version: `1.0.0`
|
- Starting package version: `1.0.0`
|
||||||
- Starting audit: 25 findings (1 critical, 22 high, 2 moderate); production-only audit 3 findings (2 high, 1 moderate)
|
- Starting audit: 25 findings (1 critical, 22 high, 2 moderate); production-only audit 3 findings (2 high, 1 moderate)
|
||||||
- Starting release state: GitPeji/GitHub split-brain, no application CI, unsafe unsigned updater, no automated tests
|
- Starting release state: split release hosting, no application CI, unsafe unsigned updater, no automated tests
|
||||||
|
|
||||||
## Guardrails
|
## Guardrails
|
||||||
|
|
||||||
- GitPeji only; GitHub remote removed.
|
- GitPeji only; alternate-host remote removed.
|
||||||
- Production Tool Hub and existing downloads remain unchanged.
|
- Production Tool Hub and existing downloads remain unchanged.
|
||||||
- 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.
|
||||||
|
|||||||
@@ -74,6 +74,11 @@
|
|||||||
<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">
|
||||||
|
<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>
|
||||||
@@ -97,6 +102,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="renderer-controller.js"></script>
|
||||||
<script src="renderer.js"></script>
|
<script src="renderer.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ function registerIpcHandlers() {
|
|||||||
registerIpc('get-devices', () => runtime.getDevices());
|
registerIpc('get-devices', () => runtime.getDevices());
|
||||||
registerIpc('get-device-sites', () => runtime.getDeviceSites());
|
registerIpc('get-device-sites', () => runtime.getDeviceSites());
|
||||||
registerIpc('get-auth-info', () => runtime.getAuthInfo());
|
registerIpc('get-auth-info', () => runtime.getAuthInfo());
|
||||||
registerIpc('launch-proxy', (deviceId) => runtime.launchProxy(deviceId));
|
registerIpc('launch-proxy', (deviceId, username) => runtime.launchProxy(deviceId, username));
|
||||||
registerIpc('stop-proxy', async (key) => {
|
registerIpc('stop-proxy', async (key) => {
|
||||||
const result = await runtime.stopProxy(key);
|
const result = await runtime.stopProxy(key);
|
||||||
sendConnectionState();
|
sendConnectionState();
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"main.js",
|
"main.js",
|
||||||
"preload.js",
|
"preload.js",
|
||||||
"renderer.js",
|
"renderer.js",
|
||||||
|
"renderer-controller.js",
|
||||||
"index.html",
|
"index.html",
|
||||||
"styles.css",
|
"styles.css",
|
||||||
"src/**/*",
|
"src/**/*",
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ contextBridge.exposeInMainWorld('electronAPI', Object.freeze({
|
|||||||
getDevices: () => ipcRenderer.invoke('get-devices'),
|
getDevices: () => ipcRenderer.invoke('get-devices'),
|
||||||
getDeviceSites: () => ipcRenderer.invoke('get-device-sites'),
|
getDeviceSites: () => ipcRenderer.invoke('get-device-sites'),
|
||||||
getAuthInfo: () => ipcRenderer.invoke('get-auth-info'),
|
getAuthInfo: () => ipcRenderer.invoke('get-auth-info'),
|
||||||
launchProxy: (deviceId) => ipcRenderer.invoke('launch-proxy', deviceId),
|
launchProxy: (deviceId, username) => ipcRenderer.invoke('launch-proxy', deviceId, username),
|
||||||
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'),
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
(function exposeRendererController(globalScope) {
|
||||||
|
function createRendererController({
|
||||||
|
disconnect,
|
||||||
|
renderConnectionState,
|
||||||
|
clearDisconnectedState,
|
||||||
|
showConnectionStatus,
|
||||||
|
} = {}) {
|
||||||
|
if (
|
||||||
|
typeof disconnect !== 'function' ||
|
||||||
|
typeof renderConnectionState !== 'function' ||
|
||||||
|
typeof clearDisconnectedState !== 'function' ||
|
||||||
|
typeof showConnectionStatus !== 'function'
|
||||||
|
) {
|
||||||
|
throw new TypeError('Renderer controller dependencies are invalid.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.freeze({
|
||||||
|
async disconnect() {
|
||||||
|
const result = await disconnect();
|
||||||
|
if (!result || result.success !== true) {
|
||||||
|
const message = result && typeof result.message === 'string'
|
||||||
|
? result.message
|
||||||
|
: 'Failed to disconnect from Alta. The current connection remains active.';
|
||||||
|
showConnectionStatus(message, 'error');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderConnectionState(result);
|
||||||
|
clearDisconnectedState();
|
||||||
|
showConnectionStatus('Disconnected from Alta.', 'info');
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = Object.freeze({ createRendererController });
|
||||||
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
||||||
|
if (globalScope) globalScope.AptRendererController = api;
|
||||||
|
}(typeof window !== 'undefined' ? window : undefined));
|
||||||
+22
-10
@@ -7,6 +7,7 @@ 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');
|
||||||
@@ -54,7 +55,8 @@ function renderConnectionState(state) {
|
|||||||
function updateProxyButtons() {
|
function updateProxyButtons() {
|
||||||
const id = selectedDevice && (selectedDevice.guid || selectedDevice.id);
|
const id = selectedDevice && (selectedDevice.guid || selectedDevice.id);
|
||||||
const active = id ? activeDeviceIds().has(id.toLowerCase()) : false;
|
const active = id ? activeDeviceIds().has(id.toLowerCase()) : false;
|
||||||
startProxyBtn.disabled = !connection.connected || !id || active;
|
const hasUsername = altaUsername.value.trim().length > 0;
|
||||||
|
startProxyBtn.disabled = !connection.connected || !id || !hasUsername || active;
|
||||||
stopProxyBtn.disabled = !id || !active;
|
stopProxyBtn.disabled = !id || !active;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,14 +206,21 @@ async function loadDevices() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rendererController = window.AptRendererController.createRendererController({
|
||||||
|
disconnect: () => window.electronAPI.disconnect(),
|
||||||
|
renderConnectionState,
|
||||||
|
clearDisconnectedState: () => {
|
||||||
|
allDevices = [];
|
||||||
|
allSites = {};
|
||||||
|
collapsedSites.clear();
|
||||||
|
altaUsername.value = '';
|
||||||
|
clearDeviceList();
|
||||||
|
},
|
||||||
|
showConnectionStatus: (message, type) => showStatus(connectionStatus, message, type),
|
||||||
|
});
|
||||||
|
|
||||||
async function handleDisconnect() {
|
async function handleDisconnect() {
|
||||||
const state = await window.electronAPI.disconnect();
|
await rendererController.disconnect();
|
||||||
renderConnectionState(state);
|
|
||||||
allDevices = [];
|
|
||||||
allSites = {};
|
|
||||||
collapsedSites.clear();
|
|
||||||
clearDeviceList();
|
|
||||||
showStatus(connectionStatus, 'Disconnected from Alta.', 'info');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleStartProxy() {
|
async function handleStartProxy() {
|
||||||
@@ -219,7 +228,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);
|
const result = await window.electronAPI.launchProxy(id, altaUsername.value);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
connection = await window.electronAPI.getConnectionState();
|
connection = await window.electronAPI.getConnectionState();
|
||||||
renderConnectionState(connection);
|
renderConnectionState(connection);
|
||||||
@@ -284,8 +293,10 @@ async function initialize() {
|
|||||||
renderPairingStatus(await window.electronAPI.getPairingStatus());
|
renderPairingStatus(await window.electronAPI.getPairingStatus());
|
||||||
if (connection.connected) await loadDevices();
|
if (connection.connected) await loadDevices();
|
||||||
window.electronAPI.onConnectionStateChanged(async (state) => {
|
window.electronAPI.onConnectionStateChanged(async (state) => {
|
||||||
|
const wasConnected = connection.connected;
|
||||||
|
const previousOrigin = connection.origin;
|
||||||
renderConnectionState(state);
|
renderConnectionState(state);
|
||||||
if (state.connected) {
|
if (state.connected && (!wasConnected || state.origin !== previousOrigin)) {
|
||||||
allDevices = [];
|
allDevices = [];
|
||||||
allSites = {};
|
allSites = {};
|
||||||
clearDeviceList();
|
clearDeviceList();
|
||||||
@@ -300,6 +311,7 @@ disconnectBtn.addEventListener('click', handleDisconnect);
|
|||||||
startProxyBtn.addEventListener('click', handleStartProxy);
|
startProxyBtn.addEventListener('click', handleStartProxy);
|
||||||
stopProxyBtn.addEventListener('click', handleStopProxy);
|
stopProxyBtn.addEventListener('click', handleStopProxy);
|
||||||
deviceSearch.addEventListener('input', filterDevices);
|
deviceSearch.addEventListener('input', filterDevices);
|
||||||
|
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'; });
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const {
|
|||||||
readJsonBody,
|
readJsonBody,
|
||||||
} = require('./bridge-auth');
|
} = require('./bridge-auth');
|
||||||
const { RELEASES_PAGE_URL } = require('./update-policy');
|
const { RELEASES_PAGE_URL } = require('./update-policy');
|
||||||
const { validateDeviceId } = require('./proxy-launch');
|
const { validateDeviceId, validateUsername } = require('./proxy-launch');
|
||||||
|
|
||||||
const PAIRING_ENVELOPE_FILENAME = 'bridge-pairing.json';
|
const PAIRING_ENVELOPE_FILENAME = 'bridge-pairing.json';
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ function safeErrorMessage(error, fallback) {
|
|||||||
'NO_ALTA_SESSION', 'INVALID_ALTA_ARGUMENTS', 'ALTA_TIMEOUT', 'ALTA_HTTP_ERROR',
|
'NO_ALTA_SESSION', 'INVALID_ALTA_ARGUMENTS', 'ALTA_TIMEOUT', 'ALTA_HTTP_ERROR',
|
||||||
'INVALID_ALTA_RESPONSE', 'INVALID_ALTA_RESPONSE_DATA', 'ALTA_RESPONSE_TOO_LARGE',
|
'INVALID_ALTA_RESPONSE', 'INVALID_ALTA_RESPONSE_DATA', 'ALTA_RESPONSE_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', 'SPAWN_FAILED',
|
'UNSUPPORTED_PLATFORM', 'INVALID_DEVICE_ID', 'INVALID_USERNAME', '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
|
||||||
@@ -253,9 +253,10 @@ class AppRuntime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async launchProxy(deviceId) {
|
async launchProxy(deviceId, username) {
|
||||||
try {
|
try {
|
||||||
const validatedId = validateDeviceId(deviceId);
|
const validatedId = validateDeviceId(deviceId);
|
||||||
|
const validatedUsername = validateUsername(username);
|
||||||
this._reconcileProxies();
|
this._reconcileProxies();
|
||||||
if (!this.allowedDeviceIds.has(validatedId)) {
|
if (!this.allowedDeviceIds.has(validatedId)) {
|
||||||
return { success: false, message: 'Select a device from the current Alta device list.' };
|
return { success: false, message: 'Select a device from the current Alta device list.' };
|
||||||
@@ -266,7 +267,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,
|
||||||
cookie: session.cookie,
|
username: validatedUsername,
|
||||||
deviceId: validatedId,
|
deviceId: validatedId,
|
||||||
});
|
});
|
||||||
this.proxyByDevice.set(validatedId, result.processId);
|
this.proxyByDevice.set(validatedId, result.processId);
|
||||||
|
|||||||
+15
-16
@@ -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_COOKIE_LENGTH = 4096;
|
const MAX_USERNAME_LENGTH = 254;
|
||||||
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])?$/;
|
||||||
@@ -41,20 +41,19 @@ function validateDeviceId(value) {
|
|||||||
return value.toLowerCase();
|
return value.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateCookie(value) {
|
function validateUsername(value) {
|
||||||
if (typeof value !== 'string' || value.length === 0 || value.length > MAX_COOKIE_LENGTH) {
|
if (typeof value !== 'string' || value.length === 0 || value.length > MAX_USERNAME_LENGTH || value.trim().length === 0) {
|
||||||
throw new ProxyLaunchError('Cookie is invalid.', 'INVALID_COOKIE');
|
throw new ProxyLaunchError('Alta username is invalid.', 'INVALID_USERNAME');
|
||||||
}
|
}
|
||||||
if (/[\u0000-\u001f\u007f]/.test(value)) {
|
if (/[\u0000-\u001f\u007f-\u009f]/.test(value)) {
|
||||||
throw new ProxyLaunchError('Cookie must not contain control characters.', 'INVALID_COOKIE');
|
throw new ProxyLaunchError('Alta username must not contain control characters.', 'INVALID_USERNAME');
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function redactMessage(error, secret) {
|
function safeProcessError(error) {
|
||||||
const source = error && typeof error.message === 'string' ? error.message : 'Unknown process error';
|
const source = error && typeof error.message === 'string' ? error.message : 'Unknown process error';
|
||||||
const withoutSecret = secret ? source.split(secret).join('[REDACTED]') : source;
|
return source.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ').slice(0, 512);
|
||||||
return withoutSecret.replace(/[\u0000-\u001f\u007f]/g, ' ').slice(0, 512);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeMetadata(entry, status = entry.status) {
|
function safeMetadata(entry, status = entry.status) {
|
||||||
@@ -98,7 +97,7 @@ function createProxyManager({
|
|||||||
|
|
||||||
const deploymentHost = validateDeploymentHost(request.deploymentHost);
|
const deploymentHost = validateDeploymentHost(request.deploymentHost);
|
||||||
const deviceId = validateDeviceId(request.deviceId);
|
const deviceId = validateDeviceId(request.deviceId);
|
||||||
const cookie = validateCookie(request.cookie);
|
const username = validateUsername(request.username);
|
||||||
|
|
||||||
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');
|
||||||
@@ -108,17 +107,17 @@ function createProxyManager({
|
|||||||
try {
|
try {
|
||||||
child = spawn(
|
child = spawn(
|
||||||
helperPath,
|
helperPath,
|
||||||
['-a', deploymentHost, '-d', deviceId, '-k', cookie],
|
['-a', deploymentHost, '-u', username, '-d', deviceId],
|
||||||
{
|
{
|
||||||
shell: false,
|
shell: false,
|
||||||
detached: false,
|
detached: true,
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
windowsHide: false
|
windowsHide: false
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new ProxyLaunchError(
|
throw new ProxyLaunchError(
|
||||||
`Failed to launch proxy helper: ${redactMessage(error, cookie)}`,
|
`Failed to launch proxy helper: ${safeProcessError(error)}`,
|
||||||
'SPAWN_FAILED'
|
'SPAWN_FAILED'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -202,10 +201,10 @@ function createProxyManager({
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
HELPER_FILENAME,
|
HELPER_FILENAME,
|
||||||
MAX_COOKIE_LENGTH,
|
MAX_USERNAME_LENGTH,
|
||||||
ProxyLaunchError,
|
ProxyLaunchError,
|
||||||
createProxyManager,
|
createProxyManager,
|
||||||
validateCookie,
|
|
||||||
validateDeploymentHost,
|
validateDeploymentHost,
|
||||||
validateDeviceId
|
validateDeviceId,
|
||||||
|
validateUsername
|
||||||
};
|
};
|
||||||
|
|||||||
+19
-20
@@ -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 SYNTHETIC_COOKIE = 'va=HERMES_SENTINEL_SECRET';
|
const VALID_USERNAME = 'proxy.operator+apt@example.com';
|
||||||
|
|
||||||
function loadModule() {
|
function loadModule() {
|
||||||
return require(MODULE_PATH);
|
return require(MODULE_PATH);
|
||||||
@@ -56,7 +56,7 @@ function validRequest(overrides = {}) {
|
|||||||
return {
|
return {
|
||||||
deploymentHost: VALID_HOST,
|
deploymentHost: VALID_HOST,
|
||||||
deviceId: VALID_DEVICE_ID,
|
deviceId: VALID_DEVICE_ID,
|
||||||
cookie: SYNTHETIC_COOKIE,
|
username: VALID_USERNAME,
|
||||||
...overrides
|
...overrides
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -72,10 +72,10 @@ test('launches the approved helper directly with exact argv and shell disabled',
|
|||||||
|
|
||||||
assert.deepEqual(calls, [[
|
assert.deepEqual(calls, [[
|
||||||
APPROVED_HELPER,
|
APPROVED_HELPER,
|
||||||
['-a', VALID_HOST, '-d', VALID_DEVICE_ID, '-k', SYNTHETIC_COOKIE],
|
['-a', VALID_HOST, '-u', VALID_USERNAME, '-d', VALID_DEVICE_ID],
|
||||||
{
|
{
|
||||||
shell: false,
|
shell: false,
|
||||||
detached: false,
|
detached: true,
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
windowsHide: false
|
windowsHide: false
|
||||||
}
|
}
|
||||||
@@ -89,13 +89,13 @@ test('launches the approved helper directly with exact argv and shell disabled',
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('passes cookie metacharacters literally in argv without invoking a shell', () => {
|
test('passes username punctuation literally in argv without invoking a shell', () => {
|
||||||
const cookie = 'va=abc&whoami|calc.exe;<>()^%!"`$';
|
const username = 'proxy+apt&literal|name@example.com';
|
||||||
const { manager, calls } = createHarness();
|
const { manager, calls } = createHarness();
|
||||||
|
|
||||||
manager.launchProxy(validRequest({ cookie }));
|
manager.launchProxy(validRequest({ username }));
|
||||||
|
|
||||||
assert.equal(calls[0][1][5], cookie);
|
assert.equal(calls[0][1][3], username);
|
||||||
assert.equal(calls[0][2].shell, false);
|
assert.equal(calls[0][2].shell, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -104,8 +104,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` },
|
||||||
{ cookie: `${SYNTHETIC_COOKIE}\r\ncalc.exe` },
|
{ username: `${VALID_USERNAME}\r\ncalc.exe` },
|
||||||
{ cookie: `${SYNTHETIC_COOKIE}\0calc.exe` }
|
{ username: `${VALID_USERNAME}\0calc.exe` }
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const attack of attacks) {
|
for (const attack of attacks) {
|
||||||
@@ -178,10 +178,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, and control-character cookies', () => {
|
test('rejects empty, oversized, non-string, and control-character usernames', () => {
|
||||||
for (const cookie of ['', 'x'.repeat(4097), 'va=abc\nxyz', 'va=abc\rxyz', 'va=abc\0xyz']) {
|
for (const username of ['', 'x'.repeat(255), 42, 'user\nname', 'user\rname', 'user\0name', 'user\u007fname']) {
|
||||||
const { manager } = createHarness();
|
const { manager } = createHarness();
|
||||||
assert.throws(() => manager.launchProxy(validRequest({ cookie })), /cookie/i);
|
assert.throws(() => manager.launchProxy(validRequest({ username })), /username/i);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -205,27 +205,25 @@ test('requires an absolute approved application directory and Windows platform',
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('redacts the cookie if spawn throws an error containing it', () => {
|
test('reports a bounded spawn failure without credential redaction machinery', () => {
|
||||||
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 for ${SYNTHETIC_COOKIE}`); }
|
spawn: () => { throw new Error('spawn failed'); }
|
||||||
});
|
});
|
||||||
|
|
||||||
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_SECRET/);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tracks only safe process metadata and never exposes or persists cookies', () => {
|
test('tracks only safe process metadata and never exposes usernames', () => {
|
||||||
const { manager } = createHarness();
|
const { manager } = createHarness();
|
||||||
manager.launchProxy(validRequest());
|
manager.launchProxy(validRequest());
|
||||||
|
|
||||||
@@ -236,7 +234,7 @@ test('tracks only safe process metadata and never exposes or persists cookies',
|
|||||||
startedAt: 1_777_777_777_777,
|
startedAt: 1_777_777_777_777,
|
||||||
status: 'running'
|
status: 'running'
|
||||||
}]);
|
}]);
|
||||||
assert.doesNotMatch(JSON.stringify(tracked), /HERMES_SENTINEL_SECRET/);
|
assert.doesNotMatch(JSON.stringify(tracked), /proxy\.operator/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('stopping one tracked process leaves the other tracked process alive', () => {
|
test('stopping one tracked process leaves the other tracked process alive', () => {
|
||||||
@@ -309,8 +307,9 @@ 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 shell launchers, broad process killers, or persistence APIs', () => {
|
test('source contains no bearer argv, shell launchers, broad process killers, or 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)/);
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { createRendererController } = require('../renderer-controller');
|
||||||
|
|
||||||
|
function harness(result) {
|
||||||
|
const calls = [];
|
||||||
|
const controller = createRendererController({
|
||||||
|
disconnect: async () => result,
|
||||||
|
renderConnectionState: (state) => calls.push(['render', state]),
|
||||||
|
clearDisconnectedState: () => calls.push(['clear']),
|
||||||
|
showConnectionStatus: (message, type) => calls.push(['status', message, type]),
|
||||||
|
});
|
||||||
|
return { controller, calls };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('failed disconnect retains visible connection, device, and proxy state', async () => {
|
||||||
|
const result = {
|
||||||
|
success: false,
|
||||||
|
connected: true,
|
||||||
|
activeProxies: [{ deviceId: '550e8400-e29b-41d4-a716-446655440000', processId: 4101 }],
|
||||||
|
message: 'Could not stop every active proxy. The Alta session remains connected.',
|
||||||
|
};
|
||||||
|
const { controller, calls } = harness(result);
|
||||||
|
|
||||||
|
assert.equal(await controller.disconnect(), result);
|
||||||
|
assert.deepEqual(calls, [['status', result.message, 'error']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('successful disconnect renders disconnected state and clears device state', async () => {
|
||||||
|
const result = { success: true, connected: false, origin: null, activeProxies: [] };
|
||||||
|
const { controller, calls } = harness(result);
|
||||||
|
|
||||||
|
assert.equal(await controller.disconnect(), result);
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
['render', result],
|
||||||
|
['clear'],
|
||||||
|
['status', 'Disconnected from Alta.', 'info'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
@@ -79,11 +79,14 @@ test('runtime keeps Alta credentials in main-owned modules and exposes only non-
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(await runtime.getDevices(), { success: true, devices: [{ guid: '550e8400-e29b-41d4-a716-446655440000' }] });
|
assert.deepEqual(await runtime.getDevices(), { success: true, devices: [{ guid: '550e8400-e29b-41d4-a716-446655440000' }] });
|
||||||
const launched = await runtime.launchProxy('550e8400-e29b-41d4-a716-446655440000');
|
const launched = await runtime.launchProxy(
|
||||||
|
'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',
|
||||||
cookie: 'top-secret-cookie',
|
username: 'proxy.operator@example.com',
|
||||||
deviceId: '550e8400-e29b-41d4-a716-446655440000',
|
deviceId: '550e8400-e29b-41d4-a716-446655440000',
|
||||||
});
|
});
|
||||||
assert.deepEqual(runtime.getConnectionState(), {
|
assert.deepEqual(runtime.getConnectionState(), {
|
||||||
@@ -99,6 +102,29 @@ test('runtime keeps Alta credentials in main-owned modules and exposes only non-
|
|||||||
assert.equal((await runtime.stopProxy(999)).success, false);
|
assert.equal((await runtime.stopProxy(999)).success, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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}`);
|
||||||
const unprotect = (value) => Buffer.from(value).toString().slice('protected:'.length);
|
const unprotect = (value) => Buffer.from(value).toString().slice('protected:'.length);
|
||||||
@@ -223,10 +249,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)).processId, 4101);
|
assert.equal((await runtime.launchProxy(deviceId, 'operator@example.com')).processId, 4101);
|
||||||
tracked.length = 0;
|
tracked.length = 0;
|
||||||
assert.deepEqual(runtime.getConnectionState().activeProxies, []);
|
assert.deepEqual(runtime.getConnectionState().activeProxies, []);
|
||||||
assert.equal((await runtime.launchProxy(deviceId)).processId, 4102);
|
assert.equal((await runtime.launchProxy(deviceId, 'operator@example.com')).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 () => {
|
||||||
@@ -304,8 +330,12 @@ 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.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.doesNotMatch(html, /id="cookieKey"|updateProgress|Install Update/);
|
assert.doesNotMatch(html, /id="cookieKey"|updateProgress|Install Update/);
|
||||||
|
assert.match(html, /id="altaUsername"/);
|
||||||
assert.match(renderer, /openFixedReleasesPage/);
|
assert.match(renderer, /openFixedReleasesPage/);
|
||||||
assert.match(html, /Bridge Pairing/);
|
assert.match(html, /Bridge Pairing/);
|
||||||
});
|
});
|
||||||
@@ -327,3 +357,15 @@ test('source policy removes legacy credential IPC, shell launch, broad kill, and
|
|||||||
assert.match(read('main.js'), /18247/);
|
assert.match(read('main.js'), /18247/);
|
||||||
assert.match(read('main.js'), /shell\.openExternal/);
|
assert.match(read('main.js'), /shell\.openExternal/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('production source and documentation are GitPeji-only with no active GitHub workflow', () => {
|
||||||
|
assert.equal(fs.existsSync(path.join(ROOT, '.github', 'workflows', 'deploy-pages.yml')), false);
|
||||||
|
const sourceAndDocs = [
|
||||||
|
'main.js', 'preload.js', 'renderer.js', 'renderer-controller.js', 'index.html',
|
||||||
|
'src/electron-runtime.js', 'src/proxy-launch.js', 'README.md', 'CLAUDE.md',
|
||||||
|
'docs/security/2026-08-security-baseline.md',
|
||||||
|
'docs/plans/2026-08-19-apt-security-foundation.md',
|
||||||
|
].map(read).join('\n');
|
||||||
|
assert.doesNotMatch(sourceAndDocs, /github/i);
|
||||||
|
assert.match(sourceAndDocs, /GitPeji/);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user