@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.
Files changed (236) hide show
  1. package/LICENSE +60 -0
  2. package/README.md +362 -0
  3. package/SKILL.md +270 -0
  4. package/dist/auth/crypto.d.ts +31 -0
  5. package/dist/auth/crypto.js +66 -0
  6. package/dist/auth/crypto.js.map +1 -0
  7. package/dist/auth/handoff.d.ts +29 -0
  8. package/dist/auth/handoff.js +180 -0
  9. package/dist/auth/handoff.js.map +1 -0
  10. package/dist/auth/manager.d.ts +46 -0
  11. package/dist/auth/manager.js +127 -0
  12. package/dist/auth/manager.js.map +1 -0
  13. package/dist/auth/oauth-refresh.d.ts +16 -0
  14. package/dist/auth/oauth-refresh.js +91 -0
  15. package/dist/auth/oauth-refresh.js.map +1 -0
  16. package/dist/auth/refresh.d.ts +43 -0
  17. package/dist/auth/refresh.js +217 -0
  18. package/dist/auth/refresh.js.map +1 -0
  19. package/dist/capture/anti-bot.d.ts +15 -0
  20. package/dist/capture/anti-bot.js +43 -0
  21. package/dist/capture/anti-bot.js.map +1 -0
  22. package/dist/capture/blocklist.d.ts +6 -0
  23. package/dist/capture/blocklist.js +70 -0
  24. package/dist/capture/blocklist.js.map +1 -0
  25. package/dist/capture/body-diff.d.ts +8 -0
  26. package/dist/capture/body-diff.js +102 -0
  27. package/dist/capture/body-diff.js.map +1 -0
  28. package/dist/capture/body-variables.d.ts +13 -0
  29. package/dist/capture/body-variables.js +142 -0
  30. package/dist/capture/body-variables.js.map +1 -0
  31. package/dist/capture/domain.d.ts +8 -0
  32. package/dist/capture/domain.js +34 -0
  33. package/dist/capture/domain.js.map +1 -0
  34. package/dist/capture/entropy.d.ts +33 -0
  35. package/dist/capture/entropy.js +100 -0
  36. package/dist/capture/entropy.js.map +1 -0
  37. package/dist/capture/filter.d.ts +11 -0
  38. package/dist/capture/filter.js +49 -0
  39. package/dist/capture/filter.js.map +1 -0
  40. package/dist/capture/graphql.d.ts +21 -0
  41. package/dist/capture/graphql.js +99 -0
  42. package/dist/capture/graphql.js.map +1 -0
  43. package/dist/capture/idle.d.ts +23 -0
  44. package/dist/capture/idle.js +44 -0
  45. package/dist/capture/idle.js.map +1 -0
  46. package/dist/capture/monitor.d.ts +26 -0
  47. package/dist/capture/monitor.js +183 -0
  48. package/dist/capture/monitor.js.map +1 -0
  49. package/dist/capture/oauth-detector.d.ts +18 -0
  50. package/dist/capture/oauth-detector.js +96 -0
  51. package/dist/capture/oauth-detector.js.map +1 -0
  52. package/dist/capture/pagination.d.ts +9 -0
  53. package/dist/capture/pagination.js +40 -0
  54. package/dist/capture/pagination.js.map +1 -0
  55. package/dist/capture/parameterize.d.ts +17 -0
  56. package/dist/capture/parameterize.js +63 -0
  57. package/dist/capture/parameterize.js.map +1 -0
  58. package/dist/capture/scrubber.d.ts +5 -0
  59. package/dist/capture/scrubber.js +38 -0
  60. package/dist/capture/scrubber.js.map +1 -0
  61. package/dist/capture/session.d.ts +46 -0
  62. package/dist/capture/session.js +445 -0
  63. package/dist/capture/session.js.map +1 -0
  64. package/dist/capture/token-detector.d.ts +16 -0
  65. package/dist/capture/token-detector.js +62 -0
  66. package/dist/capture/token-detector.js.map +1 -0
  67. package/dist/capture/verifier.d.ts +17 -0
  68. package/dist/capture/verifier.js +147 -0
  69. package/dist/capture/verifier.js.map +1 -0
  70. package/dist/cli.d.ts +2 -0
  71. package/dist/cli.js +930 -0
  72. package/dist/cli.js.map +1 -0
  73. package/dist/discovery/auth.d.ts +17 -0
  74. package/dist/discovery/auth.js +81 -0
  75. package/dist/discovery/auth.js.map +1 -0
  76. package/dist/discovery/fetch.d.ts +17 -0
  77. package/dist/discovery/fetch.js +59 -0
  78. package/dist/discovery/fetch.js.map +1 -0
  79. package/dist/discovery/frameworks.d.ts +11 -0
  80. package/dist/discovery/frameworks.js +249 -0
  81. package/dist/discovery/frameworks.js.map +1 -0
  82. package/dist/discovery/index.d.ts +21 -0
  83. package/dist/discovery/index.js +219 -0
  84. package/dist/discovery/index.js.map +1 -0
  85. package/dist/discovery/openapi.d.ts +13 -0
  86. package/dist/discovery/openapi.js +175 -0
  87. package/dist/discovery/openapi.js.map +1 -0
  88. package/dist/discovery/probes.d.ts +9 -0
  89. package/dist/discovery/probes.js +70 -0
  90. package/dist/discovery/probes.js.map +1 -0
  91. package/dist/index.d.ts +25 -0
  92. package/dist/index.js +25 -0
  93. package/dist/index.js.map +1 -0
  94. package/dist/inspect/report.d.ts +52 -0
  95. package/dist/inspect/report.js +191 -0
  96. package/dist/inspect/report.js.map +1 -0
  97. package/dist/mcp.d.ts +8 -0
  98. package/dist/mcp.js +526 -0
  99. package/dist/mcp.js.map +1 -0
  100. package/dist/orchestration/browse.d.ts +38 -0
  101. package/dist/orchestration/browse.js +198 -0
  102. package/dist/orchestration/browse.js.map +1 -0
  103. package/dist/orchestration/cache.d.ts +15 -0
  104. package/dist/orchestration/cache.js +24 -0
  105. package/dist/orchestration/cache.js.map +1 -0
  106. package/dist/plugin.d.ts +17 -0
  107. package/dist/plugin.js +158 -0
  108. package/dist/plugin.js.map +1 -0
  109. package/dist/read/decoders/deepwiki.d.ts +2 -0
  110. package/dist/read/decoders/deepwiki.js +148 -0
  111. package/dist/read/decoders/deepwiki.js.map +1 -0
  112. package/dist/read/decoders/grokipedia.d.ts +2 -0
  113. package/dist/read/decoders/grokipedia.js +210 -0
  114. package/dist/read/decoders/grokipedia.js.map +1 -0
  115. package/dist/read/decoders/hackernews.d.ts +2 -0
  116. package/dist/read/decoders/hackernews.js +168 -0
  117. package/dist/read/decoders/hackernews.js.map +1 -0
  118. package/dist/read/decoders/index.d.ts +2 -0
  119. package/dist/read/decoders/index.js +12 -0
  120. package/dist/read/decoders/index.js.map +1 -0
  121. package/dist/read/decoders/reddit.d.ts +2 -0
  122. package/dist/read/decoders/reddit.js +142 -0
  123. package/dist/read/decoders/reddit.js.map +1 -0
  124. package/dist/read/decoders/twitter.d.ts +12 -0
  125. package/dist/read/decoders/twitter.js +187 -0
  126. package/dist/read/decoders/twitter.js.map +1 -0
  127. package/dist/read/decoders/wikipedia.d.ts +2 -0
  128. package/dist/read/decoders/wikipedia.js +66 -0
  129. package/dist/read/decoders/wikipedia.js.map +1 -0
  130. package/dist/read/decoders/youtube.d.ts +2 -0
  131. package/dist/read/decoders/youtube.js +69 -0
  132. package/dist/read/decoders/youtube.js.map +1 -0
  133. package/dist/read/extract.d.ts +25 -0
  134. package/dist/read/extract.js +320 -0
  135. package/dist/read/extract.js.map +1 -0
  136. package/dist/read/index.d.ts +14 -0
  137. package/dist/read/index.js +66 -0
  138. package/dist/read/index.js.map +1 -0
  139. package/dist/read/peek.d.ts +9 -0
  140. package/dist/read/peek.js +137 -0
  141. package/dist/read/peek.js.map +1 -0
  142. package/dist/read/types.d.ts +44 -0
  143. package/dist/read/types.js +3 -0
  144. package/dist/read/types.js.map +1 -0
  145. package/dist/replay/engine.d.ts +53 -0
  146. package/dist/replay/engine.js +441 -0
  147. package/dist/replay/engine.js.map +1 -0
  148. package/dist/replay/truncate.d.ts +16 -0
  149. package/dist/replay/truncate.js +92 -0
  150. package/dist/replay/truncate.js.map +1 -0
  151. package/dist/serve.d.ts +31 -0
  152. package/dist/serve.js +149 -0
  153. package/dist/serve.js.map +1 -0
  154. package/dist/skill/generator.d.ts +44 -0
  155. package/dist/skill/generator.js +419 -0
  156. package/dist/skill/generator.js.map +1 -0
  157. package/dist/skill/importer.d.ts +26 -0
  158. package/dist/skill/importer.js +80 -0
  159. package/dist/skill/importer.js.map +1 -0
  160. package/dist/skill/search.d.ts +19 -0
  161. package/dist/skill/search.js +51 -0
  162. package/dist/skill/search.js.map +1 -0
  163. package/dist/skill/signing.d.ts +16 -0
  164. package/dist/skill/signing.js +34 -0
  165. package/dist/skill/signing.js.map +1 -0
  166. package/dist/skill/ssrf.d.ts +27 -0
  167. package/dist/skill/ssrf.js +210 -0
  168. package/dist/skill/ssrf.js.map +1 -0
  169. package/dist/skill/store.d.ts +7 -0
  170. package/dist/skill/store.js +93 -0
  171. package/dist/skill/store.js.map +1 -0
  172. package/dist/stats/report.d.ts +26 -0
  173. package/dist/stats/report.js +157 -0
  174. package/dist/stats/report.js.map +1 -0
  175. package/dist/types.d.ts +214 -0
  176. package/dist/types.js +3 -0
  177. package/dist/types.js.map +1 -0
  178. package/package.json +58 -0
  179. package/src/auth/crypto.ts +92 -0
  180. package/src/auth/handoff.ts +229 -0
  181. package/src/auth/manager.ts +140 -0
  182. package/src/auth/oauth-refresh.ts +120 -0
  183. package/src/auth/refresh.ts +300 -0
  184. package/src/capture/anti-bot.ts +63 -0
  185. package/src/capture/blocklist.ts +75 -0
  186. package/src/capture/body-diff.ts +109 -0
  187. package/src/capture/body-variables.ts +156 -0
  188. package/src/capture/domain.ts +34 -0
  189. package/src/capture/entropy.ts +121 -0
  190. package/src/capture/filter.ts +56 -0
  191. package/src/capture/graphql.ts +124 -0
  192. package/src/capture/idle.ts +45 -0
  193. package/src/capture/monitor.ts +224 -0
  194. package/src/capture/oauth-detector.ts +106 -0
  195. package/src/capture/pagination.ts +49 -0
  196. package/src/capture/parameterize.ts +68 -0
  197. package/src/capture/scrubber.ts +49 -0
  198. package/src/capture/session.ts +502 -0
  199. package/src/capture/token-detector.ts +76 -0
  200. package/src/capture/verifier.ts +171 -0
  201. package/src/cli.ts +1031 -0
  202. package/src/discovery/auth.ts +99 -0
  203. package/src/discovery/fetch.ts +85 -0
  204. package/src/discovery/frameworks.ts +231 -0
  205. package/src/discovery/index.ts +256 -0
  206. package/src/discovery/openapi.ts +230 -0
  207. package/src/discovery/probes.ts +76 -0
  208. package/src/index.ts +26 -0
  209. package/src/inspect/report.ts +247 -0
  210. package/src/mcp.ts +618 -0
  211. package/src/orchestration/browse.ts +250 -0
  212. package/src/orchestration/cache.ts +37 -0
  213. package/src/plugin.ts +188 -0
  214. package/src/read/decoders/deepwiki.ts +180 -0
  215. package/src/read/decoders/grokipedia.ts +246 -0
  216. package/src/read/decoders/hackernews.ts +198 -0
  217. package/src/read/decoders/index.ts +15 -0
  218. package/src/read/decoders/reddit.ts +158 -0
  219. package/src/read/decoders/twitter.ts +211 -0
  220. package/src/read/decoders/wikipedia.ts +75 -0
  221. package/src/read/decoders/youtube.ts +75 -0
  222. package/src/read/extract.ts +396 -0
  223. package/src/read/index.ts +78 -0
  224. package/src/read/peek.ts +175 -0
  225. package/src/read/types.ts +37 -0
  226. package/src/replay/engine.ts +559 -0
  227. package/src/replay/truncate.ts +116 -0
  228. package/src/serve.ts +189 -0
  229. package/src/skill/generator.ts +473 -0
  230. package/src/skill/importer.ts +107 -0
  231. package/src/skill/search.ts +76 -0
  232. package/src/skill/signing.ts +36 -0
  233. package/src/skill/ssrf.ts +238 -0
  234. package/src/skill/store.ts +107 -0
  235. package/src/stats/report.ts +208 -0
  236. 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
+ }