2026-07-17 13:50:30 +08:00
'use strict'
2026-06-18 15:01:39 +08:00
/ * *
* Metro 插件 : 自动发现 . xuqmconfig 配置文件 。
*
* 宿主只需将平台下载的 . xuqmconfig 文件放入 src / assets / config / ,
* SDK 自动读取并完成初始化 , 无需重命名或创建 . ts 包装文件 。
*
2026-07-26 23:47:30 +08:00
* 目录内没有配置时保持 common - only ; 唯一一个 * . xuqmconfig 自动使用 ; 多个文件
* 无法判断权威配置 , 必须在构建期明确失败 。
2026-06-18 15:01:39 +08:00
*
* @ param { import ( '@react-native/metro-config' ) . MetroConfig } metroConfig
* @ returns { import ( '@react-native/metro-config' ) . MetroConfig }
* /
2026-07-17 13:50:30 +08:00
const fs = require ( 'fs' )
const path = require ( 'path' )
2026-07-20 19:31:43 +08:00
const crypto = require ( 'node:crypto' )
2026-06-18 15:01:39 +08:00
2026-07-26 23:47:30 +08:00
const VIRTUAL _MODULE _ID = '@xuqm/rn-common/auto-init-config'
2026-07-17 13:50:30 +08:00
const AUTO _INIT _MODULE = path . resolve ( _ _dirname , '../src/internal.ts' )
2026-07-26 23:47:30 +08:00
const CONFIG _DIRECTORY _RELATIVE _PATH = 'src/assets/config'
const CONFIG _MAGIC = 'XUQM-CONFIG-V2'
const CONFIG _PASSPHRASE = 'xuqm-config-file-v2.2026.internal'
2026-07-20 19:31:43 +08:00
const PBKDF2 _ITERATIONS = 120_000
2026-07-27 00:39:26 +08:00
const TRUSTED _CONFIG _KEYS = require ( '../security/trusted-signing-keys.json' )
const { canonicalJson } = require ( '../security/canonical-json.js' )
const ED25519 _SPKI _PREFIX = Buffer . from ( '302a300506032b6570032100' , 'hex' )
2026-07-20 19:31:43 +08:00
function decodeBase64Url ( value ) {
return Buffer . from ( value , 'base64url' )
}
/ * *
* 配置文件只在 Node / Metro 构建期解密 。 移动端运行时直接消费解析后的配置 ,
* 避免 Hermes 在应用启动阶段执行 12 万次 PBKDF2 并阻塞 JS 线程 。
* /
2026-07-26 23:47:30 +08:00
function assertConfigPayload ( config , plaintext ) {
const allowedFields = new Set ( [
'schemaVersion' ,
'configId' ,
'revision' ,
'issuedAt' ,
'expiresAt' ,
'appKey' ,
'appName' ,
'packageName' ,
'iosBundleId' ,
'harmonyBundleName' ,
'serverUrl' ,
'signingKey' ,
] )
const unknownField = Object . keys ( config ) . find ( field => ! allowedFields . has ( field ) )
if ( unknownField ) throw new Error ( ` 配置包含不受支持字段: ${ unknownField } ` )
if ( config . schemaVersion !== 2 ) throw new Error ( '配置 schemaVersion 必须为 2' )
if (
typeof config . configId !== 'string' ||
! /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i . test (
config . configId ,
)
) {
throw new Error ( '配置 configId 不是有效 UUID' )
}
if ( ! Number . isInteger ( config . revision ) || config . revision <= 0 ) {
throw new Error ( '配置 revision 必须为正整数' )
}
for ( const field of [ 'issuedAt' , 'appKey' , 'appName' , 'serverUrl' , 'signingKey' ] ) {
if ( typeof config [ field ] !== 'string' || ! config [ field ] . trim ( ) ) {
throw new Error ( ` 配置 ${ field } 不能为空 ` )
}
}
if ( ! config . issuedAt . endsWith ( 'Z' ) || Number . isNaN ( Date . parse ( config . issuedAt ) ) ) {
throw new Error ( '配置 issuedAt 必须为 RFC3339 UTC 时间' )
}
if ( config . expiresAt !== undefined ) {
const expiresAt = Date . parse ( config . expiresAt )
if ( ! config . expiresAt . endsWith ( 'Z' ) || Number . isNaN ( expiresAt ) ) {
throw new Error ( '配置 expiresAt 必须为 RFC3339 UTC 时间' )
}
if ( expiresAt <= Date . now ( ) ) throw new Error ( '配置文件已过期,请从租户平台重新生成' )
}
if ( canonicalJson ( config ) !== plaintext ) {
throw new Error ( '配置正文不是规范 canonical JSON' )
}
}
function decryptConfigAtBuildTime ( content , trustedKeys = TRUSTED _CONFIG _KEYS ) {
2026-07-20 19:31:43 +08:00
const parts = content . trim ( ) . split ( '.' )
2026-07-26 23:47:30 +08:00
if ( parts . length !== 6 || parts [ 0 ] !== CONFIG _MAGIC ) {
throw new Error ( '仅支持 XUQM-CONFIG-V2;请从租户平台重新下载配置文件' )
2026-07-20 19:31:43 +08:00
}
2026-07-26 23:47:30 +08:00
const keyId = parts [ 1 ]
const publicKeyBase64 = trustedKeys [ keyId ]
if ( ! publicKeyBase64 ) throw new Error ( ` 配置签名 keyId 不受信任: ${ keyId } ` )
const unsignedToken = parts . slice ( 0 , 5 ) . join ( '.' )
const signatureValid = crypto . verify (
null ,
Buffer . from ( unsignedToken , 'ascii' ) ,
{
2026-07-27 00:39:26 +08:00
key : Buffer . concat ( [ ED25519 _SPKI _PREFIX , Buffer . from ( publicKeyBase64 , 'base64' ) ] ) ,
2026-07-26 23:47:30 +08:00
format : 'der' ,
type : 'spki' ,
} ,
decodeBase64Url ( parts [ 5 ] ) ,
)
if ( ! signatureValid ) throw new Error ( '配置文件签名验证失败' )
const salt = decodeBase64Url ( parts [ 2 ] )
const iv = decodeBase64Url ( parts [ 3 ] )
const encrypted = decodeBase64Url ( parts [ 4 ] )
2026-07-20 19:31:43 +08:00
if ( salt . length === 0 || iv . length !== 12 || encrypted . length <= 16 ) {
throw new Error ( '配置文件加密参数无效' )
}
const body = encrypted . subarray ( 0 , - 16 )
const authTag = encrypted . subarray ( - 16 )
const key = crypto . pbkdf2Sync ( CONFIG _PASSPHRASE , salt , PBKDF2 _ITERATIONS , 32 , 'sha256' )
const decipher = crypto . createDecipheriv ( 'aes-256-gcm' , key , iv )
decipher . setAuthTag ( authTag )
try {
const plaintext = Buffer . concat ( [ decipher . update ( body ) , decipher . final ( ) ] )
2026-07-26 23:47:30 +08:00
const text = plaintext . toString ( 'utf8' )
const config = JSON . parse ( text )
assertConfigPayload ( config , text )
return config
2026-07-20 19:31:43 +08:00
} catch {
throw new Error ( '配置文件解密或内容校验失败' )
}
}
2026-06-18 15:01:39 +08:00
function findConfigFile ( projectRoot ) {
2026-07-26 23:47:30 +08:00
const configDirectory = path . resolve ( projectRoot , CONFIG _DIRECTORY _RELATIVE _PATH )
if ( ! fs . existsSync ( configDirectory ) ) return null
const candidates = fs
. readdirSync ( configDirectory , { withFileTypes : true } )
. filter ( entry => entry . isFile ( ) && entry . name . toLowerCase ( ) . endsWith ( '.xuqmconfig' ) )
. map ( entry => path . join ( configDirectory , entry . name ) )
. sort ( )
if ( candidates . length === 0 ) return null
if ( candidates . length > 1 ) {
throw new Error (
` [XuqmConfig] ${ configDirectory } : 只能存在一个 .xuqmconfig 文件,当前发现 ${ candidates . length } 个 ` ,
)
2026-06-18 15:01:39 +08:00
}
2026-07-26 23:47:30 +08:00
return candidates [ 0 ]
2026-06-18 15:01:39 +08:00
}
2026-06-25 17:50:29 +08:00
function applyBugCollect ( metroConfig ) {
2026-06-25 19:04:55 +08:00
// 不再自动链式 withBugCollect: 基于 customSerializer 的 sourcemap 上传在 Metro 0.84
// 下不可靠( exports 限制无法获取默认 serializer,会破坏 dev 打包)。
// sourcemap 上传改由独立后处理脚本( react-native bundle 后单独上传)实现。
2026-07-17 13:50:30 +08:00
return metroConfig
2026-06-25 17:50:29 +08:00
}
2026-06-18 15:01:39 +08:00
function withXuqmConfig ( metroConfig ) {
2026-07-17 13:50:30 +08:00
const projectRoot = metroConfig . projectRoot ? ? process . cwd ( )
const configFile = findConfigFile ( projectRoot )
2026-06-18 15:01:39 +08:00
if ( ! configFile ) {
// 未找到配置文件,返回原配置( autoInit 会静默跳过)
2026-07-17 13:50:30 +08:00
return applyBugCollect ( metroConfig )
2026-06-18 15:01:39 +08:00
}
// 读取加密配置内容
2026-07-17 13:50:30 +08:00
const encryptedContent = fs . readFileSync ( configFile , 'utf-8' ) . trim ( )
2026-06-18 15:01:39 +08:00
2026-07-20 19:31:43 +08:00
let resolvedConfig
try {
resolvedConfig = decryptConfigAtBuildTime ( encryptedContent )
} catch ( error ) {
throw new Error ( ` [XuqmConfig] ${ configFile } : ${ error . message } ` )
}
// 生成临时 TS 文件作为虚拟模块。这里只传输已解析对象,移动端不再解密。
2026-07-17 13:50:30 +08:00
// Metro 0.84+ only hashes files under projectRoot/watchFolders. Keep the
// generated transport module in the ignored package-manager cache instead
// of the system temp directory; the .xuqmconfig file remains the sole source.
const generatedDir = path . join ( projectRoot , 'node_modules' , '.cache' , 'xuqm-sdk' )
fs . mkdirSync ( generatedDir , { recursive : true } )
const virtualFile = path . join ( generatedDir , 'autoinit-config.ts' )
2026-06-18 15:01:39 +08:00
fs . writeFileSync (
virtualFile ,
2026-07-26 23:47:30 +08:00
` // Auto-generated by @xuqm/rn-common/metro — do not edit \n export const RESOLVED_CONFIG = ${ JSON . stringify ( resolvedConfig ) } as const; \n export const BUILD_OPTIONS = ${ JSON . stringify ( { bugCollectMode : process . env . XUQM _BUGCOLLECT _MODE ? ? 'platform' } )} as const; \n ` ,
2026-06-18 15:01:39 +08:00
'utf-8' ,
2026-07-17 13:50:30 +08:00
)
2026-06-18 15:01:39 +08:00
2026-07-26 23:47:30 +08:00
// 拦截默认空实现,将 @xuqm/rn-common/auto-init-config 指向虚拟文件
2026-07-17 13:50:30 +08:00
const existingResolveRequest = metroConfig . resolver ? . resolveRequest
const existingGetModulesRunBeforeMainModule =
metroConfig . serializer ? . getModulesRunBeforeMainModule
2026-06-18 15:01:39 +08:00
2026-06-25 17:50:29 +08:00
return applyBugCollect ( {
2026-06-18 15:01:39 +08:00
... metroConfig ,
resolver : {
... metroConfig . resolver ,
resolveRequest : ( context , moduleName , platform ) => {
if ( moduleName === VIRTUAL _MODULE _ID ) {
return {
type : 'sourceFile' ,
filePath : virtualFile ,
2026-07-17 13:50:30 +08:00
}
2026-06-18 15:01:39 +08:00
}
if ( existingResolveRequest ) {
2026-07-17 13:50:30 +08:00
return existingResolveRequest ( context , moduleName , platform )
2026-06-18 15:01:39 +08:00
}
2026-07-17 13:50:30 +08:00
return context . resolveRequest ( context , moduleName , platform )
2026-06-18 15:01:39 +08:00
} ,
} ,
2026-07-17 13:50:30 +08:00
serializer : {
... metroConfig . serializer ,
getModulesRunBeforeMainModule : entryFilePath => {
const existingModules = existingGetModulesRunBeforeMainModule
? existingGetModulesRunBeforeMainModule ( entryFilePath )
: [ ]
return [ ... new Set ( [ ... existingModules , AUTO _INIT _MODULE ] ) ]
} ,
} ,
} )
2026-06-18 15:01:39 +08:00
}
2026-07-26 23:47:30 +08:00
module . exports = { decryptConfigAtBuildTime , findConfigFile , withXuqmConfig }