@schalkneethling/toolkit 0.1.5 → 0.3.0

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.
Files changed (30) hide show
  1. package/README.md +29 -7
  2. package/dist/index.mjs +90 -6
  3. package/dist/index.mjs.map +1 -1
  4. package/hooks/auto-approve-safe-commands/hook.mjs +134 -0
  5. package/hooks/auto-approve-safe-commands/hook.mts +188 -0
  6. package/hooks/auto-approve-safe-commands/settings-fragment.json +17 -0
  7. package/hooks/block-dangerous-commands/hook.mjs +3 -3
  8. package/hooks/block-dangerous-commands/hook.mts +23 -10
  9. package/package.json +8 -10
  10. package/skills/css-coder/SKILL.md +95 -0
  11. package/skills/css-coder/references/patterns.md +224 -0
  12. package/skills/css-tokens/README.md +152 -0
  13. package/skills/css-tokens/SKILL.md +125 -0
  14. package/skills/css-tokens/references/tokens.css +162 -0
  15. package/skills/frontend-security/SKILL.md +134 -0
  16. package/skills/frontend-security/references/csp-configuration.md +191 -0
  17. package/skills/frontend-security/references/csrf-protection.md +327 -0
  18. package/skills/frontend-security/references/dom-security.md +229 -0
  19. package/skills/frontend-security/references/file-upload-security.md +310 -0
  20. package/skills/frontend-security/references/framework-patterns.md +307 -0
  21. package/skills/frontend-security/references/input-validation.md +232 -0
  22. package/skills/frontend-security/references/jwt-security.md +300 -0
  23. package/skills/frontend-security/references/nodejs-npm-security.md +261 -0
  24. package/skills/frontend-security/references/xss-prevention.md +163 -0
  25. package/skills/frontend-testing/SKILL.md +357 -0
  26. package/skills/frontend-testing/references/accessibility-testing.md +368 -0
  27. package/skills/frontend-testing/references/aria-snapshots.md +517 -0
  28. package/skills/frontend-testing/references/locator-strategies.md +295 -0
  29. package/skills/frontend-testing/references/visual-regression.md +466 -0
  30. package/skills/refined-plan-mode/SKILL.md +84 -0
