thatcher 1.0.41 → 1.0.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +27 -0
  2. package/package.json +2 -5
  3. package/src/app/api/audit/dashboard/route.js +2 -2
  4. package/src/app/api/audit/logs/route.js +2 -2
  5. package/src/app/api/audit/permissions/[id]/route.js +2 -2
  6. package/src/app/api/audit/permissions/route.js +3 -3
  7. package/src/app/api/audit/permissions/stats/route.js +2 -2
  8. package/src/app/api/audit/route.js +2 -3
  9. package/src/app/api/audit/stats/route.js +2 -2
  10. package/src/app/api/auth/google/callback/route.js +1 -1
  11. package/src/app/api/auth/google/route.js +1 -1
  12. package/src/app/api/auth/login/route.js +1 -1
  13. package/src/app/api/auth/logout/route.js +1 -1
  14. package/src/app/api/auth/me/route.js +1 -1
  15. package/src/app/api/auth/mwr-bridge/route.js +1 -1
  16. package/src/app/api/auth/password-reset/route.js +1 -1
  17. package/src/app/api/csrf-token/route.js +1 -1
  18. package/src/app/api/debug/[[...path]]/route.js +2 -2
  19. package/src/app/api/debug/config/route.js +1 -1
  20. package/src/app/api/debug/hooks/route.js +1 -1
  21. package/src/app/api/debug/plugins/route.js +1 -1
  22. package/src/app/api/debug/sqlite/route.js +1 -1
  23. package/src/app/api/debug/sync/route.js +1 -1
  24. package/src/app/api/debug/workflow/route.js +1 -1
  25. package/src/app/api/domains/[domain]/route.js +1 -2
  26. package/src/app/api/domains/route.js +1 -1
  27. package/src/app/api/email/allocate/batch/route.js +1 -1
  28. package/src/app/api/email/allocate/route.js +1 -1
  29. package/src/app/api/email/receive/route.js +1 -1
  30. package/src/app/api/email/send/route.js +1 -1
  31. package/src/app/api/email/unallocated/route.js +1 -1
  32. package/src/app/api/files/[id]/route.js +1 -1
  33. package/src/app/api/health/route.js +1 -2
  34. package/src/app/api/metrics/route.js +1 -2
  35. package/src/engine.js +3 -3
  36. package/src/engine.server.js +3 -3
  37. package/src/index.js +35 -10
  38. package/src/lib/action-factory.js +1 -1
  39. package/src/lib/api-helpers.js +1 -1
  40. package/src/lib/api.js +3 -4
  41. package/src/lib/audit-logger-enhanced.js +1 -1
  42. package/src/lib/auth-middleware.js +1 -1
  43. package/src/lib/auth-route-helpers.js +1 -1
  44. package/src/lib/{busybase-adapter.js → busybase/adapter.js} +3 -3
  45. package/src/lib/{busybase-audit-reads.js → busybase/audit-reads.js} +1 -1
  46. package/src/lib/{busybase-audit.js → busybase/audit.js} +2 -2
  47. package/src/lib/{busybase-lucia-adapter.js → busybase/lucia-adapter.js} +1 -1
  48. package/src/lib/{busybase-store.js → busybase/store.js} +3 -3
  49. package/src/lib/config-generator-engine.js +13 -1
  50. package/src/lib/crud-action-helpers.js +1 -1
  51. package/src/lib/crud-handlers.js +4 -4
  52. package/src/lib/email-sender.js +1 -1
  53. package/src/lib/errors/index.js +3 -0
  54. package/src/lib/{error-recovery.js → errors/recovery.js} +120 -4
  55. package/src/lib/{error-handler.js → errors/types.js} +1 -1
  56. package/src/lib/errors/wrap.js +225 -0
  57. package/src/lib/events-engine.js +1 -1
  58. package/src/lib/hot-reload/index.js +34 -0
  59. package/src/lib/index.js +5 -5
  60. package/src/lib/keyed-cache.js +112 -0
  61. package/src/lib/monitor.js +285 -0
  62. package/src/lib/next-shim.js +294 -0
  63. package/src/lib/observability-init.js +213 -0
  64. package/src/lib/perf.js +468 -0
  65. package/src/lib/query-cache.js +40 -46
  66. package/src/lib/realtime-server.js +18 -0
  67. package/src/lib/render-cache.js +14 -28
  68. package/src/lib/{request-tracing.js → request-trace.js} +57 -2
  69. package/src/lib/response-formatter.js +18 -0
  70. package/src/lib/route-helpers.js +1 -1
  71. package/src/lib/state-protocol.js +13 -0
  72. package/src/lib/state-transport-client.js +6 -0
  73. package/src/lib/state-transport-reconnect.js +5 -0
  74. package/src/lib/state-transport-server.js +8 -0
  75. package/src/lib/utils.js +1 -1
  76. package/src/lib/{validate.js → validation/entity-validators.js} +9 -8
  77. package/src/lib/validation/index.js +1 -1
  78. package/src/lib/validation/security-validators.js +1 -1
  79. package/src/lib/validation-middleware.js +2 -2
  80. package/src/lib/workflow-engine.js +23 -3
  81. package/src/lib/xstate-workflow-engine.js +18 -2
  82. package/src/services/collaborator-role.service.js +2 -2
  83. package/src/services/notification-engine.js +5 -5
  84. package/src/services/permission.service.js +1 -1
  85. package/src/ui/format-helpers.js +1 -4
  86. package/src/ui/highlight-threading-renderer.js +1 -1
  87. package/src/ui/page-handler-admin.js +6 -5
  88. package/src/ui/page-handler-helpers.js +2 -2
  89. package/src/ui/page-handler-reviews.js +4 -4
  90. package/src/ui/page-handler-rfi.js +1 -1
  91. package/src/ui/page-handler.js +2 -2
  92. package/src/ui/render-helpers.js +1 -1
  93. package/src/ui/renderer.js +1 -1
  94. package/src/ui/{review-comparison-renderer.js → review/comparison.js} +1 -1
  95. package/src/ui/{review-detail-renderer.js → review/detail-renderer.js} +3 -3
  96. package/src/ui/review/index.js +32 -0
  97. package/src/ui/{review-mwr-renderer.js → review/mwr.js} +1 -1
  98. package/src/ui/{review-renderer.js → review/renderer.js} +1 -1
  99. package/src/ui/{settings-renderer.js → settings/home.js} +7 -34
  100. package/src/ui/{settings-renderer-advanced.js → settings/review.js} +117 -56
  101. package/src/ui/settings/shared.js +45 -0
  102. package/src/ui/{settings-renderer-teams.js → settings/teams.js} +1 -2
  103. package/src/ui/settings/templates.js +116 -0
  104. package/src/lib/api-error-wrapper.js +0 -125
  105. package/src/lib/db-monitor.js +0 -127
  106. package/src/lib/error-resilience.js +0 -132
  107. package/src/lib/monitoring-init.js +0 -67
  108. package/src/lib/next-compat.js +0 -94
  109. package/src/lib/next-polyfills.js +0 -135
  110. package/src/lib/observability-bootstrap.js +0 -116
  111. package/src/lib/perf-monitor.js +0 -94
  112. package/src/lib/perf-profiler.js +0 -233
  113. package/src/lib/query-perf.js +0 -117
  114. package/src/lib/request-tracker.js +0 -43
  115. package/src/lib/resource-monitor.js +0 -120
  116. package/src/lib/validators.js +0 -67
  117. package/src/lib/with-error-handler.js +0 -31
  118. package/src/ui/settings-renderer-advanced2.js +0 -159
  119. /package/src/ui/{review-detail-panels.js → review/detail-panels.js} +0 -0
  120. /package/src/ui/{review-detail-script.js → review/detail-script.js} +0 -0
  121. /package/src/ui/{review-widgets.js → review/widgets.js} +0 -0
  122. /package/src/ui/{review-zone-nav.js → review/zone-nav.js} +0 -0
