@apiposture/cli 1.0.1

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 (110) hide show
  1. package/.apiposture.json.example +56 -0
  2. package/.github/workflows/publish.yml +38 -0
  3. package/.github/workflows/test.yml +42 -0
  4. package/LICENSE +21 -0
  5. package/README.md +156 -0
  6. package/dist/cli/commands/license/activate.d.ts +3 -0
  7. package/dist/cli/commands/license/activate.js +35 -0
  8. package/dist/cli/commands/license/deactivate.d.ts +3 -0
  9. package/dist/cli/commands/license/deactivate.js +28 -0
  10. package/dist/cli/commands/license/status.d.ts +3 -0
  11. package/dist/cli/commands/license/status.js +36 -0
  12. package/dist/cli/commands/scan.d.ts +3 -0
  13. package/dist/cli/commands/scan.js +211 -0
  14. package/dist/cli/options.d.ts +27 -0
  15. package/dist/cli/options.js +30 -0
  16. package/dist/core/analysis/project-analyzer.d.ts +16 -0
  17. package/dist/core/analysis/project-analyzer.js +54 -0
  18. package/dist/core/analysis/source-file-loader.d.ts +32 -0
  19. package/dist/core/analysis/source-file-loader.js +155 -0
  20. package/dist/core/authorization/authorization-extractor.d.ts +11 -0
  21. package/dist/core/authorization/authorization-extractor.js +2 -0
  22. package/dist/core/authorization/express-auth-extractor.d.ts +10 -0
  23. package/dist/core/authorization/express-auth-extractor.js +106 -0
  24. package/dist/core/authorization/global-auth-analyzer.d.ts +12 -0
  25. package/dist/core/authorization/global-auth-analyzer.js +74 -0
  26. package/dist/core/authorization/nestjs-auth-extractor.d.ts +13 -0
  27. package/dist/core/authorization/nestjs-auth-extractor.js +142 -0
  28. package/dist/core/configuration/config-loader.d.ts +27 -0
  29. package/dist/core/configuration/config-loader.js +72 -0
  30. package/dist/core/configuration/suppression-matcher.d.ts +14 -0
  31. package/dist/core/configuration/suppression-matcher.js +79 -0
  32. package/dist/core/discovery/discoverer-interface.d.ts +7 -0
  33. package/dist/core/discovery/discoverer-interface.js +2 -0
  34. package/dist/core/discovery/express-discoverer.d.ts +20 -0
  35. package/dist/core/discovery/express-discoverer.js +223 -0
  36. package/dist/core/discovery/fastify-discoverer.d.ts +19 -0
  37. package/dist/core/discovery/fastify-discoverer.js +249 -0
  38. package/dist/core/discovery/framework-detector.d.ts +9 -0
  39. package/dist/core/discovery/framework-detector.js +61 -0
  40. package/dist/core/discovery/index.d.ts +8 -0
  41. package/dist/core/discovery/index.js +8 -0
  42. package/dist/core/discovery/koa-discoverer.d.ts +16 -0
  43. package/dist/core/discovery/koa-discoverer.js +151 -0
  44. package/dist/core/discovery/nestjs-discoverer.d.ts +16 -0
  45. package/dist/core/discovery/nestjs-discoverer.js +180 -0
  46. package/dist/core/discovery/route-group-registry.d.ts +18 -0
  47. package/dist/core/discovery/route-group-registry.js +50 -0
  48. package/dist/core/licensing/license-context.d.ts +17 -0
  49. package/dist/core/licensing/license-context.js +15 -0
  50. package/dist/core/licensing/license-features.d.ts +14 -0
  51. package/dist/core/licensing/license-features.js +47 -0
  52. package/dist/core/models/authorization-info.d.ts +13 -0
  53. package/dist/core/models/authorization-info.js +25 -0
  54. package/dist/core/models/endpoint-type.d.ts +8 -0
  55. package/dist/core/models/endpoint-type.js +12 -0
  56. package/dist/core/models/endpoint.d.ts +16 -0
  57. package/dist/core/models/endpoint.js +16 -0
  58. package/dist/core/models/finding.d.ts +19 -0
  59. package/dist/core/models/finding.js +8 -0
  60. package/dist/core/models/http-method.d.ts +14 -0
  61. package/dist/core/models/http-method.js +25 -0
  62. package/dist/core/models/index.d.ts +10 -0
  63. package/dist/core/models/index.js +10 -0
  64. package/dist/core/models/scan-result.d.ts +21 -0
  65. package/dist/core/models/scan-result.js +35 -0
  66. package/dist/core/models/security-classification.d.ts +8 -0
  67. package/dist/core/models/security-classification.js +12 -0
  68. package/dist/core/models/severity.d.ts +11 -0
  69. package/dist/core/models/severity.js +23 -0
  70. package/dist/core/models/source-location.d.ts +7 -0
  71. package/dist/core/models/source-location.js +4 -0
  72. package/dist/index.d.ts +3 -0
  73. package/dist/index.js +23 -0
  74. package/dist/licensing/license-manager.d.ts +38 -0
  75. package/dist/licensing/license-manager.js +184 -0
  76. package/dist/output/accessibility-helper.d.ts +22 -0
  77. package/dist/output/accessibility-helper.js +98 -0
  78. package/dist/output/formatter-interface.d.ts +11 -0
  79. package/dist/output/formatter-interface.js +2 -0
  80. package/dist/output/index.d.ts +6 -0
  81. package/dist/output/index.js +6 -0
  82. package/dist/output/json-formatter.d.ts +7 -0
  83. package/dist/output/json-formatter.js +72 -0
  84. package/dist/output/markdown-formatter.d.ts +10 -0
  85. package/dist/output/markdown-formatter.js +114 -0
  86. package/dist/output/terminal-formatter.d.ts +12 -0
  87. package/dist/output/terminal-formatter.js +82 -0
  88. package/dist/rules/consistency/controller-action-conflict.d.ts +19 -0
  89. package/dist/rules/consistency/controller-action-conflict.js +40 -0
  90. package/dist/rules/consistency/missing-auth-on-writes.d.ts +21 -0
  91. package/dist/rules/consistency/missing-auth-on-writes.js +59 -0
  92. package/dist/rules/exposure/allow-anonymous-on-write.d.ts +20 -0
  93. package/dist/rules/exposure/allow-anonymous-on-write.js +42 -0
  94. package/dist/rules/exposure/public-without-explicit-intent.d.ts +20 -0
  95. package/dist/rules/exposure/public-without-explicit-intent.js +58 -0
  96. package/dist/rules/index.d.ts +11 -0
  97. package/dist/rules/index.js +11 -0
  98. package/dist/rules/privilege/excessive-role-access.d.ts +20 -0
  99. package/dist/rules/privilege/excessive-role-access.js +36 -0
  100. package/dist/rules/privilege/weak-role-naming.d.ts +20 -0
  101. package/dist/rules/privilege/weak-role-naming.js +50 -0
  102. package/dist/rules/rule-engine.d.ts +15 -0
  103. package/dist/rules/rule-engine.js +52 -0
  104. package/dist/rules/rule-interface.d.ts +16 -0
  105. package/dist/rules/rule-interface.js +2 -0
  106. package/dist/rules/surface/sensitive-route-keywords.d.ts +20 -0
  107. package/dist/rules/surface/sensitive-route-keywords.js +63 -0
  108. package/dist/rules/surface/unprotected-endpoint.d.ts +20 -0
  109. package/dist/rules/surface/unprotected-endpoint.js +61 -0
  110. package/package.json +60 -0
