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,164 @@
1
+ /**
2
+ * Environment configuration
3
+ * Provides typed access to environment variables with defaults
4
+ */
5
+
6
+ /**
7
+ * Get environment variable with optional default
8
+ */
9
+ function getEnv(key, defaultValue = null) {
10
+ const val = process.env[key];
11
+ if (val === undefined || val === null) return defaultValue;
12
+ return val;
13
+ }
14
+
15
+ /**
16
+ * Get boolean from env
17
+ */
18
+ function getEnvBool(key, defaultValue = false) {
19
+ const val = getEnv(key);
20
+ if (val === null) return defaultValue;
21
+ return val === 'true' || val === '1';
22
+ }
23
+
24
+ /**
25
+ * Get number from env
26
+ */
27
+ function getEnvInt(key, defaultValue = 0) {
28
+ const val = getEnv(key);
29
+ if (val === null) return defaultValue;
30
+ const num = parseInt(val, 10);
31
+ return isNaN(num) ? defaultValue : num;
32
+ }
33
+
34
+ /**
35
+ * Configuration object builder
36
+ */
37
+ export function buildConfig(overrides = {}) {
38
+ const config = {
39
+ db: {
40
+ path: overrides.databasePath || getEnv('DATABASE_PATH', './data/app.db'),
41
+ ...overrides.db,
42
+ },
43
+
44
+ auth: {
45
+ google: {
46
+ clientId: getEnv('GOOGLE_CLIENT_ID'),
47
+ clientSecret: getEnv('GOOGLE_CLIENT_SECRET'),
48
+ redirectUri: getEnv('GOOGLE_REDIRECT_URI') || 'http://localhost:3000/api/auth/google/callback',
49
+ serviceAccountPath: getEnv('GOOGLE_SERVICE_ACCOUNT_PATH', './config/service-account.json'),
50
+ ...overrides.auth?.google,
51
+ },
52
+ session: {
53
+ secure: getEnv('NODE_ENV') === 'production',
54
+ expires: false,
55
+ cookieName: getEnv('SESSION_COOKIE_NAME', 'thatcher_session'),
56
+ maxAge: getEnvInt('SESSION_MAX_AGE', 60 * 60 * 24 * 30), // 30 days
57
+ ...overrides.auth?.session,
58
+ },
59
+ ...overrides.auth,
60
+ },
61
+
62
+ drive: {
63
+ serviceAccountPath: getEnv('GOOGLE_SERVICE_ACCOUNT_PATH', './config/service-account.json'),
64
+ rootFolderId: getEnv('GOOGLE_DRIVE_FOLDER_ID'),
65
+ cache: {
66
+ enabled: true,
67
+ ttl: 3600,
68
+ bucket: getEnv('CACHE_BUCKET', 'thatcher_cache'),
69
+ ...overrides.drive?.cache,
70
+ },
71
+ ...overrides.drive,
72
+ },
73
+
74
+ email: {
75
+ provider: getEnv('EMAIL_PROVIDER', 'nodemailer'),
76
+ from: getEnv('EMAIL_FROM', 'noreply@example.com'),
77
+ smtp: {
78
+ host: getEnv('EMAIL_HOST', 'smtp.gmail.com'),
79
+ port: getEnvInt('EMAIL_PORT', 587),
80
+ user: getEnv('EMAIL_USER'),
81
+ password: getEnv('EMAIL_PASSWORD'),
82
+ ...overrides.email?.smtp,
83
+ },
84
+ limits: {
85
+ daily: getEnvInt('EMAIL_DAILY_LIMIT', 500),
86
+ ratePerMinute: getEnvInt('EMAIL_RATE_PER_MINUTE', 10),
87
+ ...overrides.email?.limits,
88
+ },
89
+ ...overrides.email,
90
+ },
91
+
92
+ app: {
93
+ url: getEnv('APP_URL', 'http://localhost:3000'),
94
+ env: getEnv('NODE_ENV', 'development'),
95
+ debug: getEnvBool('DEBUG', false),
96
+ name: getEnv('APP_NAME', 'Thatcher App'),
97
+ ...overrides.app,
98
+ },
99
+
100
+ server: {
101
+ port: getEnvInt('PORT', 3000),
102
+ host: getEnv('HOST', '0.0.0.0'),
103
+ ...overrides.server,
104
+ },
105
+ };
106
+
107
+ return config;
108
+ }
109
+
110
+ /**
111
+ * Check if Google OAuth is configured
112
+ */
113
+ export function hasGoogleAuth(config) {
114
+ const cfg = config?.auth?.google || buildConfig().auth.google;
115
+ return !!(cfg.clientId && cfg.clientSecret);
116
+ }
117
+
118
+ /**
119
+ * Check if Drive integration is configured
120
+ */
121
+ export function hasDriveConfig(config) {
122
+ const cfg = config?.drive || buildConfig().drive;
123
+ return !!cfg.rootFolderId;
124
+ }
125
+
126
+ /**
127
+ * Check if email sending is configured
128
+ */
129
+ export function hasEmailConfig(config) {
130
+ const cfg = config?.email || buildConfig().email;
131
+ return !!(cfg.smtp.user && cfg.smtp.password);
132
+ }
133
+
134
+ /**
135
+ * Validate environment configuration
136
+ * Returns array of warnings (non-fatal)
137
+ */
138
+ export function validateEnv(config = buildConfig()) {
139
+ const warnings = [];
140
+
141
+ if (!getEnv('DATABASE_PATH')) {
142
+ warnings.push('DATABASE_PATH not set, using default ./data/app.db');
143
+ }
144
+
145
+ if (config.app.env === 'production') {
146
+ if (!config.email.smtp.user) warnings.push('EMAIL_USER not set - email sending disabled');
147
+ if (!config.email.smtp.password) warnings.push('EMAIL_PASSWORD not set - email sending disabled');
148
+ if (!config.app.url) warnings.push('APP_URL not set - links in emails will use localhost');
149
+ }
150
+
151
+ if (!hasGoogleAuth(config)) {
152
+ warnings.push('Google OAuth not configured - auth will be email/password only');
153
+ }
154
+
155
+ return warnings;
156
+ }
157
+
158
+ export default {
159
+ buildConfig,
160
+ hasGoogleAuth,
161
+ hasDriveConfig,
162
+ hasEmailConfig,
163
+ validateEnv,
164
+ };
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Spec Helpers - Utility functions for entity specifications
3
+ * Provides easy access to entity specs, navigation, and derived properties
4
+ */
5
+
6
+ /**
7
+ * Get entity spec by name
8
+ * @param {string} name - Entity name
9
+ * @param {object} configEngine - Config engine instance
10
+ * @returns {object|null} Entity specification or null if not found
11
+ */
12
+ export function getSpec(name, configEngine) {
13
+ try {
14
+ return configEngine.generateEntitySpec(name);
15
+ } catch (error) {
16
+ if (error.message.includes('not found') || error.message.includes('Unknown entity')) {
17
+ return null;
18
+ }
19
+ throw error;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Get all navigable entities (top-level, non-embedded)
25
+ * @param {object} configEngine
26
+ * @returns {Array} Sorted array of entity specs
27
+ */
28
+ export function getNavItems(configEngine) {
29
+ try {
30
+ const allEntities = configEngine.getAllEntities();
31
+ return allEntities
32
+ .map(e => configEngine.generateEntitySpec(e))
33
+ .filter(s => !s.embedded && !s.parent && !s.system_entity)
34
+ .sort((a, b) => (a.order || 999) - (b.order || 999))
35
+ .map(s => ({
36
+ name: s.name,
37
+ label: s.labelPlural || s.label,
38
+ icon: s.icon,
39
+ href: `/${s.name}`,
40
+ }));
41
+ } catch (error) {
42
+ console.error('[spec-helpers] getNavItems error:', error.message);
43
+ return [];
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Get child entity definitions for a parent spec
49
+ * @param {object} spec - Parent entity spec
50
+ * @returns {Array} Child entity configs
51
+ */
52
+ export function getChildEntities(spec) {
53
+ if (!spec.children) return [];
54
+ return Object.entries(spec.children).map(([key, child]) => ({
55
+ key,
56
+ entity: child.entity,
57
+ label: child.label,
58
+ fk: child.fk,
59
+ filter: child.filter,
60
+ component: child.component,
61
+ }));
62
+ }
63
+
64
+ /**
65
+ * Get parent entity name
66
+ * @param {object} spec
67
+ * @returns {string|null}
68
+ */
69
+ export function getParentEntity(spec) {
70
+ return spec.parent || null;
71
+ }
72
+
73
+ /**
74
+ * Get default sort configuration
75
+ * @param {object} spec
76
+ * @returns {{field: string, dir: string}}
77
+ */
78
+ export function getDefaultSort(spec) {
79
+ return spec.list?.defaultSort || { field: 'created_at', dir: 'desc' };
80
+ }
81
+
82
+ /**
83
+ * Get available filters for entity
84
+ * @param {object} spec
85
+ * @returns {Array}
86
+ */
87
+ export function getAvailableFilters(spec) {
88
+ return spec.list?.filters || [];
89
+ }
90
+
91
+ /**
92
+ * Get page size for entity
93
+ * @param {object} spec
94
+ * @returns {number}
95
+ */
96
+ export function getPageSize(spec, systemPagination = { default_page_size: 20 }) {
97
+ return spec.list?.pageSize || systemPagination.default_page_size;
98
+ }
99
+
100
+ /**
101
+ * Get entity label (singular or plural)
102
+ * @param {object} spec
103
+ * @param {boolean} plural
104
+ * @returns {string}
105
+ */
106
+ export function getEntityLabel(spec, plural = false) {
107
+ return plural ? (spec.labelPlural || spec.label) : spec.label;
108
+ }
109
+
110
+ /**
111
+ * Get default initial state for entity (empty record with defaults)
112
+ * @param {object} spec
113
+ * @returns {object}
114
+ */
115
+ export function getInitialState(spec) {
116
+ const state = {};
117
+ for (const [key, field] of Object.entries(spec.fields)) {
118
+ if (field.type === 'id') continue;
119
+ if (field.default !== undefined) {
120
+ state[key] = field.default;
121
+ } else if (field.type === 'bool') {
122
+ state[key] = false;
123
+ } else if (field.type === 'int' || field.type === 'decimal') {
124
+ state[key] = 0;
125
+ } else if (field.type === 'json') {
126
+ state[key] = [];
127
+ } else if (field.type === 'date' || field.type === 'timestamp') {
128
+ state[key] = null;
129
+ } else {
130
+ state[key] = '';
131
+ }
132
+ }
133
+ return state;
134
+ }
135
+
136
+ /**
137
+ * Check if entity is embedded (used in parent contexts)
138
+ * @param {object} spec
139
+ * @returns {boolean}
140
+ */
141
+ export function isEmbeddedEntity(spec) {
142
+ return spec.embedded === true;
143
+ }
144
+
145
+ /**
146
+ * Check if entity is a top-level parent
147
+ * @param {object} spec
148
+ * @returns {boolean}
149
+ */
150
+ export function isParentEntity(spec) {
151
+ return !spec.embedded && !spec.parent;
152
+ }
153
+
154
+ /**
155
+ * Check if entity has child relationships
156
+ * @param {object} spec
157
+ * @returns {boolean}
158
+ */
159
+ export function hasChildRelationships(spec) {
160
+ return !!spec.children && Object.keys(spec.children).length > 0;
161
+ }
162
+
163
+ /**
164
+ * Get entity options (enums, picklists)
165
+ * @param {object} spec
166
+ * @param {string} optionKey
167
+ * @returns {Array}
168
+ */
169
+ export function getOptions(spec, optionKey) {
170
+ return spec.options?.[optionKey] || [];
171
+ }
172
+
173
+ /**
174
+ * Get display label for an option value
175
+ * @param {object} spec
176
+ * @param {string} optionKey
177
+ * @param {string} value
178
+ * @returns {string}
179
+ */
180
+ export function getOptionLabel(spec, optionKey, value) {
181
+ const option = getOptions(spec, optionKey).find(o => o.value === value);
182
+ return option?.label || String(value);
183
+ }
184
+
185
+ /**
186
+ * Get color for an option value (for UI badges)
187
+ * @param {object} spec
188
+ * @param {string} optionKey
189
+ * @param {string} value
190
+ * @returns {string}
191
+ */
192
+ export function getOptionColor(spec, optionKey, value) {
193
+ const option = getOptions(spec, optionKey).find(o => o.value === value);
194
+ return option?.color || 'gray';
195
+ }
196
+
197
+ /**
198
+ * Build navigation structure for menus
199
+ * @param {object} configEngine
200
+ * @param {object} user - Optional user for permission filtering
201
+ * @returns {Array} Navigation items
202
+ */
203
+ export function buildNavigation(configEngine, user = null) {
204
+ try {
205
+ const allEntities = configEngine.getAllEntities();
206
+ const items = allEntities
207
+ .map(e => configEngine.generateEntitySpec(e))
208
+ .filter(s => !s.embedded && !s.parent && !s.system_entity)
209
+ .sort((a, b) => (a.order || 999) - (b.order || 999))
210
+ .map(s => ({
211
+ name: s.name,
212
+ label: s.labelPlural || s.label,
213
+ icon: s.icon,
214
+ href: `/${s.name}`,
215
+ badge: s.badge,
216
+ }));
217
+
218
+ // Filter by user permissions if provided
219
+ if (user && configEngine.getRolePermissions) {
220
+ return items.filter(item => {
221
+ const spec = configEngine.generateEntitySpec(item.name);
222
+ const perms = configEngine.getRolePermissions(user.role, item.name);
223
+ return perms.includes('list') || perms.includes('view') || perms.includes('all');
224
+ });
225
+ }
226
+
227
+ return items;
228
+ } catch (error) {
229
+ console.error('[spec-helpers] buildNavigation error:', error.message);
230
+ return [];
231
+ }
232
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Auth Engine - Lucia session management and Google OAuth
3
+ * Simplified extraction from moonlanding/src/engine.server.js
4
+ */
5
+
6
+ import { Lucia } from 'lucia';
7
+ import { BetterSqlite3Adapter } from '@lucia-auth/adapter-sqlite';
8
+ import { Google } from 'arctic';
9
+ import bcrypt from 'bcrypt';
10
+ import { getDatabase } from './database-core.js';
11
+ import { buildConfig, hasGoogleAuth } from '../config/env.js';
12
+
13
+ let _lucia = null;
14
+ let _google = null;
15
+ let _adapter = null;
16
+
17
+ /**
18
+ * Initialize auth engine with database and config
19
+ * @param {object} [config] - Optional config override
20
+ */
21
+ export function initAuth(config = null) {
22
+ const cfg = config || buildConfig();
23
+ const db = getDatabase(cfg.db.path);
24
+
25
+ _adapter = new BetterSqlite3Adapter(db, { user: 'users', session: 'sessions' });
26
+
27
+ _lucia = new Lucia(_adapter, {
28
+ sessionCookie: {
29
+ expires: cfg.auth.session.expires,
30
+ attributes: {
31
+ secure: cfg.auth.session.secure,
32
+ httpOnly: true,
33
+ sameSite: 'lax',
34
+ },
35
+ },
36
+ getUserAttributes: (row) => ({
37
+ id: row.id,
38
+ email: row.email,
39
+ name: row.name,
40
+ avatar: row.avatar,
41
+ type: row.type,
42
+ role: row.role,
43
+ }),
44
+ });
45
+
46
+ if (hasGoogleAuth(cfg)) {
47
+ _google = new Google(
48
+ cfg.auth.google.clientId,
49
+ cfg.auth.google.clientSecret,
50
+ cfg.auth.google.redirectUri
51
+ );
52
+ }
53
+
54
+ return { lucia: _lucia, google: _google };
55
+ }
56
+
57
+ /**
58
+ * Get Lucia instance
59
+ * @returns {Lucia}
60
+ */
61
+ export function getLucia() {
62
+ if (!_lucia) initAuth();
63
+ return _lucia;
64
+ }
65
+
66
+ /**
67
+ * Get Google OAuth client
68
+ * @returns {Google|null}
69
+ */
70
+ export function getGoogle() {
71
+ if (!_google) initAuth();
72
+ return _google;
73
+ }
74
+
75
+ /**
76
+ * Parse cookies from header
77
+ * @param {string} cookieHeader
78
+ * @returns {object}
79
+ */
80
+ function parseCookies(cookieHeader) {
81
+ const cookies = {};
82
+ if (!cookieHeader) return cookies;
83
+ cookieHeader.split(';').forEach(cookie => {
84
+ const [name, value] = cookie.trim().split('=');
85
+ if (name && value) cookies[name] = decodeURIComponent(value);
86
+ });
87
+ return cookies;
88
+ }
89
+
90
+ let _currentRequest = null;
91
+
92
+ /**
93
+ * Set current request context (for auth)
94
+ * @param {object} req
95
+ */
96
+ export function setCurrentRequest(req) {
97
+ _currentRequest = req;
98
+ }
99
+
100
+ /**
101
+ * Get current user from session
102
+ * @returns {object|null}
103
+ */
104
+ export async function getUser() {
105
+ try {
106
+ const request = _currentRequest;
107
+ if (!request) return null;
108
+
109
+ const lucia = getLucia();
110
+ const cookies = parseCookies(request.headers?.cookie || '');
111
+ const sessionId = cookies[lucia.sessionCookieName];
112
+ if (!sessionId) return null;
113
+
114
+ const { user, session } = await lucia.validateSession(sessionId);
115
+ if (!user || !session) return null;
116
+
117
+ return user;
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Require user (throw if not authenticated)
125
+ * @returns {Promise<object>}
126
+ */
127
+ export async function requireUser() {
128
+ const user = await getUser();
129
+ if (!user) throw new Error('Unauthorized');
130
+ return user;
131
+ }
132
+
133
+ /**
134
+ * Create a new session
135
+ * @param {string} userId
136
+ * @returns {{session: object, sessionCookie: object}}
137
+ */
138
+ export async function createSession(userId) {
139
+ const lucia = getLucia();
140
+ // Note: cookies() requires Next.js polyfill in moonlanding; for SDK we return session data
141
+ const session = await lucia.createSession(userId, {});
142
+ const sessionCookie = lucia.createSessionCookie(session.id);
143
+ return { session, sessionCookie };
144
+ }
145
+
146
+ /**
147
+ * Invalidate current session
148
+ */
149
+ export async function invalidateSession() {
150
+ const lucia = getLucia();
151
+ // Simplified - caller handles cookie clearing
152
+ // In full app this uses @lucia-auth/adapter-sqlite's cookie management
153
+ }
154
+
155
+ /**
156
+ * Hash password with bcrypt
157
+ * @param {string} password
158
+ * @returns {Promise<string>}
159
+ */
160
+ export async function hashPassword(password) {
161
+ return bcrypt.hash(password, 12);
162
+ }
163
+
164
+ /**
165
+ * Verify password
166
+ * @param {string} password
167
+ * @param {string} hash
168
+ * @returns {Promise<boolean>}
169
+ */
170
+ export async function verifyPassword(password, hash) {
171
+ return bcrypt.compare(password, hash);
172
+ }
173
+
174
+ /**
175
+ * Get user by email
176
+ * @param {string} email
177
+ * @returns {object|null}
178
+ */
179
+ export function getUserByEmail(email) {
180
+ const db = getDatabase();
181
+ return db.prepare('SELECT * FROM users WHERE email = ?').get(email);
182
+ }
183
+
184
+ /**
185
+ * Create a new user
186
+ * @param {object} userData
187
+ * @returns {object}
188
+ */
189
+ export async function createUser(userData) {
190
+ const hashedPassword = await hashPassword(userData.password);
191
+ const { create } = await import('./query-engine-write.js');
192
+ return create('user', {
193
+ ...userData,
194
+ password: hashedPassword,
195
+ }, { id: 'system' });
196
+ }
197
+
198
+ /**
199
+ * Authenticate user with credentials
200
+ * @param {string} email
201
+ * @param {string} password
202
+ * @returns {object|null} User if valid, null otherwise
203
+ */
204
+ export async function authenticate(email, password) {
205
+ const user = getUserByEmail(email);
206
+ if (!user) return null;
207
+
208
+ const valid = await verifyPassword(password, user.password);
209
+ if (!valid) return null;
210
+
211
+ return user;
212
+ }