offbyt 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 (103) hide show
  1. package/README.md +2 -0
  2. package/cli/index.js +2 -0
  3. package/cli.js +206 -0
  4. package/core/detector/detectAxios.js +107 -0
  5. package/core/detector/detectFetch.js +148 -0
  6. package/core/detector/detectForms.js +55 -0
  7. package/core/detector/detectSocket.js +341 -0
  8. package/core/generator/generateControllers.js +17 -0
  9. package/core/generator/generateModels.js +25 -0
  10. package/core/generator/generateRoutes.js +17 -0
  11. package/core/generator/generateServer.js +18 -0
  12. package/core/generator/generateSocket.js +160 -0
  13. package/core/index.js +14 -0
  14. package/core/ir/IRTypes.js +25 -0
  15. package/core/ir/buildIR.js +83 -0
  16. package/core/parser/parseJS.js +26 -0
  17. package/core/parser/parseTS.js +27 -0
  18. package/core/rules/relationRules.js +38 -0
  19. package/core/rules/resourceRules.js +32 -0
  20. package/core/rules/schemaInference.js +26 -0
  21. package/core/scanner/scanProject.js +58 -0
  22. package/deploy/cloudflare.js +41 -0
  23. package/deploy/cloudflareWorker.js +122 -0
  24. package/deploy/connect.js +198 -0
  25. package/deploy/flyio.js +51 -0
  26. package/deploy/index.js +322 -0
  27. package/deploy/netlify.js +29 -0
  28. package/deploy/railway.js +215 -0
  29. package/deploy/render.js +195 -0
  30. package/deploy/utils.js +383 -0
  31. package/deploy/vercel.js +29 -0
  32. package/index.js +18 -0
  33. package/lib/generator/advancedCrudGenerator.js +475 -0
  34. package/lib/generator/crudCodeGenerator.js +486 -0
  35. package/lib/generator/irBasedGenerator.js +360 -0
  36. package/lib/ir-builder/index.js +16 -0
  37. package/lib/ir-builder/irBuilder.js +330 -0
  38. package/lib/ir-builder/rulesEngine.js +353 -0
  39. package/lib/ir-builder/templateEngine.js +193 -0
  40. package/lib/ir-builder/templates/index.js +14 -0
  41. package/lib/ir-builder/templates/model.template.js +47 -0
  42. package/lib/ir-builder/templates/routes-generic.template.js +66 -0
  43. package/lib/ir-builder/templates/routes-user.template.js +105 -0
  44. package/lib/ir-builder/templates/routes.template.js +102 -0
  45. package/lib/ir-builder/templates/validation.template.js +15 -0
  46. package/lib/ir-integration.js +349 -0
  47. package/lib/modes/benchmark.js +162 -0
  48. package/lib/modes/configBasedGenerator.js +2258 -0
  49. package/lib/modes/connect.js +1125 -0
  50. package/lib/modes/doctorAi.js +172 -0
  51. package/lib/modes/generateApi.js +435 -0
  52. package/lib/modes/interactiveSetup.js +548 -0
  53. package/lib/modes/offline.clean.js +14 -0
  54. package/lib/modes/offline.enhanced.js +787 -0
  55. package/lib/modes/offline.js +295 -0
  56. package/lib/modes/offline.v2.js +13 -0
  57. package/lib/modes/sync.js +629 -0
  58. package/lib/scanner/apiEndpointExtractor.js +387 -0
  59. package/lib/scanner/authPatternDetector.js +54 -0
  60. package/lib/scanner/frontendScanner.js +642 -0
  61. package/lib/utils/apiClientGenerator.js +242 -0
  62. package/lib/utils/apiScanner.js +95 -0
  63. package/lib/utils/codeInjector.js +350 -0
  64. package/lib/utils/doctor.js +381 -0
  65. package/lib/utils/envGenerator.js +36 -0
  66. package/lib/utils/loadTester.js +61 -0
  67. package/lib/utils/performanceAnalyzer.js +298 -0
  68. package/lib/utils/resourceDetector.js +281 -0
  69. package/package.json +20 -0
  70. package/templates/.env.template +31 -0
  71. package/templates/advanced.model.template.js +201 -0
  72. package/templates/advanced.route.template.js +341 -0
  73. package/templates/auth.middleware.template.js +87 -0
  74. package/templates/auth.routes.template.js +238 -0
  75. package/templates/auth.user.model.template.js +78 -0
  76. package/templates/cache.middleware.js +34 -0
  77. package/templates/chat.models.template.js +260 -0
  78. package/templates/chat.routes.template.js +478 -0
  79. package/templates/compression.middleware.js +19 -0
  80. package/templates/database.config.js +74 -0
  81. package/templates/errorHandler.middleware.js +54 -0
  82. package/templates/express/controller.ejs +26 -0
  83. package/templates/express/model.ejs +9 -0
  84. package/templates/express/route.ejs +18 -0
  85. package/templates/express/server.ejs +16 -0
  86. package/templates/frontend.env.template +14 -0
  87. package/templates/model.template.js +86 -0
  88. package/templates/package.production.json +51 -0
  89. package/templates/package.template.json +41 -0
  90. package/templates/pagination.utility.js +110 -0
  91. package/templates/production.server.template.js +233 -0
  92. package/templates/rateLimiter.middleware.js +36 -0
  93. package/templates/requestLogger.middleware.js +19 -0
  94. package/templates/response.helper.js +179 -0
  95. package/templates/route.template.js +130 -0
  96. package/templates/security.middleware.js +78 -0
  97. package/templates/server.template.js +91 -0
  98. package/templates/socket.server.template.js +433 -0
  99. package/templates/utils.helper.js +157 -0
  100. package/templates/validation.middleware.js +63 -0
  101. package/templates/validation.schema.js +128 -0
  102. package/utils/fileWriter.js +15 -0
  103. package/utils/logger.js +18 -0
