'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const { SessionStore } = require('../src/session-store'); 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'; function readyStore() { const store = new SessionStore(); store.establish(ORIGIN, SENTINEL); return store; } function response(data, overrides = {}) { return { status: 200, headers: {}, data, ...overrides }; } test('uses only stored authority, fixed endpoint paths and hardened transport options', async () => { const calls = []; const transport = async (options) => { 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) { assert.equal(call.method, 'GET'); 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 <= 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({ sessionStore: readyStore(), transport: async () => { calls += 1; return response([]); }, }); await assert.rejects( client.getDevices({ deploymentUrl: 'https://evil.example', cookies: [SENTINEL] }), { code: 'INVALID_ALTA_ARGUMENTS' }, ); await assert.rejects(client.getAuthInfo(SENTINEL), { code: 'INVALID_ALTA_ARGUMENTS' }); assert.equal(calls, 0); }); test('allows only bounded exact-same-origin redirects', async () => { const seen = []; const transport = async (options) => { seen.push(options.url); if (seen.length === 1) { return response('', { status: 307, headers: { location: '/api/v1/devices?cursor=next' } }); } return response([]); }; const client = new AltaClient({ sessionStore: readyStore(), transport }); assert.deepEqual(await client.getDevices(), []); assert.deepEqual(seen, [ `${ORIGIN}/api/v1/devices`, `${ORIGIN}/api/v1/devices?cursor=next`, ]); for (const location of [ 'https://evil.example/steal', 'https://other.avasecurity.com/api/v1/devices', 'http://tenant.avasecurity.com/api/v1/devices', '//evil.example/steal', `https://user:pass@tenant.avasecurity.com/api/v1/devices`, `${ORIGIN}/api/v1/devices#not-sent-to-server`, ]) { let count = 0; const blocked = new AltaClient({ sessionStore: readyStore(), transport: async () => { count += 1; return response('', { status: 302, headers: { location } }); }, }); await assert.rejects(blocked.getDevices(), { code: 'UNSAFE_ALTA_REDIRECT' }); assert.equal(count, 1, location); } }); test('enforces redirect limit and rejects redirects without locations', async () => { const looping = new AltaClient({ sessionStore: readyStore(), maxRedirects: 2, transport: async () => response('', { status: 302, headers: { location: '/api/v1/devices' } }), }); await assert.rejects(looping.getDevices(), { code: 'TOO_MANY_ALTA_REDIRECTS' }); const missing = new AltaClient({ sessionStore: readyStore(), transport: async () => response('', { status: 302, headers: {} }), }); await assert.rejects(missing.getDevices(), { code: 'UNSAFE_ALTA_REDIRECT' }); }); test('enforces a total timeout even when injected transport does not cooperate', async () => { const client = new AltaClient({ sessionStore: readyStore(), timeoutMs: 25, transport: async () => new Promise(() => {}), }); await assert.rejects(client.getDevices(), { code: 'ALTA_TIMEOUT', timeout: true }); }); test('rejects oversized responses using content-length and actual data size', async () => { const byHeader = new AltaClient({ sessionStore: readyStore(), maxResponseBytes: 32, transport: async () => response([], { headers: { 'content-length': '33' } }), }); await assert.rejects(byHeader.getDevices(), { code: 'ALTA_RESPONSE_TOO_LARGE' }); const byBody = new AltaClient({ sessionStore: readyStore(), maxResponseBytes: 32, transport: async () => response(JSON.stringify([{ value: 'x'.repeat(40) }])), }); await assert.rejects(byBody.getDevices(), { code: 'ALTA_RESPONSE_TOO_LARGE' }); for (const status of [302, 500]) { const oversizedFailure = new AltaClient({ sessionStore: readyStore(), maxResponseBytes: 32, transport: async () => response('x'.repeat(33), { status, headers: status === 302 ? { location: '/api/v1/devices' } : {}, }), }); await assert.rejects(oversizedFailure.getDevices(), { code: 'ALTA_RESPONSE_TOO_LARGE' }); } }); test('rejects malformed response envelopes, JSON, status and endpoint data shapes', async () => { const cases = [ [undefined, 'INVALID_ALTA_RESPONSE'], [response('{broken'), 'INVALID_ALTA_RESPONSE_DATA'], [response([], { status: 500 }), 'ALTA_HTTP_ERROR'], [response({ not: 'an array' }), 'INVALID_ALTA_RESPONSE_DATA'], ]; for (const [value, code] of cases) { const client = new AltaClient({ sessionStore: readyStore(), transport: async () => value }); await assert.rejects(client.getDevices(), { code }); } const authClient = new AltaClient({ sessionStore: readyStore(), transport: async () => response([]) }); await assert.rejects(authClient.getAuthInfo(), { code: 'INVALID_ALTA_RESPONSE_DATA' }); }); test('returns a fresh allowlisted error when transport throws an ALTA error with secret-bearing fields', async () => { const source = Object.assign(new Error(`Alta request failed for va=${SENTINEL}`), { code: 'ALTA_TRANSPORT_FAILURE', status: 503, timeout: true, config: { headers: { Cookie: `va=${SENTINEL}` }, data: SENTINEL }, request: { headers: { Cookie: `va=${SENTINEL}` }, body: SENTINEL }, response: { status: 502, headers: { 'set-cookie': `va=${SENTINEL}` }, data: SENTINEL }, body: SENTINEL, }); const client = new AltaClient({ sessionStore: readyStore(), transport: async () => { throw source; }, }); const caught = await client.getDevices().catch((error) => error); assert.notEqual(caught, source); assert.equal(caught.code, 'ALTA_TRANSPORT_FAILURE'); assert.equal(caught.message, 'Alta request failed for va=[REDACTED]'); assert.equal(caught.status, 502); assert.equal(caught.timeout, true); assert.deepEqual(Object.keys(caught).sort(), ['code', 'status', 'timeout']); for (const field of ['config', 'request', 'response', 'headers', 'data', 'body']) { assert.equal(field in caught, false, field); } assert.doesNotMatch(JSON.stringify(caught), new RegExp(SENTINEL)); }); test('safely formats a transport error whose code getter throws a secret', async () => { const source = new Error('Transport failed safely'); Object.defineProperty(source, 'code', { enumerable: true, get() { throw new Error(SENTINEL); }, }); source.config = { headers: { Cookie: `va=${SENTINEL}` } }; const client = new AltaClient({ sessionStore: readyStore(), transport: async () => { throw source; }, }); const caught = await client.getDevices().catch((error) => error); assert.notEqual(caught, source); assert.equal(caught.code, 'ALTA_ERROR'); assert.equal(caught.message, 'Transport failed safely'); assert.equal(caught.status, null); assert.equal(caught.timeout, false); assert.deepEqual(Object.keys(caught).sort(), ['code', 'status', 'timeout']); assert.equal('config' in caught, false); assert.doesNotMatch(JSON.stringify(caught), new RegExp(SENTINEL)); }); test('revalidates the stored origin before each request', async () => { const store = { requireSession: () => Object.freeze({ origin: 'https://evil.example', cookie: SENTINEL }) }; let called = false; const client = new AltaClient({ sessionStore: store, transport: async () => { called = true; } }); await assert.rejects(client.getDevices(), { code: 'INVALID_ALTA_ORIGIN' }); assert.equal(called, false); });