'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const crypto = require('node:crypto'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const { canonical, createPairingService, pairingProof, signEnvelope, createEnvelopeVerifier, redact, createStagingService } = require('../../src/services/etahen-link-agent'); test('pairing requires expiring proof-of-possession and cannot be replayed', () => { let clock = 1000; let seed = 0; const randomBytes = size => Buffer.alloc(size, ++seed); const service = createPairingService({ now: () => clock, randomBytes, ttlMs: 100 }); const challenge = service.begin({ consoleId: 'console_12345678', runtimeEpoch: 'runtime_12345678' }); assert.ok(challenge.code.length >= 22); const peer = { peerId: 'server_12345678', peerPublicKeySha256: 'a'.repeat(64) }; assert.throws(() => service.confirm({ challengeId: challenge.challengeId, ...peer, proof: 'b'.repeat(64) }), /proof/i); const paired = service.confirm({ challengeId: challenge.challengeId, ...peer, proof: pairingProof(challenge, peer) }); assert.equal(paired.consoleId, 'console_12345678'); assert.equal(paired.sessionKey.length, 64); assert.equal(service.peer(peer.peerId).sessionKey, undefined); assert.throws(() => service.confirm({ challengeId: challenge.challengeId, ...peer, proof: pairingProof(challenge, peer) }), /unavailable/i); clock = 2000; const expired = service.begin({ consoleId: 'console_12345678', runtimeEpoch: 'runtime_12345678' }); clock += 101; assert.throws(() => service.confirm({ challengeId: expired.challengeId, ...peer, proof: pairingProof(expired, peer) }), /unavailable/i); }); test('signed envelopes bind console, runtime, expiry and one-time nonce', () => { const key = 'c'.repeat(64); const now = 100000; const base = { jobId: 'job_12345678', nonce: 'nonce_12345678', consoleId: 'console_12345678', runtimeEpoch: 'runtime_12345678', issuedAt: now, expiresAt: now + 1000, job: { type: 'inventory.scan', input: {} } }; const verify = createEnvelopeVerifier({ consoleId: base.consoleId, runtimeEpoch: base.runtimeEpoch, sessionKey: key, now: () => now }); const signed = signEnvelope(base, key); assert.equal(verify(signed).jobId, base.jobId); assert.throws(() => verify(signed), /already used/); const wrong = signEnvelope({ ...base, nonce: 'nonce_22345678', consoleId: 'console_87654321' }, key); assert.throws(() => verify(wrong), /another console/); }); test('staging is resumable, hash-bound, atomic and never installed evidence', t => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-etahen-agent-')); t.after(() => fs.rmSync(root, { recursive: true, force: true })); const bytes = Buffer.from('bounded staged artifact'); const sha256 = crypto.createHash('sha256').update(bytes).digest('hex'); const staging = createStagingService({ root }); const session = staging.create({ artifactId: 'artifact_12345678', size: bytes.length, sha256 }); const first = bytes.subarray(0, 7); staging.append({ sessionId: session.sessionId, offset: 0, bytes: first, chunkSha256: crypto.createHash('sha256').update(first).digest('hex') }); assert.throws(() => staging.append({ sessionId: session.sessionId, offset: 0, bytes: Buffer.from('x'), chunkSha256: crypto.createHash('sha256').update('x').digest('hex') }), /chunk/); const second = bytes.subarray(7); staging.append({ sessionId: session.sessionId, offset: 7, bytes: second, chunkSha256: crypto.createHash('sha256').update(second).digest('hex') }); const result = staging.finalize({ sessionId: session.sessionId }); assert.equal(result.status, 'staged'); assert.equal(result.installed, false); assert.deepEqual(fs.readFileSync(result.finalPath), bytes); }); test('recovery trusts file evidence, not stale persisted offset', t => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-etahen-recover-')); t.after(() => fs.rmSync(root, { recursive: true, force: true })); const staging = createStagingService({ root }); const session = staging.create({ artifactId: 'artifact_87654321', size: 10, sha256: 'a'.repeat(64) }); fs.appendFileSync(session.partialPath, Buffer.from('abc')); assert.deepEqual(staging.recover().find(item => item.sessionId === session.sessionId), { ...session, offset: 3, status: 'interrupted' }); const resumed = staging.resume({ sessionId: session.sessionId }); assert.equal(resumed.status, 'receiving'); assert.equal(resumed.offset, 3); const remainder = Buffer.from('defghij'); staging.append({ sessionId: session.sessionId, offset: 3, bytes: remainder, chunkSha256: crypto.createHash('sha256').update(remainder).digest('hex') }); }); test('recovery closes the crash window after atomic publication', t => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-etahen-publish-recover-')); t.after(() => fs.rmSync(root, { recursive: true, force: true })); const bytes = Buffer.from('published before state persisted'); const sha256 = crypto.createHash('sha256').update(bytes).digest('hex'); const staging = createStagingService({ root, now: () => 12345 }); const session = staging.create({ artifactId: 'artifact_publish1', size: bytes.length, sha256 }); staging.append({ sessionId: session.sessionId, offset: 0, bytes, chunkSha256: sha256 }); fs.renameSync(session.partialPath, session.finalPath); const recovered = staging.recover().find(item => item.sessionId === session.sessionId); assert.equal(recovered.status, 'staged'); assert.equal(recovered.offset, bytes.length); assert.equal(recovered.stagedAt, 12345); }); test('audit redaction removes nested secrets without altering evidence fields', () => { assert.deepEqual(redact({ jobId: 'job_1', auth: { token: 'secret', sessionKey: 'key' }, result: 'staged' }), { jobId: 'job_1', auth: { token: '[REDACTED]', sessionKey: '[REDACTED]' }, result: 'staged' }); }); test('canonical envelopes reject cycles, excessive nesting and non-JSON values', () => { const cyclic = {}; cyclic.self = cyclic; assert.throws(() => canonical(cyclic), /cycle/i); let deep = {}; let cursor = deep; for (let index = 0; index < 34; index += 1) { cursor.child = {}; cursor = cursor.child; } assert.throws(() => canonical(deep), /nesting/i); assert.throws(() => canonical({ value: undefined }), /non-JSON/i); }); test('staging rejects oversized chunks, external state and symlink swaps', t => { const parent = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-etahen-hostile-')); t.after(() => fs.rmSync(parent, { recursive: true, force: true })); const root = path.join(parent, 'staging'); fs.mkdirSync(root); assert.throws(() => createStagingService({ root, stateFile: path.join(parent, 'outside.json') }), /managed root/); const service = createStagingService({ root, maxChunkBytes: 2 }); const session = service.create({ artifactId: 'artifact_hostile1', size: 3, sha256: 'a'.repeat(64) }); assert.throws(() => service.append({ sessionId: session.sessionId, offset: 0, bytes: Buffer.from('abc'), chunkSha256: crypto.createHash('sha256').update('abc').digest('hex') }), /configured bound/); fs.rmSync(session.partialPath); try { fs.symlinkSync(path.join(parent, 'outside.bin'), session.partialPath); assert.throws(() => service.append({ sessionId: session.sessionId, offset: 0, bytes: Buffer.from('a'), chunkSha256: crypto.createHash('sha256').update('a').digest('hex') }), /escaped/); } catch (error) { if (error.code !== 'EPERM') throw error; fs.writeFileSync(session.partialPath, Buffer.alloc(0)); const statePath = path.join(root, 'sessions.json'); const persisted = JSON.parse(fs.readFileSync(statePath, 'utf8')); persisted.sessions[0].partialPath = path.join(parent, 'outside.bin'); fs.writeFileSync(statePath, JSON.stringify(persisted)); const reloaded = createStagingService({ root, maxChunkBytes: 2 }); assert.throws(() => reloaded.append({ sessionId: session.sessionId, offset: 0, bytes: Buffer.from('a'), chunkSha256: crypto.createHash('sha256').update('a').digest('hex') }), /identity/); } });