thatcher 1.0.41 → 1.0.43
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/README.md +27 -0
- package/package.json +2 -5
- package/src/app/api/audit/dashboard/route.js +2 -2
- package/src/app/api/audit/logs/route.js +2 -2
- package/src/app/api/audit/permissions/[id]/route.js +2 -2
- package/src/app/api/audit/permissions/route.js +3 -3
- package/src/app/api/audit/permissions/stats/route.js +2 -2
- package/src/app/api/audit/route.js +2 -3
- package/src/app/api/audit/stats/route.js +2 -2
- package/src/app/api/auth/google/callback/route.js +1 -1
- package/src/app/api/auth/google/route.js +1 -1
- package/src/app/api/auth/login/route.js +1 -1
- package/src/app/api/auth/logout/route.js +1 -1
- package/src/app/api/auth/me/route.js +1 -1
- package/src/app/api/auth/mwr-bridge/route.js +1 -1
- package/src/app/api/auth/password-reset/route.js +1 -1
- package/src/app/api/csrf-token/route.js +1 -1
- package/src/app/api/debug/[[...path]]/route.js +2 -2
- package/src/app/api/debug/config/route.js +1 -1
- package/src/app/api/debug/hooks/route.js +1 -1
- package/src/app/api/debug/plugins/route.js +1 -1
- package/src/app/api/debug/sqlite/route.js +1 -1
- package/src/app/api/debug/sync/route.js +1 -1
- package/src/app/api/debug/workflow/route.js +1 -1
- package/src/app/api/domains/[domain]/route.js +1 -2
- package/src/app/api/domains/route.js +1 -1
- package/src/app/api/email/allocate/batch/route.js +1 -1
- package/src/app/api/email/allocate/route.js +1 -1
- package/src/app/api/email/receive/route.js +1 -1
- package/src/app/api/email/send/route.js +1 -1
- package/src/app/api/email/unallocated/route.js +1 -1
- package/src/app/api/files/[id]/route.js +1 -1
- package/src/app/api/health/route.js +1 -2
- package/src/app/api/metrics/route.js +1 -2
- package/src/engine.js +3 -3
- package/src/engine.server.js +3 -3
- package/src/index.js +35 -10
- package/src/lib/action-factory.js +1 -1
- package/src/lib/api-helpers.js +1 -1
- package/src/lib/api.js +3 -4
- package/src/lib/audit-logger-enhanced.js +1 -1
- package/src/lib/auth-middleware.js +1 -1
- package/src/lib/auth-route-helpers.js +1 -1
- package/src/lib/{busybase-adapter.js → busybase/adapter.js} +3 -3
- package/src/lib/{busybase-audit-reads.js → busybase/audit-reads.js} +1 -1
- package/src/lib/{busybase-audit.js → busybase/audit.js} +2 -2
- package/src/lib/{busybase-lucia-adapter.js → busybase/lucia-adapter.js} +1 -1
- package/src/lib/{busybase-store.js → busybase/store.js} +3 -3
- package/src/lib/config-generator-engine.js +13 -1
- package/src/lib/crud-action-helpers.js +1 -1
- package/src/lib/crud-handlers.js +4 -4
- package/src/lib/email-sender.js +1 -1
- package/src/lib/errors/index.js +3 -0
- package/src/lib/{error-recovery.js → errors/recovery.js} +120 -4
- package/src/lib/{error-handler.js → errors/types.js} +1 -1
- package/src/lib/errors/wrap.js +225 -0
- package/src/lib/events-engine.js +1 -1
- package/src/lib/hot-reload/index.js +34 -0
- package/src/lib/index.js +5 -5
- package/src/lib/keyed-cache.js +112 -0
- package/src/lib/monitor.js +285 -0
- package/src/lib/next-shim.js +294 -0
- package/src/lib/observability-init.js +213 -0
- package/src/lib/perf.js +468 -0
- package/src/lib/query-cache.js +40 -46
- package/src/lib/realtime-server.js +18 -0
- package/src/lib/render-cache.js +14 -28
- package/src/lib/{request-tracing.js → request-trace.js} +57 -2
- package/src/lib/response-formatter.js +18 -0
- package/src/lib/route-helpers.js +1 -1
- package/src/lib/state-protocol.js +13 -0
- package/src/lib/state-transport-client.js +6 -0
- package/src/lib/state-transport-reconnect.js +5 -0
- package/src/lib/state-transport-server.js +8 -0
- package/src/lib/utils.js +1 -1
- package/src/lib/{validate.js → validation/entity-validators.js} +9 -8
- package/src/lib/validation/index.js +1 -1
- package/src/lib/validation/security-validators.js +1 -1
- package/src/lib/validation-middleware.js +2 -2
- package/src/lib/workflow-engine.js +23 -3
- package/src/lib/xstate-workflow-engine.js +18 -2
- package/src/services/collaborator-role.service.js +2 -2
- package/src/services/notification-engine.js +5 -5
- package/src/services/permission.service.js +1 -1
- package/src/ui/format-helpers.js +1 -4
- package/src/ui/highlight-threading-renderer.js +1 -1
- package/src/ui/page-handler-admin.js +6 -5
- package/src/ui/page-handler-helpers.js +2 -2
- package/src/ui/page-handler-reviews.js +4 -4
- package/src/ui/page-handler-rfi.js +1 -1
- package/src/ui/page-handler.js +2 -2
- package/src/ui/render-helpers.js +1 -1
- package/src/ui/renderer.js +1 -1
- package/src/ui/{review-comparison-renderer.js → review/comparison.js} +1 -1
- package/src/ui/{review-detail-renderer.js → review/detail-renderer.js} +3 -3
- package/src/ui/review/index.js +32 -0
- package/src/ui/{review-mwr-renderer.js → review/mwr.js} +1 -1
- package/src/ui/{review-renderer.js → review/renderer.js} +1 -1
- package/src/ui/{settings-renderer.js → settings/home.js} +7 -34
- package/src/ui/{settings-renderer-advanced.js → settings/review.js} +117 -56
- package/src/ui/settings/shared.js +45 -0
- package/src/ui/{settings-renderer-teams.js → settings/teams.js} +1 -2
- package/src/ui/settings/templates.js +116 -0
- package/src/lib/api-error-wrapper.js +0 -125
- package/src/lib/db-monitor.js +0 -127
- package/src/lib/error-resilience.js +0 -132
- package/src/lib/monitoring-init.js +0 -67
- package/src/lib/next-compat.js +0 -94
- package/src/lib/next-polyfills.js +0 -135
- package/src/lib/observability-bootstrap.js +0 -116
- package/src/lib/perf-monitor.js +0 -94
- package/src/lib/perf-profiler.js +0 -233
- package/src/lib/query-perf.js +0 -117
- package/src/lib/request-tracker.js +0 -43
- package/src/lib/resource-monitor.js +0 -120
- package/src/lib/validators.js +0 -67
- package/src/lib/with-error-handler.js +0 -31
- package/src/ui/settings-renderer-advanced2.js +0 -159
- /package/src/ui/{review-detail-panels.js → review/detail-panels.js} +0 -0
- /package/src/ui/{review-detail-script.js → review/detail-script.js} +0 -0
- /package/src/ui/{review-widgets.js → review/widgets.js} +0 -0
- /package/src/ui/{review-zone-nav.js → review/zone-nav.js} +0 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// Merged from db-monitor.js + resource-monitor.js into one pluggable-probe
|
|
2
|
+
// monitor rather than two hardcoded monitor types. A "probe" is anything
|
|
3
|
+
// shaped like { name, start(), stop(), collect() } registered into the
|
|
4
|
+
// shared registry below; the db and resource probes that used to be two
|
|
5
|
+
// separate files are now just the two probes registered by default. Every
|
|
6
|
+
// original exported function name is preserved unchanged for call-site
|
|
7
|
+
// compatibility (getDatabaseStats, clearDatabaseStats, wrapDatabase,
|
|
8
|
+
// getCPUUsage, getMemoryUsage, getDiskUsage, collectResources,
|
|
9
|
+
// startMonitoring, stopMonitoring, getCurrentResources).
|
|
10
|
+
|
|
11
|
+
import os from 'os';
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import { recordDatabase, recordResource } from './metrics-collector.js';
|
|
14
|
+
import { createLogger } from './logger.js';
|
|
15
|
+
|
|
16
|
+
const log = createLogger('[Monitor]');
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Pluggable probe registry
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
const probes = new Map();
|
|
22
|
+
|
|
23
|
+
function registerProbe(name, probe) {
|
|
24
|
+
probes.set(name, probe);
|
|
25
|
+
return probe;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getProbe(name) {
|
|
29
|
+
return probes.get(name);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function listProbes() {
|
|
33
|
+
return Array.from(probes.keys());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// db probe — from db-monitor.js
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
const dbStats = {
|
|
40
|
+
connections: 0,
|
|
41
|
+
activeQueries: 0,
|
|
42
|
+
locks: 0,
|
|
43
|
+
slowQueries: []
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function recordSlowQuery(sql, duration) {
|
|
47
|
+
dbStats.slowQueries.push({
|
|
48
|
+
sql: sql.substring(0, 200),
|
|
49
|
+
duration,
|
|
50
|
+
timestamp: Date.now()
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (dbStats.slowQueries.length > 100) {
|
|
54
|
+
dbStats.slowQueries.shift();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function wrapDatabase(db) {
|
|
59
|
+
const originalPrepare = db.prepare.bind(db);
|
|
60
|
+
|
|
61
|
+
db.prepare = function(sql) {
|
|
62
|
+
const stmt = originalPrepare(sql);
|
|
63
|
+
const originalRun = stmt.run.bind(stmt);
|
|
64
|
+
const originalGet = stmt.get.bind(stmt);
|
|
65
|
+
const originalAll = stmt.all.bind(stmt);
|
|
66
|
+
|
|
67
|
+
stmt.run = function(...args) {
|
|
68
|
+
const start = process.hrtime.bigint();
|
|
69
|
+
dbStats.activeQueries++;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const result = originalRun(...args);
|
|
73
|
+
const duration = Number(process.hrtime.bigint() - start) / 1000000;
|
|
74
|
+
|
|
75
|
+
recordDatabase('run', duration, sql);
|
|
76
|
+
if (duration > 100) {
|
|
77
|
+
recordSlowQuery(sql, duration);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return result;
|
|
81
|
+
} catch (err) {
|
|
82
|
+
recordDatabase('error', 0, sql);
|
|
83
|
+
throw err;
|
|
84
|
+
} finally {
|
|
85
|
+
dbStats.activeQueries--;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
stmt.get = function(...args) {
|
|
90
|
+
const start = process.hrtime.bigint();
|
|
91
|
+
dbStats.activeQueries++;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const result = originalGet(...args);
|
|
95
|
+
const duration = Number(process.hrtime.bigint() - start) / 1000000;
|
|
96
|
+
|
|
97
|
+
recordDatabase('get', duration, sql);
|
|
98
|
+
if (duration > 100) {
|
|
99
|
+
recordSlowQuery(sql, duration);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return result;
|
|
103
|
+
} catch (err) {
|
|
104
|
+
recordDatabase('error', 0, sql);
|
|
105
|
+
throw err;
|
|
106
|
+
} finally {
|
|
107
|
+
dbStats.activeQueries--;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
stmt.all = function(...args) {
|
|
112
|
+
const start = process.hrtime.bigint();
|
|
113
|
+
dbStats.activeQueries++;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const result = originalAll(...args);
|
|
117
|
+
const duration = Number(process.hrtime.bigint() - start) / 1000000;
|
|
118
|
+
|
|
119
|
+
recordDatabase('all', duration, sql);
|
|
120
|
+
if (duration > 100) {
|
|
121
|
+
recordSlowQuery(sql, duration);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return result;
|
|
125
|
+
} catch (err) {
|
|
126
|
+
recordDatabase('error', 0, sql);
|
|
127
|
+
throw err;
|
|
128
|
+
} finally {
|
|
129
|
+
dbStats.activeQueries--;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return stmt;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
return db;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getDatabaseStats() {
|
|
140
|
+
return {
|
|
141
|
+
connections: dbStats.connections,
|
|
142
|
+
activeQueries: dbStats.activeQueries,
|
|
143
|
+
locks: dbStats.locks,
|
|
144
|
+
slowQueries: dbStats.slowQueries.slice(-10)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function clearDatabaseStats() {
|
|
149
|
+
dbStats.slowQueries.length = 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
registerProbe('db', {
|
|
153
|
+
name: 'db',
|
|
154
|
+
start: () => {},
|
|
155
|
+
stop: () => {},
|
|
156
|
+
collect: getDatabaseStats,
|
|
157
|
+
clear: clearDatabaseStats,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// resource probe — from resource-monitor.js
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
let monitoringInterval = null;
|
|
164
|
+
let dbPath = null;
|
|
165
|
+
|
|
166
|
+
export function getCPUUsage() {
|
|
167
|
+
const cpus = os.cpus();
|
|
168
|
+
let totalIdle = 0;
|
|
169
|
+
let totalTick = 0;
|
|
170
|
+
|
|
171
|
+
cpus.forEach(cpu => {
|
|
172
|
+
for (const type in cpu.times) {
|
|
173
|
+
totalTick += cpu.times[type];
|
|
174
|
+
}
|
|
175
|
+
totalIdle += cpu.times.idle;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const idle = totalIdle / cpus.length;
|
|
179
|
+
const total = totalTick / cpus.length;
|
|
180
|
+
const usage = 1 - (idle / total);
|
|
181
|
+
|
|
182
|
+
return Math.max(0, Math.min(1, usage));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function getMemoryUsage() {
|
|
186
|
+
const totalMem = os.totalmem();
|
|
187
|
+
const freeMem = os.freemem();
|
|
188
|
+
const usedMem = totalMem - freeMem;
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
total: totalMem,
|
|
192
|
+
used: usedMem,
|
|
193
|
+
free: freeMem,
|
|
194
|
+
percentage: usedMem / totalMem
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function getDiskUsage() {
|
|
199
|
+
if (!dbPath) return null;
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const stats = fs.statSync(dbPath);
|
|
203
|
+
const totalSpace = 1024 * 1024 * 1024 * 100;
|
|
204
|
+
const usedSpace = stats.size;
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
total: totalSpace,
|
|
208
|
+
used: usedSpace,
|
|
209
|
+
percentage: usedSpace / totalSpace
|
|
210
|
+
};
|
|
211
|
+
} catch (err) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function collectResources() {
|
|
217
|
+
try {
|
|
218
|
+
const cpu = getCPUUsage();
|
|
219
|
+
const memory = getMemoryUsage();
|
|
220
|
+
const disk = getDiskUsage();
|
|
221
|
+
|
|
222
|
+
recordResource(
|
|
223
|
+
cpu,
|
|
224
|
+
memory.percentage,
|
|
225
|
+
disk?.percentage
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
return { cpu, memory, disk };
|
|
229
|
+
} catch (err) {
|
|
230
|
+
log.error('collection error:', { message: err?.message || String(err) });
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function startMonitoring(interval = 5000, databasePath = null) {
|
|
236
|
+
if (monitoringInterval) {
|
|
237
|
+
stopMonitoring();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
dbPath = databasePath;
|
|
241
|
+
|
|
242
|
+
monitoringInterval = setInterval(() => {
|
|
243
|
+
collectResources();
|
|
244
|
+
}, interval);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function stopMonitoring() {
|
|
248
|
+
if (monitoringInterval) {
|
|
249
|
+
clearInterval(monitoringInterval);
|
|
250
|
+
monitoringInterval = null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function getCurrentResources() {
|
|
255
|
+
return collectResources();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
registerProbe('resource', {
|
|
259
|
+
name: 'resource',
|
|
260
|
+
start: startMonitoring,
|
|
261
|
+
stop: stopMonitoring,
|
|
262
|
+
collect: getCurrentResources,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
// Probe registry surface (new — the pluggable-probe pattern the merge adds)
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
export { registerProbe, getProbe, listProbes };
|
|
269
|
+
|
|
270
|
+
if (typeof globalThis !== 'undefined') {
|
|
271
|
+
globalThis.__dbMonitor = {
|
|
272
|
+
getDatabaseStats,
|
|
273
|
+
clearDatabaseStats
|
|
274
|
+
};
|
|
275
|
+
globalThis.__resources = {
|
|
276
|
+
getCurrentResources,
|
|
277
|
+
startMonitoring,
|
|
278
|
+
stopMonitoring
|
|
279
|
+
};
|
|
280
|
+
globalThis.__monitor = {
|
|
281
|
+
registerProbe,
|
|
282
|
+
getProbe,
|
|
283
|
+
listProbes,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// next-shim.js
|
|
2
|
+
//
|
|
3
|
+
// Merged compatibility layer standing in for parts of the real Next.js API
|
|
4
|
+
// surface, so thatcher's route handlers (src/app/api/**/route.js) and
|
|
5
|
+
// server.js can run outside actual Next.js (e.g. under plain Node/Express).
|
|
6
|
+
//
|
|
7
|
+
// This is the union of the former src/lib/next-compat.js and
|
|
8
|
+
// src/lib/next-polyfills.js, combined verbatim (no behavior changes).
|
|
9
|
+
//
|
|
10
|
+
// Supports:
|
|
11
|
+
// - NextRequest: constructed from a raw Node IncomingMessage + parsed body
|
|
12
|
+
// + url. `.method`, `.headers` (Proxy giving both `.get(name)` /
|
|
13
|
+
// `.has(name)` fetch-style access AND raw `headers['x']` index access,
|
|
14
|
+
// case-insensitive via lowercasing), `.url`, `.body`. `.json()` returns
|
|
15
|
+
// the already-parsed body; `.text()` returns it as a string (or
|
|
16
|
+
// JSON.stringify's it if not already a string).
|
|
17
|
+
// - NextResponse: `.body`, `.status` (default 200), `.headers` (a Map).
|
|
18
|
+
// `.json()` returns `.body`. Static `NextResponse.json(body, init)`
|
|
19
|
+
// constructs one. Static `NextResponse.redirect(url, status = 307)`
|
|
20
|
+
// constructs a redirect response with a `Location` header.
|
|
21
|
+
// - readBody(req): buffers a raw Node request stream into a JSON-parsed
|
|
22
|
+
// object (falls back to the raw string on parse failure), with a
|
|
23
|
+
// 10 MB body-size cap that destroys the request and rejects if
|
|
24
|
+
// exceeded.
|
|
25
|
+
// - normalizeHeaderName(key): maps a lowercased header name to its
|
|
26
|
+
// canonical mixed-case form for a small fixed set of common headers
|
|
27
|
+
// (content-type, content-length, set-cookie, cache-control, expires,
|
|
28
|
+
// etag, last-modified, location, date, connection); passes through
|
|
29
|
+
// unrecognized keys unchanged.
|
|
30
|
+
// - registerGlobals(): assigns NextRequest/NextResponse onto globalThis.
|
|
31
|
+
// - cookies() (async, server-side only -- throws if `window` is defined):
|
|
32
|
+
// reads the request's `Cookie` header (via AsyncLocalStorage-scoped
|
|
33
|
+
// request, falling back to the last value passed to
|
|
34
|
+
// setCurrentRequest()) into a name->value map, and returns an object
|
|
35
|
+
// with `.get(name)` (returns `{name, value}` or undefined), `.set(name,
|
|
36
|
+
// value, options)` / `.delete(name)` (append a Set-Cookie header onto
|
|
37
|
+
// the AsyncLocalStorage-scoped response via `res.setHeader`, supporting
|
|
38
|
+
// `path`/`maxAge`/`expires`/`secure`/`httpOnly`/`sameSite` options), and
|
|
39
|
+
// `.getAll()` / `.has(name)`.
|
|
40
|
+
// - headers(): a STUB read-only headers accessor -- `.get()` always
|
|
41
|
+
// returns null, `.has()` always returns false, `.getSetCookie()` /
|
|
42
|
+
// `.entries()` always return an empty array. It does NOT read the
|
|
43
|
+
// actual request headers (unlike `cookies()`, which does).
|
|
44
|
+
// - setCurrentRequest(req) / setCurrentResponse(res): set a module-level
|
|
45
|
+
// fallback request/response used by cookies() when no
|
|
46
|
+
// AsyncLocalStorage context is active.
|
|
47
|
+
// - runWithContext(req, res, fn): runs `fn` inside an AsyncLocalStorage
|
|
48
|
+
// context carrying `{req, res}`, so cookies() can resolve the current
|
|
49
|
+
// request/response without them being threaded through every call.
|
|
50
|
+
// - revalidatePath() / revalidateTag(): both no-op stubs.
|
|
51
|
+
// - redirect(path, status = 302): server-side throws an Error tagged
|
|
52
|
+
// `error.type = 'redirect'` with `.location`/`.status` (for a caller to
|
|
53
|
+
// catch and translate into an actual HTTP redirect); client-side
|
|
54
|
+
// (`typeof window !== 'undefined'`) sets `window.location.href`
|
|
55
|
+
// directly instead of throwing.
|
|
56
|
+
// - notFound(): server-side throws a plain `Error('notFound()')`;
|
|
57
|
+
// client-side sets `window.location.href = '/404'`.
|
|
58
|
+
//
|
|
59
|
+
// Does NOT support:
|
|
60
|
+
// - Streaming request/response bodies (readBody fully buffers before
|
|
61
|
+
// resolving; NextResponse.body is a plain in-memory value, not a
|
|
62
|
+
// ReadableStream).
|
|
63
|
+
// - `NextRequest.nextUrl` (parsed URL object with `.searchParams` etc.) --
|
|
64
|
+
// only a raw `.url` string is exposed.
|
|
65
|
+
// - `NextRequest.cookies` as a request-bound property -- cookie access is
|
|
66
|
+
// only available via the separate top-level `cookies()` function.
|
|
67
|
+
// - Any real Web Headers object -- `NextRequest.headers` is a Proxy over
|
|
68
|
+
// a plain object, and `NextResponse.headers` is a bare Map, neither
|
|
69
|
+
// implements the full Headers interface (no `.append()`, `.forEach()`,
|
|
70
|
+
// `.delete()`, iteration protocol, etc).
|
|
71
|
+
// - Edge runtime APIs (no `NextFetchEvent`, no `waitUntil`, no Edge
|
|
72
|
+
// `fetch` overrides).
|
|
73
|
+
// - Middleware matchers / `middleware.js` config conventions.
|
|
74
|
+
// - `headers()` returning real request headers -- it is a hardcoded stub
|
|
75
|
+
// that never reflects the actual incoming request.
|
|
76
|
+
// - Response streaming helpers (`NextResponse.next()`, rewriting, etc.)
|
|
77
|
+
// -- only `.json()` and `.redirect()` static constructors exist.
|
|
78
|
+
// - Any multipart/form-data or non-JSON body parsing in readBody --
|
|
79
|
+
// non-JSON bodies resolve as a raw string.
|
|
80
|
+
|
|
81
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
82
|
+
|
|
83
|
+
const MAX_BODY_SIZE = 10 * 1024 * 1024;
|
|
84
|
+
|
|
85
|
+
// Wrap node's raw (plain-object, lowercased) headers so both the Fetch-style
|
|
86
|
+
// `headers.get('x')` API and direct `headers['x']` index access work. Node
|
|
87
|
+
// already lowercases header names; `.get()` lowercases its argument to match.
|
|
88
|
+
function wrapHeaders(raw) {
|
|
89
|
+
const h = raw || {};
|
|
90
|
+
return new Proxy(h, {
|
|
91
|
+
get(target, prop) {
|
|
92
|
+
if (prop === 'get') return (name) => target[String(name).toLowerCase()] ?? null;
|
|
93
|
+
if (prop === 'has') return (name) => String(name).toLowerCase() in target;
|
|
94
|
+
return target[prop];
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export class NextRequest {
|
|
100
|
+
constructor(req, body, url) {
|
|
101
|
+
this.method = req.method;
|
|
102
|
+
this.headers = wrapHeaders(req.headers);
|
|
103
|
+
this.url = url;
|
|
104
|
+
this.body = body;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async json() {
|
|
108
|
+
return this.body;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async text() {
|
|
112
|
+
return typeof this.body === 'string' ? this.body : JSON.stringify(this.body);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export class NextResponse {
|
|
117
|
+
constructor(body, init = {}) {
|
|
118
|
+
this.body = body;
|
|
119
|
+
this.status = init.status || 200;
|
|
120
|
+
this.headers = new Map(Object.entries(init.headers || {}));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async json() {
|
|
124
|
+
return this.body;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
static json(body, init = {}) {
|
|
128
|
+
return new NextResponse(body, init);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
static redirect(url, status = 307) {
|
|
132
|
+
return new NextResponse(null, {
|
|
133
|
+
status,
|
|
134
|
+
headers: { Location: url }
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function readBody(req) {
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
let data = '';
|
|
142
|
+
let size = 0;
|
|
143
|
+
req.on('data', (chunk) => {
|
|
144
|
+
size += chunk.length;
|
|
145
|
+
if (size > MAX_BODY_SIZE) {
|
|
146
|
+
req.destroy();
|
|
147
|
+
reject(new Error('Request body too large'));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
data += chunk;
|
|
151
|
+
});
|
|
152
|
+
req.on('end', () => {
|
|
153
|
+
try {
|
|
154
|
+
resolve(data ? JSON.parse(data) : {});
|
|
155
|
+
} catch {
|
|
156
|
+
resolve(data);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
req.on('error', (err) => reject(err));
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const HEADER_MAP = {
|
|
164
|
+
'content-type': 'Content-Type',
|
|
165
|
+
'content-length': 'Content-Length',
|
|
166
|
+
'set-cookie': 'Set-Cookie',
|
|
167
|
+
'cache-control': 'Cache-Control',
|
|
168
|
+
'expires': 'Expires',
|
|
169
|
+
'etag': 'ETag',
|
|
170
|
+
'last-modified': 'Last-Modified',
|
|
171
|
+
'location': 'Location',
|
|
172
|
+
'date': 'Date',
|
|
173
|
+
'connection': 'Connection',
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export function normalizeHeaderName(key) {
|
|
177
|
+
return HEADER_MAP[key.toLowerCase()] || key;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function registerGlobals() {
|
|
181
|
+
globalThis.NextRequest = NextRequest;
|
|
182
|
+
globalThis.NextResponse = NextResponse;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const requestContext = new AsyncLocalStorage();
|
|
186
|
+
|
|
187
|
+
let fallbackRequest = null;
|
|
188
|
+
let fallbackResponse = null;
|
|
189
|
+
|
|
190
|
+
export function setCurrentRequest(req) {
|
|
191
|
+
fallbackRequest = req;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function setCurrentResponse(res) {
|
|
195
|
+
fallbackResponse = res;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function runWithContext(req, res, fn) {
|
|
199
|
+
return requestContext.run({ req, res }, fn);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function getRequest() {
|
|
203
|
+
const store = requestContext.getStore();
|
|
204
|
+
return store?.req || fallbackRequest;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function getResponse() {
|
|
208
|
+
const store = requestContext.getStore();
|
|
209
|
+
return store?.res || fallbackResponse;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function cookies() {
|
|
213
|
+
if (typeof window !== 'undefined') {
|
|
214
|
+
throw new Error('cookies() should only be called on the server side');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const req = getRequest();
|
|
218
|
+
const res = getResponse();
|
|
219
|
+
const cookieHeader = req?.headers?.cookie || '';
|
|
220
|
+
const cookieMap = {};
|
|
221
|
+
|
|
222
|
+
if (cookieHeader) {
|
|
223
|
+
cookieHeader.split(';').forEach(cookie => {
|
|
224
|
+
const [name, value] = cookie.split('=').map(s => s.trim());
|
|
225
|
+
if (name) cookieMap[name] = decodeURIComponent(value || '');
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
get: (name) => {
|
|
231
|
+
const value = cookieMap[name];
|
|
232
|
+
return value ? { name, value } : undefined;
|
|
233
|
+
},
|
|
234
|
+
set: (name, value, options = {}) => {
|
|
235
|
+
if (!res) return;
|
|
236
|
+
let setCookieValue = `${name}=${encodeURIComponent(value)}`;
|
|
237
|
+
if (options.path) setCookieValue += `; Path=${options.path}`;
|
|
238
|
+
if (options.maxAge) setCookieValue += `; Max-Age=${options.maxAge}`;
|
|
239
|
+
if (options.expires) setCookieValue += `; Expires=${options.expires}`;
|
|
240
|
+
if (options.secure) setCookieValue += '; Secure';
|
|
241
|
+
if (options.httpOnly) setCookieValue += '; HttpOnly';
|
|
242
|
+
if (options.sameSite) setCookieValue += `; SameSite=${options.sameSite}`;
|
|
243
|
+
const existing = res.getHeader('Set-Cookie') || [];
|
|
244
|
+
const setCookies = Array.isArray(existing) ? existing : [existing];
|
|
245
|
+
res.setHeader('Set-Cookie', [...setCookies, setCookieValue]);
|
|
246
|
+
},
|
|
247
|
+
delete: (name) => {
|
|
248
|
+
if (!res) return;
|
|
249
|
+
const setCookieValue = `${name}=; Path=/; Max-Age=0`;
|
|
250
|
+
const existing = res.getHeader('Set-Cookie') || [];
|
|
251
|
+
const setCookies = Array.isArray(existing) ? existing : [existing];
|
|
252
|
+
res.setHeader('Set-Cookie', [...setCookies, setCookieValue]);
|
|
253
|
+
},
|
|
254
|
+
getAll: () => {
|
|
255
|
+
return Object.entries(cookieMap).map(([name, value]) => ({ name, value }));
|
|
256
|
+
},
|
|
257
|
+
has: (name) => {
|
|
258
|
+
return name in cookieMap;
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function headers() {
|
|
264
|
+
return {
|
|
265
|
+
get: (name) => null,
|
|
266
|
+
getSetCookie: () => [],
|
|
267
|
+
has: (name) => false,
|
|
268
|
+
entries: () => [],
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function revalidatePath() {}
|
|
273
|
+
|
|
274
|
+
export function revalidateTag() {}
|
|
275
|
+
|
|
276
|
+
export function redirect(path, status = 302) {
|
|
277
|
+
if (typeof window !== 'undefined') {
|
|
278
|
+
window.location.href = path;
|
|
279
|
+
} else {
|
|
280
|
+
const error = new Error(`Redirect to ${path}`);
|
|
281
|
+
error.type = 'redirect';
|
|
282
|
+
error.location = path;
|
|
283
|
+
error.status = status;
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function notFound() {
|
|
289
|
+
if (typeof window !== 'undefined') {
|
|
290
|
+
window.location.href = '/404';
|
|
291
|
+
} else {
|
|
292
|
+
throw new Error('notFound()');
|
|
293
|
+
}
|
|
294
|
+
}
|