feat: add bounded large-deployment discovery

This commit is contained in:
2026-08-20 01:05:46 +00:00
parent 4a10e6051e
commit 6a8a4cf95a
8 changed files with 449 additions and 24 deletions
+55 -2
View File
@@ -3,7 +3,12 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const { SessionStore } = require('../src/session-store');
const { AltaClient } = require('../src/alta-client');
const {
AltaClient,
DEVICE_MAX_RESPONSE_BYTES,
HIERARCHY_MAX_RESPONSE_BYTES,
MAX_ARRAY_OBJECTS,
} = require('../src/alta-client');
const ORIGIN = 'https://tenant.avasecurity.com';
const SENTINEL = 'HERMES_SENTINEL_SECRET';
@@ -24,17 +29,20 @@ test('uses only stored authority, fixed endpoint paths and hardened transport op
calls.push(options);
if (options.url.endsWith('/devices')) return response([]);
if (options.url.endsWith('/deviceSites')) return response([{ id: 'site-1' }]);
if (options.url.endsWith('/deviceGroups')) return response([{ id: 'group-1' }]);
return response({ user: 'engineer' });
};
const client = new AltaClient({ sessionStore: readyStore(), transport });
assert.deepEqual(await client.getDevices(), []);
assert.deepEqual(await client.getDeviceSites(), [{ id: 'site-1' }]);
assert.deepEqual(await client.getDeviceGroups(), [{ id: 'group-1' }]);
assert.deepEqual(await client.getAuthInfo(), { user: 'engineer' });
assert.deepEqual(calls.map((call) => call.url), [
`${ORIGIN}/api/v1/devices`,
`${ORIGIN}/api/v1/deviceSites`,
`${ORIGIN}/api/v1/deviceGroups`,
`${ORIGIN}/api/v1/auth`,
]);
for (const call of calls) {
@@ -42,11 +50,56 @@ test('uses only stored authority, fixed endpoint paths and hardened transport op
assert.equal(call.proxy, false);
assert.equal(call.maxRedirects, 0);
assert.match(call.headers.Cookie, /^va=HERMES_SENTINEL_SECRET$/);
assert.ok(call.timeout > 0 && call.timeout <= 10_000);
assert.ok(call.timeout > 0 && call.timeout <= 30_000);
assert.ok(call.maxResponseBytes > 0);
}
});
test('uses endpoint-specific 32 MiB/4 MiB limits and a 30 second request deadline', async () => {
const calls = [];
const client = new AltaClient({
sessionStore: readyStore(),
transport: async (options) => { calls.push(options); return response([]); },
});
await client.getDevices();
await client.getDeviceSites();
await client.getDeviceGroups();
assert.deepEqual(calls.map(({ maxResponseBytes }) => maxResponseBytes), [
DEVICE_MAX_RESPONSE_BYTES,
HIERARCHY_MAX_RESPONSE_BYTES,
HIERARCHY_MAX_RESPONSE_BYTES,
]);
assert.ok(calls.every(({ timeout }) => timeout > 0 && timeout <= 30_000));
});
test('accepts device bodies above 2 MiB and rejects bodies above 32 MiB before parsing', async () => {
const acceptedBody = JSON.stringify([{
guid: '550e8400-e29b-41d4-a716-446655440000',
padding: 'x'.repeat((2 * 1024 * 1024) + 1),
}]);
const accepted = new AltaClient({
sessionStore: readyStore(),
transport: async () => response(Buffer.from(acceptedBody)),
});
assert.equal((await accepted.getDevices()).length, 1);
const rejected = new AltaClient({
sessionStore: readyStore(),
transport: async () => response(Buffer.alloc(DEVICE_MAX_RESPONSE_BYTES + 1, 0x20)),
});
await assert.rejects(rejected.getDevices(), { code: 'ALTA_RESPONSE_TOO_LARGE' });
});
test('rejects plain array responses containing more than 10,000 objects', async () => {
const client = new AltaClient({
sessionStore: readyStore(),
transport: async () => response(Array.from({ length: MAX_ARRAY_OBJECTS + 1 }, () => ({}))),
});
await assert.rejects(client.getDevices(), { code: 'ALTA_RESPONSE_TOO_MANY_OBJECTS' });
});
test('rejects renderer-supplied URL/cookie parameters before transport', async () => {
let calls = 0;
const client = new AltaClient({
+83
View File
@@ -0,0 +1,83 @@
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const {
MAX_PROJECTED_PAYLOAD_BYTES,
projectDeviceHierarchy,
} = require('../src/device-projection');
function uuid(index) {
return `00000000-0000-4000-8000-${index.toString(16).padStart(12, '0')}`;
}
function rawDevice(index, overrides = {}) {
return {
guid: uuid(index),
name: `Camera ${index}`,
type: 'camera',
model: 'Synthetic',
address: `10.0.${Math.floor(index / 255)}.${index % 255}`,
server_group_id: 'site-1',
device_group_id: 'group-1',
capabilities: { localStorage: index % 2 === 0, secretCapability: 'drop-me' },
live: { display_status: 'online', private: 'drop-me' },
cookie: 'must-not-cross-ipc',
...overrides,
};
}
test('projects only allowlisted fields and honestly diagnoses ineligible devices', () => {
const payload = projectDeviceHierarchy({
devices: [rawDevice(1), rawDevice(2, { guid: 'not-a-uuid' })],
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' }],
});
assert.deepEqual(payload.devices, [{
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,
}]);
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.diagnostics.devices, { received: 2, eligible: 1, invalid: 1 });
assert.doesNotMatch(JSON.stringify(payload), /drop-me|cookie|secretCapability|private/);
});
test('projects synthetic 1,500 and 10,000 camera deployments below the 8 MiB IPC cap', () => {
for (const count of [1_500, 10_000]) {
const payload = projectDeviceHierarchy({
devices: Array.from({ length: count }, (_, index) => rawDevice(index)),
sites: [],
groups: [],
});
assert.equal(payload.devices.length, count);
assert.equal(payload.diagnostics.devices.received, count);
assert.ok(Buffer.byteLength(JSON.stringify(payload)) <= MAX_PROJECTED_PAYLOAD_BYTES);
}
});
test('malformed hierarchy metadata does not hide valid cameras', () => {
const payload = projectDeviceHierarchy({
devices: [rawDevice(7)],
sites: [null, { id: {}, name: 'bad' }],
groups: ['bad'],
metadataErrors: { sites: 'Unavailable', groups: 'Unavailable' },
});
assert.equal(payload.devices.length, 1);
assert.deepEqual(payload.sites, []);
assert.deepEqual(payload.groups, []);
assert.equal(payload.diagnostics.sites.invalid, 2);
assert.equal(payload.diagnostics.groups.invalid, 1);
assert.equal(payload.diagnostics.sites.error, 'Unavailable');
});
test('rejects a projected hierarchy payload above 8 MiB', () => {
const devices = Array.from({ length: 10_000 }, (_, index) => rawDevice(index, {
name: `Camera ${index} ${'x'.repeat(900)}`,
}));
assert.throws(
() => projectDeviceHierarchy({ devices, sites: [], groups: [] }),
{ code: 'PROJECTED_PAYLOAD_TOO_LARGE' },
);
});
+67 -2
View File
@@ -78,7 +78,20 @@ test('runtime keeps Alta credentials in main-owned modules and exposes only non-
openExternal: async () => {},
});
assert.deepEqual(await runtime.getDevices(), { success: true, devices: [{ guid: '550e8400-e29b-41d4-a716-446655440000' }] });
assert.deepEqual(await runtime.getDevices(), {
success: true,
devices: [{
id: '550e8400-e29b-41d4-a716-446655440000',
name: null,
type: null,
model: null,
address: null,
siteId: null,
deviceGroupId: null,
displayStatus: null,
localStorage: null,
}],
});
const launched = await runtime.launchProxy(
'550e8400-e29b-41d4-a716-446655440000',
'proxy.operator@example.com'
@@ -102,6 +115,57 @@ test('runtime keeps Alta credentials in main-owned modules and exposes only non-
assert.equal((await runtime.stopProxy(999)).success, false);
});
test('runtime discovers and projects hierarchy while metadata failures preserve cameras', async () => {
const deviceId = '550e8400-e29b-41d4-a716-446655440000';
const runtime = new AppRuntime({
sessionStore: createSessionStore(),
altaClient: {
getDevices: async () => [{
guid: deviceId,
name: 'Camera',
server_group_id: 'site-1',
secret: 'drop-me',
}],
getDeviceSites: async () => { throw Object.assign(new Error('tenant secret'), { code: 'ALTA_HTTP_ERROR' }); },
getDeviceGroups: async () => [{ id: 'group-1', name: 'Lobby', parent_id: null }],
},
proxyManager: { listTrackedProxies: () => [] },
});
const result = await runtime.getDeviceHierarchy();
assert.equal(result.success, true);
assert.deepEqual(result.hierarchy.devices.map(({ id }) => id), [deviceId]);
assert.deepEqual(result.hierarchy.sites, []);
assert.deepEqual(result.hierarchy.groups, [{
id: 'group-1', name: 'Lobby', parentId: null, pendingDeletionStart: null,
}]);
assert.equal(result.hierarchy.diagnostics.sites.error, 'Device sites unavailable');
assert.doesNotMatch(JSON.stringify(result), /drop-me|tenant secret/);
});
test('runtime enforces a bounded whole-discovery deadline', async () => {
const runtime = new AppRuntime({
sessionStore: createSessionStore(),
altaClient: {
getDevices: async () => new Promise(() => {}),
getDeviceSites: async () => [],
getDeviceGroups: async () => [],
},
proxyManager: { listTrackedProxies: () => [] },
discoveryTimeoutMs: 25,
});
const result = await runtime.getDeviceHierarchy();
assert.deepEqual(result, {
success: false,
hierarchy: { devices: [], sites: [], groups: [] },
message: 'Alta device discovery timed out',
});
});
test('runtime rejects discovery deadlines above 60 seconds', () => {
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');
@@ -364,7 +428,8 @@ test('preload and renderer expose only narrow, credential-free contracts', () =>
const renderer = read('renderer.js');
const html = read('index.html');
const expectedMethods = [
'getDevices', 'getDeviceSites', 'getAuthInfo', 'launchProxy', 'stopProxy', 'disconnect',
'getDevices', 'getDeviceSites', 'getDeviceGroups', 'getDeviceHierarchy', 'getAuthInfo',
'launchProxy', 'stopProxy', 'disconnect',
'getConnectionState', 'checkForUpdates', 'openFixedReleasesPage', 'rotatePairing',
'revokePairing', 'getPairingStatus', 'onConnectionStateChanged',
];