@autumnsgrove/groveengine 0.4.4 → 0.4.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/README.md +20 -0
- package/dist/ui/components/ui/toast.js +5 -5
- package/dist/utils/csrf.js +41 -34
- package/dist/utils/sanitize.js +241 -81
- package/package.json +2 -1
- package/static/favicon.png +1 -0
- package/static/fonts/AtkinsonHyperlegible-Regular.ttf +0 -0
- package/static/fonts/BodoniModa-Regular.ttf +0 -0
- package/static/fonts/Cormorant-Regular.ttf +0 -0
- package/static/fonts/CozetteVector.ttf +0 -0
- package/static/fonts/IBMPlexMono-Regular.ttf +0 -0
- package/static/fonts/Lexend-Regular.ttf +0 -0
- package/static/fonts/OpenDyslexic-Regular.otf +0 -0
- package/static/fonts/Quicksand-Regular.ttf +0 -0
- package/static/fonts/alagard.ttf +0 -0
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
|
|
@@ -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
|
};
|
package/dist/utils/csrf.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @returns {string} UUID v4 token
|
|
9
9
|
*/
|
|
10
10
|
export function generateCSRFToken() {
|
|
11
|
-
|
|
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
|
-
|
|
21
|
+
if (!sessionToken) return false;
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
const headerToken = request.headers.get("x-csrf-token");
|
|
24
|
+
const bodyToken = request.headers.get("csrf-token"); // fallback
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
if (!headerToken && !bodyToken) return false;
|
|
27
27
|
|
|
28
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
// Handle edge cases
|
|
38
|
+
if (!request || typeof request !== "object") {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
if (!request.headers || typeof request.headers.get !== "function") {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
const origin = request.headers.get("origin");
|
|
47
|
+
const host = request.headers.get("host");
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
// Allow same-origin requests
|
|
50
|
+
if (origin) {
|
|
51
|
+
try {
|
|
52
|
+
const originUrl = new URL(origin);
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
// Validate protocol (must be http or https)
|
|
55
|
+
if (!["http:", "https:"].includes(originUrl.protocol)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
const isLocalhost =
|
|
60
|
+
originUrl.hostname === "localhost" ||
|
|
61
|
+
originUrl.hostname === "127.0.0.1";
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
63
|
+
// Require HTTPS for non-localhost
|
|
64
|
+
if (!isLocalhost && originUrl.protocol !== "https:") {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
70
67
|
|
|
71
|
-
|
|
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
|
}
|
package/dist/utils/sanitize.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
if (!html || typeof html !== "string") {
|
|
15
|
+
return "";
|
|
16
|
+
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
60
|
+
if (!svg || typeof svg !== "string") {
|
|
61
|
+
return "";
|
|
62
|
+
}
|
|
39
63
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
155
|
+
if (!markdownHTML || typeof markdownHTML !== "string") {
|
|
156
|
+
return "";
|
|
157
|
+
}
|
|
72
158
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
261
|
+
if (!url || typeof url !== "string") {
|
|
262
|
+
return "";
|
|
263
|
+
}
|
|
104
264
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
265
|
+
// Allow relative URLs
|
|
266
|
+
if (url.startsWith("/") || url.startsWith("./") || url.startsWith("../")) {
|
|
267
|
+
return url;
|
|
268
|
+
}
|
|
109
269
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
+
"version": "0.4.7",
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|