thatcher 1.0.44 → 1.0.46
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 +44 -30
- package/package.json +1 -1
- package/src/adapters/google-auth.js +0 -7
- package/src/adapters/google-drive.js +1 -3
- package/src/adapters/google-gmail.js +1 -0
- package/src/app/api/[entity]/[[...path]]/route.js +1 -1
- package/src/app/api/audit/logs/route.js +1 -1
- package/src/app/api/audit/permissions/route.js +0 -1
- package/src/app/api/audit/route.js +2 -4
- 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/logout/route.js +1 -1
- package/src/app/api/csrf-token/route.js +1 -1
- package/src/app/api/domains/route.js +1 -1
- package/src/app/api/email/allocate/route.js +0 -1
- package/src/app/api/email/receive/route.js +1 -1
- package/src/app/api/files/[id]/route.js +1 -1
- package/src/app/api/health/route.js +1 -1
- package/src/cli.js +9 -4
- package/src/config/spec-helpers.js +0 -1
- package/src/engine.server.js +1 -1
- package/src/lib/alert-manager.js +1 -1
- package/src/lib/api-helpers.js +1 -1
- package/src/lib/auth-route-helpers.js +1 -1
- package/src/lib/business-rules-engine.js +3 -3
- package/src/lib/busybase/adapter.js +2 -2
- package/src/lib/busybase/store.js +1 -1
- package/src/lib/config-generator-engine.js +0 -2
- package/src/lib/crud-action-helpers.js +1 -1
- package/src/lib/crud-handlers.js +2 -2
- package/src/lib/csrf-protection.js +0 -1
- package/src/lib/debug-registry.js +1 -1
- package/src/lib/errors/recovery.js +0 -8
- package/src/lib/errors/wrap.js +1 -1
- package/src/lib/events-engine.js +1 -1
- package/src/lib/field-iterator.js +2 -2
- package/src/lib/field-registry.js +2 -1
- package/src/lib/hot-reload/index.js +2 -1
- package/src/lib/minifier.js +1 -1
- package/src/lib/next-shim.js +2 -2
- package/src/lib/perf.js +0 -2
- package/src/lib/request-trace.js +1 -4
- package/src/lib/route-resolver.js +1 -1
- package/src/lib/stage-pipeline.js +0 -1
- package/src/lib/static-server.js +1 -1
- package/src/lib/tracing.js +0 -3
- package/src/lib/validation/security-validators.js +2 -2
- package/src/lib/validation-middleware.js +1 -1
- package/src/lib/workflow-engine.js +1 -1
- package/src/lib/xstate-workflow-engine.js +1 -1
- package/src/server/server.js +5 -9
- package/src/services/collaborator-role.service.js +1 -3
- package/src/services/notification-engine.js +3 -3
- package/src/services/permission.service.js +1 -1
- package/src/ui/advanced-widgets.js +0 -3
- package/src/ui/auth-pages.js +1 -1
- package/src/ui/common-handlers.js +2 -2
- package/src/ui/component-engine.js +2 -2
- package/src/ui/dashboard-renderer.js +1 -1
- package/src/ui/dialog-engine.js +11 -9
- package/src/ui/engagement-cards.js +0 -1
- package/src/ui/engagement-dialogs.js +0 -2
- package/src/ui/entity-renderer.js +2 -3
- package/src/ui/event-delegation.js +1 -1
- package/src/ui/file-dialogs.js +0 -2
- package/src/ui/job-management-renderer.js +0 -1
- package/src/ui/layout.js +4 -4
- package/src/ui/page-handler-admin.js +2 -2
- package/src/ui/page-handler-helpers.js +3 -3
- package/src/ui/page-handler-rfi.js +1 -1
- package/src/ui/page-handler.js +1 -1
- package/src/ui/permissions-ui.js +0 -12
- package/src/ui/picker-dialogs.js +0 -1
- package/src/ui/review/comparison.js +1 -1
- package/src/ui/review/detail-renderer.js +1 -1
- package/src/ui/review/mwr.js +1 -1
- package/src/ui/review/widgets.js +0 -1
- package/src/ui/rfi-detail-renderer.js +1 -1
- package/src/ui/settings/home.js +1 -1
- package/src/ui/settings/review.js +0 -1
- package/src/ui/settings/teams.js +1 -1
- package/src/ui/spacing-system.js +1 -1
package/README.md
CHANGED
|
@@ -133,36 +133,50 @@ const canEdit = await thatcher.can(user, thatcher.getEntitySpec('item'), 'edit')
|
|
|
133
133
|
|
|
134
134
|
```
|
|
135
135
|
thatcher/
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
136
|
+
src/
|
|
137
|
+
index.js # Main entry: createThatcher()
|
|
138
|
+
engine.js / engine.server.js # Runtime engine, server-only auth/session surface
|
|
139
|
+
cli.js # CLI commands
|
|
140
|
+
config/
|
|
141
|
+
config-loader.js # YAML loading & validation
|
|
142
|
+
spec-helpers.js # Entity spec utilities
|
|
143
|
+
env.js # Environment config
|
|
144
|
+
constants.js # HTTP codes, statuses
|
|
145
|
+
lib/
|
|
146
|
+
busybase/
|
|
147
|
+
adapter.js # busybase connection/adapter layer
|
|
148
|
+
store.js # busybase data layer (async CRUD)
|
|
149
|
+
audit.js / audit-reads.js # Audit log writes/reads
|
|
150
|
+
lucia-adapter.js # Lucia auth session adapter
|
|
151
|
+
errors/
|
|
152
|
+
types.js # AppError and error type hierarchy
|
|
153
|
+
wrap.js # wrap() - unified handler wrapper (timeout/retry/logging)
|
|
154
|
+
recovery.js # Circuit breaker, checkpoint, retry-with-backoff
|
|
155
|
+
validation/
|
|
156
|
+
entity-validators.js / business-validators.js / format-validators.js / security-validators.js / file-validators.js
|
|
157
|
+
csrf.js / rate-limit.js
|
|
158
|
+
config-generator-engine.js # Spec builder
|
|
159
|
+
hook-engine.js # Event system
|
|
160
|
+
workflow-engine.js # State machines (legacy/canonical) - xstate-workflow-engine.js is the debug/test-only successor
|
|
161
|
+
auth-middleware.js # Auth checks
|
|
162
|
+
crud-factory.js # Handler factory
|
|
163
|
+
crud-handlers.js # HTTP handlers - read + write CRUD operations
|
|
164
|
+
logger.js # Structured logging
|
|
165
|
+
services/
|
|
166
|
+
permission.service.js # Authorization
|
|
167
|
+
collaborator-role.service.js
|
|
168
|
+
notification-engine.js / email-sender.js
|
|
169
|
+
adapters/
|
|
170
|
+
google-auth.js # Google OAuth
|
|
171
|
+
google-drive.js # Drive file operations
|
|
172
|
+
google-gmail.js # Gmail send/receive
|
|
173
|
+
plugins/
|
|
174
|
+
index.js # Plugin auto-discovery
|
|
175
|
+
app/api/ # Route handlers (Next.js-style file routing, [entity]/[[...path]] catch-all + explicit routes)
|
|
176
|
+
ui/ # Server-rendered HTML components + client-side scripts
|
|
177
|
+
server/
|
|
178
|
+
server.js # HTTP server
|
|
179
|
+
package.json
|
|
166
180
|
```
|
|
167
181
|
|
|
168
182
|
### How It Works
|
package/package.json
CHANGED
|
@@ -4,19 +4,12 @@ import { google } from 'googleapis';
|
|
|
4
4
|
const log = createLogger('[GoogleAuth]');
|
|
5
5
|
import { buildConfig } from '../config/env.js';
|
|
6
6
|
import fs from 'fs';
|
|
7
|
-
import path from 'path';
|
|
8
7
|
|
|
9
8
|
let _oauth2Client = null;
|
|
10
9
|
let _jwtClient = null;
|
|
11
10
|
|
|
12
11
|
export function initGoogleAuth(config = null) {
|
|
13
12
|
const cfg = config || buildConfig();
|
|
14
|
-
const scopes = [
|
|
15
|
-
'https://www.googleapis.com/auth/userinfo.email',
|
|
16
|
-
'https://www.googleapis.com/auth/userinfo.profile',
|
|
17
|
-
'https://www.googleapis.com/auth/drive',
|
|
18
|
-
'https://www.googleapis.com/auth/gmail.send',
|
|
19
|
-
];
|
|
20
13
|
|
|
21
14
|
// OAuth2 client for web flow
|
|
22
15
|
_oauth2Client = new google.auth.OAuth2(
|
|
@@ -4,8 +4,6 @@ import path from 'path';
|
|
|
4
4
|
import { getJWTClient, getOAuth2Client } from './google-auth.js';
|
|
5
5
|
import { buildConfig } from '../config/env.js';
|
|
6
6
|
|
|
7
|
-
const DRIVE_SCOPES = ['https://www.googleapis.com/auth/drive'];
|
|
8
|
-
|
|
9
7
|
export function getDriveClient(user = null) {
|
|
10
8
|
let client;
|
|
11
9
|
|
|
@@ -26,7 +24,7 @@ export function getDriveClient(user = null) {
|
|
|
26
24
|
export async function uploadFile(filePath, name, options = {}, user = null) {
|
|
27
25
|
const drive = getDriveClient(user);
|
|
28
26
|
const absPath = path.resolve(filePath);
|
|
29
|
-
|
|
27
|
+
fs.statSync(absPath);
|
|
30
28
|
|
|
31
29
|
const res = await drive.files.create({
|
|
32
30
|
requestBody: {
|
|
@@ -24,6 +24,7 @@ export function getGmailClient(user = null) {
|
|
|
24
24
|
|
|
25
25
|
function encodeHeader(value) {
|
|
26
26
|
// RFC 2047 encoded-word for any non-ASCII header value (subject/name).
|
|
27
|
+
// eslint-disable-next-line no-control-regex -- \x00-\x7F is the intentional ASCII range check, not a stray control char
|
|
27
28
|
if (/^[\x00-\x7F]*$/.test(value)) return value;
|
|
28
29
|
return `=?UTF-8?B?${Buffer.from(value, 'utf8').toString('base64')}?=`;
|
|
29
30
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createCrudHandlers } from '@/lib/crud-factory';
|
|
2
2
|
import { setCurrentRequest } from '@/engine.server';
|
|
3
3
|
|
|
4
|
-
function wrapHandler(
|
|
4
|
+
function wrapHandler(_method) {
|
|
5
5
|
return async (request, context) => {
|
|
6
6
|
setCurrentRequest(request);
|
|
7
7
|
const params = await context.params;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from '@/lib/next-shim';
|
|
2
2
|
import { requireAuth } from '@/lib/auth-middleware';
|
|
3
|
-
import { searchLogs,
|
|
3
|
+
import { searchLogs, rotateLogsOlderThan } from '@/lib/audit-logger-enhanced';
|
|
4
4
|
import { withErrorHandler } from '@/lib/errors';
|
|
5
5
|
|
|
6
6
|
export const GET = withErrorHandler(async (request) => {
|
|
@@ -33,7 +33,6 @@ export async function GET(request) {
|
|
|
33
33
|
const search = searchParams.get('search');
|
|
34
34
|
const startDate = searchParams.get('start_date');
|
|
35
35
|
const endDate = searchParams.get('end_date');
|
|
36
|
-
const action = searchParams.get('action');
|
|
37
36
|
const limit = parseInt(searchParams.get('limit') || '100', 10);
|
|
38
37
|
const offset = parseInt(searchParams.get('offset') || '0', 10);
|
|
39
38
|
const format = searchParams.get('format');
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import { requireAuth
|
|
1
|
+
import { requireAuth } from '@/lib/auth-middleware';
|
|
2
2
|
import { getAuditHistory, getActionStats, getUserStats } from '@/lib/busybase/audit-reads';
|
|
3
|
-
import { getSpec } from '@/config/spec-helpers';
|
|
4
3
|
import { paginated, ok } from '@/lib/response-formatter';
|
|
5
4
|
import { withErrorHandler, AppError } from '@/lib/errors';
|
|
6
5
|
import { HTTP } from '@/config/constants';
|
|
7
|
-
import { parse as parseQuery } from '@/lib/query-string-adapter';
|
|
8
6
|
|
|
9
7
|
export const GET = withErrorHandler(async (request) => {
|
|
10
|
-
|
|
8
|
+
await requireAuth();
|
|
11
9
|
const url = new URL(request.url);
|
|
12
10
|
|
|
13
11
|
const entityType = url.searchParams.get('entityType');
|
|
@@ -2,7 +2,7 @@ import { generateToken } from '@/lib/csrf-protection';
|
|
|
2
2
|
import { ok } from '@/lib/response-formatter';
|
|
3
3
|
import { withErrorHandler } from '@/lib/errors';
|
|
4
4
|
|
|
5
|
-
export const GET = withErrorHandler(async (
|
|
5
|
+
export const GET = withErrorHandler(async (_request) => {
|
|
6
6
|
const token = generateToken();
|
|
7
7
|
return ok({ csrfToken: token });
|
|
8
8
|
}, 'CSRF:GetToken');
|
|
@@ -6,7 +6,7 @@ import { ok } from '@/lib/response-formatter';
|
|
|
6
6
|
import { withErrorHandler } from '@/lib/errors';
|
|
7
7
|
import { getConfigEngine } from '@/lib/config-generator-engine';
|
|
8
8
|
|
|
9
|
-
export const GET = withErrorHandler(async (
|
|
9
|
+
export const GET = withErrorHandler(async (_request) => {
|
|
10
10
|
await getConfigEngine();
|
|
11
11
|
const domainLoader = getDomainLoader();
|
|
12
12
|
const validDomains = domainLoader.getValidDomains();
|
|
@@ -27,7 +27,7 @@ export async function GET(request, { params }) {
|
|
|
27
27
|
|
|
28
28
|
const content = await fileService.download(fileRecord.drive_file_id);
|
|
29
29
|
|
|
30
|
-
const safeName = (fileRecord.file_name || 'download').replace(/[^\w
|
|
30
|
+
const safeName = (fileRecord.file_name || 'download').replace(/[^\w.-]/g, '_');
|
|
31
31
|
return new NextResponse(content, {
|
|
32
32
|
headers: {
|
|
33
33
|
'Content-Type': fileRecord.mime_type || 'application/octet-stream',
|
package/src/cli.js
CHANGED
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
* thatcher example - Generate example config
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { Thatcher } from './index.js';
|
|
15
15
|
import * as readline from 'readline';
|
|
16
|
+
import fs from 'fs';
|
|
17
|
+
import path from 'path';
|
|
16
18
|
|
|
17
19
|
const args = process.argv.slice(2);
|
|
18
20
|
const command = args[0];
|
|
@@ -20,7 +22,7 @@ const command = args[0];
|
|
|
20
22
|
async function main() {
|
|
21
23
|
switch (command) {
|
|
22
24
|
case 'start':
|
|
23
|
-
case 'dev':
|
|
25
|
+
case 'dev': {
|
|
24
26
|
console.log(`[Thatcher] Starting in ${command} mode...`);
|
|
25
27
|
const thatcher = new Thatcher({
|
|
26
28
|
server: { hotReload: command === 'dev' },
|
|
@@ -34,15 +36,17 @@ async function main() {
|
|
|
34
36
|
process.exit(1);
|
|
35
37
|
}
|
|
36
38
|
break;
|
|
39
|
+
}
|
|
37
40
|
|
|
38
|
-
case 'migrate':
|
|
41
|
+
case 'migrate': {
|
|
39
42
|
console.log('[Thatcher] Running migrations...');
|
|
40
43
|
const t1 = new Thatcher({});
|
|
41
44
|
await t1.init();
|
|
42
45
|
console.log('[Thatcher] Migrations complete');
|
|
43
46
|
break;
|
|
47
|
+
}
|
|
44
48
|
|
|
45
|
-
case 'validate':
|
|
49
|
+
case 'validate': {
|
|
46
50
|
console.log('[Thatcher] Validating configuration...');
|
|
47
51
|
try {
|
|
48
52
|
const t2 = new Thatcher({});
|
|
@@ -55,6 +59,7 @@ async function main() {
|
|
|
55
59
|
process.exit(1);
|
|
56
60
|
}
|
|
57
61
|
break;
|
|
62
|
+
}
|
|
58
63
|
|
|
59
64
|
case 'console':
|
|
60
65
|
case 'repl':
|
|
@@ -135,7 +135,6 @@ export function buildNavigation(configEngine, user = null) {
|
|
|
135
135
|
// Filter by user permissions if provided
|
|
136
136
|
if (user && configEngine.getRolePermissions) {
|
|
137
137
|
return items.filter(item => {
|
|
138
|
-
const spec = configEngine.generateEntitySpec(item.name);
|
|
139
138
|
const perms = configEngine.getRolePermissions(user.role, item.name);
|
|
140
139
|
return perms.includes('list') || perms.includes('view') || perms.includes('all');
|
|
141
140
|
});
|
package/src/engine.server.js
CHANGED
|
@@ -104,7 +104,7 @@ export async function createSession(userId) {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
export async function invalidateSession() {
|
|
107
|
-
|
|
107
|
+
getLucia();
|
|
108
108
|
// Simplified - caller handles cookie clearing
|
|
109
109
|
// In full app this uses @lucia-auth/adapter-sqlite's cookie management
|
|
110
110
|
}
|
package/src/lib/alert-manager.js
CHANGED
|
@@ -22,7 +22,7 @@ function checkErrorRate(errorMetrics) {
|
|
|
22
22
|
const window = thresholds.errorRate.window
|
|
23
23
|
let recentErrors = 0
|
|
24
24
|
|
|
25
|
-
for (const [
|
|
25
|
+
for (const [, data] of Object.entries(errorMetrics.byEndpoint)) {
|
|
26
26
|
const recent = (data.recent || []).filter(e => (now - e.ts) < window)
|
|
27
27
|
recentErrors += recent.length
|
|
28
28
|
}
|
package/src/lib/api-helpers.js
CHANGED
|
@@ -30,7 +30,7 @@ export const ok = (data) => NextResponse.json(withMetadata(data, HTTP.OK, 'succe
|
|
|
30
30
|
export const created = (data) => NextResponse.json(withMetadata(data, HTTP.CREATED, 'created'), { status: HTTP.CREATED });
|
|
31
31
|
export const notFound = (entity = 'Resource') => NextResponse.json(withMetadata({ error: ERROR_MESSAGES.notFound(entity) }, HTTP.NOT_FOUND, 'error'), { status: HTTP.NOT_FOUND });
|
|
32
32
|
export const badRequest = (reason = 'invalid data') => NextResponse.json(withMetadata({ error: ERROR_MESSAGES.invalidRequest(reason) }, HTTP.BAD_REQUEST, 'error'), { status: HTTP.BAD_REQUEST });
|
|
33
|
-
export const unauthorized = (
|
|
33
|
+
export const unauthorized = (_action = 'perform this action') => NextResponse.json(withMetadata({ error: ERROR_MESSAGES.permission.denied }, HTTP.FORBIDDEN, 'error'), { status: HTTP.FORBIDDEN });
|
|
34
34
|
export const serverError = (msg = null) => NextResponse.json(withMetadata({ error: msg || ERROR_MESSAGES.operationFailed('server operation') }, HTTP.INTERNAL_ERROR, 'error'), { status: HTTP.INTERNAL_ERROR });
|
|
35
35
|
|
|
36
36
|
export async function withEntityAccess(entity, action, handler) {
|
|
@@ -23,7 +23,7 @@ export function validateOAuthProvider(provider) {
|
|
|
23
23
|
return { valid: true };
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export async function setOAuthCookie(name, value
|
|
26
|
+
export async function setOAuthCookie(name, value) {
|
|
27
27
|
const timestamp = Date.now();
|
|
28
28
|
const key = `oauth-${timestamp}-${Math.random().toString(36).substring(7)}`;
|
|
29
29
|
const expiresAt = timestamp + (SESSION.cookieMaxAge * 1000);
|
|
@@ -11,7 +11,7 @@ export class BusinessRulesEngine {
|
|
|
11
11
|
return rule && !this.isDuplicateEngagement(engagement);
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
this.registerRule('daysOutstandingNotificationNeeded', (
|
|
14
|
+
this.registerRule('daysOutstandingNotificationNeeded', (_rfi) => {
|
|
15
15
|
return this.businessRules.rfiDaysOutstanding.enabled;
|
|
16
16
|
});
|
|
17
17
|
|
|
@@ -23,7 +23,7 @@ export class BusinessRulesEngine {
|
|
|
23
23
|
return true;
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
this.registerRule('clientCanRate', (user,
|
|
26
|
+
this.registerRule('clientCanRate', (user, _engagement) => {
|
|
27
27
|
return user.type === 'client' && user.role === 'client_admin';
|
|
28
28
|
});
|
|
29
29
|
|
|
@@ -66,7 +66,7 @@ export class BusinessRulesEngine {
|
|
|
66
66
|
return this.businessRules.recreationRules.engagement[interval];
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
isDuplicateEngagement(
|
|
69
|
+
isDuplicateEngagement(_engagement) {
|
|
70
70
|
return false;
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -32,7 +32,7 @@ export class BusyBaseAdapter {
|
|
|
32
32
|
this._client = this._embedded;
|
|
33
33
|
logger.info('Embedded mode initialized', { dir: this.config.dir });
|
|
34
34
|
} else {
|
|
35
|
-
const { default: BB
|
|
35
|
+
const { default: BB } = await import('busybase');
|
|
36
36
|
this._client = BB(this.config.url, this.config.project);
|
|
37
37
|
logger.info('Remote mode initialized', { url: this.config.url });
|
|
38
38
|
}
|
|
@@ -106,7 +106,7 @@ export class BusyBaseAdapter {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
async unsubscribeAll() {
|
|
109
|
-
for (const [
|
|
109
|
+
for (const [, { channel }] of _realtimeChannels.entries()) {
|
|
110
110
|
channel.unsubscribe();
|
|
111
111
|
}
|
|
112
112
|
_realtimeChannels.clear();
|
|
@@ -271,7 +271,7 @@ export async function create(entity, data, user) {
|
|
|
271
271
|
// unconditional-write behaviour, unchanged for every existing caller; _version
|
|
272
272
|
// is added as a new column, ignored by every reader that doesn't ask for it.
|
|
273
273
|
export async function update(entity, id, data, opts = {}) {
|
|
274
|
-
|
|
274
|
+
specOf(entity);
|
|
275
275
|
const tbl = tableName(entity);
|
|
276
276
|
|
|
277
277
|
const existing = await get(entity, id);
|
|
@@ -12,9 +12,7 @@
|
|
|
12
12
|
// as of this writing, vs. workflow-engine.js's 2 and
|
|
13
13
|
// xstate-workflow-engine.js's 2 debug/test-only importers) -- this is live,
|
|
14
14
|
// central infrastructure, not a candidate for deprecation.
|
|
15
|
-
import { load as yamlLoad } from 'js-yaml';
|
|
16
15
|
import { LRUCache, deepFreeze, deepClone, recursiveResolve } from './config-helpers.js';
|
|
17
|
-
import { generateFieldsFromOverrides, ensureFieldLabels } from './config-field-helpers.js';
|
|
18
16
|
|
|
19
17
|
export class ConfigGeneratorEngine {
|
|
20
18
|
constructor(masterConfig) {
|
|
@@ -9,7 +9,7 @@ const ACTION_FIELD_MAPS = {
|
|
|
9
9
|
respond: (data, user) => ({ status: 'responded', response: data.response, responded_at: now(), responded_by: user.id }),
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
export function resolveActionUpdate(action, data, user,
|
|
12
|
+
export function resolveActionUpdate(action, data, user, _record) {
|
|
13
13
|
const mapped = ACTION_FIELD_MAPS[action];
|
|
14
14
|
if (typeof mapped === 'function') return mapped(data, user);
|
|
15
15
|
if (mapped) {
|
package/src/lib/crud-handlers.js
CHANGED
|
@@ -3,10 +3,10 @@ import { get, listWithPagination, searchWithPagination, create, update, remove }
|
|
|
3
3
|
|
|
4
4
|
const log = createLogger('[CRUD]');
|
|
5
5
|
import { validateEntity, validateUpdate, sanitizeData } from './validation/index.js';
|
|
6
|
-
import { requirePermission
|
|
6
|
+
import { requirePermission } from './auth-middleware.js';
|
|
7
7
|
import { executeHook } from './hook-engine.js';
|
|
8
8
|
import { AppError, NotFoundError, ValidationError } from './errors/index.js';
|
|
9
|
-
import { ok, created, paginated, noContent
|
|
9
|
+
import { ok, created, paginated, noContent } 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';
|
|
@@ -43,8 +43,6 @@ export async function withCircuitBreaker(name, fn, options = {}) {
|
|
|
43
43
|
|
|
44
44
|
try {
|
|
45
45
|
const result = await fn();
|
|
46
|
-
if (breaker.state === 'half-open') {
|
|
47
|
-
}
|
|
48
46
|
breaker.failures = 0;
|
|
49
47
|
breaker.state = 'closed';
|
|
50
48
|
return result;
|
|
@@ -248,12 +246,6 @@ export async function withRecovery(fn, options = {}) {
|
|
|
248
246
|
|
|
249
247
|
let operation = fn;
|
|
250
248
|
|
|
251
|
-
if (checkpointName) {
|
|
252
|
-
const savedState = restoreCheckpoint(checkpointName);
|
|
253
|
-
if (savedState) {
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
249
|
if (supervisor) {
|
|
258
250
|
operation = () => supervise(supervisor, operation, options);
|
|
259
251
|
}
|
package/src/lib/errors/wrap.js
CHANGED
|
@@ -48,7 +48,7 @@ export function wrap(fn, options = {}) {
|
|
|
48
48
|
// retryWithBackoff lives here (not recovery.js) because wrap() itself needs it
|
|
49
49
|
// with no circular dependency; recovery.js imports it back from here.
|
|
50
50
|
export async function retryWithBackoff(fn, options = {}) {
|
|
51
|
-
const { maxAttempts = 3, delay = 1000, backoff = 2
|
|
51
|
+
const { maxAttempts = 3, delay = 1000, backoff = 2 } = options;
|
|
52
52
|
|
|
53
53
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
54
54
|
try {
|
package/src/lib/events-engine.js
CHANGED
|
@@ -175,7 +175,7 @@ export const registerEntityHandlers = () => {
|
|
|
175
175
|
create('removed_highlight', { review_id: highlight.review_id, original_id: highlight.id, highlight_data: JSON.stringify(highlight) }, user);
|
|
176
176
|
});
|
|
177
177
|
|
|
178
|
-
hookEngine.on('collaborator:afterCreate', async (collaborator,
|
|
178
|
+
hookEngine.on('collaborator:afterCreate', async (collaborator, _user) => {
|
|
179
179
|
const review = get('review', collaborator.review_id);
|
|
180
180
|
const collabUser = get('user', collaborator.user_id);
|
|
181
181
|
if (review && collabUser) {
|
|
@@ -20,13 +20,13 @@ export function getFieldNames(spec) {
|
|
|
20
20
|
|
|
21
21
|
export function getEditableFields(spec) {
|
|
22
22
|
return Object.entries(spec.fields || {})
|
|
23
|
-
.filter(([
|
|
23
|
+
.filter(([, field]) => !field.readonly && !field.auto && !field.auto_generate)
|
|
24
24
|
.map(([key]) => key);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export function getFieldsByType(spec, type) {
|
|
28
28
|
return Object.entries(spec.fields || {})
|
|
29
|
-
.filter(([
|
|
29
|
+
.filter(([, field]) => field.type === type);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export function fieldQuery(spec, predicates, options = {}) {
|
|
@@ -11,9 +11,10 @@ export function coerceFieldValue(value, type) {
|
|
|
11
11
|
case 'json':
|
|
12
12
|
return typeof value === 'string' ? JSON.parse(value) : value;
|
|
13
13
|
case 'date':
|
|
14
|
-
case 'timestamp':
|
|
14
|
+
case 'timestamp': {
|
|
15
15
|
const num = Number(value);
|
|
16
16
|
return isNaN(num) ? null : num;
|
|
17
|
+
}
|
|
17
18
|
case 'ref':
|
|
18
19
|
return String(value);
|
|
19
20
|
default:
|
|
@@ -41,7 +41,7 @@ const __dirname = path.dirname(__filename);
|
|
|
41
41
|
const require = createRequire(import.meta.url);
|
|
42
42
|
|
|
43
43
|
const promiseContainerMod = require('./promise-container.js');
|
|
44
|
-
|
|
44
|
+
import { Mutex, MutexManager, globalManager } from './mutex.js';
|
|
45
45
|
import { Supervisor, SupervisorTree, globalTree } from './supervisor.js';
|
|
46
46
|
const checkpointMod = require('./checkpoint.js');
|
|
47
47
|
const timeoutMod = require('./timeout-wrapper.js');
|
|
@@ -72,6 +72,7 @@ expose('hotReload', {
|
|
|
72
72
|
}, 'Hot reload infrastructure');
|
|
73
73
|
|
|
74
74
|
export {
|
|
75
|
+
Mutex, MutexManager, globalManager,
|
|
75
76
|
PromiseContainer, globalContainer, contain,
|
|
76
77
|
Supervisor, SupervisorTree, globalTree,
|
|
77
78
|
CheckpointManager, globalCheckpoint,
|
package/src/lib/minifier.js
CHANGED
package/src/lib/next-shim.js
CHANGED
|
@@ -262,9 +262,9 @@ export async function cookies() {
|
|
|
262
262
|
|
|
263
263
|
export function headers() {
|
|
264
264
|
return {
|
|
265
|
-
get: (
|
|
265
|
+
get: (_name) => null,
|
|
266
266
|
getSetCookie: () => [],
|
|
267
|
-
has: (
|
|
267
|
+
has: (_name) => false,
|
|
268
268
|
entries: () => [],
|
|
269
269
|
};
|
|
270
270
|
}
|
package/src/lib/perf.js
CHANGED
|
@@ -22,7 +22,6 @@ const logger = createLogger('[Perf]');
|
|
|
22
22
|
const _profiles = new Map();
|
|
23
23
|
const _alerts = [];
|
|
24
24
|
const MAX_ALERTS = 100;
|
|
25
|
-
const BASELINE_WINDOW_MS = 3600000;
|
|
26
25
|
|
|
27
26
|
const DEFAULT_THRESHOLDS = {
|
|
28
27
|
'http.request': 500,
|
|
@@ -119,7 +118,6 @@ export class PerfProfiler {
|
|
|
119
118
|
if (!profile) return null;
|
|
120
119
|
|
|
121
120
|
const durations = profile.durations.map(d => d.durationMs).sort((a, b) => a - b);
|
|
122
|
-
const count = durations.length;
|
|
123
121
|
|
|
124
122
|
return {
|
|
125
123
|
operation: profile.operation,
|
package/src/lib/request-trace.js
CHANGED
|
@@ -6,11 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
import { tracer } from './tracing.js';
|
|
8
8
|
import { perfProfiler } from './perf.js';
|
|
9
|
-
import { createLogger } from './logger.js';
|
|
10
9
|
import { recordRequest, recordError } from './metrics-collector.js';
|
|
11
10
|
|
|
12
|
-
const logger = createLogger('[RequestTracing]');
|
|
13
|
-
|
|
14
11
|
// ---------------------------------------------------------------------------
|
|
15
12
|
// From request-tracing.js
|
|
16
13
|
// ---------------------------------------------------------------------------
|
|
@@ -223,7 +220,7 @@ export default withTracing;
|
|
|
223
220
|
// ---------------------------------------------------------------------------
|
|
224
221
|
// From request-tracker.js
|
|
225
222
|
// ---------------------------------------------------------------------------
|
|
226
|
-
function wrapHandler(handler,
|
|
223
|
+
function wrapHandler(handler, _metadata = {}) {
|
|
227
224
|
return async (request, context) => {
|
|
228
225
|
const start = process.hrtime.bigint();
|
|
229
226
|
const method = request.method;
|