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
@@ -7,7 +7,7 @@
7
7
  * receives back; busybase stores it as an epoch-ms number, so we convert at the boundary.
8
8
  */
9
9
 
10
- import { list, get, create, update, remove } from '@/lib/busybase-store.js';
10
+ import { list, get, create, update, remove } from '@/lib/busybase/store.js';
11
11
 
12
12
  const SESSION = 'sessions';
13
13
  const USER = 'users';
@@ -14,9 +14,9 @@
14
14
  * Requires Bun (busybase embedded uses Bun.password + the vectordb native binding).
15
15
  */
16
16
 
17
- import { getSpec } from '../config/spec-helpers.js';
18
- import { RECORD_STATUS } from '../config/constants.js';
19
- import { genId, now } from './id-helpers.js';
17
+ import { getSpec } from '../../config/spec-helpers.js';
18
+ import { RECORD_STATUS } from '../../config/constants.js';
19
+ import { genId, now } from '../id-helpers.js';
20
20
 
21
21
  let _client = null;
22
22
 
@@ -1,5 +1,17 @@
1
1
  // Adapted from moonlanding/src/lib/config-generator-engine.js
2
-
2
+ //
3
+ // Config/spec generation layer -- a genuinely SEPARATE concern from
4
+ // workflow-engine.js / xstate-workflow-engine.js. This file loads+validates
5
+ // the master YAML config and generates cached, deep-frozen derived specs
6
+ // (entity field specs via generateEntitySpec, roles, permission templates,
7
+ // status enums, theme, thresholds, automation jobs). It does not itself
8
+ // validate or execute a workflow transition -- both workflow engines simply
9
+ // READ the `workflows` config section through this engine's
10
+ // getConfigEngineSync().getConfig(). By far the most widely consumed of the
11
+ // three files (16 importers repo-wide across services/lib/ui/app-api routes
12
+ // as of this writing, vs. workflow-engine.js's 2 and
13
+ // xstate-workflow-engine.js's 2 debug/test-only importers) -- this is live,
14
+ // central infrastructure, not a candidate for deprecation.
3
15
  import { load as yamlLoad } from 'js-yaml';
4
16
  import { LRUCache, deepFreeze, deepClone, recursiveResolve } from './config-helpers.js';
5
17
  import { generateFieldsFromOverrides, ensureFieldLabels } from './config-field-helpers.js';
@@ -1,4 +1,4 @@
1
- import { AppError } from '@/lib/error-handler';
1
+ import { AppError } from '@/lib/errors';
2
2
  import { HTTP } from '@/config/constants';
3
3
  import { now } from '@/lib/id-helpers';
4
4
 
@@ -1,18 +1,18 @@
1
1
  import { createLogger } from './logger.js';
2
- import { get, listWithPagination, searchWithPagination, create, update, remove } from './busybase-store.js';
2
+ import { get, listWithPagination, searchWithPagination, create, update, remove } from './busybase/store.js';
3
3
 
4
4
  const log = createLogger('[CRUD]');
5
- import { validateEntity, validateUpdate, sanitizeData } from './validate.js';
5
+ import { validateEntity, validateUpdate, sanitizeData } from './validation/index.js';
6
6
  import { requirePermission, getSessionToken } from './auth-middleware.js';
7
7
  import { executeHook } from './hook-engine.js';
8
- import { AppError, NotFoundError, ValidationError } from './error-handler.js';
8
+ import { AppError, NotFoundError, ValidationError } from './errors/index.js';
9
9
  import { ok, created, paginated, noContent, error } from './response-formatter.js';
10
10
  import { HTTP } from '../config/constants.js';
11
11
  import { permissionService } from '../services/permission.service.js';
12
12
  import { parse as parseQuery } from './query-string-adapter.js';
13
13
  import { now } from './id-helpers.js';
14
14
  import { getConfigEngineSync } from './config-generator-engine.js';