@@ -0,0 +1,307 @@
1
+ # Framework-Specific Security Patterns
2
+
3
+ ## React Security
4
+
5
+ ### XSS Prevention
6
+
7
+ ```jsx
8
+ // DEFAULT SAFE - React escapes by default
9
+ <div>{userInput}</div>
10
+
11
+ // DANGEROUS - bypasses escaping
12
+ <div dangerouslySetInnerHTML={{ __html: userInput }} />
13
+
14
+ // If HTML is required, sanitize first
15
+ import DOMPurify from 'dompurify';
16
+ <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />
17
+ ```
18
+
19
+ ### URL Handling
20
+
21
+ ```jsx
22
+ // DANGEROUS - javascript: URLs in href
23
+ <a href={userInput}>Link</a>
24
+
25
+ // SAFE - validate URL protocol
26
+ function SafeLink({ href, children }) {
27
+ const safeHref = useMemo(() => {
28
+ try {
29
+ const url = new URL(href, window.location.origin);
30
+ if (['http:', 'https:', 'mailto:'].includes(url.protocol)) {
31
+ return href;
32
+ }
33
+ } catch {}
34
+ return '#';
35
+ }, [href]);
36
+
37
+ return <a href={safeHref}>{children}</a>;
38
+ }
39
+ ```
40
+
41
+ ### State and Props
42
+
43
+ ```jsx
44
+ // DANGEROUS - spreading user-controlled props
45
+ <Component {...userControlledObject} />
46
+
47
+ // SAFE - explicitly pass allowed props
48
+ <Component
49
+ title={userControlledObject.title}
50
+ description={userControlledObject.description}
51
+ />
52
+ ```
53
+
54
+ ### Server-Side Rendering (SSR)
55
+
56
+ ```jsx
57
+ // DANGEROUS - injecting user data into SSR without escaping
58
+ <script>
59
+ window.__INITIAL_STATE__ = {JSON.stringify(userControlledData)}
60
+ </script>
61
+
62
+ // SAFE - serialize with escaping
63
+ import serialize from 'serialize-javascript';
64
+ <script
65
+ dangerouslySetInnerHTML={{
66
+ __html: `window.__INITIAL_STATE__ = ${serialize(data, { isJSON: true })}`
67
+ }}
68
+ />
69
+ ```
70
+
71
+ ## Astro Security
72
+
73
+ ### Content Escaping
74
+
75
+ ```astro
76
+ ---
77
+ const userInput = Astro.props.userInput;
78
+ ---
79
+
80
+ <!-- SAFE - auto-escaped -->
81
+ <div>{userInput}</div>
82
+
83
+ <!-- DANGEROUS - bypasses escaping -->
84
+ <div set:html={userInput} />
85
+
86
+ <!-- If HTML required, sanitize -->
87
+ ---
88
+ import DOMPurify from 'dompurify';
89
+ const sanitized = DOMPurify.sanitize(userInput);
90
+ ---
91
+ <div set:html={sanitized} />
92
+ ```
93
+
94
+ ### Dynamic Imports
95
+
96
+ ```astro
97
+ ---
98
+ // DANGEROUS - user-controlled import path
99
+ const component = await import(userInput);
100
+
101
+ // SAFE - allowlist approach
102
+ const allowedComponents = {
103
+ 'card': () => import('./Card.astro'),
104
+ 'button': () => import('./Button.astro')
105
+ };
106
+
107
+ const loadComponent = allowedComponents[userInput];
108
+ if (!loadComponent) throw new Error('Invalid component');
109
+ const Component = await loadComponent();
110
+ ---
111
+ ```
112
+
113
+ ### API Endpoints
114
+
115
+ ```javascript
116
+ // src/pages/api/data.js
117
+ export async function POST({ request }) {
118
+ // Validate Content-Type
119
+ const contentType = request.headers.get('content-type');
120
+ if (!contentType?.includes('application/json')) {
121
+ return new Response('Invalid content type', { status: 415 });
122
+ }
123
+
124
+ // Validate and sanitize input
125
+ const body = await request.json();
126
+ if (!validateInput(body)) {
127
+ return new Response('Invalid input', { status: 400 });
128
+ }
129
+
130
+ // Process request
131
+ return new Response(JSON.stringify(result), {
132
+ headers: { 'Content-Type': 'application/json' }
133
+ });
134
+ }
135
+ ```
136
+
137
+ ## Twig Security
138
+
139
+ ### Output Escaping
140
+
141
+ ```twig
142
+ {# SAFE - auto-escaped for HTML context #}
143
+ {{ userInput }}
144
+
145
+ {# DANGEROUS - raw bypasses escaping #}
146
+ {{ userInput|raw }}
147
+
148
+ {# DANGEROUS - autoescape disabled #}
149
+ {% autoescape false %}
150
+ {{ userInput }}
151
+ {% endautoescape %}
152
+
153
+ {# Context-specific escaping #}
154
+ {{ userInput|e('html') }}
155
+ {{ userInput|e('js') }}
156
+ {{ userInput|e('css') }}
157
+ {{ userInput|e('url') }}
158
+ {{ userInput|e('html_attr') }}
159
+ ```
160
+
161
+ ### Template Inclusion
162
+
163
+ ```twig
164
+ {# DANGEROUS - user-controlled template path #}
165
+ {% include userInput %}
166
+
167
+ {# SAFE - use allowlist #}
168
+ {% if templateName in ['header', 'footer', 'sidebar'] %}
169
+ {% include templateName ~ '.html.twig' %}
170
+ {% endif %}
171
+ ```
172
+
173
+ ### Sandbox Mode (Symfony)
174
+
175
+ ```yaml
176
+ # config/packages/twig.yaml
177
+ twig:
178
+ sandbox:
179
+ policy:
180
+ tags: ['if', 'for', 'set']
181
+ filters: ['escape', 'upper', 'lower']
182
+ methods:
183
+ Symfony\Component\Routing\Generator\UrlGeneratorInterface: ['generate']
184
+ properties: []
185
+ functions: ['path', 'url']
186
+ ```
187
+
188
+ ### CSRF in Forms
189
+
190
+ ```twig
191
+ {# Symfony CSRF protection #}
192
+ <form method="post">
193
+ <input type="hidden" name="_csrf_token" value="{{ csrf_token('form_name') }}">
194
+ {# form fields #}
195
+ </form>
196
+ ```
197
+
198
+ ## Bun Security
199
+
200
+ ### Request Handling
201
+
202
+ ```javascript
203
+ // Bun HTTP server
204
+ Bun.serve({
205
+ port: 3000,
206
+ fetch(req) {
207
+ const url = new URL(req.url);
208
+
209
+ // Validate origin for CORS
210
+ const origin = req.headers.get('origin');
211
+ if (origin && !isAllowedOrigin(origin)) {
212
+ return new Response('Forbidden', { status: 403 });
213
+ }
214
+
215
+ // Rate limiting
216
+ if (isRateLimited(req)) {
217
+ return new Response('Too Many Requests', { status: 429 });
218
+ }
219
+
220
+ return handleRequest(req);
221
+ }
222
+ });
223
+ ```
224
+
225
+ ### File Handling
226
+
227
+ ```javascript
228
+ // Validate file paths
229
+ function safeReadFile(userPath) {
230
+ const baseDir = '/app/public';
231
+ const resolved = Bun.resolveSync(userPath, baseDir);
232
+
233
+ if (!resolved.startsWith(baseDir)) {
234
+ throw new Error('Path traversal detected');
235
+ }
236
+
237
+ return Bun.file(resolved).text();
238
+ }
239
+ ```
240
+
241
+ ## HTML5 APIs Security
242
+
243
+ ### Web Storage
244
+
245
+ ```javascript
246
+ // NEVER store sensitive data in localStorage
247
+ localStorage.setItem('token', jwt); // DANGEROUS
248
+
249
+ // Use httpOnly cookies for tokens instead
250
+ // Or store in memory with short expiration
251
+
252
+ // If localStorage is necessary, encrypt
253
+ import { encrypt, decrypt } from './crypto';
254
+ localStorage.setItem('data', encrypt(sensitiveData, key));
255
+ ```
256
+
257
+ ### postMessage
258
+
259
+ ```javascript
260
+ // Always validate origin and data
261
+ window.addEventListener('message', (event) => {
262
+ // Validate origin
263
+ const allowedOrigins = ['https://trusted.com'];
264
+ if (!allowedOrigins.includes(event.origin)) return;
265
+
266
+ // Validate data structure
267
+ if (typeof event.data !== 'object') return;
268
+ if (!['action1', 'action2'].includes(event.data.type)) return;
269
+
270
+ handleMessage(event.data);
271
+ });
272
+
273
+ // Always specify target origin when sending
274
+ iframe.contentWindow.postMessage(data, 'https://specific-origin.com');
275
+ // NEVER use '*' for sensitive data
276
+ ```
277
+
278
+ ### WebSockets
279
+
280
+ ```javascript
281
+ // Validate WebSocket origin
282
+ const wss = new WebSocket.Server({
283
+ server,
284
+ verifyClient: ({ origin, req }, callback) => {
285
+ const allowed = ['https://myapp.com'];
286
+ callback(allowed.includes(origin));
287
+ }
288
+ });
289
+
290
+ // Validate messages
291
+ wss.on('connection', (ws) => {
292
+ ws.on('message', (data) => {
293
+ try {
294
+ const msg = JSON.parse(data);
295
+ if (!isValidMessage(msg)) {
296
+ ws.close(1008, 'Invalid message');
297
+ return;
298
+ }
299
+ handleMessage(msg);
300
+ } catch {
301
+ ws.close(1008, 'Invalid JSON');
302
+ }
303
+ });
304
+ });
305
+ ```
306
+
307
+ OWASP Reference: https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html
@@ -0,0 +1,232 @@
1
+ # Input Validation Reference
2
+
3
+ ## Validation Strategy
4
+
5
+ **Always validate on the server.** Client-side validation improves UX but provides no security.
6
+
7
+ ### Allowlist vs Denylist
8
+
9
+ ```javascript
10
+ // PREFERRED: Allowlist (accept known good)
11
+ function validateUsername(input) {
12
+ const allowedPattern = /^[a-zA-Z0-9_]{3,20}$/;
13
+ return allowedPattern.test(input);
14
+ }
15
+
16
+ // AVOID: Denylist (block known bad)
17
+ function validateInput(input) {
18
+ const blocked = ['<script>', 'javascript:', 'onerror'];
19
+ return !blocked.some(bad => input.includes(bad)); // Easily bypassed
20
+ }
21
+ ```
22
+
23
+ ## Common Validation Patterns
24
+
25
+ ### Email
26
+
27
+ ```javascript
28
+ // Basic validation (server should still verify)
29
+ function validateEmail(email) {
30
+ // Simple pattern - not comprehensive but catches most issues
31
+ const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
32
+ return pattern.test(email) && email.length <= 254;
33
+ }
34
+
35
+ // Use built-in browser validation
36
+ const input = document.createElement('input');
37
+ input.type = 'email';
38
+ input.value = email;
39
+ return input.checkValidity();
40
+ ```
41
+
42
+ ### URL
43
+
44
+ ```javascript
45
+ function validateUrl(input) {
46
+ try {
47
+ const url = new URL(input);
48
+ return ['http:', 'https:'].includes(url.protocol);
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ // For user-facing URLs, also check for malicious patterns
55
+ function validateSafeUrl(input) {
56
+ const url = validateUrl(input);
57
+ if (!url) return false;
58
+
59
+ // Block data: and javascript: schemes
60
+ const dangerous = ['javascript:', 'data:', 'vbscript:'];
61
+ return !dangerous.some(scheme => input.toLowerCase().startsWith(scheme));
62
+ }
63
+ ```
64
+
65
+ ### Numbers
66
+
67
+ ```javascript
68
+ function validateInteger(input, min, max) {
69
+ const num = parseInt(input, 10);
70
+ if (isNaN(num)) return false;
71
+ if (num.toString() !== input.toString()) return false; // Reject "123abc"
72
+ return num >= min && num <= max;
73
+ }
74
+
75
+ function validateDecimal(input, min, max, decimals) {
76
+ const num = parseFloat(input);
77
+ if (isNaN(num)) return false;
78
+ if (num < min || num > max) return false;
79
+
80
+ const parts = input.split('.');
81
+ if (parts.length > 2) return false;
82
+ if (parts[1] && parts[1].length > decimals) return false;
83
+
84
+ return true;
85
+ }
86
+ ```
87
+
88
+ ### Date
89
+
90
+ ```javascript
91
+ function validateDate(input) {
92
+ const date = new Date(input);
93
+ return date instanceof Date && !isNaN(date);
94
+ }
95
+
96
+ function validateDateRange(input, minDate, maxDate) {
97
+ const date = new Date(input);
98
+ if (isNaN(date)) return false;
99
+ return date >= minDate && date <= maxDate;
100
+ }
101
+ ```
102
+
103
+ ### Phone Numbers
104
+
105
+ ```javascript
106
+ // International format
107
+ function validatePhone(input) {
108
+ // E.164 format: +[country][number], max 15 digits
109
+ const pattern = /^\+[1-9]\d{1,14}$/;
110
+ return pattern.test(input.replace(/[\s\-()]/g, ''));
111
+ }
112
+ ```
113
+
114
+ ## Sanitization Functions
115
+
116
+ ### HTML Entities
117
+
118
+ ```javascript
119
+ function escapeHtml(input) {
120
+ const map = {
121
+ '&': '&amp;',
122
+ '<': '&lt;',
123
+ '>': '&gt;',
124
+ '"': '&quot;',
125
+ "'": '&#x27;',
126
+ '/': '&#x2F;'
127
+ };
128
+ return String(input).replace(/[&<>"'/]/g, char => map[char]);
129
+ }
130
+ ```
131
+
132
+ ### SQL (Use Parameterized Queries Instead)
133
+
134
+ ```javascript
135
+ // WRONG - never build SQL strings
136
+ const query = `SELECT * FROM users WHERE name = '${userInput}'`;
137
+
138
+ // RIGHT - use parameterized queries
139
+ const query = 'SELECT * FROM users WHERE name = ?';
140
+ db.query(query, [userInput]);
141
+ ```
142
+
143
+ ### Path Traversal Prevention
144
+
145
+ ```javascript
146
+ const path = require('path');
147
+
148
+ function validateFilePath(userPath, baseDir) {
149
+ const baseCanonical = path.resolve(baseDir);
150
+ const resolved = path.resolve(baseDir, userPath);
151
+ const relativePath = path.relative(baseCanonical, resolved);
152
+
153
+ // Ensure resolved path stays inside the base directory
154
+ if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
155
+ throw new Error('Path traversal detected');
156
+ }
157
+
158
+ return resolved;
159
+ }
160
+ ```
161
+
162
+ ## Framework Validation
163
+
164
+ ### Node.js with Joi
165
+
166
+ ```javascript
167
+ const Joi = require('joi');
168
+
169
+ const userSchema = Joi.object({
170
+ username: Joi.string().alphanum().min(3).max(30).required(),
171
+ email: Joi.string().email().required(),
172
+ age: Joi.number().integer().min(0).max(150),
173
+ website: Joi.string().uri({ scheme: ['http', 'https'] })
174
+ });
175
+
176
+ function validateUser(data) {
177
+ const { error, value } = userSchema.validate(data);
178
+ if (error) throw new Error(error.details[0].message);
179
+ return value;
180
+ }
181
+ ```
182
+
183
+ ### Express Validator
184
+
185
+ ```javascript
186
+ const { body, validationResult } = require('express-validator');
187
+
188
+ app.post('/user',
189
+ body('email').isEmail().normalizeEmail(),
190
+ body('password').isLength({ min: 8 }),
191
+ body('age').isInt({ min: 0, max: 150 }),
192
+ (req, res) => {
193
+ const errors = validationResult(req);
194
+ if (!errors.isEmpty()) {
195
+ return res.status(400).json({ errors: errors.array() });
196
+ }
197
+ // Process valid input
198
+ }
199
+ );
200
+ ```
201
+
202
+ ### Zod (TypeScript)
203
+
204
+ ```typescript
205
+ import { z } from 'zod';
206
+
207
+ const UserSchema = z.object({
208
+ username: z.string().min(3).max(30).regex(/^[a-zA-Z0-9_]+$/),
209
+ email: z.string().email(),
210
+ age: z.number().int().min(0).max(150).optional(),
211
+ website: z.string().url().optional()
212
+ });
213
+
214
+ type User = z.infer<typeof UserSchema>;
215
+
216
+ function validateUser(data: unknown): User {
217
+ return UserSchema.parse(data);
218
+ }
219
+ ```
220
+
221
+ ## Validation Checklist
222
+
223
+ - [ ] Validate all input on the server
224
+ - [ ] Use allowlist validation when possible
225
+ - [ ] Validate data type, length, format, and range
226
+ - [ ] Reject unexpected input rather than sanitizing
227
+ - [ ] Use parameterized queries for database operations
228
+ - [ ] Validate file uploads (type, size, content)
229
+ - [ ] Canonicalize paths before validation
230
+ - [ ] Log validation failures for monitoring
231
+
232
+ OWASP Reference: https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html