import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { newFixture, type Fixture } from '../helpers/fixture.js'; let fixture: Fixture; const servers = ['t5_zombie_theater_1', 't6_zombie_town_1']; const filename = 'electric_cherry_bank.cfg'; const secret = Buffer.from('set tws_bank_token "ec_bank_private_regression_secret"\n'); beforeEach(async () => { vi.resetModules(); fixture = await newFixture(servers); const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js'); await seedServerRegistry(); const { destOfKind } = await import('../../src/lib/server/config.js'); for (const server of servers) { await mkdir(destOfKind(server, 'config'), { recursive: true }); await writeFile(join(destOfKind(server, 'config'), filename), secret); } }); afterEach(async () => { vi.restoreAllMocks(); await fixture.cleanup(); }); async function evidenceIsPrivate() { const { db } = await import('../../src/lib/server/db/index.js'); expect(db().prepare('SELECT 1 FROM file_blobs WHERE content=?').all(secret)).toEqual([]); expect(db().prepare('SELECT 1 FROM deployment_files WHERE rel_path=?').all(filename)).toEqual( [] ); } describe.each(servers)('private bank config on %s', (server) => { it('does not read, preview, stage, or capture the generated file while repairing real drift', async () => { const drift = await import('../../src/lib/server/fleet/drift.js'); const { destOfKind } = await import('../../src/lib/server/config.js'); const { db } = await import('../../src/lib/server/db/index.js'); const { createScript } = await import('../../src/lib/server/fleet/scripts.js'); const { requestRestart } = await import('../../src/lib/server/provisioning/requests.js'); const { applyStagedFilesForOperation } = await import('../../src/lib/server/fleet/staged-files.js'); const backend = await import('../../src/lib/server/backends/files.js'); const reads = vi.spyOn(backend, 'readFile'); expect(await drift.scanServerDrift(server)).toEqual([]); expect((await drift.previewDrift([server])).files).toEqual([]); expect((await drift.prepareDriftCampaign(server)).files).toEqual([]); expect((await drift.runDriftDeployment({ servers: [server] })).deploymentId).toBeNull(); const dest = destOfKind(server, 'config'); await writeFile(join(dest, 'stray.cfg'), 'unwanted'); // The exemption is for the generated path, not every matching basename. await mkdir(join(dest, 'nested')); await writeFile(join(dest, 'nested', filename), 'unwanted nested file'); for (const name of ['modified.gsc', 'missing.gsc']) { const artifact = createScript({ name, releaseVersion: '1.0.0', targetRelPath: name, kind: 'script', scopeType: 'game', scopeValue: server.slice(0, 2), content: Buffer.from('promoted') }); db() .prepare('UPDATE scripts SET current_version_id=? WHERE id=?') .run(artifact.version.id, artifact.script.id); } await writeFile(join(destOfKind(server, 'script'), 'modified.gsc'), 'modified'); const plan = await drift.previewDrift([server]); expect(plan.files.map((file) => file.relPath).sort()).toEqual([ 'missing.gsc', 'modified.gsc', `nested/${filename}`, 'stray.cfg' ]); const result = await drift.runDriftDeployment({ servers: [server], expectedFingerprint: plan.fingerprint }); const operation = requestRestart(server, { kind: 'campaign', id: result.deploymentId! }); applyStagedFilesForOperation(operation.id, server); expect(await drift.scanServerDrift(server)).toEqual([]); expect(await readFile(join(dest, filename))).toEqual(secret); expect(reads.mock.calls.some(([path]) => path === join(dest, filename))).toBe(false); await evidenceIsPrivate(); }); it('filters old prepared bank evidence while retaining legitimate campaign changes', async () => { const { prepareDriftCampaign, createDriftRebuildCampaign } = await import('../../src/lib/server/fleet/drift.js'); const { beginDeployment } = await import('../../src/lib/server/provisioning/history.js'); const { destOfKind } = await import('../../src/lib/server/config.js'); const { db } = await import('../../src/lib/server/db/index.js'); await writeFile(join(destOfKind(server, 'config'), 'stray.cfg'), 'unwanted'); const prepared = await prepareDriftCampaign(server); const privateFile = { ...prepared.files[0], before: secret, file: { ...prepared.files[0].file, relPath: filename } }; prepared.files.push(privateFile); prepared.plan.files.push(privateFile.file); const operation = beginDeployment(server, 'rebuild', 'watchdog'); const id = createDriftRebuildCampaign(server, operation, 'old-incident', prepared); expect( db().prepare('SELECT rel_path FROM deployment_files WHERE deployment_id=?').all(id) ).toEqual([{ rel_path: 'stray.cfg' }]); expect( createDriftRebuildCampaign(server, operation, 'bank-only', { ...prepared, files: [privateFile] }) ).toBeNull(); await evidenceIsPrivate(); }); it('rejects new and persisted private deletions before activation or evidence capture', async () => { const staged = await import('../../src/lib/server/fleet/staged-files.js'); const { db } = await import('../../src/lib/server/db/index.js'); const { requestRestart } = await import('../../src/lib/server/provisioning/requests.js'); const { applyActivationFiles } = await import('../../src/lib/server/provisioning/activation-files.js'); const { captureRecipePlan } = await import('../../src/lib/server/fleet/launch-recipes.js'); const { destOfKind } = await import('../../src/lib/server/config.js'); db() .prepare( "INSERT INTO deployments(id,action,started_at,triggered_by) VALUES('old','drift',datetime('now'),'ui')" ) .run(); db() .prepare( "INSERT INTO deployment_servers(deployment_id,server,files_changed,restart_state) VALUES('old',?,1,'pending')" ) .run(server); const change = { sourceKind: 'campaign' as const, sourceId: 'old', server, kind: 'config' as const, relPath: filename, content: null, inventorySource: 'drift:old' }; expect(() => staged.queueFileChange(change)).toThrow('Private bank configuration'); db() .prepare( "INSERT INTO staged_file_changes(source_kind,source_id,server,kind,rel_path,inventory_source) VALUES('campaign','old',?,'config',?,'drift:old')" ) .run(server, filename); const operation = requestRestart(server, { kind: 'campaign', id: 'old' }); expect(() => staged.applyStagedFilesForOperation(operation.id, server)).toThrow( 'Private bank configuration' ); const recipe = captureRecipePlan( server.startsWith('t6') ? 't6' : 't5', server.startsWith('t6') ? 'town' : 'theater' ); expect(() => applyActivationFiles(recipe, operation.id, true)).toThrow( 'Private bank configuration' ); expect(await readFile(join(destOfKind(server, 'config'), filename))).toEqual(secret); await evidenceIsPrivate(); }); }); it('rejects the reserved upload name and redacts legacy receipts before packing their bytes', async () => { const { createScript } = await import('../../src/lib/server/fleet/scripts.js'); const { packFileJson } = await import('../../src/lib/server/db/file-blobs.js'); expect(() => createScript({ name: 'private', kind: 'config', targetRelPath: filename, scopeType: 'game', scopeValue: 't5', content: secret }) ).toThrow('reserved'); const packed = packFileJson({ before: [{ path: `configs/${filename}`, contentBase64: secret.toString('base64') }] }); expect(packed.hashes.size).toBe(0); expect(JSON.stringify(packed.value)).not.toContain(secret.toString('base64')); await evidenceIsPrivate(); }); it('blocks legacy private campaign diffs and blob downloads', async () => { const { db } = await import('../../src/lib/server/db/index.js'); const { putFileBlob, getFileBlob } = await import('../../src/lib/server/db/file-blobs.js'); db() .prepare( "INSERT INTO deployments(id,action,started_at,triggered_by) VALUES('legacy','drift',datetime('now'),'ui')" ) .run(); const hash = putFileBlob(secret)!; const file = db() .prepare( "INSERT INTO deployment_files(deployment_id,server,verb,rel_path,content_before_hash) VALUES('legacy',?,'removed',?,?)" ) .run(servers[0], filename, hash); for (const route of [ await import('../../src/routes/api/campaigns/[id]/file/[fileId]/+server.js'), await import('../../src/routes/api/deployments/[id]/file/[fileId]/+server.js') ]) { await expect( route.GET({ params: { id: 'legacy', fileId: String(file.lastInsertRowid) } }) ).rejects.toMatchObject({ status: 404 }); } expect(() => getFileBlob(hash)).toThrow('Private file content'); }); it('blocks old host commands and redacts late private read receipts from older agents', async () => { const hosts = await import('../../src/lib/server/hosts/index.js'); const { db } = await import('../../src/lib/server/db/index.js'); const host = hosts.createHost({ name: 'Legacy private command host' }); const credential = hosts.enrollHost(hosts.createEnrollment(host.id).token, {}).credential; const payload = { files: [{ path: `configs/${filename}`, contentBase64: secret.toString('base64') }] }; expect(() => hosts.queueHostOperation(host.id, 'files.stage', payload)).toThrow('Private bank'); const stage = hosts.queueHostOperation(host.id, 'files.stage', { files: [] }); db() .prepare('UPDATE ec_host_operations SET payload=? WHERE id=?') .run(JSON.stringify(payload), stage.id); expect(() => hosts.getHostOperationPayload(host.id, stage.id)).toThrow('Private bank'); const healthyRead = hosts.queueHostOperation(host.id, 'files.read', { path: 'configs/stray.cfg' }); expect(hosts.pendingHostOperationRows(host.id).map((operation) => operation.id)).toContain( healthyRead.id ); expect(hosts.pendingHostOperationRows(host.id).map((operation) => operation.id)).not.toContain( stage.id ); const read = hosts.queueHostOperation(host.id, 'files.read', { path: 'configs/stray.cfg' }); db() .prepare('UPDATE ec_host_operations SET payload=?,target_path=? WHERE id=?') .run(JSON.stringify({ path: `configs/${filename}` }), `configs/${filename}`, read.id); hosts.reportHostOperationStatus(host.id, read.id, { state: 'completed', result: { contentBase64: secret.toString('base64') } }); expect(hosts.getHostOperation(read.id)?.result).toEqual({ redacted: true }); await evidenceIsPrivate(); // Even an old operation reference cannot authorize downloading captured credentials. const { putFileBlob, referenceBlobs } = await import('../../src/lib/server/db/file-blobs.js'); const hash = putFileBlob(secret)!; referenceBlobs('operation', stage.id, [hash]); db() .prepare( "INSERT INTO deployments(id,action,started_at,triggered_by) VALUES('legacy','drift',datetime('now'),'ui')" ) .run(); db() .prepare( "INSERT INTO deployment_files(deployment_id,server,verb,rel_path,content_before_hash) VALUES('legacy',?,'removed',?,?)" ) .run(servers[0], filename, hash); const { GET } = await import('../../src/routes/api/agent/blobs/[sha256]/+server.js'); expect(() => GET({ request: new Request('https://test', { headers: { Authorization: `Bearer ${credential}` } }), params: { sha256: hash } }) ).toThrow(); });