@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
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// src/discovery/openapi.ts
|
|
2
|
+
import type { SkillEndpoint, SkillFile, DiscoveredSpec } from '../types.js';
|
|
3
|
+
import { safeFetch } from './fetch.js';
|
|
4
|
+
|
|
5
|
+
/** Paths to check for API specs, in priority order */
|
|
6
|
+
const SPEC_PATHS = [
|
|
7
|
+
'/openapi.json',
|
|
8
|
+
'/swagger.json',
|
|
9
|
+
'/api-docs',
|
|
10
|
+
'/api/docs',
|
|
11
|
+
'/.well-known/openapi',
|
|
12
|
+
'/v1/openapi.json',
|
|
13
|
+
'/v2/openapi.json',
|
|
14
|
+
'/v3/openapi.json',
|
|
15
|
+
'/docs/api.json',
|
|
16
|
+
'/api/swagger.json',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
interface OpenApiSpec {
|
|
20
|
+
openapi?: string; // "3.x.x"
|
|
21
|
+
swagger?: string; // "2.0"
|
|
22
|
+
info?: { title?: string; version?: string };
|
|
23
|
+
paths?: Record<string, Record<string, OpenApiOperation>>;
|
|
24
|
+
servers?: { url: string }[];
|
|
25
|
+
host?: string; // Swagger 2.0
|
|
26
|
+
basePath?: string; // Swagger 2.0
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface OpenApiOperation {
|
|
30
|
+
operationId?: string;
|
|
31
|
+
summary?: string;
|
|
32
|
+
parameters?: OpenApiParameter[];
|
|
33
|
+
requestBody?: {
|
|
34
|
+
content?: Record<string, { schema?: unknown }>;
|
|
35
|
+
};
|
|
36
|
+
responses?: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface OpenApiParameter {
|
|
40
|
+
name: string;
|
|
41
|
+
in: 'query' | 'path' | 'header' | 'cookie';
|
|
42
|
+
required?: boolean;
|
|
43
|
+
schema?: { type?: string };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SpecDiscoveryOptions {
|
|
47
|
+
skipSsrf?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check for API specs at common paths and in Link headers.
|
|
52
|
+
* Returns discovered specs with their URLs.
|
|
53
|
+
*/
|
|
54
|
+
export async function discoverSpecs(
|
|
55
|
+
baseUrl: string,
|
|
56
|
+
homepageHeaders?: Record<string, string>,
|
|
57
|
+
options: SpecDiscoveryOptions = {},
|
|
58
|
+
): Promise<DiscoveredSpec[]> {
|
|
59
|
+
const specs: DiscoveredSpec[] = [];
|
|
60
|
+
const origin = new URL(baseUrl).origin;
|
|
61
|
+
|
|
62
|
+
// Check Link header from homepage for rel="describedby"
|
|
63
|
+
if (homepageHeaders) {
|
|
64
|
+
const linkHeader = homepageHeaders['link'] || homepageHeaders['Link'];
|
|
65
|
+
if (linkHeader) {
|
|
66
|
+
const describedBy = parseLinkHeader(linkHeader, 'describedby');
|
|
67
|
+
if (describedBy) {
|
|
68
|
+
const specUrl = describedBy.startsWith('http') ? describedBy : `${origin}${describedBy}`;
|
|
69
|
+
const result = await tryFetchSpec(specUrl, options);
|
|
70
|
+
if (result) specs.push(result);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Probe common spec paths in parallel
|
|
76
|
+
const checks = SPEC_PATHS.map(async (path) => {
|
|
77
|
+
const specUrl = `${origin}${path}`;
|
|
78
|
+
return tryFetchSpec(specUrl, options);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const results = await Promise.all(checks);
|
|
82
|
+
for (const result of results) {
|
|
83
|
+
if (result && !specs.some(s => s.url === result.url)) {
|
|
84
|
+
specs.push(result);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return specs;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function tryFetchSpec(url: string, options: SpecDiscoveryOptions = {}): Promise<DiscoveredSpec | null> {
|
|
92
|
+
const result = await safeFetch(url, { timeout: 5000, skipSsrf: options.skipSsrf });
|
|
93
|
+
if (!result || result.status !== 200) return null;
|
|
94
|
+
|
|
95
|
+
// Must look like JSON
|
|
96
|
+
const ct = result.contentType.toLowerCase();
|
|
97
|
+
if (!ct.includes('json') && !ct.includes('yaml') && !ct.includes('text/plain')) return null;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const spec = JSON.parse(result.body) as OpenApiSpec;
|
|
101
|
+
if (spec.openapi || spec.swagger) {
|
|
102
|
+
const endpointCount = spec.paths ? Object.keys(spec.paths).reduce((sum, path) => {
|
|
103
|
+
return sum + Object.keys(spec.paths![path]).filter(m => ['get', 'post', 'put', 'patch', 'delete'].includes(m)).length;
|
|
104
|
+
}, 0) : 0;
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
type: spec.openapi ? 'openapi' : 'swagger',
|
|
108
|
+
url,
|
|
109
|
+
version: spec.openapi || spec.swagger,
|
|
110
|
+
endpointCount,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// Not valid JSON or not an API spec
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Parse an OpenAPI/Swagger spec into a SkillFile.
|
|
121
|
+
*/
|
|
122
|
+
export async function parseSpecToSkillFile(
|
|
123
|
+
specUrl: string,
|
|
124
|
+
domain: string,
|
|
125
|
+
baseUrl: string,
|
|
126
|
+
options: SpecDiscoveryOptions = {},
|
|
127
|
+
): Promise<SkillFile | null> {
|
|
128
|
+
const result = await safeFetch(specUrl, { timeout: 10000, skipSsrf: options.skipSsrf });
|
|
129
|
+
if (!result || result.status !== 200) return null;
|
|
130
|
+
|
|
131
|
+
let spec: OpenApiSpec;
|
|
132
|
+
try {
|
|
133
|
+
spec = JSON.parse(result.body);
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!spec.paths) return null;
|
|
139
|
+
|
|
140
|
+
// Determine API base URL
|
|
141
|
+
let apiBase = baseUrl;
|
|
142
|
+
if (spec.servers?.[0]?.url) {
|
|
143
|
+
const serverUrl = spec.servers[0].url;
|
|
144
|
+
apiBase = serverUrl.startsWith('http') ? serverUrl : `${baseUrl}${serverUrl}`;
|
|
145
|
+
} else if (spec.host) {
|
|
146
|
+
const scheme = specUrl.startsWith('https') ? 'https' : 'http';
|
|
147
|
+
apiBase = `${scheme}://${spec.host}${spec.basePath || ''}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const endpoints: SkillEndpoint[] = [];
|
|
151
|
+
|
|
152
|
+
for (const [path, methods] of Object.entries(spec.paths)) {
|
|
153
|
+
for (const [method, operation] of Object.entries(methods)) {
|
|
154
|
+
if (!['get', 'post', 'put', 'patch', 'delete'].includes(method)) continue;
|
|
155
|
+
const op = operation as OpenApiOperation;
|
|
156
|
+
|
|
157
|
+
// Parameterize path: {id} → :id
|
|
158
|
+
const paramPath = path.replace(/\{([^}]+)\}/g, ':$1');
|
|
159
|
+
|
|
160
|
+
// Extract query params
|
|
161
|
+
const queryParams: Record<string, { type: string; example: string }> = {};
|
|
162
|
+
if (op.parameters) {
|
|
163
|
+
for (const param of op.parameters) {
|
|
164
|
+
if (param.in === 'query') {
|
|
165
|
+
queryParams[param.name] = {
|
|
166
|
+
type: param.schema?.type || 'string',
|
|
167
|
+
example: '',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Generate endpoint ID
|
|
174
|
+
const id = op.operationId
|
|
175
|
+
? method.toLowerCase() + '-' + op.operationId.replace(/[^a-z0-9]/gi, '-').toLowerCase()
|
|
176
|
+
: generateId(method, paramPath);
|
|
177
|
+
|
|
178
|
+
endpoints.push({
|
|
179
|
+
id,
|
|
180
|
+
method: method.toUpperCase(),
|
|
181
|
+
path: paramPath,
|
|
182
|
+
queryParams,
|
|
183
|
+
headers: {},
|
|
184
|
+
responseShape: { type: 'unknown' },
|
|
185
|
+
examples: {
|
|
186
|
+
request: { url: `${apiBase}${path}`, headers: {} },
|
|
187
|
+
responsePreview: null,
|
|
188
|
+
},
|
|
189
|
+
replayability: {
|
|
190
|
+
tier: 'unknown',
|
|
191
|
+
verified: false,
|
|
192
|
+
signals: ['discovered-from-spec'],
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (endpoints.length === 0) return null;
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
version: '1.2',
|
|
202
|
+
domain,
|
|
203
|
+
capturedAt: new Date().toISOString(),
|
|
204
|
+
baseUrl: apiBase,
|
|
205
|
+
endpoints,
|
|
206
|
+
metadata: {
|
|
207
|
+
captureCount: 0,
|
|
208
|
+
filteredCount: 0,
|
|
209
|
+
toolVersion: '1.0.0',
|
|
210
|
+
},
|
|
211
|
+
provenance: 'unsigned',
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function generateId(method: string, path: string): string {
|
|
216
|
+
const segments = path.split('/').filter(s => s !== '' && !s.startsWith(':'));
|
|
217
|
+
const slug = segments.join('-').replace(/[^a-z0-9-]/gi, '').toLowerCase() || 'root';
|
|
218
|
+
return `${method.toLowerCase()}-${slug}`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function parseLinkHeader(header: string, rel: string): string | null {
|
|
222
|
+
const parts = header.split(',');
|
|
223
|
+
for (const part of parts) {
|
|
224
|
+
const match = part.match(/<([^>]+)>.*rel\s*=\s*"?([^",;]+)"?/);
|
|
225
|
+
if (match && match[2].trim() === rel) {
|
|
226
|
+
return match[1];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// src/discovery/probes.ts
|
|
2
|
+
import type { ProbeResult } from '../types.js';
|
|
3
|
+
import { safeFetch } from './fetch.js';
|
|
4
|
+
|
|
5
|
+
/** Common API paths to probe */
|
|
6
|
+
const PROBE_PATHS = [
|
|
7
|
+
'/api/',
|
|
8
|
+
'/api/v1/',
|
|
9
|
+
'/api/v2/',
|
|
10
|
+
'/_api/',
|
|
11
|
+
'/rest/',
|
|
12
|
+
'/graphql',
|
|
13
|
+
'/gql',
|
|
14
|
+
'/api/graphql',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export interface ProbeOptions {
|
|
18
|
+
skipSsrf?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Probe common API paths with GET requests.
|
|
23
|
+
* Returns results for paths that respond with API-like content types.
|
|
24
|
+
*/
|
|
25
|
+
export async function probeApiPaths(baseUrl: string, options: ProbeOptions = {}): Promise<ProbeResult[]> {
|
|
26
|
+
const origin = new URL(baseUrl).origin;
|
|
27
|
+
const results: ProbeResult[] = [];
|
|
28
|
+
|
|
29
|
+
const checks = PROBE_PATHS.map(async (path): Promise<ProbeResult | null> => {
|
|
30
|
+
const url = `${origin}${path}`;
|
|
31
|
+
const result = await safeFetch(url, { timeout: 5000, method: 'GET', maxBodySize: 4096, skipSsrf: options.skipSsrf });
|
|
32
|
+
if (!result) return null;
|
|
33
|
+
|
|
34
|
+
// Don't count redirects to login pages or error pages
|
|
35
|
+
if (result.status >= 400 && result.status !== 401 && result.status !== 403) return null;
|
|
36
|
+
|
|
37
|
+
const ct = result.contentType.toLowerCase();
|
|
38
|
+
const isApi = isApiContentType(ct, result.body, result.status);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
method: 'GET',
|
|
42
|
+
path,
|
|
43
|
+
status: result.status,
|
|
44
|
+
contentType: result.contentType,
|
|
45
|
+
isApi,
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const settled = await Promise.all(checks);
|
|
50
|
+
for (const result of settled) {
|
|
51
|
+
if (result) results.push(result);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return results;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isApiContentType(contentType: string, body: string, status: number): boolean {
|
|
58
|
+
// JSON responses are API
|
|
59
|
+
if (contentType.includes('json')) return true;
|
|
60
|
+
// XML/SOAP
|
|
61
|
+
if (contentType.includes('xml')) return true;
|
|
62
|
+
// 401/403 at an API path means something is there (but needs auth)
|
|
63
|
+
if ((status === 401 || status === 403) && !contentType.includes('html')) return true;
|
|
64
|
+
// GraphQL introspection response
|
|
65
|
+
if (body.includes('"data"') && body.includes('"__schema"')) return true;
|
|
66
|
+
// Check if body looks like JSON even without proper content-type
|
|
67
|
+
if (body.trim().startsWith('{') || body.trim().startsWith('[')) {
|
|
68
|
+
try {
|
|
69
|
+
JSON.parse(body);
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
// Not JSON
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
export { capture, type CaptureOptions, type CaptureResult } from './capture/monitor.js';
|
|
3
|
+
export { shouldCapture } from './capture/filter.js';
|
|
4
|
+
export { isBlocklisted } from './capture/blocklist.js';
|
|
5
|
+
export { isDomainMatch } from './capture/domain.js';
|
|
6
|
+
export { scrubPII } from './capture/scrubber.js';
|
|
7
|
+
export { SkillGenerator } from './skill/generator.js';
|
|
8
|
+
export { writeSkillFile, readSkillFile, listSkillFiles } from './skill/store.js';
|
|
9
|
+
export { signSkillFile, verifySignature } from './skill/signing.js';
|
|
10
|
+
export { validateImport, importSkillFile } from './skill/importer.js';
|
|
11
|
+
export { validateUrl, validateSkillFileUrls, resolveAndValidateUrl, resolveAndValidateSkillFileUrls } from './skill/ssrf.js';
|
|
12
|
+
export { replayEndpoint, type ReplayResult } from './replay/engine.js';
|
|
13
|
+
export { peek, read, type PeekOptions, type ReadOptions } from './read/index.js';
|
|
14
|
+
export type { PeekResult, ReadResult, Decoder } from './read/types.js';
|
|
15
|
+
export { AuthManager, getMachineId } from './auth/manager.js';
|
|
16
|
+
export { parameterizePath, cleanFrameworkPath } from './capture/parameterize.js';
|
|
17
|
+
export { detectPagination } from './capture/pagination.js';
|
|
18
|
+
export { verifyEndpoints } from './capture/verifier.js';
|
|
19
|
+
export { IdleTracker } from './capture/idle.js';
|
|
20
|
+
export { isPathNoise } from './capture/filter.js';
|
|
21
|
+
export { searchSkills, type SearchResult, type SearchResponse } from './skill/search.js';
|
|
22
|
+
export { createPlugin, type Plugin, type ToolDefinition, type PluginOptions } from './plugin.js';
|
|
23
|
+
export { shannonEntropy, isLikelyToken, parseJwtClaims, type TokenClassification, type JwtClaims } from './capture/entropy.js';
|
|
24
|
+
export { isOAuthTokenRequest, type OAuthInfo } from './capture/oauth-detector.js';
|
|
25
|
+
export { CaptureSession, type SessionOptions, type InteractionAction } from './capture/session.js';
|
|
26
|
+
export type { SkillFile, SkillEndpoint, SkillSummary, CapturedExchange, StoredAuth, OAuthConfig, Replayability, PaginationInfo, PageSnapshot, PageElement, InteractionResult, FinishResult } from './types.js';
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// src/inspect/report.ts
|
|
2
|
+
import type { SkillFile, SkillEndpoint } from '../types.js';
|
|
3
|
+
import { detectAntiBot, type AntiBotSignal } from '../capture/anti-bot.js';
|
|
4
|
+
|
|
5
|
+
export interface InspectReport {
|
|
6
|
+
domain: string;
|
|
7
|
+
scanDuration: number;
|
|
8
|
+
totalRequests: number;
|
|
9
|
+
filteredRequests: number;
|
|
10
|
+
domBytes?: number;
|
|
11
|
+
endpoints: InspectEndpoint[];
|
|
12
|
+
antiBot: AntiBotSignal[];
|
|
13
|
+
summary: {
|
|
14
|
+
total: number;
|
|
15
|
+
replayable: number;
|
|
16
|
+
authRequired: number;
|
|
17
|
+
framework: string | null;
|
|
18
|
+
browserTokens: number;
|
|
19
|
+
replayTokens: number;
|
|
20
|
+
savingsPercent: number;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface InspectEndpoint {
|
|
25
|
+
method: string;
|
|
26
|
+
path: string;
|
|
27
|
+
tier: string;
|
|
28
|
+
auth: string;
|
|
29
|
+
responseBytes: number;
|
|
30
|
+
responseShape: { type: string; fields?: string[] };
|
|
31
|
+
graphql: { operations: string[] } | null;
|
|
32
|
+
pagination: { type: string; paramName: string } | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build an inspect report from capture results.
|
|
37
|
+
*/
|
|
38
|
+
export function buildInspectReport(options: {
|
|
39
|
+
skills: Map<string, SkillFile>;
|
|
40
|
+
totalRequests: number;
|
|
41
|
+
filteredRequests: number;
|
|
42
|
+
duration: number;
|
|
43
|
+
domBytes?: number;
|
|
44
|
+
antiBotSignals: AntiBotSignal[];
|
|
45
|
+
targetDomain: string;
|
|
46
|
+
}): InspectReport {
|
|
47
|
+
const { skills, totalRequests, filteredRequests, duration, domBytes, antiBotSignals, targetDomain } = options;
|
|
48
|
+
|
|
49
|
+
// Merge all endpoints across domains for the report
|
|
50
|
+
const allEndpoints: InspectEndpoint[] = [];
|
|
51
|
+
let totalResponseBytes = 0;
|
|
52
|
+
let authCount = 0;
|
|
53
|
+
|
|
54
|
+
for (const skill of skills.values()) {
|
|
55
|
+
for (const ep of skill.endpoints) {
|
|
56
|
+
const auth = getAuthLabel(ep);
|
|
57
|
+
if (auth !== 'none') authCount++;
|
|
58
|
+
|
|
59
|
+
const respBytes = ep.responseBytes ?? 0;
|
|
60
|
+
totalResponseBytes += respBytes;
|
|
61
|
+
|
|
62
|
+
allEndpoints.push({
|
|
63
|
+
method: ep.method,
|
|
64
|
+
path: ep.path,
|
|
65
|
+
tier: ep.replayability?.tier ?? 'unknown',
|
|
66
|
+
auth,
|
|
67
|
+
responseBytes: respBytes,
|
|
68
|
+
responseShape: ep.responseShape,
|
|
69
|
+
graphql: isGraphQL(ep) ? { operations: [ep.id.replace(/^(get|post)-graphql-/, '')] } : null,
|
|
70
|
+
pagination: ep.pagination ? { type: ep.pagination.type, paramName: ep.pagination.paramName } : null,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const replayable = allEndpoints.filter(ep =>
|
|
76
|
+
ep.tier === 'green' || ep.tier === 'yellow',
|
|
77
|
+
).length;
|
|
78
|
+
|
|
79
|
+
const browserTokens = domBytes ? Math.round(domBytes / 4) : 0;
|
|
80
|
+
const replayTokens = Math.round(totalResponseBytes / 4);
|
|
81
|
+
const savingsPercent = browserTokens > 0
|
|
82
|
+
? Math.round((1 - replayTokens / browserTokens) * 1000) / 10
|
|
83
|
+
: 0;
|
|
84
|
+
|
|
85
|
+
const framework = detectFramework(allEndpoints);
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
domain: targetDomain,
|
|
89
|
+
scanDuration: duration,
|
|
90
|
+
totalRequests,
|
|
91
|
+
filteredRequests,
|
|
92
|
+
domBytes,
|
|
93
|
+
endpoints: allEndpoints,
|
|
94
|
+
antiBot: antiBotSignals,
|
|
95
|
+
summary: {
|
|
96
|
+
total: allEndpoints.length,
|
|
97
|
+
replayable,
|
|
98
|
+
authRequired: authCount,
|
|
99
|
+
framework,
|
|
100
|
+
browserTokens,
|
|
101
|
+
replayTokens,
|
|
102
|
+
savingsPercent,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function formatInspectHuman(report: InspectReport): string {
|
|
108
|
+
const lines: string[] = [
|
|
109
|
+
'',
|
|
110
|
+
` ${report.domain} — API Discovery Report`,
|
|
111
|
+
' ' + '═'.repeat(report.domain.length + 25),
|
|
112
|
+
'',
|
|
113
|
+
` Scan duration: ${report.scanDuration}s`,
|
|
114
|
+
` Total requests: ${report.totalRequests}`,
|
|
115
|
+
` Filtered (noise): ${report.filteredRequests} (${pct(report.filteredRequests, report.totalRequests)})`,
|
|
116
|
+
` API endpoints: ${report.summary.total}` + endpointBreakdown(report.endpoints),
|
|
117
|
+
'',
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
// Auth and anti-bot info
|
|
121
|
+
if (report.summary.authRequired > 0) {
|
|
122
|
+
lines.push(` Auth required: ${report.summary.authRequired} endpoints`);
|
|
123
|
+
} else {
|
|
124
|
+
lines.push(' Auth required: None');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (report.antiBot.length > 0) {
|
|
128
|
+
lines.push(` Anti-bot: ${report.antiBot.join(', ')} detected`);
|
|
129
|
+
} else {
|
|
130
|
+
lines.push(' Anti-bot: None detected');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (report.summary.framework) {
|
|
134
|
+
lines.push(` Framework: ${report.summary.framework}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
lines.push('');
|
|
138
|
+
|
|
139
|
+
// Endpoint table
|
|
140
|
+
if (report.endpoints.length > 0) {
|
|
141
|
+
lines.push(' Endpoints:');
|
|
142
|
+
const methodW = 8;
|
|
143
|
+
const pathW = 32;
|
|
144
|
+
const tierW = 8;
|
|
145
|
+
const authW = 8;
|
|
146
|
+
const sizeW = 10;
|
|
147
|
+
|
|
148
|
+
const sep = ' ' + '─'.repeat(methodW + pathW + tierW + authW + sizeW);
|
|
149
|
+
lines.push(sep);
|
|
150
|
+
lines.push(' ' + pad('Method', methodW) + pad('Path', pathW) + pad('Tier', tierW) + pad('Auth', authW) + 'Size');
|
|
151
|
+
lines.push(sep);
|
|
152
|
+
|
|
153
|
+
for (const ep of report.endpoints) {
|
|
154
|
+
const pathStr = ep.path.length > pathW - 2 ? ep.path.slice(0, pathW - 3) + '…' : ep.path;
|
|
155
|
+
lines.push(' ' +
|
|
156
|
+
pad(ep.method, methodW) +
|
|
157
|
+
pad(pathStr, pathW) +
|
|
158
|
+
pad(ep.tier, tierW) +
|
|
159
|
+
pad(ep.auth, authW) +
|
|
160
|
+
formatBytes(ep.responseBytes),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
lines.push(sep);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
lines.push('');
|
|
167
|
+
|
|
168
|
+
// Data shapes
|
|
169
|
+
const shapedEndpoints = report.endpoints.filter(ep => ep.responseShape.fields?.length);
|
|
170
|
+
if (shapedEndpoints.length > 0) {
|
|
171
|
+
lines.push(' Data shapes:');
|
|
172
|
+
for (const ep of shapedEndpoints.slice(0, 5)) {
|
|
173
|
+
const fields = ep.responseShape.fields!.slice(0, 6).join(', ');
|
|
174
|
+
const suffix = ep.responseShape.fields!.length > 6 ? ', ...' : '';
|
|
175
|
+
lines.push(` ${ep.method} ${ep.path} → { type: "${ep.responseShape.type}", fields: [${fields}${suffix}] }`);
|
|
176
|
+
}
|
|
177
|
+
lines.push('');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Summary
|
|
181
|
+
lines.push(' Summary:');
|
|
182
|
+
lines.push(` Replayable: ${report.summary.replayable} of ${report.summary.total} endpoints`);
|
|
183
|
+
if (report.domBytes && report.summary.browserTokens > 0) {
|
|
184
|
+
lines.push(` DOM size: ${formatBytes(report.domBytes)} = ${formatTokens(report.summary.browserTokens)} (what browser automation sends to LLM)`);
|
|
185
|
+
lines.push(` API replay: ${formatTokens(report.summary.replayTokens)}`);
|
|
186
|
+
lines.push(` Savings: ${report.summary.savingsPercent}%`);
|
|
187
|
+
if (report.summary.savingsPercent < 0) {
|
|
188
|
+
lines.push(' API responses exceed DOM size — browser automation may be more token-efficient');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
lines.push('');
|
|
192
|
+
lines.push(` To capture these endpoints: apitap capture ${report.domain}`);
|
|
193
|
+
lines.push('');
|
|
194
|
+
|
|
195
|
+
return lines.join('\n');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getAuthLabel(ep: SkillEndpoint): string {
|
|
199
|
+
const headers = ep.headers;
|
|
200
|
+
if (headers.authorization?.includes('[stored]')) return 'Bearer';
|
|
201
|
+
if (headers['x-api-key']?.includes('[stored]')) return 'API Key';
|
|
202
|
+
for (const [key, val] of Object.entries(headers)) {
|
|
203
|
+
if (val === '[stored]') return key;
|
|
204
|
+
}
|
|
205
|
+
return 'none';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isGraphQL(ep: SkillEndpoint): boolean {
|
|
209
|
+
return ep.id.includes('graphql');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function detectFramework(endpoints: InspectEndpoint[]): string | null {
|
|
213
|
+
const paths = endpoints.map(e => e.path);
|
|
214
|
+
if (paths.some(p => p.includes('_next/'))) return 'Next.js';
|
|
215
|
+
if (paths.some(p => p.includes('__nuxt'))) return 'Nuxt';
|
|
216
|
+
if (endpoints.some(e => e.graphql)) return 'GraphQL';
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function endpointBreakdown(endpoints: InspectEndpoint[]): string {
|
|
221
|
+
const rest = endpoints.filter(e => !e.graphql).length;
|
|
222
|
+
const gql = endpoints.filter(e => e.graphql).length;
|
|
223
|
+
if (gql > 0) return ` (${rest} REST, ${gql} GraphQL)`;
|
|
224
|
+
return '';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function pct(part: number, total: number): string {
|
|
228
|
+
if (total === 0) return '0%';
|
|
229
|
+
return `${Math.round((part / total) * 100)}%`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function pad(s: string, width: number): string {
|
|
233
|
+
return s.length >= width ? s : s + ' '.repeat(width - s.length);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function formatBytes(bytes: number): string {
|
|
237
|
+
if (bytes >= 1_000_000) return `${(bytes / 1_000_000).toFixed(1)} MB`;
|
|
238
|
+
if (bytes >= 1_000) return `${(bytes / 1_000).toFixed(1)} KB`;
|
|
239
|
+
if (bytes > 0) return `${bytes} B`;
|
|
240
|
+
return '-';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function formatTokens(tokens: number): string {
|
|
244
|
+
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(2)}M tokens`;
|
|
245
|
+
if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}K tokens`;
|
|
246
|
+
return `${tokens} tokens`;
|
|
247
|
+
}
|