@@ -0,0 +1,27 @@
1
+ import { RuleConfig } from '../../rules/rule-interface.js';
2
+ export interface ApiPostureConfig {
3
+ rules?: Record<string, RuleConfig>;
4
+ suppressions?: SuppressionConfig[];
5
+ output?: {
6
+ format?: 'terminal' | 'json' | 'markdown';
7
+ noColor?: boolean;
8
+ noIcons?: boolean;
9
+ };
10
+ scan?: {
11
+ excludePatterns?: string[];
12
+ includePatterns?: string[];
13
+ };
14
+ }
15
+ export interface SuppressionConfig {
16
+ ruleId?: string;
17
+ route?: string;
18
+ routePattern?: string;
19
+ method?: string;
20
+ reason: string;
21
+ }
22
+ export declare class ConfigLoader {
23
+ load(configPath?: string): Promise<ApiPostureConfig>;
24
+ private findConfigFile;
25
+ private validateConfig;
26
+ }
27
+ //# sourceMappingURL=config-loader.d.ts.map
@@ -0,0 +1,72 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { parseSeverity } from '../models/severity.js';
4
+ const CONFIG_FILE_NAMES = [
5
+ '.apiposture.json',
6
+ 'apiposture.json',
7
+ '.apiposture.config.json',
8
+ ];
9
+ export class ConfigLoader {
10
+ async load(configPath) {
11
+ let filePath = configPath ?? null;
12
+ // If no path specified, search for config file
13
+ if (!filePath) {
14
+ filePath = this.findConfigFile(process.cwd());
15
+ }
16
+ if (!filePath) {
17
+ return {};
18
+ }
19
+ try {
20
+ const content = fs.readFileSync(filePath, 'utf-8');
21
+ const config = JSON.parse(content);
22
+ return this.validateConfig(config);
23
+ }
24
+ catch (error) {
25
+ if (configPath) {
26
+ // Only throw if user explicitly specified a config path
27
+ throw new Error(`Failed to load config from ${filePath}: ${error}`);
28
+ }
29
+ return {};
30
+ }
31
+ }
32
+ findConfigFile(startDir) {
33
+ let currentDir = startDir;
34
+ let parentDir = path.dirname(currentDir);
35
+ while (currentDir !== parentDir) {
36
+ for (const fileName of CONFIG_FILE_NAMES) {
37
+ const filePath = path.join(currentDir, fileName);
38
+ if (fs.existsSync(filePath)) {
39
+ return filePath;
40
+ }
41
+ }
42
+ currentDir = parentDir;
43
+ parentDir = path.dirname(currentDir);
44
+ }
45
+ return null;
46
+ }
47
+ validateConfig(config) {
48
+ // Validate rules config
49
+ if (config.rules) {
50
+ for (const [ruleId, ruleConfig] of Object.entries(config.rules)) {
51
+ if (ruleConfig.severity) {
52
+ const parsed = parseSeverity(ruleConfig.severity);
53
+ if (!parsed) {
54
+ console.warn(`Invalid severity "${ruleConfig.severity}" for rule ${ruleId}`);
55
+ }
56
+ }
57
+ }
58
+ }
59
+ // Validate suppressions
60
+ if (config.suppressions) {
61
+ config.suppressions = config.suppressions.filter((s) => {
62
+ if (!s.reason) {
63
+ console.warn('Suppression missing required "reason" field, skipping');
64
+ return false;
65
+ }
66
+ return true;
67
+ });
68
+ }
69
+ return config;
70
+ }
71
+ }
72
+ //# sourceMappingURL=config-loader.js.map
@@ -0,0 +1,14 @@
1
+ import { Finding } from '../models/finding.js';
2
+ import { Endpoint } from '../models/endpoint.js';
3
+ import { SuppressionConfig } from './config-loader.js';
4
+ export declare class SuppressionMatcher {
5
+ private suppressions;
6
+ constructor(suppressions?: SuppressionConfig[]);
7
+ applySuppressionsToFindings(findings: Finding[]): Finding[];
8
+ isEndpointSuppressed(endpoint: Endpoint, ruleId: string): SuppressionConfig | null;
9
+ private findMatchingSuppression;
10
+ private matchesFinding;
11
+ private matchesEndpoint;
12
+ private matchesRoutePattern;
13
+ }
14
+ //# sourceMappingURL=suppression-matcher.d.ts.map
@@ -0,0 +1,79 @@
1
+ export class SuppressionMatcher {
2
+ suppressions;
3
+ constructor(suppressions = []) {
4
+ this.suppressions = suppressions;
5
+ }
6
+ applySuppressionsToFindings(findings) {
7
+ return findings.map((finding) => {
8
+ const suppression = this.findMatchingSuppression(finding);
9
+ if (suppression) {
10
+ return {
11
+ ...finding,
12
+ suppressed: true,
13
+ suppressionReason: suppression.reason,
14
+ };
15
+ }
16
+ return finding;
17
+ });
18
+ }
19
+ isEndpointSuppressed(endpoint, ruleId) {
20
+ for (const suppression of this.suppressions) {
21
+ if (this.matchesEndpoint(suppression, endpoint, ruleId)) {
22
+ return suppression;
23
+ }
24
+ }
25
+ return null;
26
+ }
27
+ findMatchingSuppression(finding) {
28
+ for (const suppression of this.suppressions) {
29
+ if (this.matchesFinding(suppression, finding)) {
30
+ return suppression;
31
+ }
32
+ }
33
+ return null;
34
+ }
35
+ matchesFinding(suppression, finding) {
36
+ return this.matchesEndpoint(suppression, finding.endpoint, finding.ruleId);
37
+ }
38
+ matchesEndpoint(suppression, endpoint, ruleId) {
39
+ // Check rule ID
40
+ if (suppression.ruleId && suppression.ruleId !== ruleId) {
41
+ return false;
42
+ }
43
+ // Check HTTP method
44
+ if (suppression.method && suppression.method.toUpperCase() !== endpoint.method) {
45
+ return false;
46
+ }
47
+ // Check exact route match
48
+ if (suppression.route && suppression.route !== endpoint.route) {
49
+ return false;
50
+ }
51
+ // Check route pattern (regex or glob-like)
52
+ if (suppression.routePattern) {
53
+ if (!this.matchesRoutePattern(suppression.routePattern, endpoint.route)) {
54
+ return false;
55
+ }
56
+ }
57
+ return true;
58
+ }
59
+ matchesRoutePattern(pattern, route) {
60
+ // Convert glob-like pattern to regex
61
+ // * matches any segment, ** matches any path
62
+ let regexPattern = pattern
63
+ .replace(/\*\*/g, '<<<DOUBLE_STAR>>>')
64
+ .replace(/\*/g, '[^/]+')
65
+ .replace(/<<<DOUBLE_STAR>>>/g, '.*');
66
+ // Escape regex special chars except those we converted
67
+ regexPattern = regexPattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
68
+ // Anchor the pattern
69
+ regexPattern = `^${regexPattern}$`;
70
+ try {
71
+ const regex = new RegExp(regexPattern);
72
+ return regex.test(route);
73
+ }
74
+ catch {
75
+ return false;
76
+ }
77
+ }
78
+ }
79
+ //# sourceMappingURL=suppression-matcher.js.map
@@ -0,0 +1,7 @@
1
+ import { Endpoint } from '../models/endpoint.js';
2
+ import { LoadedSourceFile } from '../analysis/source-file-loader.js';
3
+ export interface EndpointDiscoverer {
4
+ readonly name: string;
5
+ discover(file: LoadedSourceFile): Promise<Endpoint[]>;
6
+ }
7
+ //# sourceMappingURL=discoverer-interface.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=discoverer-interface.js.map
@@ -0,0 +1,20 @@
1
+ import { EndpointDiscoverer } from './discoverer-interface.js';
2
+ import { LoadedSourceFile } from '../analysis/source-file-loader.js';
3
+ import { Endpoint } from '../models/endpoint.js';
4
+ export declare class ExpressDiscoverer implements EndpointDiscoverer {
5
+ readonly name = "Express.js";
6
+ private registry;
7
+ private authExtractor;
8
+ constructor();
9
+ discover(file: LoadedSourceFile): Promise<Endpoint[]>;
10
+ private collectRouterMounts;
11
+ private processCallExpression;
12
+ private getCallerName;
13
+ private isExpressIdentifier;
14
+ private extractRoutePath;
15
+ private extractMiddlewares;
16
+ private extractMiddlewareName;
17
+ private extractHandlerName;
18
+ private normalizePath;
19
+ }
20
+ //# sourceMappingURL=express-discoverer.d.ts.map
@@ -0,0 +1,223 @@
1
+ import * as ts from 'typescript';
2
+ import { getLineAndColumn, findNodes } from '../analysis/source-file-loader.js';
3
+ import { createEndpoint } from '../models/endpoint.js';
4
+ import { EndpointType } from '../models/endpoint-type.js';
5
+ import { HttpMethod, parseHttpMethod } from '../models/http-method.js';
6
+ import { RouteGroupRegistry } from './route-group-registry.js';
7
+ import { ExpressAuthExtractor } from '../authorization/express-auth-extractor.js';
8
+ const EXPRESS_HTTP_METHODS = new Set([
9
+ 'get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'all'
10
+ ]);
11
+ const EXPRESS_IDENTIFIERS = new Set(['app', 'router', 'express']);
12
+ export class ExpressDiscoverer {
13
+ name = 'Express.js';
14
+ registry;
15
+ authExtractor;
16
+ constructor() {
17
+ this.registry = new RouteGroupRegistry();
18
+ this.authExtractor = new ExpressAuthExtractor();
19
+ }
20
+ async discover(file) {
21
+ const endpoints = [];
22
+ const { sourceFile, filePath } = file;
23
+ // First pass: collect router mounts and route groups
24
+ this.collectRouterMounts(sourceFile, filePath);
25
+ // Second pass: find route definitions
26
+ const callExpressions = findNodes(sourceFile, ts.isCallExpression);
27
+ for (const callExpr of callExpressions) {
28
+ const endpoint = this.processCallExpression(callExpr, sourceFile, filePath);
29
+ if (endpoint) {
30
+ endpoints.push(endpoint);
31
+ }
32
+ }
33
+ return endpoints;
34
+ }
35
+ collectRouterMounts(sourceFile, filePath) {
36
+ const callExpressions = findNodes(sourceFile, ts.isCallExpression);
37
+ for (const callExpr of callExpressions) {
38
+ // Look for app.use('/prefix', router)
39
+ if (!ts.isPropertyAccessExpression(callExpr.expression))
40
+ continue;
41
+ const propAccess = callExpr.expression;
42
+ const methodName = propAccess.name.text;
43
+ if (methodName !== 'use')
44
+ continue;
45
+ const args = callExpr.arguments;
46
+ if (args.length < 2)
47
+ continue;
48
+ const firstArg = args[0];
49
+ const secondArg = args[1];
50
+ // Check for app.use('/prefix', router)
51
+ if (ts.isStringLiteral(firstArg) && ts.isIdentifier(secondArg)) {
52
+ const prefix = firstArg.text;
53
+ const routerName = secondArg.text;
54
+ const appName = ts.isIdentifier(propAccess.expression)
55
+ ? propAccess.expression.text
56
+ : '';
57
+ if (appName) {
58
+ this.registry.registerRouterMount(filePath, appName, prefix, routerName);
59
+ }
60
+ }
61
+ }
62
+ }
63
+ processCallExpression(callExpr, sourceFile, filePath) {
64
+ // Check for pattern: app.get('/path', handler) or router.post('/path', handler)
65
+ if (!ts.isPropertyAccessExpression(callExpr.expression)) {
66
+ return null;
67
+ }
68
+ const propAccess = callExpr.expression;
69
+ const methodName = propAccess.name.text.toLowerCase();
70
+ // Check if this is an HTTP method call
71
+ if (!EXPRESS_HTTP_METHODS.has(methodName)) {
72
+ return null;
73
+ }
74
+ // Check if caller is an Express identifier
75
+ const callerName = this.getCallerName(propAccess.expression);
76
+ if (!callerName || !this.isExpressIdentifier(callerName)) {
77
+ return null;
78
+ }
79
+ // Get route path from first argument
80
+ const args = callExpr.arguments;
81
+ if (args.length === 0) {
82
+ return null;
83
+ }
84
+ const routePath = this.extractRoutePath(args[0]);
85
+ if (!routePath) {
86
+ return null;
87
+ }
88
+ // Get prefix if this is a router
89
+ const prefix = this.registry.getRouterPrefix(filePath, callerName);
90
+ const fullRoute = this.normalizePath(prefix + routePath);
91
+ // Extract middleware chain (all arguments except last handler)
92
+ const middlewares = this.extractMiddlewares(args, sourceFile);
93
+ // Get handler name
94
+ const handlerName = this.extractHandlerName(args[args.length - 1], sourceFile);
95
+ // Get location
96
+ const location = getLineAndColumn(sourceFile, callExpr.getStart(sourceFile));
97
+ // Extract authorization info
98
+ const authorization = this.authExtractor.extract(middlewares, {
99
+ isRouter: callerName !== 'app',
100
+ routerMiddlewares: this.registry.getAllMiddlewares(filePath, callerName),
101
+ });
102
+ return createEndpoint({
103
+ route: fullRoute,
104
+ method: parseHttpMethod(methodName.toUpperCase()) ?? HttpMethod.GET,
105
+ handlerName,
106
+ type: EndpointType.Express,
107
+ location: {
108
+ filePath,
109
+ line: location.line,
110
+ column: location.column,
111
+ },
112
+ authorization,
113
+ });
114
+ }
115
+ getCallerName(expression) {
116
+ if (ts.isIdentifier(expression)) {
117
+ return expression.text;
118
+ }
119
+ return null;
120
+ }
121
+ isExpressIdentifier(name) {
122
+ // Common Express variable names
123
+ if (EXPRESS_IDENTIFIERS.has(name)) {
124
+ return true;
125
+ }
126
+ // Also match common patterns like userRouter, apiRouter, etc.
127
+ if (name.toLowerCase().includes('router') || name.toLowerCase().includes('app')) {
128
+ return true;
129
+ }
130
+ return false;
131
+ }
132
+ extractRoutePath(node) {
133
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
134
+ return node.text;
135
+ }
136
+ // Handle template literals with embedded expressions (extract just the static parts)
137
+ if (ts.isTemplateExpression(node)) {
138
+ let path = node.head.text;
139
+ for (const span of node.templateSpans) {
140
+ path += ':param' + span.literal.text;
141
+ }
142
+ return path;
143
+ }
144
+ return null;
145
+ }
146
+ extractMiddlewares(args, sourceFile) {
147
+ const middlewares = [];
148
+ // All arguments except possibly the last one (the main handler) could be middleware
149
+ for (let i = 0; i < args.length - 1; i++) {
150
+ const arg = args[i];
151
+ // Skip the route path (first arg if it's a string)
152
+ if (i === 0 && (ts.isStringLiteral(arg) || ts.isTemplateExpression(arg))) {
153
+ continue;
154
+ }
155
+ const middlewareName = this.extractMiddlewareName(arg, sourceFile);
156
+ if (middlewareName) {
157
+ middlewares.push(middlewareName);
158
+ }
159
+ }
160
+ return middlewares;
161
+ }
162
+ extractMiddlewareName(node, sourceFile) {
163
+ // Direct identifier: requireAuth
164
+ if (ts.isIdentifier(node)) {
165
+ return node.text;
166
+ }
167
+ // Call expression: passport.authenticate('jwt')
168
+ if (ts.isCallExpression(node)) {
169
+ if (ts.isPropertyAccessExpression(node.expression)) {
170
+ const obj = node.expression.expression;
171
+ const method = node.expression.name;
172
+ if (ts.isIdentifier(obj) && ts.isIdentifier(method)) {
173
+ return `${obj.text}.${method.text}`;
174
+ }
175
+ }
176
+ if (ts.isIdentifier(node.expression)) {
177
+ return node.expression.text;
178
+ }
179
+ }
180
+ // Property access: auth.requireRole
181
+ if (ts.isPropertyAccessExpression(node)) {
182
+ const text = node.getText(sourceFile);
183
+ return text;
184
+ }
185
+ // Array of middleware: [auth, validate]
186
+ if (ts.isArrayLiteralExpression(node)) {
187
+ return node.elements
188
+ .map((el) => this.extractMiddlewareName(el, sourceFile))
189
+ .filter(Boolean)
190
+ .join(',');
191
+ }
192
+ return null;
193
+ }
194
+ extractHandlerName(node, sourceFile) {
195
+ // Direct identifier
196
+ if (ts.isIdentifier(node)) {
197
+ return node.text;
198
+ }
199
+ // Property access: controller.method
200
+ if (ts.isPropertyAccessExpression(node)) {
201
+ return node.getText(sourceFile);
202
+ }
203
+ // Arrow function or function expression
204
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
205
+ return 'anonymous';
206
+ }
207
+ return 'unknown';
208
+ }
209
+ normalizePath(path) {
210
+ // Ensure path starts with /
211
+ if (!path.startsWith('/')) {
212
+ path = '/' + path;
213
+ }
214
+ // Remove duplicate slashes
215
+ path = path.replace(/\/+/g, '/');
216
+ // Remove trailing slash (except for root)
217
+ if (path.length > 1 && path.endsWith('/')) {
218
+ path = path.slice(0, -1);
219
+ }
220
+ return path;
221
+ }
222
+ }
223
+ //# sourceMappingURL=express-discoverer.js.map
@@ -0,0 +1,19 @@
1
+ import { EndpointDiscoverer } from './discoverer-interface.js';
2
+ import { LoadedSourceFile } from '../analysis/source-file-loader.js';
3
+ import { Endpoint } from '../models/endpoint.js';
4
+ export declare class FastifyDiscoverer implements EndpointDiscoverer {
5
+ readonly name = "Fastify";
6
+ discover(file: LoadedSourceFile): Promise<Endpoint[]>;
7
+ private processShorthandRoute;
8
+ private processRouteMethod;
9
+ private extractAuthFromOptions;
10
+ private extractHooksAuth;
11
+ private extractHookName;
12
+ private isAuthHook;
13
+ private getCallerName;
14
+ private isFastifyIdentifier;
15
+ private extractStringValue;
16
+ private extractHandlerName;
17
+ private normalizePath;
18
+ }
19
+ //# sourceMappingURL=fastify-discoverer.d.ts.map