thatcher 1.0.44 → 1.0.45

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 CHANGED
@@ -133,36 +133,50 @@ const canEdit = await thatcher.can(user, thatcher.getEntitySpec('item'), 'edit')
133
133
 
134
134
  ```
135
135
  thatcher/
136
- ├── src/
137
- │ ├── index.js # Main entry: createThatcher()
138
- │ ├── cli.js # CLI commands
139
- │ ├── config/
140
- │ │ ├── config-loader.js # YAML loading & validation
141
- │ │ ├── spec-helpers.js # Entity spec utilities
142
- │ │ ├── env.js # Environment config
143
- │ │ └── constants.js # HTTP codes, statuses
144
- │ ├── lib/
145
- │ │ ├── busybase-store.js # busybase data layer (async CRUD)
146
- │ │ ├── query-engine.js # Read operations (GET, search)
147
- │ │ ├── query-engine-write.js # Write operations (CRUD)
148
- │ │ ├── config-generator-engine.js # Spec builder
149
- │ │ ├── hook-engine.js # Event system
150
- │ │ ├── workflow-engine.js # State machines
151
- │ │ ├── auth-middleware.js # Auth checks
152
- │ │ ├── crud-factory.js # Handler factory
153
- │ │ ├── crud-handlers.js # HTTP handlers
154
- │ │ ├── validate.js # Validation
155
- │ │ └── logger.js # Structured logging
156
- │ ├── services/
157
- │ │ └── permission.service.js # Authorization
158
- │ ├── adapters/
159
- │ │ ├── google-auth.js # Google OAuth
160
- │ │ └── google-drive.js # Drive file operations
161
- │ ├── plugins/
162
- │ │ └── index.js # Plugin auto-discovery
163
- │ └── server/
164
- │ └── server.js # HTTP server
165
- └── package.json
136
+ src/
137
+ index.js # Main entry: createThatcher()
138
+ engine.js / engine.server.js # Runtime engine, server-only auth/session surface
139
+ cli.js # CLI commands
140
+ config/
141
+ config-loader.js # YAML loading & validation
142
+ spec-helpers.js # Entity spec utilities
143
+ env.js # Environment config
144
+ constants.js # HTTP codes, statuses
145
+ lib/
146
+ busybase/
147
+ adapter.js # busybase connection/adapter layer
148
+ store.js # busybase data layer (async CRUD)
149
+ audit.js / audit-reads.js # Audit log writes/reads
150
+ lucia-adapter.js # Lucia auth session adapter
151
+ errors/
152
+ types.js # AppError and error type hierarchy
153
+ wrap.js # wrap() - unified handler wrapper (timeout/retry/logging)
154
+ recovery.js # Circuit breaker, checkpoint, retry-with-backoff
155
+ validation/
156
+ entity-validators.js / business-validators.js / format-validators.js / security-validators.js / file-validators.js
157
+ csrf.js / rate-limit.js
158
+ config-generator-engine.js # Spec builder
159
+ hook-engine.js # Event system
160
+ workflow-engine.js # State machines (legacy/canonical) - xstate-workflow-engine.js is the debug/test-only successor
161
+ auth-middleware.js # Auth checks
162
+ crud-factory.js # Handler factory
163
+ crud-handlers.js # HTTP handlers - read + write CRUD operations
164
+ logger.js # Structured logging
165
+ services/
166
+ permission.service.js # Authorization
167
+ collaborator-role.service.js
168
+ notification-engine.js / email-sender.js
169
+ adapters/
170
+ google-auth.js # Google OAuth
171
+ google-drive.js # Drive file operations
172
+ google-gmail.js # Gmail send/receive
173
+ plugins/
174
+ index.js # Plugin auto-discovery
175
+ app/api/ # Route handlers (Next.js-style file routing, [entity]/[[...path]] catch-all + explicit routes)
176
+ ui/ # Server-rendered HTML components + client-side scripts
177
+ server/
178
+ server.js # HTTP server
179
+ package.json
166
180
  ```
167
181
 
