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.
Files changed (174) hide show
  1. package/README.md +4 -16
  2. package/package.json +12 -18
  3. package/src/adapters/google-auth.js +4 -43
  4. package/src/adapters/google-drive.js +12 -67
  5. package/src/app/api/audit/dashboard/route.js +14 -11
  6. package/src/app/api/audit/logs/route.js +2 -2
  7. package/src/app/api/audit/permissions/[id]/route.js +6 -3
  8. package/src/app/api/audit/permissions/route.js +10 -7
  9. package/src/app/api/audit/permissions/stats/route.js +8 -5
  10. package/src/app/api/audit/route.js +4 -4
  11. package/src/app/api/audit/stats/route.js +1 -1
  12. package/src/app/api/auth/google/callback/route.js +5 -2
  13. package/src/app/api/auth/google/route.js +4 -1
  14. package/src/app/api/auth/login/route.js +5 -2
  15. package/src/app/api/auth/mwr-bridge/route.js +11 -21
  16. package/src/app/api/auth/password-reset/route.js +15 -14
  17. package/src/app/api/cron/trigger/route.js +4 -1
  18. package/src/app/api/debug/[[...path]]/route.js +6 -5
  19. package/src/app/api/debug/sqlite/route.js +7 -12
  20. package/src/app/api/debug/sync/route.js +5 -5
  21. package/src/app/api/domains/route.js +4 -1
  22. package/src/app/api/email/allocate/batch/route.js +19 -29
  23. package/src/app/api/email/allocate/route.js +16 -17
  24. package/src/app/api/email/receive/route.js +31 -42
  25. package/src/app/api/email/send/route.js +28 -16
  26. package/src/app/api/email/unallocated/route.js +10 -13
  27. package/src/app/api/files/[id]/route.js +4 -1
  28. package/src/app/api/formula/[[...path]]/route.js +0 -4
  29. package/src/app/api/health/route.js +10 -9
  30. package/src/app/api/metrics/route.js +1 -1
  31. package/src/cli.js +4 -4
  32. package/src/config/config-loader.js +1 -21
  33. package/src/config/constants.js +0 -35
  34. package/src/config/env.js +0 -30
  35. package/src/config/spec-helpers.js +6 -95
  36. package/src/engine.js +23 -0
  37. package/src/engine.server.js +8 -78
  38. package/src/index.js +42 -67
  39. package/src/lib/alert-manager.js +7 -3
  40. package/src/lib/api-helpers.js +4 -3
  41. package/src/lib/api.js +10 -10
  42. package/src/lib/audit-logger-enhanced.js +77 -83
  43. package/src/lib/auth-middleware.js +1 -32
  44. package/src/lib/auth-route-helpers.js +5 -0
  45. package/src/lib/busybase-adapter.js +0 -5
  46. package/src/lib/busybase-audit-reads.js +142 -0
  47. package/src/lib/busybase-audit.js +41 -0
  48. package/src/lib/busybase-lucia-adapter.js +72 -0
  49. package/src/lib/busybase-store.js +338 -0
  50. package/src/lib/config-field-helpers.js +0 -17
  51. package/src/lib/config-generator-engine.js +19 -36
  52. package/src/lib/config-helpers.js +0 -21
  53. package/src/lib/crud-action-helpers.js +2 -2
  54. package/src/lib/crud-factory.js +1 -16
  55. package/src/lib/crud-handlers.js +17 -52
  56. package/src/lib/csrf-protection.js +5 -2
  57. package/src/lib/date-utils.js +3 -127
  58. package/src/lib/debug-registry.js +0 -5
  59. package/src/lib/domain-loader.js +4 -1
  60. package/src/lib/email-sender.js +25 -23
  61. package/src/lib/error-handler.js +5 -41
  62. package/src/lib/error-recovery.js +10 -7
  63. package/src/lib/error-resilience.js +7 -5
  64. package/src/lib/events-engine.js +9 -6
  65. package/src/lib/export-sink.js +0 -5
  66. package/src/lib/field-iterator.js +7 -76
  67. package/src/lib/field-registry.js +0 -22
  68. package/src/lib/generic-crud-handler.js +0 -16
  69. package/src/lib/hook-engine.js +5 -53
  70. package/src/lib/hot-reload/checkpoint.js +5 -2
  71. package/src/lib/hot-reload/promise-container.js +7 -3
  72. package/src/lib/hot-reload/route-wrapper.js +4 -1
  73. package/src/lib/hot-reload/supervisor.js +6 -3
  74. package/src/lib/hyperformula-service.js +0 -5
  75. package/src/lib/id-helpers.js +10 -0
  76. package/src/lib/index.js +4 -7
  77. package/src/lib/log-aggregator.js +0 -1
  78. package/src/lib/logger.js +0 -15
  79. package/src/lib/metrics-collector.js +2 -46
  80. package/src/lib/next-compat.js +15 -1
  81. package/src/lib/observability-bootstrap.js +0 -5
  82. package/src/lib/perf-monitor.js +4 -1
  83. package/src/lib/perf-profiler.js +0 -5
  84. package/src/lib/query-string-adapter.js +5 -47
  85. package/src/lib/realtime-server.js +4 -26
  86. package/src/lib/request-tracing.js +0 -5
  87. package/src/lib/resource-monitor.js +4 -1
  88. package/src/lib/response-formatter.js +0 -38
  89. package/src/lib/route-helpers.js +3 -1
  90. package/src/lib/safe-json.js +5 -1
  91. package/src/lib/server-bootstrap.js +10 -9
  92. package/src/lib/stage-pipeline.js +2 -2
  93. package/src/lib/state-transport-client.js +4 -1
  94. package/src/lib/static-server.js +2 -2
  95. package/src/lib/status-helpers.js +2 -51
  96. package/src/lib/tracing.js +0 -5
  97. package/src/lib/utils.js +6 -67
  98. package/src/lib/validate.js +5 -88
  99. package/src/lib/validators.js +0 -38
  100. package/src/lib/with-audit-logging.js +1 -1
  101. package/src/lib/workflow-engine.js +10 -77
  102. package/src/lib/xstate-workflow-engine.js +3 -9
  103. package/src/plugins/index.js +5 -3
  104. package/src/server/server.js +26 -27
  105. package/src/services/collaborator-role.service.js +21 -77
  106. package/src/services/email-sender.js +7 -34
  107. package/src/services/notification-engine.js +12 -33
  108. package/src/services/permission.service.js +10 -73
  109. package/src/ui/advanced-search-renderer.js +3 -2
  110. package/src/ui/advanced-widgets.js +1 -1
  111. package/src/ui/auth-pages.js +10 -9
  112. package/src/ui/auth-styles.js +1 -1
  113. package/src/ui/client.js +1 -1
  114. package/src/ui/component-engine.js +1 -1
  115. package/src/ui/dashboard-renderer.js +5 -5
  116. package/src/ui/dialog-engine.js +6 -9
  117. package/src/ui/dialog-factory.js +2 -2
  118. package/src/ui/engagement-cards.js +7 -7
  119. package/src/ui/engagement-dialogs.js +2 -3
  120. package/src/ui/engagement-grid-renderer.js +20 -19
  121. package/src/ui/entity-renderer.js +56 -42
  122. package/src/ui/file-dialogs.js +25 -9
  123. package/src/ui/flexup-report-renderer.js +21 -18
  124. package/src/ui/format-helpers.js +27 -28
  125. package/src/ui/highlight-threading-renderer.js +9 -9
  126. package/src/ui/job-management-renderer.js +18 -16
  127. package/src/ui/layout.js +51 -25
  128. package/src/ui/ml-console-renderer.js +15 -16
  129. package/src/ui/monitoring-dashboard-client.js +9 -8
  130. package/src/ui/notifications-renderer.js +11 -10
  131. package/src/ui/page-handler-admin.js +32 -23
  132. package/src/ui/page-handler-helpers.js +27 -23
  133. package/src/ui/page-handler-reviews.js +71 -35
  134. package/src/ui/page-handler-rfi.js +28 -14
  135. package/src/ui/page-handler.js +65 -48
  136. package/src/ui/picker-dialogs.js +26 -19
  137. package/src/ui/render-helpers.js +84 -46
  138. package/src/ui/review-comparison-renderer.js +8 -7
  139. package/src/ui/review-detail-panels.js +8 -9
  140. package/src/ui/review-detail-renderer.js +3 -3
  141. package/src/ui/review-detail-script.js +13 -12
  142. package/src/ui/review-mwr-renderer.js +6 -5
  143. package/src/ui/review-renderer.js +7 -6
  144. package/src/ui/review-widgets.js +3 -3
  145. package/src/ui/rfi-detail-renderer.js +37 -20
  146. package/src/ui/rfi-report-renderer.js +14 -9
  147. package/src/ui/settings-renderer-advanced.js +21 -18
  148. package/src/ui/settings-renderer-advanced2.js +20 -19
  149. package/src/ui/settings-renderer-teams.js +4 -4
  150. package/src/ui/settings-renderer.js +4 -4
  151. package/src/ui/spacing-system.js +5 -2
  152. package/src/ui/standalone-login.js +2 -2
  153. package/src/ui/styles.css +2 -1
  154. package/src/ui/styles2.css +75 -5
  155. package/src/ui/test-page.js +4 -4
  156. package/src/ui/widgets.js +20 -15
  157. package/src/lib/audit-logger.js +0 -193
  158. package/src/lib/database-core.js +0 -255
  159. package/src/lib/database-migrations.js +0 -350
  160. package/src/lib/db-backup.js +0 -97
  161. package/src/lib/error-boundary.js +0 -134
  162. package/src/lib/errors.js +0 -69
  163. package/src/lib/health-monitor.js +0 -134
  164. package/src/lib/index-optimizer.js +0 -96
  165. package/src/lib/query-engine-write.js +0 -221
  166. package/src/lib/query-engine.js +0 -446
  167. package/src/ui/collaboration-dialogs.js +0 -31
  168. package/src/ui/global-tags.js +0 -144
  169. package/src/ui/idle-logout.js +0 -156
  170. package/src/ui/letter-dialogs.js +0 -37
  171. package/src/ui/password-reset-page.js +0 -146
  172. package/src/ui/perf-renderer.js +0 -71
  173. package/src/ui/rfi-renderer.js +0 -194
  174. package/src/ui/validation-ui.js +0 -147
