'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 { Store } = require('../lib/store'); const { Orchestrator, rebuildNode } = require('../lib/orchestrator'); const { generateRealityKeyPair, ensureRealityKey } = require('../lib/protocols'); const B64URL_43 = /^[A-Za-z0-9_-]{43}$/; test('generateRealityKeyPair produces consistent 43-char base64url x25519 keys', () => { const pair = generateRealityKeyPair(); assert.match(pair.privateKey, B64URL_43); assert.match(pair.publicKey, B64URL_43); // Deriving the public key from the private one must reproduce the pair: // these are real x25519 keys in the exact encoding sing-box expects. const priv = crypto.createPrivateKey({ key: { kty: 'OKP', crv: 'X25519', d: pair.privateKey, x: pair.publicKey }, format: 'jwk' }); const derived = crypto.createPublicKey(priv).export({ format: 'jwk' }); assert.equal(derived.x, pair.publicKey); assert.notEqual(generateRealityKeyPair().privateKey, pair.privateKey); }); test('ensureRealityKey generates once, reuses verbatim, ignores non-Reality nodes', () => { const data = { keyStore: {} }; const node = { id: 'nod_x', protocol: 'vless-reality', topology: 'direct', realityServerName: 'www.tesla.com' }; const first = ensureRealityKey(data, node); assert.equal(node.reality.keyId, 'reality-nod_x'); assert.match(first.privateKey, B64URL_43); assert.match(first.publicKey, B64URL_43); assert.strictEqual(ensureRealityKey(data, node), first); assert.equal(ensureRealityKey(data, { id: 'nod_y', protocol: 'shadowsocks-2022-aes128', topology: 'direct' }), null); assert.deepEqual(Object.keys(data.keyStore), ['reality-nod_x']); }); async function fixture(t) { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'nexusgate-keys-')); t.after(() => fs.rmSync(dir, { recursive: true, force: true })); const store = await new Store(path.join(dir, 'data.json')).init(); assert.deepEqual(store.data.keyStore, {}); const stamp = new Date().toISOString(); await store.transaction((data) => { data.servers.push({ id: 'srv_key', name: '入口', publicAddress: '203.0.113.10', portRangeStart: 20000, portRangeEnd: 50000, status: 'online', lastSeenAt: stamp }); data.customers.push({ id: 'cus_key', name: '客户', status: 'active' }); data.nodes.push({ id: 'nod_key', serverId: 'srv_key', name: 'Reality 节点', protocol: 'vless-reality-vision', topology: 'direct', networkMode: 'ipv4', port: 23456, resourceId: 'res_nod_key_1', status: 'draft', realityServerName: 'www.tesla.com', createdAt: stamp, updatedAt: stamp, }); data.nodeClients.push({ id: 'ncl_key', nodeId: 'nod_key', customerId: 'cus_key', status: 'active', credentials: { clientId: crypto.randomUUID(), relayPassword: 'secret', shortId: 'abcd1234' }, createdAt: stamp, updatedAt: stamp, }); }); return { store, orchestrator: new Orchestrator(store) }; } function applyJobs(store) { return store.data.jobs.filter((job) => job.action === 'apply_resource' && job.payload && job.payload.kind === 'node'); } test('deploy payload carries the panel-hosted keyMaterial and reuses it verbatim', async (t) => { const { store } = await fixture(t); await store.transaction(async (data) => { await rebuildNode(data, data.nodes[0]); }); const first = applyJobs(store).at(-1); assert.ok(first, 'an apply job was queued'); // The resource itself keeps the placeholder; the key travels as keyMaterial. assert.match(JSON.stringify(first.payload.resource), /\$\{REALITY_PRIVATE:reality-nod_key\}/); const material = first.payload.keyMaterial; assert.deepEqual(Object.keys(material), ['reality-nod_key']); assert.match(material['reality-nod_key'].privateKey, B64URL_43); assert.match(material['reality-nod_key'].publicKey, B64URL_43); assert.equal(store.data.keyStore['reality-nod_key'].privateKey, material['reality-nod_key'].privateKey); assert.equal(store.data.keyStore['reality-nod_key'].publicKey, material['reality-nod_key'].publicKey); // A redeploy ships the identical keypair: the publicKey is stable. await store.transaction(async (data) => { await rebuildNode(data, data.nodes[0]); }); const second = applyJobs(store).at(-1); assert.deepEqual(second.payload.keyMaterial, material); }); test('agent backfill fills a keyStore gap and is stripped from node artifacts', async (t) => { const { store, orchestrator } = await fixture(t); const agentPair = generateRealityKeyPair(); await store.transaction((data) => { const node = data.nodes[0]; node.status = 'deploying'; node.reality = { keyId: 'reality-nod_key', serverName: 'www.tesla.com', destPort: 443 }; data.jobs.push({ id: 'job_bf', serverId: 'srv_key', nodeId: 'nod_key', action: 'apply_resource', payload: { kind: 'node' }, status: 'running', attempts: 1, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }); }); await orchestrator.completeJob('job_bf', true, { artifacts: { realityPublicKey: agentPair.publicKey, keyMaterial: { 'reality-nod_key': agentPair } }, }); const stored = store.data.keyStore['reality-nod_key']; assert.equal(stored.privateKey, agentPair.privateKey); assert.equal(stored.publicKey, agentPair.publicKey); const node = store.data.nodes[0]; assert.equal(node.artifacts.realityPublicKey, agentPair.publicKey); assert.equal(node.artifacts.keyMaterial, undefined); }); test('agent backfill never overwrites a panel-hosted key', async (t) => { const { store, orchestrator } = await fixture(t); await store.transaction((data) => { ensureRealityKey(data, data.nodes[0]); data.nodes[0].status = 'deploying'; data.jobs.push({ id: 'job_cf', serverId: 'srv_key', nodeId: 'nod_key', action: 'apply_resource', payload: { kind: 'node' }, status: 'running', attempts: 1, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }); }); const panelPair = store.data.keyStore['reality-nod_key']; const agentPair = generateRealityKeyPair(); assert.notEqual(agentPair.publicKey, panelPair.publicKey); await orchestrator.completeJob('job_cf', true, { artifacts: { realityPublicKey: agentPair.publicKey, keyMaterial: { 'reality-nod_key': agentPair } }, }); const stored = store.data.keyStore['reality-nod_key']; assert.equal(stored.privateKey, panelPair.privateKey); assert.equal(stored.publicKey, panelPair.publicKey); // Subscription-facing publicKey is the panel's, not the agent's divergent one. assert.equal(store.data.nodes[0].artifacts.realityPublicKey, panelPair.publicKey); assert.ok(store.data.activity.some((event) => event.action === 'reality_key_conflict')); }); test('node deletion drops the hosted key (draft delete and confirmed delete)', async (t) => { const { store, orchestrator } = await fixture(t); const stamp = new Date().toISOString(); await store.transaction((data) => { ensureRealityKey(data, data.nodes[0]); // Second node mid-deletion: record removed when the agent confirms. data.nodes.push({ id: 'nod_key2', serverId: 'srv_key', name: '待删节点', protocol: 'vless-reality', topology: 'direct', networkMode: 'ipv4', port: 23457, resourceId: 'res_nod_key_2', status: 'removing', pendingDelete: true, entryReady: true, reality: { keyId: 'reality-nod_key2', serverName: 'www.tesla.com', destPort: 443 }, createdAt: stamp, updatedAt: stamp, }); data.keyStore['reality-nod_key2'] = { ...generateRealityKeyPair(), updatedAt: stamp }; data.jobs.push({ id: 'job_del', serverId: 'srv_key', nodeId: 'nod_key2', action: 'delete_resource', payload: { resourceId: 'res_nod_key_2', kind: 'node' }, status: 'running', attempts: 1, createdAt: stamp, updatedAt: stamp }); }); assert.ok(store.data.keyStore['reality-nod_key']); assert.ok(store.data.keyStore['reality-nod_key2']); await orchestrator.removeNode('nod_key', 'admin', true); assert.equal(store.data.nodes.some((node) => node.id === 'nod_key'), false); assert.equal(store.data.keyStore['reality-nod_key'], undefined); await orchestrator.completeJob('job_del', true, {}); assert.equal(store.data.nodes.some((node) => node.id === 'nod_key2'), false); assert.equal(store.data.keyStore['reality-nod_key2'], undefined); }); test('legacy database without keyStore loads with an empty store', async (t) => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'nexusgate-legacy-')); t.after(() => fs.rmSync(dir, { recursive: true, force: true })); const file = path.join(dir, 'data.json'); await new Store(file).init(); const raw = JSON.parse(fs.readFileSync(file, 'utf8')); delete raw.keyStore; fs.writeFileSync(file, JSON.stringify(raw)); const reopened = await new Store(file).init(); assert.deepEqual(reopened.data.keyStore, {}); });