thatcher 1.0.5 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thatcher",
3
- "version": "1.0.5",
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/cli.js CHANGED
File without changes
@@ -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, AppError);
13
+ if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor);
13
14
  }
14
15
  }
15
16
 
16
- export class UnauthorizedError extends AppError {
17
- constructor(message = 'Authentication required') {
18
- super(message, 'UNAUTHORIZED', 401);
19
- this.name = 'UnauthorizedError';
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
- export class PermissionError extends AppError {
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
- export class NotFoundError extends AppError {
31
- constructor(entity = 'Resource', id = null) {
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
- export class ValidationError extends AppError {
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
- export class DatabaseError extends AppError {
49
- constructor(operation = 'Database operation', originalError = null) {
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
- export class ConflictError extends AppError {
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
- export class BadRequestError extends AppError {
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, String(value ?? ''));
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
- return `<span class="avatar-item" style="background:${item.color}">${item.name.charAt(0)}</span>`;
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 String(item);
58
+ return esc(item);
50
59
  }
51
60
 
52
61
  export function getComponentConfig(componentId) {
@@ -121,6 +121,7 @@ export function nameHash(name) {
121
121
  }
122
122
 
123
123
  export { getInitials } from '@/lib/utils.js'
124
+ export { icon } from '@/ui/format-helpers.js'
124
125
 
125
126
  export function fmtDate(ts) {
126
127
  if (!ts) return '-';