auto-api-discovery 1.0.1 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/crawler.js +1 -8
- package/dist/db.js +0 -2
- package/dist/index.js +0 -4
- package/dist/interceptor.js +8 -10
- package/dist/openapi-generator.js +1 -2
- package/dist/schema-engine.js +1 -2
- package/package.json +4 -3
package/dist/crawler.js
CHANGED
|
@@ -51,7 +51,6 @@ async function startCrawler(targetUrl, maxDepth = 2, maxPages = 50) {
|
|
|
51
51
|
try {
|
|
52
52
|
browser = await playwright_1.chromium.launch({ headless: true });
|
|
53
53
|
const context = await browser.newContext();
|
|
54
|
-
// Load cookies if available to run fully authenticated
|
|
55
54
|
if (fs.existsSync(SESSION_FILE)) {
|
|
56
55
|
try {
|
|
57
56
|
const cookies = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8'));
|
|
@@ -63,7 +62,6 @@ async function startCrawler(targetUrl, maxDepth = 2, maxPages = 50) {
|
|
|
63
62
|
}
|
|
64
63
|
}
|
|
65
64
|
const page = await context.newPage();
|
|
66
|
-
// Attach the EXACT same interceptor from Milestone 1
|
|
67
65
|
(0, interceptor_1.attachInterceptor)(page);
|
|
68
66
|
let parsedTargetUrl;
|
|
69
67
|
try {
|
|
@@ -84,7 +82,6 @@ async function startCrawler(targetUrl, maxDepth = 2, maxPages = 50) {
|
|
|
84
82
|
if (!current)
|
|
85
83
|
break;
|
|
86
84
|
const { url: currentUrl, depth } = current;
|
|
87
|
-
// Normalize URL (strip pure hash fragments to avoid duplicates)
|
|
88
85
|
let normalizedUrl = currentUrl;
|
|
89
86
|
try {
|
|
90
87
|
const pureUrl = new URL(currentUrl);
|
|
@@ -99,28 +96,24 @@ async function startCrawler(targetUrl, maxDepth = 2, maxPages = 50) {
|
|
|
99
96
|
try {
|
|
100
97
|
await page.goto(normalizedUrl, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
101
98
|
pagesProcessed++;
|
|
102
|
-
// Random delay to avoid aggressive WAF blocks
|
|
103
99
|
await randomDelay(500, 1500);
|
|
104
100
|
if (depth < maxDepth) {
|
|
105
|
-
// Extract all <a> tags and map their absolute URLs
|
|
106
101
|
const links = await page.$$eval('a', anchors => anchors.map(a => a.href));
|
|
107
102
|
for (const link of links) {
|
|
108
103
|
if (!link)
|
|
109
104
|
continue;
|
|
110
105
|
try {
|
|
111
106
|
const parsedLink = new URL(link);
|
|
112
|
-
// Filter out external links strictly based on target domain boundaries
|
|
113
107
|
if (parsedLink.hostname === domain || parsedLink.hostname.endsWith(`.${domain}`)) {
|
|
114
108
|
parsedLink.hash = '';
|
|
115
109
|
const nextUrl = parsedLink.toString();
|
|
116
|
-
// Add new links to queue
|
|
117
110
|
if (!visited.has(nextUrl)) {
|
|
111
|
+
visited.add(nextUrl);
|
|
118
112
|
queue.push({ url: nextUrl, depth: depth + 1 });
|
|
119
113
|
}
|
|
120
114
|
}
|
|
121
115
|
}
|
|
122
116
|
catch (err) {
|
|
123
|
-
// Ignore invalid or weird internal hrefs (`javascript:`, etc)
|
|
124
117
|
}
|
|
125
118
|
}
|
|
126
119
|
}
|
package/dist/db.js
CHANGED
|
@@ -7,11 +7,9 @@ exports.insertEndpoint = insertEndpoint;
|
|
|
7
7
|
exports.getAllEndpoints = getAllEndpoints;
|
|
8
8
|
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
|
-
// Initialize db
|
|
11
10
|
const dbPath = path_1.default.resolve(process.cwd(), 'apigen.db');
|
|
12
11
|
const db = new better_sqlite3_1.default(dbPath);
|
|
13
12
|
db.pragma('journal_mode = WAL');
|
|
14
|
-
// Create table
|
|
15
13
|
db.exec(`
|
|
16
14
|
CREATE TABLE IF NOT EXISTS endpoints (
|
|
17
15
|
id TEXT PRIMARY KEY,
|
package/dist/index.js
CHANGED
|
@@ -66,7 +66,6 @@ program
|
|
|
66
66
|
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
67
67
|
console.log(chalk_1.default.green('Navigation complete. Intercepting API traffic...'));
|
|
68
68
|
console.log(chalk_1.default.gray('Terminal output shows real-time capture. Close the browser window to exit.'));
|
|
69
|
-
// Save cookies safely in a loop to guarantee they are captured before exit
|
|
70
69
|
const sessionFile = path.resolve(process.cwd(), '.apigen-session.json');
|
|
71
70
|
let isRunning = true;
|
|
72
71
|
browser.on('disconnected', () => {
|
|
@@ -74,14 +73,12 @@ program
|
|
|
74
73
|
console.log(chalk_1.default.yellow('\nBrowser closed. Session saved. Exiting apigen gracefully...'));
|
|
75
74
|
process.exit(0);
|
|
76
75
|
});
|
|
77
|
-
// Periodically sync the cookies securely to avoid missing them if context is torn down quickly
|
|
78
76
|
while (isRunning) {
|
|
79
77
|
try {
|
|
80
78
|
const cookies = await context.cookies();
|
|
81
79
|
fs.writeFileSync(sessionFile, JSON.stringify(cookies, null, 2), 'utf-8');
|
|
82
80
|
}
|
|
83
81
|
catch (e) {
|
|
84
|
-
// Might hit target closed error silently during exit
|
|
85
82
|
}
|
|
86
83
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
87
84
|
}
|
|
@@ -103,7 +100,6 @@ program
|
|
|
103
100
|
process.exit(0);
|
|
104
101
|
}
|
|
105
102
|
console.log(chalk_1.default.blue(`Found ${endpoints.length} raw endpoints. Generating schema map...`));
|
|
106
|
-
// Process URLs and inference schemas
|
|
107
103
|
const schemaMap = (0, schema_engine_1.generateSchemaMap)(endpoints);
|
|
108
104
|
const finalMap = Object.values(schemaMap);
|
|
109
105
|
console.log(chalk_1.default.green(`Folded into ${finalMap.length} unique routes.`));
|
package/dist/interceptor.js
CHANGED
|
@@ -7,25 +7,21 @@ exports.attachInterceptor = attachInterceptor;
|
|
|
7
7
|
const crypto_1 = require("crypto");
|
|
8
8
|
const chalk_1 = __importDefault(require("chalk"));
|
|
9
9
|
const db_1 = require("./db");
|
|
10
|
-
const IGNORED_RESOURCE_TYPES = new Set(['image', 'stylesheet', 'font', 'media', 'script', 'document']);
|
|
11
10
|
const TARGET_RESOURCE_TYPES = new Set(['xhr', 'fetch']);
|
|
12
11
|
function attachInterceptor(page) {
|
|
13
12
|
page.on('response', async (response) => {
|
|
14
13
|
const request = response.request();
|
|
15
14
|
const resourceType = request.resourceType();
|
|
16
|
-
// 1. Crucial Filter: ONLY capture traffic if it is XHR, Fetch etc.
|
|
17
15
|
if (!TARGET_RESOURCE_TYPES.has(resourceType)) {
|
|
18
16
|
return;
|
|
19
17
|
}
|
|
20
18
|
const url = request.url();
|
|
21
|
-
// Ignore tracking domains / static extensions
|
|
22
19
|
if (url.includes('google-analytics.com') ||
|
|
23
20
|
url.includes('googletagmanager.com') ||
|
|
24
21
|
url.match(/\.(png|jpg|jpeg|gif|css|woff2?|js|ico|svg)$/i)) {
|
|
25
22
|
return;
|
|
26
23
|
}
|
|
27
24
|
const method = request.method();
|
|
28
|
-
// Ignore preflight requests
|
|
29
25
|
if (method === 'OPTIONS')
|
|
30
26
|
return;
|
|
31
27
|
try {
|
|
@@ -33,17 +29,15 @@ function attachInterceptor(page) {
|
|
|
33
29
|
const headers = request.headers();
|
|
34
30
|
let reqBodyParsed = null;
|
|
35
31
|
let resBodyParsed = null;
|
|
36
|
-
// Parse request post body
|
|
37
32
|
const postData = request.postData();
|
|
38
33
|
if (postData) {
|
|
39
34
|
try {
|
|
40
35
|
reqBodyParsed = JSON.parse(postData);
|
|
41
36
|
}
|
|
42
37
|
catch {
|
|
43
|
-
reqBodyParsed = postData;
|
|
38
|
+
reqBodyParsed = postData;
|
|
44
39
|
}
|
|
45
40
|
}
|
|
46
|
-
// Parse response body (JSON, text)
|
|
47
41
|
const contentType = response.headers()['content-type'] || '';
|
|
48
42
|
if (contentType.includes('application/json') || contentType.includes('text/')) {
|
|
49
43
|
try {
|
|
@@ -63,16 +57,20 @@ function attachInterceptor(page) {
|
|
|
63
57
|
else {
|
|
64
58
|
resBodyParsed = "[Binary or Unsupported Content]";
|
|
65
59
|
}
|
|
66
|
-
|
|
60
|
+
let finalUrl = url;
|
|
61
|
+
if (reqBodyParsed && reqBodyParsed.operationName && finalUrl.includes('/graphql')) {
|
|
62
|
+
const separator = finalUrl.includes('?') ? '&' : '?';
|
|
63
|
+
finalUrl = `${finalUrl}${separator}op=${reqBodyParsed.operationName}`;
|
|
64
|
+
}
|
|
67
65
|
let pathPattern = '/';
|
|
68
66
|
try {
|
|
69
|
-
pathPattern = new URL(
|
|
67
|
+
pathPattern = new URL(finalUrl).pathname;
|
|
70
68
|
}
|
|
71
69
|
catch { }
|
|
72
70
|
const data = {
|
|
73
71
|
id: (0, crypto_1.randomUUID)(),
|
|
74
72
|
method,
|
|
75
|
-
url,
|
|
73
|
+
url: finalUrl,
|
|
76
74
|
path_pattern: pathPattern,
|
|
77
75
|
request_headers: headers,
|
|
78
76
|
request_body: reqBodyParsed,
|
|
@@ -26,7 +26,7 @@ function mapSchemaToOpenAPI(customSchema) {
|
|
|
26
26
|
}
|
|
27
27
|
return { type: 'object', properties };
|
|
28
28
|
}
|
|
29
|
-
return {};
|
|
29
|
+
return {};
|
|
30
30
|
}
|
|
31
31
|
function extractPathParams(path) {
|
|
32
32
|
const matches = path.match(/\{([^}]+)\}/g);
|
|
@@ -77,7 +77,6 @@ function generateOpenAPI(customSchema, baseUrl) {
|
|
|
77
77
|
}
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
|
-
// Default response if empty
|
|
81
80
|
if (Object.keys(operation.responses).length === 0) {
|
|
82
81
|
operation.responses['200'] = { description: 'Success' };
|
|
83
82
|
}
|
package/dist/schema-engine.js
CHANGED
|
@@ -44,7 +44,6 @@ function inferSchema(data) {
|
|
|
44
44
|
if (Array.isArray(data)) {
|
|
45
45
|
if (data.length === 0)
|
|
46
46
|
return ['any'];
|
|
47
|
-
// Infer schema of the object inside the array
|
|
48
47
|
return [inferSchema(data[0])];
|
|
49
48
|
}
|
|
50
49
|
if (typeof data === 'object') {
|
|
@@ -74,7 +73,7 @@ function generateSchemaMap(endpoints) {
|
|
|
74
73
|
try {
|
|
75
74
|
bodyData = JSON.parse(bodyData);
|
|
76
75
|
}
|
|
77
|
-
catch { }
|
|
76
|
+
catch { }
|
|
78
77
|
}
|
|
79
78
|
if (bodyData && typeof bodyData === 'object') {
|
|
80
79
|
const schema = inferSchema(bodyData);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auto-api-discovery",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "API discovery automation CLI",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build": "tsc",
|
|
14
14
|
"dev": "ts-node src/index.ts",
|
|
15
|
+
"test": "tsc --noEmit",
|
|
15
16
|
"prepublishOnly": "npm run build",
|
|
16
|
-
"postinstall": "playwright install"
|
|
17
|
+
"postinstall": "playwright install chromium"
|
|
17
18
|
},
|
|
18
19
|
"keywords": [
|
|
19
20
|
"api",
|
|
@@ -36,4 +37,4 @@
|
|
|
36
37
|
"ts-node": "^10.9.2",
|
|
37
38
|
"typescript": "^5.3.3"
|
|
38
39
|
}
|
|
39
|
-
}
|
|
40
|
+
}
|