package/README.md CHANGED
@@ -12,7 +12,7 @@ Thatcher is a complete extraction of the moonlanding application framework. Buil
12
12
  - **Plugin System** — Extend entities with hooks, validators, and custom fields
13
13
  - **External Integrations** — Google Drive/Gmail, Email (SMTP), PDF generation
14
14
  - **Hot Reload** — Config changes reflect instantly without restart
15
- - **Production-Ready** — SQLite, WAL mode, connection pooling, metrics, audit logging
15
+ - **Production-Ready** — busybase (LanceDB) document store, vector search, metrics, audit logging
16
16
 
17
17
  ## Quick Start
18
18
 
@@ -84,7 +84,7 @@ thresholds:
84
84
  ```
85
85
 
86
86
  That's it. Start the server and you have:
87
- - SQLite tables `item` with correct schema
87
+ - busybase `item` table (schemaless; created on first insert)
88
88
  - REST endpoints: `GET/POST/PUT/DELETE /api/item`
89
89
  - Role-based permissions enforced
90
90
  - State transitions via `POST /api/item/:id/transition`
@@ -143,7 +143,7 @@ thatcher/
143
143
  │ │ ├── env.js # Environment config
144
144
  │ │ └── constants.js # HTTP codes, statuses
145
145
  │ ├── lib/
146
- │ │ ├── database-core.js # SQLite init, migrations
146
+ │ │ ├── busybase-store.js # busybase data layer (async CRUD)
147
147
  │ │ ├── query-engine.js # Read operations (GET, search)
148
148
  │ │ ├── query-engine-write.js # Write operations (CRUD)
149
149
  │ │ ├── config-generator-engine.js # Spec builder
@@ -359,7 +359,7 @@ All APIs accessible via thatcher instance:
359
359
  | Variable | Purpose | Default |
360
360
  |----------|---------|---------|
361
361
  | `PORT` | Server port | 3000 |
362
- | `DATABASE_PATH` | SQLite file path | `./data/app.db` |
362
+ | `BUSYBASE_DIR` | busybase data directory | `busybase_data` |
363
363
  | `NODE_ENV` | `development` \| `production` | `development` |
364
364
  | `DEBUG` | Enable debug logging | `false` |
365
365
  | `GOOGLE_CLIENT_ID` | OAuth client ID | — |
@@ -370,18 +370,6 @@ All APIs accessible via thatcher instance:
370
370
  | `EMAIL_USER` | SMTP username | — |
371
371
  | `EMAIL_PASSWORD` | SMTP password | — |
372
372
 
373
- ## Migrating from Moonlanding
374
-
375
- Thatcher is a drop-in replacement for moonlanding's core:
376
-
377
- 1. Copy `master-config.yml` → `thatcher.config.yml`
378
- 2. Move any `.plugin.js` files to new project's `plugins/`
379
- 3. Change import paths: `@/lib/...` → `thatcher/...`
380
- 4. Replace `import ... from '@/engine'` with `thatcher.*` methods
381
- 5. Update server initialization: `new Thatcher({ config: '...' })`
382
-
383
- All business logic remains in config and plugins — no code changes required.
384
-
385
373
  ## Publishing
386
374
 
387
375
  Thatcher is published to npm as `thatcher`. Every push to `main` triggers:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "thatcher",
3
- "version": "1.0.7",
4
- "description": "A config-driven application framework for building data-intensive web apps without code. 100% feature-complete extraction from moonlanding.",
3
+ "version": "1.0.34",
4
+ "description": "A config-driven application framework for building data-intensive web apps without code.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
7
7
  "exports": {
@@ -15,8 +15,7 @@
15
15
  "./config/*.js": "./src/config/*.js",
16
16
  "./services/*": "./src/services/*.js",
17
17
  "./services/*.js": "./src/services/*.js",
18
- "./migration/*": "./src/migration/*.js",
19
- "./validation/*": "./src/validation/*.js",
18
+ "./validation/*": "./src/lib/validation/*.js",
20
19
  "./adapters/*": "./src/adapters/*.js",
21
20
  "./plugins/*": "./src/plugins/*.js",
22
21
  "./package.json": "./package.json"
@@ -28,7 +27,7 @@
28
27
  "scripts": {
29
28
  "dev": "bun run src/cli.js dev",
30
29
  "start": "bun run src/cli.js start",
31
- "test": "bun test",
30
+ "test": "node test.js",
32
31
  "lint": "eslint src/",
33
32
  "typecheck": "tsc --noEmit"
34
33
  },
@@ -41,32 +40,27 @@
41
40
  "workflow",
42
41
  "sdk",
43
42
  "bun",
44
- "sqlite"
43
+ "busybase"
45
44
  ],
46
- "author": "Extracted from moonlanding",
45
+ "author": "lanmower",
47
46
  "license": "MIT",
48
47
  "engines": {
49
48
  "node": ">=18.0.0",
50
49
  "bun": ">=1.0.0"
51
50
  },
52
51
  "dependencies": {
53
- "@casl/ability": "^6.8.0",
54
- "@lucia-auth/adapter-sqlite": "^3.0.2",
55
52
  "arctic": "^1.9.2",
56
- "bcrypt": "^5.1.1",
57
- "better-sqlite3": "^11.10.0",
58
- "compression": "^1.8.1",
59
- "googleapis": "^140.0.0",
53
+ "bcrypt": "^6.0.0",
54
+ "busybase": "^1.0.2",
55
+ "googleapis": "^173.0.0",
56
+ "hyperformula": "^3.2.0",
60
57
  "js-yaml": "^4.1.1",
61
58
  "lucia": "^3.2.2",
62
- "nanoid": "^5.0.7",
63
- "nodemailer": "^6.9.7",
64
- "puppeteer": "^21.6.1",
59
+ "nodemailer": "^9.0.1",
65
60
  "webjsx": "^0.0.73",
66
- "tsx": "^4.7.0"
61
+ "xstate": "^5.0.0"
67
62
  },
68
63
  "devDependencies": {
69
- "@types/better-sqlite3": "^7.6.11",
70
64
  "@types/node": "^20.16.5",
71
65
  "eslint": "^8.56.0",
72
66
  "typescript": "^5.3.3"
@@ -1,9 +1,7 @@
1
- /**
2
- * Google Auth Adapter - Factory for Google OAuth2 clients
3
- * Provides JWT and OAuth2 clients for Google APIs
4
- */
5
-
1
+ import { createLogger } from '../lib/logger.js';
6
2
  import { google } from 'googleapis';
3
+
4
+ const log = createLogger('[GoogleAuth]');
7
5
  import { buildConfig } from '../config/env.js';
8
6
  import fs from 'fs';
9
7
  import path from 'path';
@@ -11,10 +9,6 @@ import path from 'path';
11
9
  let _oauth2Client = null;
12
10
  let _jwtClient = null;
13
11
 
14
- /**
15
- * Initialize Google auth clients
16
- * @param {object} [config] - Optional config override
17
- */
18
12
  export function initGoogleAuth(config = null) {
19
13
  const cfg = config || buildConfig();
20
14
  const scopes = [
@@ -45,36 +39,23 @@ export function initGoogleAuth(config = null) {
45
39
  ],
46
40
  });
47
41
  } catch (err) {
48
- console.warn('[GoogleAuth] Failed to load service account:', err.message);
42
+ log.warn('failed to load service account:', { message: err.message });
49
43
  }
50
44
  }
51
45
 
52
46
  return { oauth2: _oauth2Client, jwt: _jwtClient };
53
47
  }
54
48
 
55
- /**
56
- * Get OAuth2 client
57
- * @returns {OAuth2Client|null}
58
- */
59
49
  export function getOAuth2Client() {
60
50
  if (!_oauth2Client) initGoogleAuth();
61
51
  return _oauth2Client;
62
52
  }
63
53
 
64
- /**
65
- * Get JWT client (service account)
66
- * @returns {JWT|null}
67
- */
68
54
  export function getJWTClient() {
69
55
  if (!_jwtClient) initGoogleAuth();
70
56
  return _jwtClient;
71
57
  }
72
58
 
73
- /**
74
- * Generate OAuth2 authorization URL
75
- * @param {object} options - state, access_type, prompt, etc.
76
- * @returns {string}
77
- */
78
59
  export function generateAuthUrl(options = {}) {
79
60
  const client = getOAuth2Client();
80
61
  if (!client) throw new Error('Google OAuth not configured');
@@ -92,11 +73,6 @@ export function generateAuthUrl(options = {}) {
92
73
  });
93
74
  }
94
75
 
95
- /**
96
- * Exchange code for tokens
97
- * @param {string} code
98
- * @returns {Promise<object>} tokens
99
- */
100
76
  export async function exchangeCode(code) {
101
77
  const client = getOAuth2Client();
102
78
  if (!client) throw new Error('Google OAuth not configured');
@@ -105,22 +81,12 @@ export async function exchangeCode(code) {
105
81
  return tokens;
106
82
  }
107
83
 
108
- /**
109
- * Get user info from Google
110
- * @param {string} accessToken
111
- * @returns {Promise<object>}
112
- */
113
84
  export async function getUserInfo(accessToken) {
114
85
  const oauth2 = google.oauth2({ version: 'v2', auth: `Bearer ${accessToken}` });
115
86
  const res = await oauth2.userinfo.get();
116
87
  return res.data;
117
88
  }
118
89
 
119
- /**
120
- * Verify ID token
121
- * @param {string} idToken
122
- * @returns {Promise<object>}
123
- */
124
90
  export async function verifyIdToken(idToken) {
125
91
  const client = getOAuth2Client();
126
92
  if (!client) throw new Error('Google OAuth not configured');
@@ -132,11 +98,6 @@ export async function verifyIdToken(idToken) {
132
98
  return ticket.getPayload();
133
99
  }
134
100
 
135
- /**
136
- * Create OAuth2 client with refresh token
137
- * @param {string} refreshToken
138
- * @returns {OAuth2Client}
139
- */
140
101
  export function createClientWithRefresh(refreshToken) {
141
102
  const client = new google.auth.OAuth2(
142
103
  buildConfig().auth.google.clientId,
@@ -1,8 +1,3 @@
1
- /**
2
- * Google Drive Adapter - File storage and manipulation
3
- * Upload, download, copy, export to PDF
4
- */
5
-
6
1
  import { google } from 'googleapis';
7
2
  import fs from 'fs';
8
3
  import path from 'path';
@@ -11,11 +6,6 @@ import { buildConfig } from '../config/env.js';
11
6
 
12
7
  const DRIVE_SCOPES = ['https://www.googleapis.com/auth/drive'];
13
8
 
14
- /**
15
- * Get authenticated Drive API client
16
- * @param {object} [user] - User with oauth_token, or null for service account
17
- * @returns {object}
18
- */
19
9
  export function getDriveClient(user = null) {
20
10
  let client;
21
11
 
@@ -33,14 +23,6 @@ export function getDriveClient(user = null) {
33
23
  return google.drive({ version: 'v3', auth: client });
34
24
  }
35
25
 
36
- /**
37
- * Upload a file to Google Drive
38
- * @param {string} filePath - Local file path
39
- * @param {string} name - File name in Drive
40
- * @param {object} [options] - { folderId, mimeType, parents }
41
- * @param {object} [user] - Optional user context
42
- * @returns {object} File metadata
43
- */
44
26
  export async function uploadFile(filePath, name, options = {}, user = null) {
45
27
  const drive = getDriveClient(user);
46
28
  const absPath = path.resolve(filePath);
@@ -61,13 +43,6 @@ export async function uploadFile(filePath, name, options = {}, user = null) {
61
43
  return res.data;
62
44
  }
63
45
 
64
- /**
65
- * Download file from Drive
66
- * @param {string} fileId
67
- * @param {string} [destPath] - If provided, write to file; else return buffer
68
- * @param {object} [user] - Optional user context
69
- * @returns {Buffer|string}
70
- */
71
46
  export async function downloadFile(fileId, destPath = null, user = null) {
72
47
  const drive = getDriveClient(user);
73
48
  const res = await drive.files.get(
@@ -87,14 +62,6 @@ export async function downloadFile(fileId, destPath = null, user = null) {
87
62
  return Buffer.from(res.data);
88
63
  }
89
64
 
90
- /**
91
- * Create a copy of a file in Drive
92
- * @param {string} fileId
93
- * @param {string} [newName]
94
- * @param {string} [folderId]
95
- * @param {object} [user]
96
- * @returns {object}
97
- */
98
65
  export async function copyFile(fileId, newName = null, folderId = null, user = null) {
99
66
  const drive = getDriveClient(user);
100
67
  const res = await drive.files.copy({
@@ -107,12 +74,6 @@ export async function copyFile(fileId, newName = null, folderId = null, user = n
107
74
  return res.data;
108
75
  }
109
76
 
110
- /**
111
- * Export Google Doc to PDF
112
- * @param {string} fileId
113
- * @param {object} [user]
114
- * @returns {Buffer}
115
- */
116
77
  export async function exportToPdf(fileId, user = null) {
117
78
  const drive = getDriveClient(user);
118
79
  const res = await drive.files.export(
@@ -122,28 +83,28 @@ export async function exportToPdf(fileId, user = null) {
122
83
  return Buffer.from(res.data);
123
84
  }
124
85
 
125
- /**
126
- * List files in folder
127
- * @param {string} [folderId]
128
- * @param {object} [query]
129
- * @param {object} [user]
130
- * @returns {Array}
131
- */
86
+ // Escape a value for inclusion in a single-quoted Google Drive query string.
87
+ // Drive Query Language escapes \ and ' with a backslash; without this an
88
+ // attacker-controlled name/id/mimeType could inject query operators.
89
+ function escapeGQLString(str) {
90
+ return String(str).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
91
+ }
92
+
132
93
  export async function listFiles(folderId = null, query = {}, user = null) {
133
94
  const drive = getDriveClient(user);
134
95
  const qParts = [];
135
96
 
136
97
  if (folderId) {
137
- qParts.push(`'${folderId}' in parents`);
98
+ qParts.push(`'${escapeGQLString(folderId)}' in parents`);
138
99
  }
139
100
  if (query.trashed === false) {
140
101
  qParts.push("trashed = false");
141
102
  }
142
103
  if (query.mimeType) {
143
- qParts.push(`mimeType = '${query.mimeType}'`);
104
+ qParts.push(`mimeType = '${escapeGQLString(query.mimeType)}'`);
144
105
  }
145
106
  if (query.name) {
146
- qParts.push(`name = '${query.name}'`);
107
+ qParts.push(`name = '${escapeGQLString(query.name)}'`);
147
108
  }
148
109
 
149
110
  const res = await drive.files.list({
@@ -155,36 +116,20 @@ export async function listFiles(folderId = null, query = {}, user = null) {
155
116
  return res.data.files || [];
156
117
  }
157
118
 
158
- /**
159
- * Delete file
160
- * @param {string} fileId
161
- * @param {object} [user]
162
- */
163
119
  export async function deleteFile(fileId, user = null) {
164
120
  const drive = getDriveClient(user);
165
121
  await drive.files.delete({ fileId });
166
122
  }
167
123
 
168
- /**
169
- * Get default root folder from config
170
- * @returns {string}
171
- */
172
124
  function getDefaultFolder() {
173
125
  const cfg = buildConfig();
174
126
  return cfg.drive.rootFolderId || 'root';
175
127
  }
176
128
 
177
- /**
178
- * Ensure folder exists, create if not
179
- * @param {string} name
180
- * @param {string} [parentId]
181
- * @param {object} [user]
182
- * @returns {string} folderId
183
- */
184
129
  export async function ensureFolder(name, parentId = null, user = null) {
185
130
  const drive = getDriveClient(user);
186
- const queryParts = [`name = '${name}'`, "mimeType = 'application/vnd.google-apps.folder'"];
187
- if (parentId) queryParts.push(`'${parentId}' in parents`);
131
+ const queryParts = [`name = '${escapeGQLString(name)}'`, "mimeType = 'application/vnd.google-apps.folder'"];
132
+ if (parentId) queryParts.push(`'${escapeGQLString(parentId)}' in parents`);
188
133
 
189
134
  const res = await drive.files.list({
190
135
  q: queryParts.join(' AND '),
@@ -1,9 +1,12 @@
1
1
  import { NextResponse } from '@/lib/next-polyfills';
2
+ import { createLogger } from '@/lib/logger.js';
3
+
4
+ const log = createLogger('[AuditDashboardAPI]');
2
5
  import { getUser, setCurrentRequest } from '@/engine.server';
3
6
  import {
4
7
  getPermissionAuditTrail, getPermissionAuditStats, getPermissionAuditBreakdown,
5
8
  getPermissionAuditByDateRange, searchPermissionAudit, getPermissionDiff,
6
- } from '@/lib/audit-logger';
9
+ } from '@/lib/busybase-audit-reads';
7
10
 
8
11
  export async function GET(request) {
9
12
  setCurrentRequest(request);
@@ -28,10 +31,10 @@ export async function GET(request) {
28
31
  let data;
29
32
  switch (view) {
30
33
  case 'summary': {
31
- const stats = getPermissionAuditStats();
32
- const actionBreakdown = getPermissionAuditBreakdown('action');
33
- const reasonBreakdown = getPermissionAuditBreakdown('reason_code');
34
- const changes = getPermissionAuditByDateRange(startTs, nowTs, limit);
34
+ const stats = await getPermissionAuditStats();
35
+ const actionBreakdown = await getPermissionAuditBreakdown('action');
36
+ const reasonBreakdown = await getPermissionAuditBreakdown('reason_code');
37
+ const changes = await getPermissionAuditByDateRange(startTs, nowTs, limit);
35
38
  data = {
36
39
  period_days: days,
37
40
  total_changes: changes.length,
@@ -42,17 +45,17 @@ export async function GET(request) {
42
45
  break;
43
46
  }
44
47
  case 'recent':
45
- data = { period: `Last ${days} days`, changes: getPermissionAuditByDateRange(startTs, nowTs, limit) };
48
+ data = { period: `Last ${days} days`, changes: await getPermissionAuditByDateRange(startTs, nowTs, limit) };
46
49
  break;
47
50
  case 'user':
48
51
  if (!userId) return NextResponse.json({ error: 'user_id required' }, { status: 400 });
49
- data = { user_id: userId, changes: getPermissionAuditTrail({ userId, limit }) };
52
+ data = { user_id: userId, changes: await getPermissionAuditTrail({ userId, limit }) };
50
53
  break;
51
54
  case 'entity':
52
55
  if (!entityType || !entityId) return NextResponse.json({ error: 'entity_type and entity_id required' }, { status: 400 });
53
56
  data = {
54
57
  entity_type: entityType, entity_id: entityId,
55
- timeline: getPermissionAuditTrail({ entityType, entityId, limit }).map(c => ({
58
+ timeline: (await getPermissionAuditTrail({ entityType, entityId, limit })).map(c => ({
56
59
  ...c, timestamp_iso: new Date(c.timestamp * 1000).toISOString(),
57
60
  diff: c.old_permissions && c.new_permissions ? getPermissionDiff(c.old_permissions, c.new_permissions) : null,
58
61
  })),
@@ -60,10 +63,10 @@ export async function GET(request) {
60
63
  break;
61
64
  case 'search':
62
65
  if (!searchTerm) return NextResponse.json({ error: 'search parameter required' }, { status: 400 });
63
- data = { search_term: searchTerm, results: searchPermissionAudit(searchTerm, limit) };
66
+ data = { search_term: searchTerm, results: await searchPermissionAudit(searchTerm, limit) };
64
67
  break;
65
68
  case 'roles':
66
- data = { changes: getPermissionAuditTrail({ limit }).filter(c => c.action === 'role_change') };
69
+ data = { changes: (await getPermissionAuditTrail({ limit })).filter(c => c.action === 'role_change') };
67
70
  break;
68
71
  default:
69
72
  return NextResponse.json({ error: `Unknown view: ${view}` }, { status: 400 });
@@ -71,7 +74,7 @@ export async function GET(request) {
71
74
 
72
75
  return NextResponse.json({ success: true, view, data });
73
76
  } catch (error) {
74
- console.error('[AuditDashboardAPI] GET error:', error);
77
+ log.error('GET error:', { message: error.message });
75
78
  return NextResponse.json({ error: 'Failed to retrieve audit dashboard data', details: error.message }, { status: 500 });
76
79
  }
77
80
  }
@@ -25,7 +25,7 @@ export const GET = withErrorHandler(async (request) => {
25
25
  const page = parseInt(url.searchParams.get('page') || '1');
26
26
  const pageSize = parseInt(url.searchParams.get('pageSize') || '100');
27
27
 
28
- const result = searchLogs(filters, page, pageSize);
28
+ const result = await searchLogs(filters, page, pageSize);
29
29
  return NextResponse.json(result);
30
30
  });
31
31
 
@@ -38,7 +38,7 @@ export const POST = withErrorHandler(async (request) => {
38
38
  const body = await request.json();
39
39
  if (body.action === 'rotate') {
40
40
  const daysOld = body.daysOld || 90;
41
- const result = rotateLogsOlderThan(daysOld);
41
+ const result = await rotateLogsOlderThan(daysOld);
42
42
  return NextResponse.json({ success: true, archived: result.archived, archiveId: result.archiveId });
43
43
  }
44
44
 
@@ -1,6 +1,9 @@
1
1
  import { NextResponse, cookies } from '@/lib/next-polyfills';
2
+ import { createLogger } from '@/lib/logger.js';
3
+
4
+ const log = createLogger('[AuditDetailAPI]');
2
5
  import { lucia } from '@/engine.server';
3
- import { getPermissionAuditById, getPermissionDiff } from '@/lib/audit-logger';
6
+ import { getPermissionAuditById, getPermissionDiff } from '@/lib/busybase-audit-reads';
4
7
 
5
8
  async function getUser() {
6
9
  const sessionId = (await cookies()).get(lucia.sessionCookieName)?.value ?? null;
@@ -18,7 +21,7 @@ export async function GET(request, context) {
18
21
  const auditId = params.id;
19
22
  if (!auditId) return NextResponse.json({ error: 'Audit ID is required' }, { status: 400 });
20
23
 
21
- const audit = getPermissionAuditById(auditId);
24
+ const audit = await getPermissionAuditById(auditId);
22
25
  if (!audit) return NextResponse.json({ error: 'Audit record not found' }, { status: 404 });
23
26
 
24
27
  if (user.role === 'clerk' && audit.user_id !== user.id) return NextResponse.json({ error: 'Permission denied' }, { status: 403 });
@@ -31,7 +34,7 @@ export async function GET(request, context) {
31
34
  data: { ...audit, timestamp_iso: new Date(audit.timestamp * 1000).toISOString(), diff },
32
35
  });
33
36
  } catch (error) {
34
- console.error('[AuditDetailAPI] GET error:', error);
37
+ log.error('GET error:', { message: error.message });
35
38
  return NextResponse.json({ error: 'Failed to retrieve audit record', details: error.message }, { status: 500 });
36
39
  }
37
40
  }
@@ -1,10 +1,13 @@
1
1
  import { NextResponse } from '@/lib/next-polyfills';
2
+ import { createLogger } from '@/lib/logger.js';
3
+
4
+ const log = createLogger('[AuditAPI]');
2
5
  import { lucia } from '@/engine.server';
3
6
  import { cookies } from '@/lib/next-polyfills';
4
7
  import {
5
8
  logPermissionChange, getPermissionAuditTrail, searchPermissionAudit,
6
9
  getPermissionAuditByDateRange, exportPermissionAuditCSV,
7
- } from '@/lib/audit-logger';
10
+ } from '@/lib/busybase-audit-reads';
8
11
 
9
12
  async function getUser() {
10
13
  const sessionId = (await cookies()).get(lucia.sessionCookieName)?.value ?? null;
@@ -37,9 +40,9 @@ export async function GET(request) {
37
40
 
38
41
  let results;
39
42
  if (search) {
40
- results = searchPermissionAudit(search, limit);
43
+ results = await searchPermissionAudit(search, limit);
41
44
  } else if (startDate && endDate) {
42
- results = getPermissionAuditByDateRange(parseInt(startDate, 10), parseInt(endDate, 10), limit);
45
+ results = await getPermissionAuditByDateRange(parseInt(startDate, 10), parseInt(endDate, 10), limit);
43
46
  } else {
44
47
  const filters = {};
45
48
  const entityType = searchParams.get('entity_type');
@@ -53,7 +56,7 @@ export async function GET(request) {
53
56
  if (affectedUserId) filters.affectedUserId = affectedUserId;
54
57
  filters.limit = limit;
55
58
  filters.offset = offset;
56
- results = getPermissionAuditTrail(filters);
59
+ results = await getPermissionAuditTrail(filters);
57
60
  }
58
61
 
59
62
  if (format === 'csv') {
@@ -63,7 +66,7 @@ export async function GET(request) {
63
66
 
64
67
  return NextResponse.json({ success: true, data: results, count: results.length, scope: permCheck.scope });
65
68
  } catch (error) {
66
- console.error('[AuditAPI] GET error:', error);
69
+ log.error('GET error:', { message: error.message });
67
70
  return NextResponse.json({ error: 'Failed to retrieve audit trail', details: error.message }, { status: 500 });
68
71
  }
69
72
  }
@@ -80,14 +83,14 @@ export async function POST(request) {
80
83
  const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || null;
81
84
  const sessionId = (await cookies()).get(lucia.sessionCookieName)?.value ?? null;
82
85
 
83
- const auditId = logPermissionChange({
86
+ const auditId = await logPermissionChange({
84
87
  userId: user.id, entityType, entityId, action, oldPermissions, newPermissions,
85
88
  reason, reasonCode: reasonCode || 'other', affectedUserId, ipAddress, sessionId, metadata,
86
89
  });
87
90
 
88
91
  return NextResponse.json({ success: true, auditId });
89
92
  } catch (error) {
90
- console.error('[AuditAPI] POST error:', error);
93
+ log.error('POST error:', { message: error.message });
91
94
  return NextResponse.json({ error: 'Failed to log permission change', details: error.message }, { status: 500 });
92
95
  }
93
96
  }
@@ -1,6 +1,9 @@
1
1
  import { NextResponse } from '@/lib/next-polyfills';
2
+ import { createLogger } from '@/lib/logger.js';
3
+
4
+ const log = createLogger('[AuditStatsAPI]');
2
5
  import { getUser, setCurrentRequest } from '@/engine.server';
3
- import { getPermissionAuditStats, getPermissionAuditBreakdown } from '@/lib/audit-logger';
6
+ import { getPermissionAuditStats, getPermissionAuditBreakdown } from '@/lib/busybase-audit-reads';
4
7
 
5
8
  export async function GET(request) {
6
9
  setCurrentRequest(request);
@@ -9,9 +12,9 @@ export async function GET(request) {
9
12
  if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
10
13
  if (user.role !== 'partner' && user.role !== 'manager') return NextResponse.json({ error: 'Permission denied' }, { status: 403 });
11
14
 
12
- const stats = getPermissionAuditStats();
13
- const actionBreakdown = getPermissionAuditBreakdown('action');
14
- const reasonCodeBreakdown = getPermissionAuditBreakdown('reason_code');
15
+ const stats = await getPermissionAuditStats();
16
+ const actionBreakdown = await getPermissionAuditBreakdown('action');
17
+ const reasonCodeBreakdown = await getPermissionAuditBreakdown('reason_code');
15
18
 
16
19
  return NextResponse.json({
17
20
  success: true,
@@ -23,7 +26,7 @@ export async function GET(request) {
23
26
  breakdown: { by_action: actionBreakdown, by_reason_code: reasonCodeBreakdown },
24
27
  });
25
28
  } catch (error) {
26
- console.error('[AuditStatsAPI] GET error:', error);
29
+ log.error('GET error:', { message: error.message });
27
30
  return NextResponse.json({ error: 'Failed to retrieve audit statistics', details: error.message }, { status: 500 });
28
31
  }
29
32
  }
@@ -1,5 +1,5 @@
1
1
  import { requireAuth, requirePermission } from '@/lib/auth-middleware';
2
- import { getAuditHistory, getActionStats, getUserStats } from '@/lib/audit-logger';
2
+ import { getAuditHistory, getActionStats, getUserStats } from '@/lib/busybase-audit-reads';
3
3
  import { getSpec } from '@/config/spec-helpers';
4
4
  import { paginated, ok } from '@/lib/response-formatter';
5
5
  import { withErrorHandler } from '@/lib/with-error-handler';
@@ -32,8 +32,8 @@ export const GET = withErrorHandler(async (request) => {
32
32
  if (!fromDate || !toDate) {
33
33
  throw new AppError('fromDate and toDate required for stats', 'BAD_REQUEST', HTTP.BAD_REQUEST);
34
34
  }
35
- const actionStats = getActionStats(fromDate, toDate);
36
- const userStats = getUserStats(fromDate, toDate);
35
+ const actionStats = await getActionStats(fromDate, toDate);
36
+ const userStats = await getUserStats(fromDate, toDate);
37
37
  return ok({
38
38
  actionStats,
39
39
  userStats
@@ -48,7 +48,7 @@ export const GET = withErrorHandler(async (request) => {
48
48
  if (fromDate) filters.fromDate = fromDate;
49
49
  if (toDate) filters.toDate = toDate;
50
50
 
51
- const { items, pagination } = getAuditHistory(filters, page, pageSize);
51
+ const { items, pagination } = await getAuditHistory(filters, page, pageSize);
52
52
 
53
53
  const enriched = items.map(log => ({
54
54
  ...log,
@@ -13,6 +13,6 @@ export const GET = withErrorHandler(async (request) => {
13
13
  const fromDate = url.searchParams.get('from') ? parseInt(url.searchParams.get('from')) : Math.floor(Date.now() / 1000) - (24 * 60 * 60);
14
14
  const toDate = url.searchParams.get('to') ? parseInt(url.searchParams.get('to')) : Math.floor(Date.now() / 1000);
15
15
 
16
- const stats = getLogStats(fromDate, toDate);
16
+ const stats = await getLogStats(fromDate, toDate);
17
17
  return NextResponse.json({ success: true, stats, period: { from: fromDate, to: toDate } });
18
18
  });
@@ -1,4 +1,7 @@
1
+ import { createLogger } from '@/lib/logger.js';
1
2
  import { google, createSession } from '@/engine.server';
3
+
4
+ const log = createLogger('[OAuth]');
2
5
  import { getBy, create } from '@/engine';
3
6
  import { Google } from 'arctic';
4
7
  import { GOOGLE_APIS } from '@/config/constants';
@@ -40,7 +43,7 @@ export async function GET(request) {
40
43
 
41
44
  const stateValidation = validateOAuthState(code, stateKey, state, codeVerifier);
42
45
  if (!stateValidation.valid) {
43
- console.error('[OAuth Callback] State validation failed:', stateValidation.error);
46
+ log.warn('state validation failed:', { error: stateValidation.error });
44
47
  return buildOAuthErrorResponse(stateValidation.error, request);
45
48
  }
46
49
 
@@ -88,7 +91,7 @@ export async function GET(request) {
88
91
 
89
92
  return response;
90
93
  } catch (error) {
91
- console.error('Google OAuth error:', error);
94
+ log.error('Google OAuth error:', { message: error.message });
92
95
  return buildOAuthErrorResponse('oauth_failed', request);
93
96
  }
94
97
  }