168 行
4.6 KiB
JavaScript
168 行
4.6 KiB
JavaScript
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,
|
|
};
|