@barndoor-ai/sdk 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,272 @@
1
+ /**
2
+ * HTTP client with retry logic and error handling.
3
+ *
4
+ * This module provides a robust HTTP client that mirrors the Python SDK's
5
+ * HTTP client functionality, including automatic retries, timeout handling,
6
+ * and proper error conversion.
7
+ */
8
+
9
+ import { HTTPError, ConnectionError, TimeoutError } from '../exceptions';
10
+
11
+ /**
12
+ * HTTP request options interface.
13
+ */
14
+ export interface HTTPRequestOptions {
15
+ /** Request headers */
16
+ headers?: Record<string, string>;
17
+ /** JSON body to send */
18
+ json?: unknown;
19
+ /** Query parameters */
20
+ params?: Record<string, string | number | boolean>;
21
+ /** Additional fetch options */
22
+ [key: string]: unknown;
23
+ }
24
+
25
+ /**
26
+ * Timeout configuration for HTTP requests.
27
+ */
28
+ export class TimeoutConfig {
29
+ /** Read timeout in milliseconds */
30
+ public readonly read: number;
31
+ /** Connect timeout in milliseconds */
32
+ public readonly connect: number;
33
+
34
+ /**
35
+ * Create a new TimeoutConfig.
36
+ * @param read - Read timeout in seconds
37
+ * @param connect - Connect timeout in seconds
38
+ */
39
+ constructor(read = 30, connect = 10) {
40
+ this.read = read * 1000; // Convert to milliseconds
41
+ this.connect = connect * 1000; // Convert to milliseconds
42
+ }
43
+ }
44
+
45
+ /**
46
+ * HTTP client with automatic retries and error handling.
47
+ *
48
+ * Provides a consistent interface for making HTTP requests with proper
49
+ * error handling, timeout management, and retry logic.
50
+ */
51
+ export class HTTPClient {
52
+ /** Timeout configuration */
53
+ private readonly timeoutConfig: TimeoutConfig;
54
+ /** Maximum number of retries */
55
+ private readonly maxRetries: number;
56
+ /** Whether the client has been closed */
57
+ public closed: boolean;
58
+
59
+ /**
60
+ * Create a new HTTPClient.
61
+ * @param timeoutConfig - Timeout configuration
62
+ * @param maxRetries - Maximum number of retries
63
+ */
64
+ constructor(timeoutConfig = new TimeoutConfig(), maxRetries = 3) {
65
+ this.timeoutConfig = timeoutConfig;
66
+ this.maxRetries = maxRetries;
67
+ this.closed = false;
68
+ }
69
+
70
+ /**
71
+ * Make an HTTP request with retry logic.
72
+ * @param method - HTTP method
73
+ * @param url - Request URL
74
+ * @param options - Request options
75
+ * @returns Response data
76
+ */
77
+ public async request(
78
+ method: string,
79
+ url: string,
80
+ options: HTTPRequestOptions = {}
81
+ ): Promise<unknown> {
82
+ if (this.closed) {
83
+ throw new Error('HTTP client has been closed');
84
+ }
85
+
86
+ const { headers = {}, json, params, ...fetchOptions } = options;
87
+
88
+ // Build URL with query parameters
89
+ const requestUrl = this._buildUrl(url, params);
90
+
91
+ // Prepare request options
92
+ const requestOptions: RequestInit = {
93
+ method: method.toUpperCase(),
94
+ headers: {
95
+ 'Content-Type': 'application/json',
96
+ 'User-Agent': 'barndoor-js-sdk/0.1.0',
97
+ ...headers,
98
+ },
99
+ ...fetchOptions,
100
+ };
101
+
102
+ // Add request body if provided
103
+ if (json) {
104
+ requestOptions.body = JSON.stringify(json);
105
+ }
106
+
107
+ let lastError: Error | undefined;
108
+
109
+ // Retry loop
110
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
111
+ // Create fresh AbortController and timeout for each attempt
112
+ const controller = new AbortController();
113
+
114
+ // Set up overall request timeout (covers both connection and read)
115
+ const totalTimeout = this.timeoutConfig.connect + this.timeoutConfig.read;
116
+ const timeoutId = setTimeout(() => controller.abort(), totalTimeout);
117
+
118
+ // Update signal for this attempt
119
+ const attemptOptions = { ...requestOptions, signal: controller.signal };
120
+
121
+ try {
122
+ const response = await globalThis.fetch!(requestUrl, attemptOptions as RequestInit);
123
+ clearTimeout(timeoutId);
124
+
125
+ // Handle HTTP errors
126
+ if (!response.ok) {
127
+ let responseText = '';
128
+ try {
129
+ const respAny: any = response as any;
130
+ if (typeof respAny.text === 'function') {
131
+ responseText = await respAny.text();
132
+ } else if (typeof respAny.json === 'function') {
133
+ // Fallback to JSON serialization if text() is not available on mocks
134
+ responseText = JSON.stringify(await respAny.json());
135
+ }
136
+ } catch {
137
+ // ignore parse errors, keep empty responseText
138
+ }
139
+ throw new HTTPError(response.status, response.statusText, responseText);
140
+ }
141
+
142
+ // Parse response based on Content-Type with robust fallbacks for test mocks
143
+ let contentType = '';
144
+ const respAny: any = response as any;
145
+ const headersObj: any = respAny && respAny.headers;
146
+ if (headersObj) {
147
+ if (typeof headersObj.get === 'function') {
148
+ contentType = headersObj.get('content-type') || headersObj.get('Content-Type') || '';
149
+ } else if (typeof headersObj === 'object') {
150
+ contentType = headersObj['content-type'] || headersObj['Content-Type'] || '';
151
+ }
152
+ }
153
+
154
+ let responseData: unknown;
155
+ if (
156
+ contentType.includes('application/json') ||
157
+ (!contentType && typeof respAny.json === 'function')
158
+ ) {
159
+ responseData = await response.json();
160
+ } else if (
161
+ contentType.includes('text/') ||
162
+ (!contentType && typeof respAny.text === 'function')
163
+ ) {
164
+ responseData = await response.text();
165
+ } else if (typeof respAny.arrayBuffer === 'function') {
166
+ // For binary data or unknown types, return as ArrayBuffer
167
+ responseData = await response.arrayBuffer();
168
+ } else {
169
+ // Last resort: return the raw response object
170
+ responseData = response as unknown;
171
+ }
172
+
173
+ return responseData;
174
+ } catch (error: unknown) {
175
+ clearTimeout(timeoutId);
176
+
177
+ // Handle different types of errors
178
+ if (error instanceof Error && error.name === 'AbortError') {
179
+ const totalTimeout = this.timeoutConfig.connect + this.timeoutConfig.read;
180
+ lastError = new TimeoutError(
181
+ `Request to ${requestUrl} timed out after ${totalTimeout}ms`
182
+ );
183
+ } else if (error instanceof HTTPError) {
184
+ // Distinguish retryable 5xx from non-retryable 4xx errors
185
+ if (error.statusCode >= 400 && error.statusCode < 500) {
186
+ // 4xx errors are client errors - don't retry
187
+ throw error;
188
+ } else if (error.statusCode >= 500 && error.statusCode < 600) {
189
+ // Retry 5xx errors only for idempotent methods (GET, HEAD, OPTIONS)
190
+ const methodUpper = method.toUpperCase();
191
+ const isIdempotent =
192
+ methodUpper === 'GET' || methodUpper === 'HEAD' || methodUpper === 'OPTIONS';
193
+ if (isIdempotent) {
194
+ lastError = error;
195
+ } else {
196
+ throw error;
197
+ }
198
+ } else {
199
+ // Other HTTP errors - don't retry
200
+ throw error;
201
+ }
202
+ } else if (
203
+ error instanceof Error &&
204
+ error.name === 'TypeError' &&
205
+ error.message.includes('fetch')
206
+ ) {
207
+ lastError = new ConnectionError(requestUrl, error);
208
+ } else {
209
+ lastError = error instanceof Error ? error : new Error(String(error));
210
+ }
211
+
212
+ // Don't retry on the last attempt
213
+ if (attempt === this.maxRetries) {
214
+ break;
215
+ }
216
+
217
+ // Wait before retrying (exponential backoff)
218
+ const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
219
+ await this._sleep(delay);
220
+ }
221
+ }
222
+
223
+ // Ensure we always have an error to throw
224
+ if (!lastError) {
225
+ lastError = new Error(
226
+ `Request to ${requestUrl} failed after ${this.maxRetries + 1} attempts with no specific error`
227
+ );
228
+ }
229
+ throw lastError;
230
+ }
231
+
232
+ /**
233
+ * Build URL with query parameters.
234
+ * @private
235
+ */
236
+ private _buildUrl(baseUrl: string, params?: Record<string, string | number | boolean>): string {
237
+ if (!params || Object.keys(params).length === 0) {
238
+ return baseUrl;
239
+ }
240
+
241
+ const url = new URL(baseUrl);
242
+ Object.entries(params).forEach(([key, value]) => {
243
+ if (value !== null && value !== undefined) {
244
+ url.searchParams.append(key, String(value));
245
+ }
246
+ });
247
+
248
+ return url.toString();
249
+ }
250
+
251
+ /**
252
+ * Sleep for the specified number of milliseconds.
253
+ * @private
254
+ */
255
+ private _sleep(ms: number): Promise<void> {
256
+ return new Promise(resolve => setTimeout(resolve, ms));
257
+ }
258
+
259
+ /**
260
+ * Close the HTTP client and clean up resources.
261
+ */
262
+ public async close(): Promise<void> {
263
+ this.closed = true;
264
+ }
265
+
266
+ /**
267
+ * Alias for close() to match Python SDK naming.
268
+ */
269
+ public async aclose(): Promise<void> {
270
+ await this.close();
271
+ }
272
+ }
package/src/index.ts ADDED
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Barndoor SDK - JavaScript client for the Barndoor Platform API.
3
+ *
4
+ * The Barndoor SDK provides a simple, async interface for interacting with
5
+ * the Barndoor platform, including:
6
+ *
7
+ * - User authentication and token management
8
+ * - MCP server discovery and connection
9
+ * - OAuth flow handling for third-party integrations
10
+ * - Agent credential exchange
11
+ *
12
+ * Quick Start
13
+ * -----------
14
+ * ```javascript
15
+ * import { BarndoorSDK } from '@barndoor/sdk';
16
+ *
17
+ * const sdk = new BarndoorSDK('https://api.barndoor.host', {
18
+ * token: 'your_token'
19
+ * });
20
+ * const servers = await sdk.listServers();
21
+ * ```
22
+ *
23
+ * For interactive login:
24
+ * ```javascript
25
+ * import { loginInteractive } from '@barndoor/sdk';
26
+ *
27
+ * const sdk = await loginInteractive();
28
+ * ```
29
+ */
30
+
31
+ // Main SDK class
32
+ export { BarndoorSDK } from './client';
33
+
34
+ // Exception classes
35
+ export {
36
+ BarndoorError,
37
+ AuthenticationError,
38
+ TokenError,
39
+ TokenExpiredError,
40
+ TokenValidationError,
41
+ ConnectionError,
42
+ HTTPError,
43
+ ServerNotFoundError,
44
+ OAuthError,
45
+ ConfigurationError,
46
+ TimeoutError,
47
+ } from './exceptions';
48
+
49
+ // Data models
50
+ export { ServerSummary, ServerDetail, AgentToken } from './models';
51
+
52
+ // Quick-start helpers
53
+ export {
54
+ loginInteractive,
55
+ ensureServerConnected,
56
+ makeMcpConnectionParams,
57
+ makeMcpClient,
58
+ } from './quickstart';
59
+
60
+ // Authentication utilities
61
+ export {
62
+ PKCEManager,
63
+ startLocalCallbackServer,
64
+ loadUserToken,
65
+ saveUserToken,
66
+ clearCachedToken,
67
+ verifyJWTLocal,
68
+ JWTVerificationResult,
69
+ isTokenActive,
70
+ isTokenActiveWithRefresh,
71
+ validateToken,
72
+ TokenManager,
73
+ setTokenLogger,
74
+ } from './auth';
75
+
76
+ // Configuration
77
+ export {
78
+ BarndoorConfig,
79
+ getStaticConfig,
80
+ getDynamicConfig,
81
+ checkTokenOrganization,
82
+ hasOrganizationInfo,
83
+ isBrowser,
84
+ isNode,
85
+ } from './config';
86
+
87
+ // Logging
88
+ export { setLogger, getLogger, createScopedLogger, debug, info, warn, error } from './logging';
89
+ export type { Logger } from './logging';
90
+
91
+ // Version
92
+ export { version } from './version';
package/src/logging.ts ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Unified logging system for the Barndoor SDK.
3
+ *
4
+ * This module provides a centralized logging interface that can be configured
5
+ * by SDK consumers to integrate with their preferred logging systems.
6
+ */
7
+
8
+ /**
9
+ * Logger interface for SDK consumers to plug their own logger.
10
+ */
11
+ export interface Logger {
12
+ debug(message: string, ...args: unknown[]): void;
13
+ info(message: string, ...args: unknown[]): void;
14
+ warn(message: string, ...args: unknown[]): void;
15
+ error(message: string, ...args: unknown[]): void;
16
+ }
17
+
18
+ /**
19
+ * Default console logger implementation.
20
+ */
21
+ const defaultLogger: Logger = {
22
+ debug: (message: string, ...args: unknown[]) => {
23
+ if (typeof console !== 'undefined' && console.debug) {
24
+ console.debug(message, ...args);
25
+ }
26
+ },
27
+ info: (message: string, ...args: unknown[]) => {
28
+ if (typeof console !== 'undefined' && console.info) {
29
+ console.info(message, ...args);
30
+ }
31
+ },
32
+ warn: (message: string, ...args: unknown[]) => {
33
+ if (typeof console !== 'undefined' && console.warn) {
34
+ console.warn(message, ...args);
35
+ }
36
+ },
37
+ error: (message: string, ...args: unknown[]) => {
38
+ if (typeof console !== 'undefined' && console.error) {
39
+ console.error(message, ...args);
40
+ }
41
+ },
42
+ };
43
+
44
+ // Global logger instance that can be configured
45
+ let _logger: Logger = defaultLogger;
46
+
47
+ /**
48
+ * Set a custom logger for the entire SDK.
49
+ * @param logger - Custom logger implementation
50
+ */
51
+ export function setLogger(logger: Logger): void {
52
+ _logger = logger;
53
+ }
54
+
55
+ /**
56
+ * Get the current logger instance.
57
+ * @returns Current logger
58
+ */
59
+ export function getLogger(): Logger {
60
+ return _logger;
61
+ }
62
+
63
+ /**
64
+ * Create a scoped logger with a prefix for better organization.
65
+ * @param scope - Scope name (e.g., 'client', 'auth', 'http')
66
+ * @returns Scoped logger
67
+ */
68
+ export function createScopedLogger(scope: string): Logger {
69
+ return {
70
+ debug: (message: string, ...args: unknown[]) => {
71
+ _logger.debug(`[${scope}] ${message}`, ...args);
72
+ },
73
+ info: (message: string, ...args: unknown[]) => {
74
+ _logger.info(`[${scope}] ${message}`, ...args);
75
+ },
76
+ warn: (message: string, ...args: unknown[]) => {
77
+ _logger.warn(`[${scope}] ${message}`, ...args);
78
+ },
79
+ error: (message: string, ...args: unknown[]) => {
80
+ _logger.error(`[${scope}] ${message}`, ...args);
81
+ },
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Convenience function to log debug messages.
87
+ */
88
+ export function debug(message: string, ...args: unknown[]): void {
89
+ _logger.debug(message, ...args);
90
+ }
91
+
92
+ /**
93
+ * Convenience function to log info messages.
94
+ */
95
+ export function info(message: string, ...args: unknown[]): void {
96
+ _logger.info(message, ...args);
97
+ }
98
+
99
+ /**
100
+ * Convenience function to log warning messages.
101
+ */
102
+ export function warn(message: string, ...args: unknown[]): void {
103
+ _logger.warn(message, ...args);
104
+ }
105
+
106
+ /**
107
+ * Convenience function to log error messages.
108
+ */
109
+ export function error(message: string, ...args: unknown[]): void {
110
+ _logger.error(message, ...args);
111
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Data models for the Barndoor SDK.
3
+ *
4
+ * This module defines the data models used for API requests and responses,
5
+ * providing type safety and validation that mirrors the Python SDK's Pydantic models.
6
+ */
7
+
8
+ /**
9
+ * Connection status for MCP servers.
10
+ */
11
+ export type ConnectionStatus = 'available' | 'pending' | 'connected';
12
+
13
+ /**
14
+ * Raw server data from API responses.
15
+ */
16
+ export interface ServerSummaryData {
17
+ /** Unique identifier (UUID) for the server */
18
+ id: string;
19
+ /** Human-readable name of the server */
20
+ name: string;
21
+ /** URL-friendly identifier used in API paths */
22
+ slug: string;
23
+ /** Third-party provider name (e.g., "github", "slack") */
24
+ provider?: string | null;
25
+ /** Current connection status */
26
+ connection_status: ConnectionStatus;
27
+ }
28
+
29
+ /**
30
+ * Summary information about an MCP server.
31
+ *
32
+ * Represents basic server information as returned by the list servers
33
+ * endpoint. This is a lightweight representation suitable for listing
34
+ * many servers at once.
35
+ */
36
+ export class ServerSummary {
37
+ /** Unique identifier (UUID) for the server */
38
+ public readonly id: string;
39
+ /** Human-readable name of the server */
40
+ public readonly name: string;
41
+ /** URL-friendly identifier used in API paths */
42
+ public readonly slug: string;
43
+ /** Third-party provider name (e.g., "github", "slack") */
44
+ public readonly provider: string | null;
45
+ /** Current connection status */
46
+ public readonly connection_status: ConnectionStatus;
47
+
48
+ /**
49
+ * Create a new ServerSummary instance.
50
+ * @param data - Server data from API response
51
+ */
52
+ constructor(data: ServerSummaryData) {
53
+ this.id = data.id;
54
+ this.name = data.name;
55
+ this.slug = data.slug;
56
+ this.provider = data.provider ?? null;
57
+ this.connection_status = data.connection_status;
58
+
59
+ // Validate required fields
60
+ if (!this.id || !this.name || !this.slug || !this.connection_status) {
61
+ throw new Error('ServerSummary missing required fields');
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Create a ServerSummary from API response data.
67
+ * @param data - Raw API response data
68
+ * @returns ServerSummary instance
69
+ */
70
+ public static fromApiResponse(data: unknown): ServerSummary {
71
+ return new ServerSummary(data as ServerSummaryData);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Raw server detail data from API responses.
77
+ */
78
+ export interface ServerDetailData extends ServerSummaryData {
79
+ /** MCP base URL from the server directory */
80
+ url?: string | null;
81
+ }
82
+
83
+ /**
84
+ * Detailed information about an MCP server.
85
+ *
86
+ * Extends ServerSummary with additional fields returned when fetching
87
+ * a single server's details.
88
+ */
89
+ export class ServerDetail extends ServerSummary {
90
+ /** MCP base URL from the server directory */
91
+ public readonly url: string | null;
92
+
93
+ /**
94
+ * Create a new ServerDetail instance.
95
+ * @param data - Server data from API response
96
+ */
97
+ constructor(data: ServerDetailData) {
98
+ super(data);
99
+ this.url = data.url ?? null;
100
+ }
101
+
102
+ /**
103
+ * Create a ServerDetail from API response data.
104
+ * @param data - Raw API response data
105
+ * @returns ServerDetail instance
106
+ */
107
+ public static override fromApiResponse(data: unknown): ServerDetail {
108
+ return new ServerDetail(data as ServerDetailData);
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Raw agent token data from API responses.
114
+ */
115
+ export interface AgentTokenData {
116
+ /** The agent access token to use for agent operations */
117
+ agent_token: string;
118
+ /** Token lifetime in seconds */
119
+ expires_in: number;
120
+ }
121
+
122
+ /**
123
+ * Response from the agent token exchange endpoint.
124
+ *
125
+ * Contains the agent access token and expiration information returned
126
+ * when exchanging client credentials.
127
+ */
128
+ export class AgentToken {
129
+ /** The agent access token to use for agent operations */
130
+ public readonly agent_token: string;
131
+ /** Token lifetime in seconds */
132
+ public readonly expires_in: number;
133
+
134
+ /**
135
+ * Create a new AgentToken instance.
136
+ * @param data - Token data from API response
137
+ */
138
+ constructor(data: AgentTokenData) {
139
+ this.agent_token = data.agent_token;
140
+ this.expires_in = data.expires_in;
141
+
142
+ // Validate required fields
143
+ if (!this.agent_token || typeof this.expires_in !== 'number') {
144
+ throw new Error('AgentToken missing required fields');
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Create an AgentToken from API response data.
150
+ * @param data - Raw API response data
151
+ * @returns AgentToken instance
152
+ */
153
+ public static fromApiResponse(data: unknown): AgentToken {
154
+ return new AgentToken(data as AgentTokenData);
155
+ }
156
+ }