@spec2tools/core 0.1.0 → 0.1.2

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,12 +104,44 @@ 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
  }
112
+ /**
113
+ * Resolve a $ref to its actual schema object
114
+ */
115
+ function resolveRef(ref, spec, visited = new Set()) {
116
+ // Detect circular references
117
+ if (visited.has(ref)) {
118
+ throw new UnsupportedSchemaError(ref, 'Circular $ref detected');
119
+ }
120
+ visited.add(ref);
121
+ // Only support #/components/schemas/ references
122
+ if (!ref.startsWith('#/components/schemas/')) {
123
+ throw new UnsupportedSchemaError(ref, 'Only #/components/schemas/ $refs are supported');
124
+ }
125
+ const schemaName = ref.replace('#/components/schemas/', '');
126
+ const schema = spec.components?.schemas?.[schemaName];
127
+ if (!schema) {
128
+ throw new UnsupportedSchemaError(ref, `Schema "${schemaName}" not found in components`);
129
+ }
130
+ // If the resolved schema has a $ref, resolve it recursively
131
+ if (schema.$ref) {
132
+ return resolveRef(schema.$ref, spec, visited);
133
+ }
134
+ return schema;
135
+ }
89
136
  /**
90
137
  * Convert an OpenAPI schema to a Zod schema
91
138
  */
