@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.
- package/htmlTemplate.js +18 -4
- package/monkey.js +117 -49
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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'))
|
|
8
|
-
if (n.includes('password') || n.includes('pass'))
|
|
9
|
-
if (n.includes('date'))
|
|
10
|
-
if (n.includes('
|
|
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 {
|
|
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))
|
|
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
|
-
|
|
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]
|
|
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 {
|
|
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
|
|
42
|
-
|
|
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
|
-
|
|
82
|
+
|
|
83
|
+
for (var i = 0; i < stack.length; i++) {
|
|
84
|
+
var layer = stack[i];
|
|
48
85
|
if (layer.route) {
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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 };
|