@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 CHANGED
@@ -1,8 +1,9 @@
1
- export function getHtmlTemplate(endpoints) {
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
- --surface: #181510;
16
- --surface2: #221d14;
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
- body {
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
- #jwt-input, #base-url {
58
- background: var(--surface2); border: 1px solid var(--border); color: var(--text);
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
- .btn-secondary { background: var(--surface2); color: var(--text-dim); border: 1px solid var(--border); }
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
- .status-err { background: #3a1a14; color: #d45c3c; }
132
- .status-idle { background: var(--surface2); color: var(--text-dim); }
133
-
134
- /* FIXED: Body panel scrolls y natively, inner token element handles x scrolling */
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="endtester-data-vault" data-payload="${safeJsonString}" style="display: none;"></div>
68
+ <div id="__monkey_data__" data-payload="${safeJsonString}" style="display:none;"></div>
177
69
 
178
70
  <header>
179
- <div class="logo">Endtester <span>Application Runtime Sandbox</span></div>
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 rawPayload = document.getElementById('endtester-data-vault').getAttribute('data-payload');
210
- const ENDPOINTS = JSON.parse(atob(rawPayload));
101
+ const ENDPOINTS = JSON.parse(atob(document.getElementById('__monkey_data__').getAttribute('data-payload')));
102
+ let currentKey = null;
211
103
 
212
- let currentEp = '';
104
+ document.getElementById('base-url').value = window.location.origin;
213
105
 
214
- document.getElementById('base-url').value = window.location.origin;
106
+ function buildSidebar() {
107
+ const sidebar = document.getElementById('sidebar-nav');
108
+ const keys = Object.keys(ENDPOINTS);
215
109
 
216
- function buildSidebar() {
217
- const sidebar = document.getElementById('sidebar-nav');
218
- const keys = Object.keys(ENDPOINTS);
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
- if (keys.length === 0) {
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
- keys.forEach((key, index) => {
135
+ function renderPanel(key) {
136
+ currentKey = key;
226
137
  const ep = ENDPOINTS[key];
227
- const div = document.createElement('div');
228
- div.className = index === 0 ? 'nav-item active' : 'nav-item';
229
- div.setAttribute('data-ep', key);
230
- div.innerHTML = '<span class="method-badge ' + ep.method + '">' + ep.method + '</span><span class="nav-label">' + ep.path + '</span>';
231
- div.addEventListener('click', () => {
232
- document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
233
- div.classList.add('active');
234
- clearResponse();
235
- renderPanel(key);
236
- });
237
- sidebar.appendChild(div);
238
- });
239
-
240
- if (keys.length > 0) renderPanel(keys[0]);
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
- function makeInputString(type, id, placeholder) {
244
- const pAttr = placeholder ? ' placeholder="' + placeholder + '"' : '';
245
- return '<input type="' + type + '" id="' + id + '"' + pAttr + ' />';
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
- function renderPanel(epKey) {
249
- currentEp = epKey;
250
- const ep = ENDPOINTS[epKey];
251
- const main = document.getElementById('main-panel');
252
- if (!ep) return;
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
- if (ep.fields && ep.fields.length) {
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
- html += \`
292
- <div class="btn-row">
293
- <button class="btn" onclick="sendRequest()">Execute Route</button>
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
- main.innerHTML = html;
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
- async function sendRequest() {
302
- const ep = ENDPOINTS[currentEp];
303
- let path = ep.path;
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
- if (ep.params) {
306
- for (const p of ep.params) {
307
- const val = document.getElementById('param-' + p.name)?.value.trim();
308
- if (!val) { showToast('Warning: Parameter ' + p.label + ' is required'); return; }
309
- path = path.replace(':' + p.name, encodeURIComponent(val));
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
- const baseUrl = document.getElementById('base-url').value.replace(/[/]+$/, '');
314
- const url = baseUrl + path;
315
- const headers = { 'Content-Type': 'application/json' };
316
-
317
- const jwt = document.getElementById('jwt-input').value.trim();
318
- if (jwt) headers['Authorization'] = 'Bearer ' + jwt;
319
-
320
- let body = undefined;
321
- if (ep.fields && ep.fields.length && ['POST', 'PUT', 'PATCH'].includes(ep.method)) {
322
- const jsonPayload = {};
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
- setResponse(null, 'loading');
337
- const start = Date.now();
338
-
339
- try {
340
- const res = await fetch(url, { method: ep.method, headers, body });
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
- if (state === 'loading') {
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.innerHTML = 'Executing transmission...';
360
- return;
250
+ body.textContent = 'Execute a request row to generate feedback data';
361
251
  }
362
252
 
363
- badge.className = 'status-badge ' + (state === 'ok' ? 'status-ok' : 'status-err');
364
- badge.textContent = status + ' · ' + ms + 'ms';
365
- body.className = 'response-body';
366
-
367
- // FIXED: Nested rendering inside dedicated horizontal scrolling code tag block wrapper elements
368
- const outputString = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
369
- body.innerHTML = '<pre class="json-render-block">' + highlightJson(outputString) + '</pre>';
370
- }
371
-
372
- function clearResponse() {
373
- const badge = document.getElementById('status-badge');
374
- const body = document.getElementById('response-body');
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, '&amp;').replace(/[<]/g, '&lt;').replace(/[>]/g, '&gt;')
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
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
- const t = document.getElementById('toast');
395
- t.textContent = msg;
396
- t.classList.add('show');
397
- setTimeout(() => t.classList.remove('show'), 2500);
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 };
@@ -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
- import { getHtmlTemplate } from './htmlTemplate.js';
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('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
- }
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
- // 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(/\/$/, '') || '';
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
- 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
- });
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: httpMethod,
167
- path: fullPath,
168
- title: `${httpMethod} ${fullPath}`,
169
- desc: `Auto-discovered endpoint ${fullPath}`,
170
- params: pathParams,
171
- fields: bodyFields,
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
- // 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') {
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
- export { endtesterExpress };
154
+ module.exports = { endtesterExpress };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aimeloic/monkey-tester",
3
- "version": "3.0.8",
3
+ "version": "3.0.10",
4
4
  "description": "Auto route scanning visual runner dashboard.",
5
5
  "main": "index.js",
6
6
  "type": "module",