zephyr-agent
Version:
Zephyr plugin agent
167 lines (166 loc) • 7.62 kB
JavaScript
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