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,250 @@
1
+ /**
2
+ * Workflow Engine - State machine management for entity lifecycle transitions
3
+ * Handles validation of state transitions and available actions
4
+ */
5
+
6
+ import { get, update, create } from './query-engine.js';
7
+ import { getConfigEngineSync } from './config-generator-engine.js';
8
+ import { AppError } from './error-handler.js';
9
+ import { HTTP } from '../config/constants.js';
10
+
11
+ const LOCKOUT_SECONDS = 300; // 5 minutes default
12
+ const workflowCache = new Map();
13
+
14
+ /**
15
+ * Get workflow definition from config
16
+ * @param {string} workflowName
17
+ * @returns {object}
18
+ */
19
+ function getWorkflowDef(workflowName) {
20
+ if (workflowCache.has(workflowName)) {
21
+ return workflowCache.get(workflowName);
22
+ }
23
+
24
+ const config = getConfigEngineSync().getConfig();
25
+ const wf = config?.workflows?.[workflowName];
26
+ if (!wf) throw new Error(`Workflow "${workflowName}" not found in config`);
27
+
28
+ const stageMap = {};
29
+ if (wf.stages) {
30
+ for (const stage of wf.stages) {
31
+ stageMap[stage.name] = stage;
32
+ }
33
+ }
34
+ if (wf.states) {
35
+ for (const state of wf.states) {
36
+ stageMap[state.name] = state;
37
+ }
38
+ }
39
+
40
+ const def = { ...wf, stageMap };
41
+ workflowCache.set(workflowName, def);
42
+ return def;
43
+ }
44
+
45
+ /**
46
+ * Validate a state transition
47
+ * @param {string} workflowName
48
+ * @param {string} fromState
49
+ * @param {string} toState
50
+ * @param {object} user
51
+ * @returns {{forward: boolean, backward: boolean}}
52
+ * @throws {AppError} if transition invalid
53
+ */
54
+ export function validateTransition(workflowName, fromState, toState, user) {
55
+ const def = getWorkflowDef(workflowName);
56
+ const fromCfg = def.stageMap[fromState];
57
+ const toCfg = def.stageMap[toState];
58
+
59
+ if (!fromCfg) throw new AppError(`Invalid current state: ${fromState}`, 'INVALID_STATE', HTTP.BAD_REQUEST);
60
+ if (!toCfg) throw new AppError(`Invalid target state: ${toState}`, 'INVALID_STATE', HTTP.BAD_REQUEST);
61
+
62
+ const forward = fromCfg.forward || [];
63
+ const backward = fromCfg.backward || [];
64
+
65
+ if (!forward.includes(toState) && !backward.includes(toState)) {
66
+ throw new AppError(
67
+ `Cannot transition from "${fromState}" to "${toState}". Allowed: ${[...forward, ...backward].join(', ') || 'none'}`,
68
+ 'TRANSITION_INVALID',
69
+ HTTP.BAD_REQUEST
70
+ );
71
+ }
72
+
73
+ // Role requirement check
74
+ const requiresRole = toCfg.requires_role || [];
75
+ if (requiresRole.length > 0 && user && !requiresRole.includes(user.role)) {
76
+ throw new AppError(`Role "${user.role}" cannot enter state "${toState}"`, 'INSUFFICIENT_PERMISSIONS', HTTP.FORBIDDEN);
77
+ }
78
+
79
+ // Entry constraint (e.g., partner_only)
80
+ if (toCfg.entry === 'partner_only' && user?.role !== 'partner') {
81
+ throw new AppError(`Only partners can enter "${toState}"`, 'ENTRY_CONSTRAINT', HTTP.FORBIDDEN);
82
+ }
83
+
84
+ // Readonly check
85
+ if (toCfg.readonly) {
86
+ throw new AppError(`State "${toState}" is read-only`, 'STATE_READONLY', HTTP.FORBIDDEN);
87
+ }
88
+
89
+ return {
90
+ forward: forward.includes(toState),
91
+ backward: backward.includes(toState),
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Get available transitions for current state
97
+ * @param {string} workflowName
98
+ * @param {string} currentState
99
+ * @param {object} user
100
+ * @param {object} [record]
101
+ * @returns {Array<{stage: string, label: string, forward: boolean, backward: boolean}>}
102
+ */
103
+ export function getAvailableTransitions(workflowName, currentState, user, record = null) {
104
+ const def = getWorkflowDef(workflowName);
105
+ const currentCfg = def.stageMap[currentState];
106
+ if (!currentCfg) return [];
107
+
108
+ const available = [];
109
+ const candidates = [...(currentCfg.forward || []), ...(currentCfg.backward || [])];
110
+ const currentOrder = currentCfg.order || 0;
111
+
112
+ for (const stateName of candidates) {
113
+ try {
114
+ // Lockout check based on last_transition_at
115
+ if (record?.last_transition_at) {
116
+ const elapsed = (Date.now() / 1000) - record.last_transition_at;
117
+ if (elapsed < LOCKOUT_SECONDS) continue;
118
+ }
119
+
120
+ validateTransition(workflowName, currentState, stateName, user);
121
+ const cfg = def.stageMap[stateName];
122
+ available.push({
123
+ stage: stateName,
124
+ label: cfg.label || stateName,
125
+ forward: (cfg.order || 0) > currentOrder,
126
+ backward: (cfg.order || 0) < currentOrder,
127
+ });
128
+ } catch {
129
+ // Skip invalid transitions
130
+ }
131
+ }
132
+
133
+ return available;
134
+ }
135
+
136
+ /**
137
+ * Get transition lockout status
138
+ * @param {object} record
139
+ * @returns {{inLockout: boolean, minutesRemaining: number, failedGates: Array}}
140
+ */
141
+ export function getTransitionStatus(record) {
142
+ let inLockout = false;
143
+ let minutesRemaining = 0;
144
+
145
+ if (record?.last_transition_at) {
146
+ const elapsed = (Date.now() / 1000) - record.last_transition_at;
147
+ if (elapsed < LOCKOUT_SECONDS) {
148
+ inLockout = true;
149
+ minutesRemaining = Math.ceil((LOCKOUT_SECONDS - elapsed) / 60);
150
+ }
151
+ }
152
+
153
+ return { inLockout, minutesRemaining, failedGates: [] };
154
+ }
155
+
156
+ /**
157
+ * Execute a state transition
158
+ * @param {string} entityType
159
+ * @param {string} entityId
160
+ * @param {string} workflowName
161
+ * @param {string} toState
162
+ * @param {object} user
163
+ * @param {string} [reason='']
164
+ * @returns {Promise<object>} Updated record
165
+ */
166
+ export async function transition(entityType, entityId, workflowName, toState, user, reason = '') {
167
+ // Validate transition
168
+ const record = get(entityType, entityId);
169
+ if (!record) throw new AppError('Record not found', 'NOT_FOUND', HTTP.NOT_FOUND);
170
+
171
+ validateTransition(workflowName, record.status || record.stage, toState, user);
172
+
173
+ // Update record
174
+ const updates = {
175
+ status: toState,
176
+ updated_at: Math.floor(Date.now() / 1000),
177
+ };
178
+
179
+ if (reason) {
180
+ updates.transition_reason = reason;
181
+ }
182
+
183
+ // Dynamically import write engine to avoid circular dependency
184
+ const { update: updateRecord } = await import('./query-engine-write.js');
185
+ const updated = updateRecord(entityType, entityId, updates, user);
186
+
187
+ // Execute hooks
188
+ executeHook(`transition:${entityType}`, {
189
+ entity: entityType,
190
+ id: entityId,
191
+ from: record.status,
192
+ to: toState,
193
+ user,
194
+ record: updated,
195
+ }).catch(err => console.error('transition hook error:', err));
196
+
197
+ return updated;
198
+ }
199
+
200
+ /**
201
+ * Get state field name for workflow (usually 'status' or 'stage')
202
+ * @param {string} workflowName
203
+ * @returns {string}
204
+ */
205
+ export function getStateField(workflowName) {
206
+ const def = getWorkflowDef(workflowName);
207
+ return def.state_field || 'status';
208
+ }
209
+
210
+ /**
211
+ * Get stage labels mapping
212
+ * @param {string} workflowName
213
+ * @returns {object}
214
+ */
215
+ export function getStageLabels(workflowName) {
216
+ const def = getWorkflowDef(workflowName);
217
+ const labels = {};
218
+ if (def.stages) {
219
+ for (const stage of def.stages) {
220
+ labels[stage.name] = stage.label || stage.name;
221
+ }
222
+ }
223
+ return labels;
224
+ }
225
+
226
+ /**
227
+ * Get locked fields for a state (readonly fields)
228
+ * @param {string} workflowName
229
+ * @param {string} state
230
+ * @returns {string[]}
231
+ */
232
+ export function getStateLocks(workflowName, state) {
233
+ const def = getWorkflowDef(workflowName);
234
+ const stage = def.stageMap[state];
235
+ if (!stage) return [];
236
+ return stage.locks || [];
237
+ }
238
+
239
+ /**
240
+ * Get actions available in a state
241
+ * @param {string} workflowName
242
+ * @param {string} state
243
+ * @returns {string[]}
244
+ */
245
+ export function getStateActions(workflowName, state) {
246
+ const def = getWorkflowDef(workflowName);
247
+ const stage = def.stageMap[state];
248
+ if (!stage) return [];
249
+ return stage.actions || [];
250
+ }
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Thatcher HTTP Server - Simplified generic CRUD router
3
+ * Provides out-of-the-box API for all entities
4
+ */
5
+
6
+ import http from 'http';
7
+ import { fileURLToPath, pathToFileURL } from 'url';
8
+ import * as thatcherLib from '../index.js';
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+
12
+ export function createServer(options) {
13
+ const { thatcher, config, configEngine, port, host } = options;
14
+ const PORT = port || 3000;
15
+ const SERVER_START = Date.now();
16
+
17
+ let systemInitialized = false;
18
+ const moduleCache = new Map();
19
+
20
+ // Debug registry
21
+ globalThis.__debug__ = globalThis.__debug__ || {};
22
+ globalThis.__debug__.moduleCache = { get size() { return moduleCache.size; }, entries: () => [...moduleCache.keys()] };
23
+ globalThis.__debug__.activeRequests = { count: 0 };
24
+ globalThis.__debug__.hooks = null;
25
+ globalThis.__debug__.serverStart = SERVER_START;
26
+ globalThis.__debug__.uptime = () => Date.now() - SERVER_START;
27
+
28
+ // Load module with caching
29
+ const load = (p) => {
30
+ const cached = moduleCache.get(p);
31
+ if (cached) return cached;
32
+ return import(`file://${p}?t=${Date.now()}`).then(mod => {
33
+ moduleCache.set(p, mod);
34
+ return mod;
35
+ });
36
+ };
37
+
38
+ const server = http.createServer(async (req, res) => {
39
+ const t0 = Date.now();
40
+ globalThis.__debug__.activeRequests.count++;
41
+
42
+ try {
43
+ // Initialize on first request
44
+ if (!systemInitialized) {
45
+ const { loadPlugins } = await load(path.join(__dirname, '../plugins/index.js'));
46
+ await loadPlugins(configEngine);
47
+ systemInitialized = true;
48
+ console.log('[Server] System ready');
49
+ }
50
+
51
+ const url = new URL(req.url, `http://${req.headers.host}`);
52
+ const pathname = url.pathname;
53
+
54
+ // API routes
55
+ if (pathname.startsWith('/api/')) {
56
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
57
+ const parts = pathname.slice(5).split('/').filter(Boolean); // remove /api/
58
+
59
+ if (parts.length === 0) {
60
+ res.writeHead(400);
61
+ res.end(JSON.stringify({ error: 'Entity required' }));
62
+ return;
63
+ }
64
+
65
+ const entity = parts[0];
66
+ const id = parts[1] || null;
67
+ const action = parts[2] || null;
68
+
69
+ // Check if user has custom route for this
70
+ const userRoutePath = path.join(process.cwd(), 'app/api', ...parts, 'route.js');
71
+ const routeExists = await fileExists(userRoutePath);
72
+
73
+ if (routeExists) {
74
+ const mod = await load(userRoutePath);
75
+ const handler = mod[req.method] || mod.default;
76
+ if (handler) {
77
+ const response = await handler(req, { params: { entity, id, action, path: parts.slice(1) }, configEngine });
78
+ return sendResponse(res, response);
79
+ }
80
+ }
81
+
82
+ // Fall back to generic CRUD
83
+ return await handleGenericCrud(req, res, entity, id, action, thatcher);
84
+ }
85
+
86
+ // Static file serving (simplified)
87
+ if (serveStaticFile(pathname, req, res, load)) {
88
+ return;
89
+ }
90
+
91
+ // Page routing (UI)
92
+ if (!pathname.startsWith('/api/')) {
93
+ try {
94
+ const { handlePage } = await load(path.join(__dirname, '../ui/page-handler.js'));
95
+ const html = await handlePage(pathname, req, res, configEngine, thatcher);
96
+ if (html) {
97
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
98
+ res.setHeader('Content-Length', Buffer.byteLength(html, 'utf-8'));
99
+ res.writeHead(200);
100
+ res.end(html);
101
+ return;
102
+ }
103
+ } catch (e) {
104
+ console.error('[Page]', e.message);
105
+ }
106
+ }
107
+
108
+ // 404
109
+ res.writeHead(404, { 'Content-Type': 'application/json' });
110
+ res.end(JSON.stringify({ error: 'Not found' }));
111
+
112
+ } catch (err) {
113
+ console.error('[Server]', err.message);
114
+ res.writeHead(500, { 'Content-Type': 'application/json' });
115
+ res.end(JSON.stringify({ error: err.message }));
116
+ } finally {
117
+ globalThis.__debug__.activeRequests.count--;
118
+ }
119
+ });
120
+
121
+ return server;
122
+ }
123
+
124
+ /**
125
+ * Serve static file if exists
126
+ */
127
+ async function serveStaticFile(pathname, req, res, load) {
128
+ if (pathname === '/' || pathname === '/index.html') {
129
+ // Could serve SPA
130
+ return false;
131
+ }
132
+ // Try to serve from public/ or static/
133
+ const filePath = path.join(process.cwd(), 'public', pathname);
134
+ try {
135
+ if (await fileExists(filePath)) {
136
+ const content = await fs.promises.readFile(filePath);
137
+ const ext = path.extname(filePath);
138
+ const mime = {
139
+ '.js': 'application/javascript',
140
+ '.css': 'text/css',
141
+ '.html': 'text/html',
142
+ '.png': 'image/png',
143
+ '.jpg': 'image/jpeg',
144
+ '.svg': 'image/svg+xml',
145
+ }[ext] || 'text/plain';
146
+ res.setHeader('Content-Type', mime);
147
+ res.writeHead(200);
148
+ res.end(content);
149
+ return true;
150
+ }
151
+ } catch {}
152
+ return false;
153
+ }
154
+
155
+ /**
156
+ * File exists check
157
+ */
158
+ function fileExists(p) {
159
+ try {
160
+ return fs.existsSync(p);
161
+ } catch {
162
+ return false;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Send response appropriately
168
+ */
169
+ async function sendResponse(res, response) {
170
+ const headerObj = {};
171
+ if (response.headers) {
172
+ for (const [k, v] of response.headers) {
173
+ headerObj[k] = v;
174
+ }
175
+ }
176
+
177
+ if (response.status >= 300 && response.status < 400) {
178
+ res.writeHead(response.status, headerObj);
179
+ res.end();
180
+ return;
181
+ }
182
+
183
+ const ct = (headerObj['Content-Type'] || '').toLowerCase();
184
+ const isJson = !ct || ct.includes('json');
185
+ let bodyOut;
186
+
187
+ if (isJson) {
188
+ bodyOut = JSON.stringify(await response.json());
189
+ if (!headerObj['Content-Type']) headerObj['Content-Type'] = 'application/json; charset=utf-8';
190
+ } else {
191
+ bodyOut = await response.text();
192
+ }
193
+
194
+ headerObj['Content-Length'] = Buffer.byteLength(bodyOut).toString();
195
+ res.writeHead(response.status, headerObj);
196
+ res.end(bodyOut);
197
+ }
198
+
199
+ /**
200
+ * Generic CRUD handler
201
+ */
202
+ async function handleGenericCrud(req, res, entity, id, action, thatcher) {
203
+ // Simple auth: get from cookie or header
204
+ let user = null;
205
+ // In a real implementation, we'd decode session token
206
+ // For now, default to system user for testing
207
+ user = { id: 'system', role: 'admin' };
208
+
209
+ const { configEngine } = thatcher;
210
+ let spec;
211
+ try {
212
+ spec = configEngine.generateEntitySpec(entity);
213
+ } catch (e) {
214
+ res.writeHead(404);
215
+ res.end(JSON.stringify({ error: `Entity '${entity}' not found` }));
216
+ return;
217
+ }
218
+
219
+ // Permission check would go here via thatcher.can(user, spec, action)
220
+
221
+ let body;
222
+ try {
223
+ body = await readBody(req);
224
+ } catch (e) {
225
+ res.writeHead(400);
226
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
227
+ return;
228
+ }
229
+
230
+ try {
231
+ let result;
232
+ let status = 200;
233
+
234
+ switch (req.method) {
235
+ case 'GET':
236
+ if (id) {
237
+ result = await thatcher.get(entity, id);
238
+ if (!result) {
239
+ res.writeHead(404);
240
+ res.end(JSON.stringify({ error: 'Not found' }));
241
+ return;
242
+ }
243
+ } else {
244
+ result = await thatcher.list(entity);
245
+ }
246
+ break;
247
+
248
+ case 'POST':
249
+ result = await thatcher.create(entity, body, user);
250
+ status = 201;
251
+ break;
252
+
253
+ case 'PUT':
254
+ case 'PATCH':
255
+ if (!id) {
256
+ res.writeHead(400);
257
+ res.end(JSON.stringify({ error: 'ID required' }));
258
+ return;
259
+ }
260
+ result = await thatcher.update(entity, id, body, user);
261
+ break;
262
+
263
+ case 'DELETE':
264
+ if (!id) {
265
+ res.writeHead(400);
266
+ res.end(JSON.stringify({ error: 'ID required' }));
267
+ return;
268
+ }
269
+ await thatcher.delete(entity, id);
270
+ res.writeHead(204);
271
+ res.end();
272
+ return;
273
+
274
+ default:
275
+ res.writeHead(405);
276
+ res.end(JSON.stringify({ error: 'Method not allowed' }));
277
+ return;
278
+ }
279
+
280
+ res.writeHead(status, { 'Content-Type': 'application/json' });
281
+ res.end(JSON.stringify(result));
282
+ } catch (err) {
283
+ console.error('[API]', err.message);
284
+ res.writeHead(500);
285
+ res.end(JSON.stringify({ error: err.message }));
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Read request body
291
+ */
292
+ async function readBody(req) {
293
+ return new Promise((resolve, reject) => {
294
+ let data = '';
295
+ req.on('data', chunk => data += chunk);
296
+ req.on('end', () => {
297
+ try {
298
+ resolve(JSON.parse(data));
299
+ } catch {
300
+ resolve(data);
301
+ }
302
+ });
303
+ req.on('error', reject);
304
+ });
305
+ }