@duckfly/proxy 1.0.4 → 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 +190 -129
- package/package.json +2 -2
- package/src/proxy-server.js +29 -7
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,
|
|
@@ -52,9 +106,10 @@ let config = {
|
|
|
52
106
|
domainPackageId: null,
|
|
53
107
|
proxyPort: 8080,
|
|
54
108
|
targetUrl: null,
|
|
109
|
+
hostAlias: null,
|
|
55
110
|
};
|
|
56
111
|
|
|
57
|
-
|
|
112
|
+
// ─── Config persistence ──────────────────────────────────────────
|
|
58
113
|
|
|
59
114
|
function loadConfig() {
|
|
60
115
|
try {
|
|
@@ -64,7 +119,7 @@ function loadConfig() {
|
|
|
64
119
|
config = { ...config, ...rest };
|
|
65
120
|
return true;
|
|
66
121
|
}
|
|
67
|
-
} catch
|
|
122
|
+
} catch {
|
|
68
123
|
console.log(chalk.yellow('⚠️ Erro ao carregar configuração salva'));
|
|
69
124
|
}
|
|
70
125
|
return false;
|
|
@@ -74,11 +129,13 @@ function saveConfig() {
|
|
|
74
129
|
try {
|
|
75
130
|
const { token, appName, appUrl, domainPackageId, proxyPort, targetUrl, hostAlias } = config;
|
|
76
131
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ token, appName, appUrl, domainPackageId, proxyPort, targetUrl, hostAlias }, null, 2));
|
|
77
|
-
} catch
|
|
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('|/-\\');
|
|
@@ -87,7 +144,6 @@ async function validateToken(token) {
|
|
|
87
144
|
try {
|
|
88
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) {
|
|
@@ -113,54 +169,55 @@ async function validateToken(token) {
|
|
|
113
169
|
}
|
|
114
170
|
}
|
|
115
171
|
|
|
116
|
-
|
|
117
|
-
|
|
172
|
+
// ─── Hybrid prompts ──────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
async function askTokenIfMissing(cliArgs) {
|
|
175
|
+
if (cliArgs.token) {
|
|
176
|
+
config.token = String(cliArgs.token).trim();
|
|
177
|
+
} else {
|
|
118
178
|
const { token } = await inquirer.prompt([
|
|
119
179
|
{
|
|
120
180
|
type: 'password',
|
|
121
181
|
name: 'token',
|
|
122
182
|
message: chalk.yellow('🔑 Cole seu token do Duckfly:'),
|
|
123
183
|
mask: '*',
|
|
124
|
-
validate: (input)
|
|
125
|
-
|
|
126
|
-
return 'Token não pode ser vazio';
|
|
127
|
-
}
|
|
128
|
-
return true;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
184
|
+
validate: input => (!!input && input.trim().length > 0) || 'Token não pode ser vazio',
|
|
185
|
+
},
|
|
131
186
|
]);
|
|
132
|
-
|
|
133
|
-
config.token = token;
|
|
134
|
-
|
|
135
|
-
console.log('');
|
|
136
|
-
const valid = await validateToken(config.token);
|
|
137
|
-
|
|
138
|
-
if (!valid) {
|
|
139
|
-
console.log(chalk.red('❌ Token inválido. Não é possível continuar.\n'));
|
|
140
|
-
console.log(chalk.yellow('💡 Obtenha um token válido em: ') + chalk.white('https://duckfly.dev\n'));
|
|
141
|
-
process.exit(1);
|
|
142
|
-
}
|
|
187
|
+
config.token = token.trim();
|
|
143
188
|
}
|
|
144
189
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (isNaN(port) || port < 1 || port > 65535) {
|
|
154
|
-
return 'Porta inválida (1-65535)';
|
|
155
|
-
}
|
|
156
|
-
return true;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
]);
|
|
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);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
160
198
|
|
|
161
|
-
|
|
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)';
|
|
213
|
+
return true;
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
]);
|
|
217
|
+
config.proxyPort = proxyPort;
|
|
218
|
+
}
|
|
162
219
|
|
|
163
|
-
//
|
|
220
|
+
// Host alias (automatic)
|
|
164
221
|
config.hostAlias = null;
|
|
165
222
|
if (config.appUrl) {
|
|
166
223
|
try {
|
|
@@ -171,37 +228,36 @@ async function askQuestions() {
|
|
|
171
228
|
config.hostAlias = appHost;
|
|
172
229
|
console.log('');
|
|
173
230
|
console.log(chalk.blue(' O domínio da aplicação (') + chalk.bold.white(appHost) + chalk.blue(') difere do proxy local.'));
|
|
174
|
-
console.log(chalk.blue(' As requisições
|
|
231
|
+
console.log(chalk.blue(' As requisições observadas serão enviadas com o domínio ') + chalk.bold.white(appHost));
|
|
175
232
|
console.log(chalk.blue(' para que possam ser processadas corretamente pela aplicação.\n'));
|
|
176
233
|
}
|
|
177
234
|
} catch { }
|
|
178
235
|
}
|
|
179
236
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
name: 'targetUrl',
|
|
187
|
-
message: chalk.yellow('🎯 URL de destino (para onde encaminhar as requisições):'),
|
|
188
|
-
default: defaultTarget,
|
|
189
|
-
validate: (input) => {
|
|
190
|
-
try {
|
|
191
|
-
new URL(input);
|
|
192
|
-
return true;
|
|
193
|
-
} catch {
|
|
194
|
-
return 'URL inválida (ex: http://localhost:3000)';
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
]);
|
|
199
|
-
|
|
200
|
-
config.targetUrl = targetUrl;
|
|
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);
|
|
201
243
|
|
|
202
|
-
|
|
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
|
+
}
|
|
203
257
|
}
|
|
204
258
|
|
|
259
|
+
// ─── Proxy startup ───────────────────────────────────────────────
|
|
260
|
+
|
|
205
261
|
let proxyServer = null;
|
|
206
262
|
|
|
207
263
|
async function startProxy() {
|
|
@@ -220,68 +276,73 @@ async function startProxy() {
|
|
|
220
276
|
console.log(chalk.yellow(' Encaminhando para: ') + chalk.white(config.targetUrl));
|
|
221
277
|
console.log(chalk.gray('─'.repeat(60)));
|
|
222
278
|
console.log(chalk.blue('\n💡 Dica: Configure sua aplicação para apontar para ') + chalk.white(`http://localhost:${config.proxyPort}`));
|
|
223
|
-
console.log(chalk.blue(' Todas as requisições serão
|
|
279
|
+
console.log(chalk.blue(' Todas as requisições serão observadas e documentadas automaticamente!\n'));
|
|
224
280
|
console.log(chalk.gray('Pressione Ctrl+C para parar o proxy\n'));
|
|
225
281
|
|
|
226
282
|
saveConfig();
|
|
227
|
-
|
|
228
283
|
} catch (error) {
|
|
229
284
|
console.log(chalk.red('❌ Erro ao iniciar proxy: ') + chalk.gray(error.message));
|
|
230
285
|
process.exit(1);
|
|
231
286
|
}
|
|
232
287
|
}
|
|
233
288
|
|
|
234
|
-
|
|
235
|
-
console.clear();
|
|
289
|
+
// ─── Main ────────────────────────────────────────────────────────
|
|
236
290
|
|
|
237
|
-
|
|
291
|
+
async function main() {
|
|
292
|
+
const cliArgs = parseArgs();
|
|
238
293
|
|
|
239
|
-
|
|
240
|
-
|
|
294
|
+
if (cliArgs.help) {
|
|
295
|
+
showHelp();
|
|
296
|
+
process.exit(0);
|
|
297
|
+
}
|
|
241
298
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
console.log(chalk.gray('─'.repeat(60)));
|
|
245
|
-
console.log(chalk.yellow(' App: ') + chalk.white(config.appName || 'N/A'));
|
|
246
|
-
console.log(chalk.yellow(' Porta: ') + chalk.white(config.proxyPort));
|
|
247
|
-
console.log(chalk.yellow(' Alvo: ') + chalk.white(config.targetUrl));
|
|
248
|
-
console.log(chalk.gray('─'.repeat(60)) + '\n');
|
|
299
|
+
console.clear();
|
|
300
|
+
console.log(DUCKFLY_LOGO);
|
|
249
301
|
|
|
250
|
-
|
|
251
|
-
{
|
|
252
|
-
type: 'confirm',
|
|
253
|
-
name: 'useExisting',
|
|
254
|
-
message: chalk.yellow('Deseja usar a configuração salva?'),
|
|
255
|
-
default: true
|
|
256
|
-
}
|
|
257
|
-
]);
|
|
302
|
+
const hasCli = hasCliArgs(cliArgs);
|
|
258
303
|
|
|
259
|
-
|
|
304
|
+
// If no CLI args, try loading saved config
|
|
305
|
+
if (!hasCli) {
|
|
306
|
+
const hasConfig = loadConfig();
|
|
260
307
|
|
|
261
|
-
if (
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
config.
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
try { fs.unlinkSync(CONFIG_FILE); } catch { }
|
|
269
|
-
}
|
|
270
|
-
}
|
|
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');
|
|
271
315
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
console.log('');
|
|
276
|
-
const valid = await validateToken(config.token);
|
|
316
|
+
const { useExisting } = await inquirer.prompt([
|
|
317
|
+
{ type: 'confirm', name: 'useExisting', message: chalk.yellow('Deseja usar a configuração salva?'), default: true },
|
|
318
|
+
]);
|
|
277
319
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
+
}
|
|
282
339
|
}
|
|
283
340
|
}
|
|
284
341
|
|
|
342
|
+
// Fresh setup (interactive or CLI args)
|
|
343
|
+
await askTokenIfMissing(cliArgs);
|
|
344
|
+
await askProxyConfig(cliArgs);
|
|
345
|
+
saveConfig();
|
|
285
346
|
await startProxy();
|
|
286
347
|
}
|
|
287
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",
|
package/src/proxy-server.js
CHANGED
|
@@ -45,15 +45,28 @@ class ProxyServer {
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
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
|
+
|
|
48
53
|
let bodyChunks = [];
|
|
54
|
+
let bodySize = 0;
|
|
55
|
+
let overflow = false;
|
|
49
56
|
|
|
50
57
|
proxyRes.on('data', (chunk) => {
|
|
51
|
-
|
|
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
|
+
}
|
|
52
67
|
});
|
|
53
68
|
|
|
54
69
|
proxyRes.on('end', () => {
|
|
55
|
-
const responseBody = Buffer.concat(bodyChunks);
|
|
56
|
-
|
|
57
70
|
const statusColor = proxyRes.statusCode >= 400 ? chalk.red :
|
|
58
71
|
proxyRes.statusCode >= 300 ? chalk.yellow :
|
|
59
72
|
chalk.green;
|
|
@@ -62,9 +75,18 @@ class ProxyServer {
|
|
|
62
75
|
chalk.blue('←'),
|
|
63
76
|
statusColor(proxyRes.statusCode),
|
|
64
77
|
chalk.gray(req.url),
|
|
65
|
-
chalk.dim(`${
|
|
78
|
+
chalk.dim(`${bodySize} bytes`)
|
|
66
79
|
);
|
|
67
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
|
+
|
|
68
90
|
this.captureResponse(req, proxyRes, responseBody);
|
|
69
91
|
});
|
|
70
92
|
});
|
|
@@ -105,7 +127,7 @@ class ProxyServer {
|
|
|
105
127
|
}
|
|
106
128
|
};
|
|
107
129
|
} catch (error) {
|
|
108
|
-
console.log(chalk.yellow('⚠'), chalk.gray('Erro ao
|
|
130
|
+
console.log(chalk.yellow('⚠'), chalk.gray('Erro ao observar requisição:'), error.message);
|
|
109
131
|
}
|
|
110
132
|
}
|
|
111
133
|
|
|
@@ -147,7 +169,7 @@ class ProxyServer {
|
|
|
147
169
|
this.stats.captured++;
|
|
148
170
|
|
|
149
171
|
} catch (error) {
|
|
150
|
-
console.log(chalk.yellow('⚠'), chalk.gray('Erro ao
|
|
172
|
+
console.log(chalk.yellow('⚠'), chalk.gray('Erro ao observar resposta:'), error.message);
|
|
151
173
|
}
|
|
152
174
|
}
|
|
153
175
|
|
|
@@ -403,7 +425,7 @@ class ProxyServer {
|
|
|
403
425
|
process.stdout.write('\r' + chalk.bold.white('📊 Estatísticas:') + '\x1b[K\n');
|
|
404
426
|
process.stdout.write('\r' + chalk.yellow(' Uptime: ') + chalk.white(`${hours}h ${minutes}m ${seconds}s`) + '\x1b[K\n');
|
|
405
427
|
process.stdout.write('\r' + chalk.yellow(' Total: ') + chalk.white(this.stats.totalRequests) + '\x1b[K\n');
|
|
406
|
-
process.stdout.write('\r' + chalk.yellow('
|
|
428
|
+
process.stdout.write('\r' + chalk.yellow(' Observadas: ') + chalk.white(this.stats.captured) + '\x1b[K\n');
|
|
407
429
|
process.stdout.write('\r' + chalk.yellow(' Enviadas: ') + chalk.green(this.stats.sent) + '\x1b[K\n');
|
|
408
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');
|
|
409
431
|
process.stdout.write('\r' + chalk.yellow(' Na fila: ') + chalk.yellow(this.requestQueue.getQueueSize()) + '\x1b[K\n');
|