92
- function schemaToZod(schema, path, depth = 0) {
139
+ function schemaToZod(schema, path, spec, depth = 0, visited = new Set()) {
140
+ // Resolve $ref if present
141
+ if (schema.$ref) {
142
+ const resolvedSchema = resolveRef(schema.$ref, spec, new Set(visited));
143
+ return schemaToZod(resolvedSchema, path, spec, depth, visited);
144
+ }
93
145
  // Check for unsupported features
94
146
  if (schema.anyOf) {
95
147
  throw new UnsupportedSchemaError(path, 'anyOf is not supported');
@@ -100,9 +152,6 @@ function schemaToZod(schema, path, depth = 0) {
100
152
  if (schema.allOf) {
101
153
  throw new UnsupportedSchemaError(path, 'allOf is not supported');
102
154
  }
103
- if (schema.$ref) {
104
- throw new UnsupportedSchemaError(path, '$ref is not supported');
105
- }
106
155
  // Handle array types
107
156
  if (schema.type === 'array') {
108
157
  if (!schema.items) {
@@ -111,7 +160,7 @@ function schemaToZod(schema, path, depth = 0) {
111
160
  if (schema.items.type === 'object') {
112
161
  throw new UnsupportedSchemaError(path, 'Arrays of objects are not supported');
113
162
  }
114
- const itemSchema = schemaToZod(schema.items, `${path}.items`, depth);
163
+ const itemSchema = schemaToZod(schema.items, `${path}.items`, spec, depth, visited);
115
164
  let arraySchema = z.array(itemSchema);
116
165
  if (schema.description) {
117
166
  arraySchema = arraySchema.describe(schema.description);
@@ -127,7 +176,7 @@ function schemaToZod(schema, path, depth = 0) {
127
176
  const required = schema.required || [];
128
177
  const shape = {};
129
178
  for (const [propName, propSchema] of Object.entries(properties)) {
130
- let zodProp = schemaToZod(propSchema, `${path}.${propName}`, depth + 1);
179
+ let zodProp = schemaToZod(propSchema, `${path}.${propName}`, spec, depth + 1, visited);
131
180
  if (!required.includes(propName)) {
132
181
  zodProp = zodProp.optional();
133
182
  }
@@ -174,15 +223,17 @@ function schemaToZod(schema, path, depth = 0) {
174
223
  /**
175
224
  * Build parameters schema from path and query parameters
176
225
  */
177
- function buildParametersSchema(parameters, operationId) {
226
+ function buildParametersSchema(parameters, operationId, spec) {
178
227
  const shape = {};
228
+ const pathParams = new Set();
229
+ const queryParams = new Set();
179
230
  for (const param of parameters) {
180
231
  if (param.in !== 'path' && param.in !== 'query') {
181
232
  continue; // Skip header and cookie parameters
182
233
  }
183
234
  let paramSchema;
184
235
  if (param.schema) {
185
- paramSchema = schemaToZod(param.schema, `${operationId}.parameters.${param.name}`, 0);
236
+ paramSchema = schemaToZod(param.schema, `${operationId}.parameters.${param.name}`, spec, 0);
186
237
  }
187
238
  else {
188
239
  paramSchema = z.string();
@@ -194,45 +245,58 @@ function buildParametersSchema(parameters, operationId) {
194
245
  paramSchema = paramSchema.optional();
195
246
  }
196
247
  shape[param.name] = paramSchema;
248
+ // Track which set this parameter belongs to
249
+ if (param.in === 'path') {
250
+ pathParams.add(param.name);
251
+ }
252
+ else if (param.in === 'query') {
253
+ queryParams.add(param.name);
254
+ }
197
255
  }
198
- return shape;
256
+ return { shape, pathParams, queryParams };
199
257
  }
200
258
  /**
201
259
  * Build request body schema
202
260
  */
203
- function buildRequestBodySchema(operation, operationId) {
261
+ function buildRequestBodySchema(operation, operationId, spec) {
262
+ const shape = {};
263
+ const bodyParams = new Set();
204
264
  if (!operation.requestBody?.content) {
205
- return {};
265
+ return { shape, bodyParams };
206
266
  }
207
267
  const jsonContent = operation.requestBody.content['application/json'];
208
268
  if (!jsonContent?.schema) {
209
- return {};
269
+ return { shape, bodyParams };
210
270
  }
211
271
  const schema = jsonContent.schema;
212
272
  // Check for file uploads
213
273
  if (schema.type === 'string' && schema.format === 'binary') {
214
274
  throw new UnsupportedSchemaError(`${operationId}.requestBody`, 'File uploads are not supported');
215
275
  }
276
+ // Resolve $ref if present
277
+ let resolvedSchema = schema;
278
+ if (schema.$ref) {
279
+ resolvedSchema = resolveRef(schema.$ref, spec);
280
+ }
216
281
  // For object schemas, flatten properties into the parameter shape
217
- if (schema.type === 'object' || schema.properties) {
218
- const properties = schema.properties || {};
219
- const required = schema.required || [];
220
- const shape = {};
282
+ if (resolvedSchema.type === 'object' || resolvedSchema.properties) {
283
+ const properties = resolvedSchema.properties || {};
284
+ const required = resolvedSchema.required || [];
221
285
  for (const [propName, propSchema] of Object.entries(properties)) {
222
286
  // Check for file upload in properties
223
287
  if (propSchema.type === 'string' &&
224
288
  propSchema.format === 'binary') {
225
289
  throw new UnsupportedSchemaError(`${operationId}.requestBody.${propName}`, 'File uploads are not supported');
226
290
  }
227
- let zodProp = schemaToZod(propSchema, `${operationId}.requestBody.${propName}`, 1);
291
+ let zodProp = schemaToZod(propSchema, `${operationId}.requestBody.${propName}`, spec, 1);
228
292
  if (!required.includes(propName)) {
229
293
  zodProp = zodProp.optional();
230
294
  }
231
295
  shape[propName] = zodProp;
296
+ bodyParams.add(propName); // Track that this is a body parameter
232
297
  }
233
- return shape;
234
298
  }
235
- return {};
299
+ return { shape, bodyParams };
236
300
  }
237
301
  /**
238
302
  * Generate tool name from operation
@@ -269,16 +333,24 @@ export function parseOperations(spec) {
269
333
  ...(operation.parameters || []),
270
334
  ];
271
335
  // Build combined schema from parameters and request body
272
- const parameterShape = buildParametersSchema(allParameters, operationId);
273
- const bodyShape = buildRequestBodySchema(operation, operationId);
274
- const combinedShape = { ...parameterShape, ...bodyShape };
336
+ const parametersResult = buildParametersSchema(allParameters, operationId, spec);
337
+ const bodyResult = buildRequestBodySchema(operation, operationId, spec);
338
+ const combinedShape = { ...parametersResult.shape, ...bodyResult.shape };
275
339
  const parameters = z.object(combinedShape);
340
+ // Extract operation-specific auth config
341
+ const authConfig = extractOperationAuthConfig(spec, operation);
276
342
  tools.push({
277
343
  name: operationId,
278
344
  description: operation.summary || operation.description || `${method} ${path}`,
279
345
  parameters,
280
346
  httpMethod: method,
281
347
  path,
348
+ authConfig,
349
+ parameterMetadata: {
350
+ pathParams: parametersResult.pathParams,
351
+ queryParams: parametersResult.queryParams,
352
+ bodyParams: bodyResult.bodyParams,
353
+ },
282
354
  });
283
355
  }
284
356
  }
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { Tool, HttpMethod } from './types.js';
2
+ import { Tool, HttpMethod, AuthConfig, ParameterMetadata } from './types.js';
3
3
  import { AuthManager } from './auth-manager.js';
4
4
  interface ToolDefinition {
5
5
  name: string;
@@ -7,6 +7,8 @@ interface ToolDefinition {
7
7
  parameters: z.ZodObject<z.ZodRawShape>;
8
8
  httpMethod: HttpMethod;
9
9
  path: string;
10
+ authConfig?: AuthConfig;
11
+ parameterMetadata?: ParameterMetadata;
10
12
  }
11
13
  /**
12
14
  * Create executable tools from tool definitions
@@ -19,8 +19,8 @@ function createExecutor(tool, baseUrl, authManager) {
19
19
  const validatedParams = tool.parameters.parse(params);
20
20
  // Build URL with path parameters replaced
21
21
  let url = buildUrl(baseUrl, tool.path, validatedParams);
22
- // Separate path, query, and body parameters
23
- const { queryParams, bodyParams } = separateParams(validatedParams, tool.path);
22
+ // Separate path, query, and body parameters using metadata
23
+ const { queryParams, bodyParams } = separateParams(validatedParams, tool.parameterMetadata);
24
24
  // Add query parameters
25
25
  const urlObj = new URL(url);
26
26
  for (const [key, value] of Object.entries(queryParams)) {
@@ -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
  }
@@ -95,39 +122,55 @@ function buildUrl(baseUrl, path, params) {
95
122
  return `${baseUrl}${finalPath}`;
96
123
  }
97
124
  /**
98
- * Separate parameters into path, query, and body params
125
+ * Separate parameters into path, query, and body params using metadata
99
126
  */
100
- function separateParams(params, path) {
127
+ function separateParams(params, metadata) {
101
128
  const pathParams = {};
102
129
  const queryParams = {};
103
130
  const bodyParams = {};
104
- // Extract path parameter names
105
- const pathParamNames = new Set();
106
- const pathParamRegex = /\{(\w+)\}/g;
107
- let match;
108
- while ((match = pathParamRegex.exec(path)) !== null) {
109
- pathParamNames.add(match[1]);
110
- }
111
- for (const [key, value] of Object.entries(params)) {
112
- if (pathParamNames.has(key)) {
113
- pathParams[key] = value;
114
- }
115
- else if (isPrimitive(value)) {
116
- // Primitive values go to query params
117
- queryParams[key] = value;
131
+ // If we have metadata, use it to categorize parameters
132
+ if (metadata) {
133
+ for (const [key, value] of Object.entries(params)) {
134
+ if (metadata.pathParams.has(key)) {
135
+ pathParams[key] = value;
136
+ }
137
+ else if (metadata.queryParams.has(key)) {
138
+ queryParams[key] = value;
139
+ }
140
+ else if (metadata.bodyParams.has(key)) {
141
+ bodyParams[key] = value;
142
+ }
143
+ else {
144
+ // Fallback: if not in metadata, treat as body param
145
+ bodyParams[key] = value;
146
+ }
118
147
  }
119
- else {
120
- // Complex values go to body
121
- bodyParams[key] = value;
148
+ }
149
+ else {
150
+ // Fallback to old behavior if no metadata (shouldn't happen with new parser)
151
+ // This keeps backward compatibility
152
+ const pathParamNames = extractPathParamNames(params);
153
+ for (const [key, value] of Object.entries(params)) {
154
+ if (pathParamNames.has(key)) {
155
+ pathParams[key] = value;
156
+ }
157
+ else if (isPrimitive(value)) {
158
+ queryParams[key] = value;
159
+ }
160
+ else {
161
+ bodyParams[key] = value;
162
+ }
122
163
  }
123
164
  }
124
- // For simplicity, if there are non-path primitive params,
125
- // we need to determine if they're query or body based on HTTP method
126
- // Since we don't have that info here, we'll treat all non-path primitives
127
- // as potential query params for GET/DELETE, and body params for POST/PUT/PATCH
128
- // This is handled by the executor which knows the method
129
165
  return { pathParams, queryParams, bodyParams };
130
166
  }
167
+ /**
168
+ * Extract path parameter names from params (fallback)
169
+ */
170
+ function extractPathParamNames(params) {
171
+ // This is a fallback - in practice, metadata should always be provided
172
+ return new Set();
173
+ }
131
174
  /**
132
175
  * Check if value is a primitive type
133
176
  */
package/dist/types.d.ts CHANGED
@@ -1,5 +1,13 @@
1
1
  import { z } from 'zod';
2
2
  export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
3
+ export interface ParameterMetadata {
4
+ /** Parameters that come from path (e.g., {id} in /users/{id}) */
5
+ pathParams: Set<string>;
6
+ /** Parameters that come from query string */
7
+ queryParams: Set<string>;
8
+ /** Parameters that come from request body */
9
+ bodyParams: Set<string>;
10
+ }
3
11
  export interface Tool {
4
12
  name: string;
5
13
  description: string;
@@ -7,8 +15,10 @@ export interface Tool {
7
15
  execute: (params: unknown) => Promise<unknown>;
8
16
  httpMethod: HttpMethod;
9
17
  path: string;
18
+ authConfig?: AuthConfig;
19
+ parameterMetadata?: ParameterMetadata;
10
20
  }
11
- export type AuthType = 'oauth2' | 'apiKey' | 'bearer' | 'none';
21
+ export type AuthType = 'oauth2' | 'apiKey' | 'bearer' | 'basic' | 'none';
12
22
  export interface AuthConfig {
13
23
  type: AuthType;
14
24
  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.2",
4
4
  "description": "Core utilities for OpenAPI parsing and authentication",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",