@anthropic-ai/sdk
Version:
The official TypeScript library for the Anthropic API
266 lines • 11.1 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.WorkloadIdentityError = exports.ADVISORY_REFRESH_BACKOFF_IN_SECONDS = exports.MANDATORY_REFRESH_THRESHOLD_IN_SECONDS = exports.ADVISORY_REFRESH_THRESHOLD_IN_SECONDS = exports.FEDERATION_BETA_HEADER = exports.OAUTH_API_BETA_HEADER = exports.TOKEN_ENDPOINT = exports.GRANT_TYPE_REFRESH_TOKEN = exports.GRANT_TYPE_JWT_BEARER = void 0;
exports.requireSecureTokenEndpoint = requireSecureTokenEndpoint;
exports.parseTokenResponse = parseTokenResponse;
exports.redactSensitive = redactSensitive;
exports.checkCredentialsFileSafety = checkCredentialsFileSafety;
exports.writeCredentialsFileAtomic = writeCredentialsFileAtomic;
const error_1 = require("../../core/error.js");
exports.GRANT_TYPE_JWT_BEARER = 'urn:ietf:params:oauth:grant-type:jwt-bearer';
exports.GRANT_TYPE_REFRESH_TOKEN = 'refresh_token';
exports.TOKEN_ENDPOINT = '/v1/oauth/token';
/**
* `anthropic-beta` value required on authenticated API requests using an
* OAuth bearer token, and on `refresh_token` grants against the token endpoint.
*/
exports.OAUTH_API_BETA_HEADER = 'oauth-2025-04-20';
/**
* `anthropic-beta` value required on jwt-bearer exchanges against the token
* endpoint. It routes the request to the federation service; it must NOT be
* sent on `refresh_token` grants, which are handled by a different backend.
*/
exports.FEDERATION_BETA_HEADER = 'oidc-federation-2026-04-01';
exports.ADVISORY_REFRESH_THRESHOLD_IN_SECONDS = 120;
exports.MANDATORY_REFRESH_THRESHOLD_IN_SECONDS = 30;
exports.ADVISORY_REFRESH_BACKOFF_IN_SECONDS = 5;
const MAX_TOKEN_RESPONSE_BYTES = 1 << 20;
/**
* Rejects base URLs that would cause a JWT assertion or refresh token to be
* sent over cleartext HTTP. Loopback hosts are allowed for local development.
*/
function requireSecureTokenEndpoint(baseURL) {
if (!baseURL)
return;
let u;
try {
u = new URL(baseURL);
}
catch (err) {
throw new WorkloadIdentityError(`Invalid token endpoint base URL "${baseURL}": ${err}`);
}
if (u.protocol === 'https:')
return;
// WHATWG URL.hostname returns bracketed IPv6 ("[::1]"); Go's net/url strips them.
const host = u.hostname.toLowerCase().replace(/^\[|\]$/g, '');
if (u.protocol === 'http:' && (host === 'localhost' || host === '127.0.0.1' || host === '::1')) {
return;
}
throw new WorkloadIdentityError(`Refusing to send credential over non-https token endpoint "${baseURL}"`);
}
/**
* Reads the response body as text, parses it as a token-endpoint JSON
* response, validates `access_token` is present, and rejects a non-Bearer
* `token_type` when one is provided. Reads at most
* {@link MAX_TOKEN_RESPONSE_BYTES} from the body stream.
*/
async function parseTokenResponse(resp, requestId) {
const text = await readLimitedText(resp);
let data;
try {
data = JSON.parse(text);
}
catch {
throw new WorkloadIdentityError(`Token endpoint returned non-JSON response (status ${resp.status})`, resp.status, redactSensitive(text), requestId);
}
if (!data.access_token) {
throw new WorkloadIdentityError(`Token endpoint response missing access_token: ${JSON.stringify(redactSensitive(data))}`, resp.status, redactSensitive(data), requestId);
}
if (data.token_type && data.token_type.toLowerCase() !== 'bearer') {
throw new WorkloadIdentityError(`Token endpoint response: unsupported token_type "${data.token_type}" (want Bearer)`, resp.status, redactSensitive(data), requestId);
}
return data;
}
const MAX_ERROR_BODY_CHARS = 2000;
// RFC 6749 §5.2 standard error-response fields. Anything else in a token
// endpoint error body is potentially echoed input (assertion, refresh_token,
// access_token, …) and is dropped rather than allowlisted-with-exceptions.
const SAFE_ERROR_KEYS = new Set(['error', 'error_description', 'error_uri']);
/**
* Returns a redacted copy of a token-endpoint error body for safe inclusion
* in an exception. Strings are truncated; objects keep only the RFC 6749
* §5.2 error fields.
*/
function redactSensitive(body) {
if (body == null)
return body;
if (typeof body === 'string') {
let parsed;
try {
parsed = JSON.parse(body);
}
catch {
if (body.length <= MAX_ERROR_BODY_CHARS)
return body;
return body.slice(0, MAX_ERROR_BODY_CHARS) + `... <${body.length - MAX_ERROR_BODY_CHARS} more chars>`;
}
return JSON.stringify(redactSensitive(parsed));
}
if (typeof body === 'object' && !Array.isArray(body)) {
const out = {};
for (const [k, v] of Object.entries(body)) {
if (SAFE_ERROR_KEYS.has(k))
out[k] = v;
}
return out;
}
return null;
}
/**
* Best-effort safety check on a credentials file before reading it.
*
* On POSIX: resolves symlinks (so containerized deployments that mount the
* credential as a symlink to a tmpfs-backed file keep working), then rejects
* the resolved target if it is group- or world- readable or writable. A uid
* mismatch on the resolved target is surfaced via `onWarn` since
* root-written/app-read is common in init-container setups. No-op on Windows.
*/
async function checkCredentialsFileSafety(path, onWarn = (m) => console.warn(`anthropic-sdk: ${m}`)) {
if (typeof process === 'undefined' || process.platform === 'win32')
return;
const fs = await Promise.resolve().then(() => __importStar(require('node:fs')));
let resolved = path;
let st;
try {
resolved = await fs.promises.realpath(path);
st = await fs.promises.stat(resolved);
}
catch {
return; // ENOENT etc — let the subsequent read surface a precise error
}
const mode = st.mode & 0o777;
// 0o022 = group/world write; 0o044 = group/world read.
if (mode & 0o022) {
throw new WorkloadIdentityError(`Credentials file at ${resolved} is group/world-writable (mode 0o${mode.toString(8)}); ` +
`this allows other local users to plant tokens. Run \`chmod 600 ${resolved}\`.`);
}
if (mode & 0o044) {
throw new WorkloadIdentityError(`Credentials file at ${resolved} is group/world-readable (mode 0o${mode.toString(8)}); ` +
`run \`chmod 600 ${resolved}\` before retrying.`);
}
if (typeof process.getuid === 'function' && st.uid !== process.getuid()) {
onWarn(`credentials file at ${resolved} is owned by uid ${st.uid} (current process uid ${process.getuid()}); ` + `verify this is intentional.`);
}
}
/**
* Atomically writes JSON to `targetPath` via a `.tmp` sibling + rename,
* with fsync on the file and (best-effort) on the parent directory.
* Creates the parent directory with mode 0700 and the file with mode 0600.
*/
async function writeCredentialsFileAtomic(targetPath, data) {
const fs = await Promise.resolve().then(() => __importStar(require('node:fs')));
const path = await Promise.resolve().then(() => __importStar(require('node:path')));
const dir = path.dirname(targetPath);
await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
// Unique temp name avoids two concurrent writers (different processes or
// SDK instances) racing on the same '.tmp' sibling and corrupting each
// other's bytes mid-write before the rename.
const tmpPath = `${targetPath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
try {
const fh = await fs.promises.open(tmpPath, 'w', 0o600);
try {
await fh.writeFile(JSON.stringify(data, null, 2));
await fh.sync();
}
finally {
await fh.close();
}
await fs.promises.rename(tmpPath, targetPath);
}
catch (err) {
// Don't leak the temp file if anything between create and rename failed.
await fs.promises.unlink(tmpPath).catch(() => { });
throw err;
}
// fsync the parent directory so the rename survives a crash.
try {
const dirFh = await fs.promises.open(dir, 'r');
try {
await dirFh.sync();
}
finally {
await dirFh.close();
}
}
catch {
// Directory fsync is best-effort (unsupported on some platforms, e.g. Windows).
}
}
async function readLimitedText(resp) {
if (!resp.body) {
return '';
}
const reader = resp.body.getReader();
const chunks = [];
let received = 0;
for (;;) {
const { done, value } = await reader.read();
if (done)
break;
if (received + value.length > MAX_TOKEN_RESPONSE_BYTES) {
const remaining = MAX_TOKEN_RESPONSE_BYTES - received;
if (remaining > 0)
chunks.push(value.subarray(0, remaining));
await reader.cancel();
break;
}
chunks.push(value);
received += value.length;
}
let merged;
if (chunks.length === 1) {
merged = chunks[0];
}
else {
merged = new Uint8Array(chunks.reduce((n, c) => n + c.length, 0));
let offset = 0;
for (const c of chunks) {
merged.set(c, offset);
offset += c.length;
}
}
return new TextDecoder('utf-8').decode(merged);
}
class WorkloadIdentityError extends error_1.AnthropicError {
constructor(message, statusCode = null, body = null, requestId = null) {
super(message);
this.statusCode = statusCode;
this.body = body;
this.requestId = requestId;
}
}
exports.WorkloadIdentityError = WorkloadIdentityError;
//# sourceMappingURL=types.js.map