@autumnsgrove/groveengine 0.4.3 → 0.4.6

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
@@ -104,6 +104,26 @@ RESEND_API_KEY=re_xxxxx
104
104
  npx wrangler pages deploy
105
105
  ```
106
106
 
107
+ ## Fonts
108
+
109
+ GroveEngine includes self-hosted accessibility-focused fonts in `static/fonts/`. After installing the package, copy the fonts to your project's static directory:
110
+
111
+ ```bash
112
+ # Copy fonts from node_modules to your static folder
113
+ cp -r node_modules/@autumnsgrove/groveengine/static/fonts/ static/fonts/
114
+ ```
115
+
116
+ **Included fonts:**
117
+ - `alagard.ttf` - Pixel art style
118
+ - `AtkinsonHyperlegible-Regular.ttf` - High legibility
119
+ - `CozetteVector.ttf` - Bitmap style
120
+ - `Cormorant-Regular.ttf` - Elegant serif
121
+ - `Lexend-Regular.ttf` - Reading optimized
122
+ - `OpenDyslexic-Regular.otf` - Dyslexia-friendly
123
+ - `Quicksand-Regular.ttf` - Rounded sans-serif
124
+
125
+ Your `@font-face` declarations should reference `/fonts/fontname.ttf`.
126
+
107
127
  ## Key Components
108
128
 
109
129
  ### Gutter System
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { Toaster } from "sonner";
2
+ import { Toaster } from "svelte-sonner";
3
3
 
4
4
  interface Props {
5
5
  position?:
@@ -25,7 +25,7 @@ export const toast = {
25
25
  success: (message, options) => {
26
26
  sonnerToast.success(message, {
27
27
  duration: options?.duration || 3000,
28
- description: options?.description
28
+ description: options?.description,
29
29
  });
30
30
  },
31
31
  /**
@@ -37,7 +37,7 @@ export const toast = {
37
37
  error: (message, options) => {
38
38
  sonnerToast.error(message, {
39
39
  duration: options?.duration || 4000,
40
- description: options?.description
40
+ description: options?.description,
41
41
  });
42
42
  },
43
43
  /**
@@ -49,7 +49,7 @@ export const toast = {
49
49
  info: (message, options) => {
50
50
  sonnerToast.info(message, {
51
51
  duration: options?.duration || 3000,
52
- description: options?.description
52
+ description: options?.description,
53
53
  });
54
54
  },
55
55
  /**
@@ -61,7 +61,7 @@ export const toast = {
61
61
  warning: (message, options) => {
62
62
  sonnerToast.warning(message, {
63
63
  duration: options?.duration || 3500,
64
- description: options?.description
64
+ description: options?.description,
65
65
  });
66
66
  },
67
67
  /**
@@ -95,5 +95,5 @@ export const toast = {
95
95
  */
96
96
  dismissAll: () => {
97
97
  sonnerToast.dismiss();
98
- }
98
+ },
99
99
  };
@@ -8,7 +8,7 @@
8
8
  * @returns {string} UUID v4 token
9
9
  */
10
10
  export function generateCSRFToken() {
11
- return crypto.randomUUID();
11
+ return crypto.randomUUID();
12
12
  }
13
13
 
14
14
  /**
@@ -18,14 +18,14 @@ export function generateCSRFToken() {
18
18
  * @returns {boolean}
19
19
  */
20
20
  export function validateCSRFToken(request, sessionToken) {
21
- if (!sessionToken) return false;
21
+ if (!sessionToken) return false;
22
22
 
23
- const headerToken = request.headers.get('x-csrf-token');
24
- const bodyToken = request.headers.get('csrf-token'); // fallback
23
+ const headerToken = request.headers.get("x-csrf-token");
24
+ const bodyToken = request.headers.get("csrf-token"); // fallback
25
25
 
26
- if (!headerToken && !bodyToken) return false;
26
+ if (!headerToken && !bodyToken) return false;
27
27
 
28
- return (headerToken === sessionToken) || (bodyToken === sessionToken);
28
+ return headerToken === sessionToken || bodyToken === sessionToken;
29
29
  }
30
30
 
31
31
  /**
@@ -34,39 +34,46 @@ export function validateCSRFToken(request, sessionToken) {
34
34
  * @returns {boolean}
35
35
  */
36
36
  export function validateCSRF(request) {
37
- // Handle edge cases
38
- if (!request || typeof request !== 'object') {
39
- return false;
40
- }
37
+ // Handle edge cases
38
+ if (!request || typeof request !== "object") {
39
+ return false;
40
+ }
41
41
 
42
- if (!request.headers || typeof request.headers.get !== 'function') {
43
- return false;
44
- }
42
+ if (!request.headers || typeof request.headers.get !== "function") {
43
+ return false;
44
+ }
45
45
 
46
- const origin = request.headers.get('origin');
47
- const host = request.headers.get('host');
46
+ const origin = request.headers.get("origin");
47
+ const host = request.headers.get("host");
48
48
 
49
- // Allow same-origin requests
50
- if (origin) {
51
- try {
52
- const originUrl = new URL(origin);
49
+ // Allow same-origin requests
50
+ if (origin) {
51
+ try {
52
+ const originUrl = new URL(origin);
53
53
 
54
- // Validate protocol (must be http or https)
55
- if (!['http:', 'https:'].includes(originUrl.protocol)) {
56
- return false;
57
- }
54
+ // Validate protocol (must be http or https)
55
+ if (!["http:", "https:"].includes(originUrl.protocol)) {
56
+ return false;
57
+ }
58
58
 
59
- const isLocalhost = originUrl.hostname === 'localhost' ||
60
- originUrl.hostname === '127.0.0.1';
61
- const hostMatches = host && originUrl.host === host;
59
+ const isLocalhost =
60
+ originUrl.hostname === "localhost" ||
61
+ originUrl.hostname === "127.0.0.1";
62
62
 
63
- if (!isLocalhost && !hostMatches) {
64
- return false;
65
- }
66
- } catch {
67
- return false;
68
- }
69
- }
63
+ // Require HTTPS for non-localhost
64
+ if (!isLocalhost && originUrl.protocol !== "https:") {
65
+ return false;
66
+ }
70
67
 
71
- return true;
68
+ const hostMatches = host && originUrl.host === host;
69
+
70
+ if (!isLocalhost && !hostMatches) {
71
+ return false;
72
+ }
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+
78
+ return true;
72
79
  }
@@ -3,7 +3,7 @@
3
3
  * Uses isomorphic-dompurify for both server-side and client-side sanitization
4
4
  */
5
5
 
6
- import DOMPurify from 'isomorphic-dompurify';
6
+ import DOMPurify from "isomorphic-dompurify";
7
7
 
8
8
  /**
9
9
  * Sanitize HTML content to prevent XSS attacks
@@ -11,20 +11,44 @@ import DOMPurify from 'isomorphic-dompurify';
11
11
  * @returns {string} - Sanitized HTML safe for rendering
12
12
  */
13
13
  export function sanitizeHTML(html) {
14
- if (!html || typeof html !== 'string') {
15
- return '';
16
- }
14
+ if (!html || typeof html !== "string") {
15
+ return "";
16
+ }
17
17
 
18
- const config = {
19
- FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'link', 'style', 'form', 'input', 'button', 'base', 'meta'],
20
- FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onfocus', 'onblur', 'onchange', 'onsubmit', 'onmouseenter', 'onmouseleave'],
21
- ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|tel):)/i,
22
- ALLOW_DATA_ATTR: false,
23
- KEEP_CONTENT: true,
24
- SAFE_FOR_TEMPLATES: true
25
- };
18
+ const config = {
19
+ FORBID_TAGS: [
20
+ "script",
21
+ "iframe",
22
+ "object",
23
+ "embed",
24
+ "link",
25
+ "style",
26
+ "form",
27
+ "input",
28
+ "button",
29
+ "base",
30
+ "meta",
31
+ ],
32
+ FORBID_ATTR: [
33
+ "onerror",
34
+ "onload",
35
+ "onclick",
36
+ "onmouseover",
37
+ "onfocus",
38
+ "onblur",
39
+ "onchange",
40
+ "onsubmit",
41
+ "onmouseenter",
42
+ "onmouseleave",
43
+ "style",
44
+ ],
45
+ ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|tel):)/i,
46
+ ALLOW_DATA_ATTR: false,
47
+ KEEP_CONTENT: true,
48
+ SAFE_FOR_TEMPLATES: true,
49
+ };
26
50
 
27
- return DOMPurify.sanitize(html, config);
51
+ return DOMPurify.sanitize(html, config);
28
52
  }
29
53
 
30
54
  /**
@@ -33,30 +57,92 @@ export function sanitizeHTML(html) {
33
57
  * @returns {string} - Sanitized SVG safe for rendering
34
58
  */
35
59
  export function sanitizeSVG(svg) {
36
- if (!svg || typeof svg !== 'string') {
37
- return '';
38
- }
60
+ if (!svg || typeof svg !== "string") {
61
+ return "";
62
+ }
39
63
 
40
- return DOMPurify.sanitize(svg, {
41
- USE_PROFILES: { svg: true, svgFilters: true },
42
- ALLOWED_TAGS: [
43
- 'svg', 'g', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon',
44
- 'ellipse', 'text', 'tspan', 'defs', 'marker', 'pattern', 'clipPath',
45
- 'mask', 'linearGradient', 'radialGradient', 'stop', 'use', 'symbol',
46
- 'title', 'desc'
47
- ],
48
- ALLOWED_ATTR: [
49
- 'class', 'id', 'transform', 'fill', 'stroke', 'stroke-width',
50
- 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'r', 'rx', 'ry',
51
- 'width', 'height', 'd', 'points', 'viewBox', 'xmlns', 'version',
52
- 'preserveAspectRatio', 'opacity', 'fill-opacity', 'stroke-opacity'
53
- ],
54
- FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'link', 'style', 'foreignObject', 'image', 'a'],
55
- FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onfocus', 'onblur', 'style', 'href', 'xlink:href'],
56
- ALLOWED_URI_REGEXP: /^$/,
57
- KEEP_CONTENT: false,
58
- SAFE_FOR_TEMPLATES: true
59
- });
64
+ return DOMPurify.sanitize(svg, {
65
+ USE_PROFILES: { svg: true, svgFilters: true },
66
+ ALLOWED_TAGS: [
67
+ "svg",
68
+ "g",
69
+ "path",
70
+ "circle",
71
+ "rect",
72
+ "line",
73
+ "polyline",
74
+ "polygon",
75
+ "ellipse",
76
+ "text",
77
+ "tspan",
78
+ "defs",
79
+ "marker",
80
+ "pattern",
81
+ "clipPath",
82
+ "mask",
83
+ "linearGradient",
84
+ "radialGradient",
85
+ "stop",
86
+ "use",
87
+ "symbol",
88
+ "title",
89
+ "desc",
90
+ ],
91
+ ALLOWED_ATTR: [
92
+ "class",
93
+ "id",
94
+ "transform",
95
+ "fill",
96
+ "stroke",
97
+ "stroke-width",
98
+ "x",
99
+ "y",
100
+ "x1",
101
+ "y1",
102
+ "x2",
103
+ "y2",
104
+ "cx",
105
+ "cy",
106
+ "r",
107
+ "rx",
108
+ "ry",
109
+ "width",
110
+ "height",
111
+ "d",
112
+ "points",
113
+ "viewBox",
114
+ "xmlns",
115
+ "version",
116
+ "preserveAspectRatio",
117
+ "opacity",
118
+ "fill-opacity",
119
+ "stroke-opacity",
120
+ ],
121
+ FORBID_TAGS: [
122
+ "script",
123
+ "iframe",
124
+ "object",
125
+ "embed",
126
+ "link",
127
+ "style",
128
+ "foreignObject",
129
+ "image",
130
+ "a",
131
+ ],
132
+ FORBID_ATTR: [
133
+ "onerror",
134
+ "onload",
135
+ "onclick",
136
+ "onmouseover",
137
+ "onfocus",
138
+ "onblur",
139
+ "style",
140
+ "href",
141
+ "xlink:href",
142
+ ],
143
+ KEEP_CONTENT: false,
144
+ SAFE_FOR_TEMPLATES: true,
145
+ });
60
146
  }
61
147
 
62
148
  /**
@@ -66,30 +152,104 @@ export function sanitizeSVG(svg) {
66
152
  * @returns {string} - Sanitized HTML safe for rendering
67
153
  */
68
154
  export function sanitizeMarkdown(markdownHTML) {
69
- if (!markdownHTML || typeof markdownHTML !== 'string') {
70
- return '';
71
- }
155
+ if (!markdownHTML || typeof markdownHTML !== "string") {
156
+ return "";
157
+ }
72
158
 
73
- // For markdown, we allow a broader set of tags but still sanitize
74
- return DOMPurify.sanitize(markdownHTML, {
75
- ALLOWED_TAGS: [
76
- 'a', 'abbr', 'b', 'blockquote', 'br', 'code', 'dd', 'del', 'div', 'dl', 'dt',
77
- 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'ins', 'kbd',
78
- 'li', 'mark', 'ol', 'p', 'pre', 'q', 's', 'samp', 'small', 'span', 'strong',
79
- 'sub', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'u', 'ul',
80
- 'var', 'input', 'label'
81
- ],
82
- ALLOWED_ATTR: [
83
- 'href', 'src', 'alt', 'title', 'class', 'id', 'target', 'rel',
84
- 'width', 'height', 'align', 'type', 'checked', 'disabled'
85
- ],
86
- FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'link', 'style', 'form', 'button'],
87
- FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onfocus', 'onblur', 'onchange', 'onsubmit', 'style'],
88
- ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|tel):)/i,
89
- ALLOW_DATA_ATTR: false,
90
- KEEP_CONTENT: true,
91
- SAFE_FOR_TEMPLATES: true
92
- });
159
+ // For markdown, we allow a broader set of tags but still sanitize
160
+ return DOMPurify.sanitize(markdownHTML, {
161
+ ALLOWED_TAGS: [
162
+ "a",
163
+ "abbr",
164
+ "b",
165
+ "blockquote",
166
+ "br",
167
+ "code",
168
+ "dd",
169
+ "del",
170
+ "div",
171
+ "dl",
172
+ "dt",
173
+ "em",
174
+ "h1",
175
+ "h2",
176
+ "h3",
177
+ "h4",
178
+ "h5",
179
+ "h6",
180
+ "hr",
181
+ "i",
182
+ "img",
183
+ "ins",
184
+ "kbd",
185
+ "li",
186
+ "mark",
187
+ "ol",
188
+ "p",
189
+ "pre",
190
+ "q",
191
+ "s",
192
+ "samp",
193
+ "small",
194
+ "span",
195
+ "strong",
196
+ "sub",
197
+ "sup",
198
+ "table",
199
+ "tbody",
200
+ "td",
201
+ "tfoot",
202
+ "th",
203
+ "thead",
204
+ "tr",
205
+ "u",
206
+ "ul",
207
+ "var",
208
+ "input",
209
+ "label",
210
+ ],
211
+ ALLOWED_ATTR: [
212
+ "href",
213
+ "src",
214
+ "alt",
215
+ "title",
216
+ "class",
217
+ "id",
218
+ "target",
219
+ "rel",
220
+ "width",
221
+ "height",
222
+ "align",
223
+ "type",
224
+ "checked",
225
+ "disabled",
226
+ ],
227
+ FORBID_TAGS: [
228
+ "script",
229
+ "iframe",
230
+ "object",
231
+ "embed",
232
+ "link",
233
+ "style",
234
+ "form",
235
+ "button",
236
+ ],
237
+ FORBID_ATTR: [
238
+ "onerror",
239
+ "onload",
240
+ "onclick",
241
+ "onmouseover",
242
+ "onfocus",
243
+ "onblur",
244
+ "onchange",
245
+ "onsubmit",
246
+ "style",
247
+ ],
248
+ ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|tel):)/i,
249
+ ALLOW_DATA_ATTR: false,
250
+ KEEP_CONTENT: true,
251
+ SAFE_FOR_TEMPLATES: true,
252
+ });
93
253
  }
94
254
 
95
255
  /**
@@ -98,30 +258,30 @@ export function sanitizeMarkdown(markdownHTML) {
98
258
  * @returns {string} - Sanitized URL (returns empty string if dangerous)
99
259
  */
100
260
  export function sanitizeURL(url) {
101
- if (!url || typeof url !== 'string') {
102
- return '';
103
- }
261
+ if (!url || typeof url !== "string") {
262
+ return "";
263
+ }
104
264
 
105
- // Allow relative URLs
106
- if (url.startsWith('/') || url.startsWith('./') || url.startsWith('../')) {
107
- return url;
108
- }
265
+ // Allow relative URLs
266
+ if (url.startsWith("/") || url.startsWith("./") || url.startsWith("../")) {
267
+ return url;
268
+ }
109
269
 
110
- // Check for dangerous protocols
111
- const dangerous = /^(javascript|data|vbscript|file|about):/i;
112
- if (dangerous.test(url)) {
113
- return '';
114
- }
270
+ // Check for dangerous protocols
271
+ const dangerous = /^(javascript|data|vbscript|file|about):/i;
272
+ if (dangerous.test(url)) {
273
+ return "";
274
+ }
115
275
 
116
- // Only allow safe protocols
117
- const safe = /^(https?|mailto|tel):/i;
118
- if (!safe.test(url)) {
119
- // If no protocol, assume relative
120
- if (!url.includes(':')) {
121
- return url;
122
- }
123
- return '';
124
- }
276
+ // Only allow safe protocols
277
+ const safe = /^(https?|mailto|tel):/i;
278
+ if (!safe.test(url)) {
279
+ // If no protocol, assume relative
280
+ if (!url.includes(":")) {
281
+ return url;
282
+ }
283
+ return "";
284
+ }
125
285
 
126
- return url;
286
+ return url;
127
287
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autumnsgrove/groveengine",
3
- "version": "0.4.3",
3
+ "version": "0.4.6",
4
4
  "description": "Multi-tenant blog engine for Grove Platform. Features gutter annotations, markdown editing, magic code auth, and Cloudflare Workers deployment.",
5
5
  "author": "AutumnsGrove",
6
6
  "license": "MIT",
@@ -127,6 +127,7 @@
127
127
  },
128
128
  "files": [
129
129
  "dist",
130
+ "static",
130
131
  "!dist/**/*.test.*"
131
132
  ],
132
133
  "scripts": {
@@ -0,0 +1 @@
1
+ This is a placeholder. Please replace with an actual favicon.
Binary file
Binary file
Binary file