package/README.md CHANGED
@@ -380,6 +380,33 @@ Thatcher is published to npm as `thatcher`. Every push to `main` triggers:
380
380
 
381
381
  CI/CD ready via included GitHub Actions workflow.
382
382
 
383
+ ## Runtime: Bun vs Node
384
+
385
+ Thatcher targets Bun as its primary runtime (`engines.bun` in `package.json`,
386
+ a `bun.lock` in the repo root, and `src/cli.js` carries a `#!/usr/bin/env bun`
387
+ shebang), but most of the codebase is plain ES modules with no Bun-only API
388
+ calls, so a Node-only consumer can run everything except the two `bun run`
389
+ scripts as-is.
390
+
391
+ | Script | Command | Runtime |
392
+ |--------|---------|---------|
393
+ | `npm run dev` | `bun run src/cli.js dev` | **Bun required** — invoked via `bun run`; `thatcher dev` also enables hot reload (`server.hotReload: true`), and the CLI's own shebang is `#!/usr/bin/env bun`. |
394
+ | `npm run start` | `bun run src/cli.js start` | **Bun required** — same `bun run` invocation as `dev`, just without hot reload. |
395
+ | `npm test` | `node test.js` | Plain Node — runs fine under `node test.js` directly. |
396
+ | `npm run lint` | `eslint src/` | Tool-agnostic — standard ESLint CLI, no runtime dependency. |
397
+ | `npm run typecheck` | `tsc --noEmit` | Tool-agnostic — standard TypeScript CLI. |
398
+
399
+ In practice, `src/cli.js` itself has no Bun-only API calls (no `Bun.*`
400
+ globals) — the `bun run` wrapping in `dev`/`start` is a process-launcher
401
+ choice, not a hard code dependency, so `node src/cli.js start` / `node
402
+ src/cli.js dev` also work for a Node-only deployer. The one place Bun is
403
+ harder to avoid is the `busybase` dependency, whose published `src/*.js` are
404
+ bun-build outputs (per its own build process) rather than hand-authored
405
+ Node-targeted files. A Node-only consumer/deployer can therefore run
406
+ `test`/`lint`/`typecheck` and the CLI itself with plain `node`, but should
407
+ expect the `dev`/`start` npm scripts as published to require Bun on `PATH`
408
+ unless invoking `src/cli.js` directly with `node`.
409
+
383
410
  ## License
