@apitap/core 1.0.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 +60 -0
- package/README.md +362 -0
- package/SKILL.md +270 -0
- package/dist/auth/crypto.d.ts +31 -0
- package/dist/auth/crypto.js +66 -0
- package/dist/auth/crypto.js.map +1 -0
- package/dist/auth/handoff.d.ts +29 -0
- package/dist/auth/handoff.js +180 -0
- package/dist/auth/handoff.js.map +1 -0
- package/dist/auth/manager.d.ts +46 -0
- package/dist/auth/manager.js +127 -0
- package/dist/auth/manager.js.map +1 -0
- package/dist/auth/oauth-refresh.d.ts +16 -0
- package/dist/auth/oauth-refresh.js +91 -0
- package/dist/auth/oauth-refresh.js.map +1 -0
- package/dist/auth/refresh.d.ts +43 -0
- package/dist/auth/refresh.js +217 -0
- package/dist/auth/refresh.js.map +1 -0
- package/dist/capture/anti-bot.d.ts +15 -0
- package/dist/capture/anti-bot.js +43 -0
- package/dist/capture/anti-bot.js.map +1 -0
- package/dist/capture/blocklist.d.ts +6 -0
- package/dist/capture/blocklist.js +70 -0
- package/dist/capture/blocklist.js.map +1 -0
- package/dist/capture/body-diff.d.ts +8 -0
- package/dist/capture/body-diff.js +102 -0
- package/dist/capture/body-diff.js.map +1 -0
- package/dist/capture/body-variables.d.ts +13 -0
- package/dist/capture/body-variables.js +142 -0
- package/dist/capture/body-variables.js.map +1 -0
- package/dist/capture/domain.d.ts +8 -0
- package/dist/capture/domain.js +34 -0
- package/dist/capture/domain.js.map +1 -0
- package/dist/capture/entropy.d.ts +33 -0
- package/dist/capture/entropy.js +100 -0
- package/dist/capture/entropy.js.map +1 -0
- package/dist/capture/filter.d.ts +11 -0
- package/dist/capture/filter.js +49 -0
- package/dist/capture/filter.js.map +1 -0
- package/dist/capture/graphql.d.ts +21 -0
- package/dist/capture/graphql.js +99 -0
- package/dist/capture/graphql.js.map +1 -0
- package/dist/capture/idle.d.ts +23 -0
- package/dist/capture/idle.js +44 -0
- package/dist/capture/idle.js.map +1 -0
- package/dist/capture/monitor.d.ts +26 -0
- package/dist/capture/monitor.js +183 -0
- package/dist/capture/monitor.js.map +1 -0
- package/dist/capture/oauth-detector.d.ts +18 -0
- package/dist/capture/oauth-detector.js +96 -0
- package/dist/capture/oauth-detector.js.map +1 -0
- package/dist/capture/pagination.d.ts +9 -0
- package/dist/capture/pagination.js +40 -0
- package/dist/capture/pagination.js.map +1 -0
- package/dist/capture/parameterize.d.ts +17 -0
- package/dist/capture/parameterize.js +63 -0
- package/dist/capture/parameterize.js.map +1 -0
- package/dist/capture/scrubber.d.ts +5 -0
- package/dist/capture/scrubber.js +38 -0
- package/dist/capture/scrubber.js.map +1 -0
- package/dist/capture/session.d.ts +46 -0
- package/dist/capture/session.js +445 -0
- package/dist/capture/session.js.map +1 -0
- package/dist/capture/token-detector.d.ts +16 -0
- package/dist/capture/token-detector.js +62 -0
- package/dist/capture/token-detector.js.map +1 -0
- package/dist/capture/verifier.d.ts +17 -0
- package/dist/capture/verifier.js +147 -0
- package/dist/capture/verifier.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +930 -0
- package/dist/cli.js.map +1 -0
- package/dist/discovery/auth.d.ts +17 -0
- package/dist/discovery/auth.js +81 -0
- package/dist/discovery/auth.js.map +1 -0
- package/dist/discovery/fetch.d.ts +17 -0
- package/dist/discovery/fetch.js +59 -0
- package/dist/discovery/fetch.js.map +1 -0
- package/dist/discovery/frameworks.d.ts +11 -0
- package/dist/discovery/frameworks.js +249 -0
- package/dist/discovery/frameworks.js.map +1 -0
- package/dist/discovery/index.d.ts +21 -0
- package/dist/discovery/index.js +219 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/discovery/openapi.d.ts +13 -0
- package/dist/discovery/openapi.js +175 -0
- package/dist/discovery/openapi.js.map +1 -0
- package/dist/discovery/probes.d.ts +9 -0
- package/dist/discovery/probes.js +70 -0
- package/dist/discovery/probes.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/inspect/report.d.ts +52 -0
- package/dist/inspect/report.js +191 -0
- package/dist/inspect/report.js.map +1 -0
- package/dist/mcp.d.ts +8 -0
- package/dist/mcp.js +526 -0
- package/dist/mcp.js.map +1 -0
- package/dist/orchestration/browse.d.ts +38 -0
- package/dist/orchestration/browse.js +198 -0
- package/dist/orchestration/browse.js.map +1 -0
- package/dist/orchestration/cache.d.ts +15 -0
- package/dist/orchestration/cache.js +24 -0
- package/dist/orchestration/cache.js.map +1 -0
- package/dist/plugin.d.ts +17 -0
- package/dist/plugin.js +158 -0
- package/dist/plugin.js.map +1 -0
- package/dist/read/decoders/deepwiki.d.ts +2 -0
- package/dist/read/decoders/deepwiki.js +148 -0
- package/dist/read/decoders/deepwiki.js.map +1 -0
- package/dist/read/decoders/grokipedia.d.ts +2 -0
- package/dist/read/decoders/grokipedia.js +210 -0
- package/dist/read/decoders/grokipedia.js.map +1 -0
- package/dist/read/decoders/hackernews.d.ts +2 -0
- package/dist/read/decoders/hackernews.js +168 -0
- package/dist/read/decoders/hackernews.js.map +1 -0
- package/dist/read/decoders/index.d.ts +2 -0
- package/dist/read/decoders/index.js +12 -0
- package/dist/read/decoders/index.js.map +1 -0
- package/dist/read/decoders/reddit.d.ts +2 -0
- package/dist/read/decoders/reddit.js +142 -0
- package/dist/read/decoders/reddit.js.map +1 -0
- package/dist/read/decoders/twitter.d.ts +12 -0
- package/dist/read/decoders/twitter.js +187 -0
- package/dist/read/decoders/twitter.js.map +1 -0
- package/dist/read/decoders/wikipedia.d.ts +2 -0
- package/dist/read/decoders/wikipedia.js +66 -0
- package/dist/read/decoders/wikipedia.js.map +1 -0
- package/dist/read/decoders/youtube.d.ts +2 -0
- package/dist/read/decoders/youtube.js +69 -0
- package/dist/read/decoders/youtube.js.map +1 -0
- package/dist/read/extract.d.ts +25 -0
- package/dist/read/extract.js +320 -0
- package/dist/read/extract.js.map +1 -0
- package/dist/read/index.d.ts +14 -0
- package/dist/read/index.js +66 -0
- package/dist/read/index.js.map +1 -0
- package/dist/read/peek.d.ts +9 -0
- package/dist/read/peek.js +137 -0
- package/dist/read/peek.js.map +1 -0
- package/dist/read/types.d.ts +44 -0
- package/dist/read/types.js +3 -0
- package/dist/read/types.js.map +1 -0
- package/dist/replay/engine.d.ts +53 -0
- package/dist/replay/engine.js +441 -0
- package/dist/replay/engine.js.map +1 -0
- package/dist/replay/truncate.d.ts +16 -0
- package/dist/replay/truncate.js +92 -0
- package/dist/replay/truncate.js.map +1 -0
- package/dist/serve.d.ts +31 -0
- package/dist/serve.js +149 -0
- package/dist/serve.js.map +1 -0
- package/dist/skill/generator.d.ts +44 -0
- package/dist/skill/generator.js +419 -0
- package/dist/skill/generator.js.map +1 -0
- package/dist/skill/importer.d.ts +26 -0
- package/dist/skill/importer.js +80 -0
- package/dist/skill/importer.js.map +1 -0
- package/dist/skill/search.d.ts +19 -0
- package/dist/skill/search.js +51 -0
- package/dist/skill/search.js.map +1 -0
- package/dist/skill/signing.d.ts +16 -0
- package/dist/skill/signing.js +34 -0
- package/dist/skill/signing.js.map +1 -0
- package/dist/skill/ssrf.d.ts +27 -0
- package/dist/skill/ssrf.js +210 -0
- package/dist/skill/ssrf.js.map +1 -0
- package/dist/skill/store.d.ts +7 -0
- package/dist/skill/store.js +93 -0
- package/dist/skill/store.js.map +1 -0
- package/dist/stats/report.d.ts +26 -0
- package/dist/stats/report.js +157 -0
- package/dist/stats/report.js.map +1 -0
- package/dist/types.d.ts +214 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +58 -0
- package/src/auth/crypto.ts +92 -0
- package/src/auth/handoff.ts +229 -0
- package/src/auth/manager.ts +140 -0
- package/src/auth/oauth-refresh.ts +120 -0
- package/src/auth/refresh.ts +300 -0
- package/src/capture/anti-bot.ts +63 -0
- package/src/capture/blocklist.ts +75 -0
- package/src/capture/body-diff.ts +109 -0
- package/src/capture/body-variables.ts +156 -0
- package/src/capture/domain.ts +34 -0
- package/src/capture/entropy.ts +121 -0
- package/src/capture/filter.ts +56 -0
- package/src/capture/graphql.ts +124 -0
- package/src/capture/idle.ts +45 -0
- package/src/capture/monitor.ts +224 -0
- package/src/capture/oauth-detector.ts +106 -0
- package/src/capture/pagination.ts +49 -0
- package/src/capture/parameterize.ts +68 -0
- package/src/capture/scrubber.ts +49 -0
- package/src/capture/session.ts +502 -0
- package/src/capture/token-detector.ts +76 -0
- package/src/capture/verifier.ts +171 -0
- package/src/cli.ts +1031 -0
- package/src/discovery/auth.ts +99 -0
- package/src/discovery/fetch.ts +85 -0
- package/src/discovery/frameworks.ts +231 -0
- package/src/discovery/index.ts +256 -0
- package/src/discovery/openapi.ts +230 -0
- package/src/discovery/probes.ts +76 -0
- package/src/index.ts +26 -0
- package/src/inspect/report.ts +247 -0
- package/src/mcp.ts +618 -0
- package/src/orchestration/browse.ts +250 -0
- package/src/orchestration/cache.ts +37 -0
- package/src/plugin.ts +188 -0
- package/src/read/decoders/deepwiki.ts +180 -0
- package/src/read/decoders/grokipedia.ts +246 -0
- package/src/read/decoders/hackernews.ts +198 -0
- package/src/read/decoders/index.ts +15 -0
- package/src/read/decoders/reddit.ts +158 -0
- package/src/read/decoders/twitter.ts +211 -0
- package/src/read/decoders/wikipedia.ts +75 -0
- package/src/read/decoders/youtube.ts +75 -0
- package/src/read/extract.ts +396 -0
- package/src/read/index.ts +78 -0
- package/src/read/peek.ts +175 -0
- package/src/read/types.ts +37 -0
- package/src/replay/engine.ts +559 -0
- package/src/replay/truncate.ts +116 -0
- package/src/serve.ts +189 -0
- package/src/skill/generator.ts +473 -0
- package/src/skill/importer.ts +107 -0
- package/src/skill/search.ts +76 -0
- package/src/skill/signing.ts +36 -0
- package/src/skill/ssrf.ts +238 -0
- package/src/skill/store.ts +107 -0
- package/src/stats/report.ts +208 -0
- package/src/types.ts +233 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,1031 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// src/cli.ts
|
|
3
|
+
import { capture } from './capture/monitor.js';
|
|
4
|
+
import { writeSkillFile, readSkillFile, listSkillFiles } from './skill/store.js';
|
|
5
|
+
import { replayEndpoint } from './replay/engine.js';
|
|
6
|
+
import { AuthManager, getMachineId } from './auth/manager.js';
|
|
7
|
+
import { deriveKey } from './auth/crypto.js';
|
|
8
|
+
import { signSkillFile } from './skill/signing.js';
|
|
9
|
+
import { importSkillFile } from './skill/importer.js';
|
|
10
|
+
import { resolveAndValidateUrl } from './skill/ssrf.js';
|
|
11
|
+
import { verifyEndpoints } from './capture/verifier.js';
|
|
12
|
+
import { searchSkills } from './skill/search.js';
|
|
13
|
+
import { refreshTokens } from './auth/refresh.js';
|
|
14
|
+
import { parseJwtClaims } from './capture/entropy.js';
|
|
15
|
+
import { createServeServer, buildServeTools } from './serve.js';
|
|
16
|
+
import { buildInspectReport, formatInspectHuman } from './inspect/report.js';
|
|
17
|
+
import { generateStatsReport, formatStatsHuman } from './stats/report.js';
|
|
18
|
+
import { detectAntiBot, type AntiBotSignal } from './capture/anti-bot.js';
|
|
19
|
+
import { discover } from './discovery/index.js';
|
|
20
|
+
import { peek } from './read/peek.js';
|
|
21
|
+
import { read } from './read/index.js';
|
|
22
|
+
import { homedir } from 'node:os';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
import { readFileSync } from 'node:fs';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
28
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
29
|
+
const VERSION = pkg.version as string;
|
|
30
|
+
|
|
31
|
+
interface ParsedArgs {
|
|
32
|
+
command: string;
|
|
33
|
+
positional: string[];
|
|
34
|
+
flags: Record<string, string | boolean>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseArgs(argv: string[]): ParsedArgs {
|
|
38
|
+
const [command = 'help', ...rest] = argv;
|
|
39
|
+
const positional: string[] = [];
|
|
40
|
+
const flags: Record<string, string | boolean> = {};
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < rest.length; i++) {
|
|
43
|
+
if (rest[i].startsWith('--')) {
|
|
44
|
+
const key = rest[i].slice(2);
|
|
45
|
+
const next = rest[i + 1];
|
|
46
|
+
if (next && !next.startsWith('--')) {
|
|
47
|
+
flags[key] = next;
|
|
48
|
+
i++;
|
|
49
|
+
} else {
|
|
50
|
+
flags[key] = true;
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
positional.push(rest[i]);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { command, positional, flags };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function printUsage(): void {
|
|
61
|
+
console.log(`
|
|
62
|
+
apitap — API interception for AI agents
|
|
63
|
+
|
|
64
|
+
Usage:
|
|
65
|
+
apitap capture <url> Capture API traffic from a website
|
|
66
|
+
apitap discover <url> Detect APIs without a browser (fast recon)
|
|
67
|
+
apitap inspect <url> Discover APIs without saving (X-ray vision)
|
|
68
|
+
apitap search <query> Search skill files for a domain or endpoint
|
|
69
|
+
apitap list List available skill files
|
|
70
|
+
apitap show <domain> Show endpoints for a domain
|
|
71
|
+
apitap replay <domain> <endpoint-id> [key=value...]
|
|
72
|
+
Replay an API endpoint
|
|
73
|
+
apitap import <file> Import a skill file with safety validation
|
|
74
|
+
apitap refresh <domain> Refresh auth tokens via browser
|
|
75
|
+
apitap auth [domain] View or manage stored auth
|
|
76
|
+
apitap serve <domain> Serve a skill file as an MCP server
|
|
77
|
+
apitap browse <url> Browse a URL (discover + replay in one step)
|
|
78
|
+
apitap peek <url> Zero-cost triage (HEAD only)
|
|
79
|
+
apitap read <url> Extract content without a browser
|
|
80
|
+
apitap stats Show token savings report
|
|
81
|
+
|
|
82
|
+
Discover options:
|
|
83
|
+
--json Output machine-readable JSON
|
|
84
|
+
--save Save discovered skill file to disk
|
|
85
|
+
|
|
86
|
+
Capture options:
|
|
87
|
+
--json Output machine-readable JSON
|
|
88
|
+
--duration <seconds> Stop capture after N seconds
|
|
89
|
+
--port <port> Connect to specific CDP port
|
|
90
|
+
--launch Always launch a new browser
|
|
91
|
+
--attach Only attach to existing browser
|
|
92
|
+
--all-domains Capture traffic from all domains (default: target only)
|
|
93
|
+
--preview Include response data previews in skill files
|
|
94
|
+
--no-scrub Disable PII scrubbing
|
|
95
|
+
--no-verify Skip auto-verification of GET endpoints
|
|
96
|
+
--verify-posts Also verify POST endpoints by replaying them (may cause side effects)
|
|
97
|
+
|
|
98
|
+
Replay options:
|
|
99
|
+
--json Output machine-readable JSON
|
|
100
|
+
--fresh Force token refresh before replay
|
|
101
|
+
--max-bytes <bytes> Truncate response to fit within byte limit
|
|
102
|
+
|
|
103
|
+
Auth options:
|
|
104
|
+
--list List all domains with stored auth
|
|
105
|
+
--clear Clear auth for a domain
|
|
106
|
+
--json Output machine-readable JSON
|
|
107
|
+
|
|
108
|
+
Browse options:
|
|
109
|
+
--json Output machine-readable JSON
|
|
110
|
+
--max-bytes <bytes> Truncate response to fit within byte limit (default: 50000)
|
|
111
|
+
|
|
112
|
+
Peek options:
|
|
113
|
+
--json Output machine-readable JSON
|
|
114
|
+
|
|
115
|
+
Read options:
|
|
116
|
+
--json Output machine-readable JSON
|
|
117
|
+
--max-bytes <bytes> Truncate content to fit within byte limit
|
|
118
|
+
|
|
119
|
+
Import options:
|
|
120
|
+
--yes Skip confirmation prompt
|
|
121
|
+
|
|
122
|
+
Serve options:
|
|
123
|
+
--json Output tool list as JSON on stderr
|
|
124
|
+
--no-auth Skip loading stored auth
|
|
125
|
+
`.trim());
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const APITAP_DIR = process.env.APITAP_DIR || join(homedir(), '.apitap');
|
|
129
|
+
const SKILLS_DIR = process.env.APITAP_SKILLS_DIR || undefined;
|
|
130
|
+
|
|
131
|
+
/** Get machine ID, allowing override via env var for testing */
|
|
132
|
+
async function getEffectiveMachineId(): Promise<string> {
|
|
133
|
+
return process.env.APITAP_MACHINE_ID || await getMachineId();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const TIER_BADGES: Record<string, string> = {
|
|
137
|
+
green: '[green]',
|
|
138
|
+
yellow: '[yellow]',
|
|
139
|
+
orange: '[orange]',
|
|
140
|
+
red: '[red]',
|
|
141
|
+
unknown: '[ ]',
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
async function handleCapture(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
|
|
145
|
+
const url = positional[0];
|
|
146
|
+
if (!url) {
|
|
147
|
+
console.error('Error: URL required. Usage: apitap capture <url>');
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const fullUrl = url.startsWith('http') ? url : `https://${url}`;
|
|
152
|
+
const json = flags.json === true;
|
|
153
|
+
const duration = typeof flags.duration === 'string' ? parseInt(flags.duration, 10) : undefined;
|
|
154
|
+
const port = typeof flags.port === 'string' ? parseInt(flags.port, 10) : undefined;
|
|
155
|
+
const skipVerify = flags['no-verify'] === true;
|
|
156
|
+
const verifyPosts = flags['verify-posts'] === true;
|
|
157
|
+
|
|
158
|
+
if (!json) {
|
|
159
|
+
const domainOnly = flags['all-domains'] !== true;
|
|
160
|
+
console.log(`\n 🔍 Capturing ${url}...${duration ? ` (${duration}s)` : ' (Ctrl+C to stop)'}${domainOnly ? ' [domain-only]' : ' [all domains]'}\n`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let endpointCount = 0;
|
|
164
|
+
let filteredCount = 0;
|
|
165
|
+
|
|
166
|
+
const result = await capture({
|
|
167
|
+
url: fullUrl,
|
|
168
|
+
duration,
|
|
169
|
+
port,
|
|
170
|
+
launch: flags.launch === true,
|
|
171
|
+
attach: flags.attach === true,
|
|
172
|
+
allDomains: flags['all-domains'] === true,
|
|
173
|
+
enablePreview: flags.preview === true,
|
|
174
|
+
scrub: flags['no-scrub'] !== true,
|
|
175
|
+
onEndpoint: (ep) => {
|
|
176
|
+
endpointCount++;
|
|
177
|
+
if (!json) {
|
|
178
|
+
console.log(` ✓ ${ep.method.padEnd(6)} ${ep.path}`);
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
onFiltered: () => {
|
|
182
|
+
filteredCount++;
|
|
183
|
+
},
|
|
184
|
+
onIdle: () => {
|
|
185
|
+
if (!json) {
|
|
186
|
+
console.log(`\n ⏸ No new endpoints for 15s — looks complete. Ctrl+C to finish.\n`);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Get machine ID for signing and auth storage
|
|
192
|
+
const machineId = await getMachineId();
|
|
193
|
+
const key = deriveKey(machineId);
|
|
194
|
+
const authManager = new AuthManager(APITAP_DIR, machineId);
|
|
195
|
+
|
|
196
|
+
// Write skill files for each domain
|
|
197
|
+
const written: string[] = [];
|
|
198
|
+
for (const [domain, generator] of result.generators) {
|
|
199
|
+
let skill = generator.toSkillFile(domain, {
|
|
200
|
+
domBytes: result.domBytes,
|
|
201
|
+
totalRequests: result.totalRequests,
|
|
202
|
+
});
|
|
203
|
+
if (skill.endpoints.length > 0) {
|
|
204
|
+
// Store extracted auth
|
|
205
|
+
const extractedAuth = generator.getExtractedAuth();
|
|
206
|
+
if (extractedAuth.length > 0) {
|
|
207
|
+
await authManager.store(domain, extractedAuth[0]);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Store OAuth credentials if detected
|
|
211
|
+
const oauthConfig = generator.getOAuthConfig();
|
|
212
|
+
if (oauthConfig) {
|
|
213
|
+
const clientSecret = generator.getOAuthClientSecret();
|
|
214
|
+
if (clientSecret) {
|
|
215
|
+
await authManager.storeOAuthCredentials(domain, { clientSecret });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Auto-verify GET endpoints
|
|
220
|
+
if (!skipVerify) {
|
|
221
|
+
if (!json) {
|
|
222
|
+
console.log(`\n 🔍 Verifying ${domain}...`);
|
|
223
|
+
}
|
|
224
|
+
skill = await verifyEndpoints(skill, { verifyPosts });
|
|
225
|
+
if (!json) {
|
|
226
|
+
for (const ep of skill.endpoints) {
|
|
227
|
+
const tier = ep.replayability?.tier ?? 'unknown';
|
|
228
|
+
const badge = TIER_BADGES[tier];
|
|
229
|
+
const check = ep.replayability?.verified ? ' ✓' : '';
|
|
230
|
+
console.log(` ${badge}${check} ${ep.method.padEnd(6)} ${ep.path}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Sign the skill file
|
|
236
|
+
skill = signSkillFile(skill, key);
|
|
237
|
+
|
|
238
|
+
const path = await writeSkillFile(skill);
|
|
239
|
+
written.push(path);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (json) {
|
|
244
|
+
const output = {
|
|
245
|
+
domains: Array.from(result.generators.entries()).map(([domain, gen]) => {
|
|
246
|
+
const skill = gen.toSkillFile(domain, {
|
|
247
|
+
domBytes: result.domBytes,
|
|
248
|
+
totalRequests: result.totalRequests,
|
|
249
|
+
});
|
|
250
|
+
return {
|
|
251
|
+
domain,
|
|
252
|
+
endpoints: skill.endpoints.map(ep => ({
|
|
253
|
+
id: ep.id,
|
|
254
|
+
method: ep.method,
|
|
255
|
+
path: ep.path,
|
|
256
|
+
...(ep.replayability ? { replayability: ep.replayability } : {}),
|
|
257
|
+
...(ep.pagination ? { pagination: ep.pagination } : {}),
|
|
258
|
+
})),
|
|
259
|
+
};
|
|
260
|
+
}),
|
|
261
|
+
totalRequests: result.totalRequests,
|
|
262
|
+
filtered: result.filteredRequests,
|
|
263
|
+
skillFiles: written,
|
|
264
|
+
};
|
|
265
|
+
console.log(JSON.stringify(output, null, 2));
|
|
266
|
+
} else {
|
|
267
|
+
console.log(`\n 📋 Capture complete\n`);
|
|
268
|
+
console.log(` Endpoints: ${endpointCount} discovered`);
|
|
269
|
+
console.log(` Requests: ${result.totalRequests} total, ${result.filteredRequests} filtered`);
|
|
270
|
+
for (const path of written) {
|
|
271
|
+
console.log(` Skill file: ${path}`);
|
|
272
|
+
}
|
|
273
|
+
console.log();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function handleSearch(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
|
|
278
|
+
const query = positional.join(' ');
|
|
279
|
+
if (!query) {
|
|
280
|
+
console.error('Error: Query required. Usage: apitap search <query>');
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const json = flags.json === true;
|
|
285
|
+
const result = await searchSkills(query, SKILLS_DIR);
|
|
286
|
+
|
|
287
|
+
if (json) {
|
|
288
|
+
console.log(JSON.stringify(result, null, 2));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!result.found) {
|
|
293
|
+
console.log(`\n ${result.suggestion}\n`);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
console.log();
|
|
298
|
+
for (const r of result.results!) {
|
|
299
|
+
const tierBadge = TIER_BADGES[r.tier] || '[?]';
|
|
300
|
+
const verified = r.verified ? ' ✓' : '';
|
|
301
|
+
console.log(` ${tierBadge}${verified} ${r.domain.padEnd(30)} ${r.method.padEnd(6)} ${r.path.padEnd(24)} ${r.endpointId}`);
|
|
302
|
+
}
|
|
303
|
+
console.log();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function handleList(flags: Record<string, string | boolean>): Promise<void> {
|
|
307
|
+
const summaries = await listSkillFiles(SKILLS_DIR);
|
|
308
|
+
const json = flags.json === true;
|
|
309
|
+
|
|
310
|
+
if (json) {
|
|
311
|
+
console.log(JSON.stringify(summaries, null, 2));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (summaries.length === 0) {
|
|
316
|
+
console.log('\n No skill files found. Run `apitap capture <url>` first.\n');
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
console.log();
|
|
321
|
+
for (const s of summaries) {
|
|
322
|
+
const ago = timeAgo(s.capturedAt);
|
|
323
|
+
const prov = s.provenance === 'self' ? '✓' : s.provenance === 'imported' ? '⬇' : '?';
|
|
324
|
+
console.log(` ${prov} ${s.domain.padEnd(28)} ${String(s.endpointCount).padStart(3)} endpoints ${ago}`);
|
|
325
|
+
}
|
|
326
|
+
console.log();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function handleShow(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
|
|
330
|
+
const domain = positional[0];
|
|
331
|
+
if (!domain) {
|
|
332
|
+
console.error('Error: Domain required. Usage: apitap show <domain>');
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const skill = await readSkillFile(domain, SKILLS_DIR);
|
|
337
|
+
if (!skill) {
|
|
338
|
+
console.error(`Error: No skill file found for "${domain}". Run \`apitap capture\` first.`);
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const json = flags.json === true;
|
|
343
|
+
|
|
344
|
+
if (json) {
|
|
345
|
+
console.log(JSON.stringify(skill, null, 2));
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const provLabel = skill.provenance === 'self' ? 'signed ✓' : skill.provenance === 'imported' ? 'imported ⬇' : 'unsigned';
|
|
350
|
+
console.log(`\n ${skill.domain} — ${skill.endpoints.length} endpoints (captured ${timeAgo(skill.capturedAt)}) [${provLabel}]\n`);
|
|
351
|
+
for (const ep of skill.endpoints) {
|
|
352
|
+
const shape = ep.responseShape.type;
|
|
353
|
+
const fields = ep.responseShape.fields?.length ?? 0;
|
|
354
|
+
const hasAuth = Object.values(ep.headers).some(v => v === '[stored]');
|
|
355
|
+
const authBadge = hasAuth ? ' 🔑' : '';
|
|
356
|
+
const tier = ep.replayability?.tier ?? 'unknown';
|
|
357
|
+
const tierBadge = TIER_BADGES[tier];
|
|
358
|
+
const verified = ep.replayability?.verified ? ' ✓' : '';
|
|
359
|
+
const pagBadge = ep.pagination ? ` 📄${ep.pagination.type}` : '';
|
|
360
|
+
const bodyBadge = ep.requestBody ? ` 📦${ep.requestBody.contentType.split('/')[1] || 'body'}` : '';
|
|
361
|
+
console.log(` ${tierBadge}${verified} ${ep.method.padEnd(6)} ${ep.path.padEnd(30)} ${shape}${fields ? ` (${fields} fields)` : ''}${authBadge}${pagBadge}${bodyBadge}`);
|
|
362
|
+
}
|
|
363
|
+
console.log(`\n Replay: apitap replay ${skill.domain} <endpoint-id>\n`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function handleReplay(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
|
|
367
|
+
const [domain, endpointId, ...paramArgs] = positional;
|
|
368
|
+
if (!domain || !endpointId) {
|
|
369
|
+
console.error('Error: Domain and endpoint required. Usage: apitap replay <domain> <endpoint-id> [key=value...]');
|
|
370
|
+
process.exit(1);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const skill = await readSkillFile(domain, SKILLS_DIR);
|
|
374
|
+
if (!skill) {
|
|
375
|
+
console.error(`Error: No skill file found for "${domain}".`);
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Parse key=value params
|
|
380
|
+
const params: Record<string, string> = {};
|
|
381
|
+
for (const arg of paramArgs) {
|
|
382
|
+
const eq = arg.indexOf('=');
|
|
383
|
+
if (eq > 0) {
|
|
384
|
+
params[arg.slice(0, eq)] = arg.slice(eq + 1);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Merge stored auth into endpoint headers for replay
|
|
389
|
+
const machineId = await getMachineId();
|
|
390
|
+
const authManager = new AuthManager(APITAP_DIR, machineId);
|
|
391
|
+
const storedAuth = await authManager.retrieve(domain);
|
|
392
|
+
|
|
393
|
+
// Check for [stored] placeholders and warn if auth missing
|
|
394
|
+
const endpoint = skill.endpoints.find(e => e.id === endpointId);
|
|
395
|
+
if (endpoint) {
|
|
396
|
+
const hasStoredPlaceholder = Object.values(endpoint.headers).some(v => v === '[stored]');
|
|
397
|
+
if (hasStoredPlaceholder && !storedAuth) {
|
|
398
|
+
console.error(`Warning: Endpoint requires auth but no stored credentials found for "${domain}".`);
|
|
399
|
+
console.error(` Run \`apitap capture ${domain}\` to capture fresh credentials.\n`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Inject stored auth into a copy of the skill for replay
|
|
403
|
+
if (storedAuth) {
|
|
404
|
+
endpoint.headers[storedAuth.header] = storedAuth.value;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const fresh = flags.fresh === true;
|
|
409
|
+
const json = flags.json === true;
|
|
410
|
+
const maxBytes = typeof flags['max-bytes'] === 'string' ? parseInt(flags['max-bytes'], 10) : undefined;
|
|
411
|
+
|
|
412
|
+
const result = await replayEndpoint(skill, endpointId, {
|
|
413
|
+
params: Object.keys(params).length > 0 ? params : undefined,
|
|
414
|
+
authManager,
|
|
415
|
+
domain,
|
|
416
|
+
fresh,
|
|
417
|
+
maxBytes,
|
|
418
|
+
_skipSsrfCheck: process.env.APITAP_SKIP_SSRF_CHECK === '1',
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
if (json) {
|
|
422
|
+
console.log(JSON.stringify({ status: result.status, data: result.data }, null, 2));
|
|
423
|
+
} else {
|
|
424
|
+
console.log(`\n Status: ${result.status}\n`);
|
|
425
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
426
|
+
console.log();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function handleImport(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
|
|
431
|
+
const filePath = positional[0];
|
|
432
|
+
if (!filePath) {
|
|
433
|
+
console.error('Error: File path required. Usage: apitap import <file>');
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const json = flags.json === true;
|
|
438
|
+
|
|
439
|
+
// Get local key for signature verification
|
|
440
|
+
const machineId = await getMachineId();
|
|
441
|
+
const key = deriveKey(machineId);
|
|
442
|
+
|
|
443
|
+
// DNS-resolving SSRF check before importing (prevents DNS rebinding attacks)
|
|
444
|
+
try {
|
|
445
|
+
const raw = JSON.parse(await import('node:fs/promises').then(fs => fs.readFile(filePath, 'utf-8')));
|
|
446
|
+
if (raw.baseUrl) {
|
|
447
|
+
const dnsCheck = await resolveAndValidateUrl(raw.baseUrl);
|
|
448
|
+
if (!dnsCheck.safe) {
|
|
449
|
+
const msg = `DNS rebinding risk: ${dnsCheck.reason}`;
|
|
450
|
+
if (json) {
|
|
451
|
+
console.log(JSON.stringify({ success: false, reason: msg }));
|
|
452
|
+
} else {
|
|
453
|
+
console.error(`Error: ${msg}`);
|
|
454
|
+
}
|
|
455
|
+
process.exit(1);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
} catch {
|
|
459
|
+
// Parse errors will be caught by importSkillFile
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const result = await importSkillFile(filePath, undefined, key);
|
|
463
|
+
|
|
464
|
+
if (!result.success) {
|
|
465
|
+
if (json) {
|
|
466
|
+
console.log(JSON.stringify({ success: false, reason: result.reason }));
|
|
467
|
+
} else {
|
|
468
|
+
console.error(`Error: ${result.reason}`);
|
|
469
|
+
}
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (json) {
|
|
474
|
+
console.log(JSON.stringify({ success: true, skillFile: result.skillFile }));
|
|
475
|
+
} else {
|
|
476
|
+
console.log(`\n ✓ Imported skill file: ${result.skillFile}\n`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function handleRefresh(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
|
|
481
|
+
const domain = positional[0];
|
|
482
|
+
if (!domain) {
|
|
483
|
+
console.error('Error: Domain required. Usage: apitap refresh <domain>');
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const skill = await readSkillFile(domain, SKILLS_DIR);
|
|
488
|
+
if (!skill) {
|
|
489
|
+
console.error(`Error: No skill file found for "${domain}".`);
|
|
490
|
+
process.exit(1);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const machineId = await getEffectiveMachineId();
|
|
494
|
+
const authManager = new AuthManager(APITAP_DIR, machineId);
|
|
495
|
+
const json = flags.json === true;
|
|
496
|
+
|
|
497
|
+
if (!json) {
|
|
498
|
+
console.log(`\n 🔄 Refreshing tokens for ${domain}...`);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const result = await refreshTokens(skill, authManager, {
|
|
502
|
+
domain,
|
|
503
|
+
browserMode: skill.auth?.captchaRisk ? 'visible' : 'headless',
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
if (json) {
|
|
507
|
+
console.log(JSON.stringify(result, null, 2));
|
|
508
|
+
} else if (result.success) {
|
|
509
|
+
if (result.oauthRefreshed) {
|
|
510
|
+
console.log(` ✓ OAuth token refreshed via token endpoint`);
|
|
511
|
+
}
|
|
512
|
+
if (Object.keys(result.tokens).length > 0) {
|
|
513
|
+
console.log(` ✓ Browser tokens refreshed: ${Object.keys(result.tokens).join(', ')}`);
|
|
514
|
+
}
|
|
515
|
+
if (result.captchaDetected) {
|
|
516
|
+
console.log(` (captcha solved: ${result.captchaDetected})`);
|
|
517
|
+
}
|
|
518
|
+
console.log();
|
|
519
|
+
} else {
|
|
520
|
+
console.error(` ✗ Refresh failed: ${result.error || 'no tokens captured'}`);
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async function handleAuth(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
|
|
526
|
+
const domain = positional[0];
|
|
527
|
+
const json = flags.json === true;
|
|
528
|
+
const machineId = await getEffectiveMachineId();
|
|
529
|
+
const authManager = new AuthManager(APITAP_DIR, machineId);
|
|
530
|
+
|
|
531
|
+
// List all domains
|
|
532
|
+
if (flags.list === true) {
|
|
533
|
+
const domains = await authManager.listDomains();
|
|
534
|
+
if (json) {
|
|
535
|
+
console.log(JSON.stringify({ domains }, null, 2));
|
|
536
|
+
} else {
|
|
537
|
+
if (domains.length === 0) {
|
|
538
|
+
console.log('\n No stored auth\n');
|
|
539
|
+
} else {
|
|
540
|
+
console.log('\n Domains with stored auth:');
|
|
541
|
+
for (const d of domains) {
|
|
542
|
+
console.log(` ${d}`);
|
|
543
|
+
}
|
|
544
|
+
console.log();
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Require domain for other operations
|
|
551
|
+
if (!domain) {
|
|
552
|
+
console.error('Error: Domain required. Usage: apitap auth <domain> [--clear] [--json]');
|
|
553
|
+
console.error(' apitap auth --list [--json]');
|
|
554
|
+
process.exit(1);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Clear auth for domain
|
|
558
|
+
if (flags.clear === true) {
|
|
559
|
+
await authManager.clear(domain);
|
|
560
|
+
if (json) {
|
|
561
|
+
console.log(JSON.stringify({ success: true, domain, cleared: true }));
|
|
562
|
+
} else {
|
|
563
|
+
console.log(`\n ✓ Cleared auth for ${domain}\n`);
|
|
564
|
+
}
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Show auth status for domain
|
|
569
|
+
const auth = await authManager.retrieve(domain);
|
|
570
|
+
const tokens = await authManager.retrieveTokens(domain);
|
|
571
|
+
const session = await authManager.retrieveSession(domain);
|
|
572
|
+
const oauthCreds = await authManager.retrieveOAuthCredentials(domain);
|
|
573
|
+
|
|
574
|
+
// Check for JWT expiry
|
|
575
|
+
let jwtExpiry: string | undefined;
|
|
576
|
+
if (auth?.value) {
|
|
577
|
+
const raw = auth.value.startsWith('Bearer ') ? auth.value.slice(7) : auth.value;
|
|
578
|
+
const jwt = parseJwtClaims(raw);
|
|
579
|
+
if (jwt?.exp) {
|
|
580
|
+
const expDate = new Date(jwt.exp * 1000);
|
|
581
|
+
const isExpired = expDate.getTime() < Date.now();
|
|
582
|
+
jwtExpiry = isExpired ? `expired ${timeAgo(expDate.toISOString())}` : `expires ${expDate.toISOString()}`;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Read skill file for OAuth config (non-secret)
|
|
587
|
+
const skill = await readSkillFile(domain, SKILLS_DIR);
|
|
588
|
+
const oauthConfig = skill?.auth?.oauthConfig;
|
|
589
|
+
|
|
590
|
+
const status = {
|
|
591
|
+
domain,
|
|
592
|
+
hasHeaderAuth: !!auth,
|
|
593
|
+
headerAuthType: auth?.type,
|
|
594
|
+
jwtExpiry,
|
|
595
|
+
tokens: tokens ? Object.keys(tokens) : [],
|
|
596
|
+
tokenRefreshTimes: tokens
|
|
597
|
+
? Object.fromEntries(
|
|
598
|
+
Object.entries(tokens).map(([k, v]) => [k, v.refreshedAt])
|
|
599
|
+
)
|
|
600
|
+
: {},
|
|
601
|
+
hasSession: !!session,
|
|
602
|
+
sessionSavedAt: session?.savedAt,
|
|
603
|
+
hasOAuth: !!oauthCreds,
|
|
604
|
+
oauthConfig: oauthConfig ?? undefined,
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
if (json) {
|
|
608
|
+
console.log(JSON.stringify(status, null, 2));
|
|
609
|
+
} else {
|
|
610
|
+
console.log(`\n Auth status for ${domain}:`);
|
|
611
|
+
console.log(` Header auth: ${auth ? `${auth.type} (${auth.header})` : 'none'}`);
|
|
612
|
+
if (jwtExpiry) {
|
|
613
|
+
console.log(` JWT: ${jwtExpiry}`);
|
|
614
|
+
}
|
|
615
|
+
if (oauthConfig) {
|
|
616
|
+
console.log(` OAuth: ${oauthConfig.grantType} via ${oauthConfig.tokenEndpoint}`);
|
|
617
|
+
if (oauthCreds?.refreshToken) {
|
|
618
|
+
console.log(` Refresh token: stored`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
console.log(` Tokens: ${tokens ? Object.keys(tokens).join(', ') || 'none' : 'none'}`);
|
|
622
|
+
if (tokens) {
|
|
623
|
+
for (const [name, info] of Object.entries(tokens)) {
|
|
624
|
+
console.log(` ${name}: refreshed ${timeAgo(info.refreshedAt)}`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
console.log(` Session cache: ${session ? `saved ${timeAgo(session.savedAt)}` : 'none'}`);
|
|
628
|
+
console.log();
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async function handleServe(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
|
|
633
|
+
const domain = positional[0];
|
|
634
|
+
if (!domain) {
|
|
635
|
+
console.error('Error: Domain required. Usage: apitap serve <domain>');
|
|
636
|
+
process.exit(1);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const noAuth = flags['no-auth'] === true;
|
|
640
|
+
const json = flags.json === true;
|
|
641
|
+
|
|
642
|
+
try {
|
|
643
|
+
const server = await createServeServer(domain, {
|
|
644
|
+
skillsDir: SKILLS_DIR,
|
|
645
|
+
noAuth,
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// Print tool list to stderr (stdout is the MCP transport)
|
|
649
|
+
const skill = await readSkillFile(domain, SKILLS_DIR);
|
|
650
|
+
const tools = buildServeTools(skill!);
|
|
651
|
+
|
|
652
|
+
if (json) {
|
|
653
|
+
console.error(JSON.stringify(tools.map(t => ({ name: t.name, description: t.description }))));
|
|
654
|
+
} else {
|
|
655
|
+
console.error(`apitap serve: ${domain} (${tools.length} tools)`);
|
|
656
|
+
for (const tool of tools) {
|
|
657
|
+
console.error(` ${tool.name}`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Start stdio transport
|
|
662
|
+
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
|
663
|
+
const transport = new StdioServerTransport();
|
|
664
|
+
await server.connect(transport);
|
|
665
|
+
} catch (err: any) {
|
|
666
|
+
console.error(`Error: ${err.message}`);
|
|
667
|
+
process.exit(1);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function timeAgo(isoDate: string): string {
|
|
672
|
+
const diff = Date.now() - new Date(isoDate).getTime();
|
|
673
|
+
const minutes = Math.floor(diff / 60000);
|
|
674
|
+
if (minutes < 1) return 'just now';
|
|
675
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
676
|
+
const hours = Math.floor(minutes / 60);
|
|
677
|
+
if (hours < 24) return `${hours}h ago`;
|
|
678
|
+
const days = Math.floor(hours / 24);
|
|
679
|
+
return `${days}d ago`;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
async function handleInspect(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
|
|
683
|
+
const url = positional[0];
|
|
684
|
+
if (!url) {
|
|
685
|
+
console.error('Usage: apitap inspect <url>');
|
|
686
|
+
process.exit(1);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const json = flags.json === true;
|
|
690
|
+
const duration = typeof flags.duration === 'string' ? parseInt(flags.duration, 10) : 30;
|
|
691
|
+
|
|
692
|
+
if (!json) {
|
|
693
|
+
console.log(`\n Inspecting ${url} (${duration}s scan)...\n`);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Track anti-bot signals during capture
|
|
697
|
+
const antiBotSignals = new Set<AntiBotSignal>();
|
|
698
|
+
|
|
699
|
+
const result = await capture({
|
|
700
|
+
url,
|
|
701
|
+
port: typeof flags.port === 'string' ? parseInt(flags.port, 10) : undefined,
|
|
702
|
+
launch: flags.launch === true,
|
|
703
|
+
attach: flags.attach === true,
|
|
704
|
+
duration,
|
|
705
|
+
allDomains: flags['all-domains'] === true,
|
|
706
|
+
enablePreview: false,
|
|
707
|
+
scrub: true,
|
|
708
|
+
onEndpoint: (ep) => {
|
|
709
|
+
if (!json) {
|
|
710
|
+
console.log(` ✓ ${ep.method.padEnd(6)} ${ep.path}`);
|
|
711
|
+
}
|
|
712
|
+
},
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
// Build skill files (without writing to disk), verify endpoints, and detect anti-bot
|
|
716
|
+
const skills = new Map<string, import('./types.js').SkillFile>();
|
|
717
|
+
for (const [domain, generator] of result.generators) {
|
|
718
|
+
let skill = generator.toSkillFile(domain, {
|
|
719
|
+
domBytes: result.domBytes,
|
|
720
|
+
totalRequests: result.totalRequests,
|
|
721
|
+
});
|
|
722
|
+
const verifyPosts = flags['verify-posts'] === true;
|
|
723
|
+
skill = await verifyEndpoints(skill, { verifyPosts });
|
|
724
|
+
if (skill.endpoints.length > 0) {
|
|
725
|
+
skills.set(domain, skill);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Extract target domain from URL
|
|
730
|
+
let targetDomain: string;
|
|
731
|
+
try {
|
|
732
|
+
targetDomain = new URL(url.startsWith('http') ? url : `https://${url}`).hostname;
|
|
733
|
+
} catch {
|
|
734
|
+
targetDomain = url;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const report = buildInspectReport({
|
|
738
|
+
skills,
|
|
739
|
+
totalRequests: result.totalRequests,
|
|
740
|
+
filteredRequests: result.filteredRequests,
|
|
741
|
+
duration,
|
|
742
|
+
domBytes: result.domBytes,
|
|
743
|
+
antiBotSignals: [...antiBotSignals],
|
|
744
|
+
targetDomain,
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
if (json) {
|
|
748
|
+
console.log(JSON.stringify(report, null, 2));
|
|
749
|
+
} else {
|
|
750
|
+
console.log(formatInspectHuman(report));
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
async function handleStats(flags: Record<string, string | boolean>): Promise<void> {
|
|
755
|
+
const json = flags.json === true;
|
|
756
|
+
const skillsDir = SKILLS_DIR || join(APITAP_DIR, 'skills');
|
|
757
|
+
|
|
758
|
+
const report = await generateStatsReport(skillsDir);
|
|
759
|
+
|
|
760
|
+
if (json) {
|
|
761
|
+
console.log(JSON.stringify(report, null, 2));
|
|
762
|
+
} else {
|
|
763
|
+
console.log(formatStatsHuman(report));
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
async function handleDiscover(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
|
|
768
|
+
const url = positional[0];
|
|
769
|
+
if (!url) {
|
|
770
|
+
console.error('Error: URL required. Usage: apitap discover <url>');
|
|
771
|
+
process.exit(1);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const json = flags.json === true;
|
|
775
|
+
const save = flags.save === true;
|
|
776
|
+
|
|
777
|
+
if (!json) {
|
|
778
|
+
console.log(`\n Discovering APIs for ${url}...\n`);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const result = await discover(url);
|
|
782
|
+
|
|
783
|
+
if (json) {
|
|
784
|
+
console.log(JSON.stringify(result, null, 2));
|
|
785
|
+
} else {
|
|
786
|
+
// Confidence summary
|
|
787
|
+
const confidenceLabels: Record<string, string> = {
|
|
788
|
+
high: 'High — API spec or strong framework signals found',
|
|
789
|
+
medium: 'Medium — known framework detected',
|
|
790
|
+
low: 'Low — some API patterns detected',
|
|
791
|
+
none: 'None — no API patterns found',
|
|
792
|
+
};
|
|
793
|
+
console.log(` Confidence: ${confidenceLabels[result.confidence]}`);
|
|
794
|
+
console.log(` Duration: ${result.duration}ms`);
|
|
795
|
+
|
|
796
|
+
if (result.frameworks && result.frameworks.length > 0) {
|
|
797
|
+
console.log(`\n Frameworks:`);
|
|
798
|
+
for (const f of result.frameworks) {
|
|
799
|
+
console.log(` ${f.name} (${f.confidence}) — ${f.apiPatterns.length} predicted patterns`);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (result.specs && result.specs.length > 0) {
|
|
804
|
+
console.log(`\n API Specs:`);
|
|
805
|
+
for (const s of result.specs) {
|
|
806
|
+
console.log(` ${s.type} ${s.version ?? ''} — ${s.url}${s.endpointCount ? ` (${s.endpointCount} endpoints)` : ''}`);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (result.probes && result.probes.length > 0) {
|
|
811
|
+
const apiProbes = result.probes.filter(p => p.isApi);
|
|
812
|
+
if (apiProbes.length > 0) {
|
|
813
|
+
console.log(`\n API Paths:`);
|
|
814
|
+
for (const p of apiProbes) {
|
|
815
|
+
console.log(` ${p.method} ${p.path} → ${p.status} (${p.contentType})`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (result.hints && result.hints.length > 0) {
|
|
821
|
+
console.log(`\n Hints:`);
|
|
822
|
+
for (const h of result.hints) {
|
|
823
|
+
console.log(` ${h}`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (result.skillFile) {
|
|
828
|
+
console.log(`\n Skill file: ${result.skillFile.endpoints.length} endpoints predicted`);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (result.confidence === 'none') {
|
|
832
|
+
console.log(`\n Recommendation: Run \`apitap capture ${url}\` for browser-based discovery`);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
console.log();
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Save skill file if requested and available
|
|
839
|
+
if (save && result.skillFile) {
|
|
840
|
+
const { writeSkillFile } = await import('./skill/store.js');
|
|
841
|
+
const { signSkillFile } = await import('./skill/signing.js');
|
|
842
|
+
const { deriveKey } = await import('./auth/crypto.js');
|
|
843
|
+
const machineId = await getMachineId();
|
|
844
|
+
const key = deriveKey(machineId);
|
|
845
|
+
|
|
846
|
+
const signed = signSkillFile(result.skillFile, key);
|
|
847
|
+
const path = await writeSkillFile(signed, SKILLS_DIR);
|
|
848
|
+
|
|
849
|
+
if (json) {
|
|
850
|
+
console.log(JSON.stringify({ saved: path }));
|
|
851
|
+
} else {
|
|
852
|
+
console.log(` Saved: ${path}\n`);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
async function handleBrowse(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
|
|
858
|
+
const url = positional[0];
|
|
859
|
+
if (!url) {
|
|
860
|
+
console.error('Error: URL required. Usage: apitap browse <url>');
|
|
861
|
+
process.exit(1);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const json = flags.json === true;
|
|
865
|
+
const fullUrl = url.startsWith('http') ? url : `https://${url}`;
|
|
866
|
+
|
|
867
|
+
if (!json) {
|
|
868
|
+
console.log(`\n Browsing ${url}...\n`);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const maxBytes = typeof flags['max-bytes'] === 'string' ? parseInt(flags['max-bytes'], 10) : 50_000;
|
|
872
|
+
|
|
873
|
+
const { browse } = await import('./orchestration/browse.js');
|
|
874
|
+
const { SessionCache } = await import('./orchestration/cache.js');
|
|
875
|
+
|
|
876
|
+
const result = await browse(fullUrl, {
|
|
877
|
+
skillsDir: SKILLS_DIR,
|
|
878
|
+
cache: new SessionCache(),
|
|
879
|
+
maxBytes,
|
|
880
|
+
_skipSsrfCheck: process.env.APITAP_SKIP_SSRF_CHECK === '1',
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
if (json) {
|
|
884
|
+
console.log(JSON.stringify(result, null, 2));
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
if (result.success) {
|
|
889
|
+
console.log(` ✓ ${result.domain} → ${result.endpointId} (${result.tier})`);
|
|
890
|
+
console.log(` Status: ${result.status}\n`);
|
|
891
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
892
|
+
console.log();
|
|
893
|
+
} else {
|
|
894
|
+
console.log(` ✗ ${result.reason}`);
|
|
895
|
+
if (result.suggestion === 'capture_needed') {
|
|
896
|
+
console.log(`\n Recommendation: apitap capture ${result.url}\n`);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
async function handlePeek(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
|
|
902
|
+
const url = positional[0];
|
|
903
|
+
if (!url) {
|
|
904
|
+
console.error('Error: URL required. Usage: apitap peek <url>');
|
|
905
|
+
process.exit(1);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const json = flags.json === true;
|
|
909
|
+
const fullUrl = url.startsWith('http') ? url : `https://${url}`;
|
|
910
|
+
|
|
911
|
+
if (!json) {
|
|
912
|
+
console.log(`\n Peeking at ${url}...\n`);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const result = await peek(fullUrl);
|
|
916
|
+
|
|
917
|
+
if (json) {
|
|
918
|
+
console.log(JSON.stringify(result, null, 2));
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const icon = result.accessible ? '\u2713' : '\u2717';
|
|
923
|
+
console.log(` ${icon} ${result.recommendation} (${result.status})`);
|
|
924
|
+
if (result.server) console.log(` Server: ${result.server}`);
|
|
925
|
+
if (result.framework) console.log(` Framework: ${result.framework}`);
|
|
926
|
+
if (result.botProtection) console.log(` Bot protection: ${result.botProtection}`);
|
|
927
|
+
if (result.signals.length > 0) console.log(` Signals: ${result.signals.join(', ')}`);
|
|
928
|
+
console.log();
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
async function handleRead(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
|
|
932
|
+
const url = positional[0];
|
|
933
|
+
if (!url) {
|
|
934
|
+
console.error('Error: URL required. Usage: apitap read <url>');
|
|
935
|
+
process.exit(1);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const json = flags.json === true;
|
|
939
|
+
const fullUrl = url.startsWith('http') ? url : `https://${url}`;
|
|
940
|
+
const maxBytes = typeof flags['max-bytes'] === 'string' ? parseInt(flags['max-bytes'], 10) : undefined;
|
|
941
|
+
|
|
942
|
+
if (!json) {
|
|
943
|
+
console.log(`\n Reading ${url}...\n`);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const result = await read(fullUrl, { maxBytes });
|
|
947
|
+
|
|
948
|
+
if (!result) {
|
|
949
|
+
if (json) {
|
|
950
|
+
console.log(JSON.stringify({ error: 'Failed to read content' }));
|
|
951
|
+
} else {
|
|
952
|
+
console.error(' Failed to read content\n');
|
|
953
|
+
}
|
|
954
|
+
process.exit(1);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if (json) {
|
|
958
|
+
console.log(JSON.stringify(result, null, 2));
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (result.title) console.log(` ${result.title}`);
|
|
963
|
+
console.log(` Source: ${result.metadata.source} | ~${result.cost.tokens} tokens\n`);
|
|
964
|
+
console.log(result.content);
|
|
965
|
+
console.log();
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
async function main(): Promise<void> {
|
|
969
|
+
const { command, positional, flags } = parseArgs(process.argv.slice(2));
|
|
970
|
+
|
|
971
|
+
// Handle --version flag before command dispatch
|
|
972
|
+
if (command === '--version' || command === 'version' || flags.version === true) {
|
|
973
|
+
console.log(VERSION);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
switch (command) {
|
|
978
|
+
case 'capture':
|
|
979
|
+
await handleCapture(positional, flags);
|
|
980
|
+
break;
|
|
981
|
+
case 'discover':
|
|
982
|
+
await handleDiscover(positional, flags);
|
|
983
|
+
break;
|
|
984
|
+
case 'list':
|
|
985
|
+
await handleList(flags);
|
|
986
|
+
break;
|
|
987
|
+
case 'show':
|
|
988
|
+
await handleShow(positional, flags);
|
|
989
|
+
break;
|
|
990
|
+
case 'replay':
|
|
991
|
+
await handleReplay(positional, flags);
|
|
992
|
+
break;
|
|
993
|
+
case 'search':
|
|
994
|
+
await handleSearch(positional, flags);
|
|
995
|
+
break;
|
|
996
|
+
case 'import':
|
|
997
|
+
await handleImport(positional, flags);
|
|
998
|
+
break;
|
|
999
|
+
case 'refresh':
|
|
1000
|
+
await handleRefresh(positional, flags);
|
|
1001
|
+
break;
|
|
1002
|
+
case 'auth':
|
|
1003
|
+
await handleAuth(positional, flags);
|
|
1004
|
+
break;
|
|
1005
|
+
case 'serve':
|
|
1006
|
+
await handleServe(positional, flags);
|
|
1007
|
+
break;
|
|
1008
|
+
case 'inspect':
|
|
1009
|
+
await handleInspect(positional, flags);
|
|
1010
|
+
break;
|
|
1011
|
+
case 'stats':
|
|
1012
|
+
await handleStats(flags);
|
|
1013
|
+
break;
|
|
1014
|
+
case 'browse':
|
|
1015
|
+
await handleBrowse(positional, flags);
|
|
1016
|
+
break;
|
|
1017
|
+
case 'peek':
|
|
1018
|
+
await handlePeek(positional, flags);
|
|
1019
|
+
break;
|
|
1020
|
+
case 'read':
|
|
1021
|
+
await handleRead(positional, flags);
|
|
1022
|
+
break;
|
|
1023
|
+
default:
|
|
1024
|
+
printUsage();
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
main().catch((err) => {
|
|
1029
|
+
console.error(`Error: ${err.message}`);
|
|
1030
|
+
process.exit(1);
|
|
1031
|
+
});
|