const tokenCache = new Map(); function toSafeString(value, maxLength = 2000) { if (value == null) { return ''; } return String(value).trim().slice(0, maxLength); } function sanitizeWecomResponse(payload) { if (!payload || typeof payload !== 'object') { return null; } const result = {}; for (const key of ['errcode', 'errmsg', 'invaliduser', 'invalidparty', 'invalidtag']) { if (payload[key] != null) { result[key] = payload[key]; } } return Object.keys(result).length ? result : null; } function getCredentialKey(config) { return [ toSafeString(config?.wecomCorpId, 128), toSafeString(config?.wecomAgentId, 64), toSafeString(config?.wecomAgentSecret, 16), ].join(':'); } function resolveTimeoutMs(timeoutMs) { return Math.max(100, Number(timeoutMs ?? 5000)); } function buildMissingConfigResult(missing) { return { ok: false, retryable: false, code: 'wecom_app_missing_config', message: `Missing WeCom app config: ${missing.join(',')}`, response: { missing, }, accessToken: null, }; } async function getWecomAppAccessToken({ config, timeoutMs = 5000, fetchImpl = fetch }) { const corpId = toSafeString(config?.wecomCorpId, 128); const agentId = toSafeString(config?.wecomAgentId, 64); const agentSecret = toSafeString(config?.wecomAgentSecret, 500); const requireAgentId = config?.wecomRequireAgentId !== false; const missing = []; if (!corpId) missing.push('WECOM_CORP_ID'); if (requireAgentId && !agentId) missing.push('WECOM_AGENT_ID'); if (!agentSecret) missing.push('WECOM_AGENT_SECRET'); if (missing.length) { return buildMissingConfigResult(missing); } const cacheKey = getCredentialKey(config); const cached = tokenCache.get(cacheKey); if (cached?.accessToken && cached.expiresAt > Date.now() + 60 * 1000) { return { ok: true, retryable: false, code: 'wecom_app_token_cached', message: null, response: { cached: true }, accessToken: cached.accessToken, }; } const timeout = resolveTimeoutMs(timeoutMs); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeout); const url = new URL('https://qyapi.weixin.qq.com/cgi-bin/gettoken'); url.searchParams.set('corpid', corpId); url.searchParams.set('corpsecret', agentSecret); try { const response = await fetchImpl(url, { method: 'GET', signal: controller.signal, }); const payload = await response.json().catch(() => null); const sanitized = sanitizeWecomResponse(payload); if (!response.ok) { return { ok: false, retryable: response.status >= 500 || response.status === 429, code: response.status >= 500 ? 'wecom_app_token_http_5xx' : response.status === 429 ? 'wecom_app_token_http_429' : 'wecom_app_token_http_4xx', message: `WeCom app token HTTP ${response.status}`, response: { status: response.status, body: sanitized, }, accessToken: null, }; } if (!payload || Number(payload.errcode ?? 0) !== 0 || !payload.access_token) { return { ok: false, retryable: false, code: 'wecom_app_token_api_error', message: `WeCom app token errcode ${payload?.errcode ?? 'missing_payload'}`, response: sanitized, accessToken: null, }; } const expiresInSeconds = Math.max(300, Number(payload.expires_in ?? 7200)); tokenCache.set(cacheKey, { accessToken: String(payload.access_token), expiresAt: Date.now() + Math.max(60, expiresInSeconds - 120) * 1000, }); return { ok: true, retryable: false, code: 'wecom_app_token_fetched', message: null, response: { cached: false, expiresIn: expiresInSeconds }, accessToken: String(payload.access_token), }; } catch (error) { if (error?.name === 'AbortError') { return { ok: false, retryable: true, code: 'wecom_app_token_timeout', message: `WeCom app token timeout (${timeout}ms)`, response: null, accessToken: null, }; } return { ok: false, retryable: true, code: 'wecom_app_token_network_error', message: String(error?.message ?? error ?? 'WeCom app token network error'), response: null, accessToken: null, }; } finally { clearTimeout(timer); } } function clearWecomAppAccessTokenCache() { tokenCache.clear(); } module.exports = { clearWecomAppAccessTokenCache, getWecomAppAccessToken, sanitizeWecomResponse, };