Introduce an outbound Python host agent, Debian installer, durable remote operations, immutable game-file libraries, host-aware deployments and ports, and occupancy-safe campaigns and draining. Native hosts do not require Komodo. Add administrator, operator and viewer accounts, protected management routes, secure sessions, password recovery, audit history and separate telemetry tokens. Document first-admin setup, reporter migration and live-host acceptance checks. Validation: 1,343 TypeScript tests and 39 agent tests passed, along with type checking, the production build, compiled SSR/API smoke tests and administrator recovery. Real Debian/Docker T5/T6 acceptance remains unrun.
113 lines
6.3 KiB
TypeScript
113 lines
6.3 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { authenticateTelemetry } from '../../src/lib/server/auth/telemetry.js';
|
|
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
|
import { gameEvent } from '../helpers/telemetry.js';
|
|
|
|
const token = 'ec_telemetry_0123456789abcdef0123456789abcdef';
|
|
const otherToken = 'ec_telemetry_fedcba9876543210fedcba9876543210';
|
|
const request = (values: Record<string, string | undefined> = {}, url = 'http://localhost/api/telemetry') => {
|
|
const headers = new Headers({ 'Content-Type': 'application/json' });
|
|
for (const [key, value] of Object.entries(values)) if (value !== undefined) headers.set(key, value);
|
|
return new Request(url, { method: 'POST', headers, body: JSON.stringify(gameEvent('heartbeat')) });
|
|
};
|
|
|
|
beforeEach(() => { vi.stubEnv('EC_TELEMETRY_TOKEN', token); });
|
|
afterEach(() => { vi.unstubAllEnvs(); vi.restoreAllMocks(); });
|
|
|
|
describe('machine telemetry credentials', () => {
|
|
it.each([
|
|
{ Authorization: `Bearer ${token}` },
|
|
{ authorization: `bearer ${token}` },
|
|
{ 'X-EC-Telemetry-Token': token },
|
|
{ Authorization: `Bearer ${token}`, 'X-EC-Telemetry-Token': token }
|
|
])('accepts an explicit telemetry secret without a browser session: %j', headers => {
|
|
expect(authenticateTelemetry(request(headers))).toEqual({ ok: true });
|
|
});
|
|
|
|
it.each([undefined, '', 'too-short', 'x'.repeat(513), 'x'.repeat(40) + ' not-header-safe'])('fails closed when configuration is unusable: %s', configured => {
|
|
vi.stubEnv('EC_TELEMETRY_TOKEN', configured);
|
|
expect(authenticateTelemetry(request({ Authorization: `Bearer ${token}` }))).toMatchObject({ ok: false, status: 503 });
|
|
});
|
|
|
|
it.each([
|
|
{},
|
|
{ Cookie: `ec_session=${token}` },
|
|
{ Authorization: `Bearer ${otherToken}` },
|
|
{ 'X-EC-Telemetry-Token': otherToken },
|
|
{ Authorization: `Basic ${token}`, 'X-EC-Telemetry-Token': token },
|
|
{ Authorization: `Bearer ${token}` },
|
|
{ Authorization: `Bearer ${token}, Bearer ${token}` },
|
|
{ Authorization: `Bearer ${token}`, 'X-EC-Telemetry-Token': otherToken },
|
|
{ Authorization: `Bearer ${otherToken}`, 'X-EC-Telemetry-Token': token },
|
|
{ Authorization: 'Bearer short', 'X-EC-Telemetry-Token': token },
|
|
{ Authorization: `Bearer ec_host_${'1'.repeat(43)}` },
|
|
{ Authorization: `Bearer ec_enroll_${'1'.repeat(43)}` }
|
|
])('rejects missing, wrong, ambiguous or unrelated credentials: %j', headers => {
|
|
const authentication = authenticateTelemetry(request(headers));
|
|
expect(authentication).toMatchObject({ ok: false, status: 401 });
|
|
expect(JSON.stringify(authentication)).not.toContain(token);
|
|
expect(JSON.stringify(authentication)).not.toContain(otherToken);
|
|
});
|
|
|
|
it('does not accept tokens placed in query parameters', () => {
|
|
expect(authenticateTelemetry(request({}, `http://localhost/api/telemetry?token=${token}`))).toMatchObject({ ok: false, status: 401 });
|
|
});
|
|
|
|
it('retains existing transport behavior for authenticated local machine relays', () => {
|
|
expect(authenticateTelemetry(request({ 'X-EC-Telemetry-Token': token }, 'http://controller.internal/api/telemetry'))).toEqual({ ok: true });
|
|
});
|
|
|
|
it('uses the current configured token after rotation and safely rejects differing token lengths', () => {
|
|
expect(authenticateTelemetry(request({ Authorization: `Bearer ${token}` }))).toEqual({ ok: true });
|
|
vi.stubEnv('EC_TELEMETRY_TOKEN', otherToken);
|
|
expect(authenticateTelemetry(request({ Authorization: `Bearer ${token}` }))).toMatchObject({ ok: false, status: 401 });
|
|
expect(authenticateTelemetry(request({ Authorization: `Bearer ${otherToken}` }))).toEqual({ ok: true });
|
|
expect(authenticateTelemetry(request({ Authorization: `Bearer ${'x'.repeat(32)}` }))).toMatchObject({ ok: false, status: 401 });
|
|
});
|
|
});
|
|
|
|
describe('telemetry endpoint authentication boundary', () => {
|
|
let fixture: Fixture;
|
|
beforeEach(async () => {
|
|
vi.resetModules();
|
|
fixture = await newFixture();
|
|
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
|
|
await seedServerRegistry();
|
|
});
|
|
afterEach(async () => { await fixture.cleanup(); });
|
|
|
|
it.each([401, 503])('returns %s before parsing or storing untrusted evidence', async status => {
|
|
if (status === 503) vi.stubEnv('EC_TELEMETRY_TOKEN', undefined);
|
|
const { POST } = await import('../../src/routes/api/telemetry/+server.js');
|
|
const { db } = await import('../../src/lib/server/db/index.js');
|
|
const { listTelemetry } = await import('../../src/lib/server/fleet/telemetry.js');
|
|
const { subscribe } = await import('../../src/lib/server/pipeline/bus.js');
|
|
const incoming = request({ Cookie: 'ec_session=browser-user' });
|
|
const parse = vi.spyOn(incoming, 'json');
|
|
const listener = vi.fn();
|
|
const unsubscribe = subscribe(listener);
|
|
const response = await POST({ request: incoming });
|
|
unsubscribe();
|
|
expect(response.status).toBe(status);
|
|
expect(response.headers.get('cache-control')).toBe('no-store');
|
|
expect(parse).not.toHaveBeenCalled();
|
|
expect(incoming.bodyUsed).toBe(false);
|
|
expect(listTelemetry()).toEqual([]);
|
|
expect(db().prepare('SELECT COUNT(*) AS count FROM telemetry_requests').get()).toEqual({ count: 0 });
|
|
expect(listener).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('accepts authenticated automation with the unchanged event payload and excludes headers from diagnostics', async () => {
|
|
const { POST } = await import('../../src/routes/api/telemetry/+server.js');
|
|
const { listTelemetryRequests } = await import('../../src/lib/server/fleet/telemetry-requests.js');
|
|
const payload = gameEvent('heartbeat');
|
|
const response = await POST({ request: request({ 'X-EC-Telemetry-Token': token }) });
|
|
expect(response.status).toBe(200);
|
|
expect(await response.json()).toMatchObject({ ok: true, telemetry: payload });
|
|
const history = listTelemetryRequests(payload.server);
|
|
expect(history).toHaveLength(1);
|
|
expect(JSON.stringify(history)).not.toContain(token);
|
|
expect(JSON.parse(history[0].payload!)).toEqual(payload);
|
|
});
|
|
});
|