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.
- package/README.md +27 -0
- package/package.json +2 -5
- package/src/app/api/audit/dashboard/route.js +2 -2
- package/src/app/api/audit/logs/route.js +2 -2
- package/src/app/api/audit/permissions/[id]/route.js +2 -2
- package/src/app/api/audit/permissions/route.js +3 -3
- package/src/app/api/audit/permissions/stats/route.js +2 -2
- package/src/app/api/audit/route.js +2 -3
- package/src/app/api/audit/stats/route.js +2 -2
- package/src/app/api/auth/google/callback/route.js +1 -1
- package/src/app/api/auth/google/route.js +1 -1
- package/src/app/api/auth/login/route.js +1 -1
- package/src/app/api/auth/logout/route.js +1 -1
- package/src/app/api/auth/me/route.js +1 -1
- package/src/app/api/auth/mwr-bridge/route.js +1 -1
- package/src/app/api/auth/password-reset/route.js +1 -1
- package/src/app/api/csrf-token/route.js +1 -1
- package/src/app/api/debug/[[...path]]/route.js +2 -2
- package/src/app/api/debug/config/route.js +1 -1
- package/src/app/api/debug/hooks/route.js +1 -1
- package/src/app/api/debug/plugins/route.js +1 -1
- package/src/app/api/debug/sqlite/route.js +1 -1
- package/src/app/api/debug/sync/route.js +1 -1
- package/src/app/api/debug/workflow/route.js +1 -1
- package/src/app/api/domains/[domain]/route.js +1 -2
- package/src/app/api/domains/route.js +1 -1
- package/src/app/api/email/allocate/batch/route.js +1 -1
- package/src/app/api/email/allocate/route.js +1 -1
- package/src/app/api/email/receive/route.js +1 -1
- package/src/app/api/email/send/route.js +1 -1
- package/src/app/api/email/unallocated/route.js +1 -1
- package/src/app/api/files/[id]/route.js +1 -1
- package/src/app/api/health/route.js +1 -2
- package/src/app/api/metrics/route.js +1 -2
- package/src/engine.js +3 -3
- package/src/engine.server.js +3 -3
- package/src/index.js +35 -10
- package/src/lib/action-factory.js +1 -1
- package/src/lib/api-helpers.js +1 -1
- package/src/lib/api.js +3 -4
- package/src/lib/audit-logger-enhanced.js +1 -1
- package/src/lib/auth-middleware.js +1 -1
- package/src/lib/auth-route-helpers.js +1 -1
- package/src/lib/{busybase-adapter.js → busybase/adapter.js} +3 -3
- package/src/lib/{busybase-audit-reads.js → busybase/audit-reads.js} +1 -1
- package/src/lib/{busybase-audit.js → busybase/audit.js} +2 -2
- package/src/lib/{busybase-lucia-adapter.js → busybase/lucia-adapter.js} +1 -1
- package/src/lib/{busybase-store.js → busybase/store.js} +3 -3
- package/src/lib/config-generator-engine.js +13 -1
- package/src/lib/crud-action-helpers.js +1 -1
- package/src/lib/crud-handlers.js +4 -4
- package/src/lib/email-sender.js +1 -1
- package/src/lib/errors/index.js +3 -0
- package/src/lib/{error-recovery.js → errors/recovery.js} +120 -4
- package/src/lib/{error-handler.js → errors/types.js} +1 -1
- package/src/lib/errors/wrap.js +225 -0
- package/src/lib/events-engine.js +1 -1
- package/src/lib/hot-reload/index.js +34 -0
- package/src/lib/index.js +5 -5
- package/src/lib/keyed-cache.js +112 -0
- package/src/lib/monitor.js +285 -0
- package/src/lib/next-shim.js +294 -0
- package/src/lib/observability-init.js +213 -0
- package/src/lib/perf.js +468 -0
- package/src/lib/query-cache.js +40 -46
- package/src/lib/realtime-server.js +18 -0
- package/src/lib/render-cache.js +14 -28
- package/src/lib/{request-tracing.js → request-trace.js} +57 -2
- package/src/lib/response-formatter.js +18 -0
- package/src/lib/route-helpers.js +1 -1
- package/src/lib/state-protocol.js +13 -0
- package/src/lib/state-transport-client.js +6 -0
- package/src/lib/state-transport-reconnect.js +5 -0
- package/src/lib/state-transport-server.js +8 -0
- package/src/lib/utils.js +1 -1
- package/src/lib/{validate.js → validation/entity-validators.js} +9 -8
- package/src/lib/validation/index.js +1 -1
- package/src/lib/validation/security-validators.js +1 -1
- package/src/lib/validation-middleware.js +2 -2
- package/src/lib/workflow-engine.js +23 -3
- package/src/lib/xstate-workflow-engine.js +18 -2
- package/src/services/collaborator-role.service.js +2 -2
- package/src/services/notification-engine.js +5 -5
- package/src/services/permission.service.js +1 -1
- package/src/ui/format-helpers.js +1 -4
- package/src/ui/highlight-threading-renderer.js +1 -1
- package/src/ui/page-handler-admin.js +6 -5
- package/src/ui/page-handler-helpers.js +2 -2
- package/src/ui/page-handler-reviews.js +4 -4
- package/src/ui/page-handler-rfi.js +1 -1
- package/src/ui/page-handler.js +2 -2
- package/src/ui/render-helpers.js +1 -1
- package/src/ui/renderer.js +1 -1
- package/src/ui/{review-comparison-renderer.js → review/comparison.js} +1 -1
- package/src/ui/{review-detail-renderer.js → review/detail-renderer.js} +3 -3
- package/src/ui/review/index.js +32 -0
- package/src/ui/{review-mwr-renderer.js → review/mwr.js} +1 -1
- package/src/ui/{review-renderer.js → review/renderer.js} +1 -1
- package/src/ui/{settings-renderer.js → settings/home.js} +7 -34
- package/src/ui/{settings-renderer-advanced.js → settings/review.js} +117 -56
- package/src/ui/settings/shared.js +45 -0
- package/src/ui/{settings-renderer-teams.js → settings/teams.js} +1 -2
- package/src/ui/settings/templates.js +116 -0
- package/src/lib/api-error-wrapper.js +0 -125
- package/src/lib/db-monitor.js +0 -127
- package/src/lib/error-resilience.js +0 -132
- package/src/lib/monitoring-init.js +0 -67
- package/src/lib/next-compat.js +0 -94
- package/src/lib/next-polyfills.js +0 -135
- package/src/lib/observability-bootstrap.js +0 -116
- package/src/lib/perf-monitor.js +0 -94
- package/src/lib/perf-profiler.js +0 -233
- package/src/lib/query-perf.js +0 -117
- package/src/lib/request-tracker.js +0 -43
- package/src/lib/resource-monitor.js +0 -120
- package/src/lib/validators.js +0 -67
- package/src/lib/with-error-handler.js +0 -31
- package/src/ui/settings-renderer-advanced2.js +0 -159
- /package/src/ui/{review-detail-panels.js → review/detail-panels.js} +0 -0
- /package/src/ui/{review-detail-script.js → review/detail-script.js} +0 -0
- /package/src/ui/{review-widgets.js → review/widgets.js} +0 -0
- /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
|
|
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 '
|
|
18
|
-
import { RECORD_STATUS } from '
|
|
19
|
-
import { genId, now } from '
|
|
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';
|
package/src/lib/crud-handlers.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { createLogger } from './logger.js';
|
|
2
|
-
import { get, listWithPagination, searchWithPagination, create, update, remove } from './busybase
|
|
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 './
|
|
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 './
|
|
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
|
|
15
|
+
import { logAction } from './busybase/audit.js';
|
|
16
16
|
|
|
17
17
|
export function createCrudHandlers(entityName, spec) {
|
|
18
18
|
if (!spec) {
|
package/src/lib/email-sender.js
CHANGED
|
@@ -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
|
|
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';
|
|
@@ -1,10 +1,120 @@
|
|
|
1
|
-
import { createLogger } from '
|
|
2
|
-
import { retryWithBackoff
|
|
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
|
-
|
|
6
|
-
|
|
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
|
}
|
|
@@ -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
|
+
}
|
package/src/lib/events-engine.js
CHANGED
|
@@ -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/
|
|
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
|
|
2
|
-
export * from '@/lib/
|
|
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/
|
|
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
|
|
27
|
-
export { logAction } from '@/lib/busybase
|
|
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
|
+
}
|