@aimeloic/monkey-tester 5.0.2 → 5.0.4

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 (3) hide show
  1. package/htmlTemplate.js +18 -4
  2. package/monkey.js +117 -49
  3. package/package.json +1 -1
package/htmlTemplate.js CHANGED
@@ -17,7 +17,15 @@ function encodePayload(obj) {
17
17
  // The reciprocal decode runs inside each HTML template (see _decode below).
18
18
  // It is injected as a one-liner so every template is self-contained.
19
19
 
20
- const _decode = `function _decode(b64){return JSON.parse(decodeURIComponent(escape(atob(b64.replace(/\s+/g,'')))));}`;
20
+ // Replace line 18 with this version:
21
+ const _decode = `function _decode(b64) {
22
+ const binStr = atob(b64.replace(/\\s+/g, ''));
23
+ const bytes = new Uint8Array(binStr.length);
24
+ for (let i = 0; i < binStr.length; i++) {
25
+ bytes[i] = binStr.charCodeAt(i);
26
+ }
27
+ return JSON.parse(new TextDecoder().decode(bytes));
28
+ }`;
21
29
 
22
30
  // ─── Runtime sandbox (injected into tester page) ─────────────────────────────
23
31
  function runtimeClientSandbox() {
@@ -33,8 +41,8 @@ function runtimeClientSandbox() {
33
41
  }
34
42
 
35
43
  const ENDPOINTS = _decode(
36
- document.getElementById('__monkey_data__').getAttribute('data-payload')
37
- );
44
+ document.getElementById('__monkey_data__').textContent.trim()
45
+ );
38
46
 
39
47
  let currentKey = null;
40
48
 
@@ -454,8 +462,14 @@ async function handleRegister() {
454
462
 
455
463
  <script>
456
464
  // ── Unicode-safe decode ──────────────────────────────────────────────────────
465
+ // Replace the old _decode function inside UI.dashboard with this:
457
466
  function _decode(b64) {
458
- return JSON.parse(decodeURIComponent(escape(atob(b64.replace(/\s+/g, '')))));
467
+ const binStr = atob(b64.replace(/\s+/g, ''));
468
+ const bytes = new Uint8Array(binStr.length);
469
+ for (let i = 0; i < binStr.length; i++) {
470
+ bytes[i] = binStr.charCodeAt(i);
471
+ }
472
+ return JSON.parse(new TextDecoder().decode(bytes));
459
473
  }
460
474
 
461
475
  // BEFORE (Broken)
package/monkey.js CHANGED
@@ -1,75 +1,129 @@
1
1
  'use strict';
2
2
 
3
- import { UI } from './htmlTemplate.js';
3
+ import { UI, encodePayload } from './htmlTemplate.js';
4
4
 
5
5
  function inferType(name) {
6
6
  const n = name.toLowerCase();
7
- if (n.includes('email')) return 'email';
8
- if (n.includes('password') || n.includes('pass')) return 'password';
9
- if (n.includes('date')) return 'date';
10
- if (n.includes('age') || n.includes('price') || n.includes('quantity') || n.includes('stock') || n === 'id' || n.endsWith('id')) return 'number';
7
+ if (n.includes('email')) return 'email';
8
+ if (n.includes('password') || n.includes('pass')) return 'password';
9
+ if (n.includes('date') || n.includes('birth')) return 'date';
10
+ if (n.includes('phone') || n.includes('tel')) return 'tel';
11
+ if (n.includes('url') || n.includes('website') || n.includes('link')) return 'url';
12
+ if (
13
+ n.includes('age') || n.includes('price') || n.includes('amount') ||
14
+ n.includes('count') || n.includes('qty') || n.includes('quantity') ||
15
+ n.includes('stock') || n.includes('salary') || n.includes('total') ||
16
+ (n === 'id') || n.endsWith('_id') || n.endsWith('Id')
17
+ ) return 'number';
11
18
  return 'text';
12
19
  }
13
20
 
14
21
  function buildField(name) {
15
- return { name, label: name.charAt(0).toUpperCase() + name.slice(1).replace(/([A-Z])/g, ' $1'), type: inferType(name), placeholder: `Enter ${name}` };
22
+ return {
23
+ name,
24
+ label: name.charAt(0).toUpperCase() + name.slice(1).replace(/([A-Z])/g, ' $1'),
25
+ type: inferType(name),
26
+ placeholder: 'Enter ' + name
27
+ };
16
28
  }
17
29
 
18
30
  function extractBodyFields(handler) {
19
31
  try {
20
32
  const source = handler.toString();
21
33
  if (!source || source.includes('[native code]')) return [];
34
+
22
35
  const seen = new Map();
23
36
  const destructRe = /(?:const|let|var)\s*\{\s*([^}]+)\s*\}\s*=\s*req\.body/g;
24
37
  let m;
25
38
  while ((m = destructRe.exec(source)) !== null) {
26
- m[1].split(',').forEach(part => {
39
+ m[1].split(',').forEach(function(part) {
27
40
  const name = part.split(':')[0].split('=')[0].trim();
28
- if (name && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) && !seen.has(name)) seen.set(name, buildField(name));
41
+ if (name && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) && !seen.has(name)) {
42
+ seen.set(name, buildField(name));
43
+ }
29
44
  });
30
45
  }
31
- const accessRe = /req\.body\.([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
46
+
47
+ const accessRe = /req\.body\.([a-zA-Z_$][a-zA-Z0-9_$]*)|req\.body\[['"]([a-zA-Z_$][a-zA-Z0-9_$]*)['"]]/g;
32
48
  while ((m = accessRe.exec(source)) !== null) {
33
- const name = m[1]; if (name && !seen.has(name)) seen.set(name, buildField(name));
49
+ const name = m[1] || m[2];
50
+ if (name && !seen.has(name)) seen.set(name, buildField(name));
34
51
  }
52
+
35
53
  return Array.from(seen.values());
36
- } catch { return []; }
54
+ } catch {
55
+ return [];
56
+ }
57
+ }
58
+
59
+ function fallbackFields(path) {
60
+ const p = path.toLowerCase();
61
+ if (p.includes('login') || p.includes('signin') || p.includes('auth/login')) return ['email', 'password'].map(buildField);
62
+ if (p.includes('register') || p.includes('signup') || p.includes('auth/register')) return ['username', 'email', 'password'].map(buildField);
63
+ if (p.includes('user')) return ['username', 'email', 'password'].map(buildField);
64
+ if (p.includes('product')) return ['name', 'price', 'stock'].map(buildField);
65
+ if (p.includes('order')) return ['productId', 'quantity', 'address'].map(buildField);
66
+ return [];
37
67
  }
38
68
 
39
69
  function extractRouterPrefix(layer) {
40
70
  if (!layer.regexp) return '';
41
- const m = [/^\^\\\/([^\\?$]+)/, /^\^\\\/([a-zA-Z0-9_/-]+)/].reduce((acc, re) => acc || re.exec(layer.regexp.source), null);
42
- return m && m[1] ? '/' + m[1].replace(/\\\//g, '/').replace(/\\/g, '') : '';
71
+ const src = layer.regexp.source;
72
+ const patterns = [/^\^\\\/([^\\?$]+)/, /^\^\\\/([a-zA-Z0-9_/-]+)/];
73
+ for (var i = 0; i < patterns.length; i++) {
74
+ const m = patterns[i].exec(src);
75
+ if (m && m[1]) return '/' + m[1].replace(/\\\//g, '/').replace(/\\/g, '');
76
+ }
77
+ return '';
43
78
  }
44
79
 
45
- function parseStack(stack, detectedEndpoints, prefix = '') {
80
+ function parseStack(stack, detectedEndpoints, prefix) {
46
81
  if (!Array.isArray(stack)) return;
47
- for (const layer of stack) {
82
+
83
+ for (var i = 0; i < stack.length; i++) {
84
+ var layer = stack[i];
48
85
  if (layer.route) {
49
- const rawPath = typeof layer.route.path === 'string' ? layer.route.path : (layer.route.path ? String(layer.route.path) : '');
50
- const fullPath = (prefix + rawPath).replace(/\/+/g, '/') || '/';
86
+ var rawPath = typeof layer.route.path === 'string' ? layer.route.path : (layer.route.path ? String(layer.route.path) : '');
87
+ var fullPath = (prefix + rawPath).replace(/\/+/g, '/') || '/';
88
+
51
89
  if (fullPath.startsWith('/api/tester')) continue;
52
90
 
53
- const methods = Object.keys(layer.route.methods || {});
54
- for (const method of methods) {
55
- const httpMethod = method.toUpperCase();
56
- const key = `${httpMethod}::${fullPath}`;
57
- const pathParams = [];
58
- const paramRe = /:([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
59
- let pm;
60
- while ((pm = paramRe.exec(fullPath)) !== null) {
61
- pathParams.push({ name: pm[1], label: pm[1].charAt(0).toUpperCase() + pm[1].slice(1), placeholder: 'value' });
62
- }
91
+ var methods = Object.keys(layer.route.methods || {});
92
+ var pathParams = [];
93
+ var paramRe = /:([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
94
+ var pm;
95
+ while ((pm = paramRe.exec(fullPath)) !== null) {
96
+ pathParams.push({ name: pm[1], label: pm[1].charAt(0).toUpperCase() + pm[1].slice(1), placeholder: 'value' });
97
+ }
63
98
 
64
- let bodyFields = [];
65
- if (['POST', 'PUT', 'PATCH'].includes(httpMethod)) {
66
- const handlers = (layer.route.stack || []).map(sl => sl.handle).filter(Boolean);
67
- for (const handler of handlers) bodyFields.push(...extractBodyFields(handler));
68
- const seen = new Map();
69
- bodyFields = bodyFields.filter(f => !seen.has(f.name) && seen.set(f.name, true));
99
+ var bodyFields = [];
100
+ if (methods.some(function(m) { return ['POST', 'PUT', 'PATCH'].indexOf(m.toUpperCase()) !== -1; })) {
101
+ var handlers = (layer.route.stack || []).map(function(sl) { return sl.handle; }).filter(Boolean);
102
+ for (var j = 0; j < handlers.length; j++) {
103
+ bodyFields.push.apply(bodyFields, extractBodyFields(handlers[j]));
70
104
  }
105
+ var seen2 = new Map();
106
+ bodyFields = bodyFields.filter(function(f) {
107
+ if (seen2.has(f.name)) return false;
108
+ seen2.set(f.name, true);
109
+ return true;
110
+ });
111
+ if (bodyFields.length === 0 && pathParams.length === 0) {
112
+ bodyFields = fallbackFields(fullPath);
113
+ }
114
+ }
71
115
 
72
- detectedEndpoints[key] = { method: httpMethod, path: fullPath, title: `${httpMethod} ${fullPath}`, desc: `Auto-discovered endpoint - ${fullPath}`, params: pathParams, fields: bodyFields };
116
+ for (var k = 0; k < methods.length; k++) {
117
+ var httpMethod = methods[k].toUpperCase();
118
+ var key = httpMethod + '::' + fullPath;
119
+ detectedEndpoints[key] = {
120
+ method: httpMethod,
121
+ path: fullPath,
122
+ title: httpMethod + ' ' + fullPath,
123
+ desc: 'Auto-discovered endpoint — ' + fullPath,
124
+ params: pathParams,
125
+ fields: bodyFields
126
+ };
73
127
  }
74
128
  } else if (layer.name === 'router' && layer.handle && layer.handle.stack) {
75
129
  parseStack(layer.handle.stack, detectedEndpoints, prefix + extractRouterPrefix(layer));
@@ -79,26 +133,40 @@ function parseStack(stack, detectedEndpoints, prefix = '') {
79
133
 
80
134
  export function endtesterExpress() {
81
135
  return function monkeyTesterMiddleware(req, res, next) {
82
- const route = req.path.toLowerCase().replace(/\/$/, '');
83
-
84
- // 1. Serve Standalone Frontend Templates Interceptions
85
- if (route === '/login') return res.send(UI.login());
86
- if (route === '/signup') return res.send(UI.signup());
87
- if (route === '/dashboard') return res.send(UI.dashboard());
88
-
89
- // 2. Serve the main Tester UI environment
90
- if (route === '/api/tester') {
91
- const app = req.app;
92
- const detectedEndpoints = {};
93
- const rootStack = (app._router && app._router.stack) || (app.router && app.router.stack) || [];
94
- parseStack(rootStack, detectedEndpoints);
95
- const b64 = Buffer.from(JSON.stringify(detectedEndpoints)).toString('base64');
136
+ var normalized = req.path.replace(/\/+$/, '');
137
+
138
+ // Tester sandbox must be checked before static pages so query strings don't collide
139
+ if (normalized.startsWith('/api/tester')) {
140
+ var app = req.app;
141
+ var eps = app.__monkey_endpoints_cache__ || {};
142
+ if (!app.__monkey_endpoints_cache__) {
143
+ eps = {};
144
+ var raw = (app._router && app._router.stack) || (app.router && app.router.stack) || [];
145
+ parseStack(raw, eps);
146
+ app.__monkey_endpoints_cache__ = eps;
147
+ }
148
+ var b64 = encodePayload(eps);
96
149
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
97
150
  return res.send(UI.tester(b64));
98
151
  }
99
152
 
153
+ // Standalone HTML pages — resolved dynamically against discovered routes
154
+ if (normalized === '/login') return res.send(UI.login());
155
+ if (normalized === '/signup') return res.send(UI.signup());
156
+ if (normalized === '/dashboard') {
157
+ if (!app.__monkey_endpoints_cache__) {
158
+ var eps2 = {};
159
+ var raw2 = (app._router && app._router.stack) || (app.router && app.router.stack) || [];
160
+ parseStack(raw2, eps2);
161
+ app.__monkey_endpoints_cache__ = eps2;
162
+ }
163
+ var b64dash = encodePayload(app.__monkey_endpoints_cache__);
164
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
165
+ return res.send(UI.dashboard(b64dash));
166
+ }
167
+
100
168
  next();
101
169
  };
102
170
  }
103
171
 
104
- export default { endtesterExpress };
172
+ export default { endtesterExpress };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aimeloic/monkey-tester",
3
- "version": "5.0.2",
3
+ "version": "5.0.4",
4
4
  "description": "Auto route scanning visual runner dashboard.",
5
5
  "main": "index.js",
6
6
  "type": "module",