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.
Files changed (221) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +398 -0
  3. package/package.json +73 -0
  4. package/src/adapters/google-auth.js +148 -0
  5. package/src/adapters/google-drive.js +209 -0
  6. package/src/app/api/[entity]/[[...path]]/route.js +33 -0
  7. package/src/app/api/audit/dashboard/route.js +77 -0
  8. package/src/app/api/audit/logs/route.js +46 -0
  9. package/src/app/api/audit/permissions/[id]/route.js +37 -0
  10. package/src/app/api/audit/permissions/route.js +93 -0
  11. package/src/app/api/audit/permissions/stats/route.js +29 -0
  12. package/src/app/api/audit/route.js +79 -0
  13. package/src/app/api/audit/stats/route.js +18 -0
  14. package/src/app/api/auth/google/callback/route.js +94 -0
  15. package/src/app/api/auth/google/route.js +58 -0
  16. package/src/app/api/auth/login/route.js +121 -0
  17. package/src/app/api/auth/logout/route.js +52 -0
  18. package/src/app/api/auth/me/route.js +16 -0
  19. package/src/app/api/auth/mwr-bridge/route.js +77 -0
  20. package/src/app/api/auth/password-reset/route.js +65 -0
  21. package/src/app/api/cron/trigger/route.js +58 -0
  22. package/src/app/api/csrf-token/route.js +8 -0
  23. package/src/app/api/debug/config/route.js +22 -0
  24. package/src/app/api/debug/hooks/route.js +16 -0
  25. package/src/app/api/debug/plugins/route.js +20 -0
  26. package/src/app/api/debug/sqlite/route.js +21 -0
  27. package/src/app/api/debug/sync/route.js +29 -0
  28. package/src/app/api/debug/workflow/route.js +20 -0
  29. package/src/app/api/domains/[domain]/route.js +26 -0
  30. package/src/app/api/domains/route.js +21 -0
  31. package/src/app/api/email/allocate/batch/route.js +106 -0
  32. package/src/app/api/email/allocate/route.js +141 -0
  33. package/src/app/api/email/receive/route.js +158 -0
  34. package/src/app/api/email/route.js +3 -0
  35. package/src/app/api/email/send/route.js +77 -0
  36. package/src/app/api/email/unallocated/route.js +47 -0
  37. package/src/app/api/files/[id]/route.js +38 -0
  38. package/src/app/api/health/route.js +95 -0
  39. package/src/app/api/metrics/route.js +73 -0
  40. package/src/app/api/monitoring/dashboard/route.js +18 -0
  41. package/src/cli.js +243 -0
  42. package/src/config/config-loader.js +112 -0
  43. package/src/config/constants.js +127 -0
  44. package/src/config/env.js +164 -0
  45. package/src/config/spec-helpers.js +232 -0
  46. package/src/engine.server.js +212 -0
  47. package/src/index.js +368 -0
  48. package/src/lib/accessibility.js +162 -0
  49. package/src/lib/action-factory.js +34 -0
  50. package/src/lib/action-utils.js +21 -0
  51. package/src/lib/alert-manager.js +189 -0
  52. package/src/lib/api-error-wrapper.js +125 -0
  53. package/src/lib/api-helpers.js +53 -0
  54. package/src/lib/api.js +82 -0
  55. package/src/lib/audit-logger-enhanced.js +117 -0
  56. package/src/lib/audit-logger.js +193 -0
  57. package/src/lib/auth-middleware.js +102 -0
  58. package/src/lib/auth-route-helpers.js +83 -0
  59. package/src/lib/business-rules-engine.js +86 -0
  60. package/src/lib/compression.js +44 -0
  61. package/src/lib/config-field-helpers.js +91 -0
  62. package/src/lib/config-generator-engine.js +445 -0
  63. package/src/lib/config-helpers.js +120 -0
  64. package/src/lib/connection-guard.js +79 -0
  65. package/src/lib/crud-action-helpers.js +34 -0
  66. package/src/lib/crud-factory.js +83 -0
  67. package/src/lib/crud-handlers.js +244 -0
  68. package/src/lib/csrf-protection.js +63 -0
  69. package/src/lib/database-core.js +258 -0
  70. package/src/lib/database-migrations.js +96 -0
  71. package/src/lib/date-utils.js +159 -0
  72. package/src/lib/db-backup.js +97 -0
  73. package/src/lib/db-monitor.js +127 -0
  74. package/src/lib/domain-loader.js +82 -0
  75. package/src/lib/email-sender.js +100 -0
  76. package/src/lib/error-boundary.js +134 -0
  77. package/src/lib/error-handler.js +84 -0
  78. package/src/lib/error-recovery.js +190 -0
  79. package/src/lib/error-resilience.js +130 -0
  80. package/src/lib/errors.js +69 -0
  81. package/src/lib/events-engine.js +182 -0
  82. package/src/lib/field-iterator.js +50 -0
  83. package/src/lib/field-registry.js +68 -0
  84. package/src/lib/field-types.js +154 -0
  85. package/src/lib/generic-crud-handler.js +32 -0
  86. package/src/lib/health-monitor.js +134 -0
  87. package/src/lib/hook-engine.js +169 -0
  88. package/src/lib/hot-reload/cache-invalidator.js +115 -0
  89. package/src/lib/hot-reload/checkpoint.js +95 -0
  90. package/src/lib/hot-reload/debug-exposure.js +67 -0
  91. package/src/lib/hot-reload/directory-watcher.js +96 -0
  92. package/src/lib/hot-reload/index.js +50 -0
  93. package/src/lib/hot-reload/mutex.js +75 -0
  94. package/src/lib/hot-reload/promise-container.js +66 -0
  95. package/src/lib/hot-reload/route-wrapper.js +46 -0
  96. package/src/lib/hot-reload/safe-error.js +51 -0
  97. package/src/lib/hot-reload/supervisor.js +161 -0
  98. package/src/lib/hot-reload/timeout-wrapper.js +52 -0
  99. package/src/lib/http-methods-factory.js +25 -0
  100. package/src/lib/index-optimizer.js +96 -0
  101. package/src/lib/index.js +35 -0
  102. package/src/lib/list-data-transform.js +39 -0
  103. package/src/lib/log-aggregator.js +116 -0
  104. package/src/lib/logger.js +55 -0
  105. package/src/lib/metrics-collector.js +102 -0
  106. package/src/lib/minifier.js +19 -0
  107. package/src/lib/monitoring-init.js +67 -0
  108. package/src/lib/next-compat.js +80 -0
  109. package/src/lib/next-polyfills.js +135 -0
  110. package/src/lib/perf-monitor.js +91 -0
  111. package/src/lib/progress-components.js +181 -0
  112. package/src/lib/query-cache.js +126 -0
  113. package/src/lib/query-engine-write.js +221 -0
  114. package/src/lib/query-engine.js +399 -0
  115. package/src/lib/query-perf.js +117 -0
  116. package/src/lib/query-string-adapter.js +75 -0
  117. package/src/lib/realtime-server.js +67 -0
  118. package/src/lib/render-cache.js +61 -0
  119. package/src/lib/request-tracker.js +43 -0
  120. package/src/lib/resource-hints.js +29 -0
  121. package/src/lib/resource-monitor.js +117 -0
  122. package/src/lib/response-formatter.js +80 -0
  123. package/src/lib/route-helpers.js +33 -0
  124. package/src/lib/route-resolver.js +142 -0
  125. package/src/lib/safe-json.js +8 -0
  126. package/src/lib/server-bootstrap.js +71 -0
  127. package/src/lib/stage-pipeline.js +153 -0
  128. package/src/lib/state-protocol.js +171 -0
  129. package/src/lib/state-transport-client.js +169 -0
  130. package/src/lib/state-transport-reconnect.js +121 -0
  131. package/src/lib/state-transport-server.js +181 -0
  132. package/src/lib/static-server.js +97 -0
  133. package/src/lib/status-helpers.js +98 -0
  134. package/src/lib/universal-handler.js +7 -0
  135. package/src/lib/utils.js +93 -0
  136. package/src/lib/validate.js +197 -0
  137. package/src/lib/validation/business-validators.js +61 -0
  138. package/src/lib/validation/csrf.js +51 -0
  139. package/src/lib/validation/file-validators.js +34 -0
  140. package/src/lib/validation/format-validators.js +106 -0
  141. package/src/lib/validation/index.js +19 -0
  142. package/src/lib/validation/rate-limit.js +31 -0
  143. package/src/lib/validation/security-validators.js +78 -0
  144. package/src/lib/validation-middleware.js +133 -0
  145. package/src/lib/validators.js +105 -0
  146. package/src/lib/with-audit-logging.js +63 -0
  147. package/src/lib/with-error-handler.js +31 -0
  148. package/src/lib/workflow-engine.js +250 -0
  149. package/src/server/server.js +305 -0
  150. package/src/services/collaborator-role.service.js +205 -0
  151. package/src/services/email-sender.js +105 -0
  152. package/src/services/notification-engine.js +110 -0
  153. package/src/services/permission.service.js +181 -0
  154. package/src/ui/advanced-search-renderer.js +42 -0
  155. package/src/ui/advanced-widgets.js +47 -0
  156. package/src/ui/auth-pages.js +114 -0
  157. package/src/ui/auth-styles.js +53 -0
  158. package/src/ui/client.js +99 -0
  159. package/src/ui/collaboration-dialogs.js +31 -0
  160. package/src/ui/common-handlers.js +156 -0
  161. package/src/ui/component-engine.js +103 -0
  162. package/src/ui/dashboard-renderer.js +150 -0
  163. package/src/ui/dialog-engine.js +147 -0
  164. package/src/ui/dialog-factory.js +38 -0
  165. package/src/ui/engagement-cards.js +76 -0
  166. package/src/ui/engagement-dialogs.js +100 -0
  167. package/src/ui/engagement-grid-renderer.js +109 -0
  168. package/src/ui/entity-renderer.js +176 -0
  169. package/src/ui/event-delegation.js +108 -0
  170. package/src/ui/fetch-json.js +24 -0
  171. package/src/ui/file-dialogs.js +81 -0
  172. package/src/ui/flexup-report-renderer.js +173 -0
  173. package/src/ui/format-helpers.js +102 -0
  174. package/src/ui/global-tags.js +144 -0
  175. package/src/ui/highlight-threading-renderer.js +176 -0
  176. package/src/ui/idle-logout.js +156 -0
  177. package/src/ui/job-management-renderer.js +61 -0
  178. package/src/ui/layout.js +219 -0
  179. package/src/ui/letter-dialogs.js +37 -0
  180. package/src/ui/ml-console-renderer.js +108 -0
  181. package/src/ui/monitoring-dashboard-client.js +136 -0
  182. package/src/ui/monitoring-dashboard.js +134 -0
  183. package/src/ui/notifications-renderer.js +51 -0
  184. package/src/ui/page-handler-admin.js +121 -0
  185. package/src/ui/page-handler-helpers.js +111 -0
  186. package/src/ui/page-handler-reviews.js +165 -0
  187. package/src/ui/page-handler-rfi.js +42 -0
  188. package/src/ui/page-handler.js +210 -0
  189. package/src/ui/password-reset-page.js +146 -0
  190. package/src/ui/perf-helpers.js +96 -0
  191. package/src/ui/perf-renderer.js +71 -0
  192. package/src/ui/permissions-ui.js +163 -0
  193. package/src/ui/picker-dialogs.js +100 -0
  194. package/src/ui/render-helpers.js +124 -0
  195. package/src/ui/renderer.js +35 -0
  196. package/src/ui/review-comparison-renderer.js +58 -0
  197. package/src/ui/review-detail-panels.js +71 -0
  198. package/src/ui/review-detail-renderer.js +170 -0
  199. package/src/ui/review-detail-script.js +95 -0
  200. package/src/ui/review-mwr-renderer.js +113 -0
  201. package/src/ui/review-renderer.js +202 -0
  202. package/src/ui/review-widgets.js +88 -0
  203. package/src/ui/review-zone-nav.js +12 -0
  204. package/src/ui/rfi-detail-renderer.js +191 -0
  205. package/src/ui/rfi-renderer.js +194 -0
  206. package/src/ui/rfi-report-renderer.js +56 -0
  207. package/src/ui/rippleui.css +1 -0
  208. package/src/ui/settings-renderer-advanced.js +195 -0
  209. package/src/ui/settings-renderer-advanced2.js +158 -0
  210. package/src/ui/settings-renderer-teams.js +112 -0
  211. package/src/ui/settings-renderer.js +166 -0
  212. package/src/ui/spacing-system.js +155 -0
  213. package/src/ui/standalone-login.js +109 -0
  214. package/src/ui/styles.css +2530 -0
  215. package/src/ui/styles2.css +1602 -0
  216. package/src/ui/test-page.js +23 -0
  217. package/src/ui/validation-rules.js +73 -0
  218. package/src/ui/validation-ui.js +147 -0
  219. package/src/ui/virtual-scroll.js +107 -0
  220. package/src/ui/webjsx.js +61 -0
  221. package/src/ui/widgets.js +152 -0
