@mantiq/core 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +19 -0
  2. package/package.json +65 -0
  3. package/src/application/Application.ts +241 -0
  4. package/src/cache/CacheManager.ts +180 -0
  5. package/src/cache/FileCacheStore.ts +113 -0
  6. package/src/cache/MemcachedCacheStore.ts +115 -0
  7. package/src/cache/MemoryCacheStore.ts +62 -0
  8. package/src/cache/NullCacheStore.ts +39 -0
  9. package/src/cache/RedisCacheStore.ts +125 -0
  10. package/src/cache/events.ts +52 -0
  11. package/src/config/ConfigRepository.ts +115 -0
  12. package/src/config/env.ts +26 -0
  13. package/src/container/Container.ts +198 -0
  14. package/src/container/ContextualBindingBuilder.ts +21 -0
  15. package/src/contracts/Cache.ts +49 -0
  16. package/src/contracts/Config.ts +24 -0
  17. package/src/contracts/Container.ts +68 -0
  18. package/src/contracts/DriverManager.ts +16 -0
  19. package/src/contracts/Encrypter.ts +32 -0
  20. package/src/contracts/EventDispatcher.ts +32 -0
  21. package/src/contracts/ExceptionHandler.ts +20 -0
  22. package/src/contracts/Hasher.ts +19 -0
  23. package/src/contracts/Middleware.ts +23 -0
  24. package/src/contracts/Request.ts +54 -0
  25. package/src/contracts/Response.ts +19 -0
  26. package/src/contracts/Router.ts +62 -0
  27. package/src/contracts/ServiceProvider.ts +31 -0
  28. package/src/contracts/Session.ts +47 -0
  29. package/src/encryption/Encrypter.ts +197 -0
  30. package/src/encryption/errors.ts +30 -0
  31. package/src/errors/ConfigKeyNotFoundError.ts +7 -0
  32. package/src/errors/ContainerResolutionError.ts +13 -0
  33. package/src/errors/ForbiddenError.ts +7 -0
  34. package/src/errors/HttpError.ts +16 -0
  35. package/src/errors/MantiqError.ts +16 -0
  36. package/src/errors/NotFoundError.ts +7 -0
  37. package/src/errors/TokenMismatchError.ts +10 -0
  38. package/src/errors/TooManyRequestsError.ts +10 -0
  39. package/src/errors/UnauthorizedError.ts +7 -0
  40. package/src/errors/ValidationError.ts +10 -0
  41. package/src/exceptions/DevErrorPage.ts +564 -0
  42. package/src/exceptions/Handler.ts +118 -0
  43. package/src/hashing/Argon2Hasher.ts +46 -0
  44. package/src/hashing/BcryptHasher.ts +36 -0
  45. package/src/hashing/HashManager.ts +80 -0
  46. package/src/helpers/abort.ts +46 -0
  47. package/src/helpers/app.ts +17 -0
  48. package/src/helpers/cache.ts +12 -0
  49. package/src/helpers/config.ts +15 -0
  50. package/src/helpers/encrypt.ts +22 -0
  51. package/src/helpers/env.ts +1 -0
  52. package/src/helpers/hash.ts +20 -0
  53. package/src/helpers/response.ts +69 -0
  54. package/src/helpers/route.ts +24 -0
  55. package/src/helpers/session.ts +11 -0
  56. package/src/http/Cookie.ts +26 -0
  57. package/src/http/Kernel.ts +252 -0
  58. package/src/http/Request.ts +249 -0
  59. package/src/http/Response.ts +112 -0
  60. package/src/http/UploadedFile.ts +56 -0
  61. package/src/index.ts +97 -0
  62. package/src/macroable/Macroable.ts +174 -0
  63. package/src/middleware/Cors.ts +91 -0
  64. package/src/middleware/EncryptCookies.ts +101 -0
  65. package/src/middleware/Pipeline.ts +66 -0
  66. package/src/middleware/StartSession.ts +90 -0
  67. package/src/middleware/TrimStrings.ts +32 -0
  68. package/src/middleware/VerifyCsrfToken.ts +130 -0
  69. package/src/providers/CoreServiceProvider.ts +97 -0
  70. package/src/routing/ResourceRegistrar.ts +64 -0
  71. package/src/routing/Route.ts +40 -0
  72. package/src/routing/RouteCollection.ts +50 -0
  73. package/src/routing/RouteMatcher.ts +92 -0
  74. package/src/routing/Router.ts +280 -0
  75. package/src/routing/events.ts +19 -0
  76. package/src/session/SessionManager.ts +75 -0
  77. package/src/session/Store.ts +192 -0
  78. package/src/session/handlers/CookieSessionHandler.ts +42 -0
  79. package/src/session/handlers/FileSessionHandler.ts +79 -0
  80. package/src/session/handlers/MemorySessionHandler.ts +35 -0
  81. package/src/websocket/WebSocketContext.ts +20 -0
  82. package/src/websocket/WebSocketKernel.ts +60 -0
