@duckfly/proxy 1.0.3 → 1.0.5
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/README.md +50 -5
- package/bin/cli.js +212 -129
- package/package.json +3 -3
- package/src/api-client.js +1 -1
- package/src/proxy-server.js +37 -8
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@ This allows your API documentation and MCP (Model Context Protocol) servers to b
|
|
|
24
24
|
- Continuously enriched API documentation
|
|
25
25
|
- MCP server generation for AI integrations
|
|
26
26
|
- Documentation aligned with real API usage
|
|
27
|
+
- No code changes required; just point your application to the proxy
|
|
27
28
|
|
|
28
29
|
## Quick Start
|
|
29
30
|
|
|
@@ -39,15 +40,41 @@ npm install -g @duckfly/proxy
|
|
|
39
40
|
npx @duckfly/proxy
|
|
40
41
|
```
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
### Interactive mode
|
|
44
|
+
|
|
45
|
+
When you run the CLI without arguments, it will guide you through the setup:
|
|
43
46
|
|
|
44
47
|
| Prompt | Description | Default |
|
|
45
48
|
|---|---|---|
|
|
46
|
-
| **Token** | Your Duckfly application token |
|
|
49
|
+
| **Token** | Your Duckfly application token | |
|
|
47
50
|
| **Proxy Port** | Port where the proxy listens | `8080` |
|
|
48
|
-
| **Target URL** | Your backend API address |
|
|
51
|
+
| **Target URL** | Your backend API address | App URL from token |
|
|
52
|
+
|
|
53
|
+
Configuration is saved locally in `.duckfly-proxy.json` so you don't need to enter it every time.
|
|
54
|
+
|
|
55
|
+
### Command line mode
|
|
56
|
+
|
|
57
|
+
You can pass all options via CLI arguments. The CLI will only ask for what is missing.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Full command line (no prompts)
|
|
61
|
+
duckfly-proxy --token abc123 --port 8080 --target http://localhost:3000
|
|
62
|
+
|
|
63
|
+
# Hybrid: pass what you know, it asks the rest
|
|
64
|
+
duckfly-proxy --token abc123
|
|
65
|
+
|
|
66
|
+
# With npx
|
|
67
|
+
npx @duckfly/proxy --token abc123 --port 8080 --target http://localhost:3000
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
All available flags:
|
|
49
71
|
|
|
50
|
-
|
|
72
|
+
| Flag | Description |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `--token <token>` | Duckfly application token |
|
|
75
|
+
| `--port <N>` | Proxy port (default: 8080) |
|
|
76
|
+
| `--target <url>` | URL to forward requests to |
|
|
77
|
+
| `--help` | Show help |
|
|
51
78
|
|
|
52
79
|
## How It Works
|
|
53
80
|
|
|
@@ -75,7 +102,25 @@ Configuration is saved locally in `.duckfly-proxy.json` so you don't need to re-
|
|
|
75
102
|
|
|
76
103
|
Duckfly Proxy processes only technical and structural information such as HTTP methods, routes, headers, and payload formats.
|
|
77
104
|
|
|
78
|
-
Request and response values are replaced with **placeholders** before being sent, ensuring no sensitive or user
|
|
105
|
+
Request and response values are replaced with **placeholders** before being sent, ensuring no sensitive or user specific data is transmitted.
|
|
106
|
+
|
|
107
|
+
### What is sent
|
|
108
|
+
|
|
109
|
+
- HTTP methods and route paths
|
|
110
|
+
- Header names (values are redacted)
|
|
111
|
+
- Body structure and field names (values replaced with type appropriate placeholders)
|
|
112
|
+
- Status codes and content types
|
|
113
|
+
|
|
114
|
+
### What is NOT sent
|
|
115
|
+
|
|
116
|
+
- Authentication tokens, passwords, or secrets
|
|
117
|
+
- Request/response body values
|
|
118
|
+
- Cookie values
|
|
119
|
+
- Query parameter values for sensitive fields
|
|
120
|
+
|
|
121
|
+
## Host Alias
|
|
122
|
+
|
|
123
|
+
When your application domain (from the Duckfly token) differs from `localhost`, the proxy automatically adjusts the Host header so that observed requests are associated with the correct domain. This happens automatically; no configuration is needed.
|
|
79
124
|
|
|
80
125
|
## License
|
|
81
126
|
|
package/bin/cli.js
CHANGED
|
@@ -8,42 +8,96 @@ const ApiClient = require('../src/api-client');
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
10
|
|
|
11
|
+
// ─── Constants ───────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
const DUCKFLY_API_URL = 'https://api.duckfly.dev';
|
|
14
|
+
const DUCKFLY_PROXY_URL = 'https://proxy.duckfly.dev';
|
|
15
|
+
|
|
16
|
+
const CONFIG_FILE = path.join(process.cwd(), '.duckfly-proxy.json');
|
|
17
|
+
|
|
18
|
+
// ─── Logo ────────────────────────────────────────────────────────
|
|
19
|
+
|
|
11
20
|
const yellow = chalk.hex('#FFD54F');
|
|
12
21
|
const gold = chalk.hex('#FFC107');
|
|
13
22
|
|
|
14
23
|
const DUCKFLY_LOGO = `
|
|
15
24
|
${yellow(`
|
|
16
|
-
@@@@@@@@@
|
|
17
|
-
@%+-------=%@
|
|
18
|
-
@@+-----------=@@
|
|
19
|
-
@@=-----=##=-----@@
|
|
20
|
-
@*------@@=#---+*%@
|
|
21
|
-
@+------+%%+-=%*+*%@@
|
|
22
|
-
@*---------=#%++%+++#%%@
|
|
23
|
-
@@=-------=@%#++++++++%@
|
|
24
|
-
@@+-------=*%%@@@@@@@
|
|
25
|
-
@@@*-------===%@
|
|
26
|
-
@%+---------------#@
|
|
27
|
-
@%----------+--------#@
|
|
28
|
-
@@@@%=------------#-------=@@
|
|
29
|
-
@*------+*=-------%-------=@@
|
|
30
|
-
@%--+@*+---------##-------+@
|
|
31
|
-
@%=-=#%%#+----+%*------=+@@
|
|
32
|
-
@@*==+%%##%%#=-----===*@@@
|
|
33
|
-
@@#=============+*%#+=+*@@
|
|
34
|
-
@@@%****#%%*+#@@@@%@@@
|
|
35
|
-
@@@@%#++#@@@
|
|
25
|
+
@@@@@@@@@
|
|
26
|
+
@%+-------=%@
|
|
27
|
+
@@+-----------=@@
|
|
28
|
+
@@=-----=##=-----@@
|
|
29
|
+
@*------@@=#---+*%@
|
|
30
|
+
@+------+%%+-=%*+*%@@
|
|
31
|
+
@*---------=#%++%+++#%%@
|
|
32
|
+
@@=-------=@%#++++++++%@
|
|
33
|
+
@@+-------=*%%@@@@@@@
|
|
34
|
+
@@@*-------===%@
|
|
35
|
+
@%+---------------#@
|
|
36
|
+
@%----------+--------#@
|
|
37
|
+
@@@@%=------------#-------=@@
|
|
38
|
+
@*------+*=-------%-------=@@
|
|
39
|
+
@%--+@*+---------##-------+@
|
|
40
|
+
@%=-=#%%#+----+%*------=+@@
|
|
41
|
+
@@*==+%%##%%#=-----===*@@@
|
|
42
|
+
@@#=============+*%#+=+*@@
|
|
43
|
+
@@@%****#%%*+#@@@@%@@@
|
|
44
|
+
@@@@%#++#@@@
|
|
36
45
|
`)}
|
|
37
46
|
${gold('═══════════════════════════════════════════════════════════════════════════════════════════')}
|
|
38
47
|
${chalk.bold.yellow(' 🦆 DUCKFLY PROXY v1.0.0 ')}
|
|
39
48
|
${gold('═══════════════════════════════════════════════════════════════════════════════════════════')}
|
|
40
|
-
${chalk.gray('
|
|
49
|
+
${chalk.gray(' Observe and document your APIs automatically ')}
|
|
41
50
|
${chalk.gray(' https://duckfly.dev ')}
|
|
42
51
|
${gold('═══════════════════════════════════════════════════════════════════════════════════════════')}
|
|
43
52
|
`;
|
|
44
53
|
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
// ─── CLI arg parsing ─────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
function parseArgs() {
|
|
57
|
+
const argv = process.argv.slice(2);
|
|
58
|
+
const args = {};
|
|
59
|
+
for (let i = 0; i < argv.length; i++) {
|
|
60
|
+
const arg = argv[i];
|
|
61
|
+
if (!arg.startsWith('--')) continue;
|
|
62
|
+
const key = arg.slice(2);
|
|
63
|
+
const next = argv[i + 1];
|
|
64
|
+
if (!next || next.startsWith('--')) {
|
|
65
|
+
args[key] = true;
|
|
66
|
+
} else {
|
|
67
|
+
args[key] = next;
|
|
68
|
+
i++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return args;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const CLI_KEYS = ['token', 'port', 'target'];
|
|
75
|
+
|
|
76
|
+
function hasCliArgs(cliArgs) {
|
|
77
|
+
return CLI_KEYS.some(k => cliArgs[k] !== undefined);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─── Help ────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
function showHelp() {
|
|
83
|
+
console.log(chalk.bold.yellow('\n🦆 Duckfly Proxy\n'));
|
|
84
|
+
console.log(chalk.white('Uso: duckfly-proxy [opções]\n'));
|
|
85
|
+
console.log(chalk.white('Opções:'));
|
|
86
|
+
console.log(chalk.yellow(' --token <token> ') + chalk.gray('Token do Duckfly'));
|
|
87
|
+
console.log(chalk.yellow(' --port <N> ') + chalk.gray('Porta do proxy (default: 8080)'));
|
|
88
|
+
console.log(chalk.yellow(' --target <url> ') + chalk.gray('URL de destino para encaminhar as requisições'));
|
|
89
|
+
console.log(chalk.yellow(' --help ') + chalk.gray('Mostrar esta ajuda'));
|
|
90
|
+
console.log('');
|
|
91
|
+
console.log(chalk.white('Exemplos:'));
|
|
92
|
+
console.log(chalk.gray(' # Modo interativo completo'));
|
|
93
|
+
console.log(chalk.white(' duckfly-proxy\n'));
|
|
94
|
+
console.log(chalk.gray(' # Tudo via CLI (sem prompts)'));
|
|
95
|
+
console.log(chalk.white(' duckfly-proxy --token abc123 --port 8080 --target http://localhost:3000\n'));
|
|
96
|
+
console.log(chalk.gray(' # Híbrido (passa o que sabe, pergunta o que falta)'));
|
|
97
|
+
console.log(chalk.white(' duckfly-proxy --token abc123\n'));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── State ───────────────────────────────────────────────────────
|
|
47
101
|
|
|
48
102
|
let config = {
|
|
49
103
|
token: null,
|
|
@@ -51,21 +105,21 @@ let config = {
|
|
|
51
105
|
appUrl: null,
|
|
52
106
|
domainPackageId: null,
|
|
53
107
|
proxyPort: 8080,
|
|
54
|
-
targetUrl:
|
|
55
|
-
|
|
56
|
-
proxyUrl: DUCKFLY_PROXY_URL
|
|
108
|
+
targetUrl: null,
|
|
109
|
+
hostAlias: null,
|
|
57
110
|
};
|
|
58
111
|
|
|
59
|
-
|
|
112
|
+
// ─── Config persistence ──────────────────────────────────────────
|
|
60
113
|
|
|
61
114
|
function loadConfig() {
|
|
62
115
|
try {
|
|
63
116
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
64
117
|
const saved = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
65
|
-
|
|
118
|
+
const { apiUrl, proxyUrl, ...rest } = saved;
|
|
119
|
+
config = { ...config, ...rest };
|
|
66
120
|
return true;
|
|
67
121
|
}
|
|
68
|
-
} catch
|
|
122
|
+
} catch {
|
|
69
123
|
console.log(chalk.yellow('⚠️ Erro ao carregar configuração salva'));
|
|
70
124
|
}
|
|
71
125
|
return false;
|
|
@@ -73,21 +127,23 @@ function loadConfig() {
|
|
|
73
127
|
|
|
74
128
|
function saveConfig() {
|
|
75
129
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
130
|
+
const { token, appName, appUrl, domainPackageId, proxyPort, targetUrl, hostAlias } = config;
|
|
131
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ token, appName, appUrl, domainPackageId, proxyPort, targetUrl, hostAlias }, null, 2));
|
|
132
|
+
} catch {
|
|
78
133
|
console.log(chalk.yellow('⚠️ Não foi possível salvar a configuração'));
|
|
79
134
|
}
|
|
80
135
|
}
|
|
81
136
|
|
|
137
|
+
// ─── Token validation ────────────────────────────────────────────
|
|
138
|
+
|
|
82
139
|
async function validateToken(token) {
|
|
83
140
|
const spinner = new Spinner(chalk.yellow('%s Validando token...'));
|
|
84
141
|
spinner.setSpinnerString('|/-\\');
|
|
85
142
|
spinner.start();
|
|
86
143
|
|
|
87
144
|
try {
|
|
88
|
-
const apiClient = new ApiClient(
|
|
145
|
+
const apiClient = new ApiClient(DUCKFLY_API_URL, DUCKFLY_PROXY_URL, token);
|
|
89
146
|
const result = await apiClient.validateToken();
|
|
90
|
-
|
|
91
147
|
spinner.stop(true);
|
|
92
148
|
|
|
93
149
|
if (result.valid) {
|
|
@@ -96,7 +152,6 @@ async function validateToken(token) {
|
|
|
96
152
|
console.log(chalk.gray('─'.repeat(60)));
|
|
97
153
|
console.log(chalk.yellow(' Nome: ') + chalk.white(result.appName));
|
|
98
154
|
console.log(chalk.yellow(' URL: ') + chalk.white(result.appUrl));
|
|
99
|
-
console.log(chalk.yellow(' Package ID: ') + chalk.white(result.domainPackageId));
|
|
100
155
|
console.log(chalk.gray('─'.repeat(60)) + '\n');
|
|
101
156
|
|
|
102
157
|
config.appName = result.appName;
|
|
@@ -114,81 +169,101 @@ async function validateToken(token) {
|
|
|
114
169
|
}
|
|
115
170
|
}
|
|
116
171
|
|
|
117
|
-
|
|
118
|
-
|
|
172
|
+
// ─── Hybrid prompts ──────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
async function askTokenIfMissing(cliArgs) {
|
|
175
|
+
if (cliArgs.token) {
|
|
176
|
+
config.token = String(cliArgs.token).trim();
|
|
177
|
+
} else {
|
|
119
178
|
const { token } = await inquirer.prompt([
|
|
120
179
|
{
|
|
121
180
|
type: 'password',
|
|
122
181
|
name: 'token',
|
|
123
182
|
message: chalk.yellow('🔑 Cole seu token do Duckfly:'),
|
|
124
183
|
mask: '*',
|
|
125
|
-
validate: (input)
|
|
126
|
-
|
|
127
|
-
return 'Token não pode ser vazio';
|
|
128
|
-
}
|
|
129
|
-
return true;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
184
|
+
validate: input => (!!input && input.trim().length > 0) || 'Token não pode ser vazio',
|
|
185
|
+
},
|
|
132
186
|
]);
|
|
187
|
+
config.token = token.trim();
|
|
188
|
+
}
|
|
133
189
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
console.log(chalk.red('❌ Token inválido. Não é possível continuar.\n'));
|
|
141
|
-
console.log(chalk.yellow('💡 Obtenha um token válido em: ') + chalk.white('https://duckfly.dev\n'));
|
|
142
|
-
process.exit(1);
|
|
143
|
-
}
|
|
190
|
+
console.log('');
|
|
191
|
+
const valid = await validateToken(config.token);
|
|
192
|
+
if (!valid) {
|
|
193
|
+
console.log(chalk.red('❌ Token inválido. Não é possível continuar.\n'));
|
|
194
|
+
console.log(chalk.yellow('💡 Obtenha um token válido em: ') + chalk.white('https://duckfly.dev\n'));
|
|
195
|
+
process.exit(1);
|
|
144
196
|
}
|
|
197
|
+
}
|
|
145
198
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return true;
|
|
161
|
-
}
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
type: 'input',
|
|
165
|
-
name: 'targetUrl',
|
|
166
|
-
message: chalk.yellow('🎯 URL de destino (para onde encaminhar as requisições):'),
|
|
167
|
-
default: config.targetUrl,
|
|
168
|
-
validate: (input) => {
|
|
169
|
-
try {
|
|
170
|
-
new URL(input);
|
|
199
|
+
async function askProxyConfig(cliArgs) {
|
|
200
|
+
// Port
|
|
201
|
+
if (cliArgs.port != null) {
|
|
202
|
+
config.proxyPort = parseInt(cliArgs.port, 10);
|
|
203
|
+
} else {
|
|
204
|
+
const { proxyPort } = await inquirer.prompt([
|
|
205
|
+
{
|
|
206
|
+
type: 'input',
|
|
207
|
+
name: 'proxyPort',
|
|
208
|
+
message: chalk.yellow('🔌 Porta do proxy (porta que vai receber as requisições):'),
|
|
209
|
+
default: config.proxyPort,
|
|
210
|
+
validate: input => {
|
|
211
|
+
const port = parseInt(input, 10);
|
|
212
|
+
if (Number.isNaN(port) || port < 1 || port > 65535) return 'Porta inválida (1-65535)';
|
|
171
213
|
return true;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
];
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
]);
|
|
217
|
+
config.proxyPort = proxyPort;
|
|
218
|
+
}
|
|
178
219
|
|
|
179
|
-
|
|
220
|
+
// Host alias (automatic)
|
|
221
|
+
config.hostAlias = null;
|
|
222
|
+
if (config.appUrl) {
|
|
223
|
+
try {
|
|
224
|
+
const appHost = new URL(config.appUrl).host;
|
|
225
|
+
const proxyHost = `localhost:${config.proxyPort}`;
|
|
226
|
+
|
|
227
|
+
if (appHost && appHost !== proxyHost && appHost !== `127.0.0.1:${config.proxyPort}`) {
|
|
228
|
+
config.hostAlias = appHost;
|
|
229
|
+
console.log('');
|
|
230
|
+
console.log(chalk.blue(' O domínio da aplicação (') + chalk.bold.white(appHost) + chalk.blue(') difere do proxy local.'));
|
|
231
|
+
console.log(chalk.blue(' As requisições observadas serão enviadas com o domínio ') + chalk.bold.white(appHost));
|
|
232
|
+
console.log(chalk.blue(' para que possam ser processadas corretamente pela aplicação.\n'));
|
|
233
|
+
}
|
|
234
|
+
} catch { }
|
|
235
|
+
}
|
|
180
236
|
|
|
181
|
-
|
|
237
|
+
// Target URL
|
|
238
|
+
if (cliArgs.target) {
|
|
239
|
+
config.targetUrl = cliArgs.target;
|
|
240
|
+
} else {
|
|
241
|
+
const isWildcard = config.appUrl && config.appUrl.includes('*');
|
|
242
|
+
const defaultTarget = isWildcard ? null : (config.appUrl || config.targetUrl);
|
|
182
243
|
|
|
183
|
-
|
|
244
|
+
const { targetUrl } = await inquirer.prompt([
|
|
245
|
+
{
|
|
246
|
+
type: 'input',
|
|
247
|
+
name: 'targetUrl',
|
|
248
|
+
message: chalk.yellow('🎯 URL de destino (para onde encaminhar as requisições):'),
|
|
249
|
+
default: defaultTarget,
|
|
250
|
+
validate: input => {
|
|
251
|
+
try { new URL(input); return true; } catch { return 'URL inválida (ex: http://localhost:3000)'; }
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
]);
|
|
255
|
+
config.targetUrl = targetUrl;
|
|
256
|
+
}
|
|
184
257
|
}
|
|
185
258
|
|
|
259
|
+
// ─── Proxy startup ───────────────────────────────────────────────
|
|
260
|
+
|
|
186
261
|
let proxyServer = null;
|
|
187
262
|
|
|
188
263
|
async function startProxy() {
|
|
189
264
|
console.log(chalk.yellow('\n🚀 Iniciando Duckfly Proxy...\n'));
|
|
190
265
|
|
|
191
|
-
proxyServer = new ProxyServer(config);
|
|
266
|
+
proxyServer = new ProxyServer({ ...config, apiUrl: DUCKFLY_API_URL, proxyUrl: DUCKFLY_PROXY_URL });
|
|
192
267
|
|
|
193
268
|
try {
|
|
194
269
|
await proxyServer.start();
|
|
@@ -200,66 +275,74 @@ async function startProxy() {
|
|
|
200
275
|
console.log(chalk.yellow(' Proxy rodando em: ') + chalk.white(`http://localhost:${config.proxyPort}`));
|
|
201
276
|
console.log(chalk.yellow(' Encaminhando para: ') + chalk.white(config.targetUrl));
|
|
202
277
|
console.log(chalk.gray('─'.repeat(60)));
|
|
203
|
-
console.log(chalk.
|
|
204
|
-
console.log(chalk.
|
|
278
|
+
console.log(chalk.blue('\n💡 Dica: Configure sua aplicação para apontar para ') + chalk.white(`http://localhost:${config.proxyPort}`));
|
|
279
|
+
console.log(chalk.blue(' Todas as requisições serão observadas e documentadas automaticamente!\n'));
|
|
205
280
|
console.log(chalk.gray('Pressione Ctrl+C para parar o proxy\n'));
|
|
206
281
|
|
|
207
282
|
saveConfig();
|
|
208
|
-
|
|
209
283
|
} catch (error) {
|
|
210
284
|
console.log(chalk.red('❌ Erro ao iniciar proxy: ') + chalk.gray(error.message));
|
|
211
285
|
process.exit(1);
|
|
212
286
|
}
|
|
213
287
|
}
|
|
214
288
|
|
|
215
|
-
|
|
216
|
-
console.clear();
|
|
289
|
+
// ─── Main ────────────────────────────────────────────────────────
|
|
217
290
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const hasConfig = loadConfig();
|
|
221
|
-
let useExisting = false;
|
|
291
|
+
async function main() {
|
|
292
|
+
const cliArgs = parseArgs();
|
|
222
293
|
|
|
223
|
-
if (
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
console.log(chalk.yellow(' Porta: ') + chalk.white(config.proxyPort));
|
|
228
|
-
console.log(chalk.yellow(' Alvo: ') + chalk.white(config.targetUrl));
|
|
229
|
-
console.log(chalk.gray('─'.repeat(60)) + '\n');
|
|
294
|
+
if (cliArgs.help) {
|
|
295
|
+
showHelp();
|
|
296
|
+
process.exit(0);
|
|
297
|
+
}
|
|
230
298
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
type: 'confirm',
|
|
234
|
-
name: 'useExisting',
|
|
235
|
-
message: chalk.yellow('Deseja usar a configuração salva?'),
|
|
236
|
-
default: true
|
|
237
|
-
}
|
|
238
|
-
]);
|
|
299
|
+
console.clear();
|
|
300
|
+
console.log(DUCKFLY_LOGO);
|
|
239
301
|
|
|
240
|
-
|
|
302
|
+
const hasCli = hasCliArgs(cliArgs);
|
|
241
303
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
config.proxyPort = 8080;
|
|
246
|
-
config.targetUrl = 'http://localhost:3000';
|
|
247
|
-
}
|
|
248
|
-
}
|
|
304
|
+
// If no CLI args, try loading saved config
|
|
305
|
+
if (!hasCli) {
|
|
306
|
+
const hasConfig = loadConfig();
|
|
249
307
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
308
|
+
if (hasConfig) {
|
|
309
|
+
console.log(chalk.green('✅ Configuração anterior encontrada!\n'));
|
|
310
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
311
|
+
console.log(chalk.yellow(' App: ') + chalk.white(config.appName || 'N/A'));
|
|
312
|
+
console.log(chalk.yellow(' Porta: ') + chalk.white(config.proxyPort));
|
|
313
|
+
console.log(chalk.yellow(' Alvo: ') + chalk.white(config.targetUrl));
|
|
314
|
+
console.log(chalk.gray('─'.repeat(60)) + '\n');
|
|
255
315
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
316
|
+
const { useExisting } = await inquirer.prompt([
|
|
317
|
+
{ type: 'confirm', name: 'useExisting', message: chalk.yellow('Deseja usar a configuração salva?'), default: true },
|
|
318
|
+
]);
|
|
319
|
+
|
|
320
|
+
if (useExisting) {
|
|
321
|
+
console.log('');
|
|
322
|
+
const valid = await validateToken(config.token);
|
|
323
|
+
if (!valid) {
|
|
324
|
+
console.log(chalk.red('❌ Token salvo está inválido. Configurando novamente...\n'));
|
|
325
|
+
config.token = null;
|
|
326
|
+
} else {
|
|
327
|
+
await startProxy();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
config.token = null;
|
|
332
|
+
config.appName = null;
|
|
333
|
+
config.appUrl = null;
|
|
334
|
+
config.domainPackageId = null;
|
|
335
|
+
config.proxyPort = 8080;
|
|
336
|
+
config.targetUrl = null;
|
|
337
|
+
try { fs.unlinkSync(CONFIG_FILE); } catch { }
|
|
338
|
+
}
|
|
260
339
|
}
|
|
261
340
|
}
|
|
262
341
|
|
|
342
|
+
// Fresh setup (interactive or CLI args)
|
|
343
|
+
await askTokenIfMissing(cliArgs);
|
|
344
|
+
await askProxyConfig(cliArgs);
|
|
345
|
+
saveConfig();
|
|
263
346
|
await startProxy();
|
|
264
347
|
}
|
|
265
348
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duckfly/proxy",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Duckfly Proxy observes real API usage to continuously enrich and generate API documentation and MCP servers on Duckfly.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"duckfly",
|
|
21
21
|
"http",
|
|
22
22
|
"request",
|
|
23
|
-
"
|
|
23
|
+
"observe"
|
|
24
24
|
],
|
|
25
25
|
"author": "Duckfly",
|
|
26
26
|
"license": "MIT",
|
|
@@ -47,4 +47,4 @@
|
|
|
47
47
|
"publishConfig": {
|
|
48
48
|
"access": "public"
|
|
49
49
|
}
|
|
50
|
-
}
|
|
50
|
+
}
|
package/src/api-client.js
CHANGED
|
@@ -9,7 +9,7 @@ class ApiClient {
|
|
|
9
9
|
|
|
10
10
|
async validateToken() {
|
|
11
11
|
try {
|
|
12
|
-
const url = `${this.apiUrl}/internal/v1/core/token/${this.token}`;
|
|
12
|
+
const url = `${this.apiUrl}/internal/v1/core/token/${this.token}?source=plugin-proxy`;
|
|
13
13
|
|
|
14
14
|
const response = await axios.get(url, {
|
|
15
15
|
timeout: 30000,
|
package/src/proxy-server.js
CHANGED
|
@@ -9,6 +9,7 @@ const { sanitizeRequestData } = require('./sanitizer');
|
|
|
9
9
|
class ProxyServer {
|
|
10
10
|
constructor(config) {
|
|
11
11
|
this.config = config;
|
|
12
|
+
this.hostAlias = config.hostAlias || null;
|
|
12
13
|
this.proxy = httpProxy.createProxyServer({});
|
|
13
14
|
this.app = express();
|
|
14
15
|
this.server = null;
|
|
@@ -44,15 +45,28 @@ class ProxyServer {
|
|
|
44
45
|
});
|
|
45
46
|
|
|
46
47
|
this.proxy.on('proxyRes', (proxyRes, req, res) => {
|
|
48
|
+
const MAX_CAPTURE = 10 * 1024 * 1024; // 10MB
|
|
49
|
+
const contentType = (proxyRes.headers['content-type'] || '').toLowerCase();
|
|
50
|
+
const binaryTypes = ['image/', 'video/', 'audio/', 'application/octet-stream', 'application/pdf', 'application/zip'];
|
|
51
|
+
const skipBody = binaryTypes.some(t => contentType.includes(t));
|
|
52
|
+
|
|
47
53
|
let bodyChunks = [];
|
|
54
|
+
let bodySize = 0;
|
|
55
|
+
let overflow = false;
|
|
48
56
|
|
|
49
57
|
proxyRes.on('data', (chunk) => {
|
|
50
|
-
|
|
58
|
+
bodySize += chunk.length;
|
|
59
|
+
if (!skipBody && !overflow) {
|
|
60
|
+
if (bodySize <= MAX_CAPTURE) {
|
|
61
|
+
bodyChunks.push(chunk);
|
|
62
|
+
} else {
|
|
63
|
+
overflow = true;
|
|
64
|
+
bodyChunks = [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
51
67
|
});
|
|
52
68
|
|
|
53
69
|
proxyRes.on('end', () => {
|
|
54
|
-
const responseBody = Buffer.concat(bodyChunks);
|
|
55
|
-
|
|
56
70
|
const statusColor = proxyRes.statusCode >= 400 ? chalk.red :
|
|
57
71
|
proxyRes.statusCode >= 300 ? chalk.yellow :
|
|
58
72
|
chalk.green;
|
|
@@ -61,9 +75,18 @@ class ProxyServer {
|
|
|
61
75
|
chalk.blue('←'),
|
|
62
76
|
statusColor(proxyRes.statusCode),
|
|
63
77
|
chalk.gray(req.url),
|
|
64
|
-
chalk.dim(`${
|
|
78
|
+
chalk.dim(`${bodySize} bytes`)
|
|
65
79
|
);
|
|
66
80
|
|
|
81
|
+
let responseBody;
|
|
82
|
+
if (skipBody) {
|
|
83
|
+
responseBody = Buffer.from(`[Binary content: ${contentType}, ${bodySize} bytes]`);
|
|
84
|
+
} else if (overflow) {
|
|
85
|
+
responseBody = Buffer.from(`[Body too large: ${bodySize} bytes]`);
|
|
86
|
+
} else {
|
|
87
|
+
responseBody = Buffer.concat(bodyChunks);
|
|
88
|
+
}
|
|
89
|
+
|
|
67
90
|
this.captureResponse(req, proxyRes, responseBody);
|
|
68
91
|
});
|
|
69
92
|
});
|
|
@@ -85,10 +108,16 @@ class ProxyServer {
|
|
|
85
108
|
|
|
86
109
|
captureRequest(req, res) {
|
|
87
110
|
try {
|
|
111
|
+
const headers = { ...req.headers };
|
|
112
|
+
|
|
113
|
+
if (this.hostAlias) {
|
|
114
|
+
headers.host = this.hostAlias;
|
|
115
|
+
}
|
|
116
|
+
|
|
88
117
|
req._captureData = {
|
|
89
118
|
method: req.method,
|
|
90
119
|
url: req.url,
|
|
91
|
-
headers
|
|
120
|
+
headers,
|
|
92
121
|
body: this.parseBody(req.rawBody, req.headers['content-type']),
|
|
93
122
|
protocol: req.socket.encrypted ? 'https' : 'http',
|
|
94
123
|
timestamp: new Date().toISOString(),
|
|
@@ -98,7 +127,7 @@ class ProxyServer {
|
|
|
98
127
|
}
|
|
99
128
|
};
|
|
100
129
|
} catch (error) {
|
|
101
|
-
console.log(chalk.yellow('⚠'), chalk.gray('Erro ao
|
|
130
|
+
console.log(chalk.yellow('⚠'), chalk.gray('Erro ao observar requisição:'), error.message);
|
|
102
131
|
}
|
|
103
132
|
}
|
|
104
133
|
|
|
@@ -140,7 +169,7 @@ class ProxyServer {
|
|
|
140
169
|
this.stats.captured++;
|
|
141
170
|
|
|
142
171
|
} catch (error) {
|
|
143
|
-
console.log(chalk.yellow('⚠'), chalk.gray('Erro ao
|
|
172
|
+
console.log(chalk.yellow('⚠'), chalk.gray('Erro ao observar resposta:'), error.message);
|
|
144
173
|
}
|
|
145
174
|
}
|
|
146
175
|
|
|
@@ -396,7 +425,7 @@ class ProxyServer {
|
|
|
396
425
|
process.stdout.write('\r' + chalk.bold.white('📊 Estatísticas:') + '\x1b[K\n');
|
|
397
426
|
process.stdout.write('\r' + chalk.yellow(' Uptime: ') + chalk.white(`${hours}h ${minutes}m ${seconds}s`) + '\x1b[K\n');
|
|
398
427
|
process.stdout.write('\r' + chalk.yellow(' Total: ') + chalk.white(this.stats.totalRequests) + '\x1b[K\n');
|
|
399
|
-
process.stdout.write('\r' + chalk.yellow('
|
|
428
|
+
process.stdout.write('\r' + chalk.yellow(' Observadas: ') + chalk.white(this.stats.captured) + '\x1b[K\n');
|
|
400
429
|
process.stdout.write('\r' + chalk.yellow(' Enviadas: ') + chalk.green(this.stats.sent) + '\x1b[K\n');
|
|
401
430
|
process.stdout.write('\r' + chalk.yellow(' Falhas: ') + (this.stats.failed > 0 ? chalk.red(this.stats.failed) : chalk.gray(this.stats.failed)) + '\x1b[K\n');
|
|
402
431
|
process.stdout.write('\r' + chalk.yellow(' Na fila: ') + chalk.yellow(this.requestQueue.getQueueSize()) + '\x1b[K\n');
|