168
182
  ### How It Works
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thatcher",
3
- "version": "1.0.44",
3
+ "version": "1.0.45",
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",
@@ -24,6 +24,7 @@ export function getGmailClient(user = null) {
24
24
 
25
25
  function encodeHeader(value) {
26
26
  // RFC 2047 encoded-word for any non-ASCII header value (subject/name).
27
+ // eslint-disable-next-line no-control-regex -- \x00-\x7F is the intentional ASCII range check, not a stray control char
27
28
  if (/^[\x00-\x7F]*$/.test(value)) return value;
28
29
  return `=?UTF-8?B?${Buffer.from(value, 'utf8').toString('base64')}?=`;
29
30
  }
@@ -27,7 +27,7 @@ export async function GET(request, { params }) {
27
27
 
28
28
  const content = await fileService.download(fileRecord.drive_file_id);
29
29
 
30
- const safeName = (fileRecord.file_name || 'download').replace(/[^\w.\-]/g, '_');
30
+ const safeName = (fileRecord.file_name || 'download').replace(/[^\w.-]/g, '_');
31
31
  return new NextResponse(content, {
32
32
  headers: {
33
33
  'Content-Type': fileRecord.mime_type || 'application/octet-stream',
package/src/cli.js CHANGED
@@ -13,6 +13,8 @@
13
13
 
14
14
  import { startThatcher, Thatcher } from './index.js';
15
15
  import * as readline from 'readline';
16
+ import fs from 'fs';
17
+ import path from 'path';
16
18
 
17
19
  const args = process.argv.slice(2);
18
20
  const command = args[0];
@@ -20,7 +22,7 @@ const command = args[0];
20
22
  async function main() {
21
23
  switch (command) {
22
24
  case 'start':
23
- case 'dev':
25
+ case 'dev': {
24
26
  console.log(`[Thatcher] Starting in ${command} mode...`);
25
27
  const thatcher = new Thatcher({
26
28
  server: { hotReload: command === 'dev' },
@@ -34,15 +36,17 @@ async function main() {
34
36
  process.exit(1);
35
37
  }
36
38
  break;
39
+ }
37
40
 
38
- case 'migrate':
41
+ case 'migrate': {
39
42
  console.log('[Thatcher] Running migrations...');
40
43
  const t1 = new Thatcher({});
41
44
  await t1.init();
42
45
  console.log('[Thatcher] Migrations complete');
43
46
  break;
47
+ }
44
48
 
45
- case 'validate':
49
+ case 'validate': {
46
50
  console.log('[Thatcher] Validating configuration...');
47
51
  try {
48
52
  const t2 = new Thatcher({});
@@ -55,6 +59,7 @@ async function main() {
55
59
  process.exit(1);
56
60
  }
57
61
  break;
62
+ }
58
63
 
59
64
  case 'console':
60
65
  case 'repl':
@@ -43,8 +43,6 @@ export async function withCircuitBreaker(name, fn, options = {}) {
43
43
 
44
44
  try {
45
45
  const result = await fn();
46
- if (breaker.state === 'half-open') {
47
- }
48
46
  breaker.failures = 0;
49
47
  breaker.state = 'closed';
50
48
  return result;
@@ -248,12 +246,6 @@ export async function withRecovery(fn, options = {}) {
248
246
 
249
247
  let operation = fn;
250
248
 
251
- if (checkpointName) {
252
- const savedState = restoreCheckpoint(checkpointName);
253
- if (savedState) {
254
- }
255
- }
256
-
257
249
  if (supervisor) {
258
250
  operation = () => supervise(supervisor, operation, options);
259
251
  }
@@ -11,9 +11,10 @@ export function coerceFieldValue(value, type) {
11
11
  case 'json':
12
12
  return typeof value === 'string' ? JSON.parse(value) : value;
13
13
  case 'date':
14
- case 'timestamp':
14
+ case 'timestamp': {
15
15
  const num = Number(value);
16
16
  return isNaN(num) ? null : num;
17
+ }
17
18
  case 'ref':
18
19
  return String(value);
19
20
  default:
@@ -41,7 +41,7 @@ const __dirname = path.dirname(__filename);
41
41
  const require = createRequire(import.meta.url);
42
42
 
43
43
  const promiseContainerMod = require('./promise-container.js');
44
- export { Mutex, MutexManager, globalManager } from './mutex.js';
44
+ import { Mutex, MutexManager, globalManager } from './mutex.js';
45
45
  import { Supervisor, SupervisorTree, globalTree } from './supervisor.js';
46
46
  const checkpointMod = require('./checkpoint.js');
47
47
  const timeoutMod = require('./timeout-wrapper.js');
@@ -72,6 +72,7 @@ expose('hotReload', {
72
72
  }, 'Hot reload infrastructure');
73
73
 
74
74
  export {
75
+ Mutex, MutexManager, globalManager,
75
76
  PromiseContainer, globalContainer, contain,
76
77
  Supervisor, SupervisorTree, globalTree,
77
78
  CheckpointManager, globalCheckpoint,
@@ -5,7 +5,7 @@ export function minifyJS(code) {
5
5
  .replace(/^\s+/gm, '')
6
6
  .replace(/\s+$/gm, '')
7
7
  .replace(/\n+/g, '\n')
8
- .replace(/\s*([{}()\[\];:,=<>!+\-*\/&|?])\s*/g, '$1')
8
+ .replace(/\s*([{}()[\];:,=<>!+\-*/&|?])\s*/g, '$1')
9
9
  .trim();
10
10
  }
11
11
 
@@ -27,7 +27,7 @@ export function resolveSpecificParams(routeFile, pathParts) {
27
27
  for (let i = 0; i < routeSegments.length && i < urlSegments.length; i++) {
28
28
  const seg = routeSegments[i];
29
29
  if (seg.startsWith('[') && seg.endsWith(']')) {
30
- const paramName = seg.replace(/^\[\.\.\./, '').replace(/[\[\]]/g, '');
30
+ const paramName = seg.replace(/^\[\.\.\./, '').replace(/[[\]]/g, '');
31
31
  result[paramName] = urlSegments[i];
32
32
  }
33
33
  }
@@ -12,7 +12,7 @@ const XSS_PATTERNS = [
12
12
  ];
13
13
 
14
14
  const SQL_INJECTION_PATTERNS = [
15
- /('|(\-\-)|(;)|(\|\|)|(\/\*)|(\*\/)|xp_)/gi,
15
+ /('|(--)|(;)|(\|\|)|(\/\*)|(\*\/)|xp_)/gi,
16
16
  /(union|select|insert|update|delete|drop|create|alter|exec|execute)\s/gi,
17
17
  /0x[0-9a-f]+/gi,
18
18
  /char\(/gi,
@@ -20,7 +20,7 @@ const SQL_INJECTION_PATTERNS = [
20
20
  ];
21
21
 
22
22
  const PATH_TRAVERSAL_PATTERNS = [
23
- /\.\.[\\\/]/g,
23
+ /\.\.[\\/]/g,
24
24
  /\.\.%/g,
25
25
  /%2e%2e/gi,
26
26
  /\.\.\./g
@@ -122,8 +122,8 @@ export const commonHandlers = {
122
122
 
123
123
  notify: {
124
124
  show(message, type = 'info') {
125
- if (typeof showToast === 'function') {
126
- showToast(message, type);
125
+ if (typeof window.showToast === 'function') {
126
+ window.showToast(message, type);
127
127
  }
128
128
  },
129
129
  success(message) { this.show(message, 'success'); },
@@ -92,21 +92,23 @@ function generateFieldHtml(field, dialogId, context) {
92
92
  case 'date':
93
93
  inputHtml = `<input type="date" id="${fieldId}" name="${name}" class="input input-bordered w-full" ${requiredAttr} ${ariaRequired} ${ariaDesc}/>`;
94
94
  break;
95
- case 'select':
95
+ case 'select': {
96
96
  const optionsHtml = (options || []).map(opt =>
97
97
  `<option value="${opt.value}">${escapeHtml(opt.label || opt.value)}</option>`
98
98
  ).join('');
99
99
  inputHtml = `<select id="${fieldId}" name="${name}" class="select select-bordered w-full" ${requiredAttr} ${ariaRequired} ${ariaDesc}><option value="">Select...</option>${optionsHtml}</select>`;
100
100
  break;
101
+ }
101
102
  case 'checkbox':
102
103
  inputHtml = `<label class="flex items-center gap-2"><input type="checkbox" id="${fieldId}" name="${name}" class="checkbox" ${requiredAttr} ${ariaRequired} ${ariaDesc}/><span class="text-sm">${label}</span></label>`;
103
104
  break;
104
- case 'multi-select':
105
+ case 'multi-select': {
105
106
  const multiOptHtml = (options || []).map(opt =>
106
107
  `<label class="flex items-center gap-2"><input type="checkbox" name="${name}" value="${opt.value}" class="checkbox"/><span class="text-sm">${escapeHtml(opt.label || opt.value)}</span></label>`
107
108
  ).join('');
108
109
  inputHtml = `<div class="flex flex-col gap-2" ${role.list}>${multiOptHtml}</div>`;
109
110
  break;
111
+ }
110
112
  case 'file':
111
113
  inputHtml = `<input type="file" id="${fieldId}" name="${name}" class="file-input file-input-bordered w-full" ${requiredAttr} ${ariaRequired} ${ariaDesc}/>`;
112
114
  break;
package/src/ui/layout.js CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable no-useless-escape -- this file embeds raw client <script> text inside a template literal; ESLint parses embedded regex escapes like \d as literal template text and misflags them */
1
2
  import { h } from '@/ui/webjsx.js'
2
3
  import { getNavItems, getAdminItems, isPartner, isClerk } from '@/ui/permissions-ui.js'
3
4
  import { TOAST_SCRIPT, AVATAR_COLORS, esc } from '@/ui/render-helpers.js'