ai-auth
Version:
Complete Auth-Agent SDK - Agent authentication for AI developers + OAuth client integration for website developers
462 lines • 17 kB
JavaScript
"use strict";
/**
* Auth-Agent SDK for AI agents using OAuth 2.1 with PKCE
* Server-side TypeScript SDK
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.AgentSDK = void 0;
const token_manager_1 = require("./token-manager");
const utils_1 = require("./utils");
class AgentSDK {
agentId;
agentSecret;
modelName;
serverUrl = 'https://api.auth-agent.com';
timeout;
customFetch;
tokenManager;
codeVerifier = null;
state = null;
constructor(config) {
this.agentId = config.agentId;
this.agentSecret = config.agentSecret;
this.modelName = config.modelName;
this.timeout = config.timeout || 10000;
this.customFetch = config.customFetch || fetch.bind(globalThis);
this.tokenManager = new token_manager_1.TokenManager();
}
// ============================================================================
// OAuth 2.1 / OIDC Authentication Flow
// ============================================================================
/**
* Generate OAuth 2.1 authorization URL with PKCE
*/
async getAuthorizationUrl(options) {
const { redirectUri, scope = 'openid profile email agent', state } = options;
// Generate PKCE parameters
const pkce = await (0, utils_1.generatePKCEAsync)();
this.codeVerifier = pkce.codeVerifier;
// Generate or use provided state
this.state = state || (0, utils_1.generateState)();
// Build authorization URL
const params = new URLSearchParams({
response_type: 'code',
client_id: this.agentId,
redirect_uri: redirectUri,
scope: scope,
state: this.state,
code_challenge: pkce.codeChallenge,
code_challenge_method: 'S256',
});
const url = `${this.serverUrl}/oauth2/authorize?${params.toString()}`;
return { url, codeVerifier: this.codeVerifier, state: this.state };
}
/**
* Exchange authorization code for access token (OAuth 2.1 with PKCE)
*/
async exchangeCode(code, state, redirectUri) {
// Validate state
if (state !== this.state) {
throw this.createError('state_mismatch', 'State parameter mismatch - possible CSRF attack');
}
if (!this.codeVerifier) {
throw this.createError('pkce_missing', 'No code_verifier found - call getAuthorizationUrl() first');
}
// Exchange code for tokens
const response = await this.customFetch(`${this.serverUrl}/oauth2/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: redirectUri,
client_id: this.agentId,
code_verifier: this.codeVerifier,
}).toString(),
signal: AbortSignal.timeout(this.timeout),
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw this.createError(error.error || 'token_exchange_failed', error.error_description, response.status);
}
const tokenData = await response.json();
this.tokenManager.setTokens(tokenData);
// Clear PKCE state
this.codeVerifier = null;
this.state = null;
return tokenData;
}
/**
* Refresh access token using refresh token
*/
async refreshAccessToken() {
if (!this.tokenManager.refreshToken) {
throw this.createError('no_refresh_token', 'No refresh token available');
}
const response = await this.customFetch(`${this.serverUrl}/oauth2/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: this.tokenManager.refreshToken,
client_id: this.agentId,
}).toString(),
signal: AbortSignal.timeout(this.timeout),
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw this.createError(error.error || 'refresh_failed', error.error_description, response.status);
}
const tokenData = await response.json();
this.tokenManager.setTokens(tokenData);
return tokenData;
}
/**
* Headless authentication for automated agents (Resource Owner Password flow)
* Note: Less secure than Authorization Code flow. Use only for trusted agents.
*/
async authenticateHeadless(username, password, scope = 'openid profile email agent') {
const response = await this.customFetch(`${this.serverUrl}/oauth2/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'password',
username: username,
password: password,
client_id: this.agentId,
scope: scope,
}).toString(),
signal: AbortSignal.timeout(this.timeout),
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw this.createError(error.error || 'authentication_failed', error.error_description, response.status);
}
const tokenData = await response.json();
this.tokenManager.setTokens(tokenData);
return tokenData;
}
// ============================================================================
// Authenticated API Requests
// ============================================================================
/**
* Make authenticated request using OAuth access token
*/
async makeRequest(endpoint, options = {}) {
const { method = 'POST', params, body, expectJson = true, autoRefresh = true, } = options;
const url = new URL(`${this.serverUrl}/${endpoint}`);
if (params) {
Object.entries(params).forEach(([key, value]) => url.searchParams.set(key, value));
}
// Get access token (try refresh if expired)
let accessToken;
try {
accessToken = this.tokenManager.getAccessToken();
}
catch (error) {
if (autoRefresh && this.tokenManager.refreshToken) {
await this.refreshAccessToken();
accessToken = this.tokenManager.getAccessToken();
}
else {
throw error;
}
}
// Build headers with Bearer token
const headers = {
'Authorization': `${this.tokenManager.tokenType} ${accessToken}`,
};
if (body) {
headers['Content-Type'] = 'application/json';
}
// Make request
const response = await this.customFetch(url.toString(), {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(this.timeout),
});
// Handle response
if (!response.ok) {
if (response.status === 401) {
this.tokenManager.clear();
throw this.createError('token_expired', 'Access token invalid or expired', 401);
}
else if (response.status === 403) {
throw this.createError('forbidden', 'Insufficient permissions', 403);
}
const error = await response.json().catch(() => ({}));
throw this.createError(error.error || 'request_failed', error.error_description || error.message, response.status);
}
if (expectJson) {
return response.json();
}
else {
return {
ok: response.ok,
status: response.status,
text: await response.text(),
};
}
}
// ============================================================================
// Agent Authentication
// ============================================================================
/**
* Authenticate agent and create auth session
* Note: Agents must be registered at https://console.auth-agent.com
*/
async authenticateAgent(request) {
const response = await this.customFetch(`${this.serverUrl}/api/agent-auth`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
agent_id: this.agentId,
agent_secret: this.agentSecret,
model_name: this.modelName,
...request,
}),
signal: AbortSignal.timeout(this.timeout),
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw this.createError(error.error || 'agent_auth_failed', error.error_description, response.status);
}
return response.json();
}
/**
* Get agent profile information
*/
async getAgentProfile() {
return this.makeRequest('api/agents/profile', {
method: 'GET',
});
}
/**
* Update agent profile
*/
async updateAgentProfile(updates) {
return this.makeRequest('api/agents/profile', {
method: 'PUT',
body: updates,
});
}
/**
* Get user information from OIDC userinfo endpoint
*/
async getUserInfo() {
return this.makeRequest('oauth2/userinfo', {
method: 'GET',
});
}
// ============================================================================
// Event Sending & Logging
// ============================================================================
/**
* Send event data to Auth-Agent
*/
async sendEvent(eventData) {
return this.makeRequest('api/events', {
method: 'POST',
body: eventData,
});
}
/**
* Send a verified click event
*/
async sendVerifiedClick(selector, site, evidence = 'oauth_authenticated') {
return this.sendEvent({
event: 'click',
selector,
site,
evidence,
});
}
/**
* Send a post-run event with result
*/
async sendPostRun(selector, site, result) {
return this.sendEvent({
event: 'post_run',
selector,
site,
result: result.substring(0, 2000), // Truncate to 2000 chars
});
}
// ============================================================================
// AI Verification / Challenge Flow
// ============================================================================
/**
* Request a verification challenge
*/
async requestChallenge() {
return this.makeRequest('api/verify_challenge', {
method: 'POST',
body: {},
});
}
/**
* Confirm verification with challenge response
*/
async confirmVerification(request) {
return this.makeRequest('api/verify_confirm', {
method: 'POST',
body: request,
});
}
/**
* Get current verification status
*/
async getVerifyStatus() {
return this.makeRequest('api/verify_status', {
method: 'GET',
});
}
/**
* Poll for challenge and authenticate using verify_ctx_id
* For challenge-based authentication flow
*/
async pollAndAuthenticate(verifyCtxId, options = {}) {
const { timeout = 30000, interval = 2000 } = options;
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
try {
// Get the challenge
const response = await this.customFetch(`${this.serverUrl}/api/verify_challenge?verify_ctx_id=${verifyCtxId}`, {
method: 'GET',
signal: AbortSignal.timeout(this.timeout),
});
if (response.status === 200) {
const challengeData = await response.json();
const challenge = challengeData.challenge;
if (challenge) {
// Confirm verification
const confirmData = {
agent_id: this.agentId,
verify_ctx_id: verifyCtxId,
challenge: challenge,
};
// Add agent_secret if provided
if (this.agentSecret) {
confirmData.agent_secret = this.agentSecret;
}
const confirmResponse = await this.customFetch(`${this.serverUrl}/api/verify_confirm`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(confirmData),
signal: AbortSignal.timeout(this.timeout),
});
if (confirmResponse.status === 200) {
return true;
}
else {
console.error('Verification confirm failed:', confirmResponse.status);
return false;
}
}
}
else if (response.status === 404) {
// No challenge available yet, keep waiting
await (0, utils_1.sleep)(interval);
continue;
}
else {
console.error('Challenge request failed:', response.status);
return false;
}
}
catch (error) {
console.error('Error during polling:', error);
await (0, utils_1.sleep)(interval);
}
}
console.error('Authentication timeout');
return false;
}
// ============================================================================
// Token & Session Management
// ============================================================================
/**
* Revoke access token or refresh token
*/
async revokeToken(token) {
const tokenToRevoke = token || this.tokenManager.accessToken;
const result = await this.makeRequest('api/revoke', {
method: 'POST',
body: { token: tokenToRevoke },
autoRefresh: false,
});
if (!token) {
// If revoking current token, clear all tokens
this.tokenManager.clear();
}
return result;
}
/**
* Logout agent (revoke tokens and clear session)
*/
async logout() {
if (this.tokenManager.accessToken) {
try {
await this.revokeToken();
}
catch (error) {
// Ignore errors, clear tokens anyway
}
}
this.tokenManager.clear();
}
/**
* Introspect token to check validity and claims
*/
async introspectToken(token) {
const tokenToCheck = token || this.tokenManager.accessToken;
return this.makeRequest('api/introspect', {
method: 'POST',
body: { token: tokenToCheck },
});
}
// ============================================================================
// Token Access & Status
// ============================================================================
/**
* Check if user is authenticated
*/
isAuthenticated() {
return !this.tokenManager.isExpired();
}
/**
* Get current access token
*/
getAccessToken() {
return this.tokenManager.accessToken;
}
/**
* Get current refresh token
*/
getRefreshToken() {
return this.tokenManager.refreshToken;
}
// ============================================================================
// Error Handling
// ============================================================================
createError(code, description, statusCode) {
const error = new Error(description || code);
error.code = code;
error.description = description;
error.statusCode = statusCode;
return error;
}
}
exports.AgentSDK = AgentSDK;
//# sourceMappingURL=agent-sdk.js.map