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,83 @@
1
+ /**
2
+ * CRUD Factory - Creates per-entity CRUD handler objects
3
+ * Adapted from moonlanding/src/lib/crud-factory.js
4
+ */
5
+
6
+ import { createCrudHandlers as buildHandlers } from './crud-handlers.js';
7
+ import { getConfigEngineSync } from './config-generator-engine.js';
8
+
9
+ /**
10
+ * Create CRUD handlers for an entity
11
+ * @param {string} entityName
12
+ * @returns {object} Handler functions (GET, POST, PUT, PATCH, DELETE)
13
+ */
14
+ export function createCrudHandlers(entityName) {
15
+ const configEngine = getConfigEngineSync();
16
+ const spec = configEngine.generateEntitySpec(entityName);
17
+
18
+ return {
19
+ GET: async (request, context) => {
20
+ // Handle list or single get based on presence of id
21
+ const { params } = context;
22
+ const id = params?.id;
23
+
24
+ if (id) {
25
+ return buildHandlers(spec).get(id, request, context);
26
+ } else {
27
+ return buildHandlers(spec).list(request, context);
28
+ }
29
+ },
30
+
31
+ POST: async (request, context) => {
32
+ return buildHandlers(spec).create(request, context);
33
+ },
34
+
35
+ PUT: async (request, context) => {
36
+ const { params } = context;
37
+ const id = params?.id;
38
+ if (!id) throw new Error('ID required for PUT');
39
+ return buildHandlers(spec).update(id, request, context);
40
+ },
41
+
42
+ PATCH: async (request, context) => {
43
+ const { params } = context;
44
+ const id = params?.id;
45
+ if (!id) throw new Error('ID required for PATCH');
46
+ return buildHandlers(spec).update(id, request, context);
47
+ },
48
+
49
+ DELETE: async (request, context) => {
50
+ const { params } = context;
51
+ const id = params?.id;
52
+ if (!id) throw new Error('ID required for DELETE');
53
+ return buildHandlers(spec).remove(id, request, context);
54
+ },
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Create generic controller for an entity
60
+ * @param {string} entityName
61
+ * @returns {object}
62
+ */
63
+ export function createEntityController(entityName) {
64
+ const configEngine = getConfigEngineSync();
65
+ const spec = configEngine.generateEntitySpec(entityName);
66
+ const handlers = buildHandlers(entityName, spec);
67
+
68
+ return {
69
+ list: handlers.list,
70
+ get: handlers.get,
71
+ create: handlers.create,
72
+ update: handlers.update,
73
+ delete: handlers.remove,
74
+
75
+ // Custom actions from config
76
+ async customAction(action, request, context) {
77
+ const { params } = context;
78
+ const id = params?.id;
79
+ const body = await request.json();
80
+ return handlers.customAction(action, id, body, context);
81
+ },
82
+ };
83
+ }
@@ -0,0 +1,244 @@
1
+ /**
2
+ * CRUD Handlers - HTTP handler implementations for entity operations
3
+ * This is where request/response handling, validation, auth, and business logic meet
4
+ */
5
+
6
+ import { list, get, listWithPagination, searchWithPagination } from './query-engine.js';
7
+ import { create, update, remove } from './query-engine-write.js';
8
+ import { validateEntity, validateUpdate, sanitizeData } from './validate.js';
9
+ import { requirePermission, getSessionToken } from './auth-middleware.js';
10
+ import { executeHook } from './hook-engine.js';
11
+ import { AppError, NotFoundError, ValidationError } from './error-handler.js';
12
+ import { ok, created, paginated, noContent, error } from './response-formatter.js';
13
+ import { HTTP } from '../config/constants.js';
14
+ import { permissionService } from '../services/permission.service.js';
15
+ import { parse as parseQuery } from './query-string-adapter.js';
16
+ import { now } from './database-core.js';
17
+ import { getConfigEngineSync } from './config-generator-engine.js';
18
+
19
+ /**
20
+ * Build a complete set of CRUD handlers for an entity
21
+ * @param {object} spec - Entity specification
22
+ * @returns {object}
23
+ */
24
+ export function createCrudHandlers(entityName, spec) {
25
+ if (!spec) {
26
+ spec = getConfigEngineSync().generateEntitySpec(entityName);
27
+ }
28
+
29
+ return {
30
+ /**
31
+ * List entities (GET /api/:entity)
32
+ */
33
+ list: async (req, context) => {
34
+ const { user } = context;
35
+ await requirePermission(user, spec, 'list');
36
+
37
+ const { q, page, pageSize, filters } = await parseQuery(req);
38
+ const config = getConfigEngineSync().getConfig();
39
+ const paginationCfg = config.system?.pagination || { default_page_size: 50, max_page_size: 500 };
40
+
41
+ const finalPage = page || 1;
42
+ if (!Number.isInteger(finalPage) || finalPage < 1) {
43
+ throw new AppError('page must be >= 1', 'BAD_REQUEST', HTTP.BAD_REQUEST);
44
+ }
45
+
46
+ const requestedPageSize = pageSize || paginationCfg.default_page_size;
47
+ if (!Number.isInteger(requestedPageSize) || requestedPageSize < 1) {
48
+ throw new AppError('pageSize must be >= 1', 'BAD_REQUEST', HTTP.BAD_REQUEST);
49
+ }
50
+
51
+ const finalPageSize = Math.min(requestedPageSize, paginationCfg.max_page_size || 500);
52
+
53
+ let items, pagination;
54
+
55
+ if (q) {
56
+ const result = await searchWithPagination(entityName, q, {}, finalPage, finalPageSize);
57
+ items = result.items;
58
+ pagination = result.pagination;
59
+ } else {
60
+ const coercedFilters = {};
61
+ if (filters) {
62
+ for (const [key, value] of Object.entries(filters)) {
63
+ const fd = spec.fields?.[key];
64
+ coercedFilters[key] = fd ? coerceFieldValue(value, fd.type) : value;
65
+ }
66
+ }
67
+ const result = await listWithPagination(entityName, coercedFilters, finalPage, finalPageSize);
68
+ items = result.items;
69
+ pagination = result.pagination;
70
+ }
71
+
72
+ const filtered = permissionService.filterRecords(user, spec, items);
73
+ const filteredItems = filtered.map(i => permissionService.filterFields(user, spec, i));
74
+
75
+ return paginated(filteredItems, pagination);
76
+ },
77
+
78
+ /**
79
+ * Get single entity (GET /api/:entity/:id)
80
+ */
81
+ get: async (id, req, context) => {
82
+ const { user } = context;
83
+ await requirePermission(user, spec, 'view');
84
+
85
+ if (!id) throw new AppError('ID required', 'BAD_REQUEST', HTTP.BAD_REQUEST);
86
+
87
+ const item = get(entityName, id);
88
+ if (!item) throw NotFoundError(entityName, id);
89
+
90
+ if (!permissionService.checkRowAccess(user, spec, item)) {
91
+ throw new AppError('Access denied', 'FORBIDDEN', HTTP.FORBIDDEN);
92
+ }
93
+
94
+ return ok(permissionService.filterFields(user, spec, item));
95
+ },
96
+
97
+ /**
98
+ * Create entity (POST /api/:entity)
99
+ */
100
+ create: async (req, context) => {
101
+ const { user } = context;
102
+ await requirePermission(user, spec, 'create');
103
+
104
+ const rawData = await req.json();
105
+ permissionService.enforceEditPermissions(user, spec, rawData);
106
+
107
+ const errors = await validateEntity(entityName, rawData);
108
+ if (Object.keys(errors).length > 0) {
109
+ throw new ValidationError('Validation failed', errors);
110
+ }
111
+
112
+ const sanitized = sanitizeData(entityName, rawData, spec);
113
+ const record = create(entityName, sanitized, user);
114
+
115
+ // Execute hooks
116
+ executeHook(`create:${entityName}:after`, {
117
+ entity: entityName,
118
+ id: record.id,
119
+ data: record,
120
+ user,
121
+ }).catch(console.error);
122
+
123
+ return created(permissionService.filterFields(user, spec, record));
124
+ },
125
+
126
+ /**
127
+ * Update entity (PUT/PATCH /api/:entity/:id)
128
+ */
129
+ update: async (id, req, context) => {
130
+ const { user } = context;
131
+ await requirePermission(user, spec, 'edit');
132
+
133
+ if (!id) throw new AppError('ID required', 'BAD_REQUEST', HTTP.BAD_REQUEST);
134
+
135
+ const existing = get(entityName, id);
136
+ if (!existing) throw NotFoundError(entityName, id);
137
+
138
+ if (!permissionService.checkRowAccess(user, spec, existing)) {
139
+ throw new AppError('Access denied', 'FORBIDDEN', HTTP.FORBIDDEN);
140
+ }
141
+
142
+ const rawData = await req.json();
143
+ permissionService.enforceEditPermissions(user, spec, rawData);
144
+
145
+ const errors = await validateUpdate(entityName, rawData, existing);
146
+ if (Object.keys(errors).length > 0) {
147
+ throw new ValidationError('Validation failed', errors);
148
+ }
149
+
150
+ const sanitized = sanitizeData(entityName, rawData, spec, existing);
151
+ const record = update(entityName, id, sanitized, user);
152
+
153
+ executeHook(`update:${entityName}:after`, {
154
+ entity: entityName,
155
+ id,
156
+ data: record,
157
+ before: existing,
158
+ after: record,
159
+ user,
160
+ }).catch(console.error);
161
+
162
+ return ok(permissionService.filterFields(user, spec, record));
163
+ },
164
+
165
+ /**
166
+ * Delete entity (DELETE /api/:entity/:id)
167
+ */
168
+ remove: async (id, req, context) => {
169
+ const { user } = context;
170
+ await requirePermission(user, spec, 'delete');
171
+
172
+ if (!id) throw new AppError('ID required', 'BAD_REQUEST', HTTP.BAD_REQUEST);
173
+
174
+ const existing = get(entityName, id);
175
+ if (!existing) throw NotFoundError(entityName, id);
176
+
177
+ if (!permissionService.checkRowAccess(user, spec, existing)) {
178
+ throw new AppError('Access denied', 'FORBIDDEN', HTTP.FORBIDDEN);
179
+ }
180
+
181
+ const result = remove(entityName, id);
182
+
183
+ executeHook(`delete:${entityName}:after`, {
184
+ entity: entityName,
185
+ id,
186
+ data: result,
187
+ user,
188
+ }).catch(console.error);
189
+
190
+ return noContent();
191
+ },
192
+
193
+ /**
194
+ * Custom action handler
195
+ */
196
+ customAction: async (action, id, data, context) => {
197
+ const { user } = context;
198
+ await requirePermission(user, spec, 'edit'); // Simplified permission check
199
+
200
+ const record = get(entityName, id);
201
+ if (!record) throw NotFoundError(entityName, id);
202
+
203
+ // Custom action logic (upload, manage_flags, etc.)
204
+ if (action === 'upload_files') {
205
+ const files = Array.isArray(data.files) ? data.files : [data.files];
206
+ executeHook(`upload_files:${entityName}:after`, {
207
+ entity: entityName,
208
+ id,
209
+ data: { id, uploaded_files: files },
210
+ user,
211
+ });
212
+ return ok({ id, uploaded_files: files });
213
+ }
214
+
215
+ // More actions can be added here
216
+ throw new AppError(`Unknown action: ${action}`, 'BAD_REQUEST', HTTP.BAD_REQUEST);
217
+ },
218
+ };
219
+ }
220
+
221
+ /**
222
+ * Coerce field value to correct type
223
+ * @param {any} value
224
+ * @param {string} type
225
+ * @returns {any}
226
+ */
227
+ function coerceFieldValue(value, type) {
228
+ if (value === null || value === undefined) return value;
229
+
230
+ switch (type) {
231
+ case 'int':
232
+ case 'decimal':
233
+ return Number(value);
234
+ case 'bool':
235
+ return Boolean(value);
236
+ case 'json':
237
+ return typeof value === 'string' ? JSON.parse(value) : value;
238
+ case 'date':
239
+ case 'timestamp':
240
+ return Number(value);
241
+ default:
242
+ return String(value);
243
+ }
244
+ }
@@ -0,0 +1,63 @@
1
+ import crypto from 'crypto';
2
+ import { HTTP } from '@/config/constants';
3
+
4
+ const tokens = new Map();
5
+ const TOKEN_TTL = 3600000;
6
+
7
+ function cleanExpiredTokens() {
8
+ const now = Date.now();
9
+ for (const [token, data] of tokens.entries()) {
10
+ if (now - data.createdAt > TOKEN_TTL) {
11
+ tokens.delete(token);
12
+ }
13
+ }
14
+ }
15
+
16
+ export function generateToken() {
17
+ const token = crypto.randomBytes(32).toString('hex');
18
+ tokens.set(token, { createdAt: Date.now() });
19
+ if (tokens.size > 10000) cleanExpiredTokens();
20
+ return token;
21
+ }
22
+
23
+ export function validateToken(token) {
24
+ if (!token || typeof token !== 'string') return false;
25
+ const data = tokens.get(token);
26
+ if (!data) return false;
27
+ if (Date.now() - data.createdAt > TOKEN_TTL) {
28
+ tokens.delete(token);
29
+ return false;
30
+ }
31
+ tokens.delete(token);
32
+ return true;
33
+ }
34
+
35
+ export async function withCsrfValidation(handler) {
36
+ return async (request, context) => {
37
+ const method = request.method?.toUpperCase();
38
+ if (!['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
39
+ return handler(request, context);
40
+ }
41
+
42
+ try {
43
+ const contentType = request.headers.get('content-type') || '';
44
+ const token = request.headers.get('x-csrf-token');
45
+
46
+ if (!token || !validateToken(token)) {
47
+ console.warn(`[CSRF] Invalid token for ${method}`);
48
+ return new Response(JSON.stringify({ error: 'Invalid CSRF token' }), {
49
+ status: HTTP.FORBIDDEN,
50
+ headers: { 'Content-Type': 'application/json' },
51
+ });
52
+ }
53
+ } catch (e) {
54
+ console.error('[CSRF] Validation error:', e.message);
55
+ return new Response(JSON.stringify({ error: 'CSRF validation failed' }), {
56
+ status: HTTP.BAD_REQUEST,
57
+ headers: { 'Content-Type': 'application/json' },
58
+ });
59
+ }
60
+
61
+ return handler(request, context);
62
+ };
63
+ }
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Database Core - SQLite initialization and schema management
3
+ * Adapted from moonlanding/src/lib/database-core.js
4
+ */
5
+
6
+ import Database from 'better-sqlite3';
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+
10
+ const SQL_TYPES = {
11
+ id: 'TEXT PRIMARY KEY',
12
+ text: 'TEXT',
13
+ textarea: 'TEXT',
14
+ email: 'TEXT',
15
+ int: 'INTEGER',
16
+ decimal: 'REAL',
17
+ bool: 'INTEGER',
18
+ date: 'INTEGER',
19
+ timestamp: 'INTEGER',
20
+ json: 'TEXT',
21
+ image: 'TEXT',
22
+ ref: 'TEXT',
23
+ enum: 'TEXT',
24
+ };
25
+
26
+ let db = null;
27
+ let migrationComplete = false;
28
+ const moduleCache = new Map();
29
+
30
+ /**
31
+ * Get or create database instance
32
+ * @param {string} dbPath - Path to SQLite database file
33
+ * @returns {Database} better-sqlite3 database instance
34
+ */
35
+ export function getDatabase(dbPath = null) {
36
+ if (db && !dbPath) return db;
37
+
38
+ const DB_PATH = dbPath || path.resolve(process.cwd(), 'data', 'app.db');
39
+ const dataDir = path.dirname(DB_PATH);
40
+
41
+ if (!fs.existsSync(dataDir)) {
42
+ fs.mkdirSync(dataDir, { recursive: true });
43
+ }
44
+
45
+ db = new Database(DB_PATH);
46
+ db.pragma('journal_mode = WAL');
47
+ const BUSY_TIMEOUT_MS = process.env.DATABASE_BUSY_TIMEOUT_MS || '5000';
48
+ db.pragma(`busy_timeout = ${BUSY_TIMEOUT_MS}`);
49
+ db.pragma('synchronous = NORMAL');
50
+ db.pragma('foreign_keys = ON');
51
+ db.pragma('auto_vacuum = INCREMENTAL');
52
+
53
+ return db;
54
+ }
55
+
56
+ /**
57
+ * Generate unique ID using nanoid-like algorithm
58
+ * @returns {string}
59
+ */
60
+ export function genId() {
61
+ // Simple but effective ID generation
62
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
63
+ }
64
+
65
+ /**
66
+ * Get current Unix timestamp (seconds)
67
+ * @returns {number}
68
+ */
69
+ export function now() {
70
+ return Math.floor(Date.now() / 1000);
71
+ }
72
+
73
+ /**
74
+ * Build column definition from field spec
75
+ * @param {string} key - Field name
76
+ * @param {object} field - Field definition
77
+ * @returns {string} SQL column definition
78
+ */
79
+ function buildColumnDef(key, field) {
80
+ let col = `"${key}" ${SQL_TYPES[field.type] || 'TEXT'}`;
81
+
82
+ if (field.required && field.type !== 'id') {
83
+ col += ' NOT NULL';
84
+ }
85
+
86
+ if (field.unique) {
87
+ col += ' UNIQUE';
88
+ }
89
+
90
+ if (field.default !== undefined) {
91
+ if (typeof field.default === 'string' || typeof field.default === 'number' || typeof field.default === 'boolean') {
92
+ const defaultVal = typeof field.default === 'string'
93
+ ? `'${field.default.replace(/'/g, "''")}'`
94
+ : field.default;
95
+ col += ` DEFAULT ${defaultVal}`;
96
+ }
97
+ }
98
+
99
+ return col;
100
+ }
101
+
102
+ /**
103
+ * Run migrations to create/update tables from entity specs
104
+ * @param {object} configEngine - Config engine with entity specs
105
+ */
106
+ export function migrate(configEngine) {
107
+ if (migrationComplete) return;
108
+
109
+ const dbInstance = getDatabase();
110
+ let specsToUse = {};
111
+
112
+ try {
113
+ const allEntities = configEngine.getAllEntities();
114
+ for (const entityName of allEntities) {
115
+ specsToUse[entityName] = configEngine.generateEntitySpec(entityName);
116
+ }
117
+ } catch (e) {
118
+ console.error('[Database] Failed to get specs from ConfigEngine during migration:', e.message);
119
+ return;
120
+ }
121
+
122
+ // Create tables
123
+ for (const spec of Object.values(specsToUse)) {
124
+ if (!spec) continue;
125
+
126
+ const tableName = spec.name === 'user' ? 'users' : spec.name;
127
+ const columns = [];
128
+ const foreignKeys = [];
129
+
130
+ // Build columns from fields
131
+ for (const [key, field] of Object.entries(spec.fields || {})) {
132
+ columns.push(buildColumnDef(key, field));
133
+
134
+ if (field.type === 'ref' && field.ref) {
135
+ const refTable = field.ref === 'user' ? 'users' : field.ref;
136
+ foreignKeys.push(`FOREIGN KEY ("${key}") REFERENCES "${refTable}"(id)`);
137
+ }
138
+ }
139
+
140
+ const fkPart = foreignKeys.length ? (',\n' + foreignKeys.join(',\n')) : '';
141
+ const sql = `CREATE TABLE IF NOT EXISTS "${tableName}" (${columns.join(',\n')}${fkPart})`;
142
+
143
+ try {
144
+ dbInstance.exec(sql);
145
+ } catch (e) {
146
+ console.error(`[Database] Table creation failed for ${tableName}:`, e.message, '\nSQL:', sql);
147
+ throw e;
148
+ }
149
+
150
+ // Add new columns (ALTER TABLE for schema evolution)
151
+ try {
152
+ const existingCols = new Set(
153
+ dbInstance.prepare(`PRAGMA table_info("${tableName}")`).all().map(c => c.name)
154
+ );
155
+
156
+ for (const [key, field] of Object.entries(spec.fields || {})) {
157
+ if (!existingCols.has(key)) {
158
+ let colType = SQL_TYPES[field.type] || 'TEXT';
159
+ let alterSql = `ALTER TABLE "${tableName}" ADD COLUMN "${key}" ${colType}`;
160
+
161
+ if (field.default !== undefined && (typeof field.default === 'string' || typeof field.default === 'number' || typeof field.default === 'boolean')) {
162
+ const defaultVal = typeof field.default === 'string'
163
+ ? `'${field.default.replace(/'/g, "''")}'`
164
+ : field.default;
165
+ alterSql += ` DEFAULT ${defaultVal}`;
166
+ }
167
+
168
+ dbInstance.exec(alterSql);
169
+ }
170
+ }
171
+ } catch (e) {
172
+ console.error(`[Database] Column migration failed for ${tableName}:`, e.message);
173
+ }
174
+ }
175
+
176
+ // Create indexes
177
+ for (const spec of Object.values(specsToUse)) {
178
+ if (!spec) continue;
179
+
180
+ const tableName = spec.name === 'user' ? 'users' : spec.name;
181
+ const searchFields = [];
182
+
183
+ for (const [key, field] of Object.entries(spec.fields || {})) {
184
+ if (field.type === 'ref' || field.sortable || field.search) {
185
+ try {
186
+ dbInstance.exec(`CREATE INDEX IF NOT EXISTS idx_${tableName}_${key} ON "${tableName}"("${key}")`);
187
+ } catch (e) {
188
+ console.error(`[Database] Index creation failed for ${tableName}.${key}:`, e.message);
189
+ }
190
+ }
191
+
192
+ if (field.search || key === 'name' || key === 'description') {
193
+ searchFields.push(`"${key}"`);
194
+ }
195
+ }
196
+
197
+ // Create FTS virtual table for search
198
+ if (searchFields.length > 0) {
199
+ try {
200
+ dbInstance.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS ${tableName}_fts USING fts5(${searchFields.join(', ')}, content="${tableName}", content_rowid=id)`);
201
+ } catch (e) {
202
+ console.error(`[Database] FTS table creation failed for ${tableName}:`, e.message);
203
+ }
204
+ }
205
+ }
206
+
207
+ // Run additional migrations
208
+ runCustomMigrations(dbInstance, specsToUse);
209
+
210
+ migrationComplete = true;
211
+ }
212
+
213
+ /**
214
+ * Run custom migration logic (triggers, data migrations, etc.)
215
+ * @param {Database} dbInstance
216
+ * @param {object} specs
217
+ */
218
+ function runCustomMigrations(dbInstance, specs) {
219
+ // Placeholder for custom migration logic
220
+ // Can be extended via plugins
221
+ try {
222
+ // Create triggers for updated_at timestamps
223
+ for (const entityName of Object.keys(specs)) {
224
+ const tableName = entityName === 'user' ? 'users' : entityName;
225
+ // Add timestamp triggers if needed
226
+ }
227
+ } catch (e) {
228
+ console.error('[Database] Custom migrations failed:', e.message);
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Ensure database is initialized (lazy migration)
234
+ */
235
+ export function ensureInitialized(configEngine) {
236
+ if (!migrationComplete) {
237
+ migrate(configEngine);
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Reset database (for testing)
243
+ */
244
+ export function resetDatabase() {
245
+ const DB_PATH = path.resolve(process.cwd(), 'data', 'app.db');
246
+ try {
247
+ if (db) db.close();
248
+ if (fs.existsSync(DB_PATH)) fs.unlinkSync(DB_PATH);
249
+ if (fs.existsSync(DB_PATH + '-wal')) fs.unlinkSync(DB_PATH + '-wal');
250
+ if (fs.existsSync(DB_PATH + '-shm')) fs.unlinkSync(DB_PATH + '-shm');
251
+ db = null;
252
+ migrationComplete = false;
253
+ } catch (e) {
254
+ console.error('[Database] Reset failed:', e.message);
255
+ }
256
+ }
257
+
258
+ export { SQL_TYPES };