const TARGETS = new Set(['stable', 'candidate']); function csvSet(value) { return new Set( String(value ?? '') .split(',') .map((item) => item.trim()) .filter(Boolean), ); } function normalizeIdentity(identity = {}) { identity = identity ?? {}; return { id: String(identity.id ?? '').trim(), username: String(identity.username ?? '').trim().toLowerCase(), wechatUserId: String(identity.wechatUserId ?? identity.wechat_user_id ?? '').trim(), }; } /** * Build a fail-closed, immutable-identity canary policy. Display names are * intentionally not accepted: nicknames are not unique enough for production * routing (there may be multiple users named “唐”). */ export function createCanaryPolicy({ userIds = [], usernames = [], wechatUserIds = [], candidateTarget = 'candidate', } = {}) { if (!TARGETS.has(candidateTarget) || candidateTarget === 'stable') { throw new Error('candidateTarget must be candidate'); } const policy = { userIds: userIds instanceof Set ? new Set(userIds) : csvSet(userIds), usernames: usernames instanceof Set ? new Set([...usernames].map((item) => String(item).trim().toLowerCase())) : new Set([...csvSet(usernames)].map((item) => item.toLowerCase())), wechatUserIds: wechatUserIds instanceof Set ? new Set(wechatUserIds) : csvSet(wechatUserIds), candidateTarget, }; if (!policy.userIds.size && !policy.usernames.size && !policy.wechatUserIds.size) { throw new Error('canary policy must contain at least one immutable user selector'); } return Object.freeze(policy); } export function canaryPolicyFromEnv(env = process.env) { return createCanaryPolicy({ userIds: csvSet(env.MEMIND_RELEASE_CANARY_USER_IDS), usernames: csvSet(env.MEMIND_RELEASE_CANARY_USERNAMES), wechatUserIds: csvSet(env.MEMIND_RELEASE_CANARY_WECHAT_USER_IDS), }); } export function resolveCanaryTarget(identity, policy) { if (!policy || typeof policy !== 'object') return 'stable'; const normalized = normalizeIdentity(identity); const matched = policy.userIds?.has(normalized.id) || policy.usernames?.has(normalized.username) || policy.wechatUserIds?.has(normalized.wechatUserId); return matched ? policy.candidateTarget : 'stable'; } export function describeCanaryMatch(identity, policy) { const normalized = normalizeIdentity(identity); const target = resolveCanaryTarget(normalized, policy); return { target, identity: normalized, matched: target === 'candidate', }; }