@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,358 @@
1
+ /**
2
+ * Quick-start helpers for the Barndoor SDK.
3
+ *
4
+ * This module provides convenience functions that remove boilerplate code
5
+ * commonly needed in examples and prototypes, mirroring the Python SDK's
6
+ * quickstart.py functionality.
7
+ */
8
+
9
+ import { BarndoorSDK } from './client';
10
+ import { PKCEManager, startLocalCallbackServer } from './auth';
11
+ import { loadUserToken, saveUserToken } from './auth';
12
+ import { getStaticConfig, getDynamicConfig, hasOrganizationInfo, isNode } from './config';
13
+ import { ServerNotFoundError } from './exceptions';
14
+ import { createScopedLogger } from './logging';
15
+ import { spawn } from 'child_process';
16
+ import os from 'os';
17
+ import crypto from 'crypto';
18
+ import { Client as McpClient } from '@modelcontextprotocol/sdk/client/index.js';
19
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
20
+
21
+ // Create scoped logger for quickstart functions
22
+ const logger = createScopedLogger('quickstart');
23
+
24
+ /**
25
+ * Perform interactive login and return an initialized SDK instance.
26
+ *
27
+ * Opens the system browser for OAuth authentication, waits for the
28
+ * user to complete login, exchanges the authorization code for a JWT,
29
+ * and returns a configured BarndoorSDK instance ready for use.
30
+ *
31
+ * @param {Object} [options={}] - Login options
32
+ * @param {string} [options.authDomain] - Auth0 domain
33
+ * @param {string} [options.clientId] - OAuth client ID
34
+ * @param {string} [options.clientSecret] - OAuth client secret
35
+ * @param {string} [options.audience] - API audience identifier
36
+ * @param {string} [options.apiBaseUrl] - Base URL of the Barndoor API
37
+ * @param {number} [options.port=52765] - Local port for OAuth callback
38
+ * @returns {Promise<BarndoorSDK>} Initialized SDK instance
39
+ */
40
+ /**
41
+ * Login options interface.
42
+ */
43
+ export interface LoginInteractiveOptions {
44
+ /** Auth0 domain */
45
+ authDomain?: string;
46
+ /** OAuth client ID */
47
+ clientId?: string;
48
+ /** OAuth client secret */
49
+ clientSecret?: string;
50
+ /** API audience identifier */
51
+ audience?: string;
52
+ /** Base URL of the Barndoor API */
53
+ apiBaseUrl?: string;
54
+ /** Local port for OAuth callback */
55
+ port?: number;
56
+ }
57
+
58
+ export async function loginInteractive(
59
+ options: LoginInteractiveOptions = {}
60
+ ): Promise<BarndoorSDK> {
61
+ if (!isNode) {
62
+ throw new Error('Interactive login is only available in Node.js environment');
63
+ }
64
+
65
+ logger.info('Starting interactive login flow');
66
+
67
+ const config = getStaticConfig();
68
+
69
+ const {
70
+ authDomain = config.authDomain,
71
+ clientId = config.clientId,
72
+ clientSecret = config.clientSecret,
73
+ audience = config.apiAudience,
74
+ apiBaseUrl: _apiBaseUrl = config.apiBaseUrl,
75
+ port = 52765,
76
+ } = options;
77
+
78
+ if (!clientId || !clientSecret) {
79
+ throw new Error(
80
+ 'AGENT_CLIENT_ID / AGENT_CLIENT_SECRET not set – create a .env file or export in the shell'
81
+ );
82
+ }
83
+
84
+ // 1. Try cached token first
85
+ const cachedToken = await loadUserToken();
86
+ if (cachedToken) {
87
+ try {
88
+ // Try to use dynamic config with org ID substitution
89
+ let sdkConfig: ReturnType<typeof getDynamicConfig>;
90
+ if (hasOrganizationInfo(cachedToken)) {
91
+ sdkConfig = getDynamicConfig(cachedToken);
92
+ } else {
93
+ logger.warn('Cached token has no organization information, using static config');
94
+ sdkConfig = getStaticConfig();
95
+ }
96
+
97
+ const sdk = new BarndoorSDK(sdkConfig.apiBaseUrl, { token: cachedToken });
98
+ await sdk.validateCachedToken();
99
+ logger.info('Using cached valid token');
100
+ return sdk;
101
+ } catch (_error) {
102
+ logger.info('Cached token invalid, starting OAuth flow');
103
+ }
104
+ } else {
105
+ logger.info('No cached token, starting OAuth flow');
106
+ }
107
+
108
+ // 2. Start interactive PKCE flow
109
+ const [redirectUri, waiter] = startLocalCallbackServer(port);
110
+
111
+ // Create PKCE manager for this login session
112
+ const pkceManager = new PKCEManager();
113
+ const authUrl = await pkceManager.buildAuthorizationUrl({
114
+ domain: authDomain,
115
+ clientId,
116
+ redirectUri,
117
+ audience,
118
+ });
119
+
120
+ // Open browser
121
+ const platform = os.platform();
122
+
123
+ // Validate URL and open without invoking a shell
124
+ let parsed: URL;
125
+ try {
126
+ parsed = new URL(authUrl);
127
+ } catch {
128
+ throw new Error('Invalid auth URL');
129
+ }
130
+ if (
131
+ parsed.protocol !== 'https:' &&
132
+ !(
133
+ parsed.protocol === 'http:' &&
134
+ (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1')
135
+ )
136
+ ) {
137
+ throw new Error('Auth URL must use HTTPS (http allowed only for localhost)');
138
+ }
139
+
140
+ try {
141
+ if (platform === 'darwin') {
142
+ spawn('open', [authUrl], { detached: true, stdio: 'ignore' }).unref();
143
+ } else if (platform === 'win32') {
144
+ spawn('powershell', ['-NoProfile', 'Start-Process', authUrl], {
145
+ detached: true,
146
+ stdio: 'ignore',
147
+ }).unref();
148
+ } else {
149
+ spawn('xdg-open', [authUrl], { detached: true, stdio: 'ignore' }).unref();
150
+ }
151
+ logger.info('Please complete login in your browser…');
152
+ } catch (_error) {
153
+ logger.warn('Failed to open browser automatically. Please visit:', authUrl);
154
+ }
155
+
156
+ // Wait for callback
157
+ const [code, _state] = await waiter;
158
+
159
+ // Exchange code for token
160
+ const tokenData = (await pkceManager.exchangeCodeForToken({
161
+ domain: authDomain,
162
+ clientId,
163
+ clientSecret,
164
+ code,
165
+ redirectUri,
166
+ })) as { access_token: string; [key: string]: unknown };
167
+
168
+ // Save token and create SDK
169
+ await saveUserToken(tokenData);
170
+
171
+ // Try to use dynamic config with org ID substitution
172
+ let sdkConfig: ReturnType<typeof getDynamicConfig>;
173
+ if (hasOrganizationInfo(tokenData.access_token)) {
174
+ sdkConfig = getDynamicConfig(tokenData.access_token);
175
+ } else {
176
+ logger.warn('New token has no organization information, using static config');
177
+ sdkConfig = getStaticConfig();
178
+ }
179
+
180
+ return new BarndoorSDK(sdkConfig.apiBaseUrl, { token: tokenData.access_token });
181
+ }
182
+
183
+ /**
184
+ * Ensure a server is connected, with user-friendly logging.
185
+ *
186
+ * This is a convenience wrapper around sdk.ensureServerConnected() that adds
187
+ * helpful console output for interactive use. The actual connection logic
188
+ * is handled by the SDK method to avoid code duplication.
189
+ *
190
+ * @param {BarndoorSDK} sdk - SDK instance
191
+ * @param {string} serverIdentifier - Server slug or provider name
192
+ * @param {Object} [options={}] - Options
193
+ * @param {number} [options.timeout=90] - Maximum seconds to wait
194
+ */
195
+ export async function ensureServerConnected(
196
+ sdk: BarndoorSDK,
197
+ serverIdentifier: string,
198
+ options: { timeout?: number } = {}
199
+ ): Promise<void> {
200
+ const { timeout = 90 } = options;
201
+
202
+ logger.info(`Ensuring ${serverIdentifier} server is connected`);
203
+
204
+ try {
205
+ // Use the SDK method directly - it handles all the logic including server lookup
206
+ await sdk.ensureServerConnected(serverIdentifier, { pollSeconds: timeout });
207
+ logger.info(`Server ${serverIdentifier} connected successfully`);
208
+ } catch (error) {
209
+ if (error instanceof ServerNotFoundError) {
210
+ logger.error(`Server '${serverIdentifier}' not found`);
211
+ } else {
212
+ logger.error(`Failed to connect to ${serverIdentifier}:`, error);
213
+ }
214
+ throw error;
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Create MCP connection parameters for a server.
220
+ *
221
+ * Returns connection parameters that can be used with any MCP client
222
+ * framework (CrewAI, LangChain, custom implementations).
223
+ *
224
+ * @param {BarndoorSDK} sdk - SDK instance
225
+ * @param {string} serverSlug - Server slug
226
+ * @param {Object} [options={}] - Options
227
+ * @param {string} [options.proxyBaseUrl='http://proxy-ingress:8080'] - Proxy base URL
228
+ * @param {string} [options.transport='streamable-http'] - Transport type
229
+ * @returns {Promise<[Object, string]>} [params, publicUrl]
230
+ */
231
+ export async function makeMcpConnectionParams(
232
+ sdk: BarndoorSDK,
233
+ serverSlug: string,
234
+ options: { proxyBaseUrl?: string; transport?: string } = {}
235
+ ): Promise<[unknown, string]> {
236
+ const {
237
+ proxyBaseUrl: _proxyBaseUrl = 'http://proxy-ingress:8080',
238
+ transport = 'streamable-http',
239
+ } = options;
240
+
241
+ // 1. Ensure server exists
242
+ const servers = await sdk.listServers();
243
+ const serverSlugs = new Set(servers.map(s => s.slug));
244
+
245
+ if (!serverSlugs.has(serverSlug)) {
246
+ throw new ServerNotFoundError(serverSlug, Array.from(serverSlugs));
247
+ }
248
+
249
+ // 2. Decide proxy vs public based on environment
250
+ const env = (isNode ? process.env['BARNDOOR_ENV'] || process.env['MODE'] : '') || 'localdev';
251
+
252
+ let url: string;
253
+ if (['localdev', 'local', 'development', 'dev'].includes(env.toLowerCase())) {
254
+ // Use dynamic configuration for local/dev environments
255
+ if (hasOrganizationInfo(sdk.token)) {
256
+ const dynamicConfig = getDynamicConfig(sdk.token);
257
+ url = `${dynamicConfig.mcpBaseUrl}/mcp/${serverSlug}`;
258
+ } else {
259
+ logger.warn('Token has no organization information, using static config for MCP connection');
260
+ const staticConfig = getStaticConfig();
261
+ url = `${staticConfig.mcpBaseUrl}/mcp/${serverSlug}`;
262
+ }
263
+ } else {
264
+ // Production - use external MCP URL (same as dynamic config)
265
+ if (hasOrganizationInfo(sdk.token)) {
266
+ const dynamicConfig = getDynamicConfig(sdk.token);
267
+ url = `${dynamicConfig.mcpBaseUrl}/mcp/${serverSlug}`;
268
+ } else {
269
+ logger.warn('Token has no organization information, using static config for MCP connection');
270
+ const staticConfig = getStaticConfig();
271
+ url = `${staticConfig.mcpBaseUrl}/mcp/${serverSlug}`;
272
+ }
273
+ }
274
+
275
+ const params = {
276
+ url,
277
+ transport,
278
+ headers: {
279
+ Accept: 'application/json, text/event-stream',
280
+ Authorization: `Bearer ${sdk.token}`,
281
+ 'x-barndoor-session-id': generateSessionId(),
282
+ },
283
+ };
284
+
285
+ return [params, url];
286
+ }
287
+
288
+ /**
289
+ * Create and connect an MCP client for the specified server.
290
+ *
291
+ * This helper uses the official `@modelcontextprotocol/sdk` package so callers
292
+ * don’t need to hand-craft JSON-RPC envelopes or manage transports manually.
293
+ *
294
+ * @param {BarndoorSDK} sdk – An initialized Barndoor SDK instance (must contain a valid JWT in `sdk.token`).
295
+ * @param {string} serverSlug – The server slug (e.g. "salesforce", "notion").
296
+ * @param {Object} [options] – Optional overrides passed to `makeMcpConnectionParams` (proxyBaseUrl, transport).
297
+ * @returns {Promise<McpClient>} A connected MCP client ready for `listTools`, `callTool`, etc.
298
+ */
299
+ export async function makeMcpClient(
300
+ sdk: BarndoorSDK,
301
+ serverSlug: string,
302
+ options: { proxyBaseUrl?: string; transport?: string } = {}
303
+ ): Promise<McpClient> {
304
+ // 1. Build URL + headers via existing helper
305
+ const [mcpParams] = await makeMcpConnectionParams(sdk, serverSlug, options);
306
+ const params = mcpParams as { url: string; headers: Record<string, string> };
307
+
308
+ // 2. Initialise MCP client
309
+ const client = new McpClient({
310
+ name: 'barndoor-js-sdk',
311
+ version: '0.1.0',
312
+ });
313
+
314
+ // 3. Create transport (handles initialize + session negotiation)
315
+ const transport = new StreamableHTTPClientTransport(new URL(params.url), {
316
+ requestInit: {
317
+ headers: params.headers,
318
+ },
319
+ });
320
+
321
+ // 4. Connect (performs `initialize` and session negotiation)
322
+ await client.connect(transport as any);
323
+ return client;
324
+ }
325
+
326
+ /**
327
+ * Generate a UUID v4 session ID.
328
+ * @private
329
+ */
330
+ function generateSessionId() {
331
+ if (isNode && typeof crypto.randomUUID === 'function') {
332
+ return crypto.randomUUID();
333
+ }
334
+ if (typeof globalThis !== 'undefined' && (globalThis as any).crypto?.randomUUID) {
335
+ return (globalThis as any).crypto.randomUUID();
336
+ }
337
+ // Secure fallback: generate UUID from cryptographically strong random bytes
338
+ let bytes: Uint8Array;
339
+ if (isNode && typeof crypto.randomBytes === 'function') {
340
+ bytes = crypto.randomBytes(16);
341
+ } else if (typeof globalThis !== 'undefined' && (globalThis as any).crypto?.getRandomValues) {
342
+ bytes = new Uint8Array(16);
343
+ (globalThis as any).crypto.getRandomValues(bytes);
344
+ } else {
345
+ throw new Error('Secure random generator not available for UUID.');
346
+ }
347
+ // Set version (4) and variant (RFC 4122)
348
+ const arr: Uint8Array = bytes ?? new Uint8Array(0);
349
+ if (arr.length < 16) {
350
+ throw new Error('Secure random generator not available for UUID.');
351
+ }
352
+ const b6 = arr[6]!;
353
+ const b8 = arr[8]!;
354
+ arr[6] = (b6 & 0x0f) | 0x40;
355
+ arr[8] = (b8 & 0x3f) | 0x80;
356
+ const hex = Array.from(arr, b => b.toString(16).padStart(2, '0')).join('');
357
+ return `${hex.substring(0, 8)}-${hex.substring(8, 12)}-${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20)}`;
358
+ }
package/src/version.ts ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Version management for the Barndoor SDK.
3
+ *
4
+ * This module provides a unified way to access the SDK version,
5
+ * reading directly from package.json to avoid duplication.
6
+ */
7
+
8
+ import { readFileSync } from 'fs';
9
+ import { fileURLToPath } from 'url';
10
+ import { dirname, join } from 'path';
11
+
12
+ /**
13
+ * Get the SDK version from package.json.
14
+ * @returns The current SDK version
15
+ */
16
+ export function getVersion(): string {
17
+ try {
18
+ // In Node.js environments, read from package.json
19
+ if (typeof process !== 'undefined' && process.versions?.node) {
20
+ const __filename = fileURLToPath(import.meta.url);
21
+ const __dirname = dirname(__filename);
22
+ const packageJsonPath = join(__dirname, '..', 'package.json');
23
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
24
+ return packageJson.version;
25
+ }
26
+
27
+ // In browser environments, fall back to a build-time constant
28
+ // This will be replaced by the build process
29
+ return '__SDK_VERSION__';
30
+ } catch (error) {
31
+ // Fallback version if package.json can't be read
32
+ console.warn('Could not read version from package.json:', error);
33
+ return '0.1.0';
34
+ }
35
+ }
36
+
37
+ /**
38
+ * SDK version constant for backward compatibility.
39
+ * This is dynamically resolved at runtime.
40
+ */
41
+ export const version = getVersion();