384
411
 
385
412
  MIT — Extracted from moonlanding with all functionality preserved.
package/package.json CHANGED
@@ -1,16 +1,13 @@
1
1
  {
2
2
  "name": "thatcher",
3
- "version": "1.0.41",
3
+ "version": "1.0.43",
4
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
+ "_exportsCuratedNote": "This map is intentionally curated, not a blanket ./lib/* + ./ui/* re-export of every internal file. Rationale (2026-07): grepping thatcher's own source found zero internal self-imports via the 'thatcher/lib/*' or 'thatcher/ui/*' package specifier (internal code uses relative paths / resolveModule file URLs instead), and grepping casey (the primary real external consumer, at ../casey) found it imports only the package root ('thatcher' -> createThatcher). README.md documents exactly two public entry shapes: the root ('thatcher') and 'thatcher/adapters/google-drive'. './engine.server' (auth) was already a named, deliberate entry point distinct from the internal lib dump. './config/*', './services/*', './validation/*', './plugins/*' are kept even though no consumer was evidenced, because they are small, purposeful, plausibly-public directories (plugin-authoring in particular mirrors casey's own plugins/case-tools pattern) and erring toward NOT breaking an unseen consumer outweighs the surface-area savings of dropping them. The wide-open './lib/*' and './ui/*' globs (70+ and 40+ internal implementation files respectively, covering error handling, rendering internals, busybase adapters, etc.) were removed because nothing -- internal or external -- was found to reference them via the package-specifier form; any of thatcher's own modules that genuinely need to reach another lib file already do so via a relative import, which this map does not gate. If a real external consumer is later found needing a specific lib/ui file, add that ONE subpath explicitly rather than reopening the blanket glob.",
7
8
  "exports": {
8
9
  ".": "./src/index.js",
9
10
  "./engine.server": "./src/engine.server.js",
10
- "./lib/*": "./src/lib/*.js",
11
- "./lib/*.js": "./src/lib/*.js",
12
- "./ui/*": "./src/ui/*.js",
13
- "./ui/*.js": "./src/ui/*.js",
14
11
  "./config/*": "./src/config/*.js",
15
12
  "./config/*.js": "./src/config/*.js",
16
13
  "./services/*": "./src/services/*.js",
@@ -1,4 +1,4 @@
1
- import { NextResponse } from '@/lib/next-polyfills';
1
+ import { NextResponse } from '@/lib/next-shim';
2
2
  import { createLogger } from '@/lib/logger.js';
3
3
 
4
4
  const log = createLogger('[AuditDashboardAPI]');
@@ -6,7 +6,7 @@ import { getUser, setCurrentRequest } from '@/engine.server';
6
6
  import {
7
7
  getPermissionAuditTrail, getPermissionAuditStats, getPermissionAuditBreakdown,
8
8
  getPermissionAuditByDateRange, searchPermissionAudit, getPermissionDiff,
9
- } from '@/lib/busybase-audit-reads';
9
+ } from '@/lib/busybase/audit-reads';
10
10
 
11
11
  export async function GET(request) {
12
12
  setCurrentRequest(request);
@@ -1,7 +1,7 @@
1
- import { NextResponse } from '@/lib/next-polyfills';
1
+ import { NextResponse } from '@/lib/next-shim';
2
2
  import { requireAuth } from '@/lib/auth-middleware';
3
3
  import { searchLogs, getLogStats, rotateLogsOlderThan } from '@/lib/audit-logger-enhanced';
4
- import { withErrorHandler } from '@/lib/with-error-handler';
4
+ import { withErrorHandler } from '@/lib/errors';
5
5
 
6
6
  export const GET = withErrorHandler(async (request) => {
7
7
  const user = await requireAuth();
@@ -1,9 +1,9 @@
1
- import { NextResponse, cookies } from '@/lib/next-polyfills';
1
+ import { NextResponse, cookies } from '@/lib/next-shim';
2
2
  import { createLogger } from '@/lib/logger.js';
3
3
 
4
4
  const log = createLogger('[AuditDetailAPI]');
5
5
  import { lucia } from '@/engine.server';
6
- import { getPermissionAuditById, getPermissionDiff } from '@/lib/busybase-audit-reads';
6
+ import { getPermissionAuditById, getPermissionDiff } from '@/lib/busybase/audit-reads';
7
7
 
8
8
  async function getUser() {
9
9
  const sessionId = (await cookies()).get(lucia.sessionCookieName)?.value ?? null;
@@ -1,13 +1,13 @@
1
- import { NextResponse } from '@/lib/next-polyfills';
1
+ import { NextResponse } from '@/lib/next-shim';
2
2
  import { createLogger } from '@/lib/logger.js';
3
3
 
4
4
  const log = createLogger('[AuditAPI]');
5
5
  import { lucia } from '@/engine.server';
6
- import { cookies } from '@/lib/next-polyfills';
6
+ import { cookies } from '@/lib/next-shim';
7
7
  import {
8
8
  logPermissionChange, getPermissionAuditTrail, searchPermissionAudit,
9
9
  getPermissionAuditByDateRange, exportPermissionAuditCSV,
10
- } from '@/lib/busybase-audit-reads';
10
+ } from '@/lib/busybase/audit-reads';
11
11
 
12
12
  async function getUser() {
13
13
  const sessionId = (await cookies()).get(lucia.sessionCookieName)?.value ?? null;
@@ -1,9 +1,9 @@
1
- import { NextResponse } from '@/lib/next-polyfills';
1
+ import { NextResponse } from '@/lib/next-shim';
2
2
  import { createLogger } from '@/lib/logger.js';
3
3
 
4
4
  const log = createLogger('[AuditStatsAPI]');
5
5
  import { getUser, setCurrentRequest } from '@/engine.server';
6
- import { getPermissionAuditStats, getPermissionAuditBreakdown } from '@/lib/busybase-audit-reads';
6
+ import { getPermissionAuditStats, getPermissionAuditBreakdown } from '@/lib/busybase/audit-reads';
7
7
 
8
8
  export async function GET(request) {
9
9
  setCurrentRequest(request);
@@ -1,9 +1,8 @@
1
1
  import { requireAuth, requirePermission } from '@/lib/auth-middleware';
2
- import { getAuditHistory, getActionStats, getUserStats } from '@/lib/busybase-audit-reads';
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
- import { withErrorHandler } from '@/lib/with-error-handler';
6
- import { AppError } from '@/lib/error-handler';
5
+ import { withErrorHandler, AppError } from '@/lib/errors';
7
6
  import { HTTP } from '@/config/constants';
8
7
  import { parse as parseQuery } from '@/lib/query-string-adapter';
9
8
 
@@ -1,7 +1,7 @@
1
- import { NextResponse } from '@/lib/next-polyfills';
1
+ import { NextResponse } from '@/lib/next-shim';
2
2
  import { requireAuth } from '@/lib/auth-middleware';
3
3
  import { getLogStats } from '@/lib/audit-logger-enhanced';
4
- import { withErrorHandler } from '@/lib/with-error-handler';
4
+ import { withErrorHandler } from '@/lib/errors';
5
5
 
6
6
  export const GET = withErrorHandler(async (request) => {
7
7
  const user = await requireAuth();
@@ -8,7 +8,7 @@ import { GOOGLE_APIS } from '@/config/constants';
8
8
  import { config } from '@/config';
9
9
  import { getConfigEngine } from '@/lib/config-generator-engine';
10
10
  import { globalManager } from '@/lib/hot-reload/mutex';
11
- import { NextResponse } from '@/lib/next-polyfills';
11
+ import { NextResponse } from '@/lib/next-shim';
12
12
  import {
13
13
  validateOAuthProvider,
14
14
  getOAuthCookie,
@@ -1,4 +1,4 @@
1
- import { NextResponse } from '@/lib/next-polyfills';
1
+ import { NextResponse } from '@/lib/next-shim';
2
2
  import { createLogger } from '@/lib/logger.js';
3
3
 
4
4
  const log = createLogger('[OAuth]');
@@ -3,7 +3,7 @@ import { getBy, verifyPassword, migrate } from '@/engine';
3
3
 
4
4
  const log = createLogger('[Login]');
5
5
  import { initializeSystemConfig } from '@/config/system-config-loader';
6
- import { withErrorHandler } from '@/lib/with-error-handler';
6
+ import { withErrorHandler } from '@/lib/errors';
7
7
  import { lucia } from '@/engine.server';
8
8
 
9
9
  let initialized = false;
@@ -1,5 +1,5 @@
1
1
  import { setCurrentRequest as setEngineRequest, invalidateSession } from '@/engine.server';
2
- import { setCurrentRequest, setCurrentResponse } from '@/lib/next-polyfills';
2
+ import { setCurrentRequest, setCurrentResponse } from '@/lib/next-shim';
3
3
 
4
4
  function buildLogoutHeaders(headerMap) {
5
5
  const setCookies = headerMap.get('Set-Cookie');
@@ -1,4 +1,4 @@
1
- import { NextResponse } from '@/lib/next-polyfills';
1
+ import { NextResponse } from '@/lib/next-shim';
2
2
  import { getUser, setCurrentRequest } from '@/engine.server';
3
3
  import { HTTP } from '@/config/constants';
4
4
 
@@ -1,7 +1,7 @@
1
1
  import { getBy, list, update } from '@/engine';
2
2
  import { lucia } from '@/engine.server';
3
3
  import { now } from '@/lib/id-helpers';
4
- import { withErrorHandler } from '@/lib/with-error-handler';
4
+ import { withErrorHandler } from '@/lib/errors';
5
5
  import crypto from 'crypto';
6
6
 
7
7
  export const POST = withErrorHandler(async (request) => {
@@ -1,6 +1,6 @@
1
1
  import { getBy, hashPassword, list, create, update, remove } from '@/engine';
2
2
  import { genId, now } from '@/lib/id-helpers';
3
- import { withErrorHandler } from '@/lib/with-error-handler';
3
+ import { withErrorHandler } from '@/lib/errors';
4
4
  import crypto from 'crypto';
5
5
 
6
6
  export const POST = withErrorHandler(async (request) => {
@@ -1,6 +1,6 @@
1
1
  import { generateToken } from '@/lib/csrf-protection';
2
2
  import { ok } from '@/lib/response-formatter';
3
- import { withErrorHandler } from '@/lib/with-error-handler';
3
+ import { withErrorHandler } from '@/lib/errors';
4
4
 
5
5
  export const GET = withErrorHandler(async (request) => {
6
6
  const token = generateToken();
@@ -1,10 +1,10 @@
1
1
  import { tracer } from '@/lib/tracing.js';
2
- import { perfProfiler } from '@/lib/perf-profiler.js';
2
+ import { perfProfiler } from '@/lib/perf.js';
3
3
  import { debugRegistry } from '@/lib/debug-registry.js';
4
4
  import { getExportSink } from '@/lib/export-sink.js';
5
5
  import { getXStateWorkflowEngine } from '@/lib/xstate-workflow-engine.js';
6
6
  import { getHyperFormulaService } from '@/lib/hyperformula-service.js';
7
- import { getBusyBaseAdapter } from '@/lib/busybase-adapter.js';
7
+ import { getBusyBaseAdapter } from '@/lib/busybase/adapter.js';
8
8
  import { hookEngine } from '@/lib/hook-engine.js';
9
9
  import { createLogger } from '@/lib/logger.js';
10
10
 
@@ -1,4 +1,4 @@
1
- import { NextResponse } from '@/lib/next-polyfills';
1
+ import { NextResponse } from '@/lib/next-shim';
2
2
 
3
3
  export async function GET() {
4
4
  if (process.env.NODE_ENV === 'production') {
@@ -1,4 +1,4 @@
1
- import { NextResponse } from '@/lib/next-polyfills';
1
+ import { NextResponse } from '@/lib/next-shim';
2
2
 
3
3
  export async function GET() {
4
4
  if (process.env.NODE_ENV === 'production') {
@@ -1,4 +1,4 @@
1
- import { NextResponse } from '@/lib/next-polyfills';
1
+ import { NextResponse } from '@/lib/next-shim';
2
2
 
3
3
  export async function GET() {
4
4
  if (process.env.NODE_ENV === 'production') {
@@ -1,4 +1,4 @@
1
- import { NextResponse } from '@/lib/next-polyfills';
1
+ import { NextResponse } from '@/lib/next-shim';
2
2
 
3
3
  // The data layer is busybase (LanceDB), not SQLite — the old pragma/page-count probe
4
4
  // no longer applies. This endpoint now reports the configured store kind.
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { NextResponse } from '@/lib/next-polyfills';
3
+ import { NextResponse } from '@/lib/next-shim';
4
4
  import { count } from '@/engine';
5
5
 
6
6
  export async function GET() {
@@ -1,4 +1,4 @@
1
- import { NextResponse } from '@/lib/next-polyfills';
1
+ import { NextResponse } from '@/lib/next-shim';
2
2
 
3
3
  export async function GET() {
4
4
  if (process.env.NODE_ENV === 'production') {
@@ -1,8 +1,7 @@
1
1
  import { getDomainLoader } from '@/lib/domain-loader';
2
2
  import { getConfigEngine } from '@/lib/config-generator-engine';
3
3
  import { ok } from '@/lib/response-formatter';
4
- import { withErrorHandler } from '@/lib/with-error-handler';
5
- import { AppError, NotFoundError } from '@/lib/error-handler';
4
+ import { withErrorHandler, AppError, NotFoundError } from '@/lib/errors';
6
5
  import { HTTP } from '@/config/constants';
7
6
 
8
7
  export const GET = withErrorHandler(async (request, context) => {
@@ -3,7 +3,7 @@ import { getDomainLoader } from '@/lib/domain-loader';
3
3
 
4
4
  const log = createLogger('[DomainsAPI]');
5
5
  import { ok } from '@/lib/response-formatter';
6
- import { withErrorHandler } from '@/lib/with-error-handler';
6
+ import { withErrorHandler } from '@/lib/errors';
7
7
  import { getConfigEngine } from '@/lib/config-generator-engine';
8
8
 
9
9
  export const GET = withErrorHandler(async (request) => {
@@ -1,4 +1,4 @@
1
- import { NextResponse } from '@/lib/next-polyfills';
1
+ import { NextResponse } from '@/lib/next-shim';
2
2
  import { createLogger } from '@/lib/logger.js';
3
3
 
4
4
  const log = createLogger('[EmailBatchAllocate]');
@@ -1,4 +1,4 @@
1
- import { NextResponse } from '@/lib/next-polyfills';
1
+ import { NextResponse } from '@/lib/next-shim';
2
2
  import { createLogger } from '@/lib/logger.js';
3
3
  import { genId, now } from '@/lib/id-helpers';
4
4
  import { get, create } from '@/engine';
@@ -1,4 +1,4 @@
1
- import { NextResponse } from '@/lib/next-polyfills';
1
+ import { NextResponse } from '@/lib/next-shim';
2
2
  import { createLogger } from '@/lib/logger.js';
3
3
  import { genId, now } from '@/lib/id-helpers';
4
4
  import { create } from '@/engine';
@@ -1,4 +1,4 @@
1
- import { NextResponse } from '@/lib/next-polyfills';
1
+ import { NextResponse } from '@/lib/next-shim';
2
2
  import { createLogger } from '@/lib/logger.js';
3
3
  import { now } from '@/lib/id-helpers';
4
4
  import { timingSafeEqual } from 'node:crypto';
@@ -1,4 +1,4 @@
1
- import { NextResponse } from '@/lib/next-polyfills';
1
+ import { NextResponse } from '@/lib/next-shim';
2
2
  import { createLogger } from '@/lib/logger.js';
3
3
  import { list } from '@/engine';
4
4
 
@@ -1,4 +1,4 @@
1
- import { NextResponse } from '@/lib/next-polyfills';
1
+ import { NextResponse } from '@/lib/next-shim';
2
2
  import { createLogger } from '@/lib/logger.js';
3
3
 
4
4
  const log = createLogger('[FilesAPI]');
@@ -5,8 +5,7 @@ const log = createLogger('[Health]');
5
5
  import path from 'path'
6
6
  import { count } from '@/engine'
7
7
  import { getAllMetrics } from '@/lib/metrics-collector.js'
8
- import { getDatabaseStats } from '@/lib/db-monitor.js'
9
- import { getCurrentResources } from '@/lib/resource-monitor.js'
8
+ import { getDatabaseStats, getCurrentResources } from '@/lib/monitor.js'
10
9
  import { getRecentAlerts } from '@/lib/alert-manager.js'
11
10
 
12
11
  function getLastSyncAt() {
@@ -1,7 +1,6 @@
1
1
  import { getAllMetrics, clearMetrics } from '@/lib/metrics-collector.js'
2
2
  import { getRecentAlerts } from '@/lib/alert-manager.js'
3
- import { getDatabaseStats } from '@/lib/db-monitor.js'
4
- import { getCurrentResources } from '@/lib/resource-monitor.js'
3
+ import { getDatabaseStats, getCurrentResources } from '@/lib/monitor.js'
5
4
  import { getLogs } from '@/lib/log-aggregator.js'
6
5
 
7
6
  export const GET = async (request) => {
package/src/engine.js CHANGED
@@ -1,12 +1,12 @@
1
1
  // Previously `@/engine` resolved to a non-existent module; this barrel makes it
2
- // concrete and points the data layer at busybase-store.
2
+ // concrete and points the data layer at busybase/store.
3
3
 
4
4
  // Data layer (busybase)
5
5
  export {
6
6
  list, get, getBy, count, listWithPagination, searchWithPagination, search,
7
7
  create, update, remove, bulkCreate, getChildren, batchGetChildren,
8
8
  setBusyBaseClient,
9
- } from '@/lib/busybase-store.js';
9
+ } from '@/lib/busybase/store.js';
10
10
 
11
11
  // IDs / time
12
12
  export { genId, now } from '@/lib/id-helpers.js';
@@ -15,7 +15,7 @@ export { genId, now } from '@/lib/id-helpers.js';
15
15
  export { hashPassword, verifyPassword } from '@/engine.server.js';
16
16
 
17
17
  // Audit
18
- export { logAction } from '@/lib/busybase-audit.js';
18
+ export { logAction } from '@/lib/busybase/audit.js';
19
19
 
20
20
  // Legacy SQLite shims (data layer is busybase now). busybase is schemaless so there
21
21
  // is no migration step; migrate() is a no-op kept for callers that still invoke it.
@@ -2,7 +2,7 @@
2
2
  import { Lucia } from 'lucia';
3
3
  import { Google } from 'arctic';
4
4
  import bcrypt from 'bcrypt';
5
- import { BusyBaseLuciaAdapter } from './lib/busybase-lucia-adapter.js';
5
+ import { BusyBaseLuciaAdapter } from './lib/busybase/lucia-adapter.js';
6
6
  import { buildConfig, hasGoogleAuth } from './config/env.js';
7
7
 
8
8
  let _lucia = null;
@@ -118,13 +118,13 @@ export async function verifyPassword(password, hash) {
118
118
  }
119
119
 
120
120
  export async function getUserByEmail(email) {
121
- const { getBy } = await import('./lib/busybase-store.js');
121
+ const { getBy } = await import('./lib/busybase/store.js');
122
122
  return getBy('user', 'email', email);
123
123
  }
124
124
 
125
125
  export async function createUser(userData) {
126
126
  const hashedPassword = await hashPassword(userData.password);
127
- const { create } = await import('./lib/busybase-store.js');
127
+ const { create } = await import('./lib/busybase/store.js');
128
128
  return create('user', {
129
129
  ...userData,
130
130
  password: hashedPassword,
package/src/index.js CHANGED
@@ -141,9 +141,9 @@ export class Thatcher {
141
141
  || 'busybase_data';
142
142
  const client = await createEmbedded({ dir });
143
143
 
144
- const store = await import(resolveModule('./lib/busybase-store.js'));
144
+ const store = await import(resolveModule('./lib/busybase/store.js'));
145
145
  store.setBusyBaseClient(client);
146
- const audit = await import(resolveModule('./lib/busybase-audit.js'));
146
+ const audit = await import(resolveModule('./lib/busybase/audit.js'));
147
147
  audit.setBusyBaseClient(client);
148
148
 
149
149
  this.busybase = client;
@@ -248,7 +248,32 @@ export class Thatcher {
248
248
  _server = null;
249
249
  }
250
250
  for (const w of _hotReloadWatchers) w.close();
251
- _hotReloadWatchers = [];
251
+ _hotReloadWatchers = []
252
+ // This never called the underlying database client's own close() at all
253
+ // -- busybase's createEmbedded() now exposes a real one (busybase's own
254
+ // embedded.ts fix), but nothing here called it, so a consumer reusing the
255
+ // SAME database file in the same process (a fresh Thatcher instance, a
256
+ // test harness) would silently open a SECOND native handle onto an
257
+ // already-open file rather than genuinely replacing the first --
258
+ // _databaseInitialized is a MODULE-level singleton (see initDatabase()'s
259
+ // own comment / AGENTS.md's documented cwd-bound handle caveat), so it
260
+ // must be reset here too or a subsequent initDatabase() call would
261
+ // silently skip re-opening and keep using the now-closed client.
262
+ // NOTE (live-witnessed, Windows): calling close() does NOT synchronously
263
+ // release the native libsql binding's OS-level file lock while THIS
264
+ // process keeps running -- the lock only actually clears once the owning
265
+ // process exits. A caller needing the underlying file/directory removable
266
+ // in the SAME still-running process (e.g. a per-run isolated-tmpdir test
267
+ // harness) cannot rely on this call alone for that and must retry the
268
+ // removal with backoff, or isolate each run in its own child process.
269
+ // close() is still correct and worth calling regardless -- it prevents
270
+ // the silent second-handle-fork above, which is a real, distinct benefit.
271
+ if (this.busybase) {
272
+ try { this.busybase.close?.() } catch { /* best-effort */ }
273
+ this.busybase = null
274
+ globalThis.__thatcherBusyBase = null
275
+ _databaseInitialized = false
276
+ }
252
277
  }
253
278
 
254
279
  getConfigEngine() {
@@ -270,38 +295,38 @@ export class Thatcher {
270
295
  // === CRUD operations ===
271
296
 
272
297
  async list(entity, where = {}, opts = {}) {
273
- const { list } = await import(resolveModule('./lib/busybase-store.js'));
298
+ const { list } = await import(resolveModule('./lib/busybase/store.js'));
274
299
  return list(entity, where, opts);
275
300
  }
276
301
 
277
302
  async get(entity, id) {
278
- const { get } = await import(resolveModule('./lib/busybase-store.js'));
303
+ const { get } = await import(resolveModule('./lib/busybase/store.js'));
279
304
  return get(entity, id);
280
305
  }
281
306
 
282
307
  async create(entity, data, user) {
283
- const { create } = await import(resolveModule('./lib/busybase-store.js'));
308
+ const { create } = await import(resolveModule('./lib/busybase/store.js'));
284
309
  return create(entity, data, user);
285
310
  }
286
311
 
287
312
  // opts.expectedVersion: optional optimistic-concurrency guard (see
288
- // busybase-store.js update()) -- a caller that read the row's _version can
313
+ // busybase/store.js update()) -- a caller that read the row's _version can
289
314
  // pass it here to detect (via a thrown {code:'conflict'} error) a concurrent
290
315
  // writer landing in between, instead of silently clobbering it. `user` stays
291
316
  // its own positional param (unused by the store today, kept for API
292
317
  // stability with existing callers); opts is a new, optional 5th param.
293
318
  async update(entity, id, data, user, opts = {}) {
294
- const { update } = await import(resolveModule('./lib/busybase-store.js'));
319
+ const { update } = await import(resolveModule('./lib/busybase/store.js'));
295
320
  return update(entity, id, data, opts);
296
321
  }
297
322
 
298
323
  async delete(entity, id) {
299
- const { remove } = await import(resolveModule('./lib/busybase-store.js'));
324
+ const { remove } = await import(resolveModule('./lib/busybase/store.js'));
300
325
  return remove(entity, id);
301
326
  }
302
327
 
303
328
  async search(entity, query, where = {}, opts = {}) {
304
- const { search } = await import(resolveModule('./lib/busybase-store.js'));
329
+ const { search } = await import(resolveModule('./lib/busybase/store.js'));
305
330
  return search(entity, query, where, opts);
306
331
  }
307
332
 
@@ -3,7 +3,7 @@
3
3
  import { create, update, remove } from '@/engine';
4
4
  import { requireUser, check } from '@/engine.server';
5
5
  import { getSpec } from '@/config/spec-helpers';
6
- import { revalidatePath } from '@/lib/next-polyfills';
6
+ import { revalidatePath } from '@/lib/next-shim';
7
7
 
8
8
  export async function serverCreateEntity(entityName, data) {
9
9
  const user = await requireUser();
@@ -1,5 +1,5 @@
1
1
 
2
- import { NextResponse } from '@/lib/next-polyfills';
2
+ import { NextResponse } from '@/lib/next-shim';
3
3
  import { getSpec } from '@/config/spec-helpers';
4
4
  import { migrate } from '@/engine';
5
5
  import { getUser } from '@/engine.server';
package/src/lib/api.js CHANGED
@@ -2,13 +2,12 @@ import { getUser } from '@/engine.server';
2
2
  import { getSpec } from '@/config/spec-helpers';
3
3
  import { API_ENDPOINTS } from '@/config';
4
4
  import { can } from '@/services/permission.service';
5
- import { get, create, update, remove, listWithPagination, search } from '@/lib/busybase-store';
6
- import { validateEntity, validateUpdate, hasErrors } from '@/lib/validate';
5
+ import { get, create, update, remove, listWithPagination, search } from '@/lib/busybase/store';
6
+ import { validateEntity, validateUpdate, hasErrors } from '@/lib/validation/entity-validators';
7
7
  import { broadcastUpdate } from '@/lib/realtime-server';
8
- import { UnauthorizedError, PermissionError, NotFoundError, ValidationError, AppError } from '@/lib/error-handler';
8
+ import { UnauthorizedError, PermissionError, NotFoundError, ValidationError, AppError, withErrorHandler } from '@/lib/errors';
9
9
  import { ok, created, paginated } from '@/lib/response-formatter';
10
10
  import { QueryAdapter } from '@/lib/query-string-adapter';
11
- import { withErrorHandler } from '@/lib/with-error-handler';
12
11
  import { HTTP } from '@/config/constants';
13
12
 
14
13
  const createHandler = (entity, action) => async (request, { params, searchParams }) => {
@@ -2,7 +2,7 @@ import { createLogger } from './logger.js';
2
2
  import { genId, now } from '@/lib/id-helpers';
3
3
 
4
4
  const log = createLogger('[AuditEnhanced]');
5
- import { list, create, remove } from '@/lib/busybase-store';
5
+ import { list, create, remove } from '@/lib/busybase/store';
6
6
 
7
7
  export const LOG_LEVELS = { DEBUG: 'debug', INFO: 'info', WARN: 'warn', ERROR: 'error' };
8
8
  export const OPERATION_TYPES = { CREATE: 'create', UPDATE: 'update', DELETE: 'delete', READ: 'read', AUTH: 'auth', AUTHZ: 'authz' };
@@ -3,7 +3,7 @@
3
3
  import { getUser, getLucia } from '../engine.server.js';
4
4
  import { getSpec } from '../config/spec-helpers.js';
5
5
  import { can } from '../services/permission.service.js';
6
- import { UnauthorizedError, PermissionError, NotFoundError } from './error-handler.js';
6
+ import { UnauthorizedError, PermissionError, NotFoundError } from './errors/index.js';
7
7
 
8
8
  const actionMap = {
9
9
  list: 'list',
@@ -1,4 +1,4 @@
1
- import { NextResponse } from '@/lib/next-polyfills';
1
+ import { NextResponse } from '@/lib/next-shim';
2
2
  import { SESSION } from '@/config/auth-config';
3
3
  import { HTTP } from '@/config/constants';
4
4
 
@@ -1,5 +1,5 @@
1
- import { createLogger } from './logger.js';
2
- import { hookEngine } from './hook-engine.js';
1
+ import { createLogger } from '../logger.js';
2
+ import { hookEngine } from '../hook-engine.js';
3
3
 
4
4
  const logger = createLogger('[BusyBase]');
5
5
 
@@ -154,4 +154,4 @@ export function isBusyBaseAvailable() {
154
154
  return _bbInstance?.initialized === true;
155
155
  }
156
156
 
157
- export default BusyBaseAdapter;
157
+ export default BusyBaseAdapter;
@@ -4,7 +4,7 @@
4
4
  * audit_logs and permission_audit tables.
5
5
  */
6
6
 
7
- import { list, create } from '@/lib/busybase-store.js';
7
+ import { list, create } from '@/lib/busybase/store.js';
8
8
  import { genId, now } from '@/lib/id-helpers.js';
9
9
 
10
10
  const parseJson = (val) => { try { return val ? JSON.parse(val) : null; } catch { return null; } };
@@ -6,8 +6,8 @@
6
6
  * forget: it returns immediately and swallows/logs write errors rather than throwing.
7
7
  */
8
8
 
9
- import { createLogger } from './logger.js';
10
- import { genId, now } from './id-helpers.js';
9
+ import { createLogger } from '../logger.js';
10
+ import { genId, now } from '../id-helpers.js';
11
11
 
12
12
  const log = createLogger('[BusyBaseAudit]');
13
13