@@ -0,0 +1,387 @@
1
+ /**
2
+ * API Endpoint Extractor - Enhanced Version
3
+ * Properly handles complex routes, nested actions, and query parameters
4
+ * Groups endpoints by actual resources (not nested actions)
5
+ */
6
+
7
+ export function extractAllApiEndpoints(content, detectedApiCalls = []) {
8
+ const resources = {};
9
+
10
+ for (const call of detectedApiCalls) {
11
+ let endpoint, method, hasAuth = false, queryParams = [];
12
+
13
+ if (typeof call === 'object' && call.route) {
14
+ endpoint = call.route.startsWith('/') ? call.route : '/' + call.route;
15
+ method = call.method || 'GET';
16
+ hasAuth = true; // Assume protected by default
17
+ queryParams = call.queryParams || [];
18
+ } else if (typeof call === 'string') {
19
+ endpoint = extractEndpoint(call);
20
+ method = extractMethod(call);
21
+ hasAuth = requiresAuth(call);
22
+ } else {
23
+ continue;
24
+ }
25
+
26
+ if (!endpoint) continue;
27
+ if (isSystemEndpoint(endpoint)) continue;
28
+
29
+ // Smart resource extraction (ignores nested actions)
30
+ const resource = extractResourceSmart(endpoint);
31
+ const action = extractActionFromEndpoint(endpoint);
32
+ const hasParam = hasParameter(endpoint);
33
+
34
+ if (!resources[resource]) {
35
+ resources[resource] = {
36
+ name: resource,
37
+ endpoints: [],
38
+ methods: new Set(),
39
+ hasAuth: false,
40
+ fields: new Set(),
41
+ actions: new Set(),
42
+ queryParams: new Set()
43
+ };
44
+ }
45
+
46
+ // Add action if it's a nested action
47
+ if (action) {
48
+ resources[resource].actions.add(action);
49
+ }
50
+
51
+ resources[resource].endpoints.push({
52
+ path: endpoint,
53
+ method: method,
54
+ requiresAuth: hasAuth,
55
+ hasParam: hasParam,
56
+ action: action,
57
+ queryParams: queryParams
58
+ });
59
+
60
+ resources[resource].methods.add(method);
61
+ if (hasAuth) resources[resource].hasAuth = true;
62
+
63
+ // Add query params to resource
64
+ queryParams.forEach(p => resources[resource].queryParams.add(p));
65
+
66
+ // Extract field names
67
+ let fields = [];
68
+ const callContent = (typeof call === 'object' && call.content) ? call.content : '';
69
+
70
+ if (typeof call === 'object' && call.fields) {
71
+ fields = Array.isArray(call.fields) ? call.fields : Object.keys(call.fields || {});
72
+ } else if (typeof call === 'string') {
73
+ fields = extractFieldNames(endpoint, call, content);
74
+ } else if (typeof call === 'object') {
75
+ // Extract from the full file content
76
+ fields = extractFieldNames(endpoint, JSON.stringify(call), callContent || content);
77
+ }
78
+ fields.forEach(f => resources[resource].fields.add(f));
79
+ }
80
+
81
+ // Convert Sets to Arrays and enrich with defaults
82
+ const result = {};
83
+ for (const [key, resource] of Object.entries(resources)) {
84
+ const methods = Array.from(resource.methods);
85
+ const fields = Array.from(resource.fields);
86
+
87
+ // Add default fields if none detected
88
+ const allFields = fields.length > 0
89
+ ? fields
90
+ : getDefaultFieldsForResource(resource.name);
91
+
92
+ result[key] = {
93
+ name: resource.name,
94
+ endpoints: resource.endpoints,
95
+ methods: methods,
96
+ hasAuth: resource.hasAuth,
97
+ fields: allFields,
98
+ actions: Array.from(resource.actions),
99
+ queryParams: Array.from(resource.queryParams),
100
+ isCrudResource: detectCrudOperations(methods),
101
+ isComplexResource: resource.actions.size > 0 || resource.queryParams.size > 0
102
+ };
103
+ }
104
+
105
+ return result;
106
+ }
107
+
108
+ function isSystemEndpoint(endpoint) {
109
+ const normalized = endpoint
110
+ .split('?')[0]
111
+ .trim()
112
+ .replace(/\/+$/, '')
113
+ .toLowerCase();
114
+
115
+ return normalized === '/health' ||
116
+ normalized === `/api/health` ||
117
+ normalized === `/api/status` ||
118
+ normalized === `/api`;
119
+ }
120
+
121
+ /**
122
+ * Smart resource extraction - ignores nested actions
123
+ * Examples:
124
+ * - /api/products → products
125
+ * - /api/products/:id → products
126
+ * - /api/products/:id/reviews → products (NOT a separate resource!)
127
+ * - /api/reviews/:id/approve → reviews (approve is an action)
128
+ * - /api/orders/:id/cancel → orders (cancel is an action)
129
+ * - /api/cart/clear → cart (clear is an action)
130
+ */
131
+ function extractResourceSmart(endpoint) {
132
+ // Remove /api prefix
133
+ let path = endpoint.replace(/^\/api\//, '');
134
+
135
+ // Split into segments
136
+ const segments = path.split('/').filter(s => s && s !== ':id');
137
+
138
+ if (segments.length === 0) return 'api';
139
+
140
+ // First segment is always the resource
141
+ const resource = segments[0];
142
+
143
+ // Common action keywords that should NOT be resources
144
+ const actionKeywords = [
145
+ 'approve', 'reject', 'cancel', 'confirm', 'verify',
146
+ 'activate', 'deactivate', 'enable', 'disable',
147
+ 'clear', 'reset', 'refresh', 'sync',
148
+ 'search', 'filter', 'sort', 'export', 'import',
149
+ 'list', 'stats', 'analytics', 'dashboard'
150
+ ];
151
+
152
+ // If second segment is an action keyword, return first segment
153
+ if (segments.length > 1 && actionKeywords.includes(segments[1].toLowerCase())) {
154
+ return resource;
155
+ }
156
+
157
+ // If resource is an action keyword, it's likely being used as a resource name
158
+ // (e.g., /api/approve) - check context
159
+ if (actionKeywords.includes(resource.toLowerCase())) {
160
+ // If there's a preceding resource, it's an action
161
+ // Otherwise, treat as resource (might be miscategorized)
162
+ return resource;
163
+ }
164
+
165
+ return resource;
166
+ }
167
+
168
+ /**
169
+ * Extract action from endpoint
170
+ * Examples:
171
+ * - /api/reviews/:id/approve → approve
172
+ * - /api/orders/:id/cancel → cancel
173
+ * - /api/cart/clear → clear
174
+ * - /api/products/search → search
175
+ */
176
+ function extractActionFromEndpoint(endpoint) {
177
+ const path = endpoint.replace(/^\/api\//, '');
178
+ const segments = path.split('/').filter(s => s && s !== ':id');
179
+
180
+ if (segments.length <= 1) return null;
181
+
182
+ // Last segment is likely the action (if not :id)
183
+ const lastSegment = segments[segments.length - 1];
184
+
185
+ const actionKeywords = [
186
+ 'approve', 'reject', 'cancel', 'confirm', 'verify',
187
+ 'activate', 'deactivate', 'enable', 'disable',
188
+ 'clear', 'reset', 'refresh', 'sync',
189
+ 'search', 'filter', 'sort', 'export', 'import',
190
+ 'stats', 'analytics', 'dashboard', 'count'
191
+ ];
192
+
193
+ if (actionKeywords.includes(lastSegment.toLowerCase())) {
194
+ return lastSegment;
195
+ }
196
+
197
+ return null;
198
+ }
199
+
200
+ /**
201
+ * Extract HTTP method from API call
202
+ */
203
+ function extractMethod(call) {
204
+ if (typeof call !== 'string') return 'GET';
205
+
206
+ if (call.includes("method: 'POST'") || call.includes('method: "POST"') || call.includes('.post(')) {
207
+ return 'POST';
208
+ }
209
+ if (call.includes("method: 'PUT'") || call.includes('method: "PUT"') || call.includes('.put(')) {
210
+ return 'PUT';
211
+ }
212
+ if (call.includes("method: 'DELETE'") || call.includes('method: "DELETE"') || call.includes('.delete(')) {
213
+ return 'DELETE';
214
+ }
215
+ if (call.includes("method: 'GET'") || call.includes('method: "GET"') || call.includes('.get(')) {
216
+ return 'GET';
217
+ }
218
+ if (call.includes('body:')) {
219
+ return 'POST'; // Default POST if body present
220
+ }
221
+ return 'GET'; // Default GET
222
+ }
223
+
224
+ /**
225
+ * Check if route requires authentication
226
+ */
227
+ function requiresAuth(call) {
228
+ if (typeof call !== 'string') return false;
229
+
230
+ const authIndicators = [
231
+ 'Authorization',
232
+ 'Bearer',
233
+ 'token',
234
+ 'jwt',
235
+ 'headers:'
236
+ ];
237
+ return authIndicators.some(indicator => call.toLowerCase().includes(indicator.toLowerCase()));
238
+ }
239
+
240
+ /**
241
+ * Check if endpoint has parameters
242
+ */
243
+ function hasParameter(endpoint) {
244
+ return endpoint.includes(':id') || endpoint.includes('${') || /\/\d+$/.test(endpoint);
245
+ }
246
+
247
+ /**
248
+ * Extract potential field names from endpoint and context
249
+ */
250
+ function extractFieldNames(endpoint, call, fullContent) {
251
+ const fields = new Set();
252
+
253
+ // Extract from JSON.stringify patterns
254
+ if (typeof call === 'string') {
255
+ const jsonPattern = /JSON\.stringify\(\s*\{([^}]+)\}/g;
256
+ let match;
257
+ while ((match = jsonPattern.exec(call)) !== null) {
258
+ const jsonContent = match[1];
259
+ const fields_in_json = jsonContent.match(/(\w+)\s*[,:]/g);
260
+ if (fields_in_json) {
261
+ fields_in_json.forEach(f => {
262
+ fields.add(f.replace(/[,:]/g, '').trim());
263
+ });
264
+ }
265
+ }
266
+ }
267
+
268
+ // Look for form input names in full content
269
+ if (fullContent && typeof fullContent === 'string') {
270
+ // Pattern 1: <input name="fieldName"
271
+ const inputPattern = /<input[^>]*name=['"`](\w+)['"`]/g;
272
+ let match;
273
+ while ((match = inputPattern.exec(fullContent)) !== null) {
274
+ fields.add(match[1]);
275
+ }
276
+
277
+ // Pattern 2: formData.append('fieldName'
278
+ const formDataPattern = /formData\.append\s*\(\s*['"`](\w+)['"`]/g;
279
+ while ((match = formDataPattern.exec(fullContent)) !== null) {
280
+ fields.add(match[1]);
281
+ }
282
+
283
+ // Pattern 3: useState for field names
284
+ const statePattern = /const\s*\[(\w+),\s*set\w+\]\s*=\s*useState/g;
285
+ while ((match = statePattern.exec(fullContent)) !== null) {
286
+ const stateName = match[1];
287
+ // Only add if looks like a field (not loading, error, etc.)
288
+ if (!['loading', 'error', 'data', 'response', 'success'].includes(stateName)) {
289
+ fields.add(stateName);
290
+ }
291
+ }
292
+
293
+ // Pattern 4: Object shorthand in body ({ name, email, price })
294
+ const bodyPattern = /body\s*:\s*JSON\.stringify\s*\(\s*\{([^}]+)\}\s*\)/g;
295
+ while ((match = bodyPattern.exec(fullContent)) !== null) {
296
+ const bodyContent = match[1];
297
+ const fieldMatches = bodyContent.match(/(\w+)\s*[,:}]/g);
298
+ if (fieldMatches) {
299
+ fieldMatches.forEach(f => {
300
+ const fieldName = f.replace(/[,:}]/g, '').trim();
301
+ if (fieldName && fieldName !== 'headers') {
302
+ fields.add(fieldName);
303
+ }
304
+ });
305
+ }
306
+ }
307
+ }
308
+
309
+ return Array.from(fields);
310
+ }
311
+
312
+ /**
313
+ * Detect if resource has CRUD operations based on methods
314
+ */
315
+ function detectCrudOperations(methods) {
316
+ // Convert Set to Array if needed
317
+ const methodsArray = Array.isArray(methods) ? methods : Array.from(methods);
318
+
319
+ const has_get = methodsArray.includes('GET');
320
+ const has_post = methodsArray.includes('POST');
321
+ const has_put = methodsArray.includes('PUT');
322
+ const has_delete = methodsArray.includes('DELETE');
323
+
324
+ return {
325
+ create: has_post,
326
+ read: has_get,
327
+ update: has_put,
328
+ delete: has_delete
329
+ };
330
+ }
331
+
332
+ /**
333
+ * Get default fields for a resource based on name
334
+ */
335
+ export function getDefaultFieldsForResource(resourceName) {
336
+ const fieldsMap = {
337
+ products: ['name', 'description', 'price', 'category', 'stock'],
338
+ posts: ['title', 'content', 'author', 'views', 'likes'],
339
+ comments: ['text', 'author', 'postId', 'likes'],
340
+ users: ['name', 'email', 'phone', 'avatar'],
341
+ orders: ['orderNumber', 'items', 'total', 'status', 'shippingAddress'],
342
+ categories: ['name', 'description', 'icon'],
343
+ tags: ['name', 'description'],
344
+ reviews: ['rating', 'text', 'author', 'productId'],
345
+ articles: ['title', 'content', 'author', 'tags', 'published'],
346
+ todos: ['title', 'completed', 'priority', 'dueDate'],
347
+ };
348
+
349
+ return fieldsMap[resourceName.toLowerCase()] || ['name', 'description'];
350
+ }
351
+
352
+ /**
353
+ * Group endpoints by operation type
354
+ */
355
+ export function groupEndpointsByOperation(resources) {
356
+ const grouped = {
357
+ create: [],
358
+ read: [],
359
+ update: [],
360
+ delete: [],
361
+ list: [],
362
+ other: []
363
+ };
364
+
365
+ for (const [resourceName, resource] of Object.entries(resources)) {
366
+ for (const endpoint of resource.endpoints) {
367
+ const path = endpoint.path.toLowerCase();
368
+ const method = endpoint.method;
369
+
370
+ if (method === 'POST' && (path.includes('create') || path.includes('add'))) {
371
+ grouped.create.push({ resource: resourceName, ...endpoint });
372
+ } else if (method === 'GET' && (path.includes('list') || path.endsWith(`/api/${resourceName}`))) {
373
+ grouped.list.push({ resource: resourceName, ...endpoint });
374
+ } else if (method === 'GET' && path.includes(':id')) {
375
+ grouped.read.push({ resource: resourceName, ...endpoint });
376
+ } else if (method === 'PUT' || method === 'PATCH') {
377
+ grouped.update.push({ resource: resourceName, ...endpoint });
378
+ } else if (method === 'DELETE') {
379
+ grouped.delete.push({ resource: resourceName, ...endpoint });
380
+ } else {
381
+ grouped.other.push({ resource: resourceName, ...endpoint });
382
+ }
383
+ }
384
+ }
385
+
386
+ return grouped;
387
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Auth Pattern Detection
3
+ * Analyzes frontend code to detect authentication patterns
4
+ */
5
+
6
+ export function detectAuthPatterns(content, apiCalls = []) {
7
+ const authPatterns = {
8
+ hasSignup: false,
9
+ hasLogin: false,
10
+ hasLogout: false,
11
+ hasProfile: false,
12
+ hasTokenStorage: false,
13
+ hasPasswordField: false,
14
+ authEndpoints: []
15
+ };
16
+
17
+ // Detect signup
18
+ if (/signup|register|createAccount/i.test(content)) {
19
+ authPatterns.hasSignup = true;
20
+ const signupCall = apiCalls.find(c => /signup|register|createAccount/i.test(c.route));
21
+ if (signupCall) authPatterns.authEndpoints.push({ type: 'signup', ...signupCall });
22
+ }
23
+
24
+ // Detect login
25
+ if (/login|authenticate|signIn/i.test(content)) {
26
+ authPatterns.hasLogin = true;
27
+ const loginCall = apiCalls.find(c => /login|authenticate|signIn/i.test(c.route));
28
+ if (loginCall) authPatterns.authEndpoints.push({ type: 'login', ...loginCall });
29
+ }
30
+
31
+ // Detect logout
32
+ if (/logout|signOut/i.test(content)) {
33
+ authPatterns.hasLogout = true;
34
+ }
35
+
36
+ // Detect profile/user endpoints
37
+ if (/profile|user|account|me(?!thod)/i.test(content)) {
38
+ authPatterns.hasProfile = true;
39
+ const profileCall = apiCalls.find(c => /profile|user|account|\/me/i.test(c.route) && c.method === 'GET');
40
+ if (profileCall) authPatterns.authEndpoints.push({ type: 'profile', ...profileCall });
41
+ }
42
+
43
+ // Detect token/storage
44
+ if (/localStorage|sessionStorage|jwt|token|Bearer/i.test(content)) {
45
+ authPatterns.hasTokenStorage = true;
46
+ }
47
+
48
+ // Detect password field
49
+ if (/password|pwd|passcode/i.test(content)) {
50
+ authPatterns.hasPasswordField = true;
51
+ }
52
+
53
+ return authPatterns;
54
+ }