thatcher 1.0.41 → 1.0.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +27 -0
  2. package/package.json +2 -5
  3. package/src/app/api/audit/dashboard/route.js +2 -2
  4. package/src/app/api/audit/logs/route.js +2 -2
  5. package/src/app/api/audit/permissions/[id]/route.js +2 -2
  6. package/src/app/api/audit/permissions/route.js +3 -3
  7. package/src/app/api/audit/permissions/stats/route.js +2 -2
  8. package/src/app/api/audit/route.js +2 -3
  9. package/src/app/api/audit/stats/route.js +2 -2
  10. package/src/app/api/auth/google/callback/route.js +1 -1
  11. package/src/app/api/auth/google/route.js +1 -1
  12. package/src/app/api/auth/login/route.js +1 -1
  13. package/src/app/api/auth/logout/route.js +1 -1
  14. package/src/app/api/auth/me/route.js +1 -1
  15. package/src/app/api/auth/mwr-bridge/route.js +1 -1
  16. package/src/app/api/auth/password-reset/route.js +1 -1
  17. package/src/app/api/csrf-token/route.js +1 -1
  18. package/src/app/api/debug/[[...path]]/route.js +2 -2
  19. package/src/app/api/debug/config/route.js +1 -1
  20. package/src/app/api/debug/hooks/route.js +1 -1
  21. package/src/app/api/debug/plugins/route.js +1 -1
  22. package/src/app/api/debug/sqlite/route.js +1 -1
  23. package/src/app/api/debug/sync/route.js +1 -1
  24. package/src/app/api/debug/workflow/route.js +1 -1
  25. package/src/app/api/domains/[domain]/route.js +1 -2
  26. package/src/app/api/domains/route.js +1 -1
  27. package/src/app/api/email/allocate/batch/route.js +1 -1
  28. package/src/app/api/email/allocate/route.js +1 -1
  29. package/src/app/api/email/receive/route.js +1 -1
  30. package/src/app/api/email/send/route.js +1 -1
  31. package/src/app/api/email/unallocated/route.js +1 -1
  32. package/src/app/api/files/[id]/route.js +1 -1
  33. package/src/app/api/health/route.js +1 -2
  34. package/src/app/api/metrics/route.js +1 -2
  35. package/src/engine.js +3 -3
  36. package/src/engine.server.js +3 -3
  37. package/src/index.js +35 -10
  38. package/src/lib/action-factory.js +1 -1
  39. package/src/lib/api-helpers.js +1 -1
  40. package/src/lib/api.js +3 -4
  41. package/src/lib/audit-logger-enhanced.js +1 -1
  42. package/src/lib/auth-middleware.js +1 -1
  43. package/src/lib/auth-route-helpers.js +1 -1
  44. package/src/lib/{busybase-adapter.js → busybase/adapter.js} +3 -3
  45. package/src/lib/{busybase-audit-reads.js → busybase/audit-reads.js} +1 -1
  46. package/src/lib/{busybase-audit.js → busybase/audit.js} +2 -2
  47. package/src/lib/{busybase-lucia-adapter.js → busybase/lucia-adapter.js} +1 -1
  48. package/src/lib/{busybase-store.js → busybase/store.js} +3 -3
  49. package/src/lib/config-generator-engine.js +13 -1
  50. package/src/lib/crud-action-helpers.js +1 -1
  51. package/src/lib/crud-handlers.js +4 -4
  52. package/src/lib/email-sender.js +1 -1
  53. package/src/lib/errors/index.js +3 -0
  54. package/src/lib/{error-recovery.js → errors/recovery.js} +120 -4
  55. package/src/lib/{error-handler.js → errors/types.js} +1 -1
  56. package/src/lib/errors/wrap.js +225 -0
  57. package/src/lib/events-engine.js +1 -1
  58. package/src/lib/hot-reload/index.js +34 -0
  59. package/src/lib/index.js +5 -5
  60. package/src/lib/keyed-cache.js +112 -0
  61. package/src/lib/monitor.js +285 -0
  62. package/src/lib/next-shim.js +294 -0
  63. package/src/lib/observability-init.js +213 -0
  64. package/src/lib/perf.js +468 -0
  65. package/src/lib/query-cache.js +40 -46
  66. package/src/lib/realtime-server.js +18 -0
  67. package/src/lib/render-cache.js +14 -28
  68. package/src/lib/{request-tracing.js → request-trace.js} +57 -2
  69. package/src/lib/response-formatter.js +18 -0
  70. package/src/lib/route-helpers.js +1 -1
  71. package/src/lib/state-protocol.js +13 -0
  72. package/src/lib/state-transport-client.js +6 -0
  73. package/src/lib/state-transport-reconnect.js +5 -0
  74. package/src/lib/state-transport-server.js +8 -0
  75. package/src/lib/utils.js +1 -1
  76. package/src/lib/{validate.js → validation/entity-validators.js} +9 -8
  77. package/src/lib/validation/index.js +1 -1
  78. package/src/lib/validation/security-validators.js +1 -1
  79. package/src/lib/validation-middleware.js +2 -2
  80. package/src/lib/workflow-engine.js +23 -3
  81. package/src/lib/xstate-workflow-engine.js +18 -2
  82. package/src/services/collaborator-role.service.js +2 -2
  83. package/src/services/notification-engine.js +5 -5
  84. package/src/services/permission.service.js +1 -1
  85. package/src/ui/format-helpers.js +1 -4
  86. package/src/ui/highlight-threading-renderer.js +1 -1
  87. package/src/ui/page-handler-admin.js +6 -5
  88. package/src/ui/page-handler-helpers.js +2 -2
  89. package/src/ui/page-handler-reviews.js +4 -4
  90. package/src/ui/page-handler-rfi.js +1 -1
  91. package/src/ui/page-handler.js +2 -2
  92. package/src/ui/render-helpers.js +1 -1
  93. package/src/ui/renderer.js +1 -1
  94. package/src/ui/{review-comparison-renderer.js → review/comparison.js} +1 -1
  95. package/src/ui/{review-detail-renderer.js → review/detail-renderer.js} +3 -3
  96. package/src/ui/review/index.js +32 -0
  97. package/src/ui/{review-mwr-renderer.js → review/mwr.js} +1 -1
  98. package/src/ui/{review-renderer.js → review/renderer.js} +1 -1
  99. package/src/ui/{settings-renderer.js → settings/home.js} +7 -34
  100. package/src/ui/{settings-renderer-advanced.js → settings/review.js} +117 -56
  101. package/src/ui/settings/shared.js +45 -0
  102. package/src/ui/{settings-renderer-teams.js → settings/teams.js} +1 -2
  103. package/src/ui/settings/templates.js +116 -0
  104. package/src/lib/api-error-wrapper.js +0 -125
  105. package/src/lib/db-monitor.js +0 -127
  106. package/src/lib/error-resilience.js +0 -132
  107. package/src/lib/monitoring-init.js +0 -67
  108. package/src/lib/next-compat.js +0 -94
  109. package/src/lib/next-polyfills.js +0 -135
  110. package/src/lib/observability-bootstrap.js +0 -116
  111. package/src/lib/perf-monitor.js +0 -94
  112. package/src/lib/perf-profiler.js +0 -233
  113. package/src/lib/query-perf.js +0 -117
  114. package/src/lib/request-tracker.js +0 -43
  115. package/src/lib/resource-monitor.js +0 -120
  116. package/src/lib/validators.js +0 -67
  117. package/src/lib/with-error-handler.js +0 -31
  118. package/src/ui/settings-renderer-advanced2.js +0 -159
  119. /package/src/ui/{review-detail-panels.js → review/detail-panels.js} +0 -0
  120. /package/src/ui/{review-detail-script.js → review/detail-script.js} +0 -0
  121. /package/src/ui/{review-widgets.js → review/widgets.js} +0 -0
  122. /package/src/ui/{review-zone-nav.js → review/zone-nav.js} +0 -0
