@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
package/src/client.ts
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main Barndoor SDK client.
|
|
3
|
+
*
|
|
4
|
+
* This module provides the primary BarndoorSDK class that mirrors the Python
|
|
5
|
+
* SDK's client.py functionality with 100% API compatibility.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { HTTPClient, TimeoutConfig } from './http/client';
|
|
9
|
+
import { ServerSummary, ServerDetail } from './models';
|
|
10
|
+
import { HTTPError, ConfigurationError, TokenError, ServerNotFoundError } from './exceptions';
|
|
11
|
+
import { getStaticConfig, isNode } from './config';
|
|
12
|
+
import { createScopedLogger } from './logging';
|
|
13
|
+
import { spawn } from 'child_process';
|
|
14
|
+
import os from 'os';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Configuration options for BarndoorSDK constructor.
|
|
18
|
+
*/
|
|
19
|
+
export interface BarndoorSDKOptions {
|
|
20
|
+
/** User JWT token (optional - can be set later via authenticate()) */
|
|
21
|
+
token?: string;
|
|
22
|
+
/** Whether to validate token on initialization */
|
|
23
|
+
validateTokenOnInit?: boolean;
|
|
24
|
+
/** Request timeout in seconds */
|
|
25
|
+
timeout?: number;
|
|
26
|
+
/** Maximum number of retries */
|
|
27
|
+
maxRetries?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Pagination metadata for API responses.
|
|
32
|
+
*/
|
|
33
|
+
interface PaginationMetadata {
|
|
34
|
+
page: number;
|
|
35
|
+
limit: number;
|
|
36
|
+
total: number;
|
|
37
|
+
pages: number;
|
|
38
|
+
previous_page: number | null;
|
|
39
|
+
next_page: number | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Paginated API response structure.
|
|
44
|
+
*/
|
|
45
|
+
interface PaginatedResponse<T> {
|
|
46
|
+
data: T[];
|
|
47
|
+
pagination: PaginationMetadata;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Options for ensureServerConnected method.
|
|
52
|
+
*/
|
|
53
|
+
export interface EnsureServerConnectedOptions {
|
|
54
|
+
/** Maximum seconds to wait for connection */
|
|
55
|
+
pollSeconds?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Response from server connection initiation.
|
|
60
|
+
*/
|
|
61
|
+
export interface ConnectionInitiationResponse {
|
|
62
|
+
/** OAuth authorization URL */
|
|
63
|
+
auth_url?: string;
|
|
64
|
+
[key: string]: unknown;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Response from connection status check.
|
|
69
|
+
*/
|
|
70
|
+
export interface ConnectionStatusResponse {
|
|
71
|
+
/** Current connection status */
|
|
72
|
+
status: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Async client for interacting with the Barndoor Platform API.
|
|
77
|
+
*
|
|
78
|
+
* This SDK provides methods to:
|
|
79
|
+
* - Manage server connections and OAuth flows
|
|
80
|
+
* - List available MCP servers
|
|
81
|
+
* - Validate user tokens
|
|
82
|
+
*
|
|
83
|
+
* The client handles authentication automatically by including the user's
|
|
84
|
+
* JWT token in all requests.
|
|
85
|
+
*/
|
|
86
|
+
export class BarndoorSDK {
|
|
87
|
+
/** Base URL of the Barndoor API */
|
|
88
|
+
public readonly base: string;
|
|
89
|
+
/** User JWT token */
|
|
90
|
+
private _token: string | null;
|
|
91
|
+
/** HTTP client instance */
|
|
92
|
+
private readonly _http: HTTPClient;
|
|
93
|
+
/** Whether token has been validated */
|
|
94
|
+
private _tokenValidated: boolean;
|
|
95
|
+
/** Whether the SDK has been closed */
|
|
96
|
+
private _closed: boolean;
|
|
97
|
+
/** Scoped logger for this SDK instance */
|
|
98
|
+
private readonly _logger = createScopedLogger('client');
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Create a new BarndoorSDK instance.
|
|
102
|
+
* @param apiBaseUrl - Base URL of the Barndoor API
|
|
103
|
+
* @param options - Configuration options (token is optional)
|
|
104
|
+
*/
|
|
105
|
+
constructor(apiBaseUrl: string, options: BarndoorSDKOptions = {}) {
|
|
106
|
+
const { token: barndoorToken, timeout = 30.0, maxRetries = 3 } = options;
|
|
107
|
+
|
|
108
|
+
// Validate inputs
|
|
109
|
+
this.base = this._validateUrl(apiBaseUrl, 'API base URL').replace(/\/$/, '');
|
|
110
|
+
|
|
111
|
+
// Token is optional - can be set later via authenticate(). If provided, validate even if empty string.
|
|
112
|
+
const hasTokenProp = Object.prototype.hasOwnProperty.call(options, 'token');
|
|
113
|
+
this._token = hasTokenProp ? this._validateToken(barndoorToken as unknown as string) : null;
|
|
114
|
+
|
|
115
|
+
// Validate configuration
|
|
116
|
+
if (typeof timeout !== 'number' || timeout <= 0) {
|
|
117
|
+
throw new ConfigurationError('timeout must be a positive number');
|
|
118
|
+
}
|
|
119
|
+
if (!Number.isInteger(maxRetries) || maxRetries < 0) {
|
|
120
|
+
throw new ConfigurationError('maxRetries must be a non-negative integer');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Initialize HTTP client
|
|
124
|
+
const timeoutConfig = new TimeoutConfig(timeout, timeout / 3);
|
|
125
|
+
this._http = new HTTPClient(timeoutConfig, maxRetries);
|
|
126
|
+
this._tokenValidated = false;
|
|
127
|
+
this._closed = false;
|
|
128
|
+
|
|
129
|
+
this._logger.info(`Initialized BarndoorSDK for ${this.base}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get the current token.
|
|
134
|
+
*/
|
|
135
|
+
public get token(): string {
|
|
136
|
+
if (!this._token) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
'No token available. Call authenticate() first or provide token in constructor.'
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return this._token;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Set authentication token for the SDK.
|
|
146
|
+
* @param token - JWT token to use for authentication
|
|
147
|
+
*/
|
|
148
|
+
public async authenticate(token: string): Promise<void> {
|
|
149
|
+
this._token = this._validateToken(token);
|
|
150
|
+
this._tokenValidated = false; // Reset validation status
|
|
151
|
+
|
|
152
|
+
// Optionally validate the token immediately
|
|
153
|
+
await this.ensureValidToken();
|
|
154
|
+
|
|
155
|
+
this._logger.info('Authentication successful');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Validate URL format.
|
|
160
|
+
* @private
|
|
161
|
+
*/
|
|
162
|
+
private _validateUrl(url: string, name: string): string {
|
|
163
|
+
if (!url || typeof url !== 'string') {
|
|
164
|
+
throw new ConfigurationError(`${name} must be a non-empty string`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
new URL(url);
|
|
169
|
+
return url;
|
|
170
|
+
} catch (_error) {
|
|
171
|
+
throw new ConfigurationError(`${name} must be a valid URL`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Validate token format.
|
|
177
|
+
* @private
|
|
178
|
+
*/
|
|
179
|
+
private _validateToken(token: string): string {
|
|
180
|
+
if (!token || typeof token !== 'string') {
|
|
181
|
+
throw new TokenError('Token must be a non-empty string');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Basic JWT format validation
|
|
185
|
+
const parts = token.split('.');
|
|
186
|
+
if (parts.length !== 3) {
|
|
187
|
+
throw new TokenError('Token must be a valid JWT');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return token;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Ensure the SDK hasn't been closed.
|
|
195
|
+
* @private
|
|
196
|
+
*/
|
|
197
|
+
private _ensureNotClosed(): void {
|
|
198
|
+
if (this._closed) {
|
|
199
|
+
throw new Error('SDK has been closed. Create a new instance or use as context manager.');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Make authenticated request with automatic token validation.
|
|
205
|
+
* @private
|
|
206
|
+
*/
|
|
207
|
+
private async _req(
|
|
208
|
+
method: string,
|
|
209
|
+
path: string,
|
|
210
|
+
options: Record<string, unknown> = {}
|
|
211
|
+
): Promise<unknown> {
|
|
212
|
+
this._ensureNotClosed();
|
|
213
|
+
await this.ensureValidToken();
|
|
214
|
+
|
|
215
|
+
const headers = (options['headers'] as Record<string, string>) ?? {};
|
|
216
|
+
headers['Authorization'] = `Bearer ${this.token}`;
|
|
217
|
+
|
|
218
|
+
const url = `${this.base}${path}`;
|
|
219
|
+
return await this._http.request(method, url, { ...options, headers });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Validate the cached token by making a test API call.
|
|
224
|
+
* @returns True if the token is valid
|
|
225
|
+
*/
|
|
226
|
+
public async validateCachedToken(): Promise<boolean> {
|
|
227
|
+
if (!this.token) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
// Use Auth0's userinfo endpoint for validation
|
|
233
|
+
const config = getStaticConfig();
|
|
234
|
+
const response = await fetch(`https://${config.authDomain}/userinfo`, {
|
|
235
|
+
headers: {
|
|
236
|
+
Authorization: `Bearer ${this.token}`,
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const isValid = response.ok;
|
|
241
|
+
// Only set _tokenValidated to true if the token is actually valid
|
|
242
|
+
if (isValid) {
|
|
243
|
+
this._tokenValidated = true;
|
|
244
|
+
}
|
|
245
|
+
return isValid;
|
|
246
|
+
} catch (_error) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Ensure token is valid, validating if necessary.
|
|
253
|
+
*/
|
|
254
|
+
public async ensureValidToken(): Promise<void> {
|
|
255
|
+
if (this._tokenValidated) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Skip validation only in explicit test/CI environments
|
|
260
|
+
const env = (isNode ? process.env['BARNDOOR_ENV'] : '') ?? '';
|
|
261
|
+
if (['test', 'ci'].includes(env.toLowerCase())) {
|
|
262
|
+
this._tokenValidated = true;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Validate token in all other environments (including staging, dev, prod)
|
|
267
|
+
const isValid = await this.validateCachedToken();
|
|
268
|
+
if (!isValid) {
|
|
269
|
+
throw new TokenError('Token validation failed. Please re-authenticate.');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
this._tokenValidated = true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* List all MCP servers available to the caller's organization.
|
|
277
|
+
* @returns Array of server summaries
|
|
278
|
+
*/
|
|
279
|
+
public async listServers(): Promise<ServerSummary[]> {
|
|
280
|
+
this._logger.debug('Fetching server list');
|
|
281
|
+
try {
|
|
282
|
+
const response = (await this._req('GET', '/servers')) as PaginatedResponse<unknown>;
|
|
283
|
+
const servers = response.data.map(data => ServerSummary.fromApiResponse(data));
|
|
284
|
+
this._logger.info(`Retrieved ${servers.length} servers`);
|
|
285
|
+
return servers;
|
|
286
|
+
} catch (error) {
|
|
287
|
+
this._logger.error('Failed to list servers:', error);
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Get detailed information about a specific server.
|
|
294
|
+
* @param serverId - Server ID
|
|
295
|
+
* @returns Server details
|
|
296
|
+
*/
|
|
297
|
+
public async getServer(serverId: string): Promise<ServerDetail> {
|
|
298
|
+
const validatedServerId = this._validateServerId(serverId);
|
|
299
|
+
|
|
300
|
+
this._logger.info(`Fetching server details for ${validatedServerId}`);
|
|
301
|
+
const response = await this._req('GET', `/servers/${validatedServerId}`);
|
|
302
|
+
return ServerDetail.fromApiResponse(response);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Initiate OAuth connection flow for a server.
|
|
307
|
+
* @param serverId - Server ID
|
|
308
|
+
* @param returnUrl - Optional return URL
|
|
309
|
+
* @returns Connection initiation response
|
|
310
|
+
*/
|
|
311
|
+
public async initiateConnection(
|
|
312
|
+
serverId: string,
|
|
313
|
+
returnUrl?: string
|
|
314
|
+
): Promise<ConnectionInitiationResponse> {
|
|
315
|
+
const validatedServerId = this._validateServerId(serverId);
|
|
316
|
+
let validatedReturnUrl: string | undefined;
|
|
317
|
+
|
|
318
|
+
if (returnUrl) {
|
|
319
|
+
validatedReturnUrl = this._validateUrl(returnUrl, 'Return URL');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
this._logger.info(`Initiating connection for server ${validatedServerId}`);
|
|
323
|
+
|
|
324
|
+
const params = validatedReturnUrl ? { return_url: validatedReturnUrl } : undefined;
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const response = await this._req('POST', `/servers/${validatedServerId}/connect`, {
|
|
328
|
+
params,
|
|
329
|
+
json: {},
|
|
330
|
+
});
|
|
331
|
+
return response as ConnectionInitiationResponse;
|
|
332
|
+
} catch (error: unknown) {
|
|
333
|
+
if (
|
|
334
|
+
error instanceof HTTPError &&
|
|
335
|
+
error.statusCode === 500 &&
|
|
336
|
+
error.responseBody?.includes('OAuth server configuration not found')
|
|
337
|
+
) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
'Server is missing OAuth configuration. ' +
|
|
340
|
+
'Ask an admin to configure credentials before initiating a connection.'
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Get the user's connection status for a specific server.
|
|
349
|
+
* @param serverId - Server ID
|
|
350
|
+
* @returns Connection status
|
|
351
|
+
*/
|
|
352
|
+
public async getConnectionStatus(serverId: string): Promise<string> {
|
|
353
|
+
const validatedServerId = this._validateServerId(serverId);
|
|
354
|
+
|
|
355
|
+
this._logger.info(`Checking connection status for server ${validatedServerId}`);
|
|
356
|
+
const response = (await this._req(
|
|
357
|
+
'GET',
|
|
358
|
+
`/servers/${validatedServerId}/connection`
|
|
359
|
+
)) as ConnectionStatusResponse;
|
|
360
|
+
return response.status;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Disconnect from a specific MCP server.
|
|
365
|
+
*
|
|
366
|
+
* This will remove the connection record and clean up any stored OAuth credentials.
|
|
367
|
+
* The user will need to reconnect to use this server again.
|
|
368
|
+
*
|
|
369
|
+
* @param serverId - Server ID or slug to disconnect from
|
|
370
|
+
*/
|
|
371
|
+
public async disconnectServer(serverId: string): Promise<void> {
|
|
372
|
+
const validatedServerId = this._validateServerId(serverId);
|
|
373
|
+
|
|
374
|
+
this._logger.info(`Disconnecting from server ${validatedServerId}`);
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
await this._req('DELETE', `/servers/${validatedServerId}/connection`);
|
|
378
|
+
this._logger.info(`Successfully disconnected from server ${validatedServerId}`);
|
|
379
|
+
} catch (error: unknown) {
|
|
380
|
+
if (error instanceof HTTPError && error.statusCode === 404) {
|
|
381
|
+
throw new Error(
|
|
382
|
+
`Connection not found for server ${validatedServerId}. Server may not be connected.`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Validate server ID format.
|
|
391
|
+
* @private
|
|
392
|
+
*/
|
|
393
|
+
private _validateServerId(serverId: string): string {
|
|
394
|
+
if (!serverId || typeof serverId !== 'string') {
|
|
395
|
+
throw new Error('Server ID must be a non-empty string');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Accept both UUIDs and slugs (as per OpenAPI spec)
|
|
399
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
400
|
+
const slugRegex = /^[a-z0-9-]+$/;
|
|
401
|
+
|
|
402
|
+
if (!uuidRegex.test(serverId) && !slugRegex.test(serverId)) {
|
|
403
|
+
throw new Error(
|
|
404
|
+
'Server ID must be a valid UUID or slug (lowercase letters, numbers, and hyphens only)'
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return serverId;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Close the SDK and clean up resources.
|
|
413
|
+
*/
|
|
414
|
+
public async close(): Promise<void> {
|
|
415
|
+
if (!this._closed) {
|
|
416
|
+
await this._http.close();
|
|
417
|
+
this._closed = true;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Alias for close() to match Python SDK naming.
|
|
423
|
+
*/
|
|
424
|
+
public async aclose(): Promise<void> {
|
|
425
|
+
await this.close();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Ensure a server is connected, initiating OAuth if needed.
|
|
430
|
+
* @param serverIdentifier - Server slug or provider name
|
|
431
|
+
* @param options - Options
|
|
432
|
+
*/
|
|
433
|
+
public async ensureServerConnected(
|
|
434
|
+
serverIdentifier: string,
|
|
435
|
+
options: EnsureServerConnectedOptions = {}
|
|
436
|
+
): Promise<void> {
|
|
437
|
+
const { pollSeconds = 60 } = options;
|
|
438
|
+
|
|
439
|
+
if (!isNode) {
|
|
440
|
+
throw new Error('ensureServerConnected requires Node.js environment for browser opening');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// 1. Locate server
|
|
444
|
+
const servers = await this.listServers();
|
|
445
|
+
const target = servers.find(
|
|
446
|
+
s =>
|
|
447
|
+
s.slug === serverIdentifier ||
|
|
448
|
+
(s.provider && s.provider.toLowerCase() === serverIdentifier.toLowerCase())
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
if (!target) {
|
|
452
|
+
throw new ServerNotFoundError(serverIdentifier);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (target.connection_status === 'connected') {
|
|
456
|
+
return; // Already connected
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// 2. Start OAuth flow
|
|
460
|
+
const connection = await this.initiateConnection(target.id);
|
|
461
|
+
const authUrl = connection.auth_url;
|
|
462
|
+
if (!authUrl) {
|
|
463
|
+
throw new Error('Registry did not return auth_url');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// 3. Open browser (shell-free)
|
|
467
|
+
const platform = os.platform();
|
|
468
|
+
|
|
469
|
+
// Validate URL scheme (require https, allow http only for localhost)
|
|
470
|
+
let parsed: URL;
|
|
471
|
+
try {
|
|
472
|
+
parsed = new URL(authUrl);
|
|
473
|
+
} catch {
|
|
474
|
+
throw new Error('Invalid auth_url returned by server');
|
|
475
|
+
}
|
|
476
|
+
if (
|
|
477
|
+
parsed.protocol !== 'https:' &&
|
|
478
|
+
!(
|
|
479
|
+
parsed.protocol === 'http:' &&
|
|
480
|
+
(parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1')
|
|
481
|
+
)
|
|
482
|
+
) {
|
|
483
|
+
throw new Error('Auth URL must use HTTPS (http allowed only for localhost)');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
if (platform === 'darwin') {
|
|
488
|
+
spawn('open', [authUrl], { detached: true, stdio: 'ignore' }).unref();
|
|
489
|
+
} else if (platform === 'win32') {
|
|
490
|
+
spawn('powershell', ['-NoProfile', 'Start-Process', authUrl], {
|
|
491
|
+
detached: true,
|
|
492
|
+
stdio: 'ignore',
|
|
493
|
+
}).unref();
|
|
494
|
+
} else {
|
|
495
|
+
spawn('xdg-open', [authUrl], { detached: true, stdio: 'ignore' }).unref();
|
|
496
|
+
}
|
|
497
|
+
} catch (error) {
|
|
498
|
+
this._logger.warn('Failed to open browser', error);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// 4. Poll until connected or timeout
|
|
502
|
+
for (let i = 0; i < pollSeconds; i++) {
|
|
503
|
+
const status = await this.getConnectionStatus(target.id);
|
|
504
|
+
if (status === 'connected') {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
throw new Error('OAuth connection was not completed in time');
|
|
511
|
+
}
|
|
512
|
+
}
|