thatcher 1.0.6 → 1.0.7
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/package.json +1 -1
- package/src/lib/error-handler.js +24 -47
- package/src/ui/component-engine.js +13 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thatcher",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "A config-driven application framework for building data-intensive web apps without code. 100% feature-complete extraction from moonlanding.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
package/src/lib/error-handler.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Error Handling - Centralized error types and utilities
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
// AppError stays a real class (base error type; `instanceof AppError` works).
|
|
5
6
|
export class AppError extends Error {
|
|
6
7
|
constructor(message, code = 'APP_ERROR', status = 500, details = null) {
|
|
7
8
|
super(message);
|
|
@@ -9,62 +10,38 @@ export class AppError extends Error {
|
|
|
9
10
|
this.code = code;
|
|
10
11
|
this.status = status;
|
|
11
12
|
this.details = details;
|
|
12
|
-
if (Error.captureStackTrace) Error.captureStackTrace(this,
|
|
13
|
+
if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor);
|
|
13
14
|
}
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
// The specific error types are exported as FACTORY FUNCTIONS, not classes, so
|
|
18
|
+
// both `throw UnauthorizedError(x)` (the style moonlanding and ~17 callsites in
|
|
19
|
+
// this repo use) and a plain call work. A derived `class X extends AppError`
|
|
20
|
+
// cannot be invoked without `new` at all (the engine throws before the body, so
|
|
21
|
+
// a new.target guard does not help) — a factory is the portable fix. Each
|
|
22
|
+
// returns an AppError instance, so `instanceof AppError` still holds in catch.
|
|
23
|
+
export function UnauthorizedError(message = 'Authentication required') {
|
|
24
|
+
const e = new AppError(message, 'UNAUTHORIZED', 401); e.name = 'UnauthorizedError'; return e;
|
|
21
25
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
constructor(message = 'Permission denied') {
|
|
25
|
-
super(message, 'PERMISSION_DENIED', 403);
|
|
26
|
-
this.name = 'PermissionError';
|
|
27
|
-
}
|
|
26
|
+
export function PermissionError(message = 'Permission denied') {
|
|
27
|
+
const e = new AppError(message, 'PERMISSION_DENIED', 403); e.name = 'PermissionError'; return e;
|
|
28
28
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const msg = id ? `${entity} with id ${id} not found` : `${entity} not found`;
|
|
33
|
-
super(msg, 'NOT_FOUND', 404);
|
|
34
|
-
this.name = 'NotFoundError';
|
|
35
|
-
this.entity = entity;
|
|
36
|
-
this.id = id;
|
|
37
|
-
}
|
|
29
|
+
export function NotFoundError(entity = 'Resource', id = null) {
|
|
30
|
+
const msg = id ? `${entity} with id ${id} not found` : `${entity} not found`;
|
|
31
|
+
const e = new AppError(msg, 'NOT_FOUND', 404); e.name = 'NotFoundError'; e.entity = entity; e.id = id; return e;
|
|
38
32
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
constructor(message = 'Validation failed', errors = {}) {
|
|
42
|
-
super(message, 'VALIDATION_ERROR', 422);
|
|
43
|
-
this.name = 'ValidationError';
|
|
44
|
-
this.errors = errors;
|
|
45
|
-
}
|
|
33
|
+
export function ValidationError(message = 'Validation failed', errors = {}) {
|
|
34
|
+
const e = new AppError(message, 'VALIDATION_ERROR', 422); e.name = 'ValidationError'; e.errors = errors; return e;
|
|
46
35
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
super(`Database ${operation} failed: ${originalError?.message || 'unknown error'}`, 'DATABASE_ERROR', 500);
|
|
51
|
-
this.name = 'DatabaseError';
|
|
52
|
-
this.originalError = originalError;
|
|
53
|
-
}
|
|
36
|
+
export function DatabaseError(operation = 'Database operation', originalError = null) {
|
|
37
|
+
const e = new AppError(`Database ${operation} failed: ${originalError?.message || 'unknown error'}`, 'DATABASE_ERROR', 500);
|
|
38
|
+
e.name = 'DatabaseError'; e.originalError = originalError; return e;
|
|
54
39
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
constructor(message = 'Resource already exists') {
|
|
58
|
-
super(message, 'CONFLICT', 409);
|
|
59
|
-
this.name = 'ConflictError';
|
|
60
|
-
}
|
|
40
|
+
export function ConflictError(message = 'Resource already exists') {
|
|
41
|
+
const e = new AppError(message, 'CONFLICT', 409); e.name = 'ConflictError'; return e;
|
|
61
42
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
constructor(message = 'Invalid request') {
|
|
65
|
-
super(message, 'BAD_REQUEST', 400);
|
|
66
|
-
this.name = 'BadRequestError';
|
|
67
|
-
}
|
|
43
|
+
export function BadRequestError(message = 'Invalid request') {
|
|
44
|
+
const e = new AppError(message, 'BAD_REQUEST', 400); e.name = 'BadRequestError'; return e;
|
|
68
45
|
}
|
|
69
46
|
|
|
70
47
|
/**
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { getConfigEngineSync } from '@/lib/config-generator-engine';
|
|
2
|
+
import { esc } from '@/ui/render-helpers';
|
|
3
|
+
|
|
4
|
+
// Allow only a safe CSS color token (hex, rgb/rgba/hsl(...), or a bare named color)
|
|
5
|
+
// so an attacker-controlled item.color cannot break out of the style attribute.
|
|
6
|
+
function safeColor(c) {
|
|
7
|
+
const s = String(c ?? '').trim();
|
|
8
|
+
return /^#[0-9a-fA-F]{3,8}$|^(rgb|rgba|hsl|hsla)\([0-9.,%\s/]+\)$|^[a-zA-Z]+$/.test(s) ? s : '';
|
|
9
|
+
}
|
|
2
10
|
|
|
3
11
|
function getComponentDefs() {
|
|
4
12
|
try {
|
|
@@ -29,7 +37,7 @@ export function renderComponent(componentId, props = {}) {
|
|
|
29
37
|
html = html.replace(regex, JSON.stringify(value));
|
|
30
38
|
}
|
|
31
39
|
} else {
|
|
32
|
-
html = html.replace(regex,
|
|
40
|
+
html = html.replace(regex, esc(value));
|
|
33
41
|
}
|
|
34
42
|
});
|
|
35
43
|
|
|
@@ -42,11 +50,12 @@ export function renderComponent(componentId, props = {}) {
|
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
function renderItem(item, key) {
|
|
45
|
-
if (typeof item === 'string') return item;
|
|
53
|
+
if (typeof item === 'string') return esc(item);
|
|
46
54
|
if (typeof item === 'object' && item.name && item.color) {
|
|
47
|
-
|
|
55
|
+
const bg = safeColor(item.color);
|
|
56
|
+
return `<span class="avatar-item"${bg ? ` style="background:${bg}"` : ''}>${esc(String(item.name).charAt(0))}</span>`;
|
|
48
57
|
}
|
|
49
|
-
return
|
|
58
|
+
return esc(item);
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
export function getComponentConfig(componentId) {
|