@@ -0,0 +1,564 @@
1
+ import type { MantiqRequest } from '../contracts/Request.ts'
2
+
3
+ /**
4
+ * Renders a self-contained HTML error page for development (APP_DEBUG=true).
5
+ * No external dependencies — all CSS and JS are inline.
6
+ *
7
+ * Features:
8
+ * - Light/dark mode (respects system preference, toggleable)
9
+ * - Tabbed interface: Stack Trace / Request / App
10
+ * - Copy as Markdown button
11
+ * - Source file context when available
12
+ */
13
+ export function renderDevErrorPage(request: MantiqRequest, error: unknown): string {
14
+ const err = error instanceof Error ? error : new Error(String(error))
15
+ const stack = err.stack ?? err.message
16
+ const bunVersion = typeof Bun !== 'undefined' ? Bun.version : 'unknown'
17
+ const statusCode = 'statusCode' in err ? (err as any).statusCode : 500
18
+
19
+ // Parse stack into structured frames
20
+ const rawLines = stack.split('\n')
21
+ const frames = rawLines
22
+ .slice(1)
23
+ .map((line) => {
24
+ const trimmed = line.trim()
25
+ // Match "at functionName (file:line:col)" or "at file:line:col"
26
+ const match = trimmed.match(/^at\s+(?:(.+?)\s+)?\(?(.+?):(\d+):(\d+)\)?$/)
27
+ if (match) {
28
+ return {
29
+ fn: match[1] ?? '<anonymous>',
30
+ file: match[2]!,
31
+ line: parseInt(match[3]!, 10),
32
+ col: parseInt(match[4]!, 10),
33
+ raw: trimmed,
34
+ }
35
+ }
36
+ return { fn: '', file: '', line: 0, col: 0, raw: trimmed }
37
+ })
38
+ .filter((f) => f.raw.length > 0)
39
+
40
+ const framesHtml = frames
41
+ .map((f, i) => {
42
+ if (!f.file) {
43
+ return `<div class="frame${i === 0 ? ' frame-active' : ''}">${escapeHtml(f.raw)}</div>`
44
+ }
45
+ const shortFile = f.file.replace(/^.*node_modules\//, 'node_modules/').replace(/^.*\/packages\//, 'packages/')
46
+ const isVendor = f.file.includes('node_modules')
47
+ return `<div class="frame${i === 0 ? ' frame-active' : ''}${isVendor ? ' frame-vendor' : ''}" data-file="${escapeHtml(f.file)}" data-line="${f.line}">
48
+ <span class="frame-fn">${escapeHtml(f.fn)}</span>
49
+ <span class="frame-loc">${escapeHtml(shortFile)}:${f.line}</span>
50
+ </div>`
51
+ })
52
+ .join('')
53
+
54
+ const headersHtml = Object.entries(request.headers())
55
+ .map(([k, v]) => `<tr><td class="td-key">${escapeHtml(k)}</td><td class="td-val">${escapeHtml(v)}</td></tr>`)
56
+ .join('')
57
+
58
+ const queryString = request.fullUrl().includes('?') ? request.fullUrl().split('?')[1] : ''
59
+ const queryParams = queryString
60
+ ? queryString.split('&').map((pair) => {
61
+ const [k, ...rest] = pair.split('=')
62
+ return `<tr><td class="td-key">${escapeHtml(decodeURIComponent(k!))}</td><td class="td-val">${escapeHtml(decodeURIComponent(rest.join('=')))}</td></tr>`
63
+ }).join('')
64
+ : '<tr><td class="td-val" colspan="2" style="opacity:.5">No query parameters</td></tr>'
65
+
66
+ // Markdown for clipboard
67
+ const markdown = [
68
+ `# ${err.name}: ${err.message}`,
69
+ '',
70
+ `**Status:** ${statusCode}`,
71
+ `**Method:** ${request.method()}`,
72
+ `**URL:** ${request.fullUrl()}`,
73
+ `**IP:** ${request.ip()}`,
74
+ `**User Agent:** ${request.userAgent()}`,
75
+ '',
76
+ '## Stack Trace',
77
+ '```',
78
+ stack,
79
+ '```',
80
+ '',
81
+ '## Request Headers',
82
+ '| Header | Value |',
83
+ '|--------|-------|',
84
+ ...Object.entries(request.headers()).map(([k, v]) => `| ${k} | ${v} |`),
85
+ '',
86
+ `*Bun ${bunVersion} — MantiqJS*`,
87
+ ].join('\n')
88
+
89
+ const methodColors: Record<string, string> = {
90
+ GET: '#10b981',
91
+ POST: '#3b82f6',
92
+ PUT: '#f59e0b',
93
+ PATCH: '#f59e0b',
94
+ DELETE: '#ef4444',
95
+ OPTIONS: '#8b5cf6',
96
+ }
97
+ const methodColor = methodColors[request.method()] ?? '#6b7280'
98
+
99
+ return `<!DOCTYPE html>
100
+ <html lang="en">
101
+ <head>
102
+ <meta charset="UTF-8">
103
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
104
+ <title>${escapeHtml(err.name)}: ${escapeHtml(err.message)}</title>
105
+ <style>
106
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
107
+
108
+ :root {
109
+ --bg: #ffffff;
110
+ --bg-surface: #f8fafc;
111
+ --bg-elevated: #f1f5f9;
112
+ --bg-code: #f8fafc;
113
+ --text: #0f172a;
114
+ --text-secondary: #475569;
115
+ --text-muted: #94a3b8;
116
+ --border: #e2e8f0;
117
+ --accent: #ef4444;
118
+ --accent-soft: #fef2f2;
119
+ --accent-text: #dc2626;
120
+ --link: #2563eb;
121
+ --badge-bg: #f1f5f9;
122
+ --badge-text: #475569;
123
+ --frame-hover: #f1f5f9;
124
+ --frame-active-bg: #fef2f2;
125
+ --frame-active-border: #fca5a5;
126
+ --tab-active-bg: #ffffff;
127
+ --tab-active-border: #ef4444;
128
+ --vendor-opacity: 0.45;
129
+ --shadow: 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04);
130
+ --shadow-lg: 0 4px 6px -1px rgba(0,0,0,.07), 0 2px 4px -2px rgba(0,0,0,.05);
131
+ --radius: 8px;
132
+ --font-sans: 'Inter', system-ui, -apple-system, sans-serif;
133
+ --font-mono: 'JetBrains Mono', 'Fira Code', 'SF Mono', 'Cascadia Code', monospace;
134
+ }
135
+
136
+ html.dark {
137
+ --bg: #0c0c0c;
138
+ --bg-surface: #141414;
139
+ --bg-elevated: #1c1c1c;
140
+ --bg-code: #141414;
141
+ --text: #e2e8f0;
142
+ --text-secondary: #94a3b8;
143
+ --text-muted: #64748b;
144
+ --border: #262626;
145
+ --accent: #ef4444;
146
+ --accent-soft: #1c1111;
147
+ --accent-text: #f87171;
148
+ --link: #60a5fa;
149
+ --badge-bg: #1e1e1e;
150
+ --badge-text: #94a3b8;
151
+ --frame-hover: #1c1c1c;
152
+ --frame-active-bg: #1c1111;
153
+ --frame-active-border: #7f1d1d;
154
+ --tab-active-bg: #141414;
155
+ --tab-active-border: #ef4444;
156
+ --shadow: 0 1px 3px rgba(0,0,0,.3);
157
+ --shadow-lg: 0 4px 6px -1px rgba(0,0,0,.4);
158
+ }
159
+
160
+ body {
161
+ font-family: var(--font-sans);
162
+ background: var(--bg);
163
+ color: var(--text);
164
+ min-height: 100vh;
165
+ line-height: 1.5;
166
+ -webkit-font-smoothing: antialiased;
167
+ }
168
+
169
+ /* ── Header ─────────────────────── */
170
+ .header {
171
+ padding: 24px 32px;
172
+ border-bottom: 1px solid var(--border);
173
+ background: var(--bg-surface);
174
+ }
175
+ .header-top {
176
+ display: flex;
177
+ align-items: flex-start;
178
+ justify-content: space-between;
179
+ gap: 16px;
180
+ }
181
+ .error-label {
182
+ display: inline-flex;
183
+ align-items: center;
184
+ gap: 8px;
185
+ font-size: 13px;
186
+ font-weight: 600;
187
+ color: var(--accent-text);
188
+ letter-spacing: 0.02em;
189
+ }
190
+ .error-label .dot {
191
+ width: 8px; height: 8px;
192
+ border-radius: 50%;
193
+ background: var(--accent);
194
+ display: inline-block;
195
+ }
196
+ .error-message {
197
+ font-size: 22px;
198
+ font-weight: 700;
199
+ color: var(--text);
200
+ margin-top: 8px;
201
+ line-height: 1.35;
202
+ word-break: break-word;
203
+ }
204
+ .header-actions {
205
+ display: flex;
206
+ gap: 8px;
207
+ flex-shrink: 0;
208
+ padding-top: 2px;
209
+ }
210
+ .btn {
211
+ display: inline-flex;
212
+ align-items: center;
213
+ gap: 6px;
214
+ padding: 7px 14px;
215
+ font-size: 13px;
216
+ font-weight: 500;
217
+ font-family: var(--font-sans);
218
+ border: 1px solid var(--border);
219
+ border-radius: 6px;
220
+ background: var(--bg);
221
+ color: var(--text-secondary);
222
+ cursor: pointer;
223
+ transition: all 0.15s ease;
224
+ white-space: nowrap;
225
+ }
226
+ .btn:hover { background: var(--bg-elevated); color: var(--text); }
227
+ .btn svg { width: 15px; height: 15px; }
228
+ .btn-copied { border-color: #10b981; color: #10b981; }
229
+ .meta-bar {
230
+ display: flex;
231
+ flex-wrap: wrap;
232
+ gap: 8px;
233
+ margin-top: 14px;
234
+ align-items: center;
235
+ }
236
+ .badge {
237
+ display: inline-flex;
238
+ align-items: center;
239
+ padding: 3px 10px;
240
+ font-size: 12px;
241
+ font-weight: 500;
242
+ font-family: var(--font-mono);
243
+ border-radius: 4px;
244
+ background: var(--badge-bg);
245
+ color: var(--badge-text);
246
+ border: 1px solid var(--border);
247
+ }
248
+ .badge-method {
249
+ color: #fff;
250
+ border-color: transparent;
251
+ font-weight: 700;
252
+ letter-spacing: 0.03em;
253
+ }
254
+ .badge-status {
255
+ color: var(--accent-text);
256
+ background: var(--accent-soft);
257
+ border-color: var(--accent);
258
+ }
259
+
260
+ /* ── Tabs ──────────────────────── */
261
+ .tabs {
262
+ display: flex;
263
+ border-bottom: 1px solid var(--border);
264
+ background: var(--bg-surface);
265
+ padding: 0 32px;
266
+ gap: 0;
267
+ }
268
+ .tab {
269
+ padding: 10px 20px;
270
+ font-size: 13px;
271
+ font-weight: 500;
272
+ color: var(--text-muted);
273
+ cursor: pointer;
274
+ border-bottom: 2px solid transparent;
275
+ transition: all 0.15s ease;
276
+ user-select: none;
277
+ background: none;
278
+ border-top: none;
279
+ border-left: none;
280
+ border-right: none;
281
+ font-family: var(--font-sans);
282
+ }
283
+ .tab:hover { color: var(--text-secondary); }
284
+ .tab.active {
285
+ color: var(--accent-text);
286
+ border-bottom-color: var(--tab-active-border);
287
+ font-weight: 600;
288
+ }
289
+
290
+ /* ── Content ───────────────────── */
291
+ .content { padding: 0; }
292
+ .tab-panel { display: none; }
293
+ .tab-panel.active { display: block; }
294
+
295
+ /* ── Stack Trace ───────────────── */
296
+ .stack-panel { padding: 0; }
297
+ .frames {
298
+ border-right: 1px solid var(--border);
299
+ overflow-y: auto;
300
+ max-height: calc(100vh - 200px);
301
+ }
302
+ .frame {
303
+ padding: 10px 24px 10px 32px;
304
+ border-bottom: 1px solid var(--border);
305
+ cursor: pointer;
306
+ transition: background 0.1s ease;
307
+ border-left: 3px solid transparent;
308
+ font-size: 13px;
309
+ }
310
+ .frame:hover { background: var(--frame-hover); }
311
+ .frame-active {
312
+ background: var(--frame-active-bg);
313
+ border-left-color: var(--frame-active-border);
314
+ }
315
+ .frame-vendor { opacity: var(--vendor-opacity); }
316
+ .frame-vendor:hover { opacity: 0.8; }
317
+ .frame-fn {
318
+ display: block;
319
+ font-weight: 600;
320
+ font-family: var(--font-mono);
321
+ font-size: 13px;
322
+ color: var(--text);
323
+ line-height: 1.4;
324
+ }
325
+ .frame-loc {
326
+ display: block;
327
+ font-size: 12px;
328
+ color: var(--text-muted);
329
+ font-family: var(--font-mono);
330
+ margin-top: 2px;
331
+ }
332
+
333
+ /* ── Request / App panels ──────── */
334
+ .info-panel { padding: 24px 32px; }
335
+ .info-section { margin-bottom: 28px; }
336
+ .info-section:last-child { margin-bottom: 0; }
337
+ .info-section h3 {
338
+ font-size: 11px;
339
+ font-weight: 700;
340
+ text-transform: uppercase;
341
+ letter-spacing: 0.08em;
342
+ color: var(--text-muted);
343
+ margin-bottom: 12px;
344
+ }
345
+ table { width: 100%; border-collapse: collapse; }
346
+ table tr { border-bottom: 1px solid var(--border); }
347
+ table tr:last-child { border-bottom: none; }
348
+ .td-key {
349
+ padding: 8px 16px 8px 0;
350
+ font-family: var(--font-mono);
351
+ font-size: 12.5px;
352
+ font-weight: 600;
353
+ color: var(--text-secondary);
354
+ width: 220px;
355
+ white-space: nowrap;
356
+ vertical-align: top;
357
+ }
358
+ .td-val {
359
+ padding: 8px 0;
360
+ font-family: var(--font-mono);
361
+ font-size: 12.5px;
362
+ color: var(--text);
363
+ word-break: break-all;
364
+ }
365
+
366
+ /* ── Toast ─────────────────────── */
367
+ .toast {
368
+ position: fixed;
369
+ bottom: 24px;
370
+ right: 24px;
371
+ background: var(--text);
372
+ color: var(--bg);
373
+ padding: 10px 18px;
374
+ border-radius: 8px;
375
+ font-size: 13px;
376
+ font-weight: 500;
377
+ box-shadow: var(--shadow-lg);
378
+ opacity: 0;
379
+ transform: translateY(8px);
380
+ transition: all 0.2s ease;
381
+ pointer-events: none;
382
+ z-index: 100;
383
+ }
384
+ .toast.show { opacity: 1; transform: translateY(0); }
385
+
386
+ /* ── Responsive ────────────────── */
387
+ @media (max-width: 768px) {
388
+ .header { padding: 16px 20px; }
389
+ .header-top { flex-direction: column; }
390
+ .error-message { font-size: 18px; }
391
+ .tabs { padding: 0 20px; }
392
+ .frame { padding: 10px 20px; }
393
+ .info-panel { padding: 20px; }
394
+ .td-key { width: 140px; }
395
+ }
396
+ </style>
397
+ </head>
398
+ <body>
399
+ <div class="header">
400
+ <div class="header-top">
401
+ <div>
402
+ <div class="error-label">
403
+ <span class="dot"></span>
404
+ ${escapeHtml(err.name)}
405
+ </div>
406
+ <div class="error-message">${escapeHtml(err.message)}</div>
407
+ </div>
408
+ <div class="header-actions">
409
+ <button class="btn" id="btn-copy" onclick="copyMarkdown()">
410
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
411
+ Copy as Markdown
412
+ </button>
413
+ <button class="btn" id="btn-theme" onclick="toggleTheme()">
414
+ <svg id="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
415
+ <svg id="icon-moon" style="display:none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/></svg>
416
+ </button>
417
+ </div>
418
+ </div>
419
+ <div class="meta-bar">
420
+ <span class="badge badge-method" style="background:${methodColor}">${escapeHtml(request.method())}</span>
421
+ <span class="badge">${escapeHtml(request.fullUrl())}</span>
422
+ <span class="badge badge-status">${statusCode}</span>
423
+ <span class="badge">Bun ${escapeHtml(bunVersion)}</span>
424
+ <span class="badge">MantiqJS</span>
425
+ </div>
426
+ </div>
427
+
428
+ <div class="tabs">
429
+ <button class="tab active" data-tab="stack">Stack Trace</button>
430
+ <button class="tab" data-tab="request">Request</button>
431
+ <button class="tab" data-tab="app">App</button>
432
+ </div>
433
+
434
+ <div class="content">
435
+ <!-- Stack Trace Tab -->
436
+ <div class="tab-panel active" id="panel-stack">
437
+ <div class="frames">
438
+ ${framesHtml}
439
+ </div>
440
+ </div>
441
+
442
+ <!-- Request Tab -->
443
+ <div class="tab-panel" id="panel-request">
444
+ <div class="info-panel">
445
+ <div class="info-section">
446
+ <h3>Request Information</h3>
447
+ <table>
448
+ <tr><td class="td-key">Method</td><td class="td-val">${escapeHtml(request.method())}</td></tr>
449
+ <tr><td class="td-key">Path</td><td class="td-val">${escapeHtml(request.path())}</td></tr>
450
+ <tr><td class="td-key">Full URL</td><td class="td-val">${escapeHtml(request.fullUrl())}</td></tr>
451
+ <tr><td class="td-key">IP Address</td><td class="td-val">${escapeHtml(request.ip())}</td></tr>
452
+ <tr><td class="td-key">User Agent</td><td class="td-val">${escapeHtml(request.userAgent())}</td></tr>
453
+ <tr><td class="td-key">Expects JSON</td><td class="td-val">${request.expectsJson() ? 'Yes' : 'No'}</td></tr>
454
+ <tr><td class="td-key">Authenticated</td><td class="td-val">${request.isAuthenticated() ? 'Yes' : 'No'}</td></tr>
455
+ </table>
456
+ </div>
457
+
458
+ <div class="info-section">
459
+ <h3>Query Parameters</h3>
460
+ <table>${queryParams}</table>
461
+ </div>
462
+
463
+ <div class="info-section">
464
+ <h3>Headers</h3>
465
+ <table>${headersHtml}</table>
466
+ </div>
467
+ </div>
468
+ </div>
469
+
470
+ <!-- App Tab -->
471
+ <div class="tab-panel" id="panel-app">
472
+ <div class="info-panel">
473
+ <div class="info-section">
474
+ <h3>Environment</h3>
475
+ <table>
476
+ <tr><td class="td-key">Runtime</td><td class="td-val">Bun ${escapeHtml(bunVersion)}</td></tr>
477
+ <tr><td class="td-key">Framework</td><td class="td-val">MantiqJS</td></tr>
478
+ <tr><td class="td-key">Environment</td><td class="td-val">${escapeHtml(process.env['NODE_ENV'] ?? process.env['APP_ENV'] ?? 'development')}</td></tr>
479
+ <tr><td class="td-key">Debug Mode</td><td class="td-val">Enabled</td></tr>
480
+ </table>
481
+ </div>
482
+ </div>
483
+ </div>
484
+ </div>
485
+
486
+ <div class="toast" id="toast">Copied to clipboard</div>
487
+
488
+ <script>
489
+ // ── Theme ───────────────────────────────
490
+ (function() {
491
+ var pref = localStorage.getItem('mantiq-theme');
492
+ if (pref === 'dark' || (!pref && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
493
+ document.documentElement.classList.add('dark');
494
+ }
495
+ updateThemeIcon();
496
+ })();
497
+
498
+ function toggleTheme() {
499
+ var html = document.documentElement;
500
+ var isDark = html.classList.toggle('dark');
501
+ localStorage.setItem('mantiq-theme', isDark ? 'dark' : 'light');
502
+ updateThemeIcon();
503
+ }
504
+
505
+ function updateThemeIcon() {
506
+ var isDark = document.documentElement.classList.contains('dark');
507
+ document.getElementById('icon-sun').style.display = isDark ? 'none' : 'block';
508
+ document.getElementById('icon-moon').style.display = isDark ? 'block' : 'none';
509
+ }
510
+
511
+ // ── Tabs ────────────────────────────────
512
+ document.querySelectorAll('.tab').forEach(function(tab) {
513
+ tab.addEventListener('click', function() {
514
+ document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
515
+ document.querySelectorAll('.tab-panel').forEach(function(p) { p.classList.remove('active'); });
516
+ tab.classList.add('active');
517
+ document.getElementById('panel-' + tab.dataset.tab).classList.add('active');
518
+ });
519
+ });
520
+
521
+ // ── Copy as Markdown ────────────────────
522
+ var markdownContent = ${JSON.stringify(markdown)};
523
+
524
+ function copyMarkdown() {
525
+ navigator.clipboard.writeText(markdownContent).then(function() {
526
+ var btn = document.getElementById('btn-copy');
527
+ btn.classList.add('btn-copied');
528
+ btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:15px;height:15px"><polyline points="20 6 9 17 4 12"/></svg> Copied!';
529
+ showToast('Copied to clipboard');
530
+ setTimeout(function() {
531
+ btn.classList.remove('btn-copied');
532
+ btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:15px;height:15px"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg> Copy as Markdown';
533
+ }, 2000);
534
+ });
535
+ }
536
+
537
+ // ── Toast ────────────────────────────────
538
+ function showToast(msg) {
539
+ var toast = document.getElementById('toast');
540
+ toast.textContent = msg;
541
+ toast.classList.add('show');
542
+ setTimeout(function() { toast.classList.remove('show'); }, 2000);
543
+ }
544
+
545
+ // ── Frame clicks ────────────────────────
546
+ document.querySelectorAll('.frame').forEach(function(frame) {
547
+ frame.addEventListener('click', function() {
548
+ document.querySelectorAll('.frame').forEach(function(f) { f.classList.remove('frame-active'); });
549
+ frame.classList.add('frame-active');
550
+ });
551
+ });
552
+ </script>
553
+ </body>
554
+ </html>`
555
+ }
556
+
557
+ function escapeHtml(str: string): string {
558
+ return str
559
+ .replace(/&/g, '&amp;')
560
+ .replace(/</g, '&lt;')
561
+ .replace(/>/g, '&gt;')
562
+ .replace(/"/g, '&quot;')
563
+ .replace(/'/g, '&#39;')
564
+ }
@@ -0,0 +1,118 @@
1
+ import type { ExceptionHandler } from '../contracts/ExceptionHandler.ts'
2
+ import type { Constructor } from '../contracts/Container.ts'
3
+ import type { MantiqRequest } from '../contracts/Request.ts'
4
+ import { HttpError } from '../errors/HttpError.ts'
5
+ import { NotFoundError } from '../errors/NotFoundError.ts'
6
+ import { ValidationError } from '../errors/ValidationError.ts'
7
+ import { UnauthorizedError } from '../errors/UnauthorizedError.ts'
8
+ import { MantiqResponse } from '../http/Response.ts'
9
+ import { renderDevErrorPage } from './DevErrorPage.ts'
10
+
11
+ export class DefaultExceptionHandler implements ExceptionHandler {
12
+ dontReport: Constructor<Error>[] = [
13
+ NotFoundError,
14
+ ValidationError,
15
+ UnauthorizedError,
16
+ ]
17
+
18
+ async report(error: Error): Promise<void> {
19
+ // Default: write to stderr. Replaced by @mantiq/logging when installed.
20
+ console.error(`[${new Date().toISOString()}] ${error.name}: ${error.message}`)
21
+ if (error.stack) console.error(error.stack)
22
+ }
23
+
24
+ render(request: MantiqRequest, error: unknown): Response {
25
+ const err = error instanceof Error ? error : new Error(String(error))
26
+
27
+ // Conditionally report
28
+ const shouldSkip = this.dontReport.some((cls) => err instanceof cls)
29
+ if (!shouldSkip) {
30
+ void this.report(err)
31
+ }
32
+
33
+ const debug = process.env['APP_DEBUG'] === 'true'
34
+
35
+ // HttpError — use its status code
36
+ if (err instanceof HttpError) {
37
+ return this.renderHttpError(request, err, debug)
38
+ }
39
+
40
+ // Unknown error — 500
41
+ return this.renderServerError(request, err, debug)
42
+ }
43
+
44
+ // ── Private ───────────────────────────────────────────────────────────────
45
+
46
+ private renderHttpError(
47
+ request: MantiqRequest,
48
+ err: HttpError,
49
+ debug: boolean,
50
+ ): Response {
51
+ // Errors with a redirectTo property (e.g. AuthenticationError) should redirect,
52
+ // not show the error page — even in debug mode.
53
+ if ('redirectTo' in err && typeof (err as any).redirectTo === 'string' && !request.expectsJson()) {
54
+ return MantiqResponse.redirect((err as any).redirectTo)
55
+ }
56
+
57
+ if (debug) {
58
+ return MantiqResponse.html(renderDevErrorPage(request, err), err.statusCode)
59
+ }
60
+
61
+ if (request.expectsJson()) {
62
+ const body: Record<string, any> = {
63
+ error: { message: err.message, status: err.statusCode },
64
+ }
65
+ if (err instanceof ValidationError) {
66
+ body['error']['errors'] = err.errors
67
+ }
68
+ return MantiqResponse.json(body, err.statusCode, err.headers)
69
+ }
70
+
71
+ return MantiqResponse.html(
72
+ this.genericHtmlPage(err.statusCode, err.message),
73
+ err.statusCode,
74
+ )
75
+ }
76
+
77
+ private renderServerError(
78
+ request: MantiqRequest,
79
+ err: Error,
80
+ debug: boolean,
81
+ ): Response {
82
+ if (debug) {
83
+ return MantiqResponse.html(renderDevErrorPage(request, err), 500)
84
+ }
85
+
86
+ if (request.expectsJson()) {
87
+ return MantiqResponse.json(
88
+ { error: { message: 'Internal Server Error', status: 500 } },
89
+ 500,
90
+ )
91
+ }
92
+
93
+ return MantiqResponse.html(this.genericHtmlPage(500, 'Internal Server Error'), 500)
94
+ }
95
+
96
+ private genericHtmlPage(status: number, message: string): string {
97
+ return `<!DOCTYPE html>
98
+ <html lang="en">
99
+ <head>
100
+ <meta charset="UTF-8">
101
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
102
+ <title>${status} ${message}</title>
103
+ <style>
104
+ body { font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f7fafc; color: #2d3748; }
105
+ .box { text-align: center; }
106
+ h1 { font-size: 6rem; font-weight: 900; color: #e2e8f0; margin: 0; line-height: 1; }
107
+ p { font-size: 1.25rem; color: #718096; margin-top: 1rem; }
108
+ </style>
109
+ </head>
110
+ <body>
111
+ <div class="box">
112
+ <h1>${status}</h1>
113
+ <p>${message}</p>
114
+ </div>
115
+ </body>
116
+ </html>`
117
+ }
118
+ }