15
- import { logAction } from './busybase-audit.js';
15
+ import { logAction } from './busybase/audit.js';
16
16
 
17
17
  export function createCrudHandlers(entityName, spec) {
18
18
  if (!spec) {
@@ -2,7 +2,7 @@ import { createLogger } from './logger.js';
2
2
  import { now, genId } from '@/lib/id-helpers';
3
3
 
4
4
  const log = createLogger('[Email]');
5
- import { list, update, create } from '@/lib/busybase-store';
5
+ import { list, update, create } from '@/lib/busybase/store';
6
6
  import { sendEmail } from '@/adapters/google-gmail';
7
7
  import { EMAIL_STATUS } from '@/config/constants';
8
8
  import { config } from '@/config/env';
@@ -0,0 +1,3 @@
1
+ export * from './types.js';
2
+ export * from './wrap.js';
3
+ export * from './recovery.js';
@@ -1,10 +1,120 @@
1
- import { createLogger } from './logger.js';
2
- import { retryWithBackoff, withCircuitBreaker, checkpoint, restoreCheckpoint, logRecovery } from '@/lib/error-resilience';
1
+ import { createLogger } from '../logger.js';
2
+ import { retryWithBackoff } from './wrap.js';
3
+ import { normalizeError, AppError } from './types.js';
4
+ import { HTTP } from '../../config/constants.js';
3
5
 
4
6
  const log = createLogger('[Recovery]');
5
- import { normalizeError, AppError } from '@/lib/error-handler';
6
- import { HTTP } from '@/config/constants';
7
+ const resilienceLog = createLogger('[Resilience]');
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Ported from error-resilience.js — circuit breaker + checkpoint primitives.
11
+ // These are used directly, and also passed as strategies into withRecovery()
12
+ // below rather than living as a second parallel wrapper module.
13
+ // ---------------------------------------------------------------------------
14
+ const errorState = { errors: [], circuitBreakers: new Map(), checkpoints: new Map() };
15
+
16
+ export function createCircuitBreaker(name, options = {}) {
17
+ const { threshold = 5, resetTimeout = 30000 } = options;
18
+
19
+ if (!errorState.circuitBreakers.has(name)) {
20
+ errorState.circuitBreakers.set(name, {
21
+ failures: 0,
22
+ state: 'closed',
23
+ lastFailure: null,
24
+ nextAttempt: null,
25
+ threshold,
26
+ resetTimeout
27
+ });
28
+ }
29
+
30
+ return errorState.circuitBreakers.get(name);
31
+ }
32
+
33
+ export async function withCircuitBreaker(name, fn, options = {}) {
34
+ const breaker = createCircuitBreaker(name, options);
35
+
36
+ if (breaker.state === 'open') {
37
+ const now = Date.now();
38
+ if (breaker.nextAttempt && now < breaker.nextAttempt) {
39
+ throw new AppError(`Service unavailable: ${name}`, 'CIRCUIT_OPEN', HTTP.SERVICE_UNAVAILABLE, { nextAttempt: breaker.nextAttempt });
40
+ }
41
+ breaker.state = 'half-open';
42
+ }
43
+
44
+ try {
45
+ const result = await fn();
46
+ if (breaker.state === 'half-open') {
47
+ }
48
+ breaker.failures = 0;
49
+ breaker.state = 'closed';
50
+ return result;
51
+ } catch (error) {
52
+ breaker.failures++;
53
+ breaker.lastFailure = Date.now();
54
+
55
+ if (breaker.failures >= breaker.threshold) {
56
+ breaker.state = 'open';
57
+ breaker.nextAttempt = Date.now() + breaker.resetTimeout;
58
+ resilienceLog.error(`circuit ${name} opened after ${breaker.failures} failures`);
59
+ }
60
+
61
+ throw error;
62
+ }
63
+ }
64
+
65
+ export function checkpoint(name, state) {
66
+ errorState.checkpoints.set(name, {
67
+ state: JSON.parse(JSON.stringify(state)),
68
+ timestamp: Date.now()
69
+ });
70
+ }
71
+
72
+ export function restoreCheckpoint(name) {
73
+ const cp = errorState.checkpoints.get(name);
74
+ if (cp) {
75
+ return cp.state;
76
+ }
77
+ return null;
78
+ }
79
+
80
+ export function logRecovery(context, action) {
81
+ errorState.errors.push({
82
+ type: 'recovery',
83
+ context,
84
+ action,
85
+ timestamp: new Date().toISOString()
86
+ });
87
+ if (errorState.errors.length > 1000) errorState.errors.shift();
88
+ }
89
+
90
+ export function getErrorStats() {
91
+ const recent = errorState.errors.slice(-100);
92
+ const byType = {};
93
+
94
+ for (const err of recent) {
95
+ byType[err.type || 'error'] = (byType[err.type || 'error'] || 0) + 1;
96
+ }
97
+
98
+ return {
99
+ total: errorState.errors.length,
100
+ recent: recent.length,
101
+ byType,
102
+ circuitBreakers: Array.from(errorState.circuitBreakers.entries()).map(([name, state]) => ({
103
+ name,
104
+ state: state.state,
105
+ failures: state.failures,
106
+ lastFailure: state.lastFailure ? new Date(state.lastFailure).toISOString() : null
107
+ })),
108
+ checkpoints: Array.from(errorState.checkpoints.keys())
109
+ };
110
+ }
7
111
 
112
+ // ---------------------------------------------------------------------------
113
+ // Ported from error-recovery.js — supervisor / degraded-mode / health-check
114
+ // strategies, now composed with the circuit-breaker + retry primitives above
115
+ // (and wrap.js's retryWithBackoff) rather than importing a second sibling
116
+ // wrapper module.
117
+ // ---------------------------------------------------------------------------
8
118
  const recoveryState = { supervisors: new Map(), lastHealthCheck: null };
9
119
 
10
120
  export function createSupervisor(name, fn, options = {}) {
@@ -190,4 +300,10 @@ if (typeof global !== 'undefined') {
190
300
  global.getSupervisorStats = getSupervisorStats;
191
301
  global.supervise = supervise;
192
302
  global.healthCheck = healthCheck;
303
+ global.errorState = errorState;
304
+ global.getErrorStats = getErrorStats;
305
+ global.retryWithBackoff = retryWithBackoff;
306
+ global.withCircuitBreaker = withCircuitBreaker;
307
+ global.checkpoint = checkpoint;
308
+ global.restoreCheckpoint = restoreCheckpoint;
193
309
  }
@@ -1,4 +1,4 @@
1
- import { createLogger } from './logger.js';
1
+ import { createLogger } from '../logger.js';
2
2
 
3
3
  // AppError stays a real class (base error type; `instanceof AppError` works).
4
4
  export class AppError extends Error {
@@ -0,0 +1,225 @@
1
+ import { AppError, normalizeError, createErrorLogger } from './types.js';
2
+ import { apiError } from '../response-formatter.js';
3
+ import { NextResponse } from '../next-shim.js';
4
+ import { HTTP } from '../../config/constants.js';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // wrap(): the single configurable wrapper subsuming what with-error-handler.js
8
+ // and api-error-wrapper.js used to do separately (timeout, retry-with-backoff,
9
+ // logging) as options. Every call site keeps its original named import below —
10
+ // each is now a thin adapter over wrap() with the exact defaults/behavior the
11
+ // original standalone function had.
12
+ // ---------------------------------------------------------------------------
13
+ export function wrap(fn, options = {}) {
14
+ const {
15
+ retry = false,
16
+ retryOptions = { maxAttempts: 3 },
17
+ timeout = null,
18
+ log = null, // { context } to enable normalize+log-on-error, or null to skip
19
+ } = options;
20
+
21
+ return async (...args) => {
22
+ const operation = async () => {
23
+ if (!timeout) return fn(...args);
24
+
25
+ const timeoutPromise = new Promise((_, reject) =>
26
+ setTimeout(() => reject(new Error(`Request timeout after ${timeout}ms`)), timeout)
27
+ );
28
+
29
+ return await Promise.race([fn(...args), timeoutPromise]);
30
+ };
31
+
32
+ try {
33
+ if (retry) {
34
+ return await retryWithBackoff(operation, retryOptions);
35
+ }
36
+ return await operation();
37
+ } catch (e) {
38
+ if (log) {
39
+ const logger = createErrorLogger(log.context || '');
40
+ const error = normalizeError(e);
41
+ logger.error(error.code || 'ERROR', error.context || {});
42
+ }
43
+ throw e;
44
+ }
45
+ };
46
+ }
47
+
48
+ // retryWithBackoff lives here (not recovery.js) because wrap() itself needs it
49
+ // with no circular dependency; recovery.js imports it back from here.
50
+ export async function retryWithBackoff(fn, options = {}) {
51
+ const { maxAttempts = 3, delay = 1000, backoff = 2, context = {} } = options;
52
+
53
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
54
+ try {
55
+ return await fn();
56
+ } catch (error) {
57
+ if (attempt === maxAttempts) {
58
+ const normalized = normalizeError(error);
59
+ throw normalized;
60
+ }
61
+
62
+ const waitTime = delay * Math.pow(backoff, attempt - 1);
63
+ await new Promise(resolve => setTimeout(resolve, waitTime));
64
+ }
65
+ }
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Ported from with-error-handler.js — identical behavior, same imports
70
+ // (including the pre-existing `apiError` import from response-formatter.js,
71
+ // which has no such export today; preserved as-is, not silently fixed, since
72
+ // fixing it is a behavior change outside this consolidation's scope).
73
+ // ---------------------------------------------------------------------------
74
+ export const withErrorHandler = (handler, operation = 'Operation') => {
75
+ const logger = createErrorLogger(operation);
76
+
77
+ return async (...args) => {
78
+ try {
79
+ return await handler(...args);
80
+ } catch (e) {
81
+ const error = normalizeError(e);
82
+ logger.error(error.code, error.context);
83
+ return apiError(error);
84
+ }
85
+ };
86
+ };
87
+
88
+ export const withAsyncErrorHandler = (handler, context = '') => {
89
+ const logger = createErrorLogger(context);
90
+
91
+ return async (...args) => {
92
+ try {
93
+ return await handler(...args);
94
+ } catch (e) {
95
+ const error = e instanceof AppError ? e : new AppError(e.message, 'INTERNAL_ERROR', HTTP.INTERNAL_ERROR, { originalMessage: e.message });
96
+ logger.error(error.code || 'ERROR', error.context || {});
97
+ throw error;
98
+ }
99
+ };
100
+ };
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Ported from api-error-wrapper.js — identical behavior/defaults.
104
+ // ---------------------------------------------------------------------------
105
+ const apiLogger = createErrorLogger('API');
106
+
107
+ export function wrapAPIRoute(handler, options = {}) {
108
+ const { retry = false, timeout = 30000, logErrors = true } = options;
109
+
110
+ return async (request, context) => {
111
+ const startTime = Date.now();
112
+ const url = new URL(request.url);
113
+
114
+ try {
115
+ const operation = async () => {
116
+ const timeoutPromise = new Promise((_, reject) =>
117
+ setTimeout(() => reject(new Error(`Request timeout after ${timeout}ms`)), timeout)
118
+ );
119
+
120
+ const handlerPromise = handler(request, context);
121
+
122
+ return await Promise.race([handlerPromise, timeoutPromise]);
123
+ };
124
+
125
+ const result = retry
126
+ ? await retryWithBackoff(operation, { maxAttempts: 3, context: { url: url.pathname } })
127
+ : await operation();
128
+
129
+ const duration = Date.now() - startTime;
130
+
131
+ if (duration > 1000) {
132
+ apiLogger.warn('Slow request', { url: url.pathname, duration });
133
+ }
134
+
135
+ return result;
136
+ } catch (error) {
137
+ const duration = Date.now() - startTime;
138
+ const normalized = normalizeError(error);
139
+
140
+ if (logErrors) {
141
+ apiLogger.error('Request failed', {
142
+ url: url.pathname,
143
+ method: request.method,
144
+ error: normalized.toJSON(),
145
+ duration
146
+ });
147
+ }
148
+
149
+ return NextResponse.json(
150
+ normalized.toJSON(),
151
+ {
152
+ status: normalized.statusCode,
153
+ headers: { 'Content-Type': 'application/json' }
154
+ }
155
+ );
156
+ }
157
+ };
158
+ }
159
+
160
+ export function wrapGETRoute(handler, options = {}) {
161
+ return wrapAPIRoute(handler, { ...options, retry: true });
162
+ }
163
+
164
+ export function wrapPOSTRoute(handler, options = {}) {
165
+ return wrapAPIRoute(handler, options);
166
+ }
167
+
168
+ export function wrapPUTRoute(handler, options = {}) {
169
+ return wrapAPIRoute(handler, options);
170
+ }
171
+
172
+ export function wrapDELETERoute(handler, options = {}) {
173
+ return wrapAPIRoute(handler, options);
174
+ }
175
+
176
+ export function createAPIHandler(handlers) {
177
+ const wrapped = {};
178
+
179
+ if (handlers.GET) wrapped.GET = wrapGETRoute(handlers.GET);
180
+ if (handlers.POST) wrapped.POST = wrapPOSTRoute(handlers.POST);
181
+ if (handlers.PUT) wrapped.PUT = wrapPUTRoute(handlers.PUT);
182
+ if (handlers.DELETE) wrapped.DELETE = wrapDELETERoute(handlers.DELETE);
183
+ if (handlers.PATCH) wrapped.PATCH = wrapAPIRoute(handlers.PATCH);
184
+ if (handlers.HEAD) wrapped.HEAD = wrapAPIRoute(handlers.HEAD);
185
+
186
+ return wrapped;
187
+ }
188
+
189
+ export async function safeJSONParse(text, fallback = null) {
190
+ try {
191
+ return JSON.parse(text);
192
+ } catch (error) {
193
+ apiLogger.warn('JSON parse failed', { error: error.message });
194
+ return fallback;
195
+ }
196
+ }
197
+
198
+ export async function safeReadBody(request, fallback = {}) {
199
+ try {
200
+ const text = await request.text();
201
+ return text ? await safeJSONParse(text, fallback) : fallback;
202
+ } catch (error) {
203
+ apiLogger.warn('Body read failed', { error: error.message });
204
+ return fallback;
205
+ }
206
+ }
207
+
208
+ export function validateRequired(data, fields) {
209
+ const missing = [];
210
+
211
+ for (const field of fields) {
212
+ if (data[field] === undefined || data[field] === null || data[field] === '') {
213
+ missing.push(field);
214
+ }
215
+ }
216
+
217
+ if (missing.length > 0) {
218
+ throw new Error(`Missing required fields: ${missing.join(', ')}`);
219
+ }
220
+ }
221
+
222
+ export function sanitizeError(error) {
223
+ const safe = String(error?.message || error || 'Unknown error');
224
+ return safe.substring(0, 500);
225
+ }
@@ -6,7 +6,7 @@ import { list, get, update, create, remove } from '@/engine.js';
6
6
  import { queueEmail } from '@/services/notification-engine.js';
7
7
  import { safeJsonParse } from '@/lib/safe-json.js';
8
8
  import { validateTransition } from '@/lib/workflow-engine.js';
9
- import { AppError } from '@/lib/error-handler';
9
+ import { AppError } from '@/lib/errors';
10
10
  import { HTTP } from '@/config/constants';
11
11
 
12
12
  const logActivity = (t, id, act, msg, u, d) =>
@@ -1,3 +1,37 @@
1
+ // NOTE (repo audit, 2026-07-16): this barrel and every module it wires together
2
+ // (promise-container, supervisor, checkpoint, timeout-wrapper, safe-error,
3
+ // cache-invalidator, directory-watcher, debug-exposure) has NO importer anywhere
4
+ // in the repo -- grepped for `hot-reload`, `@/lib/hot-reload`, and each filename
5
+ // individually across src/, bin/, scripts/, server-bootstrap.js, cli.js. This
6
+ // index.js is never imported, so `expose('hotReload', ...)` and the unconditional
7
+ // `globalThis.__debug__` assignment in debug-exposure.js never actually fire.
8
+ // None of these files gate on NODE_ENV or any dev-only flag internally (the one
9
+ // NODE_ENV check in route-wrapper.js only toggles whether a stack trace is
10
+ // included in an error body, not whether the module runs) -- so "dev-mode-only"
11
+ // is not enforced by this code, it is simply unreferenced.
12
+ //
13
+ // The ONE exception is mutex.js: `globalManager` is imported directly (bypassing
14
+ // this barrel) by src/app/api/auth/google/route.js and
15
+ // src/app/api/auth/google/callback/route.js to lock an OAuth token-refresh
16
+ // critical section -- a genuinely generic, domain-free primitive with no
17
+ // hot-reload-specific coupling, and it is live production code.
18
+ //
19
+ // Collapse decision: did NOT merge cache-invalidator.js/directory-watcher.js/
20
+ // supervisor.js. Since none of the three (nor checkpoint.js, debug-exposure.js,
21
+ // route-wrapper.js, promise-container.js, safe-error.js, timeout-wrapper.js) has
22
+ // a single live call site, merging them carries real risk (losing individually
23
+ // resumable git history, plus this index.js barrel still exports all of them by
24
+ // name so any future consumer added via this barrel would need the merge
25
+ // un-done) for zero runtime benefit -- there is nothing executing today to make
26
+ // faster, safer, or smaller. Generic/extractable-if-ever-needed: mutex.js (already
27
+ // proven generic by its live OAuth use), promise-container.js, timeout-wrapper.js,
28
+ // safe-error.js -- each is domain-free with no hot-reload-specific coupling.
29
+ // Hot-reload-coupled and not generic: cache-invalidator.js (require.cache
30
+ // invalidation), directory-watcher.js (fs.watch tree), debug-exposure.js
31
+ // (globalThis.__debug__ registry) -- these only make sense together as a dev
32
+ // hot-reload feature, but since nothing invokes them, no risky structural
33
+ // change was forced. Left as-is, working code, simply dormant.
34
+
1
35
  import { createRequire } from 'module';
2
36
  import { fileURLToPath } from 'url';
3
37
  import path from 'path';
package/src/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
- export * from '@/lib/busybase-store';
2
- export * from '@/lib/validate';
1
+ export * from '@/lib/busybase/store';
2
+ export * from '@/lib/validation/entity-validators';
3
3
  export * from '@/lib/field-types';
4
4
  export * from '@/lib/field-iterator';
5
5
  export * from '@/lib/list-data-transform';
@@ -20,11 +20,11 @@ export {
20
20
  normalizeError,
21
21
  formatErrorResponse,
22
22
  createErrorLogger,
23
- } from '@/lib/error-handler';
23
+ } from '@/lib/errors';
24
24
  export { createApiHandler } from '@/lib/api';
25
25
  export { genId, now } from '@/lib/id-helpers';
26
- export { setBusyBaseClient } from '@/lib/busybase-store';
27
- export { logAction } from '@/lib/busybase-audit';
26
+ export { setBusyBaseClient } from '@/lib/busybase/store';
27
+ export { logAction } from '@/lib/busybase/audit';
28
28
  export * from '@/lib/realtime-server';
29
29
  export * from '@/lib/hook-engine';
30
30
  export * from '@/lib/events-engine';
@@ -0,0 +1,112 @@
1
+ /*
2
+ * Shared keyed-cache primitive: a Map-backed store with TTL expiry and
3
+ * size-bounded eviction, factored out of the near-identical Map+eviction
4
+ * logic query-cache.js and render-cache.js each rolled independently.
5
+ *
6
+ * createCache({ ttlMs, maxSize }) returns a cache with get/set/delete/clear,
7
+ * plus size/stats helpers callers can build their own public stats on top of.
8
+ *
9
+ * Eviction policy: insertion-order (oldest-first / FIFO), matching what both
10
+ * original callers did via `cache.keys().next().value` -- not LRU. A `get()`
11
+ * does not move a key to the back of the map, so read-heavy hot keys are not
12
+ * specially protected from eviction; this preserves the exact prior behaviour
13
+ * of both callers being ported onto this primitive.
14
+ *
15
+ * ttlMs: 0 disables storage entirely (every set() is a no-op, every get() is
16
+ * a miss) -- mirrors query-cache.js's per-entity TTL_CONFIG=0 "never cache"
17
+ * entries (e.g. audit_logs). Omit/undefined ttlMs means "no expiry" (entries
18
+ * live until evicted by size or explicitly cleared) -- matches a cache that
19
+ * never expired anything by time.
20
+ */
21
+
22
+ export function createCache({ ttlMs, maxSize = Infinity } = {}) {
23
+ const store = new Map();
24
+ let hits = 0;
25
+ let misses = 0;
26
+
27
+ function isExpired(entry) {
28
+ if (ttlMs == null || ttlMs < 0) return false;
29
+ return Date.now() - entry.ts > ttlMs;
30
+ }
31
+
32
+ function evictIfFull() {
33
+ while (store.size >= maxSize && maxSize > 0) {
34
+ const firstKey = store.keys().next().value;
35
+ if (firstKey === undefined) break;
36
+ store.delete(firstKey);
37
+ }
38
+ }
39
+
40
+ function get(key) {
41
+ const entry = store.get(key);
42
+ if (!entry) {
43
+ misses++;
44
+ return undefined;
45
+ }
46
+ if (isExpired(entry)) {
47
+ store.delete(key);
48
+ misses++;
49
+ return undefined;
50
+ }
51
+ hits++;
52
+ return entry.value;
53
+ }
54
+
55
+ function has(key) {
56
+ const entry = store.get(key);
57
+ if (!entry) return false;
58
+ if (isExpired(entry)) {
59
+ store.delete(key);
60
+ return false;
61
+ }
62
+ return true;
63
+ }
64
+
65
+ function set(key, value) {
66
+ if (ttlMs === 0) return; // 0 means "never cache" (matches TTL_CONFIG=0 entities)
67
+ if (!store.has(key)) evictIfFull();
68
+ store.set(key, { value, ts: Date.now() });
69
+ }
70
+
71
+ function del(key) {
72
+ return store.delete(key);
73
+ }
74
+
75
+ function clear() {
76
+ store.clear();
77
+ }
78
+
79
+ function keys() {
80
+ return store.keys();
81
+ }
82
+
83
+ function deleteWhere(predicate) {
84
+ let count = 0;
85
+ for (const key of store.keys()) {
86
+ if (predicate(key)) {
87
+ store.delete(key);
88
+ count++;
89
+ }
90
+ }
91
+ return count;
92
+ }
93
+
94
+ function stats() {
95
+ const total = hits + misses;
96
+ return {
97
+ size: store.size,
98
+ maxSize,
99
+ ttlMs,
100
+ hits,
101
+ misses,
102
+ hitRate: total > 0 ? hits / total : 0,
103
+ };
104
+ }
105
+
106
+ function resetStats() {
107
+ hits = 0;
108
+ misses = 0;
109
+ }
110
+
111
+ return { get, set, has, delete: del, clear, keys, deleteWhere, stats, resetStats };
112
+ }