thatcher 1.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/LICENSE +21 -0
- package/README.md +398 -0
- package/package.json +73 -0
- package/src/adapters/google-auth.js +148 -0
- package/src/adapters/google-drive.js +209 -0
- package/src/app/api/[entity]/[[...path]]/route.js +33 -0
- package/src/app/api/audit/dashboard/route.js +77 -0
- package/src/app/api/audit/logs/route.js +46 -0
- package/src/app/api/audit/permissions/[id]/route.js +37 -0
- package/src/app/api/audit/permissions/route.js +93 -0
- package/src/app/api/audit/permissions/stats/route.js +29 -0
- package/src/app/api/audit/route.js +79 -0
- package/src/app/api/audit/stats/route.js +18 -0
- package/src/app/api/auth/google/callback/route.js +94 -0
- package/src/app/api/auth/google/route.js +58 -0
- package/src/app/api/auth/login/route.js +121 -0
- package/src/app/api/auth/logout/route.js +52 -0
- package/src/app/api/auth/me/route.js +16 -0
- package/src/app/api/auth/mwr-bridge/route.js +77 -0
- package/src/app/api/auth/password-reset/route.js +65 -0
- package/src/app/api/cron/trigger/route.js +58 -0
- package/src/app/api/csrf-token/route.js +8 -0
- package/src/app/api/debug/config/route.js +22 -0
- package/src/app/api/debug/hooks/route.js +16 -0
- package/src/app/api/debug/plugins/route.js +20 -0
- package/src/app/api/debug/sqlite/route.js +21 -0
- package/src/app/api/debug/sync/route.js +29 -0
- package/src/app/api/debug/workflow/route.js +20 -0
- package/src/app/api/domains/[domain]/route.js +26 -0
- package/src/app/api/domains/route.js +21 -0
- package/src/app/api/email/allocate/batch/route.js +106 -0
- package/src/app/api/email/allocate/route.js +141 -0
- package/src/app/api/email/receive/route.js +158 -0
- package/src/app/api/email/route.js +3 -0
- package/src/app/api/email/send/route.js +77 -0
- package/src/app/api/email/unallocated/route.js +47 -0
- package/src/app/api/files/[id]/route.js +38 -0
- package/src/app/api/health/route.js +95 -0
- package/src/app/api/metrics/route.js +73 -0
- package/src/app/api/monitoring/dashboard/route.js +18 -0
- package/src/cli.js +243 -0
- package/src/config/config-loader.js +112 -0
- package/src/config/constants.js +127 -0
- package/src/config/env.js +164 -0
- package/src/config/spec-helpers.js +232 -0
- package/src/engine.server.js +212 -0
- package/src/index.js +368 -0
- package/src/lib/accessibility.js +162 -0
- package/src/lib/action-factory.js +34 -0
- package/src/lib/action-utils.js +21 -0
- package/src/lib/alert-manager.js +189 -0
- package/src/lib/api-error-wrapper.js +125 -0
- package/src/lib/api-helpers.js +53 -0
- package/src/lib/api.js +82 -0
- package/src/lib/audit-logger-enhanced.js +117 -0
- package/src/lib/audit-logger.js +193 -0
- package/src/lib/auth-middleware.js +102 -0
- package/src/lib/auth-route-helpers.js +83 -0
- package/src/lib/business-rules-engine.js +86 -0
- package/src/lib/compression.js +44 -0
- package/src/lib/config-field-helpers.js +91 -0
- package/src/lib/config-generator-engine.js +445 -0
- package/src/lib/config-helpers.js +120 -0
- package/src/lib/connection-guard.js +79 -0
- package/src/lib/crud-action-helpers.js +34 -0
- package/src/lib/crud-factory.js +83 -0
- package/src/lib/crud-handlers.js +244 -0
- package/src/lib/csrf-protection.js +63 -0
- package/src/lib/database-core.js +258 -0
- package/src/lib/database-migrations.js +96 -0
- package/src/lib/date-utils.js +159 -0
- package/src/lib/db-backup.js +97 -0
- package/src/lib/db-monitor.js +127 -0
- package/src/lib/domain-loader.js +82 -0
- package/src/lib/email-sender.js +100 -0
- package/src/lib/error-boundary.js +134 -0
- package/src/lib/error-handler.js +84 -0
- package/src/lib/error-recovery.js +190 -0
- package/src/lib/error-resilience.js +130 -0
- package/src/lib/errors.js +69 -0
- package/src/lib/events-engine.js +182 -0
- package/src/lib/field-iterator.js +50 -0
- package/src/lib/field-registry.js +68 -0
- package/src/lib/field-types.js +154 -0
- package/src/lib/generic-crud-handler.js +32 -0
- package/src/lib/health-monitor.js +134 -0
- package/src/lib/hook-engine.js +169 -0
- package/src/lib/hot-reload/cache-invalidator.js +115 -0
- package/src/lib/hot-reload/checkpoint.js +95 -0
- package/src/lib/hot-reload/debug-exposure.js +67 -0
- package/src/lib/hot-reload/directory-watcher.js +96 -0
- package/src/lib/hot-reload/index.js +50 -0
- package/src/lib/hot-reload/mutex.js +75 -0
- package/src/lib/hot-reload/promise-container.js +66 -0
- package/src/lib/hot-reload/route-wrapper.js +46 -0
- package/src/lib/hot-reload/safe-error.js +51 -0
- package/src/lib/hot-reload/supervisor.js +161 -0
- package/src/lib/hot-reload/timeout-wrapper.js +52 -0
- package/src/lib/http-methods-factory.js +25 -0
- package/src/lib/index-optimizer.js +96 -0
- package/src/lib/index.js +35 -0
- package/src/lib/list-data-transform.js +39 -0
- package/src/lib/log-aggregator.js +116 -0
- package/src/lib/logger.js +55 -0
- package/src/lib/metrics-collector.js +102 -0
- package/src/lib/minifier.js +19 -0
- package/src/lib/monitoring-init.js +67 -0
- package/src/lib/next-compat.js +80 -0
- package/src/lib/next-polyfills.js +135 -0
- package/src/lib/perf-monitor.js +91 -0
- package/src/lib/progress-components.js +181 -0
- package/src/lib/query-cache.js +126 -0
- package/src/lib/query-engine-write.js +221 -0
- package/src/lib/query-engine.js +399 -0
- package/src/lib/query-perf.js +117 -0
- package/src/lib/query-string-adapter.js +75 -0
- package/src/lib/realtime-server.js +67 -0
- package/src/lib/render-cache.js +61 -0
- package/src/lib/request-tracker.js +43 -0
- package/src/lib/resource-hints.js +29 -0
- package/src/lib/resource-monitor.js +117 -0
- package/src/lib/response-formatter.js +80 -0
- package/src/lib/route-helpers.js +33 -0
- package/src/lib/route-resolver.js +142 -0
- package/src/lib/safe-json.js +8 -0
- package/src/lib/server-bootstrap.js +71 -0
- package/src/lib/stage-pipeline.js +153 -0
- package/src/lib/state-protocol.js +171 -0
- package/src/lib/state-transport-client.js +169 -0
- package/src/lib/state-transport-reconnect.js +121 -0
- package/src/lib/state-transport-server.js +181 -0
- package/src/lib/static-server.js +97 -0
- package/src/lib/status-helpers.js +98 -0
- package/src/lib/universal-handler.js +7 -0
- package/src/lib/utils.js +93 -0
- package/src/lib/validate.js +197 -0
- package/src/lib/validation/business-validators.js +61 -0
- package/src/lib/validation/csrf.js +51 -0
- package/src/lib/validation/file-validators.js +34 -0
- package/src/lib/validation/format-validators.js +106 -0
- package/src/lib/validation/index.js +19 -0
- package/src/lib/validation/rate-limit.js +31 -0
- package/src/lib/validation/security-validators.js +78 -0
- package/src/lib/validation-middleware.js +133 -0
- package/src/lib/validators.js +105 -0
- package/src/lib/with-audit-logging.js +63 -0
- package/src/lib/with-error-handler.js +31 -0
- package/src/lib/workflow-engine.js +250 -0
- package/src/server/server.js +305 -0
- package/src/services/collaborator-role.service.js +205 -0
- package/src/services/email-sender.js +105 -0
- package/src/services/notification-engine.js +110 -0
- package/src/services/permission.service.js +181 -0
- package/src/ui/advanced-search-renderer.js +42 -0
- package/src/ui/advanced-widgets.js +47 -0
- package/src/ui/auth-pages.js +114 -0
- package/src/ui/auth-styles.js +53 -0
- package/src/ui/client.js +99 -0
- package/src/ui/collaboration-dialogs.js +31 -0
- package/src/ui/common-handlers.js +156 -0
- package/src/ui/component-engine.js +103 -0
- package/src/ui/dashboard-renderer.js +150 -0
- package/src/ui/dialog-engine.js +147 -0
- package/src/ui/dialog-factory.js +38 -0
- package/src/ui/engagement-cards.js +76 -0
- package/src/ui/engagement-dialogs.js +100 -0
- package/src/ui/engagement-grid-renderer.js +109 -0
- package/src/ui/entity-renderer.js +176 -0
- package/src/ui/event-delegation.js +108 -0
- package/src/ui/fetch-json.js +24 -0
- package/src/ui/file-dialogs.js +81 -0
- package/src/ui/flexup-report-renderer.js +173 -0
- package/src/ui/format-helpers.js +102 -0
- package/src/ui/global-tags.js +144 -0
- package/src/ui/highlight-threading-renderer.js +176 -0
- package/src/ui/idle-logout.js +156 -0
- package/src/ui/job-management-renderer.js +61 -0
- package/src/ui/layout.js +219 -0
- package/src/ui/letter-dialogs.js +37 -0
- package/src/ui/ml-console-renderer.js +108 -0
- package/src/ui/monitoring-dashboard-client.js +136 -0
- package/src/ui/monitoring-dashboard.js +134 -0
- package/src/ui/notifications-renderer.js +51 -0
- package/src/ui/page-handler-admin.js +121 -0
- package/src/ui/page-handler-helpers.js +111 -0
- package/src/ui/page-handler-reviews.js +165 -0
- package/src/ui/page-handler-rfi.js +42 -0
- package/src/ui/page-handler.js +210 -0
- package/src/ui/password-reset-page.js +146 -0
- package/src/ui/perf-helpers.js +96 -0
- package/src/ui/perf-renderer.js +71 -0
- package/src/ui/permissions-ui.js +163 -0
- package/src/ui/picker-dialogs.js +100 -0
- package/src/ui/render-helpers.js +124 -0
- package/src/ui/renderer.js +35 -0
- package/src/ui/review-comparison-renderer.js +58 -0
- package/src/ui/review-detail-panels.js +71 -0
- package/src/ui/review-detail-renderer.js +170 -0
- package/src/ui/review-detail-script.js +95 -0
- package/src/ui/review-mwr-renderer.js +113 -0
- package/src/ui/review-renderer.js +202 -0
- package/src/ui/review-widgets.js +88 -0
- package/src/ui/review-zone-nav.js +12 -0
- package/src/ui/rfi-detail-renderer.js +191 -0
- package/src/ui/rfi-renderer.js +194 -0
- package/src/ui/rfi-report-renderer.js +56 -0
- package/src/ui/rippleui.css +1 -0
- package/src/ui/settings-renderer-advanced.js +195 -0
- package/src/ui/settings-renderer-advanced2.js +158 -0
- package/src/ui/settings-renderer-teams.js +112 -0
- package/src/ui/settings-renderer.js +166 -0
- package/src/ui/spacing-system.js +155 -0
- package/src/ui/standalone-login.js +109 -0
- package/src/ui/styles.css +2530 -0
- package/src/ui/styles2.css +1602 -0
- package/src/ui/test-page.js +23 -0
- package/src/ui/validation-rules.js +73 -0
- package/src/ui/validation-ui.js +147 -0
- package/src/ui/virtual-scroll.js +107 -0
- package/src/ui/webjsx.js +61 -0
- package/src/ui/widgets.js +152 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
const alerts = []
|
|
2
|
+
const alertHandlers = []
|
|
3
|
+
const thresholds = {
|
|
4
|
+
errorRate: { window: 60000, maxErrors: 10 },
|
|
5
|
+
p95Latency: { threshold: 500 },
|
|
6
|
+
p99Latency: { threshold: 1000 },
|
|
7
|
+
memoryUsage: { threshold: 0.9 },
|
|
8
|
+
diskUsage: { threshold: 0.9 },
|
|
9
|
+
dbConnections: { threshold: 100 },
|
|
10
|
+
slowQuery: { threshold: 1000 }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let alertingEnabled = true
|
|
14
|
+
const alertCooldowns = new Map()
|
|
15
|
+
|
|
16
|
+
function checkErrorRate(errorMetrics) {
|
|
17
|
+
const now = Date.now()
|
|
18
|
+
const window = thresholds.errorRate.window
|
|
19
|
+
let recentErrors = 0
|
|
20
|
+
|
|
21
|
+
for (const [endpoint, data] of Object.entries(errorMetrics.byEndpoint)) {
|
|
22
|
+
const recent = (data.recent || []).filter(e => (now - e.ts) < window)
|
|
23
|
+
recentErrors += recent.length
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (recentErrors > thresholds.errorRate.maxErrors) {
|
|
27
|
+
triggerAlert('error_rate', `High error rate: ${recentErrors} errors in last ${window/1000}s`, {
|
|
28
|
+
count: recentErrors,
|
|
29
|
+
threshold: thresholds.errorRate.maxErrors
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function checkLatency(requestMetrics) {
|
|
35
|
+
for (const [endpoint, stats] of Object.entries(requestMetrics)) {
|
|
36
|
+
if (stats.p95 > thresholds.p95Latency.threshold) {
|
|
37
|
+
triggerAlert('p95_latency', `P95 latency exceeded for ${endpoint}: ${stats.p95.toFixed(2)}ms`, {
|
|
38
|
+
endpoint,
|
|
39
|
+
p95: stats.p95,
|
|
40
|
+
threshold: thresholds.p95Latency.threshold
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (stats.p99 > thresholds.p99Latency.threshold) {
|
|
45
|
+
triggerAlert('p99_latency', `P99 latency exceeded for ${endpoint}: ${stats.p99.toFixed(2)}ms`, {
|
|
46
|
+
endpoint,
|
|
47
|
+
p99: stats.p99,
|
|
48
|
+
threshold: thresholds.p99Latency.threshold
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function checkResources(resourceMetrics) {
|
|
55
|
+
if (!resourceMetrics) return
|
|
56
|
+
|
|
57
|
+
if (resourceMetrics.memory && resourceMetrics.memory.avg > thresholds.memoryUsage.threshold) {
|
|
58
|
+
triggerAlert('memory_usage', `High memory usage: ${(resourceMetrics.memory.avg * 100).toFixed(1)}%`, {
|
|
59
|
+
usage: resourceMetrics.memory.avg,
|
|
60
|
+
threshold: thresholds.memoryUsage.threshold
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (resourceMetrics.disk && resourceMetrics.disk.avg > thresholds.diskUsage.threshold) {
|
|
65
|
+
triggerAlert('disk_usage', `High disk usage: ${(resourceMetrics.disk.avg * 100).toFixed(1)}%`, {
|
|
66
|
+
usage: resourceMetrics.disk.avg,
|
|
67
|
+
threshold: thresholds.diskUsage.threshold
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function checkDatabase(dbMetrics) {
|
|
73
|
+
for (const [operation, stats] of Object.entries(dbMetrics)) {
|
|
74
|
+
if (stats.p95 > thresholds.slowQuery.threshold) {
|
|
75
|
+
triggerAlert('slow_query', `Slow ${operation}: ${stats.p95.toFixed(2)}ms`, {
|
|
76
|
+
operation,
|
|
77
|
+
p95: stats.p95,
|
|
78
|
+
threshold: thresholds.slowQuery.threshold
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function triggerAlert(type, message, metadata = {}) {
|
|
85
|
+
if (!alertingEnabled) return
|
|
86
|
+
|
|
87
|
+
const cooldownKey = `${type}:${message}`
|
|
88
|
+
const now = Date.now()
|
|
89
|
+
const lastAlert = alertCooldowns.get(cooldownKey)
|
|
90
|
+
|
|
91
|
+
if (lastAlert && (now - lastAlert) < 60000) {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
alertCooldowns.set(cooldownKey, now)
|
|
96
|
+
|
|
97
|
+
const alert = {
|
|
98
|
+
type,
|
|
99
|
+
message,
|
|
100
|
+
metadata,
|
|
101
|
+
timestamp: now,
|
|
102
|
+
severity: getSeverity(type)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
alerts.push(alert)
|
|
106
|
+
if (alerts.length > 1000) alerts.shift()
|
|
107
|
+
|
|
108
|
+
console.error(`[ALERT] [${alert.severity.toUpperCase()}] ${type}: ${message}`, metadata)
|
|
109
|
+
|
|
110
|
+
for (const handler of alertHandlers) {
|
|
111
|
+
try {
|
|
112
|
+
handler(alert)
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error('[AlertManager] Handler error:', err)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getSeverity(type) {
|
|
120
|
+
if (type.includes('error_rate') || type.includes('memory') || type.includes('disk')) {
|
|
121
|
+
return 'critical'
|
|
122
|
+
}
|
|
123
|
+
if (type.includes('p99') || type.includes('slow_query')) {
|
|
124
|
+
return 'warning'
|
|
125
|
+
}
|
|
126
|
+
return 'info'
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function checkAllThresholds(allMetrics) {
|
|
130
|
+
try {
|
|
131
|
+
checkErrorRate(allMetrics.errors)
|
|
132
|
+
checkLatency(allMetrics.requests)
|
|
133
|
+
checkResources(allMetrics.resources)
|
|
134
|
+
checkDatabase(allMetrics.database)
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.error('[AlertManager] Check error:', err)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function registerAlertHandler(handler) {
|
|
141
|
+
alertHandlers.push(handler)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getRecentAlerts(limit = 50) {
|
|
145
|
+
return alerts.slice(-limit)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function clearAlerts() {
|
|
149
|
+
alerts.length = 0
|
|
150
|
+
alertCooldowns.clear()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function setThreshold(metric, value) {
|
|
154
|
+
if (thresholds[metric]) {
|
|
155
|
+
if (typeof thresholds[metric] === 'object' && 'threshold' in thresholds[metric]) {
|
|
156
|
+
thresholds[metric].threshold = value
|
|
157
|
+
} else {
|
|
158
|
+
thresholds[metric] = value
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function enableAlerting() {
|
|
164
|
+
alertingEnabled = true
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function disableAlerting() {
|
|
168
|
+
alertingEnabled = false
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export {
|
|
172
|
+
checkAllThresholds,
|
|
173
|
+
triggerAlert,
|
|
174
|
+
registerAlertHandler,
|
|
175
|
+
getRecentAlerts,
|
|
176
|
+
clearAlerts,
|
|
177
|
+
setThreshold,
|
|
178
|
+
enableAlerting,
|
|
179
|
+
disableAlerting
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (typeof globalThis !== 'undefined') {
|
|
183
|
+
globalThis.__alerts = {
|
|
184
|
+
getRecentAlerts,
|
|
185
|
+
triggerAlert,
|
|
186
|
+
clearAlerts,
|
|
187
|
+
setThreshold
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { normalizeError, createErrorLogger, retryWithBackoff } from '@/lib/error-handler';
|
|
2
|
+
import { NextResponse } from '@/lib/next-polyfills';
|
|
3
|
+
import { HTTP } from '@/config/constants';
|
|
4
|
+
|
|
5
|
+
const logger = createErrorLogger('API');
|
|
6
|
+
|
|
7
|
+
export function wrapAPIRoute(handler, options = {}) {
|
|
8
|
+
const { retry = false, timeout = 30000, logErrors = true } = options;
|
|
9
|
+
|
|
10
|
+
return async (request, context) => {
|
|
11
|
+
const startTime = Date.now();
|
|
12
|
+
const url = new URL(request.url);
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const operation = async () => {
|
|
16
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
17
|
+
setTimeout(() => reject(new Error(`Request timeout after ${timeout}ms`)), timeout)
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const handlerPromise = handler(request, context);
|
|
21
|
+
|
|
22
|
+
return await Promise.race([handlerPromise, timeoutPromise]);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const result = retry
|
|
26
|
+
? await retryWithBackoff(operation, { maxAttempts: 3, context: { url: url.pathname } })
|
|
27
|
+
: await operation();
|
|
28
|
+
|
|
29
|
+
const duration = Date.now() - startTime;
|
|
30
|
+
|
|
31
|
+
if (duration > 1000) {
|
|
32
|
+
logger.warn('Slow request', { url: url.pathname, duration });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return result;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
const duration = Date.now() - startTime;
|
|
38
|
+
const normalized = normalizeError(error);
|
|
39
|
+
|
|
40
|
+
if (logErrors) {
|
|
41
|
+
logger.error('Request failed', {
|
|
42
|
+
url: url.pathname,
|
|
43
|
+
method: request.method,
|
|
44
|
+
error: normalized.toJSON(),
|
|
45
|
+
duration
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return NextResponse.json(
|
|
50
|
+
normalized.toJSON(),
|
|
51
|
+
{
|
|
52
|
+
status: normalized.statusCode,
|
|
53
|
+
headers: { 'Content-Type': 'application/json' }
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function wrapGETRoute(handler, options = {}) {
|
|
61
|
+
return wrapAPIRoute(handler, { ...options, retry: true });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function wrapPOSTRoute(handler, options = {}) {
|
|
65
|
+
return wrapAPIRoute(handler, options);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function wrapPUTRoute(handler, options = {}) {
|
|
69
|
+
return wrapAPIRoute(handler, options);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function wrapDELETERoute(handler, options = {}) {
|
|
73
|
+
return wrapAPIRoute(handler, options);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createAPIHandler(handlers) {
|
|
77
|
+
const wrapped = {};
|
|
78
|
+
|
|
79
|
+
if (handlers.GET) wrapped.GET = wrapGETRoute(handlers.GET);
|
|
80
|
+
if (handlers.POST) wrapped.POST = wrapPOSTRoute(handlers.POST);
|
|
81
|
+
if (handlers.PUT) wrapped.PUT = wrapPUTRoute(handlers.PUT);
|
|
82
|
+
if (handlers.DELETE) wrapped.DELETE = wrapDELETERoute(handlers.DELETE);
|
|
83
|
+
if (handlers.PATCH) wrapped.PATCH = wrapAPIRoute(handlers.PATCH);
|
|
84
|
+
if (handlers.HEAD) wrapped.HEAD = wrapAPIRoute(handlers.HEAD);
|
|
85
|
+
|
|
86
|
+
return wrapped;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function safeJSONParse(text, fallback = null) {
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(text);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
logger.warn('JSON parse failed', { error: error.message });
|
|
94
|
+
return fallback;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function safeReadBody(request, fallback = {}) {
|
|
99
|
+
try {
|
|
100
|
+
const text = await request.text();
|
|
101
|
+
return text ? await safeJSONParse(text, fallback) : fallback;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
logger.warn('Body read failed', { error: error.message });
|
|
104
|
+
return fallback;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function validateRequired(data, fields) {
|
|
109
|
+
const missing = [];
|
|
110
|
+
|
|
111
|
+
for (const field of fields) {
|
|
112
|
+
if (data[field] === undefined || data[field] === null || data[field] === '') {
|
|
113
|
+
missing.push(field);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (missing.length > 0) {
|
|
118
|
+
throw new Error(`Missing required fields: ${missing.join(', ')}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function sanitizeError(error) {
|
|
123
|
+
const safe = String(error?.message || error || 'Unknown error');
|
|
124
|
+
return safe.substring(0, 500);
|
|
125
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
|
|
2
|
+
import { NextResponse } from '@/lib/next-polyfills';
|
|
3
|
+
import { getSpec } from '@/config/spec-helpers';
|
|
4
|
+
import { migrate } from '@/engine';
|
|
5
|
+
import { getUser } from '@/engine.server';
|
|
6
|
+
import { can } from '@/services/permission.service';
|
|
7
|
+
import { logger } from '@/lib/logger';
|
|
8
|
+
import { HTTP } from '@/config/constants';
|
|
9
|
+
import { ERROR_MESSAGES } from '@/config';
|
|
10
|
+
|
|
11
|
+
let dbInit = false;
|
|
12
|
+
export function ensureDb() {
|
|
13
|
+
if (!dbInit) {
|
|
14
|
+
migrate();
|
|
15
|
+
dbInit = true;
|
|
16
|
+
} else {
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const withMetadata = (data, status = HTTP.OK, type = 'success') => ({
|
|
21
|
+
status,
|
|
22
|
+
type,
|
|
23
|
+
timestamp: new Date().toISOString(),
|
|
24
|
+
data,
|
|
25
|
+
...(!data.error && { success: true }),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const ok = (data) => NextResponse.json(withMetadata(data, HTTP.OK, 'success'));
|
|
29
|
+
export const created = (data) => NextResponse.json(withMetadata(data, HTTP.CREATED, 'created'), { status: HTTP.CREATED });
|
|
30
|
+
export const notFound = (entity = 'Resource') => NextResponse.json(withMetadata({ error: ERROR_MESSAGES.notFound(entity) }, HTTP.NOT_FOUND, 'error'), { status: HTTP.NOT_FOUND });
|
|
31
|
+
export const badRequest = (reason = 'invalid data') => NextResponse.json(withMetadata({ error: ERROR_MESSAGES.invalidRequest(reason) }, HTTP.BAD_REQUEST, 'error'), { status: HTTP.BAD_REQUEST });
|
|
32
|
+
export const unauthorized = (action = 'perform this action') => NextResponse.json(withMetadata({ error: ERROR_MESSAGES.permission.denied }, HTTP.FORBIDDEN, 'error'), { status: HTTP.FORBIDDEN });
|
|
33
|
+
export const serverError = (msg = null) => NextResponse.json(withMetadata({ error: msg || ERROR_MESSAGES.operationFailed('server operation') }, HTTP.INTERNAL_ERROR, 'error'), { status: HTTP.INTERNAL_ERROR });
|
|
34
|
+
|
|
35
|
+
export async function withEntityAccess(entity, action, handler) {
|
|
36
|
+
ensureDb();
|
|
37
|
+
try {
|
|
38
|
+
let spec;
|
|
39
|
+
try { spec = getSpec(entity); } catch { return notFound(entity); }
|
|
40
|
+
const user = await getUser();
|
|
41
|
+
if (!can(user, spec, action)) return unauthorized(action);
|
|
42
|
+
return await handler(spec, user);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
logger.apiError(action, entity, e);
|
|
45
|
+
return serverError(e.message);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function parseParams(params) {
|
|
50
|
+
const { entity, path = [] } = await params;
|
|
51
|
+
const [id, childKey] = path;
|
|
52
|
+
return { entity, id, childKey, path };
|
|
53
|
+
}
|
package/src/lib/api.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { getUser } from '@/engine.server';
|
|
2
|
+
import { getSpec } from '@/config/spec-helpers';
|
|
3
|
+
import { API_ENDPOINTS } from '@/config';
|
|
4
|
+
import { can } from '@/services/permission.service';
|
|
5
|
+
import { list, get, create, update, remove, listWithPagination, search } from '@/lib/query-engine';
|
|
6
|
+
import { validateEntity, validateUpdate, hasErrors } from '@/lib/validate';
|
|
7
|
+
import { broadcastUpdate } from '@/lib/realtime-server';
|
|
8
|
+
import { UnauthorizedError, PermissionError, NotFoundError, ValidationError, AppError } from '@/lib/error-handler';
|
|
9
|
+
import { ok, created, paginated } from '@/lib/response-formatter';
|
|
10
|
+
import { QueryAdapter } from '@/lib/query-string-adapter';
|
|
11
|
+
import { withErrorHandler } from '@/lib/with-error-handler';
|
|
12
|
+
import { HTTP } from '@/config/constants';
|
|
13
|
+
|
|
14
|
+
const createHandler = (entity, action) => async (request, { params, searchParams }) => {
|
|
15
|
+
const user = await getUser();
|
|
16
|
+
if (!user) throw UnauthorizedError('Authentication required');
|
|
17
|
+
|
|
18
|
+
const spec = getSpec(entity);
|
|
19
|
+
if (!spec) throw NotFoundError('entity', entity);
|
|
20
|
+
if (!can(user, spec, action)) throw PermissionError(`Cannot ${action} ${entity}`);
|
|
21
|
+
|
|
22
|
+
const { id } = params;
|
|
23
|
+
const { q, page, pageSize } = QueryAdapter.fromSearchParams(searchParams, spec);
|
|
24
|
+
|
|
25
|
+
if (action === 'list') {
|
|
26
|
+
if (q) {
|
|
27
|
+
const items = search(entity, q);
|
|
28
|
+
return ok({ items });
|
|
29
|
+
}
|
|
30
|
+
const { items, pagination } = listWithPagination(entity, {}, page, pageSize);
|
|
31
|
+
return paginated(items, pagination);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (action === 'get') {
|
|
35
|
+
if (!id) throw new AppError('ID required', 'BAD_REQUEST', HTTP.BAD_REQUEST);
|
|
36
|
+
const item = get(entity, id);
|
|
37
|
+
if (!item) throw NotFoundError(entity, id);
|
|
38
|
+
return ok(item);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (action === 'create') {
|
|
42
|
+
const data = await request.json();
|
|
43
|
+
const errors = await validateEntity(spec, data);
|
|
44
|
+
if (hasErrors(errors)) throw new ValidationError('Validation failed', errors);
|
|
45
|
+
|
|
46
|
+
const result = create(entity, data, user);
|
|
47
|
+
broadcastUpdate(API_ENDPOINTS.entity(entity), 'create', result);
|
|
48
|
+
return created(result);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (action === 'update') {
|
|
52
|
+
if (!id) throw new AppError('ID required', 'BAD_REQUEST', HTTP.BAD_REQUEST);
|
|
53
|
+
const prev = get(entity, id);
|
|
54
|
+
if (!prev) throw NotFoundError(entity, id);
|
|
55
|
+
|
|
56
|
+
const data = await request.json();
|
|
57
|
+
const errors = await validateUpdate(spec, id, data);
|
|
58
|
+
if (hasErrors(errors)) throw new ValidationError('Validation failed', errors);
|
|
59
|
+
|
|
60
|
+
update(entity, id, data, user);
|
|
61
|
+
const result = get(entity, id);
|
|
62
|
+
broadcastUpdate(API_ENDPOINTS.entityId(entity, id), 'update', result);
|
|
63
|
+
return ok(result);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (action === 'delete') {
|
|
67
|
+
if (!id) throw new AppError('ID required', 'BAD_REQUEST', HTTP.BAD_REQUEST);
|
|
68
|
+
if (!get(entity, id)) throw NotFoundError(entity, id);
|
|
69
|
+
|
|
70
|
+
remove(entity, id);
|
|
71
|
+
broadcastUpdate(API_ENDPOINTS.entityId(entity, id), 'delete', { id });
|
|
72
|
+
return ok({ success: true });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
throw new AppError('Unknown action', 'BAD_REQUEST', HTTP.BAD_REQUEST);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const createApiHandler = (entity, action) =>
|
|
79
|
+
withErrorHandler(
|
|
80
|
+
(request, { params, searchParams }) => createHandler(entity, action)(request, { params, searchParams }),
|
|
81
|
+
`API:${entity}:${action}`
|
|
82
|
+
);
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { getDatabase, genId, now } from '@/lib/database-core';
|
|
2
|
+
import { Mutex } from '@/lib/hot-reload/mutex';
|
|
3
|
+
|
|
4
|
+
const db = getDatabase();
|
|
5
|
+
const auditMutex = new Mutex('audit-logger-enhanced');
|
|
6
|
+
|
|
7
|
+
export const LOG_LEVELS = { DEBUG: 'debug', INFO: 'info', WARN: 'warn', ERROR: 'error' };
|
|
8
|
+
export const OPERATION_TYPES = { CREATE: 'create', UPDATE: 'update', DELETE: 'delete', READ: 'read', AUTH: 'auth', AUTHZ: 'authz' };
|
|
9
|
+
|
|
10
|
+
const MAX_LOG_SIZE = 10000;
|
|
11
|
+
const MAX_STACK_DEPTH = 20;
|
|
12
|
+
|
|
13
|
+
export const logStructured = ({ level = LOG_LEVELS.INFO, operation, entityType, entityId, userId, action, details = {}, error = null, performanceMs = null }) => {
|
|
14
|
+
return auditMutex.runExclusive(() => {
|
|
15
|
+
const id = genId();
|
|
16
|
+
const timestamp = now();
|
|
17
|
+
const logEntry = {
|
|
18
|
+
id, timestamp, level, operation, entity_type: entityType, entity_id: entityId, user_id: userId, action,
|
|
19
|
+
details: JSON.stringify(details).substring(0, MAX_LOG_SIZE),
|
|
20
|
+
error_message: error ? String(error.message || error).substring(0, MAX_LOG_SIZE) : null,
|
|
21
|
+
error_stack: error && error.stack ? String(error.stack).split('\n').slice(0, MAX_STACK_DEPTH).join('\n') : null,
|
|
22
|
+
performance_ms: performanceMs,
|
|
23
|
+
};
|
|
24
|
+
db.prepare(`INSERT INTO structured_logs (id, timestamp, level, operation, entity_type, entity_id, user_id, action, details, error_message, error_stack, performance_ms)
|
|
25
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
|
26
|
+
id, timestamp, level, operation, entityType, entityId, userId, action, logEntry.details, logEntry.error_message, logEntry.error_stack, performanceMs
|
|
27
|
+
);
|
|
28
|
+
return id;
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const logCreate = (entityType, entityId, userId, afterState) => {
|
|
33
|
+
return logStructured({ level: LOG_LEVELS.INFO, operation: OPERATION_TYPES.CREATE, entityType, entityId, userId, action: 'create', details: { after_state: afterState } });
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const logUpdate = (entityType, entityId, userId, beforeState, afterState) => {
|
|
37
|
+
return logStructured({ level: LOG_LEVELS.INFO, operation: OPERATION_TYPES.UPDATE, entityType, entityId, userId, action: 'update', details: { before_state: beforeState, after_state: afterState } });
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const logDelete = (entityType, entityId, userId, beforeState) => {
|
|
41
|
+
return logStructured({ level: LOG_LEVELS.INFO, operation: OPERATION_TYPES.DELETE, entityType, entityId, userId, action: 'delete', details: { before_state: beforeState } });
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const logAuthSuccess = (userId, method, metadata = {}) => {
|
|
45
|
+
return logStructured({ level: LOG_LEVELS.INFO, operation: OPERATION_TYPES.AUTH, entityType: 'auth', entityId: userId, userId, action: 'login_success', details: { method, ...metadata } });
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const logAuthFailure = (email, reason, metadata = {}) => {
|
|
49
|
+
return logStructured({ level: LOG_LEVELS.WARN, operation: OPERATION_TYPES.AUTH, entityType: 'auth', entityId: email, userId: null, action: 'login_failure', details: { reason, ...metadata } });
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const logAuthzFailure = (userId, entityType, entityId, requiredPermission, metadata = {}) => {
|
|
53
|
+
return logStructured({ level: LOG_LEVELS.WARN, operation: OPERATION_TYPES.AUTHZ, entityType, entityId, userId, action: 'access_denied', details: { required_permission: requiredPermission, ...metadata } });
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const logPerformance = (operation, entityType, durationMs, userId = null, metadata = {}) => {
|
|
57
|
+
const level = durationMs > 1000 ? LOG_LEVELS.WARN : LOG_LEVELS.DEBUG;
|
|
58
|
+
return logStructured({ level, operation: 'performance', entityType, entityId: null, userId, action: operation, performanceMs: durationMs, details: metadata });
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const logError = (error, context = {}) => {
|
|
62
|
+
return logStructured({ level: LOG_LEVELS.ERROR, operation: 'error', entityType: context.entityType || 'system', entityId: context.entityId || null, userId: context.userId || null, action: context.action || 'error', error, details: context });
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const searchLogs = (filters = {}, page = 1, pageSize = 100) => {
|
|
66
|
+
const wc = [], params = [];
|
|
67
|
+
if (filters.level) { wc.push('level = ?'); params.push(filters.level); }
|
|
68
|
+
if (filters.operation) { wc.push('operation = ?'); params.push(filters.operation); }
|
|
69
|
+
if (filters.entityType) { wc.push('entity_type = ?'); params.push(filters.entityType); }
|
|
70
|
+
if (filters.entityId) { wc.push('entity_id = ?'); params.push(filters.entityId); }
|
|
71
|
+
if (filters.userId) { wc.push('user_id = ?'); params.push(filters.userId); }
|
|
72
|
+
if (filters.action) { wc.push('action = ?'); params.push(filters.action); }
|
|
73
|
+
if (filters.fromDate) { wc.push('timestamp >= ?'); params.push(filters.fromDate); }
|
|
74
|
+
if (filters.toDate) { wc.push('timestamp <= ?'); params.push(filters.toDate); }
|
|
75
|
+
if (filters.searchText) { wc.push('(details LIKE ? OR error_message LIKE ? OR action LIKE ?)'); const escaped = filters.searchText.replace(/[%_]/g, c => '\\' + c); const pat = `%${escaped}%`; params.push(pat, pat, pat); }
|
|
76
|
+
const where = wc.length ? 'WHERE ' + wc.join(' AND ') : '';
|
|
77
|
+
const { count: total } = db.prepare(`SELECT COUNT(*) as count FROM structured_logs ${where}`).get(...params);
|
|
78
|
+
const offset = (page - 1) * pageSize;
|
|
79
|
+
const items = db.prepare(`SELECT * FROM structured_logs ${where} ORDER BY timestamp DESC LIMIT ? OFFSET ?`).all(...params, pageSize, offset);
|
|
80
|
+
return { items: items.map(i => ({ ...i, details: i.details ? JSON.parse(i.details) : null })), pagination: { page, pageSize, total, totalPages: Math.ceil(total / pageSize) } };
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const getLogStats = (fromDate, toDate) => {
|
|
84
|
+
const byLevel = db.prepare('SELECT level, COUNT(*) as count FROM structured_logs WHERE timestamp >= ? AND timestamp <= ? GROUP BY level').all(fromDate, toDate);
|
|
85
|
+
const byOperation = db.prepare('SELECT operation, COUNT(*) as count FROM structured_logs WHERE timestamp >= ? AND timestamp <= ? GROUP BY operation ORDER BY count DESC').all(fromDate, toDate);
|
|
86
|
+
const byEntity = db.prepare('SELECT entity_type, COUNT(*) as count FROM structured_logs WHERE timestamp >= ? AND timestamp <= ? GROUP BY entity_type ORDER BY count DESC LIMIT 20').all(fromDate, toDate);
|
|
87
|
+
const errorRate = db.prepare('SELECT COUNT(*) as total, SUM(CASE WHEN level = ? THEN 1 ELSE 0 END) as errors FROM structured_logs WHERE timestamp >= ? AND timestamp <= ?').get(LOG_LEVELS.ERROR, fromDate, toDate);
|
|
88
|
+
const avgPerf = db.prepare('SELECT AVG(performance_ms) as avg, MAX(performance_ms) as max FROM structured_logs WHERE timestamp >= ? AND timestamp <= ? AND performance_ms IS NOT NULL').get(fromDate, toDate);
|
|
89
|
+
return { byLevel, byOperation, byEntity, errorRate: errorRate.total > 0 ? errorRate.errors / errorRate.total : 0, avgPerformanceMs: avgPerf.avg, maxPerformanceMs: avgPerf.max };
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const rotateLogsOlderThan = (daysOld = 90) => {
|
|
93
|
+
const cutoffTimestamp = now() - (daysOld * 24 * 60 * 60);
|
|
94
|
+
const archiveId = genId();
|
|
95
|
+
const archived = db.prepare('SELECT * FROM structured_logs WHERE timestamp < ?').all(cutoffTimestamp);
|
|
96
|
+
if (archived.length > 0) {
|
|
97
|
+
db.prepare('INSERT INTO archived_logs (archive_id, archived_at, log_data) VALUES (?, ?, ?)').run(archiveId, now(), JSON.stringify(archived));
|
|
98
|
+
db.prepare('DELETE FROM structured_logs WHERE timestamp < ?').run(cutoffTimestamp);
|
|
99
|
+
}
|
|
100
|
+
return { archived: archived.length, archiveId };
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const ensureTables = () => {
|
|
104
|
+
db.exec(`CREATE TABLE IF NOT EXISTS structured_logs (
|
|
105
|
+
id TEXT PRIMARY KEY, timestamp INTEGER NOT NULL, level TEXT NOT NULL, operation TEXT, entity_type TEXT,
|
|
106
|
+
entity_id TEXT, user_id TEXT, action TEXT, details TEXT, error_message TEXT, error_stack TEXT, performance_ms REAL
|
|
107
|
+
)`);
|
|
108
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_structured_logs_timestamp ON structured_logs(timestamp)`);
|
|
109
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_structured_logs_level ON structured_logs(level)`);
|
|
110
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_structured_logs_entity ON structured_logs(entity_type, entity_id)`);
|
|
111
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_structured_logs_user ON structured_logs(user_id)`);
|
|
112
|
+
db.exec(`CREATE TABLE IF NOT EXISTS archived_logs (
|
|
113
|
+
archive_id TEXT PRIMARY KEY, archived_at INTEGER NOT NULL, log_data TEXT NOT NULL
|
|
114
|
+
)`);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
ensureTables();
|