thatcher 1.0.43 → 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 +44 -30
- package/package.json +1 -1
- package/src/adapters/google-gmail.js +1 -0
- package/src/app/api/files/[id]/route.js +1 -1
- package/src/cli.js +8 -3
- package/src/lib/errors/recovery.js +0 -8
- package/src/lib/field-registry.js +2 -1
- package/src/lib/hot-reload/index.js +2 -1
- package/src/lib/minifier.js +1 -1
- package/src/lib/route-resolver.js +1 -1
- package/src/lib/validation/security-validators.js +2 -2
- package/src/ui/common-handlers.js +2 -2
- package/src/ui/dialog-engine.js +4 -2
- package/src/ui/layout.js +1 -0
package/README.md
CHANGED
|
@@ -133,36 +133,50 @@ const canEdit = await thatcher.can(user, thatcher.getEntitySpec('item'), 'edit')
|
|
|
133
133
|
|
|
134
134
|
```
|
|
135
135
|
thatcher/
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
136
|
+
src/
|
|
137
|
+
index.js # Main entry: createThatcher()
|
|
138
|
+
engine.js / engine.server.js # Runtime engine, server-only auth/session surface
|
|
139
|
+
cli.js # CLI commands
|
|
140
|
+
config/
|
|
141
|
+
config-loader.js # YAML loading & validation
|
|
142
|
+
spec-helpers.js # Entity spec utilities
|
|
143
|
+
env.js # Environment config
|
|
144
|
+
constants.js # HTTP codes, statuses
|
|
145
|
+
lib/
|
|
146
|
+
busybase/
|
|
147
|
+
adapter.js # busybase connection/adapter layer
|
|
148
|
+
store.js # busybase data layer (async CRUD)
|
|
149
|
+
audit.js / audit-reads.js # Audit log writes/reads
|
|
150
|
+
lucia-adapter.js # Lucia auth session adapter
|
|
151
|
+
errors/
|
|
152
|
+
types.js # AppError and error type hierarchy
|
|
153
|
+
wrap.js # wrap() - unified handler wrapper (timeout/retry/logging)
|
|
154
|
+
recovery.js # Circuit breaker, checkpoint, retry-with-backoff
|
|
155
|
+
validation/
|
|
156
|
+
entity-validators.js / business-validators.js / format-validators.js / security-validators.js / file-validators.js
|
|
157
|
+
csrf.js / rate-limit.js
|
|
158
|
+
config-generator-engine.js # Spec builder
|
|
159
|
+
hook-engine.js # Event system
|
|
160
|
+
workflow-engine.js # State machines (legacy/canonical) - xstate-workflow-engine.js is the debug/test-only successor
|
|
161
|
+
auth-middleware.js # Auth checks
|
|
162
|
+
crud-factory.js # Handler factory
|
|
163
|
+
crud-handlers.js # HTTP handlers - read + write CRUD operations
|
|
164
|
+
logger.js # Structured logging
|
|
165
|
+
services/
|
|
166
|
+
permission.service.js # Authorization
|
|
167
|
+
collaborator-role.service.js
|
|
168
|
+
notification-engine.js / email-sender.js
|
|
169
|
+
adapters/
|
|
170
|
+
google-auth.js # Google OAuth
|
|
171
|
+
google-drive.js # Drive file operations
|
|
172
|
+
google-gmail.js # Gmail send/receive
|
|
173
|
+
plugins/
|
|
174
|
+
index.js # Plugin auto-discovery
|
|
175
|
+
app/api/ # Route handlers (Next.js-style file routing, [entity]/[[...path]] catch-all + explicit routes)
|
|
176
|
+
ui/ # Server-rendered HTML components + client-side scripts
|
|
177
|
+
server/
|
|
178
|
+
server.js # HTTP server
|
|
179
|
+
package.json
|
|
166
180
|
```
|
|
167
181
|
|
|
168
182
|
### How It Works
|
package/package.json
CHANGED
|
@@ -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
|
|
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
|
-
|
|
44
|
+
import { Mutex, MutexManager, globalManager } from './mutex.js';
|
|
45
45
|
import { Supervisor, SupervisorTree, globalTree } from './supervisor.js';
|
|
46
46
|
const checkpointMod = require('./checkpoint.js');
|
|
47
47
|
const timeoutMod = require('./timeout-wrapper.js');
|
|
@@ -72,6 +72,7 @@ expose('hotReload', {
|
|
|
72
72
|
}, 'Hot reload infrastructure');
|
|
73
73
|
|
|
74
74
|
export {
|
|
75
|
+
Mutex, MutexManager, globalManager,
|
|
75
76
|
PromiseContainer, globalContainer, contain,
|
|
76
77
|
Supervisor, SupervisorTree, globalTree,
|
|
77
78
|
CheckpointManager, globalCheckpoint,
|
package/src/lib/minifier.js
CHANGED
|
@@ -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(/[
|
|
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
|
-
/('|(
|
|
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
|
-
/\.\.[
|
|
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'); },
|
package/src/ui/dialog-engine.js
CHANGED
|
@@ -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'
|