@@ -0,0 +1,94 @@
1
+ import { google, createSession } from '@/engine.server';
2
+ import { getBy, create } from '@/engine';
3
+ import { Google } from 'arctic';
4
+ import { GOOGLE_APIS } from '@/config/constants';
5
+ import { config } from '@/config';
6
+ import { getConfigEngine } from '@/lib/config-generator-engine';
7
+ import { globalManager } from '@/lib/hot-reload/mutex';
8
+ import { NextResponse } from '@/lib/next-polyfills';
9
+ import {
10
+ validateOAuthProvider,
11
+ getOAuthCookie,
12
+ deleteOAuthCookie,
13
+ buildOAuthErrorResponse,
14
+ validateOAuthState,
15
+ } from '@/lib/auth-route-helpers';
16
+
17
+ export async function GET(request) {
18
+ const protocol = request.headers['x-forwarded-proto'] || 'http';
19
+ const host = request.headers['x-forwarded-host'] || request.headers.host || 'localhost:3000';
20
+ const redirectUri = `${protocol}://${host}/api/auth/google/callback`;
21
+
22
+ const dynamicGoogle = new Google(
23
+ config.auth.google.clientId,
24
+ config.auth.google.clientSecret,
25
+ redirectUri
26
+ );
27
+
28
+ const { valid, error } = validateOAuthProvider(dynamicGoogle);
29
+ if (!valid) {
30
+ return buildOAuthErrorResponse(error, request);
31
+ }
32
+
33
+ const url = new URL(request.url);
34
+ const code = url.searchParams.get('code');
35
+ const stateKey = url.searchParams.get('state'); // Google returns the key we sent
36
+
37
+ const storedData = await getOAuthCookie(stateKey);
38
+ const state = storedData?.state;
39
+ const codeVerifier = storedData?.codeVerifier;
40
+
41
+ const stateValidation = validateOAuthState(code, stateKey, state, codeVerifier);
42
+ if (!stateValidation.valid) {
43
+ console.error('[OAuth Callback] State validation failed:', stateValidation.error);
44
+ return buildOAuthErrorResponse(stateValidation.error, request);
45
+ }
46
+
47
+ try {
48
+ const tokens = await dynamicGoogle.validateAuthorizationCode(code, codeVerifier);
49
+ const accessToken = tokens.accessToken;
50
+
51
+ const googleResponse = await fetch(GOOGLE_APIS.oauth2, {
52
+ headers: { Authorization: `Bearer ${accessToken}` },
53
+ });
54
+
55
+ if (!googleResponse.ok) {
56
+ throw new Error('Failed to fetch user info');
57
+ }
58
+
59
+ const googleUser = await googleResponse.json();
60
+
61
+ const user = await globalManager.lock('oauth-user-create', async () => {
62
+ let existing = getBy('user', 'email', googleUser.email);
63
+ if (existing) return existing;
64
+
65
+ const engine = await getConfigEngine();
66
+ const roles = engine.getRoles();
67
+ const defaultRole = Object.keys(roles)[0] || 'clerk';
68
+
69
+ return create('user', {
70
+ email: googleUser.email,
71
+ name: googleUser.name,
72
+ avatar: googleUser.picture,
73
+ type: 'auditor',
74
+ role: defaultRole,
75
+ status: 'active',
76
+ });
77
+ });
78
+
79
+ const { sessionCookie } = await createSession(user.id);
80
+
81
+ await deleteOAuthCookie(stateKey);
82
+
83
+ const redirectUrl = new URL('/', request.url);
84
+ const response = NextResponse.redirect(redirectUrl);
85
+
86
+ const cookieValue = `${sessionCookie.value}; Path=${sessionCookie.attributes.path || '/'}; HttpOnly${sessionCookie.attributes.secure ? '; Secure' : ''}; SameSite=${sessionCookie.attributes.sameSite || 'Lax'}`;
87
+ response.headers.set('Set-Cookie', `${sessionCookie.name}=${cookieValue}`);
88
+
89
+ return response;
90
+ } catch (error) {
91
+ console.error('Google OAuth error:', error);
92
+ return buildOAuthErrorResponse('oauth_failed', request);
93
+ }
94
+ }
@@ -0,0 +1,58 @@
1
+ import { NextResponse } from '@/lib/next-polyfills';
2
+ import { google } from '@/engine.server';
3
+ import { Google } from 'arctic';
4
+ import { generateState, generateCodeVerifier } from 'arctic';
5
+ import { globalManager } from '@/lib/hot-reload/mutex';
6
+ import { config } from '@/config';
7
+ import { validateOAuthProvider, setOAuthCookie, buildOAuthErrorResponse } from '@/lib/auth-route-helpers';
8
+
9
+ export async function GET(request) {
10
+ const url = new URL(request.url);
11
+ const isCheck = url.searchParams.get('check') === '1';
12
+
13
+ if (isCheck) {
14
+ const { valid } = validateOAuthProvider(google);
15
+ return new Response(JSON.stringify({ configured: valid }), {
16
+ status: 200,
17
+ headers: { 'Content-Type': 'application/json' }
18
+ });
19
+ }
20
+
21
+ const protocol = request.headers['x-forwarded-proto'] || 'http';
22
+ const host = request.headers['x-forwarded-host'] || request.headers.host || 'localhost:3000';
23
+ const redirectUri = `${protocol}://${host}/api/auth/google/callback`;
24
+
25
+ const dynamicGoogle = new Google(
26
+ config.auth.google.clientId,
27
+ config.auth.google.clientSecret,
28
+ redirectUri
29
+ );
30
+
31
+ const { valid, error } = validateOAuthProvider(dynamicGoogle);
32
+ if (!valid) {
33
+ return buildOAuthErrorResponse(error);
34
+ }
35
+
36
+ return globalManager.lock('oauth-state-init', async () => {
37
+ const state = generateState();
38
+ const codeVerifier = generateCodeVerifier();
39
+
40
+ const stateKey = await setOAuthCookie('google_oauth_state', { state, codeVerifier });
41
+
42
+ const url = await dynamicGoogle.createAuthorizationURL(stateKey, codeVerifier, {
43
+ scopes: ['profile', 'email'],
44
+ });
45
+
46
+ return NextResponse.redirect(url);
47
+ });
48
+ }
49
+
50
+ export async function HEAD(request) {
51
+ try {
52
+ const { valid } = validateOAuthProvider(google);
53
+ return new Response(null, { status: valid ? 200 : 503 });
54
+ } catch (error) {
55
+ console.error('[OAuth HEAD] Error:', error);
56
+ return new Response(null, { status: 500 });
57
+ }
58
+ }
@@ -0,0 +1,121 @@
1
+ import { getBy, verifyPassword, migrate } from '@/engine';
2
+ import { initializeSystemConfig } from '@/config/system-config-loader';
3
+ import { withErrorHandler } from '@/lib/with-error-handler';
4
+ import { lucia } from '@/engine.server';
5
+
6
+ let initialized = false;
7
+
8
+ const loginAttempts = new Map();
9
+ const MAX_ATTEMPTS = 5;
10
+ const LOCKOUT_MS = 15 * 60 * 1000;
11
+
12
+ function checkRateLimit(ip) {
13
+ const record = loginAttempts.get(ip);
14
+ if (!record) return { allowed: true };
15
+ if (Date.now() - record.firstAttempt > LOCKOUT_MS) {
16
+ loginAttempts.delete(ip);
17
+ return { allowed: true };
18
+ }
19
+ if (record.count >= MAX_ATTEMPTS) {
20
+ const retryAfter = Math.ceil((record.firstAttempt + LOCKOUT_MS - Date.now()) / 1000);
21
+ return { allowed: false, retryAfter };
22
+ }
23
+ return { allowed: true };
24
+ }
25
+
26
+ function recordFailedAttempt(ip) {
27
+ const record = loginAttempts.get(ip) || { count: 0, firstAttempt: Date.now() };
28
+ record.count++;
29
+ loginAttempts.set(ip, record);
30
+ }
31
+
32
+ function clearAttempts(ip) {
33
+ loginAttempts.delete(ip);
34
+ }
35
+
36
+ export const POST = withErrorHandler(async (request) => {
37
+ if (!initialized) {
38
+ initialized = true;
39
+ try {
40
+ await initializeSystemConfig();
41
+ migrate();
42
+ } catch (e) {
43
+ console.error('[Login] Init failed:', e.message);
44
+ return new Response(JSON.stringify({ error: 'System init failed' }), { status: 500, headers: { 'Content-Type': 'application/json' } });
45
+ }
46
+ }
47
+
48
+ const ip = request.headers?.get?.('x-forwarded-for') || request.headers?.['x-forwarded-for'] || 'unknown';
49
+ const rateCheck = checkRateLimit(ip);
50
+ if (!rateCheck.allowed) {
51
+ return new Response(JSON.stringify({ error: 'Too many login attempts. Try again later.' }), {
52
+ status: 429,
53
+ headers: { 'Content-Type': 'application/json', 'Retry-After': String(rateCheck.retryAfter) }
54
+ });
55
+ }
56
+
57
+ try {
58
+ let email, password;
59
+ const body = request.body;
60
+
61
+ if (typeof body === 'object' && body !== null) {
62
+ email = body.email;
63
+ password = body.password;
64
+ } else if (typeof body === 'string') {
65
+ const params = new URLSearchParams(body);
66
+ email = params.get('email');
67
+ password = params.get('password');
68
+ } else {
69
+ return new Response(JSON.stringify({ error: 'Invalid request format' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
70
+ }
71
+
72
+ if (!email || !password) {
73
+ return new Response(JSON.stringify({ error: 'Email and password required' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
74
+ }
75
+
76
+ const user = getBy('user', 'email', email);
77
+ if (!user) {
78
+ recordFailedAttempt(ip);
79
+ return new Response(JSON.stringify({ error: 'Invalid email or password' }), { status: 401, headers: { 'Content-Type': 'application/json' } });
80
+ }
81
+
82
+ if (!user.password_hash) {
83
+ recordFailedAttempt(ip);
84
+ return new Response(JSON.stringify({ error: 'Invalid email or password' }), { status: 401, headers: { 'Content-Type': 'application/json' } });
85
+ }
86
+
87
+ const passwordValid = await verifyPassword(password, user.password_hash);
88
+ if (!passwordValid) {
89
+ recordFailedAttempt(ip);
90
+ return new Response(JSON.stringify({ error: 'Invalid email or password' }), { status: 401, headers: { 'Content-Type': 'application/json' } });
91
+ }
92
+
93
+ clearAttempts(ip);
94
+
95
+ const session = await lucia.createSession(user.id, {});
96
+ const sessionCookie = lucia.createSessionCookie(session.id);
97
+ const cookieHeader = `${sessionCookie.name}=${sessionCookie.value}; Path=/; HttpOnly; SameSite=Lax${sessionCookie.attributes.secure ? '; Secure' : ''}`;
98
+
99
+ const responseBody = JSON.stringify({
100
+ status: 'success',
101
+ message: 'Login successful',
102
+ user: {
103
+ id: user.id,
104
+ email: user.email,
105
+ name: user.name,
106
+ role: user.role
107
+ }
108
+ });
109
+
110
+ return new Response(responseBody, {
111
+ status: 200,
112
+ headers: {
113
+ 'Content-Type': 'application/json',
114
+ 'Set-Cookie': cookieHeader
115
+ }
116
+ });
117
+ } catch (err) {
118
+ console.error('[Login] Error:', err.message);
119
+ return new Response(JSON.stringify({ error: 'Authentication failed' }), { status: 500, headers: { 'Content-Type': 'application/json' } });
120
+ }
121
+ }, 'Auth:Login');
@@ -0,0 +1,52 @@
1
+ import { setCurrentRequest as setEngineRequest, invalidateSession } from '@/engine.server';
2
+ import { setCurrentRequest, setCurrentResponse } from '@/lib/next-polyfills';
3
+
4
+ function buildLogoutHeaders(headerMap) {
5
+ const setCookies = headerMap.get('Set-Cookie');
6
+ return setCookies
7
+ ? (Array.isArray(setCookies) ? setCookies : [setCookies])
8
+ : [];
9
+ }
10
+
11
+ export async function GET(request, { res } = {}) {
12
+ try {
13
+ setEngineRequest(request);
14
+ setCurrentRequest(request);
15
+ const headers = new Map();
16
+ setCurrentResponse({
17
+ getHeader: (name) => headers.get(name),
18
+ setHeader: (name, value) => headers.set(name, value),
19
+ });
20
+ await invalidateSession();
21
+ const response = new Response(null, { status: 307 });
22
+ buildLogoutHeaders(headers).forEach(cookie => response.headers.append('Set-Cookie', cookie));
23
+ response.headers.set('Location', `/login`);
24
+ return response;
25
+ } catch {
26
+ return new Response(null, { status: 307, headers: { Location: '/login' } });
27
+ }
28
+ }
29
+
30
+ export async function POST(request) {
31
+ try {
32
+ setEngineRequest(request);
33
+ setCurrentRequest(request);
34
+ const headers = new Map();
35
+ setCurrentResponse({
36
+ getHeader: (name) => headers.get(name),
37
+ setHeader: (name, value) => headers.set(name, value),
38
+ });
39
+ await invalidateSession();
40
+ const response = new Response(JSON.stringify({ success: true }), {
41
+ status: 200,
42
+ headers: { 'Content-Type': 'application/json' }
43
+ });
44
+ buildLogoutHeaders(headers).forEach(cookie => response.headers.append('Set-Cookie', cookie));
45
+ return response;
46
+ } catch {
47
+ return new Response(JSON.stringify({ success: true }), {
48
+ status: 200,
49
+ headers: { 'Content-Type': 'application/json' }
50
+ });
51
+ }
52
+ }
@@ -0,0 +1,16 @@
1
+ import { NextResponse } from '@/lib/next-polyfills';
2
+ import { getUser, setCurrentRequest } from '@/engine.server';
3
+ import { HTTP } from '@/config/constants';
4
+
5
+ export async function GET(request) {
6
+ try {
7
+ setCurrentRequest(request);
8
+ const user = await getUser();
9
+ if (!user) {
10
+ return NextResponse.json({ user: null }, { status: HTTP.UNAUTHORIZED });
11
+ }
12
+ return NextResponse.json({ user });
13
+ } catch {
14
+ return NextResponse.json({ user: null }, { status: HTTP.UNAUTHORIZED });
15
+ }
16
+ }
@@ -0,0 +1,77 @@
1
+ import { getBy } from '@/engine';
2
+ import { lucia } from '@/engine.server';
3
+ import { getDatabase, now } from '@/lib/database-core';
4
+ import { withErrorHandler } from '@/lib/with-error-handler';
5
+ import crypto from 'crypto';
6
+
7
+ export const POST = withErrorHandler(async (request) => {
8
+ const body = await request.json();
9
+ const { token, email } = body || {};
10
+
11
+ if (!token) {
12
+ return new Response(JSON.stringify({ error: 'Token is required' }), {
13
+ status: 400, headers: { 'Content-Type': 'application/json' }
14
+ });
15
+ }
16
+
17
+ if (!email) {
18
+ return new Response(JSON.stringify({ error: 'Email is required' }), {
19
+ status: 400, headers: { 'Content-Type': 'application/json' }
20
+ });
21
+ }
22
+
23
+ const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
24
+ const db = getDatabase();
25
+
26
+ let bridgeRecord;
27
+ try {
28
+ bridgeRecord = db.prepare('SELECT * FROM mwr_bridge_tokens WHERE token_hash = ? AND used = 0').get(tokenHash);
29
+ } catch (e) {
30
+ db.exec(`CREATE TABLE IF NOT EXISTS mwr_bridge_tokens (
31
+ id TEXT PRIMARY KEY,
32
+ token_hash TEXT NOT NULL,
33
+ email TEXT NOT NULL,
34
+ expires_at INTEGER NOT NULL,
35
+ used INTEGER DEFAULT 0,
36
+ created_at INTEGER NOT NULL
37
+ )`);
38
+ db.exec('CREATE INDEX IF NOT EXISTS idx_mwr_bridge_token ON mwr_bridge_tokens(token_hash)');
39
+ bridgeRecord = null;
40
+ }
41
+
42
+ const user = getBy('user', 'email', email.toLowerCase().trim());
43
+ if (!user) {
44
+ return new Response(JSON.stringify({ error: 'User not found in Moonlanding' }), {
45
+ status: 404, headers: { 'Content-Type': 'application/json' }
46
+ });
47
+ }
48
+
49
+ if (bridgeRecord) {
50
+ if (bridgeRecord.expires_at < now()) {
51
+ db.prepare('UPDATE mwr_bridge_tokens SET used = 1 WHERE id = ?').run(bridgeRecord.id);
52
+ return new Response(JSON.stringify({ error: 'Bridge token has expired' }), {
53
+ status: 401, headers: { 'Content-Type': 'application/json' }
54
+ });
55
+ }
56
+
57
+ if (bridgeRecord.email.toLowerCase() !== email.toLowerCase().trim()) {
58
+ return new Response(JSON.stringify({ error: 'Token email mismatch' }), {
59
+ status: 401, headers: { 'Content-Type': 'application/json' }
60
+ });
61
+ }
62
+
63
+ db.prepare('UPDATE mwr_bridge_tokens SET used = 1 WHERE id = ?').run(bridgeRecord.id);
64
+ }
65
+
66
+ const session = await lucia.createSession(user.id, {});
67
+ const sessionCookie = lucia.createSessionCookie(session.id);
68
+ const cookieHeader = `${sessionCookie.name}=${sessionCookie.value}; Path=/; HttpOnly; SameSite=Lax${sessionCookie.attributes.secure ? '; Secure' : ''}`;
69
+
70
+ return new Response(JSON.stringify({
71
+ status: 'success',
72
+ user: { id: user.id, email: user.email, name: user.name, role: user.role }
73
+ }), {
74
+ status: 200,
75
+ headers: { 'Content-Type': 'application/json', 'Set-Cookie': cookieHeader }
76
+ });
77
+ }, 'Auth:MWRBridge');
@@ -0,0 +1,65 @@
1
+ import { getBy } from '@/engine';
2
+ import { hashPassword } from '@/engine';
3
+ import { getDatabase, genId, now } from '@/lib/database-core';
4
+ import { withErrorHandler } from '@/lib/with-error-handler';
5
+ import crypto from 'crypto';
6
+
7
+ export const POST = withErrorHandler(async (request) => {
8
+ const body = await request.json();
9
+ const email = body?.email?.trim()?.toLowerCase();
10
+
11
+ if (!email) {
12
+ return new Response(JSON.stringify({ error: 'Email is required' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
13
+ }
14
+
15
+ const user = getBy('user', 'email', email);
16
+
17
+ if (user) {
18
+ const db = getDatabase();
19
+ const token = crypto.randomBytes(32).toString('hex');
20
+ const expiresAt = now() + 3600;
21
+
22
+ db.prepare('DELETE FROM password_reset_tokens WHERE user_id = ?').run(user.id);
23
+ db.prepare('INSERT INTO password_reset_tokens (id, user_id, token, expires_at, used, created_at) VALUES (?, ?, ?, ?, 0, ?)').run(genId(), user.id, token, expiresAt, now());
24
+
25
+ }
26
+
27
+ return new Response(JSON.stringify({ status: 'success', message: 'If an account exists with that email, a reset link has been sent.' }), {
28
+ status: 200, headers: { 'Content-Type': 'application/json' }
29
+ });
30
+ }, 'Auth:PasswordReset');
31
+
32
+ export const PUT = withErrorHandler(async (request) => {
33
+ const body = await request.json();
34
+ const { token, password } = body || {};
35
+
36
+ if (!token || !password) {
37
+ return new Response(JSON.stringify({ error: 'Token and password are required' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
38
+ }
39
+
40
+ if (password.length < 8) {
41
+ return new Response(JSON.stringify({ error: 'Password must be at least 8 characters' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
42
+ }
43
+
44
+ const db = getDatabase();
45
+ const resetToken = db.prepare('SELECT * FROM password_reset_tokens WHERE token = ? AND used = 0').get(token);
46
+
47
+ if (!resetToken) {
48
+ return new Response(JSON.stringify({ error: 'Invalid or expired reset token' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
49
+ }
50
+
51
+ if (resetToken.expires_at < now()) {
52
+ db.prepare('UPDATE password_reset_tokens SET used = 1 WHERE id = ?').run(resetToken.id);
53
+ return new Response(JSON.stringify({ error: 'Reset token has expired. Please request a new one.' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
54
+ }
55
+
56
+ const passwordHash = await hashPassword(password);
57
+
58
+ db.prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(passwordHash, resetToken.user_id);
59
+ db.prepare('UPDATE password_reset_tokens SET used = 1 WHERE id = ?').run(resetToken.id);
60
+ db.prepare('DELETE FROM sessions WHERE user_id = ?').run(resetToken.user_id);
61
+
62
+ return new Response(JSON.stringify({ status: 'success', message: 'Password updated successfully' }), {
63
+ status: 200, headers: { 'Content-Type': 'application/json' }
64
+ });
65
+ }, 'Auth:PasswordResetConfirm');
@@ -0,0 +1,58 @@
1
+ import { runDueJobs } from '@/engine/job-engine';
2
+ import { create } from '@/engine';
3
+
4
+ export const runtime = 'nodejs';
5
+
6
+ export async function POST(request) {
7
+ const startTime = Date.now();
8
+
9
+ try {
10
+ const authHeader = request.headers.get('authorization');
11
+ const token = authHeader?.replace('Bearer ', '');
12
+
13
+ if (!token || token !== process.env.CRON_SECRET) {
14
+ return new Response(
15
+ JSON.stringify({ status: 'error', message: 'Unauthorized' }),
16
+ { status: 401, headers: { 'Content-Type': 'application/json' } }
17
+ );
18
+ }
19
+
20
+ const results = await runDueJobs();
21
+ const duration = Date.now() - startTime;
22
+
23
+ await create('job_execution_log', {
24
+ timestamp: Math.floor(Date.now() / 1000),
25
+ total_jobs: results.total || 0,
26
+ executed_jobs: results.executed || 0,
27
+ failed_jobs: results.failed || 0,
28
+ duration_ms: duration,
29
+ status: results.failed > 0 ? 'partial_failure' : 'success',
30
+ error_details: results.errors || null
31
+ }).catch(err => console.error('[Cron] Log error:', err.message));
32
+
33
+ return new Response(
34
+ JSON.stringify({
35
+ status: 'success',
36
+ timestamp: new Date().toISOString(),
37
+ total_jobs: results.total,
38
+ executed_jobs: results.executed,
39
+ failed_jobs: results.failed,
40
+ duration_ms: duration,
41
+ details: results.details || []
42
+ }),
43
+ { status: 200, headers: { 'Content-Type': 'application/json' } }
44
+ );
45
+ } catch (error) {
46
+ return new Response(
47
+ JSON.stringify({ status: 'error', message: error.message, timestamp: new Date().toISOString() }),
48
+ { status: 500, headers: { 'Content-Type': 'application/json' } }
49
+ );
50
+ }
51
+ }
52
+
53
+ export async function GET() {
54
+ return new Response(
55
+ JSON.stringify({ status: 'ok', message: 'Cron trigger endpoint active. POST with Bearer token to execute.', timestamp: new Date().toISOString() }),
56
+ { status: 200, headers: { 'Content-Type': 'application/json' } }
57
+ );
58
+ }
@@ -0,0 +1,8 @@
1
+ import { generateToken } from '@/lib/csrf-protection';
2
+ import { ok } from '@/lib/response-formatter';
3
+ import { withErrorHandler } from '@/lib/with-error-handler';
4
+
5
+ export const GET = withErrorHandler(async (request) => {
6
+ const token = generateToken();
7
+ return ok({ csrfToken: token });
8
+ }, 'CSRF:GetToken');
@@ -0,0 +1,22 @@
1
+ import { NextResponse } from '@/lib/next-polyfills';
2
+
3
+ export async function GET() {
4
+ if (process.env.NODE_ENV === 'production') {
5
+ return NextResponse.json({ error: 'Not available in production' }, { status: 403 });
6
+ }
7
+ const { getConfigEngineSync } = await import('@/lib/config-generator-engine.js');
8
+ const engine = getConfigEngineSync();
9
+ const plugins = engine._plugins ? Object.fromEntries(
10
+ [...engine._plugins.entries()].map(([k, v]) => [k, { fields: Object.keys(v.fields || {}), hooks: v.hooks?.length || 0, validators: v.validators?.length || 0 }])
11
+ ) : {};
12
+ return NextResponse.json({
13
+ entities: engine.getAllEntities(),
14
+ roles: Object.keys(engine.getRoles()),
15
+ workflows: Object.keys(engine.getConfig().workflows || {}),
16
+ domains: Object.keys(engine.getDomains()),
17
+ plugins,
18
+ specCacheSize: engine.specCache?.cache?.size ?? 0,
19
+ });
20
+ }
21
+
22
+ export const config = { runtime: 'nodejs' };
@@ -0,0 +1,16 @@
1
+ import { NextResponse } from '@/lib/next-polyfills';
2
+
3
+ export async function GET() {
4
+ if (process.env.NODE_ENV === 'production') {
5
+ return NextResponse.json({ error: 'Not available in production' }, { status: 403 });
6
+ }
7
+ const { hookEngine } = await import('@/lib/hook-engine.js');
8
+ const stats = hookEngine.stats();
9
+ const listeners = {};
10
+ for (const name of Object.keys(stats)) {
11
+ listeners[name] = hookEngine.listeners(name).map(h => ({ priority: h.priority, once: h.once, name: h.callback?.name || 'anonymous' }));
12
+ }
13
+ return NextResponse.json({ hooks: stats, listeners });
14
+ }
15
+
16
+ export const config = { runtime: 'nodejs' };
@@ -0,0 +1,20 @@
1
+ import { NextResponse } from '@/lib/next-polyfills';
2
+
3
+ export async function GET() {
4
+ if (process.env.NODE_ENV === 'production') {
5
+ return NextResponse.json({ error: 'Not available in production' }, { status: 403 });
6
+ }
7
+ const { getConfigEngineSync } = await import('@/lib/config-generator-engine.js');
8
+ const engine = getConfigEngineSync();
9
+ const plugins = engine._plugins ? Object.fromEntries(
10
+ [...engine._plugins.entries()].map(([k, v]) => [k, {
11
+ entityName: v.entityName,
12
+ fields: Object.keys(v.fields || {}),
13
+ hooks: Object.keys(v.hooks || {}),
14
+ validators: Object.keys(v.validators || {}),
15
+ }])
16
+ ) : {};
17
+ return NextResponse.json({ plugins, count: Object.keys(plugins).length });
18
+ }
19
+
20
+ export const config = { runtime: 'nodejs' };
@@ -0,0 +1,21 @@
1
+ import { NextResponse } from '@/lib/next-polyfills';
2
+ import { getDatabase } from '@/engine';
3
+
4
+ export async function GET() {
5
+ if (process.env.NODE_ENV === 'production') {
6
+ return NextResponse.json({ error: 'Not available in production' }, { status: 403 });
7
+ }
8
+ try {
9
+ const db = getDatabase();
10
+ const page_count = db.pragma('page_count', { simple: true });
11
+ const page_size = db.pragma('page_size', { simple: true });
12
+ const journal_mode = db.pragma('journal_mode', { simple: true });
13
+ const wal_autocheckpoint = db.pragma('wal_autocheckpoint', { simple: true });
14
+ const path = db.name;
15
+ return NextResponse.json({ path, page_count, page_size, bytes: page_count * page_size, journal_mode, wal_autocheckpoint });
16
+ } catch (e) {
17
+ return NextResponse.json({ error: String(e?.message || e) }, { status: 500 });
18
+ }
19
+ }
20
+
21
+ export const config = { runtime: 'nodejs' };
@@ -0,0 +1,29 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { NextResponse } from '@/lib/next-polyfills';
4
+ import { getDatabase } from '@/engine';
5
+
6
+ export async function GET() {
7
+ if (process.env.NODE_ENV === 'production') {
8
+ return NextResponse.json({ error: 'Not available in production' }, { status: 403 });
9
+ }
10
+ const dataDir = path.resolve('data');
11
+ let last_run_at = null;
12
+ try {
13
+ if (fs.existsSync(dataDir)) {
14
+ const files = fs.readdirSync(dataDir).map(f => path.join(dataDir, f)).filter(p => { try { return fs.statSync(p).isFile() } catch { return false } });
15
+ if (files.length) last_run_at = new Date(Math.max(...files.map(p => fs.statSync(p).mtimeMs))).toISOString();
16
+ }
17
+ } catch {}
18
+ let table_counts = {};
19
+ try {
20
+ const db = getDatabase();
21
+ const tables = ['users', 'engagement', 'review', 'rfi_template', 'entity_type', 'engagement_type', 'client'];
22
+ for (const t of tables) {
23
+ try { table_counts[t] = db.prepare(`SELECT COUNT(*) c FROM ${t}`).get()?.c ?? null; } catch { table_counts[t] = null; }
24
+ }
25
+ } catch (e) { table_counts._error = String(e?.message || e); }
26
+ return NextResponse.json({ last_run_at, table_counts });
27
+ }
28
+
29
+ export const config = { runtime: 'nodejs' };