import { describe, expect, test } from 'bun:test' import { readFileSync } from 'node:fs' import { join } from 'node:path' import { privateKeyToAccount } from 'viem/accounts' import { RawPrivkeyOperatorSigner } from '../operator/raw-privkey' import { decryptKey } from './keystore' import { OPERATOR_BLOB_SCOPES, OPERATOR_KEYSTORE_VERSION, decodeKeystoreBytes, decodeOperatorBlobBytes, decryptAgentKey, decryptOperatorBlob, sniffKeystoreVersion, } from './operator-keystore-crypto' /** * BACKWARD-COMPAT FREEZE TESTS. * * These read FROZEN on-disk keystore/blob fixtures committed under * test/fixtures/compat/ and decode them with the CURRENT code. The fixtures * were produced once by test/fixtures/compat/__generate.ts using fixed * throwaway keys (see constants below); the bytes are immutable. If a future * PR changes a decoder or the key-derivation in a non-additive way, decoding * these frozen bytes fails -> CI red. That is the WHOLE point: today every * other keystore test generates its input with the current encoder, so a * breaking decoder change ships silently and bricks existing users on upgrade. * * NEW format versions add a NEW fixture file; never edit a frozen one. * * The constants below MUST match test/fixtures/compat/__generate.ts. They are * throwaway test material, NOT a real operator key. */ const FIXTURES = join(import.meta.dir, '../../../../test/fixtures/compat') const TEST_OPERATOR_PRIVKEY = '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d' const TEST_AGENT_PRIVKEY = '0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba' const TEST_V1_PASSPHRASE = 'frozen-compat-v1-passphrase' function fixture(name: string): Uint8Array { return new Uint8Array(readFileSync(join(FIXTURES, name))) } describe('keystore backward-compat (frozen fixtures)', () => { test('FROZEN operator keystore v2 still decrypts with current decryptAgentKey', async () => { const bytes = fixture('operator-keystore-v2.json') expect(sniffKeystoreVersion(bytes)).toBe(2) const keystore = decodeKeystoreBytes(bytes) expect(keystore.version).toBe(OPERATOR_KEYSTORE_VERSION) const signer = new RawPrivkeyOperatorSigner({ privkey: TEST_OPERATOR_PRIVKEY }) const agentAddress = privateKeyToAccount(TEST_AGENT_PRIVKEY).address const decrypted = await decryptAgentKey({ signer, agentAddress, keystore }) expect(decrypted).toBe(TEST_AGENT_PRIVKEY) }) test('FROZEN operator scoped blob v2 (TELEGRAM) still decrypts with current decryptOperatorBlob', async () => { const bytes = fixture('operator-blob-telegram-v2.json') const blob = decodeOperatorBlobBytes(bytes) expect(blob.version).toBe(OPERATOR_KEYSTORE_VERSION) expect(blob.scope).toBe(OPERATOR_BLOB_SCOPES.TELEGRAM) const signer = new RawPrivkeyOperatorSigner({ privkey: TEST_OPERATOR_PRIVKEY }) const agentAddress = privateKeyToAccount(TEST_AGENT_PRIVKEY).address const plaintext = await decryptOperatorBlob({ signer, scope: OPERATOR_BLOB_SCOPES.TELEGRAM, agentAddress, blob, }) const parsed = JSON.parse(new TextDecoder().decode(plaintext)) expect(parsed.telegram.botToken).toBe('123456:FROZEN-TEST-TOKEN') expect(parsed.telegram.allowedUserIds).toEqual([42]) }) test('FROZEN legacy v1 passphrase keystore still decrypts with current decryptKey', () => { const bytes = fixture('keystore-v1.json') // v1 is the passphrase keystore (EncryptedKeystore version:1), a different // on-disk shape than the operator-sig v2 blob; sniff confirms the version. expect(sniffKeystoreVersion(bytes)).toBe(1) const keystore = JSON.parse(new TextDecoder().decode(bytes)) expect(keystore.version).toBe(1) const decrypted = decryptKey(keystore, TEST_V1_PASSPHRASE) const hex = `0x${Buffer.from(decrypted).toString('hex')}` expect(hex).toBe(TEST_AGENT_PRIVKEY) }) test('FROZEN v1 keystore is rejected by the v2 decoder (version gate still fires)', () => { const bytes = fixture('keystore-v1.json') // Guards the explicit v1->v2 version gate in decodeKeystoreBytes: a v1 blob // must never silently decode as v2. expect(() => decodeKeystoreBytes(bytes)).toThrow(/version 1/) }) })