@spec2tools/core 0.1.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/LICENSE +21 -0
- package/README.md +117 -0
- package/dist/agent.d.ts +38 -0
- package/dist/agent.js +126 -0
- package/dist/auth-manager.d.ts +67 -0
- package/dist/auth-manager.js +308 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +315 -0
- package/dist/errors.d.ts +27 -0
- package/dist/errors.js +41 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +9 -0
- package/dist/lib.d.ts +29 -0
- package/dist/lib.js +99 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +3 -0
- package/dist/openapi-parser.d.ts +26 -0
- package/dist/openapi-parser.js +368 -0
- package/dist/tool-executor.d.ts +20 -0
- package/dist/tool-executor.js +149 -0
- package/dist/types.d.ts +117 -0
- package/dist/types.js +2 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# @spec2tools/core
|
|
2
|
+
|
|
3
|
+
Core utilities for OpenAPI parsing and authentication.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @spec2tools/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## API
|
|
12
|
+
|
|
13
|
+
### OpenAPI Parsing
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import {
|
|
17
|
+
loadOpenAPISpec,
|
|
18
|
+
extractBaseUrl,
|
|
19
|
+
extractAuthConfig,
|
|
20
|
+
parseOperations,
|
|
21
|
+
formatToolSchema,
|
|
22
|
+
formatToolSignature,
|
|
23
|
+
} from '@spec2tools/core';
|
|
24
|
+
|
|
25
|
+
// Load an OpenAPI specification from file or URL
|
|
26
|
+
const spec = await loadOpenAPISpec('./openapi.yaml');
|
|
27
|
+
|
|
28
|
+
// Extract the base URL
|
|
29
|
+
const baseUrl = extractBaseUrl(spec);
|
|
30
|
+
|
|
31
|
+
// Extract authentication configuration
|
|
32
|
+
const authConfig = extractAuthConfig(spec);
|
|
33
|
+
|
|
34
|
+
// Parse operations into tool definitions
|
|
35
|
+
const tools = parseOperations(spec);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Authentication Manager
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { AuthManager } from '@spec2tools/core';
|
|
42
|
+
|
|
43
|
+
const authManager = new AuthManager(authConfig);
|
|
44
|
+
|
|
45
|
+
// Check if auth is required
|
|
46
|
+
if (authManager.requiresAuth()) {
|
|
47
|
+
// Perform authentication (OAuth2, API Key, or Bearer token)
|
|
48
|
+
await authManager.authenticate();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Get auth headers for requests
|
|
52
|
+
const headers = authManager.getAuthHeaders();
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Tool Execution
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { createExecutableTools, executeToolByName } from '@spec2tools/core';
|
|
59
|
+
|
|
60
|
+
// Create executable tools from tool definitions
|
|
61
|
+
const tools = createExecutableTools(toolDefs, baseUrl, authManager);
|
|
62
|
+
|
|
63
|
+
// Execute a tool by name
|
|
64
|
+
const result = await executeToolByName(tools, 'getUser', { id: '123' });
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Error Classes
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import {
|
|
71
|
+
UnsupportedSchemaError,
|
|
72
|
+
AuthenticationError,
|
|
73
|
+
ToolExecutionError,
|
|
74
|
+
SpecLoadError,
|
|
75
|
+
} from '@spec2tools/core';
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Types
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import type {
|
|
82
|
+
HttpMethod,
|
|
83
|
+
Tool,
|
|
84
|
+
AuthType,
|
|
85
|
+
AuthConfig,
|
|
86
|
+
Session,
|
|
87
|
+
OpenAPISpec,
|
|
88
|
+
PathItem,
|
|
89
|
+
Operation,
|
|
90
|
+
Parameter,
|
|
91
|
+
RequestBody,
|
|
92
|
+
MediaType,
|
|
93
|
+
Response,
|
|
94
|
+
SchemaObject,
|
|
95
|
+
SecurityScheme,
|
|
96
|
+
} from '@spec2tools/core';
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Supported OpenAPI Features
|
|
100
|
+
|
|
101
|
+
### Supported
|
|
102
|
+
- `GET`, `POST`, `PUT`, `PATCH`, `DELETE` operations
|
|
103
|
+
- Path parameters (string, number, boolean)
|
|
104
|
+
- Query parameters (string, number, boolean)
|
|
105
|
+
- Request body with simple JSON schemas (primitives, flat objects)
|
|
106
|
+
- Security schemes: OAuth2 (authorization code with PKCE), API Key, Bearer token
|
|
107
|
+
|
|
108
|
+
### Not Supported (throws error)
|
|
109
|
+
- Nested objects beyond 1 level
|
|
110
|
+
- Arrays of objects
|
|
111
|
+
- `anyOf`, `oneOf`, `allOf` schemas
|
|
112
|
+
- File uploads
|
|
113
|
+
- `$ref` references
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Tool } from './types.js';
|
|
2
|
+
interface AgentConfig {
|
|
3
|
+
tools: Tool[];
|
|
4
|
+
model?: string;
|
|
5
|
+
maxSteps?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* AI Agent that uses OpenAPI tools
|
|
9
|
+
*/
|
|
10
|
+
export declare class Agent {
|
|
11
|
+
private tools;
|
|
12
|
+
private model;
|
|
13
|
+
private maxSteps;
|
|
14
|
+
private conversationHistory;
|
|
15
|
+
constructor(config: AgentConfig);
|
|
16
|
+
/**
|
|
17
|
+
* Get available tools description for the agent
|
|
18
|
+
*/
|
|
19
|
+
getToolsDescription(): string;
|
|
20
|
+
/**
|
|
21
|
+
* Process a user message and return the response
|
|
22
|
+
*/
|
|
23
|
+
chat(userMessage: string): Promise<string>;
|
|
24
|
+
/**
|
|
25
|
+
* Clear conversation history
|
|
26
|
+
*/
|
|
27
|
+
clearHistory(): void;
|
|
28
|
+
/**
|
|
29
|
+
* Get the list of tool names
|
|
30
|
+
*/
|
|
31
|
+
getToolNames(): string[];
|
|
32
|
+
/**
|
|
33
|
+
* Get a specific tool by name
|
|
34
|
+
*/
|
|
35
|
+
getTool(name: string): Tool | undefined;
|
|
36
|
+
}
|
|
37
|
+
export {};
|
|
38
|
+
//# sourceMappingURL=agent.d.ts.map
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { generateText, tool, stepCountIs } from 'ai';
|
|
2
|
+
import { openai } from '@ai-sdk/openai';
|
|
3
|
+
import { ToolExecutionError } from './errors.js';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
const MAX_OUTPUT_LENGTH = 500;
|
|
6
|
+
/**
|
|
7
|
+
* Trim a string if it exceeds the maximum length
|
|
8
|
+
*/
|
|
9
|
+
function trimOutput(value) {
|
|
10
|
+
const str = typeof value === 'string' ? value : JSON.stringify(value);
|
|
11
|
+
if (str.length > MAX_OUTPUT_LENGTH) {
|
|
12
|
+
return str.substring(0, MAX_OUTPUT_LENGTH) + '...';
|
|
13
|
+
}
|
|
14
|
+
return str;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* AI Agent that uses OpenAPI tools
|
|
18
|
+
*/
|
|
19
|
+
export class Agent {
|
|
20
|
+
tools;
|
|
21
|
+
model;
|
|
22
|
+
maxSteps;
|
|
23
|
+
conversationHistory;
|
|
24
|
+
constructor(config) {
|
|
25
|
+
this.tools = config.tools;
|
|
26
|
+
this.model = config.model || 'gpt-5.1-chat-latest';
|
|
27
|
+
this.maxSteps = config.maxSteps || 10;
|
|
28
|
+
this.conversationHistory = [];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Get available tools description for the agent
|
|
32
|
+
*/
|
|
33
|
+
getToolsDescription() {
|
|
34
|
+
if (this.tools.length === 0) {
|
|
35
|
+
return 'No tools available.';
|
|
36
|
+
}
|
|
37
|
+
const toolDescriptions = this.tools.map((tool) => {
|
|
38
|
+
return `- ${tool.name}: ${tool.description}`;
|
|
39
|
+
});
|
|
40
|
+
return `I have access to the following tools:\n${toolDescriptions.join('\n')}`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Process a user message and return the response
|
|
44
|
+
*/
|
|
45
|
+
async chat(userMessage) {
|
|
46
|
+
// Add user message to history
|
|
47
|
+
this.conversationHistory.push({
|
|
48
|
+
role: 'user',
|
|
49
|
+
content: userMessage,
|
|
50
|
+
});
|
|
51
|
+
try {
|
|
52
|
+
// Build AI SDK tools from our tool definitions
|
|
53
|
+
const aiTools = {};
|
|
54
|
+
for (const t of this.tools) {
|
|
55
|
+
const toolExecute = t.execute;
|
|
56
|
+
const toolName = t.name;
|
|
57
|
+
aiTools[t.name] = tool({
|
|
58
|
+
description: t.description,
|
|
59
|
+
inputSchema: t.parameters,
|
|
60
|
+
execute: async (params) => {
|
|
61
|
+
console.log(chalk.dim(`\n[Calling ${toolName} with ${JSON.stringify(params)}]`));
|
|
62
|
+
try {
|
|
63
|
+
const result = await toolExecute(params);
|
|
64
|
+
console.log(chalk.dim(`[${toolName} returned: ${trimOutput(result)}]\n`));
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error instanceof ToolExecutionError) {
|
|
69
|
+
console.log(chalk.red(`[${toolName} failed: ${error.message}]\n`));
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// Build system prompt
|
|
78
|
+
const systemPrompt = `You are a helpful AI assistant with access to various API tools.
|
|
79
|
+
When the user asks you to perform actions, use the available tools to help them.
|
|
80
|
+
Always explain what you're doing and present results in a clear, readable format.
|
|
81
|
+
If a tool call fails, explain the error to the user.`;
|
|
82
|
+
// Generate response with tool use
|
|
83
|
+
const result = await generateText({
|
|
84
|
+
model: openai(this.model),
|
|
85
|
+
system: systemPrompt,
|
|
86
|
+
messages: this.conversationHistory,
|
|
87
|
+
tools: aiTools,
|
|
88
|
+
stopWhen: stepCountIs(this.maxSteps),
|
|
89
|
+
});
|
|
90
|
+
// Add assistant response to history
|
|
91
|
+
this.conversationHistory.push({
|
|
92
|
+
role: 'assistant',
|
|
93
|
+
content: result.text,
|
|
94
|
+
});
|
|
95
|
+
return result.text;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
99
|
+
// Add error response to history
|
|
100
|
+
this.conversationHistory.push({
|
|
101
|
+
role: 'assistant',
|
|
102
|
+
content: `I encountered an error: ${errorMessage}`,
|
|
103
|
+
});
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Clear conversation history
|
|
109
|
+
*/
|
|
110
|
+
clearHistory() {
|
|
111
|
+
this.conversationHistory = [];
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get the list of tool names
|
|
115
|
+
*/
|
|
116
|
+
getToolNames() {
|
|
117
|
+
return this.tools.map((t) => t.name);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get a specific tool by name
|
|
121
|
+
*/
|
|
122
|
+
getTool(name) {
|
|
123
|
+
return this.tools.find((t) => t.name === name);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=agent.js.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { AuthConfig } from './types.js';
|
|
2
|
+
export declare class AuthManager {
|
|
3
|
+
private authConfig;
|
|
4
|
+
private accessToken?;
|
|
5
|
+
private refreshToken?;
|
|
6
|
+
private clientId?;
|
|
7
|
+
private clientSecret?;
|
|
8
|
+
private codeVerifier?;
|
|
9
|
+
constructor(authConfig: AuthConfig);
|
|
10
|
+
/**
|
|
11
|
+
* Check if authentication is required
|
|
12
|
+
*/
|
|
13
|
+
requiresAuth(): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Get the current access token
|
|
16
|
+
*/
|
|
17
|
+
getAccessToken(): string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Get authorization headers for requests
|
|
20
|
+
*/
|
|
21
|
+
getAuthHeaders(): Record<string, string>;
|
|
22
|
+
/**
|
|
23
|
+
* Get query parameters for API key auth
|
|
24
|
+
*/
|
|
25
|
+
getAuthQueryParams(): Record<string, string>;
|
|
26
|
+
/**
|
|
27
|
+
* Perform authentication based on config type
|
|
28
|
+
*/
|
|
29
|
+
authenticate(): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Prompt user for API key
|
|
32
|
+
*/
|
|
33
|
+
private promptForApiKey;
|
|
34
|
+
/**
|
|
35
|
+
* Prompt user for bearer token
|
|
36
|
+
*/
|
|
37
|
+
private promptForBearerToken;
|
|
38
|
+
/**
|
|
39
|
+
* Perform OAuth2 authorization code flow
|
|
40
|
+
*/
|
|
41
|
+
private performOAuth2Flow;
|
|
42
|
+
/**
|
|
43
|
+
* Register OAuth2 client dynamically
|
|
44
|
+
*/
|
|
45
|
+
private registerClient;
|
|
46
|
+
/**
|
|
47
|
+
* Start local callback server and initiate authorization
|
|
48
|
+
*/
|
|
49
|
+
private startCallbackServerAndAuthorize;
|
|
50
|
+
/**
|
|
51
|
+
* Generate PKCE code verifier and challenge
|
|
52
|
+
*/
|
|
53
|
+
private generatePKCE;
|
|
54
|
+
/**
|
|
55
|
+
* Build the OAuth2 authorization URL
|
|
56
|
+
*/
|
|
57
|
+
private buildAuthorizationUrl;
|
|
58
|
+
/**
|
|
59
|
+
* Exchange authorization code for access token
|
|
60
|
+
*/
|
|
61
|
+
private exchangeCodeForToken;
|
|
62
|
+
/**
|
|
63
|
+
* Set access token directly (for testing or manual token entry)
|
|
64
|
+
*/
|
|
65
|
+
setAccessToken(token: string): void;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=auth-manager.d.ts.map
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import open from 'open';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { createServer } from 'http';
|
|
5
|
+
import { AuthenticationError } from './errors.js';
|
|
6
|
+
import * as readline from 'readline';
|
|
7
|
+
const CALLBACK_PORT = 54321;
|
|
8
|
+
const CALLBACK_PATH = '/callback';
|
|
9
|
+
export class AuthManager {
|
|
10
|
+
authConfig;
|
|
11
|
+
accessToken;
|
|
12
|
+
refreshToken;
|
|
13
|
+
clientId;
|
|
14
|
+
clientSecret;
|
|
15
|
+
codeVerifier;
|
|
16
|
+
constructor(authConfig) {
|
|
17
|
+
this.authConfig = authConfig;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Check if authentication is required
|
|
21
|
+
*/
|
|
22
|
+
requiresAuth() {
|
|
23
|
+
return this.authConfig.type !== 'none';
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get the current access token
|
|
27
|
+
*/
|
|
28
|
+
getAccessToken() {
|
|
29
|
+
return this.accessToken;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Get authorization headers for requests
|
|
33
|
+
*/
|
|
34
|
+
getAuthHeaders() {
|
|
35
|
+
if (!this.accessToken) {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
switch (this.authConfig.type) {
|
|
39
|
+
case 'oauth2':
|
|
40
|
+
case 'bearer':
|
|
41
|
+
return { Authorization: `Bearer ${this.accessToken}` };
|
|
42
|
+
case 'apiKey':
|
|
43
|
+
if (this.authConfig.apiKeyIn === 'header' && this.authConfig.apiKeyHeader) {
|
|
44
|
+
return { [this.authConfig.apiKeyHeader]: this.accessToken };
|
|
45
|
+
}
|
|
46
|
+
return {};
|
|
47
|
+
default:
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get query parameters for API key auth
|
|
53
|
+
*/
|
|
54
|
+
getAuthQueryParams() {
|
|
55
|
+
if (this.authConfig.type === 'apiKey' &&
|
|
56
|
+
this.authConfig.apiKeyIn === 'query' &&
|
|
57
|
+
this.authConfig.apiKeyHeader &&
|
|
58
|
+
this.accessToken) {
|
|
59
|
+
return { [this.authConfig.apiKeyHeader]: this.accessToken };
|
|
60
|
+
}
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Perform authentication based on config type
|
|
65
|
+
*/
|
|
66
|
+
async authenticate() {
|
|
67
|
+
switch (this.authConfig.type) {
|
|
68
|
+
case 'oauth2':
|
|
69
|
+
await this.performOAuth2Flow();
|
|
70
|
+
break;
|
|
71
|
+
case 'apiKey':
|
|
72
|
+
await this.promptForApiKey();
|
|
73
|
+
break;
|
|
74
|
+
case 'bearer':
|
|
75
|
+
await this.promptForBearerToken();
|
|
76
|
+
break;
|
|
77
|
+
case 'none':
|
|
78
|
+
// No authentication needed
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Prompt user for API key
|
|
84
|
+
*/
|
|
85
|
+
async promptForApiKey() {
|
|
86
|
+
const rl = readline.createInterface({
|
|
87
|
+
input: process.stdin,
|
|
88
|
+
output: process.stdout,
|
|
89
|
+
});
|
|
90
|
+
const headerName = this.authConfig.apiKeyHeader || 'API-Key';
|
|
91
|
+
this.accessToken = await new Promise((resolve) => {
|
|
92
|
+
rl.question(`Enter your API key (${headerName}): `, (answer) => {
|
|
93
|
+
rl.close();
|
|
94
|
+
resolve(answer.trim());
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
if (!this.accessToken) {
|
|
98
|
+
throw new AuthenticationError('API key is required');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Prompt user for bearer token
|
|
103
|
+
*/
|
|
104
|
+
async promptForBearerToken() {
|
|
105
|
+
const rl = readline.createInterface({
|
|
106
|
+
input: process.stdin,
|
|
107
|
+
output: process.stdout,
|
|
108
|
+
});
|
|
109
|
+
this.accessToken = await new Promise((resolve) => {
|
|
110
|
+
rl.question('Enter your Bearer token: ', (answer) => {
|
|
111
|
+
rl.close();
|
|
112
|
+
resolve(answer.trim());
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
if (!this.accessToken) {
|
|
116
|
+
throw new AuthenticationError('Bearer token is required');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Perform OAuth2 authorization code flow
|
|
121
|
+
*/
|
|
122
|
+
async performOAuth2Flow() {
|
|
123
|
+
if (!this.authConfig.authorizationUrl || !this.authConfig.tokenUrl) {
|
|
124
|
+
throw new AuthenticationError('OAuth2 requires authorizationUrl and tokenUrl');
|
|
125
|
+
}
|
|
126
|
+
// Register client dynamically
|
|
127
|
+
await this.registerClient();
|
|
128
|
+
// Start local callback server
|
|
129
|
+
const authCode = await this.startCallbackServerAndAuthorize();
|
|
130
|
+
// Exchange code for token
|
|
131
|
+
await this.exchangeCodeForToken(authCode);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Register OAuth2 client dynamically
|
|
135
|
+
*/
|
|
136
|
+
async registerClient() {
|
|
137
|
+
// Derive registration URL from token URL (replace /token with /register)
|
|
138
|
+
const registrationUrl = this.authConfig.tokenUrl.replace(/\/token$/, '/register');
|
|
139
|
+
const redirectUri = `http://127.0.0.1:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
|
140
|
+
console.log('Registering OAuth2 client...');
|
|
141
|
+
const response = await fetch(registrationUrl, {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
headers: {
|
|
144
|
+
'Content-Type': 'application/json',
|
|
145
|
+
},
|
|
146
|
+
body: JSON.stringify({
|
|
147
|
+
client_name: 'OpenAPI Agent CLI',
|
|
148
|
+
redirect_uris: [redirectUri],
|
|
149
|
+
grant_types: ['authorization_code'],
|
|
150
|
+
token_endpoint_auth_method: 'none',
|
|
151
|
+
}),
|
|
152
|
+
});
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
const errorText = await response.text();
|
|
155
|
+
throw new AuthenticationError(`Client registration failed: ${response.status} ${errorText}`);
|
|
156
|
+
}
|
|
157
|
+
const registration = (await response.json());
|
|
158
|
+
this.clientId = registration.client_id;
|
|
159
|
+
this.clientSecret = registration.client_secret;
|
|
160
|
+
console.log('Client registered successfully.');
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Start local callback server and initiate authorization
|
|
164
|
+
*/
|
|
165
|
+
async startCallbackServerAndAuthorize() {
|
|
166
|
+
return new Promise((resolve, reject) => {
|
|
167
|
+
const app = express();
|
|
168
|
+
let server;
|
|
169
|
+
app.get(CALLBACK_PATH, (req, res) => {
|
|
170
|
+
const code = req.query.code;
|
|
171
|
+
const error = req.query.error;
|
|
172
|
+
if (error) {
|
|
173
|
+
res.send(`
|
|
174
|
+
<html>
|
|
175
|
+
<body>
|
|
176
|
+
<h1>Authentication Failed</h1>
|
|
177
|
+
<p>Error: ${error}</p>
|
|
178
|
+
<p>You can close this window.</p>
|
|
179
|
+
</body>
|
|
180
|
+
</html>
|
|
181
|
+
`);
|
|
182
|
+
server.close();
|
|
183
|
+
reject(new AuthenticationError(error));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (!code) {
|
|
187
|
+
res.send(`
|
|
188
|
+
<html>
|
|
189
|
+
<body>
|
|
190
|
+
<h1>Authentication Failed</h1>
|
|
191
|
+
<p>No authorization code received.</p>
|
|
192
|
+
<p>You can close this window.</p>
|
|
193
|
+
</body>
|
|
194
|
+
</html>
|
|
195
|
+
`);
|
|
196
|
+
server.close();
|
|
197
|
+
reject(new AuthenticationError('No authorization code received'));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
res.send(`
|
|
201
|
+
<html>
|
|
202
|
+
<body>
|
|
203
|
+
<h1>Authentication Successful!</h1>
|
|
204
|
+
<p>You can close this window and return to the CLI.</p>
|
|
205
|
+
</body>
|
|
206
|
+
</html>
|
|
207
|
+
`);
|
|
208
|
+
server.close();
|
|
209
|
+
resolve(code);
|
|
210
|
+
});
|
|
211
|
+
server = createServer(app);
|
|
212
|
+
server.listen(CALLBACK_PORT, '127.0.0.1', () => {
|
|
213
|
+
const redirectUri = `http://127.0.0.1:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
|
214
|
+
const authUrl = this.buildAuthorizationUrl(redirectUri);
|
|
215
|
+
console.log('\nAuthentication required. Opening browser...');
|
|
216
|
+
console.log(`If browser doesn't open, visit: ${authUrl}\n`);
|
|
217
|
+
open(authUrl).catch(() => {
|
|
218
|
+
console.log('Could not open browser automatically.');
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
server.on('error', (err) => {
|
|
222
|
+
if (err.code === 'EADDRINUSE') {
|
|
223
|
+
reject(new AuthenticationError(`Port ${CALLBACK_PORT} is already in use. Please free the port and try again.`));
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
reject(new AuthenticationError(err.message));
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
// Timeout after 5 minutes
|
|
230
|
+
setTimeout(() => {
|
|
231
|
+
server.close();
|
|
232
|
+
reject(new AuthenticationError('Authentication timed out'));
|
|
233
|
+
}, 5 * 60 * 1000);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Generate PKCE code verifier and challenge
|
|
238
|
+
*/
|
|
239
|
+
generatePKCE() {
|
|
240
|
+
// Generate random code verifier (43-128 characters)
|
|
241
|
+
const verifier = crypto.randomBytes(32).toString('base64url');
|
|
242
|
+
// Create code challenge using SHA-256
|
|
243
|
+
const challenge = crypto
|
|
244
|
+
.createHash('sha256')
|
|
245
|
+
.update(verifier)
|
|
246
|
+
.digest('base64url');
|
|
247
|
+
return { verifier, challenge };
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Build the OAuth2 authorization URL
|
|
251
|
+
*/
|
|
252
|
+
buildAuthorizationUrl(redirectUri) {
|
|
253
|
+
const url = new URL(this.authConfig.authorizationUrl);
|
|
254
|
+
// Generate PKCE
|
|
255
|
+
const pkce = this.generatePKCE();
|
|
256
|
+
this.codeVerifier = pkce.verifier;
|
|
257
|
+
url.searchParams.set('response_type', 'code');
|
|
258
|
+
url.searchParams.set('client_id', this.clientId);
|
|
259
|
+
url.searchParams.set('redirect_uri', redirectUri);
|
|
260
|
+
url.searchParams.set('code_challenge', pkce.challenge);
|
|
261
|
+
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(' '));
|
|
264
|
+
}
|
|
265
|
+
// Generate state for CSRF protection
|
|
266
|
+
const state = crypto.randomBytes(16).toString('base64url');
|
|
267
|
+
url.searchParams.set('state', state);
|
|
268
|
+
return url.toString();
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Exchange authorization code for access token
|
|
272
|
+
*/
|
|
273
|
+
async exchangeCodeForToken(code) {
|
|
274
|
+
const redirectUri = `http://127.0.0.1:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
|
275
|
+
const params = new URLSearchParams({
|
|
276
|
+
grant_type: 'authorization_code',
|
|
277
|
+
code,
|
|
278
|
+
redirect_uri: redirectUri,
|
|
279
|
+
client_id: this.clientId,
|
|
280
|
+
code_verifier: this.codeVerifier,
|
|
281
|
+
});
|
|
282
|
+
// Only include client_secret if set (public clients don't have one)
|
|
283
|
+
if (this.clientSecret) {
|
|
284
|
+
params.set('client_secret', this.clientSecret);
|
|
285
|
+
}
|
|
286
|
+
const response = await fetch(this.authConfig.tokenUrl, {
|
|
287
|
+
method: 'POST',
|
|
288
|
+
headers: {
|
|
289
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
290
|
+
},
|
|
291
|
+
body: params.toString(),
|
|
292
|
+
});
|
|
293
|
+
if (!response.ok) {
|
|
294
|
+
const errorText = await response.text();
|
|
295
|
+
throw new AuthenticationError(`Token exchange failed: ${response.status} ${errorText}`);
|
|
296
|
+
}
|
|
297
|
+
const tokenResponse = (await response.json());
|
|
298
|
+
this.accessToken = tokenResponse.access_token;
|
|
299
|
+
this.refreshToken = tokenResponse.refresh_token;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Set access token directly (for testing or manual token entry)
|
|
303
|
+
*/
|
|
304
|
+
setAccessToken(token) {
|
|
305
|
+
this.accessToken = token;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
//# sourceMappingURL=auth-manager.js.map
|
package/dist/cli.d.ts
ADDED