UNPKG

@anthropic-ai/sdk

Version:
266 lines 11.1 kB
"use strict"; 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