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,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured Logger - Consistent logging across the SDK
|
|
3
|
+
* Simple wrapper with prefix support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const LOG_PREFIXES = {
|
|
7
|
+
API: '[API]',
|
|
8
|
+
DB: '[DB]',
|
|
9
|
+
AUTH: '[AUTH]',
|
|
10
|
+
CONFIG: '[Config]',
|
|
11
|
+
SERVICE: '[Service]',
|
|
12
|
+
system: '[System]',
|
|
13
|
+
email: '[Email]',
|
|
14
|
+
validation: '[Validation]',
|
|
15
|
+
database: '[DB]',
|
|
16
|
+
hook: '[Hook]',
|
|
17
|
+
workflow: '[Workflow]',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create a logger with prefix
|
|
22
|
+
* @param {string} prefix - Log prefix (from LOG_PREFIXES or custom)
|
|
23
|
+
* @returns {object} Logger with methods
|
|
24
|
+
*/
|
|
25
|
+
export function createLogger(prefix) {
|
|
26
|
+
const logger = {
|
|
27
|
+
error: (msg, meta = {}) => {
|
|
28
|
+
console.error(`${prefix} ${msg}`, meta);
|
|
29
|
+
},
|
|
30
|
+
warn: (msg, meta = {}) => {
|
|
31
|
+
console.warn(`${prefix} ${msg}`, meta);
|
|
32
|
+
},
|
|
33
|
+
info: (msg, meta = {}) => {
|
|
34
|
+
console.info(`${prefix} ${msg}`, meta);
|
|
35
|
+
},
|
|
36
|
+
debug: (msg, meta = {}) => {
|
|
37
|
+
if (process.env.DEBUG === 'true') {
|
|
38
|
+
console.debug(`${prefix} ${msg}`, meta);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return logger;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get prefixed logger
|
|
48
|
+
* @param {string} key - Key from LOG_PREFIXES
|
|
49
|
+
* @returns {object}
|
|
50
|
+
*/
|
|
51
|
+
export function getLogger(key) {
|
|
52
|
+
return createLogger(LOG_PREFIXES[key] || `[${key.toUpperCase()}]`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { LOG_PREFIXES };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metrics Collector - Request metrics and stats
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const metrics = {
|
|
6
|
+
requests: [],
|
|
7
|
+
slowQueries: [],
|
|
8
|
+
errors: [],
|
|
9
|
+
startTime: Date.now(),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
let _requestCount = 0;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Record a request
|
|
16
|
+
* @param {string} endpoint
|
|
17
|
+
* @param {string} method
|
|
18
|
+
* @param {number} durationMs
|
|
19
|
+
* @param {number} statusCode
|
|
20
|
+
*/
|
|
21
|
+
export function recordRequest(endpoint, method, durationMs, statusCode) {
|
|
22
|
+
_requestCount++;
|
|
23
|
+
|
|
24
|
+
const record = {
|
|
25
|
+
timestamp: Date.now(),
|
|
26
|
+
endpoint,
|
|
27
|
+
method,
|
|
28
|
+
durationMs,
|
|
29
|
+
statusCode,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
metrics.requests.push(record);
|
|
33
|
+
|
|
34
|
+
// Keep last 1000 requests
|
|
35
|
+
if (metrics.requests.length > 1000) {
|
|
36
|
+
metrics.requests.shift();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Track slow queries (>500ms)
|
|
40
|
+
if (durationMs > 500) {
|
|
41
|
+
metrics.slowQueries.push(record);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Track errors
|
|
45
|
+
if (statusCode >= 400) {
|
|
46
|
+
metrics.errors.push(record);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get all metrics
|
|
52
|
+
* @returns {object}
|
|
53
|
+
*/
|
|
54
|
+
export function getMetrics() {
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
const uptimeSec = Math.floor((now - metrics.startTime) / 1000);
|
|
57
|
+
|
|
58
|
+
const requests = metrics.requests;
|
|
59
|
+
const recent = requests.filter(r => now - r.timestamp < 60000); // last minute
|
|
60
|
+
|
|
61
|
+
const avgDuration = recent.length
|
|
62
|
+
? recent.reduce((sum, r) => sum + r.durationMs, 0) / recent.length
|
|
63
|
+
: 0;
|
|
64
|
+
|
|
65
|
+
const errorRate = recent.length
|
|
66
|
+
? recent.filter(r => r.statusCode >= 400).length / recent.length
|
|
67
|
+
: 0;
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
uptime_seconds: uptimeSec,
|
|
71
|
+
total_requests: _requestCount,
|
|
72
|
+
requests_per_minute: recent.length,
|
|
73
|
+
average_response_time_ms: Math.round(avgDuration),
|
|
74
|
+
error_rate_percent: Math.round(errorRate * 100),
|
|
75
|
+
slow_queries: metrics.slowQueries.slice(-10),
|
|
76
|
+
recent_errors: metrics.errors.slice(-10),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get summary statistics
|
|
82
|
+
* @returns {object}
|
|
83
|
+
*/
|
|
84
|
+
export function getSummary() {
|
|
85
|
+
const m = getMetrics();
|
|
86
|
+
return {
|
|
87
|
+
requests: m.total_requests,
|
|
88
|
+
avgMs: m.average_response_time_ms,
|
|
89
|
+
errors: m.recent_errors.length,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Reset metrics (for testing)
|
|
95
|
+
*/
|
|
96
|
+
export function resetMetrics() {
|
|
97
|
+
metrics.requests = [];
|
|
98
|
+
metrics.slowQueries = [];
|
|
99
|
+
metrics.errors = [];
|
|
100
|
+
_requestCount = 0;
|
|
101
|
+
metrics.startTime = Date.now();
|
|
102
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function minifyJS(code) {
|
|
2
|
+
return code
|
|
3
|
+
.replace(/\/\/.*$/gm, '')
|
|
4
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
5
|
+
.replace(/^\s+/gm, '')
|
|
6
|
+
.replace(/\s+$/gm, '')
|
|
7
|
+
.replace(/\n+/g, '\n')
|
|
8
|
+
.replace(/\s*([{}()\[\];:,=<>!+\-*\/&|?])\s*/g, '$1')
|
|
9
|
+
.trim();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function minifyCSS(css) {
|
|
13
|
+
return css
|
|
14
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
15
|
+
.replace(/\s+/g, ' ')
|
|
16
|
+
.replace(/\s*([{}:;,>~+])\s*/g, '$1')
|
|
17
|
+
.replace(/;}/g, '}')
|
|
18
|
+
.trim();
|
|
19
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { startMonitoring as startResourceMonitoring } from '@/lib/resource-monitor.js'
|
|
2
|
+
import { checkAllThresholds, registerAlertHandler } from '@/lib/alert-manager.js'
|
|
3
|
+
import { getAllMetrics } from '@/lib/metrics-collector.js'
|
|
4
|
+
import { info, warn, error } from '@/lib/log-aggregator.js'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
import { fileURLToPath } from 'url'
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
let alertCheckInterval = null
|
|
10
|
+
let initialized = false
|
|
11
|
+
|
|
12
|
+
function initializeMonitoring(config = {}) {
|
|
13
|
+
if (initialized) {
|
|
14
|
+
warn('Monitoring already initialized')
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
resourceInterval = 5000,
|
|
20
|
+
alertCheckInterval: alertInterval = 10000,
|
|
21
|
+
dbPath = path.join(__dirname, '../../data/app.db')
|
|
22
|
+
} = config
|
|
23
|
+
|
|
24
|
+
startResourceMonitoring(resourceInterval, dbPath)
|
|
25
|
+
info('Resource monitoring started', { interval: resourceInterval })
|
|
26
|
+
|
|
27
|
+
alertCheckInterval = setInterval(() => {
|
|
28
|
+
try {
|
|
29
|
+
const metrics = getAllMetrics()
|
|
30
|
+
checkAllThresholds(metrics)
|
|
31
|
+
} catch (err) {
|
|
32
|
+
error('Alert check failed', { error: err.message })
|
|
33
|
+
}
|
|
34
|
+
}, alertInterval)
|
|
35
|
+
|
|
36
|
+
info('Alert checking started', { interval: alertInterval })
|
|
37
|
+
|
|
38
|
+
registerAlertHandler((alert) => {
|
|
39
|
+
if (alert.severity === 'critical') {
|
|
40
|
+
error(`CRITICAL ALERT: ${alert.message}`, alert.metadata)
|
|
41
|
+
} else if (alert.severity === 'warning') {
|
|
42
|
+
warn(`WARNING: ${alert.message}`, alert.metadata)
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
info('Alert handlers registered')
|
|
47
|
+
|
|
48
|
+
initialized = true
|
|
49
|
+
info('Monitoring system initialized')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function shutdownMonitoring() {
|
|
53
|
+
if (!initialized) return
|
|
54
|
+
|
|
55
|
+
if (alertCheckInterval) {
|
|
56
|
+
clearInterval(alertCheckInterval)
|
|
57
|
+
alertCheckInterval = null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { stopMonitoring } = await import('@/lib/resource-monitor.js')
|
|
61
|
+
stopMonitoring()
|
|
62
|
+
|
|
63
|
+
initialized = false
|
|
64
|
+
info('Monitoring system shutdown')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { initializeMonitoring, shutdownMonitoring }
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const MAX_BODY_SIZE = 10 * 1024 * 1024;
|
|
2
|
+
|
|
3
|
+
export class NextRequest {
|
|
4
|
+
constructor(req, body, url) {
|
|
5
|
+
this.method = req.method;
|
|
6
|
+
this.headers = req.headers;
|
|
7
|
+
this.url = url;
|
|
8
|
+
this.body = body;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async json() {
|
|
12
|
+
return this.body;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async text() {
|
|
16
|
+
return typeof this.body === 'string' ? this.body : JSON.stringify(this.body);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class NextResponse {
|
|
21
|
+
constructor(body, init = {}) {
|
|
22
|
+
this.body = body;
|
|
23
|
+
this.status = init.status || 200;
|
|
24
|
+
this.headers = new Map(Object.entries(init.headers || {}));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async json() {
|
|
28
|
+
return this.body;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static json(body, init = {}) {
|
|
32
|
+
return new NextResponse(body, init);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function readBody(req) {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
let data = '';
|
|
39
|
+
let size = 0;
|
|
40
|
+
req.on('data', (chunk) => {
|
|
41
|
+
size += chunk.length;
|
|
42
|
+
if (size > MAX_BODY_SIZE) {
|
|
43
|
+
req.destroy();
|
|
44
|
+
reject(new Error('Request body too large'));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
data += chunk;
|
|
48
|
+
});
|
|
49
|
+
req.on('end', () => {
|
|
50
|
+
try {
|
|
51
|
+
resolve(data ? JSON.parse(data) : {});
|
|
52
|
+
} catch {
|
|
53
|
+
resolve(data);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
req.on('error', (err) => reject(err));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const HEADER_MAP = {
|
|
61
|
+
'content-type': 'Content-Type',
|
|
62
|
+
'content-length': 'Content-Length',
|
|
63
|
+
'set-cookie': 'Set-Cookie',
|
|
64
|
+
'cache-control': 'Cache-Control',
|
|
65
|
+
'expires': 'Expires',
|
|
66
|
+
'etag': 'ETag',
|
|
67
|
+
'last-modified': 'Last-Modified',
|
|
68
|
+
'location': 'Location',
|
|
69
|
+
'date': 'Date',
|
|
70
|
+
'connection': 'Connection',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export function normalizeHeaderName(key) {
|
|
74
|
+
return HEADER_MAP[key.toLowerCase()] || key;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function registerGlobals() {
|
|
78
|
+
globalThis.NextRequest = NextRequest;
|
|
79
|
+
globalThis.NextResponse = NextResponse;
|
|
80
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
+
|
|
3
|
+
const requestContext = new AsyncLocalStorage();
|
|
4
|
+
|
|
5
|
+
export class NextResponse {
|
|
6
|
+
constructor(body, init = {}) {
|
|
7
|
+
this.body = body;
|
|
8
|
+
this.status = init.status || 200;
|
|
9
|
+
this.headers = new Map(Object.entries(init.headers || {}));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async json() {
|
|
13
|
+
return this.body;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static json(body, init = {}) {
|
|
17
|
+
return new NextResponse(body, init);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static redirect(url, status = 307) {
|
|
21
|
+
return new NextResponse(null, {
|
|
22
|
+
status,
|
|
23
|
+
headers: { Location: url }
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let fallbackRequest = null;
|
|
29
|
+
let fallbackResponse = null;
|
|
30
|
+
|
|
31
|
+
export function setCurrentRequest(req) {
|
|
32
|
+
fallbackRequest = req;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function setCurrentResponse(res) {
|
|
36
|
+
fallbackResponse = res;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function runWithContext(req, res, fn) {
|
|
40
|
+
return requestContext.run({ req, res }, fn);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getRequest() {
|
|
44
|
+
const store = requestContext.getStore();
|
|
45
|
+
return store?.req || fallbackRequest;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getResponse() {
|
|
49
|
+
const store = requestContext.getStore();
|
|
50
|
+
return store?.res || fallbackResponse;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function cookies() {
|
|
54
|
+
if (typeof window !== 'undefined') {
|
|
55
|
+
throw new Error('cookies() should only be called on the server side');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const req = getRequest();
|
|
59
|
+
const res = getResponse();
|
|
60
|
+
const cookieHeader = req?.headers?.cookie || '';
|
|
61
|
+
const cookieMap = {};
|
|
62
|
+
|
|
63
|
+
if (cookieHeader) {
|
|
64
|
+
cookieHeader.split(';').forEach(cookie => {
|
|
65
|
+
const [name, value] = cookie.split('=').map(s => s.trim());
|
|
66
|
+
if (name) cookieMap[name] = decodeURIComponent(value || '');
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
get: (name) => {
|
|
72
|
+
const value = cookieMap[name];
|
|
73
|
+
return value ? { name, value } : undefined;
|
|
74
|
+
},
|
|
75
|
+
set: (name, value, options = {}) => {
|
|
76
|
+
if (!res) return;
|
|
77
|
+
let setCookieValue = `${name}=${encodeURIComponent(value)}`;
|
|
78
|
+
if (options.path) setCookieValue += `; Path=${options.path}`;
|
|
79
|
+
if (options.maxAge) setCookieValue += `; Max-Age=${options.maxAge}`;
|
|
80
|
+
if (options.expires) setCookieValue += `; Expires=${options.expires}`;
|
|
81
|
+
if (options.secure) setCookieValue += '; Secure';
|
|
82
|
+
if (options.httpOnly) setCookieValue += '; HttpOnly';
|
|
83
|
+
if (options.sameSite) setCookieValue += `; SameSite=${options.sameSite}`;
|
|
84
|
+
const existing = res.getHeader('Set-Cookie') || [];
|
|
85
|
+
const setCookies = Array.isArray(existing) ? existing : [existing];
|
|
86
|
+
res.setHeader('Set-Cookie', [...setCookies, setCookieValue]);
|
|
87
|
+
},
|
|
88
|
+
delete: (name) => {
|
|
89
|
+
if (!res) return;
|
|
90
|
+
const setCookieValue = `${name}=; Path=/; Max-Age=0`;
|
|
91
|
+
const existing = res.getHeader('Set-Cookie') || [];
|
|
92
|
+
const setCookies = Array.isArray(existing) ? existing : [existing];
|
|
93
|
+
res.setHeader('Set-Cookie', [...setCookies, setCookieValue]);
|
|
94
|
+
},
|
|
95
|
+
getAll: () => {
|
|
96
|
+
return Object.entries(cookieMap).map(([name, value]) => ({ name, value }));
|
|
97
|
+
},
|
|
98
|
+
has: (name) => {
|
|
99
|
+
return name in cookieMap;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function headers() {
|
|
105
|
+
return {
|
|
106
|
+
get: (name) => null,
|
|
107
|
+
getSetCookie: () => [],
|
|
108
|
+
has: (name) => false,
|
|
109
|
+
entries: () => [],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function revalidatePath() {}
|
|
114
|
+
|
|
115
|
+
export function revalidateTag() {}
|
|
116
|
+
|
|
117
|
+
export function redirect(path, status = 302) {
|
|
118
|
+
if (typeof window !== 'undefined') {
|
|
119
|
+
window.location.href = path;
|
|
120
|
+
} else {
|
|
121
|
+
const error = new Error(`Redirect to ${path}`);
|
|
122
|
+
error.type = 'redirect';
|
|
123
|
+
error.location = path;
|
|
124
|
+
error.status = status;
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function notFound() {
|
|
130
|
+
if (typeof window !== 'undefined') {
|
|
131
|
+
window.location.href = '/404';
|
|
132
|
+
} else {
|
|
133
|
+
throw new Error('notFound()');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const metrics = new Map()
|
|
2
|
+
const thresholds = { render: 100, query: 50, api: 200, total: 500 }
|
|
3
|
+
let enabled = process.env.PERF_MONITOR !== 'false'
|
|
4
|
+
|
|
5
|
+
export function startTimer(key) {
|
|
6
|
+
if (!enabled) return null
|
|
7
|
+
const start = process.hrtime.bigint()
|
|
8
|
+
return { key, start }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function endTimer(timer) {
|
|
12
|
+
if (!enabled || !timer) return 0
|
|
13
|
+
const end = process.hrtime.bigint()
|
|
14
|
+
const ms = Number(end - timer.start) / 1000000
|
|
15
|
+
recordMetric(timer.key, ms)
|
|
16
|
+
return ms
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function recordMetric(key, value) {
|
|
20
|
+
if (!enabled) return
|
|
21
|
+
if (!metrics.has(key)) metrics.set(key, [])
|
|
22
|
+
const arr = metrics.get(key)
|
|
23
|
+
arr.push({ value, ts: Date.now() })
|
|
24
|
+
if (arr.length > 1000) arr.shift()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getMetrics(key) {
|
|
28
|
+
if (!key) return Object.fromEntries(metrics.entries())
|
|
29
|
+
return metrics.get(key) || []
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getStats(key) {
|
|
33
|
+
const data = metrics.get(key) || []
|
|
34
|
+
if (data.length === 0) return null
|
|
35
|
+
const values = data.map(d => d.value)
|
|
36
|
+
values.sort((a, b) => a - b)
|
|
37
|
+
return {
|
|
38
|
+
count: values.length,
|
|
39
|
+
min: values[0],
|
|
40
|
+
max: values[values.length - 1],
|
|
41
|
+
avg: values.reduce((a, b) => a + b, 0) / values.length,
|
|
42
|
+
p50: values[Math.floor(values.length * 0.5)],
|
|
43
|
+
p95: values[Math.floor(values.length * 0.95)],
|
|
44
|
+
p99: values[Math.floor(values.length * 0.99)]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function checkThreshold(key, value) {
|
|
49
|
+
if (!enabled) return true
|
|
50
|
+
const limit = thresholds[key]
|
|
51
|
+
if (limit && value > limit) {
|
|
52
|
+
console.warn(`[PERF] ${key} exceeded threshold: ${value.toFixed(2)}ms > ${limit}ms`)
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
return true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function setThreshold(key, ms) {
|
|
59
|
+
thresholds[key] = ms
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function clearMetrics() {
|
|
63
|
+
metrics.clear()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function enable() {
|
|
67
|
+
enabled = true
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function disable() {
|
|
71
|
+
enabled = false
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function measure(key, fn) {
|
|
75
|
+
const timer = startTimer(key)
|
|
76
|
+
try {
|
|
77
|
+
const result = fn()
|
|
78
|
+
if (result instanceof Promise) {
|
|
79
|
+
return result.finally(() => {
|
|
80
|
+
const ms = endTimer(timer)
|
|
81
|
+
checkThreshold(key, ms)
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
const ms = endTimer(timer)
|
|
85
|
+
checkThreshold(key, ms)
|
|
86
|
+
return result
|
|
87
|
+
} catch (err) {
|
|
88
|
+
endTimer(timer)
|
|
89
|
+
throw err
|
|
90
|
+
}
|
|
91
|
+
}
|