@spec2tools/core 0.1.0 → 0.1.1

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.
@@ -1,32 +1,35 @@
1
1
  import { AuthConfig } from './types.js';
2
2
  export declare class AuthManager {
3
- private authConfig;
3
+ private globalAuthConfig;
4
4
  private accessToken?;
5
5
  private refreshToken?;
6
6
  private clientId?;
7
7
  private clientSecret?;
8
8
  private codeVerifier?;
9
- constructor(authConfig: AuthConfig);
9
+ constructor(globalAuthConfig: AuthConfig);
10
10
  /**
11
11
  * Check if authentication is required
12
12
  */
13
- requiresAuth(): boolean;
13
+ requiresAuth(authConfig?: AuthConfig): boolean;
14
14
  /**
15
15
  * Get the current access token
16
16
  */
17
17
  getAccessToken(): string | undefined;
18
18
  /**
19
19
  * Get authorization headers for requests
20
+ * @param authConfig Optional tool-specific auth config that overrides the global config
20
21
  */
21
- getAuthHeaders(): Record<string, string>;
22
+ getAuthHeaders(authConfig?: AuthConfig): Record<string, string>;
22
23
  /**
23
24
  * Get query parameters for API key auth
25
+ * @param authConfig Optional tool-specific auth config that overrides the global config
24
26
  */
25
- getAuthQueryParams(): Record<string, string>;
27
+ getAuthQueryParams(authConfig?: AuthConfig): Record<string, string>;
26
28
  /**
27
29
  * Perform authentication based on config type
30
+ * @param authConfig Optional tool-specific auth config that overrides the global config
28
31
  */
29
- authenticate(): Promise<void>;
32
+ authenticate(authConfig?: AuthConfig): Promise<void>;
30
33
  /**
31
34
  * Prompt user for API key
32
35
  */
@@ -35,6 +38,10 @@ export declare class AuthManager {
35
38
  * Prompt user for bearer token
36
39
  */
37
40
  private promptForBearerToken;
41
+ /**
42
+ * Prompt user for basic auth credentials
43
+ */
44
+ private promptForBasicAuth;
38
45
  /**
39
46
  * Perform OAuth2 authorization code flow
40
47
  */
@@ -7,20 +7,21 @@ import * as readline from 'readline';
7
7
  const CALLBACK_PORT = 54321;
8
8
  const CALLBACK_PATH = '/callback';
9
9
  export class AuthManager {
10
- authConfig;
10
+ globalAuthConfig;
11
11
  accessToken;
12
12
  refreshToken;
13
13
  clientId;
14
14
  clientSecret;
15
15
  codeVerifier;
16
- constructor(authConfig) {
17
- this.authConfig = authConfig;
16
+ constructor(globalAuthConfig) {
17
+ this.globalAuthConfig = globalAuthConfig;
18
18
  }
19
19
  /**
20
20
  * Check if authentication is required
21
21
  */
22
- requiresAuth() {
23
- return this.authConfig.type !== 'none';
22
+ requiresAuth(authConfig) {
23
+ const config = authConfig ?? this.globalAuthConfig;
24
+ return config.type !== 'none';
24
25
  }
25
26
  /**
26
27
  * Get the current access token
@@ -30,18 +31,22 @@ export class AuthManager {
30
31
  }
31
32
  /**
32
33
  * Get authorization headers for requests
34
+ * @param authConfig Optional tool-specific auth config that overrides the global config
33
35
  */
34
- getAuthHeaders() {
36
+ getAuthHeaders(authConfig) {
35
37
  if (!this.accessToken) {
36
38
  return {};
37
39
  }
38
- switch (this.authConfig.type) {
40
+ const config = authConfig ?? this.globalAuthConfig;
41
+ switch (config.type) {
39
42
  case 'oauth2':
40
43
  case 'bearer':
41
44
  return { Authorization: `Bearer ${this.accessToken}` };
45
+ case 'basic':
46
+ return { Authorization: `Basic ${this.accessToken}` };
42
47
  case 'apiKey':
43
- if (this.authConfig.apiKeyIn === 'header' && this.authConfig.apiKeyHeader) {
44
- return { [this.authConfig.apiKeyHeader]: this.accessToken };
48
+ if (config.apiKeyIn === 'header' && config.apiKeyHeader) {
49
+ return { [config.apiKeyHeader]: this.accessToken };
45
50
  }
46
51
  return {};
47
52
  default:
@@ -50,30 +55,37 @@ export class AuthManager {
50
55
  }
51
56
  /**
52
57
  * Get query parameters for API key auth
58
+ * @param authConfig Optional tool-specific auth config that overrides the global config
53
59
  */
54
- getAuthQueryParams() {
55
- if (this.authConfig.type === 'apiKey' &&
56
- this.authConfig.apiKeyIn === 'query' &&
57
- this.authConfig.apiKeyHeader &&
60
+ getAuthQueryParams(authConfig) {
61
+ const config = authConfig ?? this.globalAuthConfig;
62
+ if (config.type === 'apiKey' &&
63
+ config.apiKeyIn === 'query' &&
64
+ config.apiKeyHeader &&
58
65
  this.accessToken) {
59
- return { [this.authConfig.apiKeyHeader]: this.accessToken };
66
+ return { [config.apiKeyHeader]: this.accessToken };
60
67
  }
61
68
  return {};
62
69
  }
63
70
  /**
64
71
  * Perform authentication based on config type
72
+ * @param authConfig Optional tool-specific auth config that overrides the global config
65
73
  */
66
- async authenticate() {
67
- switch (this.authConfig.type) {
74
+ async authenticate(authConfig) {
75
+ const config = authConfig ?? this.globalAuthConfig;
76
+ switch (config.type) {
68
77
  case 'oauth2':
69
- await this.performOAuth2Flow();
78
+ await this.performOAuth2Flow(config);
70
79
  break;
71
80
  case 'apiKey':
72
- await this.promptForApiKey();
81
+ await this.promptForApiKey(config);
73
82
  break;
74
83
  case 'bearer':
75
84
  await this.promptForBearerToken();
76
85
  break;
86
+ case 'basic':
87
+ await this.promptForBasicAuth();
88
+ break;
77
89
  case 'none':
78
90
  // No authentication needed
79
91
  break;
@@ -82,12 +94,12 @@ export class AuthManager {
82
94
  /**
83
95
  * Prompt user for API key
84
96
  */
85
- async promptForApiKey() {
97
+ async promptForApiKey(config) {
86
98
  const rl = readline.createInterface({
87
99
  input: process.stdin,
88
100
  output: process.stdout,
89
101
  });
90
- const headerName = this.authConfig.apiKeyHeader || 'API-Key';
102
+ const headerName = config.apiKeyHeader || 'API-Key';
91
103
  this.accessToken = await new Promise((resolve) => {
92
104
  rl.question(`Enter your API key (${headerName}): `, (answer) => {
93
105
  rl.close();
@@ -116,26 +128,50 @@ export class AuthManager {
116
128
  throw new AuthenticationError('Bearer token is required');
117
129
  }
118
130
  }
131
+ /**
132
+ * Prompt user for basic auth credentials
133
+ */
134
+ async promptForBasicAuth() {
135
+ const rl = readline.createInterface({
136
+ input: process.stdin,
137
+ output: process.stdout,
138
+ });
139
+ const question = (prompt) => {
140
+ return new Promise((resolve) => {
141
+ rl.question(prompt, (answer) => {
142
+ resolve(answer.trim());
143
+ });
144
+ });
145
+ };
146
+ const username = await question('Enter username: ');
147
+ const password = await question('Enter password: ');
148
+ rl.close();
149
+ if (!username || !password) {
150
+ throw new AuthenticationError('Username and password are required for Basic auth');
151
+ }
152
+ // Base64 encode the credentials
153
+ this.accessToken = Buffer.from(`${username}:${password}`).toString('base64');
154
+ }
119
155
  /**
120
156
  * Perform OAuth2 authorization code flow
121
157
  */
122
- async performOAuth2Flow() {
123
- if (!this.authConfig.authorizationUrl || !this.authConfig.tokenUrl) {
158
+ async performOAuth2Flow(config) {
159
+ if (!config.authorizationUrl || !config.tokenUrl) {
124
160
  throw new AuthenticationError('OAuth2 requires authorizationUrl and tokenUrl');
125
161
  }
126
162
  // Register client dynamically
127
- await this.registerClient();
163
+ await this.registerClient(config);
128
164
  // Start local callback server
129
- const authCode = await this.startCallbackServerAndAuthorize();
165
+ const authCode = await this.startCallbackServerAndAuthorize(config);
130
166
  // Exchange code for token
131
- await this.exchangeCodeForToken(authCode);
167
+ await this.exchangeCodeForToken(authCode, config);
132
168
  }
133
169
  /**
134
170
  * Register OAuth2 client dynamically
135
171
  */
136
- async registerClient() {
172
+ async registerClient(config) {
137
173
  // Derive registration URL from token URL (replace /token with /register)
138
- const registrationUrl = this.authConfig.tokenUrl.replace(/\/token$/, '/register');
174
+ const registrationUrl = config.tokenUrl.replace(/\/token$/, '/register');
139
175
  const redirectUri = `http://127.0.0.1:${CALLBACK_PORT}${CALLBACK_PATH}`;
140
176
  console.log('Registering OAuth2 client...');
141
177
  const response = await fetch(registrationUrl, {
@@ -162,7 +198,7 @@ export class AuthManager {
162
198
  /**
163
199
  * Start local callback server and initiate authorization
164
200
  */
165
- async startCallbackServerAndAuthorize() {
201
+ async startCallbackServerAndAuthorize(config) {
166
202
  return new Promise((resolve, reject) => {
167
203
  const app = express();
168
204
  let server;
@@ -211,7 +247,7 @@ export class AuthManager {
211
247
  server = createServer(app);
212
248
  server.listen(CALLBACK_PORT, '127.0.0.1', () => {
213
249
  const redirectUri = `http://127.0.0.1:${CALLBACK_PORT}${CALLBACK_PATH}`;
214
- const authUrl = this.buildAuthorizationUrl(redirectUri);
250
+ const authUrl = this.buildAuthorizationUrl(redirectUri, config);
215
251
  console.log('\nAuthentication required. Opening browser...');
216
252
  console.log(`If browser doesn't open, visit: ${authUrl}\n`);
217
253
  open(authUrl).catch(() => {
@@ -249,8 +285,8 @@ export class AuthManager {
249
285
  /**
250
286
  * Build the OAuth2 authorization URL
251
287
  */
252
- buildAuthorizationUrl(redirectUri) {
253
- const url = new URL(this.authConfig.authorizationUrl);
288
+ buildAuthorizationUrl(redirectUri, config) {
289
+ const url = new URL(config.authorizationUrl);
254
290
  // Generate PKCE
255
291
  const pkce = this.generatePKCE();
256
292
  this.codeVerifier = pkce.verifier;
@@ -259,8 +295,8 @@ export class AuthManager {
259
295
  url.searchParams.set('redirect_uri', redirectUri);
260
296
  url.searchParams.set('code_challenge', pkce.challenge);
261
297
  url.searchParams.set('code_challenge_method', 'S256');
262
- if (this.authConfig.scopes && this.authConfig.scopes.length > 0) {
263
- url.searchParams.set('scope', this.authConfig.scopes.join(' '));
298
+ if (config.scopes && config.scopes.length > 0) {
299
+ url.searchParams.set('scope', config.scopes.join(' '));
264
300
  }
265
301
  // Generate state for CSRF protection
266
302
  const state = crypto.randomBytes(16).toString('base64url');
@@ -270,7 +306,7 @@ export class AuthManager {
270
306
  /**
271
307
  * Exchange authorization code for access token
272
308
  */
273
- async exchangeCodeForToken(code) {
309
+ async exchangeCodeForToken(code, config) {
274
310
  const redirectUri = `http://127.0.0.1:${CALLBACK_PORT}${CALLBACK_PATH}`;
275
311
  const params = new URLSearchParams({
276
312
  grant_type: 'authorization_code',
@@ -283,7 +319,7 @@ export class AuthManager {
283
319
  if (this.clientSecret) {
284
320
  params.set('client_secret', this.clientSecret);
285
321
  }
286
- const response = await fetch(this.authConfig.tokenUrl, {
322
+ const response = await fetch(config.tokenUrl, {
287
323
  method: 'POST',
288
324
  headers: {
289
325
  'Content-Type': 'application/x-www-form-urlencoded',
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { UnsupportedSchemaError, AuthenticationError, ToolExecutionError, SpecLoadError, } from './errors.js';
2
2
  export type { HttpMethod, Tool, AuthType, AuthConfig, Session, OpenAPISpec, PathItem, Operation, Parameter, RequestBody, MediaType, Response, SchemaObject, SecurityScheme, } from './types.js';
3
- export { loadOpenAPISpec, extractBaseUrl, extractAuthConfig, parseOperations, formatToolSchema, formatToolSignature, } from './openapi-parser.js';
3
+ export { loadOpenAPISpec, extractBaseUrl, extractAuthConfig, extractOperationAuthConfig, parseOperations, formatToolSchema, formatToolSignature, } from './openapi-parser.js';
4
4
  export { AuthManager } from './auth-manager.js';
5
5
  export { createExecutableTools, executeToolByName } from './tool-executor.js';
6
6
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Error classes
2
2
  export { UnsupportedSchemaError, AuthenticationError, ToolExecutionError, SpecLoadError, } from './errors.js';
3
3
  // OpenAPI Parser
4
- export { loadOpenAPISpec, extractBaseUrl, extractAuthConfig, parseOperations, formatToolSchema, formatToolSignature, } from './openapi-parser.js';
4
+ export { loadOpenAPISpec, extractBaseUrl, extractAuthConfig, extractOperationAuthConfig, parseOperations, formatToolSchema, formatToolSignature, } from './openapi-parser.js';
5
5
  // Auth Manager
6
6
  export { AuthManager } from './auth-manager.js';
7
7
  // Tool Executor
@@ -1,4 +1,4 @@
1
- import { OpenAPISpec, Tool, AuthConfig } from './types.js';
1
+ import { OpenAPISpec, Operation, Tool, AuthConfig } from './types.js';
2
2
  /**
3
3
  * Fetch and parse an OpenAPI specification from URL or file path
4
4
  */
@@ -11,6 +11,11 @@ export declare function extractBaseUrl(spec: OpenAPISpec): string;
11
11
  * Extract authentication configuration from security schemes
12
12
  */
13
13
  export declare function extractAuthConfig(spec: OpenAPISpec): AuthConfig;
14
+ /**
15
+ * Extract authentication configuration for a specific operation
16
+ * Uses operation-level security if defined, otherwise falls back to global security
17
+ */
18
+ export declare function extractOperationAuthConfig(spec: OpenAPISpec, operation: Operation): AuthConfig;
14
19
  /**
15
20
  * Parse all operations from the OpenAPI spec and generate tool definitions
16
21
  */
@@ -47,13 +47,33 @@ export function extractBaseUrl(spec) {
47
47
  * Extract authentication configuration from security schemes
48
48
  */
49
49
  export function extractAuthConfig(spec) {
50
- const securitySchemes = spec.components?.securitySchemes;
51
- const globalSecurity = spec.security;
52
- if (!securitySchemes || !globalSecurity || globalSecurity.length === 0) {
50
+ return extractAuthConfigFromSecurity(spec.security, spec.components?.securitySchemes);
51
+ }
52
+ /**
53
+ * Extract authentication configuration for a specific operation
54
+ * Uses operation-level security if defined, otherwise falls back to global security
55
+ */
56
+ export function extractOperationAuthConfig(spec, operation) {
57
+ // If operation has its own security field, use it (even if empty array = no auth)
58
+ if (operation.security !== undefined) {
59
+ return extractAuthConfigFromSecurity(operation.security, spec.components?.securitySchemes);
60
+ }
61
+ // Fall back to global security
62
+ return extractAuthConfig(spec);
63
+ }
64
+ /**
65
+ * Extract auth config from a security requirement array
66
+ */
67
+ function extractAuthConfigFromSecurity(security, securitySchemes) {
68
+ // Empty security array means explicitly no auth required
69
+ if (security !== undefined && security.length === 0) {
70
+ return { type: 'none' };
71
+ }
72
+ if (!securitySchemes || !security || security.length === 0) {
53
73
  return { type: 'none' };
54
74
  }
55
75
  // Get the first security requirement
56
- const securityReq = globalSecurity[0];
76
+ const securityReq = security[0];
57
77
  const schemeName = Object.keys(securityReq)[0];
58
78
  const scheme = securitySchemes[schemeName];
59
79
  if (!scheme) {
@@ -84,6 +104,9 @@ function parseSecurityScheme(scheme, scopes) {
84
104
  if (scheme.type === 'http' && scheme.scheme === 'bearer') {
85
105
  return { type: 'bearer' };
86
106
  }
107
+ if (scheme.type === 'http' && scheme.scheme === 'basic') {
108
+ return { type: 'basic' };
109
+ }
87
110
  return { type: 'none' };
88
111
  }
89
112
  /**
@@ -273,12 +296,15 @@ export function parseOperations(spec) {
273
296
  const bodyShape = buildRequestBodySchema(operation, operationId);
274
297
  const combinedShape = { ...parameterShape, ...bodyShape };
275
298
  const parameters = z.object(combinedShape);
299
+ // Extract operation-specific auth config
300
+ const authConfig = extractOperationAuthConfig(spec, operation);
276
301
  tools.push({
277
302
  name: operationId,
278
303
  description: operation.summary || operation.description || `${method} ${path}`,
279
304
  parameters,
280
305
  httpMethod: method,
281
306
  path,
307
+ authConfig,
282
308
  });
283
309
  }
284
310
  }
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { Tool, HttpMethod } from './types.js';
2
+ import { Tool, HttpMethod, AuthConfig } from './types.js';
3
3
  import { AuthManager } from './auth-manager.js';
4
4
  interface ToolDefinition {
5
5
  name: string;
@@ -7,6 +7,7 @@ interface ToolDefinition {
7
7
  parameters: z.ZodObject<z.ZodRawShape>;
8
8
  httpMethod: HttpMethod;
9
9
  path: string;
10
+ authConfig?: AuthConfig;
10
11
  }
11
12
  /**
12
13
  * Create executable tools from tool definitions
@@ -28,15 +28,20 @@ function createExecutor(tool, baseUrl, authManager) {
28
28
  urlObj.searchParams.set(key, String(value));
29
29
  }
30
30
  }
31
+ // Check if this tool requires auth (respects operation-level security)
32
+ // Tool-level authConfig overrides the global authConfig
33
+ const toolRequiresAuth = authManager.requiresAuth(tool.authConfig);
31
34
  // Add auth query params if needed
32
- const authQueryParams = authManager.getAuthQueryParams();
33
- for (const [key, value] of Object.entries(authQueryParams)) {
34
- urlObj.searchParams.set(key, value);
35
+ if (toolRequiresAuth) {
36
+ const authQueryParams = authManager.getAuthQueryParams(tool.authConfig);
37
+ for (const [key, value] of Object.entries(authQueryParams)) {
38
+ urlObj.searchParams.set(key, value);
39
+ }
35
40
  }
36
41
  url = urlObj.toString();
37
42
  // Build request options
38
43
  const headers = {
39
- ...authManager.getAuthHeaders(),
44
+ ...(toolRequiresAuth ? authManager.getAuthHeaders(tool.authConfig) : {}),
40
45
  };
41
46
  const fetchOptions = {
42
47
  method: tool.httpMethod,
@@ -62,7 +67,29 @@ function createExecutor(tool, baseUrl, authManager) {
62
67
  responseData = await response.text();
63
68
  }
64
69
  if (!response.ok) {
65
- throw new Error(`HTTP ${response.status}: ${JSON.stringify(responseData)}`);
70
+ // Build detailed error message
71
+ const errorDetails = [
72
+ `HTTP ${response.status} ${response.statusText}`,
73
+ `URL: ${tool.httpMethod} ${url}`,
74
+ `Response: ${typeof responseData === 'string' ? responseData : JSON.stringify(responseData, null, 2)}`
75
+ ];
76
+ // Add helpful context for common errors
77
+ if (response.status === 401) {
78
+ errorDetails.push('Authentication failed. Check your API key/token.');
79
+ }
80
+ else if (response.status === 403) {
81
+ errorDetails.push('Access forbidden. Verify your permissions.');
82
+ }
83
+ else if (response.status === 404) {
84
+ errorDetails.push('Resource not found.');
85
+ }
86
+ else if (response.status === 429) {
87
+ errorDetails.push('Rate limit exceeded. Try again later.');
88
+ }
89
+ else if (response.status >= 500) {
90
+ errorDetails.push('Server error. The API service may be experiencing issues.');
91
+ }
92
+ throw new Error(errorDetails.join('\n'));
66
93
  }
67
94
  return responseData;
68
95
  }
package/dist/types.d.ts CHANGED
@@ -7,8 +7,9 @@ export interface Tool {
7
7
  execute: (params: unknown) => Promise<unknown>;
8
8
  httpMethod: HttpMethod;
9
9
  path: string;
10
+ authConfig?: AuthConfig;
10
11
  }
11
- export type AuthType = 'oauth2' | 'apiKey' | 'bearer' | 'none';
12
+ export type AuthType = 'oauth2' | 'apiKey' | 'bearer' | 'basic' | 'none';
12
13
  export interface AuthConfig {
13
14
  type: AuthType;
14
15
  authorizationUrl?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spec2tools/core",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Core utilities for OpenAPI parsing and authentication",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",