thatcher 1.0.7 → 1.0.34
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 +4 -16
- package/package.json +12 -18
- package/src/adapters/google-auth.js +4 -43
- package/src/adapters/google-drive.js +12 -67
- package/src/app/api/audit/dashboard/route.js +14 -11
- package/src/app/api/audit/logs/route.js +2 -2
- package/src/app/api/audit/permissions/[id]/route.js +6 -3
- package/src/app/api/audit/permissions/route.js +10 -7
- package/src/app/api/audit/permissions/stats/route.js +8 -5
- package/src/app/api/audit/route.js +4 -4
- package/src/app/api/audit/stats/route.js +1 -1
- package/src/app/api/auth/google/callback/route.js +5 -2
- package/src/app/api/auth/google/route.js +4 -1
- package/src/app/api/auth/login/route.js +5 -2
- package/src/app/api/auth/mwr-bridge/route.js +11 -21
- package/src/app/api/auth/password-reset/route.js +15 -14
- package/src/app/api/cron/trigger/route.js +4 -1
- package/src/app/api/debug/[[...path]]/route.js +6 -5
- package/src/app/api/debug/sqlite/route.js +7 -12
- package/src/app/api/debug/sync/route.js +5 -5
- package/src/app/api/domains/route.js +4 -1
- package/src/app/api/email/allocate/batch/route.js +19 -29
- package/src/app/api/email/allocate/route.js +16 -17
- package/src/app/api/email/receive/route.js +31 -42
- package/src/app/api/email/send/route.js +28 -16
- package/src/app/api/email/unallocated/route.js +10 -13
- package/src/app/api/files/[id]/route.js +4 -1
- package/src/app/api/formula/[[...path]]/route.js +0 -4
- package/src/app/api/health/route.js +10 -9
- package/src/app/api/metrics/route.js +1 -1
- package/src/cli.js +4 -4
- package/src/config/config-loader.js +1 -21
- package/src/config/constants.js +0 -35
- package/src/config/env.js +0 -30
- package/src/config/spec-helpers.js +6 -95
- package/src/engine.js +23 -0
- package/src/engine.server.js +8 -78
- package/src/index.js +42 -67
- package/src/lib/alert-manager.js +7 -3
- package/src/lib/api-helpers.js +4 -3
- package/src/lib/api.js +10 -10
- package/src/lib/audit-logger-enhanced.js +77 -83
- package/src/lib/auth-middleware.js +1 -32
- package/src/lib/auth-route-helpers.js +5 -0
- package/src/lib/busybase-adapter.js +0 -5
- package/src/lib/busybase-audit-reads.js +142 -0
- package/src/lib/busybase-audit.js +41 -0
- package/src/lib/busybase-lucia-adapter.js +72 -0
- package/src/lib/busybase-store.js +338 -0
- package/src/lib/config-field-helpers.js +0 -17
- package/src/lib/config-generator-engine.js +19 -36
- package/src/lib/config-helpers.js +0 -21
- package/src/lib/crud-action-helpers.js +2 -2
- package/src/lib/crud-factory.js +1 -16
- package/src/lib/crud-handlers.js +17 -52
- package/src/lib/csrf-protection.js +5 -2
- package/src/lib/date-utils.js +3 -127
- package/src/lib/debug-registry.js +0 -5
- package/src/lib/domain-loader.js +4 -1
- package/src/lib/email-sender.js +25 -23
- package/src/lib/error-handler.js +5 -41
- package/src/lib/error-recovery.js +10 -7
- package/src/lib/error-resilience.js +7 -5
- package/src/lib/events-engine.js +9 -6
- package/src/lib/export-sink.js +0 -5
- package/src/lib/field-iterator.js +7 -76
- package/src/lib/field-registry.js +0 -22
- package/src/lib/generic-crud-handler.js +0 -16
- package/src/lib/hook-engine.js +5 -53
- package/src/lib/hot-reload/checkpoint.js +5 -2
- package/src/lib/hot-reload/promise-container.js +7 -3
- package/src/lib/hot-reload/route-wrapper.js +4 -1
- package/src/lib/hot-reload/supervisor.js +6 -3
- package/src/lib/hyperformula-service.js +0 -5
- package/src/lib/id-helpers.js +10 -0
- package/src/lib/index.js +4 -7
- package/src/lib/log-aggregator.js +0 -1
- package/src/lib/logger.js +0 -15
- package/src/lib/metrics-collector.js +2 -46
- package/src/lib/next-compat.js +15 -1
- package/src/lib/observability-bootstrap.js +0 -5
- package/src/lib/perf-monitor.js +4 -1
- package/src/lib/perf-profiler.js +0 -5
- package/src/lib/query-string-adapter.js +5 -47
- package/src/lib/realtime-server.js +4 -26
- package/src/lib/request-tracing.js +0 -5
- package/src/lib/resource-monitor.js +4 -1
- package/src/lib/response-formatter.js +0 -38
- package/src/lib/route-helpers.js +3 -1
- package/src/lib/safe-json.js +5 -1
- package/src/lib/server-bootstrap.js +10 -9
- package/src/lib/stage-pipeline.js +2 -2
- package/src/lib/state-transport-client.js +4 -1
- package/src/lib/static-server.js +2 -2
- package/src/lib/status-helpers.js +2 -51
- package/src/lib/tracing.js +0 -5
- package/src/lib/utils.js +6 -67
- package/src/lib/validate.js +5 -88
- package/src/lib/validators.js +0 -38
- package/src/lib/with-audit-logging.js +1 -1
- package/src/lib/workflow-engine.js +10 -77
- package/src/lib/xstate-workflow-engine.js +3 -9
- package/src/plugins/index.js +5 -3
- package/src/server/server.js +26 -27
- package/src/services/collaborator-role.service.js +21 -77
- package/src/services/email-sender.js +7 -34
- package/src/services/notification-engine.js +12 -33
- package/src/services/permission.service.js +10 -73
- package/src/ui/advanced-search-renderer.js +3 -2
- package/src/ui/advanced-widgets.js +1 -1
- package/src/ui/auth-pages.js +10 -9
- package/src/ui/auth-styles.js +1 -1
- package/src/ui/client.js +1 -1
- package/src/ui/component-engine.js +1 -1
- package/src/ui/dashboard-renderer.js +5 -5
- package/src/ui/dialog-engine.js +6 -9
- package/src/ui/dialog-factory.js +2 -2
- package/src/ui/engagement-cards.js +7 -7
- package/src/ui/engagement-dialogs.js +2 -3
- package/src/ui/engagement-grid-renderer.js +20 -19
- package/src/ui/entity-renderer.js +56 -42
- package/src/ui/file-dialogs.js +25 -9
- package/src/ui/flexup-report-renderer.js +21 -18
- package/src/ui/format-helpers.js +27 -28
- package/src/ui/highlight-threading-renderer.js +9 -9
- package/src/ui/job-management-renderer.js +18 -16
- package/src/ui/layout.js +51 -25
- package/src/ui/ml-console-renderer.js +15 -16
- package/src/ui/monitoring-dashboard-client.js +9 -8
- package/src/ui/notifications-renderer.js +11 -10
- package/src/ui/page-handler-admin.js +32 -23
- package/src/ui/page-handler-helpers.js +27 -23
- package/src/ui/page-handler-reviews.js +71 -35
- package/src/ui/page-handler-rfi.js +28 -14
- package/src/ui/page-handler.js +65 -48
- package/src/ui/picker-dialogs.js +26 -19
- package/src/ui/render-helpers.js +84 -46
- package/src/ui/review-comparison-renderer.js +8 -7
- package/src/ui/review-detail-panels.js +8 -9
- package/src/ui/review-detail-renderer.js +3 -3
- package/src/ui/review-detail-script.js +13 -12
- package/src/ui/review-mwr-renderer.js +6 -5
- package/src/ui/review-renderer.js +7 -6
- package/src/ui/review-widgets.js +3 -3
- package/src/ui/rfi-detail-renderer.js +37 -20
- package/src/ui/rfi-report-renderer.js +14 -9
- package/src/ui/settings-renderer-advanced.js +21 -18
- package/src/ui/settings-renderer-advanced2.js +20 -19
- package/src/ui/settings-renderer-teams.js +4 -4
- package/src/ui/settings-renderer.js +4 -4
- package/src/ui/spacing-system.js +5 -2
- package/src/ui/standalone-login.js +2 -2
- package/src/ui/styles.css +2 -1
- package/src/ui/styles2.css +75 -5
- package/src/ui/test-page.js +4 -4
- package/src/ui/widgets.js +20 -15
- package/src/lib/audit-logger.js +0 -193
- package/src/lib/database-core.js +0 -255
- package/src/lib/database-migrations.js +0 -350
- package/src/lib/db-backup.js +0 -97
- package/src/lib/error-boundary.js +0 -134
- package/src/lib/errors.js +0 -69
- package/src/lib/health-monitor.js +0 -134
- package/src/lib/index-optimizer.js +0 -96
- package/src/lib/query-engine-write.js +0 -221
- package/src/lib/query-engine.js +0 -446
- package/src/ui/collaboration-dialogs.js +0 -31
- package/src/ui/global-tags.js +0 -144
- package/src/ui/idle-logout.js +0 -156
- package/src/ui/letter-dialogs.js +0 -37
- package/src/ui/password-reset-page.js +0 -146
- package/src/ui/perf-renderer.js +0 -71
- package/src/ui/rfi-renderer.js +0 -194
- package/src/ui/validation-ui.js +0 -147
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from '@/lib/next-polyfills';
|
|
2
|
+
import { createLogger } from '@/lib/logger.js';
|
|
3
|
+
|
|
4
|
+
const log = createLogger('[OAuth]');
|
|
2
5
|
import { google } from '@/engine.server';
|
|
3
6
|
import { Google } from 'arctic';
|
|
4
7
|
import { generateState, generateCodeVerifier } from 'arctic';
|
|
@@ -52,7 +55,7 @@ export async function HEAD(request) {
|
|
|
52
55
|
const { valid } = validateOAuthProvider(google);
|
|
53
56
|
return new Response(null, { status: valid ? 200 : 503 });
|
|
54
57
|
} catch (error) {
|
|
55
|
-
|
|
58
|
+
log.error('HEAD error:', { message: error.message });
|
|
56
59
|
return new Response(null, { status: 500 });
|
|
57
60
|
}
|
|
58
61
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { createLogger } from '@/lib/logger.js';
|
|
1
2
|
import { getBy, verifyPassword, migrate } from '@/engine';
|
|
3
|
+
|
|
4
|
+
const log = createLogger('[Login]');
|
|
2
5
|
import { initializeSystemConfig } from '@/config/system-config-loader';
|
|
3
6
|
import { withErrorHandler } from '@/lib/with-error-handler';
|
|
4
7
|
import { lucia } from '@/engine.server';
|
|
@@ -40,7 +43,7 @@ export const POST = withErrorHandler(async (request) => {
|
|
|
40
43
|
await initializeSystemConfig();
|
|
41
44
|
migrate();
|
|
42
45
|
} catch (e) {
|
|
43
|
-
|
|
46
|
+
log.error('init failed:', { message: e.message });
|
|
44
47
|
return new Response(JSON.stringify({ error: 'System init failed' }), { status: 500, headers: { 'Content-Type': 'application/json' } });
|
|
45
48
|
}
|
|
46
49
|
}
|
|
@@ -115,7 +118,7 @@ export const POST = withErrorHandler(async (request) => {
|
|
|
115
118
|
}
|
|
116
119
|
});
|
|
117
120
|
} catch (err) {
|
|
118
|
-
|
|
121
|
+
log.error(err.message);
|
|
119
122
|
return new Response(JSON.stringify({ error: 'Authentication failed' }), { status: 500, headers: { 'Content-Type': 'application/json' } });
|
|
120
123
|
}
|
|
121
124
|
}, 'Auth:Login');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { getBy } from '@/engine';
|
|
1
|
+
import { getBy, list, update } from '@/engine';
|
|
2
2
|
import { lucia } from '@/engine.server';
|
|
3
|
-
import {
|
|
3
|
+
import { now } from '@/lib/id-helpers';
|
|
4
4
|
import { withErrorHandler } from '@/lib/with-error-handler';
|
|
5
5
|
import crypto from 'crypto';
|
|
6
6
|
|
|
@@ -21,34 +21,24 @@ export const POST = withErrorHandler(async (request) => {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
|
|
24
|
-
const db = getDatabase();
|
|
25
24
|
|
|
26
|
-
let bridgeRecord;
|
|
25
|
+
let bridgeRecord = null;
|
|
27
26
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
token_hash TEXT NOT NULL,
|
|
33
|
-
email TEXT NOT NULL,
|
|
34
|
-
expires_at INTEGER NOT NULL,
|
|
35
|
-
used INTEGER DEFAULT 0,
|
|
36
|
-
created_at INTEGER NOT NULL
|
|
37
|
-
)`);
|
|
38
|
-
db.exec('CREATE INDEX IF NOT EXISTS idx_mwr_bridge_token ON mwr_bridge_tokens(token_hash)');
|
|
39
|
-
bridgeRecord = null;
|
|
40
|
-
}
|
|
27
|
+
const matches = (await list('mwr_bridge_tokens', { token_hash: tokenHash }))
|
|
28
|
+
.filter(r => !r.used || r.used === 0);
|
|
29
|
+
bridgeRecord = matches[0] || null;
|
|
30
|
+
} catch { bridgeRecord = null; }
|
|
41
31
|
|
|
42
|
-
const user = getBy('user', 'email', email.toLowerCase().trim());
|
|
32
|
+
const user = await getBy('user', 'email', email.toLowerCase().trim());
|
|
43
33
|
if (!user) {
|
|
44
|
-
return new Response(JSON.stringify({ error: 'User not found
|
|
34
|
+
return new Response(JSON.stringify({ error: 'User not found' }), {
|
|
45
35
|
status: 404, headers: { 'Content-Type': 'application/json' }
|
|
46
36
|
});
|
|
47
37
|
}
|
|
48
38
|
|
|
49
39
|
if (bridgeRecord) {
|
|
50
40
|
if (bridgeRecord.expires_at < now()) {
|
|
51
|
-
|
|
41
|
+
await update('mwr_bridge_tokens', bridgeRecord.id, { used: 1 });
|
|
52
42
|
return new Response(JSON.stringify({ error: 'Bridge token has expired' }), {
|
|
53
43
|
status: 401, headers: { 'Content-Type': 'application/json' }
|
|
54
44
|
});
|
|
@@ -60,7 +50,7 @@ export const POST = withErrorHandler(async (request) => {
|
|
|
60
50
|
});
|
|
61
51
|
}
|
|
62
52
|
|
|
63
|
-
|
|
53
|
+
await update('mwr_bridge_tokens', bridgeRecord.id, { used: 1 });
|
|
64
54
|
}
|
|
65
55
|
|
|
66
56
|
const session = await lucia.createSession(user.id, {});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { getBy } from '@/engine';
|
|
2
|
-
import {
|
|
3
|
-
import { getDatabase, genId, now } from '@/lib/database-core';
|
|
1
|
+
import { getBy, hashPassword, list, create, update, remove } from '@/engine';
|
|
2
|
+
import { genId, now } from '@/lib/id-helpers';
|
|
4
3
|
import { withErrorHandler } from '@/lib/with-error-handler';
|
|
5
4
|
import crypto from 'crypto';
|
|
6
5
|
|
|
@@ -12,16 +11,16 @@ export const POST = withErrorHandler(async (request) => {
|
|
|
12
11
|
return new Response(JSON.stringify({ error: 'Email is required' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
const user = getBy('user', 'email', email);
|
|
14
|
+
const user = await getBy('user', 'email', email);
|
|
16
15
|
|
|
17
16
|
if (user) {
|
|
18
|
-
const db = getDatabase();
|
|
19
17
|
const token = crypto.randomBytes(32).toString('hex');
|
|
20
18
|
const expiresAt = now() + 3600;
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
// Clear any existing reset tokens for this user, then issue a fresh one.
|
|
21
|
+
const existing = await list('password_reset_tokens', { user_id: user.id });
|
|
22
|
+
for (const t of existing) await remove('password_reset_tokens', t.id);
|
|
23
|
+
await create('password_reset_tokens', { id: genId(), user_id: user.id, token, expires_at: expiresAt, used: 0, created_at: now() });
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
return new Response(JSON.stringify({ status: 'success', message: 'If an account exists with that email, a reset link has been sent.' }), {
|
|
@@ -41,23 +40,25 @@ export const PUT = withErrorHandler(async (request) => {
|
|
|
41
40
|
return new Response(JSON.stringify({ error: 'Password must be at least 8 characters' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
const
|
|
45
|
-
const resetToken =
|
|
43
|
+
const matches = (await list('password_reset_tokens', { token })).filter(t => !t.used || t.used === 0);
|
|
44
|
+
const resetToken = matches[0];
|
|
46
45
|
|
|
47
46
|
if (!resetToken) {
|
|
48
47
|
return new Response(JSON.stringify({ error: 'Invalid or expired reset token' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
|
|
49
48
|
}
|
|
50
49
|
|
|
51
50
|
if (resetToken.expires_at < now()) {
|
|
52
|
-
|
|
51
|
+
await update('password_reset_tokens', resetToken.id, { used: 1 });
|
|
53
52
|
return new Response(JSON.stringify({ error: 'Reset token has expired. Please request a new one.' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
const passwordHash = await hashPassword(password);
|
|
57
56
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
await update('user', resetToken.user_id, { password_hash: passwordHash });
|
|
58
|
+
await update('password_reset_tokens', resetToken.id, { used: 1 });
|
|
59
|
+
// Invalidate all sessions for this user.
|
|
60
|
+
const sessions = await list('sessions', { user_id: resetToken.user_id });
|
|
61
|
+
for (const s of sessions) await remove('sessions', s.id);
|
|
61
62
|
|
|
62
63
|
return new Response(JSON.stringify({ status: 'success', message: 'Password updated successfully' }), {
|
|
63
64
|
status: 200, headers: { 'Content-Type': 'application/json' }
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { createLogger } from '@/lib/logger.js';
|
|
1
2
|
import { runDueJobs } from '@/engine/job-engine';
|
|
3
|
+
|
|
4
|
+
const log = createLogger('[Cron]');
|
|
2
5
|
import { create } from '@/engine';
|
|
3
6
|
|
|
4
7
|
export const runtime = 'nodejs';
|
|
@@ -28,7 +31,7 @@ export async function POST(request) {
|
|
|
28
31
|
duration_ms: duration,
|
|
29
32
|
status: results.failed > 0 ? 'partial_failure' : 'success',
|
|
30
33
|
error_details: results.errors || null
|
|
31
|
-
}).catch(err =>
|
|
34
|
+
}).catch(err => log.error('log error:', { message: err.message }));
|
|
32
35
|
|
|
33
36
|
return new Response(
|
|
34
37
|
JSON.stringify({
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Debug API Routes - Comprehensive observability endpoints
|
|
3
|
-
* Exposes all subsystem state as JSON for debugging and monitoring
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
import { tracer } from '@/lib/tracing.js';
|
|
7
2
|
import { perfProfiler } from '@/lib/perf-profiler.js';
|
|
8
3
|
import { debugRegistry } from '@/lib/debug-registry.js';
|
|
@@ -27,6 +22,12 @@ function errorResponse(message, status = 400) {
|
|
|
27
22
|
}
|
|
28
23
|
|
|
29
24
|
export async function GET(request) {
|
|
25
|
+
// The debug surface exposes memory, CPU, queries, hooks, config and env state.
|
|
26
|
+
// That is operational intelligence an attacker must never get in production, so
|
|
27
|
+
// the endpoint is disabled there outright (opt back in only behind real auth).
|
|
28
|
+
if (process.env.NODE_ENV === 'production') {
|
|
29
|
+
return errorResponse('Debug endpoints are disabled in production', 403);
|
|
30
|
+
}
|
|
30
31
|
const url = new URL(request.url);
|
|
31
32
|
const path = url.pathname;
|
|
32
33
|
const params = url.searchParams;
|
|
@@ -1,21 +1,16 @@
|
|
|
1
1
|
import { NextResponse } from '@/lib/next-polyfills';
|
|
2
|
-
import { getDatabase } from '@/engine';
|
|
3
2
|
|
|
3
|
+
// The data layer is busybase (LanceDB), not SQLite — the old pragma/page-count probe
|
|
4
|
+
// no longer applies. This endpoint now reports the configured store kind.
|
|
4
5
|
export async function GET() {
|
|
5
6
|
if (process.env.NODE_ENV === 'production') {
|
|
6
7
|
return NextResponse.json({ error: 'Not available in production' }, { status: 403 });
|
|
7
8
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const wal_autocheckpoint = db.pragma('wal_autocheckpoint', { simple: true });
|
|
14
|
-
const path = db.name;
|
|
15
|
-
return NextResponse.json({ path, page_count, page_size, bytes: page_count * page_size, journal_mode, wal_autocheckpoint });
|
|
16
|
-
} catch (e) {
|
|
17
|
-
return NextResponse.json({ error: String(e?.message || e) }, { status: 500 });
|
|
18
|
-
}
|
|
9
|
+
return NextResponse.json({
|
|
10
|
+
store: 'busybase',
|
|
11
|
+
dir: process.env.BUSYBASE_DIR || 'busybase_data',
|
|
12
|
+
note: 'SQLite has been replaced by busybase (LanceDB); pragma stats are not applicable.',
|
|
13
|
+
});
|
|
19
14
|
}
|
|
20
15
|
|
|
21
16
|
export const config = { runtime: 'nodejs' };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { NextResponse } from '@/lib/next-polyfills';
|
|
4
|
-
import {
|
|
4
|
+
import { count } from '@/engine';
|
|
5
5
|
|
|
6
6
|
export async function GET() {
|
|
7
7
|
if (process.env.NODE_ENV === 'production') {
|
|
@@ -17,10 +17,10 @@ export async function GET() {
|
|
|
17
17
|
} catch {}
|
|
18
18
|
let table_counts = {};
|
|
19
19
|
try {
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
for (const
|
|
23
|
-
try { table_counts[
|
|
20
|
+
// entity names (busybase tables); 'user' maps to the users table in the store
|
|
21
|
+
const entities = ['user', 'engagement', 'review', 'rfi_template', 'entity_type', 'engagement_type', 'client'];
|
|
22
|
+
for (const e of entities) {
|
|
23
|
+
try { table_counts[e] = await count(e, {}); } catch { table_counts[e] = null; }
|
|
24
24
|
}
|
|
25
25
|
} catch (e) { table_counts._error = String(e?.message || e); }
|
|
26
26
|
return NextResponse.json({ last_run_at, table_counts });
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { createLogger } from '@/lib/logger.js';
|
|
1
2
|
import { getDomainLoader } from '@/lib/domain-loader';
|
|
3
|
+
|
|
4
|
+
const log = createLogger('[DomainsAPI]');
|
|
2
5
|
import { ok } from '@/lib/response-formatter';
|
|
3
6
|
import { withErrorHandler } from '@/lib/with-error-handler';
|
|
4
7
|
import { getConfigEngine } from '@/lib/config-generator-engine';
|
|
@@ -12,7 +15,7 @@ export const GET = withErrorHandler(async (request) => {
|
|
|
12
15
|
try {
|
|
13
16
|
return domainLoader.getDomainInfo(domainName);
|
|
14
17
|
} catch (error) {
|
|
15
|
-
|
|
18
|
+
log.error(`error loading domain ${domainName}:`, { message: error.message });
|
|
16
19
|
return null;
|
|
17
20
|
}
|
|
18
21
|
}).filter(Boolean);
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { NextResponse } from '@/lib/next-polyfills';
|
|
2
|
-
import {
|
|
2
|
+
import { createLogger } from '@/lib/logger.js';
|
|
3
|
+
|
|
4
|
+
const log = createLogger('[EmailBatchAllocate]');
|
|
5
|
+
import { genId, now } from '@/lib/id-helpers';
|
|
6
|
+
import { list, create, update } from '@/engine';
|
|
3
7
|
import { autoAllocateEmail } from '@/lib/email-parser';
|
|
4
8
|
|
|
5
9
|
export async function POST(request) {
|
|
@@ -14,14 +18,9 @@ export async function POST(request) {
|
|
|
14
18
|
const body = await request.json();
|
|
15
19
|
const { min_confidence = 70, batch_size = 50 } = body;
|
|
16
20
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
SELECT * FROM email
|
|
21
|
-
WHERE allocated = 0 AND status = 'pending'
|
|
22
|
-
ORDER BY received_at DESC
|
|
23
|
-
LIMIT ?
|
|
24
|
-
`).all(batch_size);
|
|
21
|
+
const unallocatedEmails = (await list('email', { status: 'pending' }, { sort: { field: 'received_at', dir: 'DESC' } }))
|
|
22
|
+
.filter(e => !e.allocated || e.allocated === 0)
|
|
23
|
+
.slice(0, batch_size);
|
|
25
24
|
|
|
26
25
|
const results = {
|
|
27
26
|
allocated: [],
|
|
@@ -38,24 +37,20 @@ export async function POST(request) {
|
|
|
38
37
|
const logId = genId();
|
|
39
38
|
const timestamp = now();
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
email.id,
|
|
49
|
-
'batch_allocated',
|
|
50
|
-
`Email batch-allocated to ${result.engagement_id ? 'engagement' : 'RFI'}`,
|
|
51
|
-
JSON.stringify({
|
|
40
|
+
await create('activity_log', {
|
|
41
|
+
id: logId,
|
|
42
|
+
entity_type: 'email',
|
|
43
|
+
entity_id: email.id,
|
|
44
|
+
action: 'batch_allocated',
|
|
45
|
+
message: `Email batch-allocated to ${result.engagement_id ? 'engagement' : 'RFI'}`,
|
|
46
|
+
details: JSON.stringify({
|
|
52
47
|
engagement_id: result.engagement_id || null,
|
|
53
48
|
rfi_id: result.rfi_id || null,
|
|
54
49
|
confidence: result.confidence,
|
|
55
50
|
method: 'batch_automatic',
|
|
56
51
|
}),
|
|
57
|
-
timestamp
|
|
58
|
-
);
|
|
52
|
+
created_at: timestamp,
|
|
53
|
+
});
|
|
59
54
|
|
|
60
55
|
results.allocated.push({
|
|
61
56
|
email_id: email.id,
|
|
@@ -85,19 +80,14 @@ export async function POST(request) {
|
|
|
85
80
|
reason: error.message,
|
|
86
81
|
});
|
|
87
82
|
|
|
88
|
-
|
|
89
|
-
UPDATE email
|
|
90
|
-
SET processing_error = ?,
|
|
91
|
-
updated_at = ?
|
|
92
|
-
WHERE id = ?
|
|
93
|
-
`).run(error.message, now(), email.id);
|
|
83
|
+
await update('email', email.id, { processing_error: error.message, updated_at: now() });
|
|
94
84
|
}
|
|
95
85
|
}
|
|
96
86
|
|
|
97
87
|
return NextResponse.json(results);
|
|
98
88
|
|
|
99
89
|
} catch (error) {
|
|
100
|
-
|
|
90
|
+
log.error('error:', { message: error.message });
|
|
101
91
|
return NextResponse.json(
|
|
102
92
|
{ error: 'Internal server error', message: error.message },
|
|
103
93
|
{ status: 500 }
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { NextResponse } from '@/lib/next-polyfills';
|
|
2
|
-
import {
|
|
2
|
+
import { createLogger } from '@/lib/logger.js';
|
|
3
|
+
import { genId, now } from '@/lib/id-helpers';
|
|
4
|
+
import { get, create } from '@/engine';
|
|
5
|
+
|
|
6
|
+
const log = createLogger('[EmailAllocate]');
|
|
3
7
|
import {
|
|
4
8
|
allocateEmailToEntity,
|
|
5
9
|
autoAllocateEmail,
|
|
@@ -23,8 +27,7 @@ export async function POST(request) {
|
|
|
23
27
|
);
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
const
|
|
27
|
-
const email = db.prepare('SELECT * FROM email WHERE id = ?').get(email_id);
|
|
30
|
+
const email = await get('email', email_id);
|
|
28
31
|
|
|
29
32
|
if (!email) {
|
|
30
33
|
return NextResponse.json(
|
|
@@ -104,24 +107,20 @@ export async function POST(request) {
|
|
|
104
107
|
const logId = genId();
|
|
105
108
|
const timestamp = now();
|
|
106
109
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
email_id,
|
|
115
|
-
'allocated',
|
|
116
|
-
`Email allocated to ${result.engagement_id ? 'engagement' : 'RFI'}`,
|
|
117
|
-
JSON.stringify({
|
|
110
|
+
await create('activity_log', {
|
|
111
|
+
id: logId,
|
|
112
|
+
entity_type: 'email',
|
|
113
|
+
entity_id: email_id,
|
|
114
|
+
action: 'allocated',
|
|
115
|
+
message: `Email allocated to ${result.engagement_id ? 'engagement' : 'RFI'}`,
|
|
116
|
+
details: JSON.stringify({
|
|
118
117
|
engagement_id: result.engagement_id || null,
|
|
119
118
|
rfi_id: result.rfi_id || null,
|
|
120
119
|
method: auto ? 'automatic' : 'manual',
|
|
121
120
|
confidence: result.confidence || 100,
|
|
122
121
|
}),
|
|
123
|
-
timestamp
|
|
124
|
-
);
|
|
122
|
+
created_at: timestamp,
|
|
123
|
+
});
|
|
125
124
|
|
|
126
125
|
return NextResponse.json({
|
|
127
126
|
success: true,
|
|
@@ -132,7 +131,7 @@ export async function POST(request) {
|
|
|
132
131
|
});
|
|
133
132
|
|
|
134
133
|
} catch (error) {
|
|
135
|
-
|
|
134
|
+
log.error('error:', { message: error.message });
|
|
136
135
|
return NextResponse.json(
|
|
137
136
|
{ error: 'Internal server error', message: error.message },
|
|
138
137
|
{ status: 500 }
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { NextResponse } from '@/lib/next-polyfills';
|
|
2
|
-
import {
|
|
2
|
+
import { createLogger } from '@/lib/logger.js';
|
|
3
|
+
import { genId, now } from '@/lib/id-helpers';
|
|
4
|
+
import { create } from '@/engine';
|
|
5
|
+
|
|
6
|
+
const log = createLogger('[EmailReceive]');
|
|
3
7
|
import path from 'path';
|
|
4
8
|
import fs from 'fs';
|
|
5
9
|
|
|
6
|
-
const
|
|
10
|
+
const EMAIL_ATTACHMENTS_DIR = path.resolve(process.cwd(), 'data', 'temp_email_attachments');
|
|
7
11
|
|
|
8
|
-
if (!fs.existsSync(
|
|
9
|
-
fs.mkdirSync(
|
|
12
|
+
if (!fs.existsSync(EMAIL_ATTACHMENTS_DIR)) {
|
|
13
|
+
fs.mkdirSync(EMAIL_ATTACHMENTS_DIR, { recursive: true });
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
export async function POST(request) {
|
|
@@ -49,7 +53,7 @@ export async function POST(request) {
|
|
|
49
53
|
const attachmentId = genId();
|
|
50
54
|
const ext = path.extname(filename);
|
|
51
55
|
const safeFilename = `${attachmentId}${ext}`;
|
|
52
|
-
const filePath = path.join(
|
|
56
|
+
const filePath = path.join(EMAIL_ATTACHMENTS_DIR, safeFilename);
|
|
53
57
|
|
|
54
58
|
const buffer = Buffer.isBuffer(content)
|
|
55
59
|
? content
|
|
@@ -86,45 +90,30 @@ export async function POST(request) {
|
|
|
86
90
|
processing_error: null,
|
|
87
91
|
};
|
|
88
92
|
|
|
89
|
-
const db = getDatabase();
|
|
90
|
-
const stmt = db.prepare(`
|
|
91
|
-
INSERT INTO email (
|
|
92
|
-
id, sender_email, sender_name, subject, body, html_body,
|
|
93
|
-
message_id, in_reply_to, references, received_at, allocated,
|
|
94
|
-
engagement_id, rfi_id, attachments, status, processed, processing_error,
|
|
95
|
-
created_at, updated_at
|
|
96
|
-
) VALUES (
|
|
97
|
-
?, ?, ?, ?, ?, ?,
|
|
98
|
-
?, ?, ?, ?, ?,
|
|
99
|
-
?, ?, ?, ?, ?, ?,
|
|
100
|
-
?, ?
|
|
101
|
-
)
|
|
102
|
-
`);
|
|
103
|
-
|
|
104
93
|
const emailId = genId();
|
|
105
94
|
const timestamp = now();
|
|
106
95
|
|
|
107
|
-
|
|
108
|
-
emailId,
|
|
109
|
-
emailRecord.sender_email,
|
|
110
|
-
emailRecord.sender_name,
|
|
111
|
-
emailRecord.subject,
|
|
112
|
-
emailRecord.body,
|
|
113
|
-
emailRecord.html_body,
|
|
114
|
-
emailRecord.message_id,
|
|
115
|
-
emailRecord.in_reply_to,
|
|
116
|
-
emailRecord.references,
|
|
117
|
-
emailRecord.received_at,
|
|
118
|
-
emailRecord.allocated ? 1 : 0,
|
|
119
|
-
emailRecord.engagement_id,
|
|
120
|
-
emailRecord.rfi_id,
|
|
121
|
-
emailRecord.attachments,
|
|
122
|
-
emailRecord.status,
|
|
123
|
-
emailRecord.processed ? 1 : 0,
|
|
124
|
-
emailRecord.processing_error,
|
|
125
|
-
timestamp,
|
|
126
|
-
timestamp
|
|
127
|
-
);
|
|
96
|
+
await create('email', {
|
|
97
|
+
id: emailId,
|
|
98
|
+
sender_email: emailRecord.sender_email,
|
|
99
|
+
sender_name: emailRecord.sender_name,
|
|
100
|
+
subject: emailRecord.subject,
|
|
101
|
+
body: emailRecord.body,
|
|
102
|
+
html_body: emailRecord.html_body,
|
|
103
|
+
message_id: emailRecord.message_id,
|
|
104
|
+
in_reply_to: emailRecord.in_reply_to || '',
|
|
105
|
+
references: emailRecord.references || '',
|
|
106
|
+
received_at: emailRecord.received_at,
|
|
107
|
+
allocated: emailRecord.allocated ? 1 : 0,
|
|
108
|
+
engagement_id: emailRecord.engagement_id || '',
|
|
109
|
+
rfi_id: emailRecord.rfi_id || '',
|
|
110
|
+
attachments: emailRecord.attachments,
|
|
111
|
+
status: emailRecord.status,
|
|
112
|
+
processed: emailRecord.processed ? 1 : 0,
|
|
113
|
+
processing_error: emailRecord.processing_error || '',
|
|
114
|
+
created_at: timestamp,
|
|
115
|
+
updated_at: timestamp,
|
|
116
|
+
});
|
|
128
117
|
|
|
129
118
|
return NextResponse.json({
|
|
130
119
|
success: true,
|
|
@@ -134,7 +123,7 @@ export async function POST(request) {
|
|
|
134
123
|
});
|
|
135
124
|
|
|
136
125
|
} catch (error) {
|
|
137
|
-
|
|
126
|
+
log.error('error processing webhook:', { message: error.message });
|
|
138
127
|
|
|
139
128
|
return NextResponse.json(
|
|
140
129
|
{
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { NextResponse } from '@/lib/next-polyfills';
|
|
2
|
-
import {
|
|
2
|
+
import { createLogger } from '@/lib/logger.js';
|
|
3
|
+
import { now } from '@/lib/id-helpers';
|
|
4
|
+
|
|
5
|
+
const log = createLogger('[Email]');
|
|
6
|
+
import { list, update } from '@/engine';
|
|
3
7
|
import { getConfigEngine } from '@/lib/config-generator-engine';
|
|
4
8
|
import { EMAIL_STATUS } from '@/config/constants';
|
|
5
9
|
import { sendSingleEmail, checkFailureRate } from '@/lib/email-sender';
|
|
@@ -19,7 +23,6 @@ export async function POST(request) {
|
|
|
19
23
|
if (!token || token !== process.env.CRON_SECRET)
|
|
20
24
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
21
25
|
|
|
22
|
-
const db = getDatabase();
|
|
23
26
|
try {
|
|
24
27
|
const emailCfg = await getEmailConfig();
|
|
25
28
|
const MAX_RETRIES = emailCfg.send_max_retries || 3;
|
|
@@ -27,8 +30,10 @@ export async function POST(request) {
|
|
|
27
30
|
const RATE_LIMIT_DELAY = emailCfg.rate_limit_delay_ms || 6000;
|
|
28
31
|
const MAX_DELAY_MS = emailCfg.retry_max_delay_ms || 30000;
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
// pending emails under the retry cap, oldest first, limited to the batch size
|
|
34
|
+
const pendingEmails = (await list('email', { status: EMAIL_STATUS.PENDING }, { sort: { field: 'created_at', dir: 'ASC' } }))
|
|
35
|
+
.filter(e => (e.retry_count == null || e.retry_count < MAX_RETRIES))
|
|
36
|
+
.slice(0, BATCH_SIZE);
|
|
32
37
|
|
|
33
38
|
if (!pendingEmails.length)
|
|
34
39
|
return NextResponse.json({ success: true, message: 'No pending emails', processed: 0 });
|
|
@@ -38,18 +43,18 @@ export async function POST(request) {
|
|
|
38
43
|
|
|
39
44
|
for (let i = 0; i < pendingEmails.length; i++) {
|
|
40
45
|
const email = pendingEmails[i];
|
|
41
|
-
|
|
42
|
-
const result = await sendSingleEmail(
|
|
46
|
+
await update('email', email.id, { status: EMAIL_STATUS.PROCESSING, updated_at: now() });
|
|
47
|
+
const result = await sendSingleEmail(email, email.retry_count || 1, MAX_RETRIES, MAX_DELAY_MS);
|
|
43
48
|
results.push(result);
|
|
44
49
|
result.success ? successCount++ : failureCount++;
|
|
45
50
|
if (i < pendingEmails.length - 1)
|
|
46
51
|
await new Promise(r => setTimeout(r, RATE_LIMIT_DELAY));
|
|
47
52
|
}
|
|
48
53
|
|
|
49
|
-
checkFailureRate(
|
|
54
|
+
await checkFailureRate();
|
|
50
55
|
return NextResponse.json({ success: true, processed: pendingEmails.length, results: { success: successCount, failed: failureCount }, details: results });
|
|
51
56
|
} catch (error) {
|
|
52
|
-
|
|
57
|
+
log.error('queue processing error:', { message: error.message });
|
|
53
58
|
return NextResponse.json({ error: 'Email queue processing failed', details: error.message }, { status: 500 });
|
|
54
59
|
}
|
|
55
60
|
}
|
|
@@ -59,16 +64,23 @@ export async function GET(request) {
|
|
|
59
64
|
if (!token || token !== process.env.CRON_SECRET)
|
|
60
65
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
61
66
|
|
|
62
|
-
const db = getDatabase();
|
|
63
67
|
try {
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
const emails = await list('email', {});
|
|
69
|
+
// status counts (old GROUP BY status)
|
|
70
|
+
const stats = {};
|
|
71
|
+
for (const e of emails) stats[e.status] = (stats[e.status] || 0) + 1;
|
|
72
|
+
// failure rate over the last 24h
|
|
73
|
+
const cutoff = now() - 86400;
|
|
74
|
+
const recent = emails.filter(e => (e.created_at || 0) >= cutoff);
|
|
75
|
+
const failedTotal = recent.filter(e => e.status === EMAIL_STATUS.FAILED).length;
|
|
76
|
+
const recentFailures = emails
|
|
77
|
+
.filter(e => e.status === EMAIL_STATUS.FAILED)
|
|
78
|
+
.sort((a, b) => (b.created_at || 0) - (a.created_at || 0))
|
|
79
|
+
.slice(0, 10)
|
|
80
|
+
.map(e => ({ id: e.id, recipient_email: e.recipient_email, subject: e.subject, processing_error: e.processing_error, retry_count: e.retry_count, created_at: e.created_at }));
|
|
69
81
|
return NextResponse.json({
|
|
70
|
-
stats
|
|
71
|
-
failureRate:
|
|
82
|
+
stats,
|
|
83
|
+
failureRate: recent.length > 0 ? failedTotal / recent.length : 0,
|
|
72
84
|
recentFailures,
|
|
73
85
|
});
|
|
74
86
|
} catch (error) {
|