feat: Phase 1 P1 — Dockerfile, AGNTCY alignment docs, Node.js SDK
Three remaining Phase 1 P1 deliverables: 1. Dockerfile — multi-stage build (builder + production), node:18-alpine, non-root USER node, .dockerignore excluding secrets and dev artifacts 2. AGNTCY alignment docs (docs/agntcy/) — README and alignment.md mapping all 6 AGNTCY domains to AgentIdP features with Phase 2/3 pending items noted 3. Node.js SDK (@sentryagent/idp-sdk) — TypeScript strict, zero any, native fetch (Node 18+), TokenManager with 60s auto-refresh, service clients for all 14 endpoints (agents, credentials, tokens, audit), AgentIdPError typed error hierarchy, full README All three changes tracked under openspec/changes/ with tasks marked complete. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
66
sdk/src/client.ts
Normal file
66
sdk/src/client.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { TokenManager } from './token-manager.js';
|
||||
import { AgentRegistryClient } from './services/agents.js';
|
||||
import { CredentialClient } from './services/credentials.js';
|
||||
import { TokenClient } from './services/token.js';
|
||||
import { AuditClient } from './services/audit.js';
|
||||
import type { AgentIdPClientConfig, OAuthScope } from './types.js';
|
||||
|
||||
/**
|
||||
* Top-level client for the SentryAgent.ai AgentIdP API.
|
||||
*
|
||||
* Composes all service clients under a single entry point.
|
||||
* Handles token acquisition and caching automatically via TokenManager.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const client = new AgentIdPClient({
|
||||
* baseUrl: 'http://localhost:3000',
|
||||
* clientId: 'your-agent-id',
|
||||
* clientSecret: 'your-client-secret',
|
||||
* });
|
||||
*
|
||||
* const agents = await client.agents.listAgents();
|
||||
* ```
|
||||
*/
|
||||
export class AgentIdPClient {
|
||||
/** Agent Registry operations: register, list, get, update, decommission. */
|
||||
readonly agents: AgentRegistryClient;
|
||||
|
||||
/** Credential operations: generate, list, rotate, revoke. */
|
||||
readonly credentials: CredentialClient;
|
||||
|
||||
/** Token operations: introspect, revoke. */
|
||||
readonly tokens: TokenClient;
|
||||
|
||||
/** Audit log operations: query, get event. */
|
||||
readonly audit: AuditClient;
|
||||
|
||||
private readonly tokenManager: TokenManager;
|
||||
|
||||
constructor(config: AgentIdPClientConfig) {
|
||||
const defaultScopes: OAuthScope[] = ['agents:read', 'agents:write', 'tokens:read', 'audit:read'];
|
||||
const scopes = (config.scopes ?? defaultScopes).join(' ');
|
||||
|
||||
this.tokenManager = new TokenManager(
|
||||
config.baseUrl,
|
||||
config.clientId,
|
||||
config.clientSecret,
|
||||
scopes,
|
||||
);
|
||||
|
||||
const getToken = () => this.tokenManager.getToken();
|
||||
|
||||
this.agents = new AgentRegistryClient(config.baseUrl, getToken);
|
||||
this.credentials = new CredentialClient(config.baseUrl, getToken);
|
||||
this.tokens = new TokenClient(config.baseUrl, getToken);
|
||||
this.audit = new AuditClient(config.baseUrl, getToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cached access token. The next API call will request a new token.
|
||||
* Use this after rotating credentials or when you suspect the token is stale.
|
||||
*/
|
||||
clearTokenCache(): void {
|
||||
this.tokenManager.clearCache();
|
||||
}
|
||||
}
|
||||
71
sdk/src/errors.ts
Normal file
71
sdk/src/errors.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Error types for the SentryAgent.ai AgentIdP SDK.
|
||||
*/
|
||||
|
||||
/** Standard error response shape from the AgentIdP API. */
|
||||
interface ApiErrorBody {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** OAuth 2.0 error response shape from the token endpoint. */
|
||||
interface OAuth2ErrorBody {
|
||||
error: string;
|
||||
error_description: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed error thrown by the AgentIdP SDK for all API failures.
|
||||
* Never throws raw fetch errors or untyped exceptions.
|
||||
*/
|
||||
export class AgentIdPError extends Error {
|
||||
/** Machine-readable error code from the API (e.g. AGENT_NOT_FOUND). */
|
||||
readonly code: string;
|
||||
/** HTTP status code of the failed response. */
|
||||
readonly httpStatus: number;
|
||||
/** Optional structured details from the API error response. */
|
||||
readonly details?: Record<string, unknown>;
|
||||
|
||||
constructor(code: string, message: string, httpStatus: number, details?: Record<string, unknown>) {
|
||||
super(message);
|
||||
this.name = 'AgentIdPError';
|
||||
this.code = code;
|
||||
this.httpStatus = httpStatus;
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an AgentIdPError from a standard API error response body.
|
||||
* Accepts unknown to allow callers to pass raw parsed JSON without pre-casting.
|
||||
*
|
||||
* @param body - Parsed API error response (or unknown).
|
||||
* @param httpStatus - HTTP status code.
|
||||
* @returns AgentIdPError instance.
|
||||
*/
|
||||
static fromApiError(body: unknown, httpStatus: number): AgentIdPError {
|
||||
if (
|
||||
typeof body === 'object' &&
|
||||
body !== null &&
|
||||
'code' in body &&
|
||||
'message' in body &&
|
||||
typeof (body as ApiErrorBody).code === 'string' &&
|
||||
typeof (body as ApiErrorBody).message === 'string'
|
||||
) {
|
||||
const typed = body as ApiErrorBody;
|
||||
return new AgentIdPError(typed.code, typed.message, httpStatus, typed.details);
|
||||
}
|
||||
return new AgentIdPError('UNKNOWN_ERROR', String(body), httpStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an AgentIdPError from an OAuth 2.0 error response body.
|
||||
*
|
||||
* @param body - Parsed OAuth2 error response.
|
||||
* @param httpStatus - HTTP status code.
|
||||
* @returns AgentIdPError instance.
|
||||
*/
|
||||
static fromOAuth2Error(body: OAuth2ErrorBody, httpStatus: number): AgentIdPError {
|
||||
return new AgentIdPError(body.error, body.error_description, httpStatus);
|
||||
}
|
||||
}
|
||||
35
sdk/src/index.ts
Normal file
35
sdk/src/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export { AgentIdPClient } from './client.js';
|
||||
export { AgentIdPError } from './errors.js';
|
||||
export { TokenManager } from './token-manager.js';
|
||||
|
||||
export type {
|
||||
// Config
|
||||
AgentIdPClientConfig,
|
||||
// Enums / union types
|
||||
AgentType,
|
||||
AgentStatus,
|
||||
DeploymentEnv,
|
||||
CredentialStatus,
|
||||
OAuthScope,
|
||||
AuditAction,
|
||||
AuditOutcome,
|
||||
// Agent Registry
|
||||
Agent,
|
||||
RegisterAgentRequest,
|
||||
UpdateAgentRequest,
|
||||
ListAgentsParams,
|
||||
PaginatedAgents,
|
||||
// Credentials
|
||||
Credential,
|
||||
CredentialWithSecret,
|
||||
GenerateCredentialRequest,
|
||||
ListCredentialsParams,
|
||||
PaginatedCredentials,
|
||||
// Tokens
|
||||
TokenResponse,
|
||||
IntrospectResponse,
|
||||
// Audit
|
||||
AuditEvent,
|
||||
QueryAuditLogParams,
|
||||
PaginatedAuditEvents,
|
||||
} from './types.js';
|
||||
72
sdk/src/request.ts
Normal file
72
sdk/src/request.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { AgentIdPError } from './errors.js';
|
||||
|
||||
type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'DELETE';
|
||||
|
||||
interface RequestOptions {
|
||||
method: HttpMethod;
|
||||
path: string;
|
||||
token: string;
|
||||
body?: unknown;
|
||||
query?: Record<string, string | number | boolean | undefined>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared HTTP request helper for all AgentIdP API calls.
|
||||
* Sets Authorization header, serialises JSON body, parses response,
|
||||
* and maps API error shapes to AgentIdPError.
|
||||
*/
|
||||
export async function request<T>(baseUrl: string, opts: RequestOptions): Promise<T> {
|
||||
const url = new URL(opts.path, baseUrl);
|
||||
|
||||
if (opts.query) {
|
||||
for (const [key, value] of Object.entries(opts.query)) {
|
||||
if (value !== undefined) {
|
||||
url.searchParams.set(key, String(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${opts.token}`,
|
||||
Accept: 'application/json',
|
||||
};
|
||||
|
||||
let bodyPayload: string | undefined;
|
||||
if (opts.body !== undefined) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
bodyPayload = JSON.stringify(opts.body);
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(url.toString(), {
|
||||
method: opts.method,
|
||||
headers,
|
||||
body: bodyPayload,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new AgentIdPError(
|
||||
'NETWORK_ERROR',
|
||||
`Network error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return undefined as unknown as T;
|
||||
}
|
||||
|
||||
let data: unknown;
|
||||
const contentType = response.headers.get('content-type') ?? '';
|
||||
if (contentType.includes('application/json')) {
|
||||
data = await response.json();
|
||||
} else {
|
||||
data = await response.text();
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw AgentIdPError.fromApiError(data, response.status);
|
||||
}
|
||||
|
||||
return data as T;
|
||||
}
|
||||
90
sdk/src/services/agents.ts
Normal file
90
sdk/src/services/agents.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { request } from '../request.js';
|
||||
import type {
|
||||
Agent,
|
||||
RegisterAgentRequest,
|
||||
UpdateAgentRequest,
|
||||
ListAgentsParams,
|
||||
PaginatedAgents,
|
||||
} from '../types.js';
|
||||
|
||||
/**
|
||||
* Client for the Agent Registry service.
|
||||
* Covers all agent CRUD operations: register, list, get, update, decommission.
|
||||
*/
|
||||
export class AgentRegistryClient {
|
||||
constructor(
|
||||
private readonly baseUrl: string,
|
||||
private readonly getToken: () => Promise<string>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Register a new AI agent.
|
||||
* Returns the created agent record including its agentId and agentSecret.
|
||||
*/
|
||||
async registerAgent(params: RegisterAgentRequest): Promise<Agent> {
|
||||
const token = await this.getToken();
|
||||
return request<Agent>(this.baseUrl, {
|
||||
method: 'POST',
|
||||
path: '/api/v1/agents',
|
||||
token,
|
||||
body: params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List all registered agents with optional filters and pagination.
|
||||
*/
|
||||
async listAgents(params: ListAgentsParams = {}): Promise<PaginatedAgents> {
|
||||
const token = await this.getToken();
|
||||
return request<PaginatedAgents>(this.baseUrl, {
|
||||
method: 'GET',
|
||||
path: '/api/v1/agents',
|
||||
token,
|
||||
query: {
|
||||
status: params.status,
|
||||
agentType: params.agentType,
|
||||
page: params.page,
|
||||
limit: params.limit,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single agent by its agentId.
|
||||
*/
|
||||
async getAgent(agentId: string): Promise<Agent> {
|
||||
const token = await this.getToken();
|
||||
return request<Agent>(this.baseUrl, {
|
||||
method: 'GET',
|
||||
path: `/api/v1/agents/${agentId}`,
|
||||
token,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update mutable fields on an existing agent (name, description, capabilities, metadata).
|
||||
* Returns the updated agent record.
|
||||
*/
|
||||
async updateAgent(agentId: string, params: UpdateAgentRequest): Promise<Agent> {
|
||||
const token = await this.getToken();
|
||||
return request<Agent>(this.baseUrl, {
|
||||
method: 'PATCH',
|
||||
path: `/api/v1/agents/${agentId}`,
|
||||
token,
|
||||
body: params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Decommission an agent. This is irreversible — the agent can no longer
|
||||
* authenticate or obtain tokens after decommission.
|
||||
*/
|
||||
async decommissionAgent(agentId: string): Promise<void> {
|
||||
const token = await this.getToken();
|
||||
return request<void>(this.baseUrl, {
|
||||
method: 'DELETE',
|
||||
path: `/api/v1/agents/${agentId}`,
|
||||
token,
|
||||
});
|
||||
}
|
||||
}
|
||||
48
sdk/src/services/audit.ts
Normal file
48
sdk/src/services/audit.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { request } from '../request.js';
|
||||
import type { AuditEvent, QueryAuditLogParams, PaginatedAuditEvents } from '../types.js';
|
||||
|
||||
/**
|
||||
* Client for the Audit Log service.
|
||||
* Covers querying the audit event list and fetching individual events.
|
||||
*/
|
||||
export class AuditClient {
|
||||
constructor(
|
||||
private readonly baseUrl: string,
|
||||
private readonly getToken: () => Promise<string>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Query audit log events with optional filters and pagination.
|
||||
* Events are retained for 90 days. Requires `audit:read` scope.
|
||||
*/
|
||||
async queryAuditLog(params: QueryAuditLogParams = {}): Promise<PaginatedAuditEvents> {
|
||||
const token = await this.getToken();
|
||||
return request<PaginatedAuditEvents>(this.baseUrl, {
|
||||
method: 'GET',
|
||||
path: '/api/v1/audit',
|
||||
token,
|
||||
query: {
|
||||
agentId: params.agentId,
|
||||
action: params.action,
|
||||
outcome: params.outcome,
|
||||
fromDate: params.fromDate,
|
||||
toDate: params.toDate,
|
||||
page: params.page,
|
||||
limit: params.limit,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single audit event by its eventId.
|
||||
* Requires `audit:read` scope.
|
||||
*/
|
||||
async getAuditEvent(eventId: string): Promise<AuditEvent> {
|
||||
const token = await this.getToken();
|
||||
return request<AuditEvent>(this.baseUrl, {
|
||||
method: 'GET',
|
||||
path: `/api/v1/audit/${eventId}`,
|
||||
token,
|
||||
});
|
||||
}
|
||||
}
|
||||
83
sdk/src/services/credentials.ts
Normal file
83
sdk/src/services/credentials.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { request } from '../request.js';
|
||||
import type {
|
||||
Credential,
|
||||
CredentialWithSecret,
|
||||
GenerateCredentialRequest,
|
||||
ListCredentialsParams,
|
||||
PaginatedCredentials,
|
||||
} from '../types.js';
|
||||
|
||||
/**
|
||||
* Client for the Credential Management service.
|
||||
* Covers generate, list, rotate, and revoke operations for agent credentials.
|
||||
*/
|
||||
export class CredentialClient {
|
||||
constructor(
|
||||
private readonly baseUrl: string,
|
||||
private readonly getToken: () => Promise<string>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Generate a new credential for an agent.
|
||||
* The `clientSecret` in the response is shown ONCE — store it securely immediately.
|
||||
*/
|
||||
async generateCredential(
|
||||
agentId: string,
|
||||
params: GenerateCredentialRequest = {},
|
||||
): Promise<CredentialWithSecret> {
|
||||
const token = await this.getToken();
|
||||
return request<CredentialWithSecret>(this.baseUrl, {
|
||||
method: 'POST',
|
||||
path: `/api/v1/agents/${agentId}/credentials`,
|
||||
token,
|
||||
body: Object.keys(params).length > 0 ? params : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List all credentials for an agent with optional pagination.
|
||||
* Secrets are never returned in list responses.
|
||||
*/
|
||||
async listCredentials(
|
||||
agentId: string,
|
||||
params: ListCredentialsParams = {},
|
||||
): Promise<PaginatedCredentials> {
|
||||
const token = await this.getToken();
|
||||
return request<PaginatedCredentials>(this.baseUrl, {
|
||||
method: 'GET',
|
||||
path: `/api/v1/agents/${agentId}/credentials`,
|
||||
token,
|
||||
query: {
|
||||
page: params.page,
|
||||
limit: params.limit,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate a credential. The same credentialId is retained but a new secret is issued.
|
||||
* The old secret is immediately invalidated upon rotation.
|
||||
* The new `clientSecret` is shown ONCE — store it securely immediately.
|
||||
*/
|
||||
async rotateCredential(agentId: string, credentialId: string): Promise<CredentialWithSecret> {
|
||||
const token = await this.getToken();
|
||||
return request<CredentialWithSecret>(this.baseUrl, {
|
||||
method: 'POST',
|
||||
path: `/api/v1/agents/${agentId}/credentials/${credentialId}/rotate`,
|
||||
token,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke a credential. Any tokens previously issued with this credential
|
||||
* remain valid until they expire — use token revocation to invalidate them immediately.
|
||||
*/
|
||||
async revokeCredential(agentId: string, credentialId: string): Promise<Credential> {
|
||||
const token = await this.getToken();
|
||||
return request<Credential>(this.baseUrl, {
|
||||
method: 'DELETE',
|
||||
path: `/api/v1/agents/${agentId}/credentials/${credentialId}`,
|
||||
token,
|
||||
});
|
||||
}
|
||||
}
|
||||
80
sdk/src/services/token.ts
Normal file
80
sdk/src/services/token.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { request } from '../request.js';
|
||||
import { AgentIdPError } from '../errors.js';
|
||||
import type { IntrospectResponse } from '../types.js';
|
||||
|
||||
/**
|
||||
* Client for OAuth 2.0 token operations (introspect and revoke).
|
||||
* Token issuance is handled separately by TokenManager.
|
||||
*/
|
||||
export class TokenClient {
|
||||
constructor(
|
||||
private readonly baseUrl: string,
|
||||
private readonly getToken: () => Promise<string>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Introspect a token to check whether it is currently active.
|
||||
* Always returns 200 — check the `active` field to determine validity.
|
||||
* Requires a Bearer token with `tokens:read` scope.
|
||||
*/
|
||||
async introspectToken(tokenToCheck: string): Promise<IntrospectResponse> {
|
||||
const token = await this.getToken();
|
||||
const body = new URLSearchParams({ token: tokenToCheck });
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(new URL('/api/v1/token/introspect', this.baseUrl).toString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: body.toString(),
|
||||
});
|
||||
} catch (err) {
|
||||
throw new AgentIdPError(
|
||||
'NETWORK_ERROR',
|
||||
`Network error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
const data: unknown = await response.json();
|
||||
if (!response.ok) {
|
||||
throw AgentIdPError.fromApiError(data, response.status);
|
||||
}
|
||||
return data as IntrospectResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke a token immediately. Idempotent — revoking an already-revoked
|
||||
* or expired token is not an error (RFC 7009).
|
||||
*/
|
||||
async revokeToken(tokenToRevoke: string): Promise<void> {
|
||||
const token = await this.getToken();
|
||||
const body = new URLSearchParams({ token: tokenToRevoke });
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(new URL('/api/v1/token/revoke', this.baseUrl).toString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: body.toString(),
|
||||
});
|
||||
} catch (err) {
|
||||
throw new AgentIdPError(
|
||||
'NETWORK_ERROR',
|
||||
`Network error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const data: unknown = await response.json();
|
||||
throw AgentIdPError.fromApiError(data, response.status);
|
||||
}
|
||||
}
|
||||
}
|
||||
97
sdk/src/token-manager.ts
Normal file
97
sdk/src/token-manager.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* TokenManager — handles OAuth 2.0 token acquisition, caching, and refresh.
|
||||
* Tokens are re-issued automatically when expired or within 60 seconds of expiry.
|
||||
*/
|
||||
|
||||
import { AgentIdPError } from './errors.js';
|
||||
import { TokenResponse } from './types.js';
|
||||
|
||||
/** Seconds before expiry at which a token refresh is triggered. */
|
||||
const REFRESH_BUFFER_SECONDS = 60;
|
||||
|
||||
interface CachedToken {
|
||||
accessToken: string;
|
||||
expiresAt: number; // Unix seconds
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages token acquisition and caching for the AgentIdP SDK.
|
||||
* Callers request a token via `getToken()` — the manager handles issuance and refresh transparently.
|
||||
*/
|
||||
export class TokenManager {
|
||||
private cached: CachedToken | null = null;
|
||||
|
||||
/**
|
||||
* @param baseUrl - AgentIdP API base URL.
|
||||
* @param clientId - The agent's clientId (agentId UUID).
|
||||
* @param clientSecret - The agent's clientSecret.
|
||||
* @param scopes - Space-separated OAuth 2.0 scopes to request.
|
||||
*/
|
||||
constructor(
|
||||
private readonly baseUrl: string,
|
||||
private readonly clientId: string,
|
||||
private readonly clientSecret: string,
|
||||
private readonly scopes: string,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns a valid access token.
|
||||
* Acquires a new token if none is cached or the cached token is near expiry.
|
||||
*
|
||||
* @returns A valid JWT access token string.
|
||||
* @throws AgentIdPError if token acquisition fails.
|
||||
*/
|
||||
async getToken(): Promise<string> {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
if (this.cached && this.cached.expiresAt - now > REFRESH_BUFFER_SECONDS) {
|
||||
return this.cached.accessToken;
|
||||
}
|
||||
|
||||
const token = await this.issueToken();
|
||||
this.cached = {
|
||||
accessToken: token.access_token,
|
||||
expiresAt: now + token.expires_in,
|
||||
};
|
||||
|
||||
return this.cached.accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls POST /token to issue a new access token.
|
||||
*
|
||||
* @returns TokenResponse from the API.
|
||||
* @throws AgentIdPError on authentication failure or API error.
|
||||
*/
|
||||
private async issueToken(): Promise<TokenResponse> {
|
||||
const body = new URLSearchParams({
|
||||
grant_type: 'client_credentials',
|
||||
client_id: this.clientId,
|
||||
client_secret: this.clientSecret,
|
||||
scope: this.scopes,
|
||||
});
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/api/v1/token`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: body.toString(),
|
||||
});
|
||||
|
||||
const data = (await response.json()) as Record<string, unknown>;
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = data as { error: string; error_description: string };
|
||||
throw AgentIdPError.fromOAuth2Error(
|
||||
{ error: String(errorBody.error ?? 'unknown_error'), error_description: String(errorBody.error_description ?? 'Token request failed.') },
|
||||
response.status,
|
||||
);
|
||||
}
|
||||
|
||||
return data as unknown as TokenResponse;
|
||||
}
|
||||
|
||||
/** Clears the cached token, forcing re-acquisition on next getToken() call. */
|
||||
clearCache(): void {
|
||||
this.cached = null;
|
||||
}
|
||||
}
|
||||
217
sdk/src/types.ts
Normal file
217
sdk/src/types.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* TypeScript types for the SentryAgent.ai AgentIdP SDK.
|
||||
* All request and response shapes derived from the AgentIdP OpenAPI 3.0 specs.
|
||||
*/
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Shared enums
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Functional classification of an AI agent. */
|
||||
export type AgentType =
|
||||
| 'screener'
|
||||
| 'classifier'
|
||||
| 'orchestrator'
|
||||
| 'extractor'
|
||||
| 'summarizer'
|
||||
| 'router'
|
||||
| 'monitor'
|
||||
| 'custom';
|
||||
|
||||
/** Lifecycle status of an AI agent. */
|
||||
export type AgentStatus = 'active' | 'suspended' | 'decommissioned';
|
||||
|
||||
/** Target deployment environment. */
|
||||
export type DeploymentEnv = 'development' | 'staging' | 'production';
|
||||
|
||||
/** Lifecycle status of a credential. */
|
||||
export type CredentialStatus = 'active' | 'revoked';
|
||||
|
||||
/** OAuth 2.0 scopes supported by AgentIdP. */
|
||||
export type OAuthScope = 'agents:read' | 'agents:write' | 'tokens:read' | 'audit:read';
|
||||
|
||||
/** Audit event action types. */
|
||||
export type AuditAction =
|
||||
| 'agent.created'
|
||||
| 'agent.updated'
|
||||
| 'agent.decommissioned'
|
||||
| 'agent.suspended'
|
||||
| 'agent.reactivated'
|
||||
| 'token.issued'
|
||||
| 'token.revoked'
|
||||
| 'token.introspected'
|
||||
| 'credential.generated'
|
||||
| 'credential.rotated'
|
||||
| 'credential.revoked'
|
||||
| 'auth.failed';
|
||||
|
||||
/** Outcome of an audited action. */
|
||||
export type AuditOutcome = 'success' | 'failure';
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Agent Registry
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** A registered AI agent identity. */
|
||||
export interface Agent {
|
||||
agentId: string;
|
||||
email: string;
|
||||
agentType: AgentType;
|
||||
version: string;
|
||||
capabilities: string[];
|
||||
owner: string;
|
||||
deploymentEnv: DeploymentEnv;
|
||||
status: AgentStatus;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** Request body for registering a new agent. */
|
||||
export interface RegisterAgentRequest {
|
||||
email: string;
|
||||
agentType: AgentType;
|
||||
version: string;
|
||||
capabilities: string[];
|
||||
owner: string;
|
||||
deploymentEnv: DeploymentEnv;
|
||||
}
|
||||
|
||||
/** Request body for updating agent metadata (all fields optional). */
|
||||
export interface UpdateAgentRequest {
|
||||
agentType?: AgentType;
|
||||
version?: string;
|
||||
capabilities?: string[];
|
||||
owner?: string;
|
||||
deploymentEnv?: DeploymentEnv;
|
||||
status?: AgentStatus;
|
||||
}
|
||||
|
||||
/** Query parameters for listing agents. */
|
||||
export interface ListAgentsParams {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
owner?: string;
|
||||
agentType?: AgentType;
|
||||
status?: AgentStatus;
|
||||
}
|
||||
|
||||
/** Paginated list of agents. */
|
||||
export interface PaginatedAgents {
|
||||
data: Agent[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Credential Management
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** A credential record (clientSecret never included). */
|
||||
export interface Credential {
|
||||
credentialId: string;
|
||||
clientId: string;
|
||||
status: CredentialStatus;
|
||||
createdAt: string;
|
||||
expiresAt: string | null;
|
||||
revokedAt: string | null;
|
||||
}
|
||||
|
||||
/** A credential record with the plain-text secret — returned once only on create/rotate. */
|
||||
export interface CredentialWithSecret extends Credential {
|
||||
clientSecret: string;
|
||||
}
|
||||
|
||||
/** Optional request body for generating or rotating a credential. */
|
||||
export interface GenerateCredentialRequest {
|
||||
expiresAt?: string;
|
||||
}
|
||||
|
||||
/** Query parameters for listing credentials. */
|
||||
export interface ListCredentialsParams {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
status?: CredentialStatus;
|
||||
}
|
||||
|
||||
/** Paginated list of credentials. */
|
||||
export interface PaginatedCredentials {
|
||||
data: Credential[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// OAuth 2.0 Tokens
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** OAuth 2.0 access token response. */
|
||||
export interface TokenResponse {
|
||||
access_token: string;
|
||||
token_type: 'Bearer';
|
||||
expires_in: number;
|
||||
scope: string;
|
||||
}
|
||||
|
||||
/** Token introspection response (RFC 7662). */
|
||||
export interface IntrospectResponse {
|
||||
active: boolean;
|
||||
sub?: string;
|
||||
client_id?: string;
|
||||
scope?: string;
|
||||
token_type?: string;
|
||||
iat?: number;
|
||||
exp?: number;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Audit Log
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** An immutable audit event record. */
|
||||
export interface AuditEvent {
|
||||
eventId: string;
|
||||
agentId: string;
|
||||
action: AuditAction;
|
||||
outcome: AuditOutcome;
|
||||
ipAddress: string;
|
||||
userAgent: string;
|
||||
metadata: Record<string, unknown>;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
/** Query parameters for the audit log. */
|
||||
export interface QueryAuditLogParams {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
agentId?: string;
|
||||
action?: AuditAction;
|
||||
outcome?: AuditOutcome;
|
||||
fromDate?: string;
|
||||
toDate?: string;
|
||||
}
|
||||
|
||||
/** Paginated list of audit events. */
|
||||
export interface PaginatedAuditEvents {
|
||||
data: AuditEvent[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// SDK Config
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Configuration for AgentIdPClient. */
|
||||
export interface AgentIdPClientConfig {
|
||||
/** Base URL of the AgentIdP server, e.g. http://localhost:3000/api/v1 */
|
||||
baseUrl: string;
|
||||
/** The agent's clientId (agentId UUID). */
|
||||
clientId: string;
|
||||
/** The agent's clientSecret. */
|
||||
clientSecret: string;
|
||||
/** OAuth 2.0 scopes to request. Defaults to all scopes. */
|
||||
scopes?: OAuthScope[];
|
||||
}
|
||||
Reference in New Issue
Block a user