@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,191 @@
1
+ # Content Security Policy Reference
2
+
3
+ ## Strict CSP Configuration
4
+
5
+ ### Nonce-Based (Recommended)
6
+
7
+ ```http
8
+ Content-Security-Policy:
9
+ script-src 'nonce-{random}' 'strict-dynamic';
10
+ object-src 'none';
11
+ base-uri 'none';
12
+ ```
13
+
14
+ Implementation:
15
+
16
+ ```javascript
17
+ // Generate unique nonce per request
18
+ const crypto = require('crypto');
19
+ const nonce = crypto.randomBytes(16).toString('base64');
20
+
21
+ // Set header
22
+ res.setHeader('Content-Security-Policy',
23
+ `script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none';`
24
+ );
25
+
26
+ // Include nonce in script tags
27
+ res.send(`<script nonce="${nonce}">/* safe code */</script>`);
28
+ ```
29
+
30
+ ### Hash-Based (For Static Scripts)
31
+
32
+ ```http
33
+ Content-Security-Policy:
34
+ script-src 'sha256-{hash}' 'strict-dynamic';
35
+ object-src 'none';
36
+ base-uri 'none';
37
+ ```
38
+
39
+ Generate hash:
40
+
41
+ ```bash
42
+ echo -n 'console.log("hello");' | openssl sha256 -binary | openssl base64
43
+ ```
44
+
45
+ ## Essential Directives
46
+
47
+ | Directive | Purpose | Recommended Value |
48
+ |-----------|---------|-------------------|
49
+ | `default-src` | Fallback for other directives | `'self'` |
50
+ | `script-src` | JavaScript sources | `'nonce-{random}' 'strict-dynamic'` |
51
+ | `style-src` | CSS sources | `'self' 'unsafe-inline'` (or nonces) |
52
+ | `img-src` | Image sources | `'self' data: https:` |
53
+ | `font-src` | Font sources | `'self'` |
54
+ | `connect-src` | AJAX/WebSocket/Fetch | `'self' https://api.example.com` |
55
+ | `frame-src` | iframe sources | `'none'` or specific origins |
56
+ | `object-src` | Plugin content | `'none'` |
57
+ | `base-uri` | Base URL restrictions | `'none'` |
58
+ | `form-action` | Form submission targets | `'self'` |
59
+ | `frame-ancestors` | Who can embed page | `'self'` or `'none'` |
60
+
61
+ ## Framework Integration
62
+
63
+ ### Express.js
64
+
65
+ ```javascript
66
+ const helmet = require('helmet');
67
+
68
+ // Nonce middleware
69
+ app.use((req, res, next) => {
70
+ res.locals.nonce = crypto.randomBytes(16).toString('base64');
71
+ next();
72
+ });
73
+
74
+ app.use(helmet.contentSecurityPolicy({
75
+ directives: {
76
+ defaultSrc: ["'self'"],
77
+ scriptSrc: [(req, res) => `'nonce-${res.locals.nonce}'`, "'strict-dynamic'"],
78
+ styleSrc: ["'self'", "'unsafe-inline'"],
79
+ imgSrc: ["'self'", "data:", "https:"],
80
+ objectSrc: ["'none'"],
81
+ baseUri: ["'none'"],
82
+ formAction: ["'self'"],
83
+ frameAncestors: ["'self'"]
84
+ }
85
+ }));
86
+ ```
87
+
88
+ ### Astro
89
+
90
+ ```javascript
91
+ // astro.config.mjs
92
+ export default defineConfig({
93
+ vite: {
94
+ plugins: [{
95
+ name: 'csp-plugin',
96
+ configureServer(server) {
97
+ server.middlewares.use((req, res, next) => {
98
+ res.setHeader('Content-Security-Policy',
99
+ "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
100
+ );
101
+ next();
102
+ });
103
+ }
104
+ }]
105
+ }
106
+ });
107
+ ```
108
+
109
+ ### Meta Tag (Fallback)
110
+
111
+ ```html
112
+ <meta http-equiv="Content-Security-Policy"
113
+ content="default-src 'self'; script-src 'self' 'nonce-abc123';">
114
+ ```
115
+
116
+ **Note**: Meta tag CSP cannot set `frame-ancestors`, `report-uri`, or `sandbox`.
117
+
118
+ ## Report-Only Mode
119
+
120
+ Test policies without enforcement:
121
+
122
+ ```http
123
+ Content-Security-Policy-Report-Only:
124
+ default-src 'self';
125
+ script-src 'self';
126
+ report-uri /csp-report;
127
+ ```
128
+
129
+ Report endpoint:
130
+
131
+ ```javascript
132
+ app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
133
+ console.log('CSP Violation:', req.body);
134
+ res.status(204).end();
135
+ });
136
+ ```
137
+
138
+ ## Common Violations and Fixes
139
+
140
+ ### Inline Scripts
141
+
142
+ ```html
143
+ <!-- Violation: inline script -->
144
+ <script>alert('hello');</script>
145
+
146
+ <!-- Fix: add nonce -->
147
+ <script nonce="abc123">alert('hello');</script>
148
+
149
+ <!-- Or: move to external file -->
150
+ <script src="/js/app.js"></script>
151
+ ```
152
+
153
+ ### Inline Event Handlers
154
+
155
+ ```html
156
+ <!-- Violation: inline event handler -->
157
+ <button onclick="handleClick()">Click</button>
158
+
159
+ <!-- Fix: use addEventListener -->
160
+ <button id="myBtn">Click</button>
161
+ <script nonce="abc123">
162
+ document.getElementById('myBtn').addEventListener('click', handleClick);
163
+ </script>
164
+ ```
165
+
166
+ ### Inline Styles
167
+
168
+ ```html
169
+ <!-- Violation: style attribute -->
170
+ <div style="color: red;">Text</div>
171
+
172
+ <!-- Fix: use classes -->
173
+ <div class="text-red">Text</div>
174
+
175
+ <!-- Or: add nonce to style tags -->
176
+ <style nonce="abc123">.text-red { color: red; }</style>
177
+ ```
178
+
179
+ ## Security Headers Companion
180
+
181
+ Deploy CSP with these additional headers:
182
+
183
+ ```http
184
+ X-Content-Type-Options: nosniff
185
+ X-Frame-Options: DENY
186
+ X-XSS-Protection: 0
187
+ Referrer-Policy: strict-origin-when-cross-origin
188
+ Permissions-Policy: geolocation=(), microphone=(), camera=()
189
+ ```
190
+
191
+ OWASP Reference: https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html
@@ -0,0 +1,327 @@
1
+ # CSRF Protection Reference
2
+
3
+ ## Token-Based Protection
4
+
5
+ ### Synchronizer Token Pattern
6
+
7
+ Generate unique per-session tokens and validate on state-changing requests:
8
+
9
+ ```javascript
10
+ // Server-side token generation (Node.js)
11
+ const crypto = require('crypto');
12
+
13
+ function generateCSRFToken(session) {
14
+ const token = crypto.randomBytes(32).toString('hex');
15
+ session.csrfToken = token;
16
+ return token;
17
+ }
18
+
19
+ // Middleware validation
20
+ function validateCSRF(req, res, next) {
21
+ const token = req.headers['x-csrf-token'] || req.body._csrf;
22
+ const sessionToken = req.session.csrfToken;
23
+
24
+ if (typeof token !== 'string' || typeof sessionToken !== 'string') {
25
+ return res.status(403).json({ error: 'Invalid CSRF token' });
26
+ }
27
+
28
+ const tokenBuffer = Buffer.from(token);
29
+ const sessionTokenBuffer = Buffer.from(sessionToken);
30
+
31
+ if (
32
+ tokenBuffer.length !== sessionTokenBuffer.length ||
33
+ !crypto.timingSafeEqual(tokenBuffer, sessionTokenBuffer)
34
+ ) {
35
+ return res.status(403).json({ error: 'Invalid CSRF token' });
36
+ }
37
+ next();
38
+ }
39
+ ```
40
+
41
+ ### Double Submit Cookie Pattern
42
+
43
+ ```javascript
44
+ // Set CSRF cookie
45
+ res.cookie('csrf_token', token, {
46
+ httpOnly: false, // Must be readable by JavaScript
47
+ secure: true,
48
+ sameSite: 'Strict'
49
+ });
50
+
51
+ // Client sends token in header
52
+ fetch('/api/action', {
53
+ method: 'POST',
54
+ headers: {
55
+ 'X-CSRF-Token': getCookie('csrf_token')
56
+ }
57
+ });
58
+
59
+ // Server validates cookie matches header
60
+ function validateDoubleSubmit(req) {
61
+ const cookieToken = req.cookies.csrf_token;
62
+ const headerToken = req.headers['x-csrf-token'];
63
+ return cookieToken && cookieToken === headerToken;
64
+ }
65
+ ```
66
+
67
+ ## SameSite Cookie Attribute
68
+
69
+ ```javascript
70
+ // Strict - never sent cross-site
71
+ res.cookie('session', value, { sameSite: 'Strict', secure: true, httpOnly: true });
72
+
73
+ // Lax - sent for top-level GET navigations (default in modern browsers)
74
+ res.cookie('session', value, { sameSite: 'Lax', secure: true, httpOnly: true });
75
+
76
+ // None - requires Secure flag, sent cross-site
77
+ res.cookie('session', value, { sameSite: 'None', secure: true, httpOnly: true });
78
+ ```
79
+
80
+ **Recommendation**: Use `SameSite=Strict` for session cookies when possible, `Lax` as minimum.
81
+
82
+ ## Fetch Metadata Headers
83
+
84
+ Validate request origin using Sec-Fetch-* headers:
85
+
86
+ ```javascript
87
+ function validateFetchMetadata(req, res, next) {
88
+ const site = req.headers['sec-fetch-site'];
89
+ const mode = req.headers['sec-fetch-mode'];
90
+ const dest = req.headers['sec-fetch-dest'];
91
+ const method = req.method;
92
+
93
+ // Allow same-origin requests
94
+ if (site === 'same-origin') return next();
95
+
96
+ // Allow user-initiated browser navigations
97
+ if (site === 'none') return next();
98
+
99
+ // Allow same-site top-level navigations and safe methods
100
+ if (
101
+ site === 'same-site' &&
102
+ (mode === 'navigate' || ['GET', 'HEAD', 'OPTIONS'].includes(method))
103
+ ) {
104
+ return next();
105
+ }
106
+
107
+ return res.status(403).json({ error: 'Fetch metadata validation failed' });
108
+ }
109
+ ```
110
+
111
+ ## Framework Integration
112
+
113
+ ### Express.js with Signed Double-Submit Tokens
114
+
115
+ ```javascript
116
+ const crypto = require('crypto');
117
+
118
+ const CSRF_COOKIE_NAME = 'csrf_token';
119
+ const CSRF_SECRET = Buffer.from(process.env.CSRF_SECRET, 'hex');
120
+
121
+ function signToken(sessionId, nonce) {
122
+ return crypto
123
+ .createHmac('sha256', CSRF_SECRET)
124
+ .update(`${sessionId}:${nonce}`)
125
+ .digest('base64url');
126
+ }
127
+
128
+ function constantTimeEqual(value, expected) {
129
+ if (typeof value !== 'string' || typeof expected !== 'string') return false;
130
+
131
+ const valueBuffer = Buffer.from(value);
132
+ const expectedBuffer = Buffer.from(expected);
133
+
134
+ return (
135
+ valueBuffer.length === expectedBuffer.length &&
136
+ crypto.timingSafeEqual(valueBuffer, expectedBuffer)
137
+ );
138
+ }
139
+
140
+ function generateToken(req) {
141
+ const nonce = crypto.randomBytes(32).toString('base64url');
142
+ const signature = signToken(req.session.id, nonce);
143
+ return `${nonce}.${signature}`;
144
+ }
145
+
146
+ function sendToken(req, res, next) {
147
+ const token = generateToken(req);
148
+
149
+ res.cookie(CSRF_COOKIE_NAME, token, {
150
+ httpOnly: true,
151
+ secure: true,
152
+ sameSite: 'Strict'
153
+ });
154
+
155
+ res.locals.csrfToken = token;
156
+ next();
157
+ }
158
+
159
+ function verifyToken(req, res, next) {
160
+ const token = req.headers['x-csrf-token'] || req.body._csrf;
161
+ const cookieToken = req.cookies[CSRF_COOKIE_NAME];
162
+
163
+ if (!constantTimeEqual(token, cookieToken)) {
164
+ return res.status(403).json({ error: 'Invalid CSRF token' });
165
+ }
166
+
167
+ const [nonce, signature, extra] = cookieToken.split('.');
168
+ if (extra || !nonce || !signature) {
169
+ return res.status(403).json({ error: 'Invalid CSRF token' });
170
+ }
171
+
172
+ if (!constantTimeEqual(signature, signToken(req.session.id, nonce))) {
173
+ return res.status(403).json({ error: 'Invalid CSRF token' });
174
+ }
175
+
176
+ next();
177
+ }
178
+
179
+ app.get('/form', sendToken, (req, res) => {
180
+ res.render('form', { csrfToken: res.locals.csrfToken });
181
+ });
182
+
183
+ app.post('/submit', verifyToken, (req, res) => {
184
+ res.json({ ok: true });
185
+ });
186
+ ```
187
+
188
+ Example tests for token issuance and verification:
189
+
190
+ ```javascript
191
+ const assert = require('node:assert/strict');
192
+ const test = require('node:test');
193
+
194
+ test('sendToken issues a cookie and exposes a form token', () => {
195
+ const req = { session: { id: 'session-123' } };
196
+ const res = {
197
+ locals: {},
198
+ cookie(name, value, options) {
199
+ this.cookieArgs = { name, value, options };
200
+ }
201
+ };
202
+
203
+ sendToken(req, res, () => {});
204
+
205
+ assert.equal(res.cookieArgs.name, CSRF_COOKIE_NAME);
206
+ assert.equal(res.locals.csrfToken, res.cookieArgs.value);
207
+ assert.equal(res.cookieArgs.options.httpOnly, true);
208
+ assert.equal(res.cookieArgs.options.sameSite, 'Strict');
209
+ });
210
+
211
+ test('verifyToken accepts a matching signed token', () => {
212
+ const req = { session: { id: 'session-123' } };
213
+ const token = generateToken(req);
214
+ let called = false;
215
+
216
+ verifyToken(
217
+ {
218
+ ...req,
219
+ headers: { 'x-csrf-token': token },
220
+ body: {},
221
+ cookies: { [CSRF_COOKIE_NAME]: token }
222
+ },
223
+ {},
224
+ () => {
225
+ called = true;
226
+ }
227
+ );
228
+
229
+ assert.equal(called, true);
230
+ });
231
+
232
+ test('verifyToken rejects mismatched or tampered tokens', () => {
233
+ const req = {
234
+ session: { id: 'session-123' },
235
+ headers: { 'x-csrf-token': 'tampered.token' },
236
+ body: {},
237
+ cookies: { [CSRF_COOKIE_NAME]: generateToken({ session: { id: 'session-123' } }) }
238
+ };
239
+ const res = {
240
+ status(code) {
241
+ this.statusCode = code;
242
+ return this;
243
+ },
244
+ json(body) {
245
+ this.body = body;
246
+ }
247
+ };
248
+
249
+ verifyToken(req, res, () => {});
250
+
251
+ assert.equal(res.statusCode, 403);
252
+ assert.deepEqual(res.body, { error: 'Invalid CSRF token' });
253
+ });
254
+ ```
255
+
256
+ ### React Forms
257
+
258
+ ```jsx
259
+ function Form({ csrfToken }) {
260
+ const handleSubmit = async (e) => {
261
+ e.preventDefault();
262
+ await fetch('/api/submit', {
263
+ method: 'POST',
264
+ headers: {
265
+ 'Content-Type': 'application/json',
266
+ 'X-CSRF-Token': csrfToken
267
+ },
268
+ body: JSON.stringify(formData)
269
+ });
270
+ };
271
+
272
+ return (
273
+ <form onSubmit={handleSubmit}>
274
+ <input type="hidden" name="_csrf" value={csrfToken} />
275
+ {/* form fields */}
276
+ </form>
277
+ );
278
+ }
279
+ ```
280
+
281
+ ### Twig Forms
282
+
283
+ ```twig
284
+ <form method="post" action="/submit">
285
+ <input type="hidden" name="_csrf_token" value="{{ csrf_token('form_name') }}">
286
+ <!-- form fields -->
287
+ </form>
288
+ ```
289
+
290
+ ## Client-Side CSRF (AJAX)
291
+
292
+ Protect against CSRF in single-page applications:
293
+
294
+ ```javascript
295
+ // Set up axios defaults
296
+ import axios from 'axios';
297
+
298
+ axios.defaults.xsrfCookieName = 'csrf_token';
299
+ axios.defaults.xsrfHeaderName = 'X-CSRF-Token';
300
+ axios.defaults.withCredentials = true;
301
+
302
+ // Or with fetch
303
+ async function secureFetch(url, options = {}) {
304
+ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
305
+
306
+ return fetch(url, {
307
+ ...options,
308
+ credentials: 'same-origin',
309
+ headers: {
310
+ ...options.headers,
311
+ 'X-CSRF-Token': csrfToken
312
+ }
313
+ });
314
+ }
315
+ ```
316
+
317
+ ## Verification Checklist
318
+
319
+ - [ ] All state-changing endpoints require CSRF tokens
320
+ - [ ] Tokens are cryptographically random (≥128 bits)
321
+ - [ ] Tokens are tied to user session
322
+ - [ ] Tokens validated server-side before processing
323
+ - [ ] SameSite cookie attribute set appropriately
324
+ - [ ] Fetch Metadata headers validated for sensitive endpoints
325
+ - [ ] GET requests are idempotent (no state changes)
326
+
327
+ OWASP Reference: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html