@@ -1,10 +1,4 @@
1
- const stmtCache = new Map();
2
- const resultCache = new Map();
3
- const queryCache = new Map();
4
- let cacheHits = 0;
5
- let cacheMisses = 0;
6
- let stmtCacheHits = 0;
7
- let stmtCacheMisses = 0;
1
+ import { createCache } from './keyed-cache.js';
8
2
 
9
3
  const MAX_STMT_CACHE = 500;
10
4
  const MAX_RESULT_CACHE = 200;
@@ -23,19 +17,31 @@ const TTL_CONFIG = {
23
17
  default: DEFAULT_TTL
24
18
  };
25
19
 
20
+ // Prepared statements never expire by time (only size-evicted), so ttlMs is
21
+ // left unset (no expiry) -- matches the original stmtCache having no TTL check.
22
+ const stmtCache = createCache({ maxSize: MAX_STMT_CACHE });
23
+ // The query cache (cacheQuery) also never expired by time in the original.
24
+ const queryCache = createCache({ maxSize: MAX_QUERY_CACHE });
25
+ // The result cache's TTL varies PER ENTRY (by the entity it was cached under),
26
+ // so a single fixed ttlMs on the shared primitive cannot express it directly.
27
+ // Kept as ONE cache instance (matching the original single-Map, single
28
+ // MAX_RESULT_CACHE=200-total ceiling -- a per-TTL-bucket split would silently
29
+ // multiply the effective capacity, a real behaviour change) with no TTL on the
30
+ // primitive itself; each entry's own per-entity TTL is checked here at read
31
+ // time against a stored timestamp, exactly like the original inline check did.
32
+ // Hit/miss counting is also done at this layer (not the primitive's own
33
+ // built-in counters) since a ttl=0/expired lookup must count as a MISS to
34
+ // match the original's exact cacheHits/cacheMisses semantics.
35
+ const resultCache = createCache({ maxSize: MAX_RESULT_CACHE });
36
+ let cacheHits = 0;
37
+ let cacheMisses = 0;
38
+
26
39
  export const prepareStmt = (db, sql) => {
27
40
  const cached = stmtCache.get(sql);
28
- if (cached) {
29
- stmtCacheHits++;
41
+ if (cached !== undefined) {
30
42
  return cached;
31
43
  }
32
44
 
33
- stmtCacheMisses++;
34
- if (stmtCache.size >= MAX_STMT_CACHE) {
35
- const firstKey = stmtCache.keys().next().value;
36
- stmtCache.delete(firstKey);
37
- }
38
-
39
45
  const stmt = db.prepare(sql);
40
46
  stmtCache.set(sql, stmt);
41
47
  return stmt;
@@ -43,7 +49,7 @@ export const prepareStmt = (db, sql) => {
43
49
 
44
50
  export const getCached = (key, entity = 'default') => {
45
51
  const entry = resultCache.get(key);
46
- if (!entry) {
52
+ if (entry === undefined) {
47
53
  cacheMisses++;
48
54
  return null;
49
55
  }
@@ -68,11 +74,6 @@ export const setCached = (key, data, entity = 'default') => {
68
74
  const ttl = TTL_CONFIG[entity] || TTL_CONFIG.default;
69
75
  if (ttl === 0) return;
70
76
 
71
- if (resultCache.size >= MAX_RESULT_CACHE) {
72
- const firstKey = resultCache.keys().next().value;
73
- resultCache.delete(firstKey);
74
- }
75
-
76
77
  resultCache.set(key, { data, timestamp: Date.now() });
77
78
  };
78
79
 
@@ -84,43 +85,36 @@ export const invalidate = (pattern) => {
84
85
  }
85
86
 
86
87
  const regex = new RegExp(pattern);
87
- for (const key of resultCache.keys()) {
88
- if (regex.test(key)) resultCache.delete(key);
89
- }
90
- for (const key of queryCache.keys()) {
91
- if (regex.test(key)) queryCache.delete(key);
92
- }
88
+ resultCache.deleteWhere(key => regex.test(key));
89
+ queryCache.deleteWhere(key => regex.test(key));
93
90
  };
94
91
 
95
92
  export const cacheQuery = (key, fn) => {
96
93
  const cached = queryCache.get(key);
97
- if (cached) return cached;
98
-
99
- if (queryCache.size >= MAX_QUERY_CACHE) {
100
- const firstKey = queryCache.keys().next().value;
101
- queryCache.delete(firstKey);
102
- }
94
+ if (cached !== undefined) return cached;
103
95
 
104
96
  const result = fn();
105
97
  queryCache.set(key, result);
106
98
  return result;
107
99
  };
108
100
 
109
- export const getStats = () => ({
110
- cacheHits,
111
- cacheMisses,
112
- hitRate: cacheHits + cacheMisses > 0 ? (cacheHits / (cacheHits + cacheMisses) * 100).toFixed(2) + '%' : '0%',
113
- stmtCacheHits,
114
- stmtCacheMisses,
115
- stmtHitRate: stmtCacheHits + stmtCacheMisses > 0 ? (stmtCacheHits / (stmtCacheHits + stmtCacheMisses) * 100).toFixed(2) + '%' : '0%',
116
- stmtCacheSize: stmtCache.size,
117
- resultCacheSize: resultCache.size,
118
- queryCacheSize: queryCache.size
119
- });
101
+ export const getStats = () => {
102
+ const stmtStats = stmtCache.stats();
103
+ return {
104
+ cacheHits,
105
+ cacheMisses,
106
+ hitRate: cacheHits + cacheMisses > 0 ? (cacheHits / (cacheHits + cacheMisses) * 100).toFixed(2) + '%' : '0%',
107
+ stmtCacheHits: stmtStats.hits,
108
+ stmtCacheMisses: stmtStats.misses,
109
+ stmtHitRate: stmtStats.hits + stmtStats.misses > 0 ? (stmtStats.hits / (stmtStats.hits + stmtStats.misses) * 100).toFixed(2) + '%' : '0%',
110
+ stmtCacheSize: stmtStats.size,
111
+ resultCacheSize: resultCache.stats().size,
112
+ queryCacheSize: queryCache.stats().size
113
+ };
114
+ };
120
115
 
121
116
  export const clearStats = () => {
122
117
  cacheHits = 0;
123
118
  cacheMisses = 0;
124
- stmtCacheHits = 0;
125
- stmtCacheMisses = 0;
119
+ stmtCache.resetStats();
126
120
  };
@@ -1,3 +1,21 @@
1
+ // ws-broadcast channel: in-process pub/sub used by the CRUD layer to fan out
2
+ // entity create/update/delete notifications (see src/lib/api.js, which calls
3
+ // broadcastUpdate() on every write, and src/lib/index.js which re-exports this
4
+ // module). Despite the "(polling mode)" log line there is no actual transport
5
+ // here -- no WebSocket, no poll endpoint -- just an in-memory Map of
6
+ // channel -> Set<callback>, so subscribers must live in the same process.
7
+ //
8
+ // This is a DIFFERENT channel from the state-transport-*.js / state-protocol.js
9
+ // quartet (state-transport-server.js / state-transport-client.js /
10
+ // state-transport-reconnect.js): that stack is a real `ws`-backed WebSocket
11
+ // server+client with a structured message protocol, vector clocks, and
12
+ // reconnect/polling-fallback semantics, but as of this writing it has ZERO
13
+ // importers anywhere in the repo outside its own internal cross-imports --
14
+ // nothing constructs a StateTransportServer/StateTransportClient. So today
15
+ // state-transport-* does NOT supersede this file; this file (realtime-server.js)
16
+ // is the one actually wired into production (src/lib/api.js), while
17
+ // state-transport-* is a more capable but currently-unwired structured protocol.
18
+ // Do not delete either without re-checking real importers first.
1
19
  import { createLogger } from './logger.js';
2
20
 
3
21
  const log = createLogger('[Realtime]');
@@ -1,8 +1,8 @@
1
- const cache = new Map()
1
+ import { createCache } from './keyed-cache.js'
2
+
2
3
  const maxSize = 500
3
4
  const ttl = 60000
4
- let hits = 0
5
- let misses = 0
5
+ const cache = createCache({ ttlMs: ttl, maxSize })
6
6
 
7
7
  function cacheKey(fn, args) {
8
8
  return `${fn.name}:${JSON.stringify(args)}`
@@ -12,17 +12,11 @@ export function memoize(fn) {
12
12
  return function(...args) {
13
13
  const key = cacheKey(fn, args)
14
14
  const cached = cache.get(key)
15
- if (cached && Date.now() - cached.ts < ttl) {
16
- hits++
17
- return cached.value
15
+ if (cached !== undefined) {
16
+ return cached
18
17
  }
19
- misses++
20
18
  const value = fn(...args)
21
- cache.set(key, { value, ts: Date.now() })
22
- if (cache.size > maxSize) {
23
- const first = cache.keys().next().value
24
- cache.delete(first)
25
- }
19
+ cache.set(key, value)
26
20
  return value
27
21
  }
28
22
  }
@@ -30,25 +24,18 @@ export function memoize(fn) {
30
24
  export function invalidate(pattern) {
31
25
  if (!pattern) {
32
26
  cache.clear()
33
- return cache.size
34
- }
35
- let count = 0
36
- for (const key of cache.keys()) {
37
- if (key.includes(pattern)) {
38
- cache.delete(key)
39
- count++
40
- }
27
+ return 0
41
28
  }
42
- return count
29
+ return cache.deleteWhere(key => key.includes(pattern))
43
30
  }
44
31
 
45
32
  export function getCacheStats() {
46
- const total = hits + misses
33
+ const s = cache.stats()
47
34
  return {
48
- size: cache.size,
49
- hits,
50
- misses,
51
- hitRate: total > 0 ? (hits / total * 100).toFixed(2) + '%' : '0%',
35
+ size: s.size,
36
+ hits: s.hits,
37
+ misses: s.misses,
38
+ hitRate: (s.hits + s.misses) > 0 ? (s.hitRate * 100).toFixed(2) + '%' : '0%',
52
39
  maxSize,
53
40
  ttl
54
41
  }
@@ -56,6 +43,5 @@ export function getCacheStats() {
56
43
 
57
44
  export function clearCache() {
58
45
  cache.clear()
59
- hits = 0
60
- misses = 0
46
+ cache.resetStats()
61
47
  }
@@ -1,9 +1,19 @@
1
+ // Merged from request-tracing.js (span/OTel-shaped tracing over the shared
2
+ // tracer + perfProfiler) and request-tracker.js (simple duration/status
3
+ // request+error recording into metrics-collector.js's recordRequest/
4
+ // recordError channel). Two request-scoped tracking layers, same concern,
5
+ // kept side by side with their original exported names unchanged.
6
+
1
7
  import { tracer } from './tracing.js';
2
- import { perfProfiler } from './perf-profiler.js';
8
+ import { perfProfiler } from './perf.js';
3
9
  import { createLogger } from './logger.js';
10
+ import { recordRequest, recordError } from './metrics-collector.js';
4
11
 
5
12
  const logger = createLogger('[RequestTracing]');
6
13
 
14
+ // ---------------------------------------------------------------------------
15
+ // From request-tracing.js
16
+ // ---------------------------------------------------------------------------
7
17
  export function withTracing(handler) {
8
18
  return async (req, ...rest) => {
9
19
  const method = req.method || 'UNKNOWN';
@@ -208,4 +218,49 @@ export function createTracingMiddleware() {
208
218
  };
209
219
  }
210
220
 
211
- export default withTracing;
221
+ export default withTracing;
222
+
223
+ // ---------------------------------------------------------------------------
224
+ // From request-tracker.js
225
+ // ---------------------------------------------------------------------------
226
+ function wrapHandler(handler, metadata = {}) {
227
+ return async (request, context) => {
228
+ const start = process.hrtime.bigint();
229
+ const method = request.method;
230
+ const path = new URL(request.url).pathname;
231
+
232
+ try {
233
+ const response = await handler(request, context);
234
+ const duration = Number(process.hrtime.bigint() - start) / 1000000;
235
+ const status = response.status || 200;
236
+
237
+ recordRequest(path, method, duration, status);
238
+
239
+ return response;
240
+ } catch (err) {
241
+ const duration = Number(process.hrtime.bigint() - start) / 1000000;
242
+
243
+ recordRequest(path, method, duration, 500);
244
+ recordError(path, method, err.message, err.stack);
245
+
246
+ throw err;
247
+ }
248
+ };
249
+ }
250
+
251
+ function trackRequest(path, method, duration, status) {
252
+ recordRequest(path, method, duration, status);
253
+ }
254
+
255
+ function trackError(path, method, error, stack) {
256
+ recordError(path, method, error, stack);
257
+ }
258
+
259
+ export { wrapHandler, trackRequest, trackError };
260
+
261
+ if (typeof globalThis !== 'undefined') {
262
+ globalThis.__requestTracker = {
263
+ trackRequest,
264
+ trackError
265
+ };
266
+ }
@@ -35,6 +35,24 @@ export function error(message, status = HTTP.INTERNAL_ERROR, code = 'ERROR') {
35
35
  });
36
36
  }
37
37
 
38
+ // Accepts a normalized AppError-shaped object (message/code/status/details)
39
+ // and renders it the same way error() renders a plain message/status/code
40
+ // triple. Pre-existing callers (withErrorHandler in lib/errors/wrap.js)
41
+ // imported this name with no such export ever having existed here; adding
42
+ // it now is the real fix for what was previously a dead, never-taken error
43
+ // path (nothing ever exercised it directly, so the gap went unnoticed).
44
+ export function apiError(err) {
45
+ const status = err?.status || err?.statusCode || HTTP.INTERNAL_ERROR;
46
+ const code = err?.code || 'ERROR';
47
+ const message = err?.message || 'An unexpected error occurred';
48
+ const body = { error: message, code };
49
+ if (err?.details) body.details = err.details;
50
+ return new Response(JSON.stringify(body), {
51
+ status,
52
+ headers: { 'Content-Type': 'application/json' },
53
+ });
54
+ }
55
+
38
56
  export function withMetadata(data, status, type = 'success') {
39
57
  return { ...data, _meta: { status, type, timestamp: Date.now() } };
40
58
  }
@@ -3,7 +3,7 @@ import { getSpec, getNavItems } from '@/config/spec-helpers';
3
3
  import { get, getChildren } from '@/engine';
4
4
  import { can } from '@/services/permission.service';
5
5
  import { withPageAuth } from '@/lib/auth-middleware';
6
- import { notFound } from '@/lib/next-polyfills';
6
+ import { notFound } from '@/lib/next-shim';
7
7
  import { loadFormOptions } from '@/lib/utils';
8
8
 
9
9
  export { withPageAuth as requireEntityAccess };
@@ -1,3 +1,16 @@
1
+ // state-sync channel: message-type vocabulary + VectorClock + schema validation
2
+ // shared by state-transport-server.js / state-transport-client.js /
3
+ // state-transport-reconnect.js. This is a structured, reconnect-aware
4
+ // WebSocket protocol (real `ws` transport, IP/rate-limited via
5
+ // connection-guard.js, exponential-backoff client reconnect with a polling
6
+ // fallback) -- distinct from the simpler in-process pub/sub in
7
+ // realtime-server.js (the "ws-broadcast" channel actually wired into the CRUD
8
+ // write path via src/lib/api.js). As of this writing, grepping the whole repo
9
+ // found ZERO importers of this quartet outside their own internal cross-imports
10
+ // (state-transport-client.js imports state-transport-reconnect.js; both import
11
+ // this file) -- nothing external constructs a StateTransportServer/Client, so
12
+ // this stack is currently unwired/dormant rather than superseding
13
+ // realtime-server.js. Keep both; re-check importers before deleting either.
1
14
  const MESSAGE_TYPES = {
2
15
  STATE_UPDATE: 'state_update',
3
16
  STATE_SNAPSHOT: 'state_snapshot',
@@ -1,3 +1,9 @@
1
+ // state-sync channel (client side): browser WebSocket client with
2
+ // exponential-backoff reconnect + polling fallback, part of the
3
+ // state-protocol.js quartet. See the header comment in state-protocol.js --
4
+ // this is a structured, reconnect-aware protocol distinct from
5
+ // realtime-server.js's simple in-process pub/sub. Zero real importers found
6
+ // repo-wide as of this writing; currently dormant/unwired.
1
7
  import Protocol from './state-protocol.js'
2
8
  import { CONFIG, ReconnectManager } from './state-transport-reconnect.js'
3
9
  import { createLogger } from './logger.js'
@@ -1,3 +1,8 @@
1
+ // state-sync channel (reconnect logic): exponential-backoff + polling-fallback
2
+ // manager consumed by state-transport-client.js, part of the state-protocol.js
3
+ // quartet. See the header comment in state-protocol.js for the full
4
+ // ws-broadcast vs state-sync distinction. Zero real importers repo-wide
5
+ // outside this quartet's own internal cross-imports as of this writing.
1
6
  import Protocol from './state-protocol.js'
2
7
 
3
8
  const CONFIG = {
@@ -1,3 +1,11 @@
1
+ // state-sync channel (server side): real `ws`-backed WebSocketServer with
2
+ // connection-guard IP/rate limiting, part of the state-protocol.js quartet.
3
+ // See the header comment in state-protocol.js for the full picture -- this is
4
+ // a structured, reconnect-aware state-sync protocol, DIFFERENT from and not a
5
+ // replacement for realtime-server.js's simple in-process pub/sub
6
+ // (the "ws-broadcast" channel actually used by the CRUD write path). As of
7
+ // this writing this file has zero real importers repo-wide; nothing
8
+ // constructs a StateTransportServer today.
1
9
  import { WebSocketServer } from 'ws'
2
10
  import { EventEmitter } from 'events'
3
11
  import Protocol from '@/lib/state-protocol.js'
package/src/lib/utils.js CHANGED
@@ -1,4 +1,4 @@
1
- import { list } from './busybase-store.js';
1
+ import { list } from './busybase/store.js';
2
2
  import { getSpec } from '../config/spec-helpers.js';
3
3
 
4
4
  // Ported from moonlanding. engagement refs get a richer label and skip archived rows; failures degrade to [].
@@ -1,16 +1,17 @@
1
1
  // Adapted from moonlanding/src/lib/validate.js
2
+ // Entity/spec-driven field, record, and status-transition validation.
2
3
 
3
- import { getSpec } from '../config/spec-helpers.js';
4
- import { isValidEmail as checkEmailFormat, isValidEmail } from './validators.js';
5
- import { getValidTransitions } from './status-helpers.js';
6
- import { isBeforeDate } from './date-utils.js';
4
+ import { getSpec } from '@/config/spec-helpers';
5
+ import { isValidEmail } from '@/lib/validation/format-validators';
6
+ import { getValidTransitions } from '@/lib/status-helpers';
7
+ import { isBeforeDate } from '@/lib/date-utils';
7
8
 
8
- // Re-export so consumers can reach the email validator from validate.js (parity with moon)
9
+ // Re-export so consumers can reach the email validator from entity-validators.js (parity with moon)
9
10
  export { isValidEmail };
10
11
 
11
12
  const HTML_ESC = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
12
13
 
13
- function sanitizeHtml(str) {
14
+ export function sanitizeHtml(str) {
14
15
  return typeof str === 'string' ? str.replace(/[&<>"']/g, c => HTML_ESC[c]) : str;
15
16
  }
16
17
 
@@ -50,7 +51,7 @@ export async function validateField(fieldDef, value, options = {}) {
50
51
  return { valid: true };
51
52
  }
52
53
  try {
53
- const { getBy } = await import('./busybase-store.js');
54
+ const { getBy } = await import('@/lib/busybase/store');
54
55
  const refTable = fieldDef.ref === 'user' ? 'users' : fieldDef.ref;
55
56
  if (!(await getBy(refTable, 'id', value))) {
56
57
  return {
@@ -141,7 +142,7 @@ async function checkUnique(fieldDef, value, { fieldName, entityName, existingRec
141
142
  const existingValue = existingRecord?.[fieldName];
142
143
  if (value === existingValue) return null;
143
144
  try {
144
- const { getBy } = await import('./busybase-store.js');
145
+ const { getBy } = await import('@/lib/busybase/store');
145
146
  const table = entityName === 'user' ? 'users' : entityName;
146
147
  const dup = await getBy(table, fieldName, value);
147
148
  if (dup && (!existingRecord || dup.id !== existingRecord.id)) {
@@ -16,4 +16,4 @@ export {
16
16
  validateDeadline,
17
17
  sanitizeData,
18
18
  sanitizeHtml
19
- } from '@/lib/validate';
19
+ } from '@/lib/validation/entity-validators';
@@ -1,4 +1,4 @@
1
- import { sanitizeHtml } from '@/lib/validate';
1
+ import { sanitizeHtml } from '@/lib/validation/entity-validators';
2
2
 
3
3
  const XSS_PATTERNS = [
4
4
  /<script[^>]*>.*?<\/script>/gi,
@@ -1,4 +1,4 @@
1
- import { NextResponse } from '@/lib/next-polyfills';
1
+ import { NextResponse } from '@/lib/next-shim';
2
2
  import {
3
3
  detectXSS,
4
4
  detectSQLInjection,
@@ -7,7 +7,7 @@ import {
7
7
  rateLimitCheck,
8
8
  validateCSRFToken
9
9
  } from '@/lib/validation';
10
- import { validateEntity, validateUpdate, hasErrors } from '@/lib/validate';
10
+ import { validateEntity, validateUpdate, hasErrors } from '@/lib/validation/entity-validators';
11
11
 
12
12
  export async function validateRequest(request, options = {}) {
13
13
  const { requireCSRF = true, rateLimit = { max: 100, window: 60000 }, entityName = null } = options;
@@ -1,7 +1,27 @@
1
- import { get, update, create } from './busybase-store.js';
1
+ // Plain-function entity lifecycle/workflow engine: reads a `config.workflows[name]`
2
+ // definition (stages with forward/backward/requires_role/entry/readonly/order/
3
+ // locks/actions) and validates+applies transitions against a busybase-backed
4
+ // entity record. THIS IS THE LIVE PATH: its `validateTransition` is called from
5
+ // src/lib/events-engine.js to gate an `engagement_lifecycle` stage change, a real
6
+ // production call site. It is also re-exported from src/lib/index.js (the top-level
7
+ // Thatcher class delegates `thatcher.transition()`/`thatcher.getAvailableTransitions()`
8
+ // here -- see src/index.js).
9
+ //
10
+ // xstate-workflow-engine.js implements the SAME job (same config shape, same
11
+ // validateTransition/getAvailableTransitions/transition/getStateField/
12
+ // getStageLabels/getStateLocks/getStateActions exports, nearly line-for-line
13
+ // duplicated validation branches) PLUS a real xstate machine + live per-entity
14
+ // actors on top. Despite the more capable implementation, as of this writing its
15
+ // only importers repo-wide are a debug route (src/app/api/debug/[[...path]]/route.js,
16
+ // which hard-disables itself when NODE_ENV=production) and a root-level manual
17
+ // test.js script -- neither is a production call site. So xstate-workflow-engine.js
18
+ // does NOT currently supersede this file; if anything the reverse holds (this file
19
+ // is live, that one is dormant/debug-only). Re-check import sites before treating
20
+ // either as the "old" one.
21
+ import { get, update, create } from './busybase/store.js';
2
22
  import { getConfigEngineSync } from './config-generator-engine.js';
3
23
  import { executeHook } from './hook-engine.js';
4
- import { AppError } from './error-handler.js';
24
+ import { AppError } from './errors/index.js';
5
25
  import { HTTP } from '../config/constants.js';
6
26
  import { createLogger } from './logger.js';
7
27
 
@@ -137,7 +157,7 @@ export async function transition(entityType, entityId, workflowName, toState, us
137
157
  }
138
158
 
139
159
  // Dynamically import write engine to avoid circular dependency
140
- const { update: updateRecord } = await import('./busybase-store.js');
160
+ const { update: updateRecord } = await import('./busybase/store.js');
141
161
  const updated = await updateRecord(entityType, entityId, updates, user);
142
162
 
143
163
  executeHook(`transition:${entityType}`, {
@@ -1,7 +1,23 @@
1
+ // xstate-backed workflow engine: reads the SAME `config.workflows[name]`
2
+ // definition as workflow-engine.js and duplicates its plain-function exports
3
+ // (validateTransition/getAvailableTransitions/transition/getStateField/
4
+ // getStageLabels/getStateLocks/getStateActions, nearly line-for-line the same
5
+ // validation logic) but additionally compiles a real xstate machine
6
+ // (compileWorkflow) and manages live per-entity actors (createActorForEntity/
7
+ // sendEvent/getSnapshot/getActiveActors).
8
+ //
9
+ // NOT currently the live path. As of this writing its only importers repo-wide
10
+ // are src/app/api/debug/[[...path]]/route.js (getXStateWorkflowEngine -- and that
11
+ // whole route hard-disables itself when NODE_ENV=production) and a root-level
12
+ // manual `test.js` script outside src/ -- neither is a production call site.
13
+ // workflow-engine.js is the one actually wired into production (called from
14
+ // src/lib/events-engine.js and re-exported by src/index.js's Thatcher class).
15
+ // This file does NOT supersede workflow-engine.js; treat it as a dormant/
16
+ // debug-only alternate implementation until a real call site adopts it.
1
17
  import { createMachine, createActor, assign, fromPromise, sendParent } from 'xstate';
2
18
  import { getConfigEngineSync } from './config-generator-engine.js';
3
19
  import { hookEngine } from './hook-engine.js';
4
- import { AppError } from './error-handler.js';
20
+ import { AppError } from './errors/index.js';
5
21
  import { HTTP } from '../config/constants.js';
6
22
  import { createLogger } from './logger.js';
7
23
 
@@ -385,7 +401,7 @@ export function getAvailableTransitions(workflowName, currentState, user, record
385
401
  }
386
402
 
387
403
  export async function transition(entityType, entityId, workflowName, toState, user, reason = '') {
388
- const { get, update: updateRecord } = await import('./busybase-store.js');
404
+ const { get, update: updateRecord } = await import('./busybase/store.js');
389
405
 
390
406
  const record = await get(entityType, entityId);
391
407
  if (!record) throw new AppError('Record not found', 'NOT_FOUND', HTTP.NOT_FOUND);
@@ -1,7 +1,7 @@
1
1
  // Extracted from moonlanding/src/services/collaborator-role.service.js
2
2
 
3
- import { list, get, update, create } from '../lib/busybase-store.js';
4
- import { AppError } from '../lib/error-handler.js';
3
+ import { list, get, update, create } from '../lib/busybase/store.js';
4
+ import { AppError } from '../lib/errors/index.js';
5
5
  import { HTTP } from '../config/constants.js';
6
6
 
7
7
  const COLLABORATOR_ROLE_PERMISSIONS = {
@@ -11,7 +11,7 @@ export function getNotificationTemplate(name) {
11
11
  }
12
12
 
13
13
  export async function createNotification(notification) {
14
- const { create } = await import('../lib/busybase-store.js');
14
+ const { create } = await import('../lib/busybase/store.js');
15
15
  return create('notification', {
16
16
  ...notification,
17
17
  created_at: Math.floor(Date.now() / 1000),
@@ -66,14 +66,14 @@ export async function sendNotification(type, userId, context = {}, options = {})
66
66
  }
67
67
 
68
68
  export async function markNotificationRead(notificationId, userId) {
69
- const { update } = await import('../lib/busybase-store.js');
69
+ const { update } = await import('../lib/busybase/store.js');
70
70
  return update('notification', notificationId, {
71
71
  read_at: Math.floor(Date.now() / 1000),
72
72
  });
73
73
  }
74
74
 
75
75
  export async function getUnreadCount(userId) {
76
- const { list } = await import('../lib/busybase-store.js');
76
+ const { list } = await import('../lib/busybase/store.js');
77
77
  return (await list('notification', {
78
78
  user_id: userId,
79
79
  read_at: null,
@@ -83,13 +83,13 @@ export async function getUnreadCount(userId) {
83
83
  // Resolve a `recipients` token (a role/group name used across events-engine.js
84
84
  // call sites, e.g. 'client_users', 'team_members', 'collaborator') to a list of
85
85
  // user records to notify. There is no dedicated recipient-resolution engine in
86
- // this codebase yet, so this composes the existing busybase-store queries the
86
+ // this codebase yet, so this composes the existing busybase/store queries the
87
87
  // same way the rest of the app looks up users -- a single explicit user object
88
88
  // on the context (context.recipientUser / a matching *_id field) is honored
89
89
  // directly, and a bare role/group token that cannot be resolved to concrete
90
90
  // users is logged and skipped rather than silently guessed at.
91
91
  async function resolveRecipients(recipients, context) {
92
- const { list, get } = await import('../lib/busybase-store.js');
92
+ const { list, get } = await import('../lib/busybase/store.js');
93
93
 
94
94
  if (context.recipientUser) return [context.recipientUser];
95
95
  if (context.collaborator?.email) return [context.collaborator];
@@ -1,7 +1,7 @@
1
1
  // Adapted from moonlanding/src/services/permission.service.js
2
2
 
3
3
  import { getCollaboratorRole, checkCollaboratorAccess } from '../services/collaborator-role.service.js';
4
- import { PermissionError } from '../lib/error-handler.js';
4
+ import { PermissionError } from '../lib/errors/index.js';
5
5
  import { getConfigEngineSync } from '../lib/config-generator-engine.js';
6
6
 
7
7
  class PermissionService {
@@ -1,4 +1,5 @@
1
1
  import { STAGE_COLORS, STATUS_COLORS } from './render-helpers.js';
2
+ export { esc } from './render-helpers.js';
2
3
 
3
4
  export function formatDate(ts, opts) {
4
5
  if (!ts) return '-';
@@ -129,7 +130,3 @@ export function emptyState(message, iconName) {
129
130
  <div style="font-size:0.88rem;font-weight:500">${message || 'No items found'}</div>
130
131
  </div>`;
131
132
  }
132
-
133
- export function esc(s) {
134
- return String(s ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
135
- }
@@ -1,6 +1,6 @@
1
1
  import { page } from '@/ui/layout.js';
2
2
  import { canEdit } from '@/ui/permissions-ui.js';
3
- import { reviewZoneNav } from '@/ui/review-zone-nav.js';
3
+ import { reviewZoneNav } from '@/ui/review/zone-nav.js';
4
4
  import { esc } from '@/ui/render-helpers.js';
5
5
  import { SPACING, renderStatsRow, renderPageHeader, renderButton, renderEmptyState } from '@/ui/spacing-system.js';
6
6