apiforge-analyzer 1.1.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.
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ /**
3
+ * Framework detection utilities
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.detectFramework = detectFramework;
7
+ /**
8
+ * Detect which framework is being used
9
+ */
10
+ function detectFramework(app) {
11
+ // Express detection
12
+ if (app && typeof app === 'function') {
13
+ if (app._router || (app.lazyrouter && typeof app.lazyrouter === 'function')) {
14
+ return { name: 'express', confidence: 1.0 };
15
+ }
16
+ }
17
+ // Next.js detection
18
+ try {
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ // Check for Next.js Pages Router
22
+ if (fs.existsSync(path.join(process.cwd(), 'pages'))) {
23
+ const hasApiFolder = fs.existsSync(path.join(process.cwd(), 'pages', 'api'));
24
+ if (hasApiFolder) {
25
+ return { name: 'nextjs-pages', confidence: 0.9 };
26
+ }
27
+ }
28
+ // Check for Next.js App Router
29
+ if (fs.existsSync(path.join(process.cwd(), 'app'))) {
30
+ return { name: 'nextjs-app', confidence: 0.9 };
31
+ }
32
+ // Check for next.config
33
+ if (fs.existsSync(path.join(process.cwd(), 'next.config.js')) ||
34
+ fs.existsSync(path.join(process.cwd(), 'next.config.mjs'))) {
35
+ return { name: 'nextjs-pages', confidence: 0.7 };
36
+ }
37
+ }
38
+ catch (e) {
39
+ // Ignore fs errors
40
+ }
41
+ // Vite detection
42
+ try {
43
+ require.resolve('vite');
44
+ return { name: 'vite', confidence: 0.8 };
45
+ }
46
+ catch (e) {
47
+ // Vite not installed
48
+ }
49
+ // React Router detection
50
+ try {
51
+ require.resolve('react-router-dom');
52
+ return { name: 'react-router', confidence: 0.7 };
53
+ }
54
+ catch (e) {
55
+ // React Router not installed
56
+ }
57
+ // Node HTTP detection
58
+ try {
59
+ const http = require('http');
60
+ if (http) {
61
+ return { name: 'node-http', confidence: 0.5 };
62
+ }
63
+ }
64
+ catch (e) {
65
+ // HTTP module not available
66
+ }
67
+ return { name: 'unknown', confidence: 0 };
68
+ }
@@ -0,0 +1,321 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.exportToJSON = exportToJSON;
37
+ exports.enrichRoutes = enrichRoutes;
38
+ exports.enrichRoute = enrichRoute;
39
+ const fs = __importStar(require("fs"));
40
+ const path = __importStar(require("path"));
41
+ /**
42
+ * Export routes to JSON file
43
+ */
44
+ async function exportToJSON(routes, projectName, filePath, baseUrl) {
45
+ const enrichedRoutes = enrichRoutes(routes);
46
+ const exportData = {
47
+ projectName,
48
+ timestamp: new Date().toISOString(),
49
+ schemaVersion: 'apiforge.route-export.v2',
50
+ framework: enrichedRoutes[0]?.framework || 'unknown',
51
+ baseUrl,
52
+ routes: enrichedRoutes,
53
+ metadata: {
54
+ totalRoutes: enrichedRoutes.length,
55
+ tags: Array.from(new Set(enrichedRoutes.map(r => r.tag))),
56
+ extractedAt: new Date().toISOString(),
57
+ routeIds: enrichedRoutes.map(r => ({ id: r.id, routeKey: r.routeKey, method: r.method, path: r.path })),
58
+ },
59
+ };
60
+ const fullPath = path.join(process.cwd(), filePath);
61
+ try {
62
+ fs.writeFileSync(fullPath, JSON.stringify(exportData, null, 2), 'utf-8');
63
+ console.log(`💾 Saved API export to: ${fullPath}`);
64
+ }
65
+ catch (error) {
66
+ console.error('Failed to save JSON:', error.message);
67
+ throw error;
68
+ }
69
+ }
70
+ function enrichRoutes(routes) {
71
+ const withIds = routes.map((route, index) => enrichRoute(route, index + 1));
72
+ return withIds.map(route => ({
73
+ ...route,
74
+ linkedPaths: route.linkedPaths?.length ? route.linkedPaths : inferLinkedPaths(route, withIds),
75
+ }));
76
+ }
77
+ function enrichRoute(route, index) {
78
+ const id = route.id || index;
79
+ const routeKey = route.routeKey || `api-${String(id).padStart(3, '0')}`;
80
+ const pathParams = unique([...(route.pathParams || []), ...extractPathParams(route.path)]);
81
+ const queryParams = unique(route.queryParams || []);
82
+ const requestBodyFields = unique(route.requestBodyFields || []);
83
+ const headerParams = unique(route.headerParams || []);
84
+ const tag = route.tag || inferTag(route.path);
85
+ const parameters = buildParameters(route.parameters, pathParams, queryParams, headerParams);
86
+ const requestBody = route.requestBody || buildRequestBody(route.method, requestBodyFields);
87
+ const responses = route.responses || buildResponses(route.method, route.path, tag, route.responseStatuses);
88
+ const authRequired = Boolean(route.authRequired || route.hasAuth || detectAuth(route.middleware || []));
89
+ return {
90
+ ...route,
91
+ id,
92
+ routeKey,
93
+ tag,
94
+ tags: route.tags?.length ? route.tags : [tag],
95
+ summary: route.summary || generateSummary(route.method, route.path),
96
+ description: route.description || generateDescription(route.method, route.path, tag),
97
+ authRequired,
98
+ hasAuth: authRequired,
99
+ pathParams,
100
+ queryParams,
101
+ requestBodyFields,
102
+ headerParams,
103
+ parameters,
104
+ requestBody,
105
+ responses,
106
+ sourceContext: {
107
+ framework: route.framework,
108
+ routeKey,
109
+ detectedFrom: route.framework,
110
+ confidence: confidenceFor(route, parameters, requestBody, responses),
111
+ middleware: route.middleware || [],
112
+ authRequired,
113
+ pathParams,
114
+ queryParams,
115
+ requestBodyFields,
116
+ headerParams,
117
+ responseStatuses: route.responseStatuses || [],
118
+ notes: buildNotes(route, requestBodyFields, queryParams, headerParams),
119
+ },
120
+ };
121
+ }
122
+ function buildParameters(existing, pathParams, queryParams, headerParams = []) {
123
+ const params = new Map();
124
+ (existing || []).forEach(param => params.set(`${param.in}:${param.name}`, param));
125
+ pathParams.forEach(name => params.set(`path:${name}`, {
126
+ name,
127
+ in: 'path',
128
+ required: true,
129
+ type: 'string',
130
+ schema: { type: 'string' },
131
+ description: `Required path parameter: ${name}.`,
132
+ }));
133
+ queryParams.forEach(name => {
134
+ const key = `query:${name}`;
135
+ if (!params.has(key))
136
+ params.set(key, {
137
+ name,
138
+ in: 'query',
139
+ required: false,
140
+ type: 'string',
141
+ schema: { type: 'string' },
142
+ description: `Optional query parameter: ${name}.`,
143
+ });
144
+ });
145
+ headerParams.forEach(name => {
146
+ const key = `header:${name}`;
147
+ if (!params.has(key))
148
+ params.set(key, {
149
+ name,
150
+ in: 'header',
151
+ required: name.toLowerCase() === 'authorization',
152
+ type: 'string',
153
+ schema: { type: 'string' },
154
+ description: `Request header: ${name}.`,
155
+ });
156
+ });
157
+ return Array.from(params.values());
158
+ }
159
+ function buildRequestBody(method, fields) {
160
+ if (!['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()) || !fields.length)
161
+ return undefined;
162
+ const properties = {};
163
+ fields.forEach(field => {
164
+ properties[field] = {
165
+ type: inferFieldType(field),
166
+ description: `Detected request body field: ${field}.`,
167
+ example: exampleForField(field),
168
+ };
169
+ });
170
+ return {
171
+ required: true,
172
+ content: {
173
+ 'application/json': {
174
+ schema: {
175
+ type: 'object',
176
+ required: fields,
177
+ properties,
178
+ },
179
+ },
180
+ },
181
+ };
182
+ }
183
+ function buildResponses(method, path, tag, detectedStatuses) {
184
+ // Prefer statuses seen in the source; fall back to method-based inference
185
+ const m = method.toUpperCase();
186
+ const inferredPrimary = m === 'POST' ? 201 : m === 'DELETE' ? 204 : 200;
187
+ const primaryStatus = detectedStatuses?.find(s => s >= 200 && s < 300) ?? inferredPrimary;
188
+ const status = String(primaryStatus);
189
+ const successBody = status === '204' ? undefined : {
190
+ 'application/json': {
191
+ schema: {
192
+ type: 'object',
193
+ properties: {
194
+ success: { type: 'boolean', example: true },
195
+ endpoint: { type: 'string', example: `${m} ${path}` },
196
+ resource: { type: 'string', example: tag.toLowerCase() },
197
+ },
198
+ },
199
+ },
200
+ };
201
+ const base = {
202
+ [status]: {
203
+ description: status === '204' ? 'Deleted successfully' : 'Successful response',
204
+ content: successBody,
205
+ },
206
+ };
207
+ (detectedStatuses || []).forEach(s => {
208
+ if (s >= 200 && s < 300)
209
+ return; // already covered
210
+ base[String(s)] = { description: statusDescription(s) };
211
+ });
212
+ // Always include standard errors if not already present
213
+ if (!base['400'])
214
+ base['400'] = { description: 'Invalid request input' };
215
+ if (!base['401'])
216
+ base['401'] = { description: 'Authentication required or invalid credentials' };
217
+ if (!base['500'])
218
+ base['500'] = { description: 'Unexpected server error' };
219
+ return base;
220
+ }
221
+ function statusDescription(code) {
222
+ const map = {
223
+ 400: 'Invalid request input',
224
+ 401: 'Authentication required or invalid credentials',
225
+ 403: 'Forbidden — insufficient permissions',
226
+ 404: 'Resource not found',
227
+ 409: 'Conflict — resource already exists',
228
+ 422: 'Unprocessable entity',
229
+ 429: 'Too many requests — rate limit exceeded',
230
+ 500: 'Unexpected server error',
231
+ 503: 'Service unavailable',
232
+ };
233
+ return map[code] || `HTTP ${code}`;
234
+ }
235
+ function inferLinkedPaths(route, routes) {
236
+ return routes
237
+ .filter(other => other.id !== route.id)
238
+ .filter(other => other.tag === route.tag || shareResource(route.path, other.path))
239
+ .slice(0, 5)
240
+ .map(other => `${other.method.toUpperCase()} ${other.path}`);
241
+ }
242
+ function extractPathParams(routePath) {
243
+ return unique([...(routePath.matchAll(/:(\w+)/g))].map(match => match[1]));
244
+ }
245
+ function detectAuth(middleware) {
246
+ const authNames = ['authenticateToken', 'authenticate', 'verifyToken', 'requireAuth', 'auth', 'protect', 'isAuth', 'checkAuth', 'jwtAuth', 'bearerAuth'];
247
+ return middleware.some(name => authNames.some(authName => name.toLowerCase().includes(authName.toLowerCase())));
248
+ }
249
+ function inferTag(routePath) {
250
+ const skip = new Set(['api', 'v1', 'v2', 'v3']);
251
+ return routePath.split('/').filter(Boolean).find(part => !skip.has(part) && !part.startsWith(':')) || 'general';
252
+ }
253
+ function generateSummary(method, routePath) {
254
+ const resource = routePath.split('/').filter(Boolean).filter(part => !part.startsWith(':')).pop() || 'resource';
255
+ const action = {
256
+ GET: `Get ${resource}`,
257
+ POST: `Create ${resource}`,
258
+ PUT: `Replace ${resource}`,
259
+ PATCH: `Update ${resource}`,
260
+ DELETE: `Delete ${resource}`,
261
+ };
262
+ return action[method.toUpperCase()] || `${method.toUpperCase()} ${routePath}`;
263
+ }
264
+ function generateDescription(method, routePath, tag) {
265
+ return `${method.toUpperCase()} ${routePath} in the ${tag} API group. Generated from runtime route metadata, middleware names, path parameters, query reads, and request body reads.`;
266
+ }
267
+ function shareResource(a, b) {
268
+ const clean = (value) => value.split('/').filter(Boolean).filter(part => !part.startsWith(':') && !['api', 'v1', 'v2', 'v3'].includes(part));
269
+ return clean(a).some(part => clean(b).includes(part));
270
+ }
271
+ function confidenceFor(route, parameters, requestBody, responses) {
272
+ let score = 0.55;
273
+ if (parameters.length)
274
+ score += 0.15;
275
+ if (requestBody)
276
+ score += 0.15;
277
+ if (responses)
278
+ score += 0.1;
279
+ if (route.middleware?.length)
280
+ score += 0.05;
281
+ return Math.min(score, 0.95);
282
+ }
283
+ function buildNotes(route, bodyFields, queryParams, headerParams = []) {
284
+ const notes = [];
285
+ if (bodyFields.length)
286
+ notes.push(`Detected request body fields: ${bodyFields.join(', ')}.`);
287
+ if (queryParams.length)
288
+ notes.push(`Detected query parameters: ${queryParams.join(', ')}.`);
289
+ if (headerParams.length)
290
+ notes.push(`Detected request headers: ${headerParams.join(', ')}.`);
291
+ if (route.responseStatuses?.length)
292
+ notes.push(`Detected response status codes: ${route.responseStatuses.join(', ')}.`);
293
+ if (route.middleware?.length)
294
+ notes.push(`Middleware chain: ${route.middleware.join(', ')}.`);
295
+ return notes;
296
+ }
297
+ function inferFieldType(name) {
298
+ if (/^(is|has|can|should)[A-Z_]/.test(name) || /active|enabled|verified/i.test(name))
299
+ return 'boolean';
300
+ if (/count|total|amount|price|limit|offset|page|age|qty|quantity/i.test(name))
301
+ return 'number';
302
+ return 'string';
303
+ }
304
+ function exampleForField(name) {
305
+ if (/email/i.test(name))
306
+ return 'user@example.com';
307
+ if (/password/i.test(name))
308
+ return 'SecurePass123!';
309
+ if (/name/i.test(name))
310
+ return 'Jane Developer';
311
+ if (/id$/i.test(name))
312
+ return 'id_123';
313
+ if (/count|total|amount|price|limit|offset|page|age|qty|quantity/i.test(name))
314
+ return 10;
315
+ if (/active|enabled|verified/i.test(name))
316
+ return true;
317
+ return `example_${name}`;
318
+ }
319
+ function unique(items) {
320
+ return Array.from(new Set(items.filter(Boolean)));
321
+ }
@@ -0,0 +1,245 @@
1
+ "use strict";
2
+ /**
3
+ * Express analyzer — accurate route + payload extraction
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.analyzeExpress = analyzeExpress;
7
+ // ─── Path normalisation ───────────────────────────────────────────────────────
8
+ function normalizePath(raw) {
9
+ return raw
10
+ .replace(/\\\//g, '/')
11
+ .replace(/\(\?[^)]*\)/g, '')
12
+ .replace(/\([^)]*\)/g, '')
13
+ .replace(/\\\w/g, '')
14
+ .replace(/[\^\$\?\\]/g, '')
15
+ .replace(/\/+/g, '/')
16
+ .replace(/\/$/, '')
17
+ || '/';
18
+ }
19
+ function tagFromPath(path) {
20
+ const skip = new Set(['api', 'v1', 'v2', 'v3', 'v4']);
21
+ const meaningful = path
22
+ .split('/')
23
+ .filter(Boolean)
24
+ .find(s => !skip.has(s) && !s.startsWith(':'));
25
+ return meaningful || 'general';
26
+ }
27
+ function generateSummary(method, path) {
28
+ const parts = path.split('/').filter(s => s && !s.startsWith(':'));
29
+ const action = parts[parts.length - 1] || '';
30
+ const resource = parts[parts.length - 2] || '';
31
+ const map = {
32
+ GET: action ? `Get ${action}` : `List ${resource}`,
33
+ POST: action ? `Create ${action}` : `Create ${resource}`,
34
+ PUT: action ? `Update ${action}` : `Update ${resource}`,
35
+ PATCH: action ? `Patch ${action}` : `Patch ${resource}`,
36
+ DELETE: action ? `Delete ${action}` : `Delete ${resource}`,
37
+ };
38
+ const base = map[method] || `${method} ${path}`;
39
+ return (base.charAt(0).toUpperCase() + base.slice(1)).replace(/[-_]/g, ' ');
40
+ }
41
+ // ─── Main analyser ────────────────────────────────────────────────────────────
42
+ function analyzeExpress(app) {
43
+ const routes = [];
44
+ const seen = new Set();
45
+ if (typeof app.lazyrouter === 'function')
46
+ app.lazyrouter();
47
+ const stack = app._router?.stack ??
48
+ app.router?.stack ??
49
+ app.layerStack ??
50
+ app.stack ??
51
+ null;
52
+ if (!stack?.length) {
53
+ console.warn('⚠️ No routes found — ensure routes are registered before analyze()');
54
+ return routes;
55
+ }
56
+ for (const layer of stack) {
57
+ if (layer.route) {
58
+ const r = buildRoute(layer.route, '');
59
+ if (r && !seen.has(r.method + r.path)) {
60
+ routes.push(r);
61
+ seen.add(r.method + r.path);
62
+ }
63
+ }
64
+ else if (layer.handle?.stack) {
65
+ let base = '';
66
+ if (layer.regexp?.source)
67
+ base = normalizePath(layer.regexp.source);
68
+ else if (layer.path)
69
+ base = normalizePath(layer.path);
70
+ else if (typeof layer.route === 'string')
71
+ base = normalizePath(layer.route);
72
+ for (const sub of layer.handle.stack) {
73
+ if (sub.route) {
74
+ const r = buildRoute(sub.route, base);
75
+ if (r && !seen.has(r.method + r.path)) {
76
+ routes.push(r);
77
+ seen.add(r.method + r.path);
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
83
+ return routes;
84
+ }
85
+ // ─── Route builder ────────────────────────────────────────────────────────────
86
+ function buildRoute(route, basePath) {
87
+ if (!route.path)
88
+ return null;
89
+ const methods = Object.keys(route.methods || {})
90
+ .filter(m => route.methods[m])
91
+ .map(m => m.toUpperCase());
92
+ if (!methods.length)
93
+ return null;
94
+ const cleanBase = normalizePath(basePath);
95
+ const cleanRoute = normalizePath(route.path);
96
+ let fullPath = (cleanBase === '/' ? '' : cleanBase) + cleanRoute;
97
+ if (!fullPath.startsWith('/'))
98
+ fullPath = '/' + fullPath;
99
+ if (fullPath !== '/' && fullPath.endsWith('/'))
100
+ fullPath = fullPath.slice(0, -1);
101
+ const stack = route.stack || [];
102
+ // ── Middleware names ───────────────────────────────────────────────────────
103
+ const middleware = stack
104
+ .map((l) => l.name)
105
+ .filter((n) => n && n !== '<anonymous>' && n !== 'bound dispatch' && n !== 'bound ');
106
+ // ── Join all handler source text ───────────────────────────────────────────
107
+ const src = stack.map((l) => l.handle?.toString() ?? '').join('\n');
108
+ // ── Path params (from route pattern) ──────────────────────────────────────
109
+ const pathParams = unique([...fullPath.matchAll(/:(\w+)/g)].map(m => m[1]));
110
+ // ── Query params ───────────────────────────────────────────────────────────
111
+ const queryParams = unique([
112
+ // req.query.foo
113
+ ...matchAll(src, /req\.query\.([a-zA-Z0-9_$]+)/g),
114
+ // { foo, bar } = req.query / { foo: renamed } = req.query
115
+ ...destructured(src, /(?:const|let|var)\s+\{([^}]+)\}\s*=\s*req\.query/g),
116
+ // req.query['foo']
117
+ ...matchAll(src, /req\.query\[['"]([a-zA-Z0-9_$]+)['"]\]/g),
118
+ ]);
119
+ // ── Request body fields ────────────────────────────────────────────────────
120
+ const requestBodyFields = unique([
121
+ // req.body.foo
122
+ ...matchAll(src, /req\.body\.([a-zA-Z0-9_$]+)/g),
123
+ // { foo, bar: alias, baz = default } = req.body
124
+ ...destructured(src, /(?:const|let|var)\s+\{([^}]+)\}\s*=\s*req\.body/g),
125
+ // req.body['foo']
126
+ ...matchAll(src, /req\.body\[['"]([a-zA-Z0-9_$]+)['"]\]/g),
127
+ // body.foo (when `const body = req.body`)
128
+ ...bodyAliasFields(src),
129
+ // Zod: z.object({ field: z.string() }).parse(req.body)
130
+ ...zodFields(src),
131
+ // Joi: Joi.object({ field: Joi.string() }).validate(req.body)
132
+ ...joiFields(src),
133
+ ]);
134
+ // ── Header params ──────────────────────────────────────────────────────────
135
+ const headerParams = unique([
136
+ ...matchAll(src, /req\.headers\[['"]([a-zA-Z0-9_$-]+)['"]\]/g),
137
+ ...matchAll(src, /req\.get\s*\(\s*['"]([a-zA-Z0-9_$-]+)['"]\s*\)/g),
138
+ // req.headers.authorization, req.headers['x-api-key'], etc.
139
+ ...matchAll(src, /req\.headers\.([a-zA-Z0-9_$]+)/g),
140
+ ]);
141
+ // ── Response status codes from `res.status(nnn)` ──────────────────────────
142
+ const responseStatuses = unique([...src.matchAll(/res\.status\s*\(\s*(\d{3})\s*\)/g)].map(m => parseInt(m[1], 10)));
143
+ // ── Auth detection ─────────────────────────────────────────────────────────
144
+ const AUTH_NAMES = new Set([
145
+ 'authenticateToken', 'authenticate', 'verifyToken', 'requireAuth', 'auth',
146
+ 'protect', 'isAuth', 'checkAuth', 'jwtAuth', 'bearerAuth', 'isAuthenticated',
147
+ 'requireLogin', 'ensureAuth', 'verifyJWT', 'checkToken', 'authMiddleware',
148
+ 'passport.authenticate', 'jwt', 'bearer',
149
+ ]);
150
+ const hasAuth = middleware.some(m => AUTH_NAMES.has(m) || [...AUTH_NAMES].some(a => m.toLowerCase().includes(a.toLowerCase()))) ||
151
+ /req\.user|req\.auth|verifyToken|jwt\.verify|passport\.authenticate/.test(src);
152
+ const method = methods[0];
153
+ const tag = tagFromPath(fullPath);
154
+ const summary = generateSummary(method, fullPath);
155
+ const operationId = `${method.toLowerCase()}_${fullPath
156
+ .replace(/[^a-zA-Z0-9]/g, '_')
157
+ .replace(/_+/g, '_')
158
+ .replace(/^_|_$/g, '')}`;
159
+ return {
160
+ method,
161
+ path: fullPath,
162
+ tag,
163
+ operationId,
164
+ summary,
165
+ middleware,
166
+ authRequired: hasAuth,
167
+ pathParams,
168
+ queryParams,
169
+ requestBodyFields,
170
+ linkedPaths: [],
171
+ hasAuth,
172
+ framework: 'express',
173
+ responseStatuses: responseStatuses.length ? responseStatuses : undefined,
174
+ headerParams: headerParams.length ? headerParams : undefined,
175
+ };
176
+ }
177
+ // ─── Extraction helpers ───────────────────────────────────────────────────────
178
+ /** Run a global regex and return capture group 1 for every match. */
179
+ function matchAll(src, re) {
180
+ return [...src.matchAll(re)].map(m => m[1]).filter(Boolean);
181
+ }
182
+ /**
183
+ * Extract field names from all destructuring patterns like:
184
+ * const { a, b: alias, c = default, d: alias2 = default } = req.body
185
+ * Runs ALL occurrences of the given regex, not just the first.
186
+ */
187
+ function destructured(src, re) {
188
+ const results = [];
189
+ for (const match of src.matchAll(re)) {
190
+ match[1]
191
+ .split(',')
192
+ .map(s => s.trim())
193
+ // handle "alias: original" — we want the local binding (left side)
194
+ .map(s => s.split(':')[0].trim())
195
+ // strip default value
196
+ .map(s => s.split('=')[0].trim())
197
+ .filter(Boolean)
198
+ .forEach(f => results.push(f));
199
+ }
200
+ return results;
201
+ }
202
+ /**
203
+ * When a developer writes `const body = req.body` and then accesses `body.foo`.
204
+ */
205
+ function bodyAliasFields(src) {
206
+ const aliasMatch = src.match(/(?:const|let|var)\s+(\w+)\s*=\s*req\.body\b/);
207
+ if (!aliasMatch)
208
+ return [];
209
+ const alias = aliasMatch[1];
210
+ if (alias === 'body')
211
+ return matchAll(src, /body\.([a-zA-Z0-9_$]+)/g);
212
+ const re = new RegExp(`\\b${alias}\\.([a-zA-Z0-9_$]+)`, 'g');
213
+ return matchAll(src, re);
214
+ }
215
+ /** Extract field names from Zod object schemas. */
216
+ function zodFields(src) {
217
+ const results = [];
218
+ // z.object({ field: z.xxx(), ... }).parse(req.body) or .safeParse
219
+ const zodRe = /z\.object\s*\(\s*\{([^}]+)\}/g;
220
+ for (const match of src.matchAll(zodRe)) {
221
+ match[1]
222
+ .split(',')
223
+ .map(s => s.trim().split(':')[0].trim())
224
+ .filter(s => /^[a-zA-Z_$]/.test(s))
225
+ .forEach(f => results.push(f));
226
+ }
227
+ return results;
228
+ }
229
+ /** Extract field names from Joi object schemas. */
230
+ function joiFields(src) {
231
+ const results = [];
232
+ // Joi.object({ field: Joi.xxx() }) or yup.object().shape({ field: ... })
233
+ const joiRe = /(?:Joi|yup)\.object\s*\(?\s*(?:\.shape\s*\()?\s*\{([^}]+)\}/g;
234
+ for (const match of src.matchAll(joiRe)) {
235
+ match[1]
236
+ .split(',')
237
+ .map(s => s.trim().split(':')[0].trim())
238
+ .filter(s => /^[a-zA-Z_$]/.test(s))
239
+ .forEach(f => results.push(f));
240
+ }
241
+ return results;
242
+ }
243
+ function unique(items) {
244
+ return Array.from(new Set(items.filter(Boolean)));
245
+ }