@aimeloic/monkey-tester 4.0.3 → 4.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/html.backup.js CHANGED
@@ -1,103 +1,8 @@
1
1
  'use strict';
2
2
 
3
- function getHtmlTemplate(endpoints) {
4
- const safeJsonString = Buffer.from(JSON.stringify(endpoints)).toString('base64');
5
-
6
- return `<!DOCTYPE html>
7
- <html lang="en">
8
- <head>
9
- <meta charset="UTF-8">
10
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
11
- <title>Endtester — API Environment</title>
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">
13
- <style>
14
- :root {
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;
18
- }
19
- * { box-sizing: border-box; margin: 0; padding: 0; }
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; }
22
- .logo { font-family: 'Playfair Display', serif; font-size: 20px; color: var(--accent); letter-spacing: 0.02em; }
23
- .logo span { color: var(--text-dim); font-size: 11px; font-family: 'DM Mono', monospace; display: inline-block; margin-left: 8px; font-weight: 400; }
24
- .header-right { margin-left: auto; display: flex; align-items: center; gap: 16px; }
25
- .jwt-wrap, .base-url-wrap { display: flex; align-items: center; gap: 8px; }
26
- .jwt-wrap label, .base-url-wrap label { color: var(--text-dim); font-size: 11px; font-family: 'DM Mono', monospace; letter-spacing: 0.05em; }
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; }
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; }
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; }
32
- .nav-item:hover { background: var(--surface); color: var(--text); }
33
- .nav-item.active { border-left-color: var(--accent); background: var(--surface); color: var(--accent); }
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; }
35
- .GET { background: #1a3a22; color: #6ba05a; } .POST { background: #1a2e3a; color: #5a86c0; } .PUT, .PATCH { background: #3a2e10; color: #e8a838; } .DELETE { background: #3a1a14; color: #d45c3c; }
36
- .nav-label { font-size: 12px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
37
- main { overflow-y: auto; padding: 32px; background: #0e0c09; }
38
- .endpoint-title { font-family: 'Playfair Display', serif; font-size: 24px; color: var(--accent); margin-bottom: 8px; }
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; }
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; }
41
- .form-section { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; margin-bottom: 20px; }
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; }
43
- .field-row { display: grid; grid-template-columns: 150px 1fr; align-items: center; gap: 16px; margin-bottom: 14px; }
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; }
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; }
46
- input:focus { border-color: var(--accent); }
47
- .btn-row { margin-top: 24px; display: flex; gap: 12px; }
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; }
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; }
51
- .response-header { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; background: var(--surface); height: 50px; }
52
- .response-header-title { font-size: 11px; font-family: 'DM Mono', monospace; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.05em; }
53
- .status-badge { font-family: 'DM Mono', monospace; font-size: 12px; margin-left: auto; padding: 2px 8px; border-radius: 4px; font-weight: 500; }
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); }
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); }
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; }
64
- </style>
65
- </head>
66
- <body>
67
-
68
- <div id="__monkey_data__" data-payload="${safeJsonString}" style="display:none;"></div>
69
-
70
- <header>
71
- <div class="logo">🐒 Endtester <span>Application Runtime Sandbox</span></div>
72
- <div class="header-right">
73
- <div class="base-url-wrap">
74
- <label>TARGET HOST</label>
75
- <input id="base-url" type="text" value="">
76
- </div>
77
- <div class="jwt-wrap">
78
- <label>BEARER AUTH</label>
79
- <input id="jwt-input" type="text" placeholder="Token value...">
80
- </div>
81
- </div>
82
- </header>
83
-
84
- <div class="layout">
85
- <aside id="sidebar-nav">
86
- <div class="section-label">Discovered Endpoints</div>
87
- </aside>
88
- <main id="main-panel"></main>
89
- <div class="response-panel">
90
- <div class="response-header">
91
- <span class="response-header-title">Response Output</span>
92
- <span id="status-badge" class="status-badge status-idle">—</span>
93
- </div>
94
- <div class="response-body empty" id="response-body">Execute a request row to generate feedback data</div>
95
- </div>
96
- </div>
97
-
98
- <div id="toast"></div>
99
-
100
- <script>
3
+ // ─── BROWSER RUNTIME ENGINE ──────────────────────────────────────────────────
4
+ // This function is stringified safely to eliminate string template syntax bugs
5
+ function runtimeClientSandbox() {
101
6
  const ENDPOINTS = JSON.parse(atob(document.getElementById('__monkey_data__').getAttribute('data-payload')));
102
7
  let currentKey = null;
103
8
 
@@ -170,11 +75,14 @@ function getHtmlTemplate(endpoints) {
170
75
 
171
76
  html +=
172
77
  '<div class="btn-row">' +
173
- '<button class="btn" onclick="sendRequest()">Execute Route</button>' +
174
- '<button class="btn btn-secondary" onclick="clearResponse()">Clear Context</button>' +
78
+ '<button class="btn" id="_exec_btn">Execute Route</button>' +
79
+ '<button class="btn btn-secondary" id="_clear_btn">Clear Context</button>' +
175
80
  '</div>';
176
81
 
177
82
  main.innerHTML = html;
83
+
84
+ document.getElementById('_exec_btn').addEventListener('click', sendRequest);
85
+ document.getElementById('_clear_btn').addEventListener('click', clearResponse);
178
86
  }
179
87
 
180
88
  async function sendRequest() {
@@ -271,9 +179,109 @@ function getHtmlTemplate(endpoints) {
271
179
  }
272
180
 
273
181
  buildSidebar();
182
+ }
183
+
184
+ // ─── BACKEND HTML GENERATOR ──────────────────────────────────────────────────
185
+ function getHtmlTemplate(endpoints) {
186
+ const safeJsonString = Buffer.from(JSON.stringify(endpoints)).toString('base64');
187
+
188
+ return `<!DOCTYPE html>
189
+ <html lang="en">
190
+ <head>
191
+ <meta charset="UTF-8">
192
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
193
+ <title>Endtester — API Environment</title>
194
+ <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">
195
+ <style>
196
+ :root {
197
+ --bg: #0e0c09; --surface: #181510; --surface2: #221d14; --border: #3a3020;
198
+ --accent: #e8a838; --accent2: #c47a1e; --text: #f0e8d8; --text-dim: #9a8c78;
199
+ --red: #d45c3c; --green: #6ba05a; --blue: #5a86c0; --radius: 8px;
200
+ }
201
+ * { box-sizing: border-box; margin: 0; padding: 0; }
202
+ 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%); }
203
+ 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; }
204
+ .logo { font-family: 'Playfair Display', serif; font-size: 20px; color: var(--accent); letter-spacing: 0.02em; }
205
+ .logo span { color: var(--text-dim); font-size: 11px; font-family: 'DM Mono', monospace; display: inline-block; margin-left: 8px; font-weight: 400; }
206
+ .header-right { margin-left: auto; display: flex; align-items: center; gap: 16px; }
207
+ .jwt-wrap, .base-url-wrap { display: flex; align-items: center; gap: 8px; }
208
+ .jwt-wrap label, .base-url-wrap label { color: var(--text-dim); font-size: 11px; font-family: 'DM Mono', monospace; letter-spacing: 0.05em; }
209
+ #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; }
210
+ .layout { display: grid; grid-template-columns: 280px 1fr 450px; height: calc(100vh - 65px); overflow: hidden; }
211
+ aside { border-right: 1px solid var(--border); overflow-y: auto; padding: 16px 0; background: #0b0907; }
212
+ .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; }
213
+ .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; }
214
+ .nav-item:hover { background: var(--surface); color: var(--text); }
215
+ .nav-item.active { border-left-color: var(--accent); background: var(--surface); color: var(--accent); }
216
+ .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; }
217
+ .GET { background: #1a3a22; color: #6ba05a; } .POST { background: #1a2e3a; color: #5a86c0; } .PUT, .PATCH { background: #3a2e10; color: #e8a838; } .DELETE { background: #3a1a14; color: #d45c3c; }
218
+ .nav-label { font-size: 12px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
219
+ main { overflow-y: auto; padding: 32px; background: #0e0c09; }
220
+ .endpoint-title { font-family: 'Playfair Display', serif; font-size: 24px; color: var(--accent); margin-bottom: 8px; }
221
+ .endpoint-path { font-family: 'DM Mono', monospace; font-size: 13px; color: var(--text-dim); margin-bottom: 24px; display: flex; align-items: center; gap: 8px; }
222
+ .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; }
223
+ .form-section { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; margin-bottom: 20px; }
224
+ .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; }
225
+ .field-row { display: grid; grid-template-columns: 150px 1fr; align-items: center; gap: 16px; margin-bottom: 14px; }
226
+ .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; }
227
+ 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; }
228
+ input:focus { border-color: var(--accent); }
229
+ .btn-row { margin-top: 24px; display: flex; gap: 12px; }
230
+ .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; }
231
+ .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); }
232
+ .response-panel { border-left: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; background: #110e0a; }
233
+ .response-header { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; background: var(--surface); height: 50px; }
234
+ .response-header-title { font-size: 11px; font-family: 'DM Mono', monospace; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.05em; }
235
+ .status-badge { font-family: 'DM Mono', monospace; font-size: 12px; margin-left: auto; padding: 2px 8px; border-radius: 4px; font-weight: 500; }
236
+ .status-ok { background: #1a3a22; color: #6ba05a; } .status-err { background: #3a1a14; color: #d45c3c; } .status-idle { background: var(--surface2); color: var(--text-dim); }
237
+ .response-body { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 0; background: #0d0b08; }
238
+ .response-body.empty { color: var(--text-dim); display: flex; align-items: center; justify-content: center; padding: 20px; text-align: center; font-size: 13px; }
239
+ .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; }
240
+ .json-key { color: #e8a838; } .json-str { color: #9ab878; } .json-num { color: #5a86c0; } .json-bool { color: #c47a1e; } .json-null { color: var(--text-dim); }
241
+ #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); }
242
+ #toast.show { opacity: 1; }
243
+ </style>
244
+ </head>
245
+ <body>
246
+
247
+ <div id="__monkey_data__" data-payload="${safeJsonString}" style="display:none;"></div>
248
+
249
+ <header>
250
+ <div class="logo">🐒 Endtester <span>Application Runtime Sandbox</span></div>
251
+ <div class="header-right">
252
+ <div class="base-url-wrap">
253
+ <label>TARGET HOST</label>
254
+ <input id="base-url" type="text" value="">
255
+ </div>
256
+ <div class="jwt-wrap">
257
+ <label>BEARER AUTH</label>
258
+ <input id="jwt-input" type="text" placeholder="Token value...">
259
+ </div>
260
+ </div>
261
+ </header>
262
+
263
+ <div class="layout">
264
+ <aside id="sidebar-nav">
265
+ <div class="section-label">Discovered Endpoints</div>
266
+ </aside>
267
+ <main id="main-panel"></main>
268
+ <div class="response-panel">
269
+ <div class="response-header">
270
+ <span class="response-header-title">Response Output</span>
271
+ <span id="status-badge" class="status-badge status-idle">—</span>
272
+ </div>
273
+ <div class="response-body empty" id="response-body">Execute a request row to generate feedback data</div>
274
+ </div>
275
+ </div>
276
+
277
+ <div id="toast"></div>
278
+
279
+ <script>
280
+ // Clean, flawless evaluation structure
281
+ (${runtimeClientSandbox.toString()})();
274
282
  </script>
275
283
  </body>
276
284
  </html>`;
277
285
  }
278
286
 
279
- module.exports = { getHtmlTemplate };
287
+ export { getHtmlTemplate };
package/htmlTemplate.js CHANGED
@@ -1,30 +1,31 @@
1
1
  'use strict';
2
2
 
3
- // ─── BROWSER RUNTIME ENGINE ──────────────────────────────────────────────────
4
- // This function is stringified safely to eliminate string template syntax bugs
3
+ // Original Sandbox Runtime Stringified safely to browser context
5
4
  function runtimeClientSandbox() {
6
5
  const ENDPOINTS = JSON.parse(atob(document.getElementById('__monkey_data__').getAttribute('data-payload')));
7
6
  let currentKey = null;
8
7
 
9
8
  document.getElementById('base-url').value = window.location.origin;
10
9
 
10
+ // Auto-paste Token directly if it exists in local storage from the turnkey login page!
11
+ const savedToken = localStorage.getItem('__auth_token__');
12
+ if (savedToken) {
13
+ document.getElementById('jwt-input').value = savedToken;
14
+ }
15
+
11
16
  function buildSidebar() {
12
17
  const sidebar = document.getElementById('sidebar-nav');
13
18
  const keys = Object.keys(ENDPOINTS);
14
-
15
19
  if (keys.length === 0) {
16
20
  sidebar.innerHTML += '<div style="padding:18px;color:var(--text-dim);font-size:12px">No endpoints discovered.</div>';
17
21
  return;
18
22
  }
19
-
20
23
  keys.forEach((key, i) => {
21
24
  const ep = ENDPOINTS[key];
22
25
  const item = document.createElement('div');
23
26
  item.className = 'nav-item' + (i === 0 ? ' active' : '');
24
27
  item.setAttribute('data-key', key);
25
- item.innerHTML =
26
- '<span class="method-badge ' + ep.method + '">' + ep.method + '</span>' +
27
- '<span class="nav-label">' + ep.path + '</span>';
28
+ item.innerHTML = '<span class="method-badge ' + ep.method + '">' + ep.method + '</span><span class="nav-label">' + ep.path + '</span>';
28
29
  item.addEventListener('click', () => {
29
30
  document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
30
31
  item.classList.add('active');
@@ -33,7 +34,6 @@ function runtimeClientSandbox() {
33
34
  });
34
35
  sidebar.appendChild(item);
35
36
  });
36
-
37
37
  renderPanel(keys[0]);
38
38
  }
39
39
 
@@ -43,44 +43,28 @@ function runtimeClientSandbox() {
43
43
  const main = document.getElementById('main-panel');
44
44
  if (!ep) return;
45
45
 
46
- let html =
47
- '<div class="endpoint-title">' + ep.title + '</div>' +
48
- '<div class="endpoint-path"><span class="method-badge ' + ep.method + '">' + ep.method + '</span>' +
49
- '<span>' + ep.path + '</span></div>' +
50
- '<div class="endpoint-desc">' + ep.desc + '</div>';
46
+ let html = '<div class="endpoint-title">' + ep.title + '</div>' +
47
+ '<div class="endpoint-path"><span class="method-badge ' + ep.method + '">' + ep.method + '</span><span>' + ep.path + '</span></div>' +
48
+ '<div class="endpoint-desc">' + ep.desc + '</div>';
51
49
 
52
50
  if (ep.params && ep.params.length) {
53
51
  html += '<div class="form-section"><div class="form-section-title">Path Parameters</div>';
54
- ep.params.forEach(function(p) {
55
- html +=
56
- '<div class="field-row">' +
57
- '<label class="field-label">' + p.label + '</label>' +
58
- '<input type="text" id="param-' + p.name + '" placeholder="' + p.placeholder + '" />' +
59
- '</div>';
52
+ ep.params.forEach(p => {
53
+ html += '<div class="field-row"><label class="field-label">' + p.label + '</label><input type="text" id="param-' + p.name + '" placeholder="' + p.placeholder + '" /></div>';
60
54
  });
61
55
  html += '</div>';
62
56
  }
63
57
 
64
58
  if (ep.fields && ep.fields.length) {
65
- html += '<div class="form-section"><div class="form-section-title">HTTP JSON Request Payload Parameters</div>';
66
- ep.fields.forEach(function(f) {
67
- html +=
68
- '<div class="field-row">' +
69
- '<label class="field-label">' + f.label + '</label>' +
70
- '<input type="' + (f.type || 'text') + '" id="field-' + f.name + '" placeholder="' + (f.placeholder || '') + '" />' +
71
- '</div>';
59
+ html += '<div class="form-section"><div class="form-section-title">HTTP JSON Payload Parameters</div>';
60
+ ep.fields.forEach(f => {
61
+ html += '<div class="field-row"><label class="field-label">' + f.label + '</label><input type="' + (f.type || 'text') + '" id="field-' + f.name + '" placeholder="' + (f.placeholder || '') + '" /></div>';
72
62
  });
73
63
  html += '</div>';
74
64
  }
75
65
 
76
- html +=
77
- '<div class="btn-row">' +
78
- '<button class="btn" id="_exec_btn">Execute Route</button>' +
79
- '<button class="btn btn-secondary" id="_clear_btn">Clear Context</button>' +
80
- '</div>';
81
-
66
+ html += '<div class="btn-row"><button class="btn" id="_exec_btn">Execute Route</button><button class="btn btn-secondary" id="_clear_btn">Clear Context</button></div>';
82
67
  main.innerHTML = html;
83
-
84
68
  document.getElementById('_exec_btn').addEventListener('click', sendRequest);
85
69
  document.getElementById('_clear_btn').addEventListener('click', clearResponse);
86
70
  }
@@ -88,7 +72,6 @@ function runtimeClientSandbox() {
88
72
  async function sendRequest() {
89
73
  const ep = ENDPOINTS[currentKey];
90
74
  let path = ep.path;
91
-
92
75
  if (ep.params && ep.params.length) {
93
76
  for (const p of ep.params) {
94
77
  const val = (document.getElementById('param-' + p.name) || {}).value || '';
@@ -96,7 +79,6 @@ function runtimeClientSandbox() {
96
79
  path = path.replace(':' + p.name, encodeURIComponent(val.trim()));
97
80
  }
98
81
  }
99
-
100
82
  const baseUrl = document.getElementById('base-url').value.replace(/\/+$/, '');
101
83
  const url = baseUrl + path;
102
84
  const headers = { 'Content-Type': 'application/json' };
@@ -106,7 +88,7 @@ function runtimeClientSandbox() {
106
88
  let body = undefined;
107
89
  if (['POST', 'PUT', 'PATCH'].includes(ep.method) && ep.fields && ep.fields.length) {
108
90
  const payload = {};
109
- ep.fields.forEach(function(f) {
91
+ ep.fields.forEach(f => {
110
92
  const el = document.getElementById('field-' + f.name);
111
93
  if (!el) return;
112
94
  let v = el.value.trim();
@@ -118,13 +100,11 @@ function runtimeClientSandbox() {
118
100
 
119
101
  setResponse(null, 'loading');
120
102
  const t0 = Date.now();
121
-
122
103
  try {
123
104
  const res = await fetch(url, { method: ep.method, headers, body });
124
105
  const ms = Date.now() - t0;
125
106
  const text = await res.text();
126
- let data;
127
- try { data = JSON.parse(text); } catch { data = text; }
107
+ let data; try { data = JSON.parse(text); } catch { data = text; }
128
108
  setResponse(data, res.ok ? 'ok' : 'err', res.status, ms);
129
109
  } catch (err) {
130
110
  setResponse({ error: err.message }, 'err', 'FAIL', 0);
@@ -133,16 +113,12 @@ function runtimeClientSandbox() {
133
113
 
134
114
  function setResponse(data, state, status, ms) {
135
115
  const badge = document.getElementById('status-badge');
136
- const body = document.getElementById('response-body');
137
-
116
+ const body = document.getElementById('response-body');
138
117
  if (state === 'loading') {
139
- badge.className = 'status-badge status-idle';
140
- badge.textContent = '…';
141
- body.className = 'response-body empty';
142
- body.textContent = 'Executing transmission…';
118
+ badge.className = 'status-badge status-idle'; badge.textContent = '…';
119
+ body.className = 'response-body empty'; body.textContent = 'Executing transmission…';
143
120
  return;
144
121
  }
145
-
146
122
  badge.className = 'status-badge ' + (state === 'ok' ? 'status-ok' : 'status-err');
147
123
  badge.textContent = status + ' · ' + ms + 'ms';
148
124
  body.className = 'response-body';
@@ -154,134 +130,150 @@ function runtimeClientSandbox() {
154
130
  document.getElementById('status-badge').className = 'status-badge status-idle';
155
131
  document.getElementById('status-badge').textContent = '—';
156
132
  const body = document.getElementById('response-body');
157
- body.className = 'response-body empty';
158
- body.textContent = 'Execute a request row to generate feedback data';
133
+ body.className = 'response-body empty'; body.textContent = 'Execute a request row to generate feedback data';
159
134
  }
160
135
 
161
136
  function highlight(str) {
162
- return str
163
- .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
137
+ return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
164
138
  .replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false)\b|\bnull\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function(m) {
165
- if (/^"/.test(m)) return /:$/.test(m)
166
- ? '<span class="json-key">' + m + '</span>'
167
- : '<span class="json-str">' + m + '</span>';
139
+ if (/^"/.test(m)) return /:$/.test(m) ? '<span class="json-key">' + m + '</span>' : '<span class="json-str">' + m + '</span>';
168
140
  if (/true|false/.test(m)) return '<span class="json-bool">' + m + '</span>';
169
- if (/null/.test(m)) return '<span class="json-null">' + m + '</span>';
141
+ if (/null/.test(m)) return '<span class="json-null">' + m + '</span>';
170
142
  return '<span class="json-num">' + m + '</span>';
171
143
  });
172
144
  }
173
145
 
174
146
  function showToast(msg) {
175
- const t = document.getElementById('toast');
176
- t.textContent = msg;
177
- t.classList.add('show');
147
+ const t = document.getElementById('toast'); t.textContent = msg; t.classList.add('show');
178
148
  setTimeout(() => t.classList.remove('show'), 2500);
179
149
  }
180
150
 
181
151
  buildSidebar();
182
152
  }
183
153
 
184
- // ─── BACKEND HTML GENERATOR ──────────────────────────────────────────────────
185
- function getHtmlTemplate(endpoints) {
186
- const safeJsonString = Buffer.from(JSON.stringify(endpoints)).toString('base64');
187
-
188
- return `<!DOCTYPE html>
154
+ // Global UI layouts object
155
+ const UI = {
156
+ // Original Tester UI Page
157
+ tester: (endpointsJsonB64) => `<!DOCTYPE html>
189
158
  <html lang="en">
190
159
  <head>
191
- <meta charset="UTF-8">
192
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
193
- <title>Endtester — API Environment</title>
160
+ <meta charset="UTF-8"><title>Endtester — Environment Hub</title>
194
161
  <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">
195
162
  <style>
196
- :root {
197
- --bg: #0e0c09; --surface: #181510; --surface2: #221d14; --border: #3a3020;
198
- --accent: #e8a838; --accent2: #c47a1e; --text: #f0e8d8; --text-dim: #9a8c78;
199
- --red: #d45c3c; --green: #6ba05a; --blue: #5a86c0; --radius: 8px;
200
- }
163
+ :root { --bg: #0e0c09; --surface: #181510; --surface2: #221d14; --border: #3a3020; --accent: #e8a838; --text: #f0e8d8; --text-dim: #9a8c78; --red: #d45c3c; --green: #6ba05a; --blue: #5a86c0; --radius: 8px; }
201
164
  * { box-sizing: border-box; margin: 0; padding: 0; }
202
165
  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%); }
203
166
  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; }
204
- .logo { font-family: 'Playfair Display', serif; font-size: 20px; color: var(--accent); letter-spacing: 0.02em; }
205
- .logo span { color: var(--text-dim); font-size: 11px; font-family: 'DM Mono', monospace; display: inline-block; margin-left: 8px; font-weight: 400; }
167
+ .logo { font-family: 'Playfair Display', serif; font-size: 20px; color: var(--accent); }
168
+ .logo span { color: var(--text-dim); font-size: 11px; font-family: 'DM Mono', monospace; margin-left: 8px; }
206
169
  .header-right { margin-left: auto; display: flex; align-items: center; gap: 16px; }
207
170
  .jwt-wrap, .base-url-wrap { display: flex; align-items: center; gap: 8px; }
208
- .jwt-wrap label, .base-url-wrap label { color: var(--text-dim); font-size: 11px; font-family: 'DM Mono', monospace; letter-spacing: 0.05em; }
171
+ .jwt-wrap label, .base-url-wrap label { color: var(--text-dim); font-size: 11px; font-family: 'DM Mono', monospace; }
209
172
  #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; }
210
173
  .layout { display: grid; grid-template-columns: 280px 1fr 450px; height: calc(100vh - 65px); overflow: hidden; }
211
174
  aside { border-right: 1px solid var(--border); overflow-y: auto; padding: 16px 0; background: #0b0907; }
212
- .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; }
213
- .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; }
214
- .nav-item:hover { background: var(--surface); color: var(--text); }
175
+ .section-label { font-size: 10px; font-family: 'DM Mono', monospace; color: var(--text-dim); text-transform: uppercase; padding: 12px 18px 6px; }
176
+ .nav-item { display: flex; align-items: center; gap: 10px; padding: 10px 18px; cursor: pointer; border-left: 2px solid transparent; color: var(--text-dim); }
215
177
  .nav-item.active { border-left-color: var(--accent); background: var(--surface); color: var(--accent); }
216
- .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; }
217
- .GET { background: #1a3a22; color: #6ba05a; } .POST { background: #1a2e3a; color: #5a86c0; } .PUT, .PATCH { background: #3a2e10; color: #e8a838; } .DELETE { background: #3a1a14; color: #d45c3c; }
178
+ .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; }
179
+ .GET { background: #1a3a22; color: #6ba05a; } .POST { background: #1a2e3a; color: #5a86c0; } .PUT { background: #3a2e10; color: #e8a838; } .DELETE { background: #3a1a14; color: #d45c3c; }
218
180
  .nav-label { font-size: 12px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
219
181
  main { overflow-y: auto; padding: 32px; background: #0e0c09; }
220
182
  .endpoint-title { font-family: 'Playfair Display', serif; font-size: 24px; color: var(--accent); margin-bottom: 8px; }
221
183
  .endpoint-path { font-family: 'DM Mono', monospace; font-size: 13px; color: var(--text-dim); margin-bottom: 24px; display: flex; align-items: center; gap: 8px; }
222
184
  .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; }
223
185
  .form-section { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; margin-bottom: 20px; }
224
- .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; }
186
+ .form-section-title { font-size: 11px; font-family: 'DM Mono', monospace; color: var(--text-dim); text-transform: uppercase; margin-bottom: 16px; border-bottom: 1px solid var(--border); padding-bottom: 6px; }
225
187
  .field-row { display: grid; grid-template-columns: 150px 1fr; align-items: center; gap: 16px; margin-bottom: 14px; }
226
- .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; }
227
- 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; }
228
- input:focus { border-color: var(--accent); }
188
+ .field-label { font-family: 'DM Mono', monospace; font-size: 12px; color: var(--text-dim); text-align: right; }
189
+ input[type=text], input[type=password], input[type=number] { background: var(--surface2); border: 1px solid var(--border); color: var(--text); font-size: 13px; padding: 8px 12px; border-radius: var(--radius); width: 100%; outline: none; }
229
190
  .btn-row { margin-top: 24px; display: flex; gap: 12px; }
230
- .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; }
231
- .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); }
232
- .response-panel { border-left: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; background: #110e0a; }
191
+ .btn { background: var(--accent); color: #0e0c09; border: none; padding: 10px 24px; border-radius: var(--radius); font-size: 13px; font-weight: 500; cursor: pointer; }
192
+ .btn-secondary { background: var(--surface2); color: var(--text-dim); border: 1px solid var(--border); }
193
+ .response-panel { border-left: 1px solid var(--border); display: flex; flex-direction: column; background: #110e0a; }
233
194
  .response-header { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; background: var(--surface); height: 50px; }
234
- .response-header-title { font-size: 11px; font-family: 'DM Mono', monospace; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.05em; }
235
- .status-badge { font-family: 'DM Mono', monospace; font-size: 12px; margin-left: auto; padding: 2px 8px; border-radius: 4px; font-weight: 500; }
195
+ .response-header-title { font-size: 11px; font-family: 'DM Mono', monospace; color: var(--text-dim); text-transform: uppercase; }
196
+ .status-badge { font-family: 'DM Mono', monospace; font-size: 12px; margin-left: auto; padding: 2px 8px; border-radius: 4px; }
236
197
  .status-ok { background: #1a3a22; color: #6ba05a; } .status-err { background: #3a1a14; color: #d45c3c; } .status-idle { background: var(--surface2); color: var(--text-dim); }
237
- .response-body { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 0; background: #0d0b08; }
238
- .response-body.empty { color: var(--text-dim); display: flex; align-items: center; justify-content: center; padding: 20px; text-align: center; font-size: 13px; }
239
- .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; }
198
+ .response-body { flex: 1; overflow-y: auto; padding: 0; background: #0d0b08; }
199
+ .response-body.empty { color: var(--text-dim); display: flex; align-items: center; justify-content: center; padding: 20px; font-size: 13px; }
200
+ .json-render-block { display: block; padding: 20px; font-family: 'DM Mono', monospace; font-size: 12px; line-height: 1.5; white-space: pre; }
240
201
  .json-key { color: #e8a838; } .json-str { color: #9ab878; } .json-num { color: #5a86c0; } .json-bool { color: #c47a1e; } .json-null { color: var(--text-dim); }
241
- #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); }
202
+ #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; font-family: 'DM Mono', monospace; font-size: 12px; color: var(--accent); }
242
203
  #toast.show { opacity: 1; }
243
204
  </style>
244
205
  </head>
245
206
  <body>
246
-
247
- <div id="__monkey_data__" data-payload="${safeJsonString}" style="display:none;"></div>
248
-
207
+ <div id="__monkey_data__" data-payload="${endpointsJsonB64}" style="display:none;"></div>
249
208
  <header>
250
- <div class="logo">🐒 Endtester <span>Application Runtime Sandbox</span></div>
209
+ <div class="logo">🐒 Endtester <span>Interactive API Hub</span></div>
251
210
  <div class="header-right">
252
- <div class="base-url-wrap">
253
- <label>TARGET HOST</label>
254
- <input id="base-url" type="text" value="">
255
- </div>
256
- <div class="jwt-wrap">
257
- <label>BEARER AUTH</label>
258
- <input id="jwt-input" type="text" placeholder="Token value...">
259
- </div>
211
+ <div class="base-url-wrap"><label>HOST</label><input id="base-url" type="text" value=""></div>
212
+ <div class="jwt-wrap"><label>BEARER AUTH</label><input id="jwt-input" type="text" placeholder="Token value..."></div>
260
213
  </div>
261
214
  </header>
262
-
263
215
  <div class="layout">
264
- <aside id="sidebar-nav">
265
- <div class="section-label">Discovered Endpoints</div>
266
- </aside>
216
+ <aside id="sidebar-nav"><div class="section-label">Discovered Endpoints</div></aside>
267
217
  <main id="main-panel"></main>
268
218
  <div class="response-panel">
269
- <div class="response-header">
270
- <span class="response-header-title">Response Output</span>
271
- <span id="status-badge" class="status-badge status-idle">—</span>
272
- </div>
219
+ <div class="response-header"><span class="response-header-title">Response Output</span><span id="status-badge" class="status-badge status-idle">—</span></div>
273
220
  <div class="response-body empty" id="response-body">Execute a request row to generate feedback data</div>
274
221
  </div>
275
222
  </div>
276
-
277
223
  <div id="toast"></div>
278
-
279
- <script>
280
- // Clean, flawless evaluation structure
281
- (${runtimeClientSandbox.toString()})();
282
- </script>
224
+ <script>(${runtimeClientSandbox.toString()})();</script>
283
225
  </body>
284
- </html>`;
285
- }
286
-
287
- export { getHtmlTemplate };
226
+ </html>`,
227
+
228
+ // Turnkey Customer Login Page
229
+ login: () => `<!DOCTYPE html><html><head><title>Sign In</title><link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet">
230
+ <style>body { background: #0e0c09; color: #f0e8d8; font-family: 'DM Sans', sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }
231
+ .card { background: #181510; border: 1px solid #3a3020; padding: 40px; border-radius: 12px; width: 340px; } h2 { color: #e8a838; margin: 0 0 24px; text-align: center; }
232
+ .field { margin-bottom: 20px; } label { display: block; font-size: 11px; color: #9a8c78; text-transform: uppercase; margin-bottom: 8px; }
233
+ input { background: #221d14; border: 1px solid #3a3020; color: #f0e8d8; padding: 12px; width: 100%; box-sizing: border-box; border-radius: 6px; outline: none; }
234
+ button { background: #e8a838; color: #0e0c09; border: none; padding: 12px; width: 100%; border-radius: 6px; font-weight: 600; cursor: pointer; }
235
+ .footer { text-align: center; margin-top: 20px; font-size: 13px; color: #9a8c78; } a { color: #e8a838; text-decoration: none; }</style></head>
236
+ <body><div class="card"><h2>Sign In</h2><div id="err" style="color:#d45c3c; font-size:13px; margin-bottom:15px; text-align:center;"></div>
237
+ <div class="field"><label>Email</label><input type="email" id="email" value="customer@bakery.com"></div>
238
+ <div class="field"><label>Password</label><input type="password" id="password" value="secret123"></div>
239
+ <button onclick="handleLogin()">Log In</button><div class="footer">Need an account? <a href="/signup">Sign up</a></div></div>
240
+ <script>async function handleLogin() {
241
+ const email = document.getElementById('email').value;
242
+ const password = document.getElementById('password').value;
243
+ const res = await fetch('/api/v1/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }) });
244
+ const data = await res.json();
245
+ if (res.ok && data.token) {
246
+ localStorage.setItem('__auth_token__', data.token);
247
+ window.location.href = '/dashboard';
248
+ } else { document.getElementById('err').textContent = data.error || 'Login failed'; }
249
+ }</script></body></html>`,
250
+
251
+ // Turnkey Customer Signup Page
252
+ signup: () => `<!DOCTYPE html><html><head><title>Create Account</title><link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet">
253
+ <style>body { background: #0e0c09; color: #f0e8d8; font-family: 'DM Sans', sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }
254
+ .card { background: #181510; border: 1px solid #3a3020; padding: 40px; border-radius: 12px; width: 340px; } h2 { color: #e8a838; margin: 0 0 24px; text-align: center; }
255
+ .field { margin-bottom: 20px; } label { display: block; font-size: 11px; color: #9a8c78; text-transform: uppercase; margin-bottom: 8px; }
256
+ input { background: #221d14; border: 1px solid #3a3020; color: #f0e8d8; padding: 12px; width: 100%; box-sizing: border-box; border-radius: 6px; outline: none; }
257
+ button { background: #e8a838; color: #0e0c09; border: none; padding: 12px; width: 100%; border-radius: 6px; font-weight: 600; cursor: pointer; }</style></head>
258
+ <body><div class="card"><h2>Sign Up</h2><div class="field"><label>Username</label><input type="text" id="username"></div>
259
+ <div class="field"><label>Email</label><input type="email" id="email"></div><div class="field"><label>Password</label><input type="password" id="password"></div>
260
+ <button onclick="window.location.href='/login'">Register Account</button></div></body></html>`,
261
+
262
+ // Turnkey Storefront Dashboard
263
+ dashboard: () => `<!DOCTYPE html><html><head><title>Storefront Catalog</title><link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet">
264
+ <style>body { background: #0e0c09; color: #f0e8d8; font-family: 'DM Sans', sans-serif; padding: 40px; margin: 0; }
265
+ header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #3a3020; padding-bottom: 20px; margin-bottom: 30px; }
266
+ h1 { color: #e8a838; margin: 0; } .logout { background: #3a1a14; color: #d45c3c; border: 1px solid #5a2014; padding: 8px 16px; border-radius: 6px; cursor: pointer; }
267
+ .grid { display: grid; grid-template-columns: 1fr; gap: 30px; } .panel { background: #181510; border: 1px solid #3a3020; padding: 24px; border-radius: 8px; }
268
+ table { width: 100%; border-collapse: collapse; } th { color: #9a8c78; text-align: left; padding: 12px; border-bottom: 2px solid #3a3020; } td { padding: 12px; border-bottom: 1px solid #221d14; }</style></head>
269
+ <body><header><h1>Bakery Storefront</h1><button class="logout" onclick="localStorage.removeItem('__auth_token__'); window.location.href='/login'">Log Out</button></header>
270
+ <div class="grid"><div class="panel"><h3>Available Baked Goods</h3><table><thead><tr><th>Product Name</th><th>Price</th><th>Stock Status</th></tr></thead><tbody id="catalog"></tbody></table></div></div>
271
+ <script>const token = localStorage.getItem('__auth_token__'); if (!token) window.location.href = '/login';
272
+ async function load() {
273
+ const res = await fetch('/api/v1/products', { headers: { 'Authorization': 'Bearer ' + token } });
274
+ const data = await res.json(); const tbody = document.getElementById('catalog'); tbody.innerHTML = '';
275
+ if(data.products) { data.products.forEach(p => { tbody.innerHTML += '<tr><td><strong>'+p.name+'</strong></td><td>$'+p.price.toFixed(2)+'</td><td>'+p.stock+' left</td></tr>'; }); }
276
+ } load();</script></body></html>`
277
+ };
278
+
279
+ export { UI };
package/monkey.backup.js CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  import { getHtmlTemplate } from './htmlTemplate.js';
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,76 +93,66 @@ 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
- 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
- }
104
+ // Inside your parseStack function in monkey.js, update this block:
105
+ if (['POST', 'PUT', 'PATCH'].includes(httpMethod)) {
106
+ const handlers = (layer.route.stack || []).map(sl => sl.handle).filter(Boolean);
107
+ for (const handler of handlers) {
108
+ bodyFields.push(...extractBodyFields(handler));
109
+ }
110
+
111
+ const seen = new Map();
112
+ bodyFields = bodyFields.filter(f => {
113
+ if (seen.has(f.name)) return false;
114
+ seen.set(f.name, true);
115
+ return true;
116
+ });
117
+
118
+ // CHANGE THIS: Only apply generic fallbacks if there are no explicit path parameters
119
+ if (bodyFields.length === 0 && pathParams.length === 0) {
120
+ bodyFields = fallbackFields(fullPath);
121
+ }
122
+ }
164
123
 
165
124
  detectedEndpoints[key] = {
166
- method: httpMethod,
167
- path: fullPath,
168
- title: `${httpMethod} ${fullPath}`,
169
- desc: `Auto-discovered endpoint ${fullPath}`,
170
- params: pathParams,
171
- fields: bodyFields,
125
+ method: httpMethod,
126
+ path: fullPath,
127
+ title: `${httpMethod} ${fullPath}`,
128
+ desc: `Auto-discovered endpoint - ${fullPath}`, // Safe ASCII character
129
+ params: pathParams,
130
+ fields: bodyFields,
172
131
  };
173
132
  }
174
- }
175
-
176
- // ── Nested router (app.use('/prefix', router)) ───────────────────────────
177
- else if (layer.handle && typeof layer.handle === 'function' && layer.handle.stack) {
133
+ } else if (layer.name === 'router' && layer.handle && layer.handle.stack) {
178
134
  const routerPrefix = extractRouterPrefix(layer);
179
135
  parseStack(layer.handle.stack, detectedEndpoints, prefix + routerPrefix);
180
136
  }
181
137
  }
182
138
  }
183
139
 
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(/\/+$/, '');
140
+ // function endtesterExpress() { ... }
189
141
 
190
- if (rawPath !== '/api/tester') {
142
+
143
+ // Change it to this instead:
144
+ export function endtesterExpress() {
145
+ return function monkeyTesterMiddleware(req, res, next) {
146
+ if (req.path !== '/api/tester' && req.path !== '/api/tester/') {
191
147
  return next();
192
148
  }
193
149
 
194
150
  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
151
  const detectedEndpoints = {};
199
152
 
200
153
  const rootStack =
201
- (app._router && app._router.stack) || // Express 4
202
- (app.router && app.router.stack) || // Express 5
154
+ (app._router && app._router.stack) ||
155
+ (app.router && app.router.stack) ||
203
156
  [];
204
157
 
205
158
  parseStack(rootStack, detectedEndpoints);
@@ -210,4 +163,4 @@ function endtesterExpress() {
210
163
  };
211
164
  }
212
165
 
213
- export { endtesterExpress };
166
+ // export { endtesterExpress };
package/monkey.js CHANGED
@@ -1,98 +1,59 @@
1
1
  'use strict';
2
2
 
3
- import { getHtmlTemplate } from './htmlTemplate.js';
3
+ import { UI } from './htmlTemplate.js';
4
4
 
5
5
  function inferType(name) {
6
6
  const n = name.toLowerCase();
7
- if (n.includes('email')) return 'email';
8
- if (n.includes('password') || n.includes('pass')) return 'password';
9
- if (n.includes('date') || 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';
7
+ if (n.includes('email')) return 'email';
8
+ if (n.includes('password') || n.includes('pass')) return 'password';
9
+ if (n.includes('date')) return 'date';
10
+ if (n.includes('age') || n.includes('price') || n.includes('quantity') || n.includes('stock') || n === 'id' || n.endsWith('id')) return 'number';
18
11
  return 'text';
19
12
  }
20
13
 
21
14
  function buildField(name) {
22
- return {
23
- name,
24
- label: name.charAt(0).toUpperCase() + name.slice(1).replace(/([A-Z])/g, ' $1'),
25
- type: inferType(name),
26
- placeholder: `Enter ${name}`
27
- };
15
+ return { name, label: name.charAt(0).toUpperCase() + name.slice(1).replace(/([A-Z])/g, ' $1'), type: inferType(name), placeholder: `Enter ${name}` };
28
16
  }
29
17
 
30
18
  function extractBodyFields(handler) {
31
19
  try {
32
20
  const source = handler.toString();
33
21
  if (!source || source.includes('[native code]')) return [];
34
-
35
22
  const seen = new Map();
36
23
  const destructRe = /(?:const|let|var)\s*\{\s*([^}]+)\s*\}\s*=\s*req\.body/g;
37
24
  let m;
38
25
  while ((m = destructRe.exec(source)) !== null) {
39
26
  m[1].split(',').forEach(part => {
40
27
  const name = part.split(':')[0].split('=')[0].trim();
41
- if (name && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) && !seen.has(name)) {
42
- seen.set(name, buildField(name));
43
- }
28
+ if (name && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) && !seen.has(name)) seen.set(name, buildField(name));
44
29
  });
45
30
  }
46
-
47
- const accessRe = /req\.body\.([a-zA-Z_$][a-zA-Z0-9_$]*)|req\.body\[['"]([a-zA-Z_$][a-zA-Z0-9_$]*)['"]]/g;
31
+ const accessRe = /req\.body\.([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
48
32
  while ((m = accessRe.exec(source)) !== null) {
49
- const name = m[1] || m[2];
50
- if (name && !seen.has(name)) seen.set(name, buildField(name));
33
+ const name = m[1]; if (name && !seen.has(name)) seen.set(name, buildField(name));
51
34
  }
52
-
53
35
  return Array.from(seen.values());
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 [];
36
+ } catch { return []; }
67
37
  }
68
38
 
69
39
  function extractRouterPrefix(layer) {
70
40
  if (!layer.regexp) return '';
71
- const src = layer.regexp.source;
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 '';
41
+ const m = [/^\^\\\/([^\\?$]+)/, /^\^\\\/([a-zA-Z0-9_/-]+)/].reduce((acc, re) => acc || re.exec(layer.regexp.source), null);
42
+ return m && m[1] ? '/' + m[1].replace(/\\\//g, '/').replace(/\\/g, '') : '';
78
43
  }
79
44
 
80
45
  function parseStack(stack, detectedEndpoints, prefix = '') {
81
46
  if (!Array.isArray(stack)) return;
82
-
83
47
  for (const layer of stack) {
84
48
  if (layer.route) {
85
49
  const rawPath = typeof layer.route.path === 'string' ? layer.route.path : (layer.route.path ? String(layer.route.path) : '');
86
50
  const fullPath = (prefix + rawPath).replace(/\/+/g, '/') || '/';
87
-
88
51
  if (fullPath.startsWith('/api/tester')) continue;
89
52
 
90
53
  const methods = Object.keys(layer.route.methods || {});
91
-
92
54
  for (const method of methods) {
93
55
  const httpMethod = method.toUpperCase();
94
56
  const key = `${httpMethod}::${fullPath}`;
95
-
96
57
  const pathParams = [];
97
58
  const paramRe = /:([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
98
59
  let pm;
@@ -101,66 +62,43 @@ function parseStack(stack, detectedEndpoints, prefix = '') {
101
62
  }
102
63
 
103
64
  let bodyFields = [];
104
- // Inside your parseStack function in monkey.js, update this block:
105
- if (['POST', 'PUT', 'PATCH'].includes(httpMethod)) {
106
- const handlers = (layer.route.stack || []).map(sl => sl.handle).filter(Boolean);
107
- for (const handler of handlers) {
108
- bodyFields.push(...extractBodyFields(handler));
109
- }
110
-
111
- const seen = new Map();
112
- bodyFields = bodyFields.filter(f => {
113
- if (seen.has(f.name)) return false;
114
- seen.set(f.name, true);
115
- return true;
116
- });
117
-
118
- // CHANGE THIS: Only apply generic fallbacks if there are no explicit path parameters
119
- if (bodyFields.length === 0 && pathParams.length === 0) {
120
- bodyFields = fallbackFields(fullPath);
121
- }
122
- }
65
+ if (['POST', 'PUT', 'PATCH'].includes(httpMethod)) {
66
+ const handlers = (layer.route.stack || []).map(sl => sl.handle).filter(Boolean);
67
+ for (const handler of handlers) bodyFields.push(...extractBodyFields(handler));
68
+ const seen = new Map();
69
+ bodyFields = bodyFields.filter(f => !seen.has(f.name) && seen.set(f.name, true));
70
+ }
123
71
 
124
- detectedEndpoints[key] = {
125
- method: httpMethod,
126
- path: fullPath,
127
- title: `${httpMethod} ${fullPath}`,
128
- desc: `Auto-discovered endpoint - ${fullPath}`, // Safe ASCII character
129
- params: pathParams,
130
- fields: bodyFields,
131
- };
72
+ detectedEndpoints[key] = { method: httpMethod, path: fullPath, title: `${httpMethod} ${fullPath}`, desc: `Auto-discovered endpoint - ${fullPath}`, params: pathParams, fields: bodyFields };
132
73
  }
133
74
  } else if (layer.name === 'router' && layer.handle && layer.handle.stack) {
134
- const routerPrefix = extractRouterPrefix(layer);
135
- parseStack(layer.handle.stack, detectedEndpoints, prefix + routerPrefix);
75
+ parseStack(layer.handle.stack, detectedEndpoints, prefix + extractRouterPrefix(layer));
136
76
  }
137
77
  }
138
78
  }
139
79
 
140
- // function endtesterExpress() { ... }
141
-
142
-
143
- // Change it to this instead:
144
80
  export function endtesterExpress() {
145
81
  return function monkeyTesterMiddleware(req, res, next) {
146
- if (req.path !== '/api/tester' && req.path !== '/api/tester/') {
147
- return next();
82
+ const route = req.path.toLowerCase().replace(/\/$/, '');
83
+
84
+ // 1. Serve Standalone Frontend Templates Interceptions
85
+ if (route === '/login') return res.send(UI.login());
86
+ if (route === '/signup') return res.send(UI.signup());
87
+ if (route === '/dashboard') return res.send(UI.dashboard());
88
+
89
+ // 2. Serve the main Tester UI environment
90
+ if (route === '/api/tester') {
91
+ const app = req.app;
92
+ const detectedEndpoints = {};
93
+ const rootStack = (app._router && app._router.stack) || (app.router && app.router.stack) || [];
94
+ parseStack(rootStack, detectedEndpoints);
95
+ const b64 = Buffer.from(JSON.stringify(detectedEndpoints)).toString('base64');
96
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
97
+ return res.send(UI.tester(b64));
148
98
  }
149
99
 
150
- const app = req.app;
151
- const detectedEndpoints = {};
152
-
153
- const rootStack =
154
- (app._router && app._router.stack) ||
155
- (app.router && app.router.stack) ||
156
- [];
157
-
158
- parseStack(rootStack, detectedEndpoints);
159
-
160
- const html = getHtmlTemplate(detectedEndpoints);
161
- res.setHeader('Content-Type', 'text/html; charset=utf-8');
162
- return res.send(html);
100
+ next();
163
101
  };
164
102
  }
165
103
 
166
- // export { endtesterExpress };
104
+ export default { endtesterExpress };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aimeloic/monkey-tester",
3
- "version": "4.0.3",
3
+ "version": "4.0.4",
4
4
  "description": "Auto route scanning visual runner dashboard.",
5
5
  "main": "index.js",
6
6
  "type": "module",