UNPKG

zephyr-agent

Version:
167 lines (166 loc) • 7.62 kB
import "node:module"; import { createHash } from "node:crypto"; import { getSecretToken } from "./secret-token.mjs"; import { getItem, removeItem, setPrivateItem, storage } from "./storage.mjs"; import { StorageKeys } from "./storage-keys.mjs"; import { withStorageLock } from "./storage-lock.mjs"; import { makeRequest } from "../http/http-request.mjs"; import { getCiToken } from "./ci-token.mjs"; import { getServerToken } from "./server-token.mjs"; import { ZE_API_ENDPOINT, ze_api_gateway } from "zephyr-edge-contract"; import { getUserEmail } from "./user-email.mjs"; import { ze_log } from "../logging/debug.mjs"; import { inferCiTokenIdentity } from "./ci-token-identity.mjs"; import { ZeErrors, ZephyrError } from "../errors/index.mjs"; import { TOKEN_EXPIRY } from "../auth/auth-flags.mjs"; import { getTokenExpirationMs, isTokenStillValid } from "../auth/token-expiry.mjs"; const CI_TOKEN_CACHE_VERSION = 1; const CI_TOKEN_SCOPE_CONTEXT = 'zephyr-ci-token-cache\0'; const activeCiTokenCacheKeys = new Set(); const TOKEN_LOCK = { whenUnavailable: 'proceed' }; async function saveToken(token) { await withStorageLock('auth-token', ()=>setPrivateItem(StorageKeys.ze_auth_token, token), TOKEN_LOCK); } async function getToken(git_config) { const tokenFromEnv = getSecretToken(); const server_token = getServerToken(); const ci_token = getCiToken(); if (tokenFromEnv) return tokenFromEnv; if (ci_token) { const ciIdentity = await inferCiTokenIdentity(); if (ciIdentity) { ze_log.auth(`Using ${ciIdentity.provider} ${ciIdentity.source} identity for CI token attribution`); return await getTokenFromCiToken(ci_token, ciIdentity); } throwCiTokenAuthError(void 0, `${StorageKeys.ze_ci_token} was provided, but no supported CI identity was detected.`); } if (server_token && git_config) return await getTokenFromServerToken(server_token, git_config.git.email); const token = await withStorageLock('auth-token', async ()=>{ await storage; return getItem(StorageKeys.ze_auth_token); }, TOKEN_LOCK); if (token) return token; if (server_token) return void ze_log.error('No git config provided, skipping server token check'); } async function removeToken(expectedToken) { await withStorageLock('auth-token', async ()=>{ await storage; const storedToken = await getItem(StorageKeys.ze_auth_token); if (void 0 === expectedToken || storedToken === expectedToken) await removeItem(StorageKeys.ze_auth_token); }, TOKEN_LOCK); } async function cleanTokens(rejectedToken) { await removeToken(rejectedToken); const cacheKeys = Array.from(activeCiTokenCacheKeys); await Promise.all(cacheKeys.map((cacheKey)=>withStorageLock(getCiTokenLockName(cacheKey), async ()=>{ await storage; const cached = await getItem(cacheKey); if (void 0 === rejectedToken || isStoredCiAccessToken(cached) && cached.accessToken === rejectedToken) await removeItem(cacheKey); }, TOKEN_LOCK))); } async function getTokenFromServerToken(server_token, git_email) { const email = getUserEmail() ?? git_email; const [ok, cause, data] = await makeRequest({ path: ze_api_gateway.get_access_token_by_server_token, base: ZE_API_ENDPOINT(), query: { email } }, { headers: { Authorization: `Bearer ${server_token}` }, credentialToken: server_token }); if (!ok) { if (cause instanceof Error) ze_log.error('Failed to get token from server token:', cause.message); else ze_log.error('Failed to get token from server token:', cause); return; } await saveToken(data?.access_token ?? ''); return data?.access_token; } async function getTokenFromCiToken(ci_token, identity) { const scope = getCiTokenScope(ci_token, identity); const cacheKey = `${StorageKeys.ze_ci_auth_token}:${scope}`; activeCiTokenCacheKeys.add(cacheKey); return withStorageLock(getCiTokenLockName(cacheKey), async ()=>{ await storage; const cached = await getItem(cacheKey); if (isReusableCiAccessToken(cached, scope)) return cached.accessToken; const [ok, cause, data] = await makeRequest({ path: ze_api_gateway.ci_token_exchange, base: ZE_API_ENDPOINT(), query: {} }, { method: 'POST', headers: { Authorization: `Bearer ${ci_token}`, 'Content-Type': 'application/json' }, credentialToken: ci_token, skipTokenCleanup: true }, JSON.stringify(identity)); if (!ok) { await removeItem(cacheKey); throwCiTokenAuthError(identity, cause); } const accessToken = data?.access_token; if (!accessToken || !isTokenStillValid(accessToken, TOKEN_EXPIRY.SHORT_VALIDITY_CHECK_SEC)) { await removeItem(cacheKey); throwCiTokenAuthError(identity, new Error('CI token exchange returned an invalid or expiring access token')); } const expiresAtMs = getTokenExpirationMs(accessToken) ?? Date.now(); await setPrivateItem(cacheKey, { version: CI_TOKEN_CACHE_VERSION, scope, accessToken }, { ttl: expiresAtMs - Date.now() }); return accessToken; }, TOKEN_LOCK); } function getCiTokenScope(ciToken, identity) { const identityScope = [ ZE_API_ENDPOINT(), identity.provider, identity.source, identity.issuer ?? '', identity.providerSubject ?? '', identity.username ?? '', identity.providerActorType ?? '', identity.email ?? '', [ ...identity.emails ?? [] ].sort() ]; return createHash('sha256').update(CI_TOKEN_SCOPE_CONTEXT).update(ciToken).update('\0').update(JSON.stringify(identityScope)).digest('hex'); } function getCiTokenLockName(cacheKey) { return `ci-auth-${cacheKey.substring(cacheKey.lastIndexOf(':') + 1)}`; } function isReusableCiAccessToken(value, scope) { return isStoredCiAccessToken(value) && value.scope === scope && isTokenStillValid(value.accessToken, TOKEN_EXPIRY.SHORT_VALIDITY_CHECK_SEC); } function isStoredCiAccessToken(value) { return Boolean(value && 'object' == typeof value && value.version === CI_TOKEN_CACHE_VERSION && 'string' == typeof value.scope && 'string' == typeof value.accessToken); } function throwCiTokenAuthError(identity, cause) { const details = cause instanceof Error ? cause.message : String(cause); ze_log.error('Failed to get token from CI token:', details); throw new ZephyrError(ZeErrors.ERR_CI_TOKEN_AUTH, { cause, provider: identity?.provider ?? 'unknown', username: identity?.username ?? 'unknown', source: identity?.source ?? 'unknown', issuer: identity?.issuer ?? 'unknown', actorType: identity?.providerActorType ?? 'unknown', resolution: identity?.providerActorType === 'bot' ? 'This bot is authorized by the CI token creator. Check that the token creator is still an active member of the Zephyr organization.' : "Link this CI actor's Git provider account in Zephyr Cloud, then rerun the workflow. Zephyr uses linked Git provider identities to map provider-native CI actor data, such as GitHub actor IDs or GitLab user IDs/emails, to a Zephyr user.", details }); } export { cleanTokens, getToken, removeToken, saveToken }; //# sourceMappingURL=token.mjs.map