XuqmGroup-RNSDK/packages/common/src/remoteConfig.ts

103 行
3.4 KiB
TypeScript

import { z } from 'zod'
import type { XuqmRemoteConfig } from './config'
const endpointSchema = z.string().refine(value => {
if (value === '') return true
try {
const protocol = new URL(value).protocol
return (
protocol === 'http:' || protocol === 'https:' || protocol === 'ws:' || protocol === 'wss:'
)
} catch {
return false
}
}, 'endpoint must be an absolute HTTP(S) or WS(S) URL')
const flatRemoteConfigSchema = z.object({
apiUrl: endpointSchema.optional(),
imApiUrl: endpointSchema.optional(),
imWsUrl: endpointSchema.optional(),
fileServiceUrl: endpointSchema.optional(),
imEnabled: z.boolean().optional(),
pushEnabled: z.boolean().optional(),
bugCollectApiUrl: endpointSchema.optional(),
bugCollectEnabled: z.boolean().optional(),
updateEnabled: z.boolean().optional(),
updateRequiresLogin: z.boolean().optional(),
})
const strictCachedRemoteConfigSchema = flatRemoteConfigSchema.strict()
const REMOTE_CONFIG_CACHE_FORMAT = 'XUQM-REMOTE-CONFIG-LKG-V1'
const remoteConfigCacheSchema = z
.object({
cachedAt: z.number().int().nonnegative(),
format: z.literal(REMOTE_CONFIG_CACHE_FORMAT),
remote: strictCachedRemoteConfigSchema,
})
.strict()
export type CachedRemoteConfig = {
cachedAt: number
remote: XuqmRemoteConfig
}
const remoteConfigResponseSchema = flatRemoteConfigSchema
.extend({
allowAnonymousUpdateCheck: z.boolean().optional(),
features: z
.object({
im: z.boolean().optional(),
push: z.boolean().optional(),
bugCollect: z.boolean().optional(),
update: z.boolean().optional(),
})
.optional(),
})
.loose()
/** 平台远程配置的唯一归一化入口;features 字段优先于历史 flat 字段。 */
export function normalizeRemoteConfig(value: unknown): XuqmRemoteConfig {
const raw = remoteConfigResponseSchema.parse(value)
const features = raw.features ?? {}
return {
apiUrl: raw.apiUrl,
imApiUrl: raw.imApiUrl,
imWsUrl: raw.imWsUrl,
fileServiceUrl: raw.fileServiceUrl,
imEnabled: features.im ?? raw.imEnabled,
pushEnabled: features.push ?? raw.pushEnabled,
bugCollectApiUrl: raw.bugCollectApiUrl,
bugCollectEnabled: features.bugCollect ?? raw.bugCollectEnabled,
updateEnabled: features.update ?? raw.updateEnabled,
updateRequiresLogin:
raw.updateRequiresLogin ??
(raw.allowAnonymousUpdateCheck === undefined ? undefined : !raw.allowAnonymousUpdateCheck),
}
}
/** 缓存只接受归一化后的非敏感字段,并拒绝所有未知字段。 */
export function parseCachedRemoteConfig(value: unknown): XuqmRemoteConfig | null {
const parsed = strictCachedRemoteConfigSchema.safeParse(value)
return parsed.success ? parsed.data : null
}
/** 非敏感 LKG 的唯一序列化入口;不加密、不携带任何客户端共享秘密。 */
export function serializeRemoteConfigCache(remote: XuqmRemoteConfig, cachedAt: number): string {
return JSON.stringify(
remoteConfigCacheSchema.parse({
cachedAt,
format: REMOTE_CONFIG_CACHE_FORMAT,
remote,
}),
)
}
/** 损坏、旧格式或含额外字段的缓存全部视为无效。 */
export function parseRemoteConfigCache(value: string): CachedRemoteConfig | null {
try {
const parsed = remoteConfigCacheSchema.safeParse(JSON.parse(value))
return parsed.success ? { cachedAt: parsed.data.cachedAt, remote: parsed.data.remote } : null
} catch {
return null
}
}