@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.
- package/.eslintignore +8 -0
- package/.eslintrc.cjs +102 -0
- package/.github/CODEOWNERS +4 -0
- package/.github/workflows/ci.yml +57 -0
- package/.prettierignore +6 -0
- package/.prettierrc +13 -0
- package/LICENSE +21 -0
- package/README.md +309 -0
- package/RELEASE.md +203 -0
- package/examples/README.md +92 -0
- package/examples/basic-mcp-client.js +134 -0
- package/examples/openai-integration.js +137 -0
- package/jest.config.js +16 -0
- package/openapi.yaml +681 -0
- package/package.json +87 -0
- package/rollup.config.js +63 -0
- package/scripts/dump-core-files.js +161 -0
- package/scripts/dump-typescript-only.js +150 -0
- package/src/auth/index.ts +26 -0
- package/src/auth/pkce.ts +346 -0
- package/src/auth/store.ts +809 -0
- package/src/client.ts +512 -0
- package/src/config.ts +402 -0
- package/src/exceptions/index.ts +205 -0
- package/src/http/client.ts +272 -0
- package/src/index.ts +92 -0
- package/src/logging.ts +111 -0
- package/src/models/index.ts +156 -0
- package/src/quickstart.ts +358 -0
- package/src/version.ts +41 -0
- package/test/client.test.js +381 -0
- package/test/config.test.js +202 -0
- package/test/exceptions.test.js +142 -0
- package/test/integration.test.js +147 -0
- package/test/models.test.js +177 -0
- package/test/token-management.test.js +81 -0
- package/test/token-validation.test.js +104 -0
- package/tsconfig.json +61 -0
|
@@ -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();
|