@aimeloic/monkey-tester 3.0.8 → 3.0.10
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/html.backup.js +176 -302
- package/monkey.backup.js +213 -0
- package/monkey.js +27 -86
- package/package.json +1 -1
package/html.backup.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function getHtmlTemplate(endpoints) {
|
|
2
4
|
const safeJsonString = Buffer.from(JSON.stringify(endpoints)).toString('base64');
|
|
3
5
|
|
|
4
|
-
return
|
|
5
|
-
<!DOCTYPE html>
|
|
6
|
+
return `<!DOCTYPE html>
|
|
6
7
|
<html lang="en">
|
|
7
8
|
<head>
|
|
8
9
|
<meta charset="UTF-8">
|
|
@@ -11,172 +12,63 @@ export function getHtmlTemplate(endpoints) {
|
|
|
11
12
|
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=DM+Mono:wght@400;500&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet">
|
|
12
13
|
<style>
|
|
13
14
|
:root {
|
|
14
|
-
--bg: #0e0c09;
|
|
15
|
-
--
|
|
16
|
-
--
|
|
17
|
-
--border: #3a3020;
|
|
18
|
-
--accent: #e8a838;
|
|
19
|
-
--accent2: #c47a1e;
|
|
20
|
-
--text: #f0e8d8;
|
|
21
|
-
--text-dim: #9a8c78;
|
|
22
|
-
--red: #d45c3c;
|
|
23
|
-
--green: #6ba05a;
|
|
24
|
-
--blue: #5a86c0;
|
|
25
|
-
--radius: 8px;
|
|
15
|
+
--bg: #0e0c09; --surface: #181510; --surface2: #221d14; --border: #3a3020;
|
|
16
|
+
--accent: #e8a838; --accent2: #c47a1e; --text: #f0e8d8; --text-dim: #9a8c78;
|
|
17
|
+
--red: #d45c3c; --green: #6ba05a; --blue: #5a86c0; --radius: 8px;
|
|
26
18
|
}
|
|
27
|
-
|
|
28
19
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
background: var(--bg);
|
|
32
|
-
color: var(--text);
|
|
33
|
-
font-family: 'DM Sans', sans-serif;
|
|
34
|
-
font-size: 14px;
|
|
35
|
-
height: 100vh;
|
|
36
|
-
overflow: hidden; /* Prevents whole-page scrolling */
|
|
37
|
-
background-image: radial-gradient(ellipse 80% 60% at 50% -20%, #3a2a0a22 0%, transparent 70%);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
header {
|
|
41
|
-
border-bottom: 1px solid var(--border);
|
|
42
|
-
padding: 16px 32px;
|
|
43
|
-
display: flex;
|
|
44
|
-
align-items: center;
|
|
45
|
-
gap: 20px;
|
|
46
|
-
background: #0e0c09ee;
|
|
47
|
-
backdrop-filter: blur(8px);
|
|
48
|
-
height: 65px;
|
|
49
|
-
}
|
|
50
|
-
|
|
20
|
+
body { background: var(--bg); color: var(--text); font-family: 'DM Sans', sans-serif; font-size: 14px; height: 100vh; overflow: hidden; background-image: radial-gradient(ellipse 80% 60% at 50% -20%, #3a2a0a22 0%, transparent 70%); }
|
|
21
|
+
header { border-bottom: 1px solid var(--border); padding: 16px 32px; display: flex; align-items: center; gap: 20px; background: #0e0c09ee; backdrop-filter: blur(8px); height: 65px; }
|
|
51
22
|
.logo { font-family: 'Playfair Display', serif; font-size: 20px; color: var(--accent); letter-spacing: 0.02em; }
|
|
52
23
|
.logo span { color: var(--text-dim); font-size: 11px; font-family: 'DM Mono', monospace; display: inline-block; margin-left: 8px; font-weight: 400; }
|
|
53
24
|
.header-right { margin-left: auto; display: flex; align-items: center; gap: 16px; }
|
|
54
25
|
.jwt-wrap, .base-url-wrap { display: flex; align-items: center; gap: 8px; }
|
|
55
26
|
.jwt-wrap label, .base-url-wrap label { color: var(--text-dim); font-size: 11px; font-family: 'DM Mono', monospace; letter-spacing: 0.05em; }
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
font-family: 'DM Mono', monospace; font-size: 12px; padding: 6px 12px; border-radius: var(--radius); width: 220px; outline: none;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/* Fixed view height viewport matrix layout grid rules */
|
|
63
|
-
.layout {
|
|
64
|
-
display: grid;
|
|
65
|
-
grid-template-columns: 280px 1fr 450px;
|
|
66
|
-
height: calc(100vh - 65px);
|
|
67
|
-
overflow: hidden;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
aside {
|
|
71
|
-
border-right: 1px solid var(--border);
|
|
72
|
-
overflow-y: auto;
|
|
73
|
-
padding: 16px 0;
|
|
74
|
-
background: #0b0907;
|
|
75
|
-
}
|
|
76
|
-
|
|
27
|
+
#jwt-input, #base-url { background: var(--surface2); border: 1px solid var(--border); color: var(--text); font-family: 'DM Mono', monospace; font-size: 12px; padding: 6px 12px; border-radius: var(--radius); width: 220px; outline: none; }
|
|
28
|
+
.layout { display: grid; grid-template-columns: 280px 1fr 450px; height: calc(100vh - 65px); overflow: hidden; }
|
|
29
|
+
aside { border-right: 1px solid var(--border); overflow-y: auto; padding: 16px 0; background: #0b0907; }
|
|
77
30
|
.section-label { font-size: 10px; font-family: 'DM Mono', monospace; color: var(--text-dim); letter-spacing: 0.12em; text-transform: uppercase; padding: 12px 18px 6px; }
|
|
78
|
-
|
|
79
31
|
.nav-item { display: flex; align-items: center; gap: 10px; padding: 10px 18px; cursor: pointer; border-left: 2px solid transparent; color: var(--text-dim); transition: all 0.2s; }
|
|
80
32
|
.nav-item:hover { background: var(--surface); color: var(--text); }
|
|
81
33
|
.nav-item.active { border-left-color: var(--accent); background: var(--surface); color: var(--accent); }
|
|
82
|
-
|
|
83
34
|
.method-badge { font-family: 'DM Mono', monospace; font-size: 9px; font-weight: 600; padding: 2px 6px; border-radius: 4px; min-width: 52px; text-align: center; text-transform: uppercase; }
|
|
84
|
-
.GET { background: #1a3a22; color: #6ba05a; }
|
|
85
|
-
.POST { background: #1a2e3a; color: #5a86c0; }
|
|
86
|
-
.PUT, .PATCH { background: #3a2e10; color: #e8a838; }
|
|
87
|
-
.DELETE { background: #3a1a14; color: #d45c3c; }
|
|
88
|
-
|
|
35
|
+
.GET { background: #1a3a22; color: #6ba05a; } .POST { background: #1a2e3a; color: #5a86c0; } .PUT, .PATCH { background: #3a2e10; color: #e8a838; } .DELETE { background: #3a1a14; color: #d45c3c; }
|
|
89
36
|
.nav-label { font-size: 12px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
90
|
-
|
|
91
|
-
main {
|
|
92
|
-
overflow-y: auto;
|
|
93
|
-
padding: 32px;
|
|
94
|
-
background: #0e0c09;
|
|
95
|
-
}
|
|
96
|
-
|
|
37
|
+
main { overflow-y: auto; padding: 32px; background: #0e0c09; }
|
|
97
38
|
.endpoint-title { font-family: 'Playfair Display', serif; font-size: 24px; color: var(--accent); margin-bottom: 8px; }
|
|
98
39
|
.endpoint-path { font-family: 'DM Mono', monospace; font-size: 13px; color: var(--text-dim); margin-bottom: 24px; display: flex; align-items: center; gap: 8px; }
|
|
99
40
|
.endpoint-desc { color: var(--text-dim); font-size: 13px; line-height: 1.6; margin-bottom: 24px; border-left: 2px solid var(--border); padding-left: 12px; }
|
|
100
|
-
|
|
101
41
|
.form-section { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; margin-bottom: 20px; }
|
|
102
42
|
.form-section-title { font-size: 11px; font-family: 'DM Mono', monospace; color: var(--text-dim); letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 16px; border-bottom: 1px solid var(--border); padding-bottom: 6px; }
|
|
103
|
-
|
|
104
43
|
.field-row { display: grid; grid-template-columns: 150px 1fr; align-items: center; gap: 16px; margin-bottom: 14px; }
|
|
105
|
-
.field-row:last-child { margin-bottom: 0; }
|
|
106
44
|
.field-label { font-family: 'DM Mono', monospace; font-size: 12px; color: var(--text-dim); text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
107
|
-
|
|
108
|
-
input[type=text], input[type=password], input[type=number], input[type=date], input[type=tel], input[type=url], select {
|
|
109
|
-
background: var(--surface2); border: 1px solid var(--border); color: var(--text); font-family: 'DM Sans', sans-serif; font-size: 13px; padding: 8px 12px; border-radius: var(--radius); width: 100%; outline: none; transition: border-color 0.2s;
|
|
110
|
-
}
|
|
45
|
+
input[type=text], input[type=password], input[type=number], input[type=date], input[type=tel], input[type=url], input[type=email], select { background: var(--surface2); border: 1px solid var(--border); color: var(--text); font-family: 'DM Sans', sans-serif; font-size: 13px; padding: 8px 12px; border-radius: var(--radius); width: 100%; outline: none; transition: border-color 0.2s; }
|
|
111
46
|
input:focus { border-color: var(--accent); }
|
|
112
|
-
|
|
113
47
|
.btn-row { margin-top: 24px; display: flex; gap: 12px; }
|
|
114
48
|
.btn { background: var(--accent); color: #0e0c09; border: none; padding: 10px 24px; border-radius: var(--radius); font-size: 13px; font-weight: 500; cursor: pointer; transition: background-color 0.2s; }
|
|
115
|
-
.btn:hover { background: #f0b850; }
|
|
116
|
-
.
|
|
117
|
-
.btn-secondary:hover { color: var(--text); background: var(--surface); }
|
|
118
|
-
|
|
119
|
-
/* FIXED: Response block scroll logic layout rules */
|
|
120
|
-
.response-panel {
|
|
121
|
-
border-left: 1px solid var(--border);
|
|
122
|
-
display: flex;
|
|
123
|
-
flex-direction: column;
|
|
124
|
-
overflow: hidden;
|
|
125
|
-
background: #110e0a;
|
|
126
|
-
}
|
|
49
|
+
.btn:hover { background: #f0b850; } .btn-secondary { background: var(--surface2); color: var(--text-dim); border: 1px solid var(--border); } .btn-secondary:hover { color: var(--text); background: var(--surface); }
|
|
50
|
+
.response-panel { border-left: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; background: #110e0a; }
|
|
127
51
|
.response-header { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; background: var(--surface); height: 50px; }
|
|
128
52
|
.response-header-title { font-size: 11px; font-family: 'DM Mono', monospace; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
129
53
|
.status-badge { font-family: 'DM Mono', monospace; font-size: 12px; margin-left: auto; padding: 2px 8px; border-radius: 4px; font-weight: 500; }
|
|
130
|
-
.status-ok { background: #1a3a22; color: #6ba05a; }
|
|
131
|
-
.
|
|
132
|
-
.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
.response-body {
|
|
136
|
-
flex: 1;
|
|
137
|
-
overflow-y: auto;
|
|
138
|
-
overflow-x: hidden;
|
|
139
|
-
padding: 0;
|
|
140
|
-
background: #0d0b08;
|
|
141
|
-
}
|
|
142
|
-
.response-body.empty {
|
|
143
|
-
color: var(--text-dim);
|
|
144
|
-
display: flex;
|
|
145
|
-
align-items: center;
|
|
146
|
-
justify-content: center;
|
|
147
|
-
padding: 20px;
|
|
148
|
-
text-align: center;
|
|
149
|
-
font-size: 13px;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/* FIXED: Token code output container handles micro horizontal data flows elegantly */
|
|
153
|
-
.json-render-block {
|
|
154
|
-
display: block;
|
|
155
|
-
padding: 20px;
|
|
156
|
-
margin: 0;
|
|
157
|
-
font-family: 'DM Mono', monospace;
|
|
158
|
-
font-size: 12px;
|
|
159
|
-
line-height: 1.5;
|
|
160
|
-
white-space: pre;
|
|
161
|
-
overflow-x: auto;
|
|
162
|
-
word-break: normal;
|
|
163
|
-
word-wrap: normal;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
.json-key { color: #e8a838; }
|
|
167
|
-
.json-str { color: #9ab878; }
|
|
168
|
-
.json-num { color: #5a86c0; }
|
|
169
|
-
|
|
54
|
+
.status-ok { background: #1a3a22; color: #6ba05a; } .status-err { background: #3a1a14; color: #d45c3c; } .status-idle { background: var(--surface2); color: var(--text-dim); }
|
|
55
|
+
.response-body { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 0; background: #0d0b08; }
|
|
56
|
+
.response-body.empty { color: var(--text-dim); display: flex; align-items: center; justify-content: center; padding: 20px; text-align: center; font-size: 13px; }
|
|
57
|
+
.json-render-block { display: block; padding: 20px; margin: 0; font-family: 'DM Mono', monospace; font-size: 12px; line-height: 1.5; white-space: pre; overflow-x: auto; word-break: normal; word-wrap: normal; }
|
|
58
|
+
.json-key { color: #e8a838; } .json-str { color: #9ab878; } .json-num { color: #5a86c0; } .json-bool { color: #c47a1e; } .json-null { color: var(--text-dim); }
|
|
170
59
|
#toast { position: fixed; bottom: 24px; right: 24px; background: var(--surface2); border: 1px solid var(--border); padding: 10px 18px; border-radius: var(--radius); opacity: 0; transition: all .25s; z-index: 1000; font-family: 'DM Mono', monospace; font-size: 12px; color: var(--accent); }
|
|
171
60
|
#toast.show { opacity: 1; }
|
|
61
|
+
.empty-state { text-align: center; padding: 60px 20px; color: var(--text-dim); }
|
|
62
|
+
.empty-state .monkey { font-size: 48px; margin-bottom: 16px; }
|
|
63
|
+
.empty-state h2 { color: var(--text); font-family: 'Playfair Display', serif; margin-bottom: 8px; }
|
|
172
64
|
</style>
|
|
173
65
|
</head>
|
|
174
66
|
<body>
|
|
175
67
|
|
|
176
|
-
<div id="
|
|
68
|
+
<div id="__monkey_data__" data-payload="${safeJsonString}" style="display:none;"></div>
|
|
177
69
|
|
|
178
70
|
<header>
|
|
179
|
-
<div class="logo"
|
|
71
|
+
<div class="logo">🐒 Endtester <span>Application Runtime Sandbox</span></div>
|
|
180
72
|
<div class="header-right">
|
|
181
73
|
<div class="base-url-wrap">
|
|
182
74
|
<label>TARGET HOST</label>
|
|
@@ -206,200 +98,182 @@ export function getHtmlTemplate(endpoints) {
|
|
|
206
98
|
<div id="toast"></div>
|
|
207
99
|
|
|
208
100
|
<script>
|
|
209
|
-
const
|
|
210
|
-
|
|
101
|
+
const ENDPOINTS = JSON.parse(atob(document.getElementById('__monkey_data__').getAttribute('data-payload')));
|
|
102
|
+
let currentKey = null;
|
|
211
103
|
|
|
212
|
-
|
|
104
|
+
document.getElementById('base-url').value = window.location.origin;
|
|
213
105
|
|
|
214
|
-
|
|
106
|
+
function buildSidebar() {
|
|
107
|
+
const sidebar = document.getElementById('sidebar-nav');
|
|
108
|
+
const keys = Object.keys(ENDPOINTS);
|
|
215
109
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
110
|
+
if (keys.length === 0) {
|
|
111
|
+
sidebar.innerHTML += '<div style="padding:18px;color:var(--text-dim);font-size:12px">No endpoints discovered.</div>';
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
keys.forEach((key, i) => {
|
|
116
|
+
const ep = ENDPOINTS[key];
|
|
117
|
+
const item = document.createElement('div');
|
|
118
|
+
item.className = 'nav-item' + (i === 0 ? ' active' : '');
|
|
119
|
+
item.setAttribute('data-key', key);
|
|
120
|
+
item.innerHTML =
|
|
121
|
+
'<span class="method-badge ' + ep.method + '">' + ep.method + '</span>' +
|
|
122
|
+
'<span class="nav-label">' + ep.path + '</span>';
|
|
123
|
+
item.addEventListener('click', () => {
|
|
124
|
+
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
125
|
+
item.classList.add('active');
|
|
126
|
+
clearResponse();
|
|
127
|
+
renderPanel(key);
|
|
128
|
+
});
|
|
129
|
+
sidebar.appendChild(item);
|
|
130
|
+
});
|
|
219
131
|
|
|
220
|
-
|
|
221
|
-
sidebar.innerHTML += '<div style="padding:15px; color:var(--text-dim)">No active application endpoints discovered.</div>';
|
|
222
|
-
return;
|
|
132
|
+
renderPanel(keys[0]);
|
|
223
133
|
}
|
|
224
134
|
|
|
225
|
-
|
|
135
|
+
function renderPanel(key) {
|
|
136
|
+
currentKey = key;
|
|
226
137
|
const ep = ENDPOINTS[key];
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
138
|
+
const main = document.getElementById('main-panel');
|
|
139
|
+
if (!ep) return;
|
|
140
|
+
|
|
141
|
+
let html =
|
|
142
|
+
'<div class="endpoint-title">' + ep.title + '</div>' +
|
|
143
|
+
'<div class="endpoint-path"><span class="method-badge ' + ep.method + '">' + ep.method + '</span>' +
|
|
144
|
+
'<span>' + ep.path + '</span></div>' +
|
|
145
|
+
'<div class="endpoint-desc">' + ep.desc + '</div>';
|
|
146
|
+
|
|
147
|
+
if (ep.params && ep.params.length) {
|
|
148
|
+
html += '<div class="form-section"><div class="form-section-title">Path Parameters</div>';
|
|
149
|
+
ep.params.forEach(function(p) {
|
|
150
|
+
html +=
|
|
151
|
+
'<div class="field-row">' +
|
|
152
|
+
'<label class="field-label">' + p.label + '</label>' +
|
|
153
|
+
'<input type="text" id="param-' + p.name + '" placeholder="' + p.placeholder + '" />' +
|
|
154
|
+
'</div>';
|
|
155
|
+
});
|
|
156
|
+
html += '</div>';
|
|
157
|
+
}
|
|
242
158
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
159
|
+
if (ep.fields && ep.fields.length) {
|
|
160
|
+
html += '<div class="form-section"><div class="form-section-title">HTTP JSON Request Payload Parameters</div>';
|
|
161
|
+
ep.fields.forEach(function(f) {
|
|
162
|
+
html +=
|
|
163
|
+
'<div class="field-row">' +
|
|
164
|
+
'<label class="field-label">' + f.label + '</label>' +
|
|
165
|
+
'<input type="' + (f.type || 'text') + '" id="field-' + f.name + '" placeholder="' + (f.placeholder || '') + '" />' +
|
|
166
|
+
'</div>';
|
|
167
|
+
});
|
|
168
|
+
html += '</div>';
|
|
169
|
+
}
|
|
247
170
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
let html = \`
|
|
255
|
-
<div class="endpoint-title">\${ep.title}</div>
|
|
256
|
-
<div class="endpoint-path">
|
|
257
|
-
<span class="method-badge \${ep.method}">\${ep.method}</span>
|
|
258
|
-
<span>\${ep.path}</span>
|
|
259
|
-
</div>
|
|
260
|
-
<div class="endpoint-desc">\${ep.desc}</div>
|
|
261
|
-
\`;
|
|
262
|
-
|
|
263
|
-
if (ep.params && ep.params.length) {
|
|
264
|
-
html += \`<div class="form-section"><div class="form-section-title">Path Parameters</div>\`;
|
|
265
|
-
ep.params.forEach(function(p) {
|
|
266
|
-
const inputHtml = makeInputString('text', 'param-' + p.name, p.placeholder);
|
|
267
|
-
html += \`
|
|
268
|
-
<div class="field-row">
|
|
269
|
-
<label class="field-label">\${p.label}</label>
|
|
270
|
-
\${inputHtml}
|
|
271
|
-
</div>
|
|
272
|
-
\`;
|
|
273
|
-
});
|
|
274
|
-
html += \`</div>\`;
|
|
275
|
-
}
|
|
171
|
+
html +=
|
|
172
|
+
'<div class="btn-row">' +
|
|
173
|
+
'<button class="btn" onclick="sendRequest()">Execute Route</button>' +
|
|
174
|
+
'<button class="btn btn-secondary" onclick="clearResponse()">Clear Context</button>' +
|
|
175
|
+
'</div>';
|
|
276
176
|
|
|
277
|
-
|
|
278
|
-
html += \`<div class="form-section"><div class="form-section-title">HTTP JSON Request Payload Parameters</div>\`;
|
|
279
|
-
ep.fields.forEach(function(f) {
|
|
280
|
-
const inputHtml = makeInputString(f.type || 'text', 'field-' + f.name, f.placeholder || '');
|
|
281
|
-
html += \`
|
|
282
|
-
<div class="field-row">
|
|
283
|
-
<label class="field-label">\${f.label}</label>
|
|
284
|
-
\${inputHtml}
|
|
285
|
-
</div>
|
|
286
|
-
\`;
|
|
287
|
-
});
|
|
288
|
-
html += \`</div>\`;
|
|
177
|
+
main.innerHTML = html;
|
|
289
178
|
}
|
|
290
179
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
<button class="btn btn-secondary" onclick="clearResponse()">Clear Context</button>
|
|
295
|
-
</div>
|
|
296
|
-
\`;
|
|
180
|
+
async function sendRequest() {
|
|
181
|
+
const ep = ENDPOINTS[currentKey];
|
|
182
|
+
let path = ep.path;
|
|
297
183
|
|
|
298
|
-
|
|
299
|
-
|
|
184
|
+
if (ep.params && ep.params.length) {
|
|
185
|
+
for (const p of ep.params) {
|
|
186
|
+
const val = (document.getElementById('param-' + p.name) || {}).value || '';
|
|
187
|
+
if (!val.trim()) { showToast('⚠ Path param "' + p.label + '" is required'); return; }
|
|
188
|
+
path = path.replace(':' + p.name, encodeURIComponent(val.trim()));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
300
191
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
192
|
+
const baseUrl = document.getElementById('base-url').value.replace(/\/+$/, '');
|
|
193
|
+
const url = baseUrl + path;
|
|
194
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
195
|
+
const jwt = document.getElementById('jwt-input').value.trim();
|
|
196
|
+
if (jwt) headers['Authorization'] = 'Bearer ' + jwt;
|
|
197
|
+
|
|
198
|
+
let body = undefined;
|
|
199
|
+
if (['POST', 'PUT', 'PATCH'].includes(ep.method) && ep.fields && ep.fields.length) {
|
|
200
|
+
const payload = {};
|
|
201
|
+
ep.fields.forEach(function(f) {
|
|
202
|
+
const el = document.getElementById('field-' + f.name);
|
|
203
|
+
if (!el) return;
|
|
204
|
+
let v = el.value.trim();
|
|
205
|
+
if (f.type === 'number' && v !== '') v = Number(v);
|
|
206
|
+
payload[f.name] = v;
|
|
207
|
+
});
|
|
208
|
+
body = JSON.stringify(payload);
|
|
209
|
+
}
|
|
304
210
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
211
|
+
setResponse(null, 'loading');
|
|
212
|
+
const t0 = Date.now();
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const res = await fetch(url, { method: ep.method, headers, body });
|
|
216
|
+
const ms = Date.now() - t0;
|
|
217
|
+
const text = await res.text();
|
|
218
|
+
let data;
|
|
219
|
+
try { data = JSON.parse(text); } catch { data = text; }
|
|
220
|
+
setResponse(data, res.ok ? 'ok' : 'err', res.status, ms);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
setResponse({ error: err.message }, 'err', 'FAIL', 0);
|
|
310
223
|
}
|
|
311
224
|
}
|
|
312
225
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
for (const f of ep.fields) {
|
|
324
|
-
const targetEl = document.getElementById('field-' + f.name);
|
|
325
|
-
if (targetEl) {
|
|
326
|
-
let entryVal = targetEl.value.trim();
|
|
327
|
-
if (f.type === 'number' && entryVal !== '') {
|
|
328
|
-
entryVal = Number(entryVal);
|
|
329
|
-
}
|
|
330
|
-
jsonPayload[f.name] = entryVal;
|
|
331
|
-
}
|
|
226
|
+
function setResponse(data, state, status, ms) {
|
|
227
|
+
const badge = document.getElementById('status-badge');
|
|
228
|
+
const body = document.getElementById('response-body');
|
|
229
|
+
|
|
230
|
+
if (state === 'loading') {
|
|
231
|
+
badge.className = 'status-badge status-idle';
|
|
232
|
+
badge.textContent = '…';
|
|
233
|
+
body.className = 'response-body empty';
|
|
234
|
+
body.textContent = 'Executing transmission…';
|
|
235
|
+
return;
|
|
332
236
|
}
|
|
333
|
-
body = JSON.stringify(jsonPayload);
|
|
334
|
-
}
|
|
335
237
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const ms = Date.now() - start;
|
|
342
|
-
const text = await res.text();
|
|
343
|
-
let json;
|
|
344
|
-
try { json = JSON.parse(text); } catch { json = text; }
|
|
345
|
-
setResponse(json, res.ok ? 'ok' : 'err', res.status, ms);
|
|
346
|
-
} catch (err) {
|
|
347
|
-
setResponse({ error: err.message }, 'err', 'FAIL', 0);
|
|
238
|
+
badge.className = 'status-badge ' + (state === 'ok' ? 'status-ok' : 'status-err');
|
|
239
|
+
badge.textContent = status + ' · ' + ms + 'ms';
|
|
240
|
+
body.className = 'response-body';
|
|
241
|
+
const str = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
|
242
|
+
body.innerHTML = '<pre class="json-render-block">' + highlight(str) + '</pre>';
|
|
348
243
|
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function setResponse(data, state, status, ms) {
|
|
352
|
-
const badge = document.getElementById('status-badge');
|
|
353
|
-
const body = document.getElementById('response-body');
|
|
354
244
|
|
|
355
|
-
|
|
356
|
-
badge.className = 'status-badge status-idle';
|
|
357
|
-
badge.textContent = '
|
|
245
|
+
function clearResponse() {
|
|
246
|
+
document.getElementById('status-badge').className = 'status-badge status-idle';
|
|
247
|
+
document.getElementById('status-badge').textContent = '—';
|
|
248
|
+
const body = document.getElementById('response-body');
|
|
358
249
|
body.className = 'response-body empty';
|
|
359
|
-
body.
|
|
360
|
-
return;
|
|
250
|
+
body.textContent = 'Execute a request row to generate feedback data';
|
|
361
251
|
}
|
|
362
252
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
badge.className = 'status-badge status-idle';
|
|
376
|
-
badge.textContent = '—';
|
|
377
|
-
body.className = 'response-body empty';
|
|
378
|
-
body.textContent = 'Execute a request row to generate feedback data';
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function highlightJson(str) {
|
|
382
|
-
return str
|
|
383
|
-
.replace(/&/g, '&').replace(/[<]/g, '<').replace(/[>]/g, '>')
|
|
384
|
-
.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function(match) {
|
|
385
|
-
if (/^"/.test(match)) {
|
|
386
|
-
if (/:$/.test(match)) return '<span class="json-key">' + match + '</span>';
|
|
387
|
-
return '<span class="json-str">' + match + '</span>';
|
|
388
|
-
}
|
|
389
|
-
return '<span class="json-num">' + match + '</span>';
|
|
390
|
-
});
|
|
391
|
-
}
|
|
253
|
+
function highlight(str) {
|
|
254
|
+
return str
|
|
255
|
+
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
|
256
|
+
.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false)\b|\bnull\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function(m) {
|
|
257
|
+
if (/^"/.test(m)) return /:$/.test(m)
|
|
258
|
+
? '<span class="json-key">' + m + '</span>'
|
|
259
|
+
: '<span class="json-str">' + m + '</span>';
|
|
260
|
+
if (/true|false/.test(m)) return '<span class="json-bool">' + m + '</span>';
|
|
261
|
+
if (/null/.test(m)) return '<span class="json-null">' + m + '</span>';
|
|
262
|
+
return '<span class="json-num">' + m + '</span>';
|
|
263
|
+
});
|
|
264
|
+
}
|
|
392
265
|
|
|
393
|
-
function showToast(msg) {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
266
|
+
function showToast(msg) {
|
|
267
|
+
const t = document.getElementById('toast');
|
|
268
|
+
t.textContent = msg;
|
|
269
|
+
t.classList.add('show');
|
|
270
|
+
setTimeout(() => t.classList.remove('show'), 2500);
|
|
271
|
+
}
|
|
399
272
|
|
|
400
|
-
buildSidebar();
|
|
273
|
+
buildSidebar();
|
|
401
274
|
</script>
|
|
402
275
|
</body>
|
|
403
|
-
</html
|
|
404
|
-
`;
|
|
276
|
+
</html>`;
|
|
405
277
|
}
|
|
278
|
+
|
|
279
|
+
module.exports = { getHtmlTemplate };
|
package/monkey.backup.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { getHtmlTemplate } from './htmlTemplate.js';
|
|
4
|
+
|
|
5
|
+
// ─── Field type inference ─────────────────────────────────────────────────────
|
|
6
|
+
function inferType(name) {
|
|
7
|
+
const n = name.toLowerCase();
|
|
8
|
+
if (n.includes('email')) return 'email';
|
|
9
|
+
if (n.includes('password') || n.includes('pass')) return 'password';
|
|
10
|
+
if (n.includes('date') || n.includes('birth')) return 'date';
|
|
11
|
+
if (n.includes('phone') || n.includes('tel')) return 'tel';
|
|
12
|
+
if (n.includes('url') || n.includes('website') || n.includes('link')) return 'url';
|
|
13
|
+
if (
|
|
14
|
+
n.includes('age') || n.includes('price') || n.includes('amount') ||
|
|
15
|
+
n.includes('count') || n.includes('qty') || n.includes('quantity') ||
|
|
16
|
+
n.includes('stock') || n.includes('salary') || n.includes('total') ||
|
|
17
|
+
(n === 'id') || n.endsWith('_id') || n.endsWith('Id')
|
|
18
|
+
) return 'number';
|
|
19
|
+
return 'text';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function buildField(name) {
|
|
23
|
+
return {
|
|
24
|
+
name,
|
|
25
|
+
label: name.charAt(0).toUpperCase() + name.slice(1).replace(/([A-Z])/g, ' $1'),
|
|
26
|
+
type: inferType(name),
|
|
27
|
+
placeholder: `Enter ${name}`
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── Extract req.body fields from handler source ──────────────────────────────
|
|
32
|
+
function extractBodyFields(handler) {
|
|
33
|
+
try {
|
|
34
|
+
const source = handler.toString();
|
|
35
|
+
if (!source || source.includes('[native code]')) return [];
|
|
36
|
+
|
|
37
|
+
const seen = new Map();
|
|
38
|
+
|
|
39
|
+
// Pattern 1 — destructuring: const { email, password } = req.body
|
|
40
|
+
const destructRe = /(?:const|let|var)\s*\{\s*([^}]+)\s*\}\s*=\s*req\.body/g;
|
|
41
|
+
let m;
|
|
42
|
+
while ((m = destructRe.exec(source)) !== null) {
|
|
43
|
+
m[1].split(',').forEach(part => {
|
|
44
|
+
const name = part.split(':')[0].split('=')[0].trim();
|
|
45
|
+
if (name && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) && !seen.has(name)) {
|
|
46
|
+
seen.set(name, buildField(name));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Pattern 2 — property access: req.body.email / req.body['email']
|
|
52
|
+
const accessRe = /req\.body\.([a-zA-Z_$][a-zA-Z0-9_$]*)|req\.body\[['"]([a-zA-Z_$][a-zA-Z0-9_$]*)['"]]/g;
|
|
53
|
+
while ((m = accessRe.exec(source)) !== null) {
|
|
54
|
+
const name = m[1] || m[2];
|
|
55
|
+
if (name && !seen.has(name)) seen.set(name, buildField(name));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return Array.from(seen.values());
|
|
59
|
+
} catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Path-based fallback fields ───────────────────────────────────────────────
|
|
65
|
+
function fallbackFields(path) {
|
|
66
|
+
const p = path.toLowerCase();
|
|
67
|
+
|
|
68
|
+
if (p.includes('login') || p.includes('signin') || p.includes('auth/login')) {
|
|
69
|
+
return ['email', 'password'].map(buildField);
|
|
70
|
+
}
|
|
71
|
+
if (p.includes('register') || p.includes('signup') || p.includes('auth/register')) {
|
|
72
|
+
return ['username', 'email', 'password'].map(buildField);
|
|
73
|
+
}
|
|
74
|
+
if (p.includes('user')) {
|
|
75
|
+
return ['username', 'email', 'password'].map(buildField);
|
|
76
|
+
}
|
|
77
|
+
if (p.includes('product')) {
|
|
78
|
+
return ['name', 'price', 'stock'].map(buildField);
|
|
79
|
+
}
|
|
80
|
+
if (p.includes('order')) {
|
|
81
|
+
return ['productId', 'quantity', 'address'].map(buildField);
|
|
82
|
+
}
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── Extract router prefix safely from Express layer ─────────────────────────
|
|
87
|
+
function extractRouterPrefix(layer) {
|
|
88
|
+
// Prefer explicit path if available
|
|
89
|
+
if (layer.path && typeof layer.path === 'string') {
|
|
90
|
+
return layer.path === '/' ? '' : layer.path;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!layer.regexp) return '';
|
|
94
|
+
|
|
95
|
+
// Convert the regexp back to a path prefix by looking at the regexp source
|
|
96
|
+
// Express generates regexps like: /^\/api\/v1\/?(?=\/|$)/i
|
|
97
|
+
const src = layer.regexp.source;
|
|
98
|
+
|
|
99
|
+
// Extract the literal path segment before any optional/lookahead parts
|
|
100
|
+
// Match from start: ^\/ then literal segments
|
|
101
|
+
const match = src.match(/^\^((?:\\\/[^\\(?[*+{}|$^]+)+)/);
|
|
102
|
+
if (!match) return '';
|
|
103
|
+
|
|
104
|
+
// Unescape the extracted path
|
|
105
|
+
const raw = match[1].replace(/\\\//g, '/');
|
|
106
|
+
|
|
107
|
+
// Remove trailing slash if present
|
|
108
|
+
return raw.replace(/\/$/, '') || '';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── Walk the Express router stack recursively ────────────────────────────────
|
|
112
|
+
function parseStack(stack, detectedEndpoints, prefix = '') {
|
|
113
|
+
if (!Array.isArray(stack)) return;
|
|
114
|
+
|
|
115
|
+
for (const layer of stack) {
|
|
116
|
+
// ── Named route (app.get / app.post …) ──────────────────────────────────
|
|
117
|
+
if (layer.route) {
|
|
118
|
+
const rawPath = typeof layer.route.path === 'string'
|
|
119
|
+
? layer.route.path
|
|
120
|
+
: (layer.route.path ? String(layer.route.path) : '');
|
|
121
|
+
|
|
122
|
+
const fullPath = (prefix + rawPath).replace(/\/+/g, '/') || '/';
|
|
123
|
+
|
|
124
|
+
// Skip the tester route itself
|
|
125
|
+
if (fullPath.startsWith('/api/tester')) continue;
|
|
126
|
+
|
|
127
|
+
const methods = Object.keys(layer.route.methods || {});
|
|
128
|
+
|
|
129
|
+
for (const method of methods) {
|
|
130
|
+
const httpMethod = method.toUpperCase();
|
|
131
|
+
const key = `${httpMethod}::${fullPath}`;
|
|
132
|
+
|
|
133
|
+
// ── Path params (:id, :slug …) ────────────────────────────────────
|
|
134
|
+
const pathParams = [];
|
|
135
|
+
const paramRe = /:([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
|
|
136
|
+
const matches = [...fullPath.matchAll(paramRe)];
|
|
137
|
+
|
|
138
|
+
for (const pm of matches) {
|
|
139
|
+
pathParams.push({
|
|
140
|
+
name: pm[1],
|
|
141
|
+
label: pm[1].charAt(0).toUpperCase() + pm[1].slice(1),
|
|
142
|
+
placeholder: 'value'
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Body fields ──────────────────────────────────────────────────
|
|
147
|
+
let bodyFields = [];
|
|
148
|
+
if (['POST', 'PUT', 'PATCH'].includes(httpMethod)) {
|
|
149
|
+
const handlers = (layer.route.stack || []).map(sl => sl.handle).filter(Boolean);
|
|
150
|
+
for (const handler of handlers) {
|
|
151
|
+
bodyFields.push(...extractBodyFields(handler));
|
|
152
|
+
}
|
|
153
|
+
// Deduplicate
|
|
154
|
+
const seen = new Map();
|
|
155
|
+
bodyFields = bodyFields.filter(f => {
|
|
156
|
+
if (seen.has(f.name)) return false;
|
|
157
|
+
seen.set(f.name, true);
|
|
158
|
+
return true;
|
|
159
|
+
});
|
|
160
|
+
if (bodyFields.length === 0) {
|
|
161
|
+
bodyFields = fallbackFields(fullPath);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
detectedEndpoints[key] = {
|
|
166
|
+
method: httpMethod,
|
|
167
|
+
path: fullPath,
|
|
168
|
+
title: `${httpMethod} ${fullPath}`,
|
|
169
|
+
desc: `Auto-discovered endpoint — ${fullPath}`,
|
|
170
|
+
params: pathParams,
|
|
171
|
+
fields: bodyFields,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Nested router (app.use('/prefix', router)) ───────────────────────────
|
|
177
|
+
else if (layer.handle && typeof layer.handle === 'function' && layer.handle.stack) {
|
|
178
|
+
const routerPrefix = extractRouterPrefix(layer);
|
|
179
|
+
parseStack(layer.handle.stack, detectedEndpoints, prefix + routerPrefix);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─── Middleware ───────────────────────────────────────────────────────────────
|
|
185
|
+
function endtesterExpress() {
|
|
186
|
+
return function monkeyTesterMiddleware(req, res, next) {
|
|
187
|
+
// Normalize path: strip trailing slash, handle both req.path and req.url
|
|
188
|
+
const rawPath = (req.path || req.url || '').split('?')[0].replace(/\/+$/, '');
|
|
189
|
+
|
|
190
|
+
if (rawPath !== '/api/tester') {
|
|
191
|
+
return next();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const app = req.app;
|
|
195
|
+
|
|
196
|
+
// Wait a tick to ensure all routes are registered before scanning
|
|
197
|
+
// (handles edge cases where middleware is mounted before some routes)
|
|
198
|
+
const detectedEndpoints = {};
|
|
199
|
+
|
|
200
|
+
const rootStack =
|
|
201
|
+
(app._router && app._router.stack) || // Express 4
|
|
202
|
+
(app.router && app.router.stack) || // Express 5
|
|
203
|
+
[];
|
|
204
|
+
|
|
205
|
+
parseStack(rootStack, detectedEndpoints);
|
|
206
|
+
|
|
207
|
+
const html = getHtmlTemplate(detectedEndpoints);
|
|
208
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
209
|
+
return res.send(html);
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export { endtesterExpress };
|
package/monkey.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const { getHtmlTemplate } = require('./htmlTemplate');
|
|
4
4
|
|
|
5
|
-
// ─── Field type inference ─────────────────────────────────────────────────────
|
|
6
5
|
function inferType(name) {
|
|
7
6
|
const n = name.toLowerCase();
|
|
8
7
|
if (n.includes('email')) return 'email';
|
|
@@ -28,15 +27,12 @@ function buildField(name) {
|
|
|
28
27
|
};
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
// ─── Extract req.body fields from handler source ──────────────────────────────
|
|
32
30
|
function extractBodyFields(handler) {
|
|
33
31
|
try {
|
|
34
32
|
const source = handler.toString();
|
|
35
33
|
if (!source || source.includes('[native code]')) return [];
|
|
36
34
|
|
|
37
35
|
const seen = new Map();
|
|
38
|
-
|
|
39
|
-
// Pattern 1 — destructuring: const { email, password } = req.body
|
|
40
36
|
const destructRe = /(?:const|let|var)\s*\{\s*([^}]+)\s*\}\s*=\s*req\.body/g;
|
|
41
37
|
let m;
|
|
42
38
|
while ((m = destructRe.exec(source)) !== null) {
|
|
@@ -48,7 +44,6 @@ function extractBodyFields(handler) {
|
|
|
48
44
|
});
|
|
49
45
|
}
|
|
50
46
|
|
|
51
|
-
// Pattern 2 — property access: req.body.email / req.body['email']
|
|
52
47
|
const accessRe = /req\.body\.([a-zA-Z_$][a-zA-Z0-9_$]*)|req\.body\[['"]([a-zA-Z_$][a-zA-Z0-9_$]*)['"]]/g;
|
|
53
48
|
while ((m = accessRe.exec(source)) !== null) {
|
|
54
49
|
const name = m[1] || m[2];
|
|
@@ -61,67 +56,35 @@ function extractBodyFields(handler) {
|
|
|
61
56
|
}
|
|
62
57
|
}
|
|
63
58
|
|
|
64
|
-
// ─── Path-based fallback fields ───────────────────────────────────────────────
|
|
65
59
|
function fallbackFields(path) {
|
|
66
60
|
const p = path.toLowerCase();
|
|
67
|
-
|
|
68
|
-
if (p.includes('
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (p.includes('
|
|
72
|
-
return ['username', 'email', 'password'].map(buildField);
|
|
73
|
-
}
|
|
74
|
-
if (p.includes('user')) {
|
|
75
|
-
return ['username', 'email', 'password'].map(buildField);
|
|
76
|
-
}
|
|
77
|
-
if (p.includes('product')) {
|
|
78
|
-
return ['name', 'price', 'stock'].map(buildField);
|
|
79
|
-
}
|
|
80
|
-
if (p.includes('order')) {
|
|
81
|
-
return ['productId', 'quantity', 'address'].map(buildField);
|
|
82
|
-
}
|
|
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);
|
|
83
66
|
return [];
|
|
84
67
|
}
|
|
85
68
|
|
|
86
|
-
// ─── Extract router prefix safely from Express layer ─────────────────────────
|
|
87
69
|
function extractRouterPrefix(layer) {
|
|
88
|
-
// Prefer explicit path if available
|
|
89
|
-
if (layer.path && typeof layer.path === 'string') {
|
|
90
|
-
return layer.path === '/' ? '' : layer.path;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
70
|
if (!layer.regexp) return '';
|
|
94
|
-
|
|
95
|
-
// Convert the regexp back to a path prefix by looking at the regexp source
|
|
96
|
-
// Express generates regexps like: /^\/api\/v1\/?(?=\/|$)/i
|
|
97
71
|
const src = layer.regexp.source;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
// Unescape the extracted path
|
|
105
|
-
const raw = match[1].replace(/\\\//g, '/');
|
|
106
|
-
|
|
107
|
-
// Remove trailing slash if present
|
|
108
|
-
return raw.replace(/\/$/, '') || '';
|
|
72
|
+
const patterns = [/^\^\\\/([^\\?$]+)/, /^\^\\\/([a-zA-Z0-9_/-]+)/];
|
|
73
|
+
for (const re of patterns) {
|
|
74
|
+
const m = re.exec(src);
|
|
75
|
+
if (m && m[1]) return '/' + m[1].replace(/\\\//g, '/').replace(/\\/g, '');
|
|
76
|
+
}
|
|
77
|
+
return '';
|
|
109
78
|
}
|
|
110
79
|
|
|
111
|
-
// ─── Walk the Express router stack recursively ────────────────────────────────
|
|
112
80
|
function parseStack(stack, detectedEndpoints, prefix = '') {
|
|
113
81
|
if (!Array.isArray(stack)) return;
|
|
114
82
|
|
|
115
83
|
for (const layer of stack) {
|
|
116
|
-
// ── Named route (app.get / app.post …) ──────────────────────────────────
|
|
117
84
|
if (layer.route) {
|
|
118
|
-
const rawPath = typeof layer.route.path === 'string'
|
|
119
|
-
? layer.route.path
|
|
120
|
-
: (layer.route.path ? String(layer.route.path) : '');
|
|
121
|
-
|
|
85
|
+
const rawPath = typeof layer.route.path === 'string' ? layer.route.path : (layer.route.path ? String(layer.route.path) : '');
|
|
122
86
|
const fullPath = (prefix + rawPath).replace(/\/+/g, '/') || '/';
|
|
123
87
|
|
|
124
|
-
// Skip the tester route itself
|
|
125
88
|
if (fullPath.startsWith('/api/tester')) continue;
|
|
126
89
|
|
|
127
90
|
const methods = Object.keys(layer.route.methods || {});
|
|
@@ -130,27 +93,19 @@ function parseStack(stack, detectedEndpoints, prefix = '') {
|
|
|
130
93
|
const httpMethod = method.toUpperCase();
|
|
131
94
|
const key = `${httpMethod}::${fullPath}`;
|
|
132
95
|
|
|
133
|
-
// ── Path params (:id, :slug …) ────────────────────────────────────
|
|
134
96
|
const pathParams = [];
|
|
135
97
|
const paramRe = /:([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
pathParams.push({
|
|
140
|
-
name: pm[1],
|
|
141
|
-
label: pm[1].charAt(0).toUpperCase() + pm[1].slice(1),
|
|
142
|
-
placeholder: 'value'
|
|
143
|
-
});
|
|
98
|
+
let pm;
|
|
99
|
+
while ((pm = paramRe.exec(fullPath)) !== null) {
|
|
100
|
+
pathParams.push({ name: pm[1], label: pm[1].charAt(0).toUpperCase() + pm[1].slice(1), placeholder: 'value' });
|
|
144
101
|
}
|
|
145
102
|
|
|
146
|
-
// ── Body fields ──────────────────────────────────────────────────
|
|
147
103
|
let bodyFields = [];
|
|
148
104
|
if (['POST', 'PUT', 'PATCH'].includes(httpMethod)) {
|
|
149
105
|
const handlers = (layer.route.stack || []).map(sl => sl.handle).filter(Boolean);
|
|
150
106
|
for (const handler of handlers) {
|
|
151
107
|
bodyFields.push(...extractBodyFields(handler));
|
|
152
108
|
}
|
|
153
|
-
// Deduplicate
|
|
154
109
|
const seen = new Map();
|
|
155
110
|
bodyFields = bodyFields.filter(f => {
|
|
156
111
|
if (seen.has(f.name)) return false;
|
|
@@ -163,51 +118,37 @@ function parseStack(stack, detectedEndpoints, prefix = '') {
|
|
|
163
118
|
}
|
|
164
119
|
|
|
165
120
|
detectedEndpoints[key] = {
|
|
166
|
-
method:
|
|
167
|
-
path:
|
|
168
|
-
title:
|
|
169
|
-
desc:
|
|
170
|
-
params:
|
|
171
|
-
fields:
|
|
121
|
+
method: httpMethod,
|
|
122
|
+
path: fullPath,
|
|
123
|
+
title: `${httpMethod} ${fullPath}`,
|
|
124
|
+
desc: `Auto-discovered endpoint - ${fullPath}`, // Safe ASCII character
|
|
125
|
+
params: pathParams,
|
|
126
|
+
fields: bodyFields,
|
|
172
127
|
};
|
|
173
128
|
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// ── Nested router (app.use('/prefix', router)) ───────────────────────────
|
|
177
|
-
else if (layer.handle && typeof layer.handle === 'function' && layer.handle.stack) {
|
|
129
|
+
} else if (layer.name === 'router' && layer.handle && layer.handle.stack) {
|
|
178
130
|
const routerPrefix = extractRouterPrefix(layer);
|
|
179
131
|
parseStack(layer.handle.stack, detectedEndpoints, prefix + routerPrefix);
|
|
180
132
|
}
|
|
181
133
|
}
|
|
182
134
|
}
|
|
183
135
|
|
|
184
|
-
// ─── Middleware ───────────────────────────────────────────────────────────────
|
|
185
136
|
function endtesterExpress() {
|
|
186
137
|
return function monkeyTesterMiddleware(req, res, next) {
|
|
187
|
-
|
|
188
|
-
const rawPath = (req.path || req.url || '').split('?')[0].replace(/\/+$/, '');
|
|
189
|
-
|
|
190
|
-
if (rawPath !== '/api/tester') {
|
|
138
|
+
if (req.path !== '/api/tester' && req.path !== '/api/tester/') {
|
|
191
139
|
return next();
|
|
192
140
|
}
|
|
193
141
|
|
|
194
142
|
const app = req.app;
|
|
195
|
-
|
|
196
|
-
// Wait a tick to ensure all routes are registered before scanning
|
|
197
|
-
// (handles edge cases where middleware is mounted before some routes)
|
|
198
143
|
const detectedEndpoints = {};
|
|
199
144
|
|
|
200
|
-
const rootStack =
|
|
201
|
-
(app._router && app._router.stack) || // Express 4
|
|
202
|
-
(app.router && app.router.stack) || // Express 5
|
|
203
|
-
[];
|
|
204
|
-
|
|
145
|
+
const rootStack = (app._router && app._router.stack) || (app.router && app.router.stack) || [];
|
|
205
146
|
parseStack(rootStack, detectedEndpoints);
|
|
206
147
|
|
|
207
148
|
const html = getHtmlTemplate(detectedEndpoints);
|
|
208
|
-
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
149
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8'); // Forces browser to parse UTF-8 encoding safely
|
|
209
150
|
return res.send(html);
|
|
210
151
|
};
|
|
211
152
|
}
|
|
212
153
|
|
|
213
|
-
|
|
154
|
+
module.exports = { endtesterExpress };
|