const assert = require('node:assert/strict') const fs = require('node:fs') const os = require('node:os') const path = require('node:path') const test = require('node:test') const crypto = require('node:crypto') const { withXuqmConfig } = require('../metro') const { canonicalJson } = require('../security/canonical-json.js') const trustedConfigKeys = require('../security/trusted-signing-keys.json') const TEST_KEY_ID = 'xuqm-config-test-no-secret' const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519') const publicKeyDer = publicKey.export({ format: 'der', type: 'spki' }) trustedConfigKeys[TEST_KEY_ID] = publicKeyDer.subarray(-32).toString('base64') function createConfigVector() { const config = { appKey: 'test-app', appName: '测试应用', configId: '123e4567-e89b-42d3-a456-426614174000', issuedAt: '2026-07-26T00:00:00Z', revision: 1, schemaVersion: 2, serverUrl: 'https://dev.xuqinmin.com', } const plaintext = Buffer.from(canonicalJson(config), 'utf8') const salt = crypto.randomBytes(16) const iv = crypto.randomBytes(12) const key = crypto.pbkdf2Sync('xuqm-config-file-v2.2026.internal', salt, 120_000, 32, 'sha256') const cipher = crypto.createCipheriv('aes-256-gcm', key, iv) const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]) const unsigned = [ 'XUQM-CONFIG-V2', TEST_KEY_ID, salt.toString('base64url'), iv.toString('base64url'), encrypted.toString('base64url'), ].join('.') return `${unsigned}.${crypto.sign(null, Buffer.from(unsigned, 'ascii'), privateKey).toString('base64url')}` } const CONFIG_VECTOR = createConfigVector() test.after(() => { delete trustedConfigKeys[TEST_KEY_ID] }) test('withXuqmConfig schedules automatic initialization before the app entry', () => { const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'xuqm-metro-')) const configDir = path.join(projectRoot, 'src/assets/config') const existingPrelude = path.join(projectRoot, 'existing-prelude.js') try { fs.mkdirSync(configDir, { recursive: true }) fs.writeFileSync(path.join(configDir, 'app.xuqmconfig'), CONFIG_VECTOR, 'utf8') const config = withXuqmConfig({ projectRoot, resolver: {}, serializer: { getModulesRunBeforeMainModule: () => [existingPrelude], }, }) const preludeModules = config.serializer.getModulesRunBeforeMainModule('index.js') assert.equal(preludeModules[0], existingPrelude) assert.match(preludeModules[1], /packages[\\/]common[\\/]src[\\/]internal\.ts$/) const virtualModule = config.resolver.resolveRequest( { resolveRequest: () => assert.fail('fallback resolver should not run') }, '@xuqm/rn-common/auto-init-config', 'android', ) assert.equal(virtualModule.type, 'sourceFile') const generatedSource = fs.readFileSync(virtualModule.filePath, 'utf8') assert.match(generatedSource, /RESOLVED_CONFIG/) assert.match(generatedSource, /"appKey":"test-app"/) assert.equal(generatedSource.includes('XUQM-CONFIG-V2'), false) } finally { fs.rmSync(projectRoot, { recursive: true, force: true }) } }) test('withXuqmConfig rejects legacy V1 files with a migration instruction', () => { const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'xuqm-metro-')) const configDir = path.join(projectRoot, 'src/assets/config') try { fs.mkdirSync(configDir, { recursive: true }) fs.writeFileSync( path.join(configDir, 'config.xuqmconfig'), 'XUQM-CONFIG-V1.invalid.invalid.invalid', 'utf8', ) assert.throws( () => withXuqmConfig({ projectRoot }), /仅支持 XUQM-CONFIG-V2;请从租户平台重新下载配置文件/, ) } finally { fs.rmSync(projectRoot, { recursive: true, force: true }) } }) test('withXuqmConfig rejects a modified V2 signature before decrypting content', () => { const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'xuqm-metro-')) const configDir = path.join(projectRoot, 'src/assets/config') try { fs.mkdirSync(configDir, { recursive: true }) const parts = CONFIG_VECTOR.split('.') parts[5] = `${parts[5][0] === 'A' ? 'B' : 'A'}${parts[5].slice(1)}` const tampered = parts.join('.') fs.writeFileSync(path.join(configDir, 'config.xuqmconfig'), tampered, 'utf8') assert.throws(() => withXuqmConfig({ projectRoot }), /配置文件签名验证失败/) assert.equal( fs.existsSync(path.join(projectRoot, 'android/app/src/main/assets/config/config.xuqmconfig')), false, ) } finally { fs.rmSync(projectRoot, { recursive: true, force: true }) } }) test('withXuqmConfig leaves common-only hosts unchanged without a config file', () => { const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'xuqm-metro-')) const original = { projectRoot, serializer: { getModulesRunBeforeMainModule: () => ['existing-prelude.js'], }, } try { assert.equal(withXuqmConfig(original), original) } finally { fs.rmSync(projectRoot, { recursive: true, force: true }) } }) test('withXuqmConfig rejects ambiguous config directories instead of choosing a file', () => { const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'xuqm-metro-')) const configDir = path.join(projectRoot, 'src/assets/config') try { fs.mkdirSync(configDir, { recursive: true }) fs.writeFileSync(path.join(configDir, 'app.xuqmconfig'), CONFIG_VECTOR, 'utf8') fs.writeFileSync(path.join(configDir, 'tenant.xuqmconfig'), CONFIG_VECTOR, 'utf8') assert.throws( () => withXuqmConfig({ projectRoot }), /只能存在一个 \.xuqmconfig 文件,当前发现 2 个/, ) } finally { fs.rmSync(projectRoot, { recursive: true, force: true }) } })