@anthropic-ai/sdk
Version:
The official TypeScript library for the Anthropic API
94 lines • 4.75 kB
JavaScript
import { CREDENTIALS_FILE_VERSION } from "../../core/credentials.mjs";
import { GRANT_TYPE_REFRESH_TOKEN, MANDATORY_REFRESH_THRESHOLD_IN_SECONDS, OAUTH_API_BETA_HEADER, TOKEN_ENDPOINT, WorkloadIdentityError, checkCredentialsFileSafety, parseTokenResponse, redactSensitive, requireSecureTokenEndpoint, writeCredentialsFileAtomic, } from "./types.mjs";
import { nowAsSeconds } from "../../internal/utils/time.mjs";
import { VERSION } from "../../version.mjs";
/**
* Reads a user-oauth credential file. Returns the cached access token while
* fresh; on expiry performs a `refresh_token` grant and writes the new
* tokens back to the credentials file (atomic replace, fsync'd).
*
* If `clientId` is empty, the access token is treated as static — the
* credentials file is read on every call but no refresh is attempted, and
* an expired token without a `refresh_token` raises.
*/
export function userOAuthProvider(config) {
return async (opts) => {
const fs = await import('node:fs');
await checkCredentialsFileSafety(config.credentialsPath, config.onSafetyWarning);
let raw;
try {
raw = await fs.promises.readFile(config.credentialsPath, 'utf-8');
}
catch (err) {
throw new WorkloadIdentityError(`Credentials file not found at ${config.credentialsPath}: ${err}`);
}
let creds;
try {
creds = JSON.parse(raw);
}
catch (err) {
throw new WorkloadIdentityError(`Credentials file at ${config.credentialsPath} is not valid JSON: ${err}`);
}
const accessToken = creds.access_token;
if (!accessToken) {
throw new WorkloadIdentityError(`Credentials file at ${config.credentialsPath} must include 'access_token'`);
}
// Return cached token if still fresh (or no expiry info), unless the
// caller is forcing a refresh after a 401 — then go straight to refresh
// even if the file's expires_at still looks valid.
const expiresAt = creds.expires_at;
if (!opts?.forceRefresh &&
(expiresAt == null || nowAsSeconds() < expiresAt - MANDATORY_REFRESH_THRESHOLD_IN_SECONDS)) {
return { token: accessToken, expiresAt: expiresAt ?? null };
}
const refreshToken = creds.refresh_token;
if (!config.clientId || !refreshToken) {
throw new WorkloadIdentityError(`Access token at ${config.credentialsPath} has expired and no refresh is available ` +
`(client_id ${config.clientId ? 'set' : 'empty'}, refresh_token ${refreshToken ? 'set' : 'empty'})`);
}
requireSecureTokenEndpoint(config.baseURL);
const body = {
grant_type: GRANT_TYPE_REFRESH_TOKEN,
refresh_token: refreshToken,
client_id: config.clientId,
};
const url = `${config.baseURL}${TOKEN_ENDPOINT}`;
let resp;
try {
resp = await config.fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'anthropic-beta': OAUTH_API_BETA_HEADER,
'User-Agent': config.userAgent || `anthropic-sdk-typescript/${VERSION} userOAuthProvider`,
},
body: JSON.stringify(body),
});
}
catch (err) {
throw new WorkloadIdentityError(`User OAuth refresh failed to reach token endpoint: ${err}`);
}
const requestId = resp.headers.get('Request-Id');
if (!resp.ok) {
const text = await resp.text().catch(() => '');
throw new WorkloadIdentityError(`User OAuth refresh failed (HTTP ${resp.status}): ${redactSensitive(text)}`, resp.status, redactSensitive(text), requestId);
}
const data = await parseTokenResponse(resp, requestId);
const expiresIn = Number(data.expires_in);
if (!Number.isFinite(expiresIn)) {
throw new WorkloadIdentityError(`User OAuth refresh response missing or invalid expires_in: ${JSON.stringify(redactSensitive(data))}`, resp.status, redactSensitive(data), requestId);
}
const newExpiresAt = nowAsSeconds() + expiresIn;
const newRefreshToken = data.refresh_token || refreshToken;
await writeCredentialsFileAtomic(config.credentialsPath, {
...creds,
version: CREDENTIALS_FILE_VERSION,
type: 'oauth_token',
access_token: data.access_token,
expires_at: newExpiresAt,
refresh_token: newRefreshToken,
});
return { token: data.access_token, expiresAt: newExpiresAt };
};
}
//# sourceMappingURL=user-oauth.mjs.map