@schalkneethling/toolkit 0.5.1 → 0.5.3

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 (26) hide show
  1. package/dist/index.mjs.map +1 -1
  2. package/hooks/auto-approve-safe-commands/hook.mjs +5 -1
  3. package/hooks/auto-approve-safe-commands/hook.mts +7 -6
  4. package/hooks/block-dangerous-commands/hook.mjs +3 -3
  5. package/hooks/block-dangerous-commands/hook.mts +10 -22
  6. package/package.json +8 -6
  7. package/skills/css-tokens/SKILL.md +1 -1
  8. package/skills/css-tokens/references/tokens.css +6 -10
  9. package/skills/frontend-security/SKILL.md +3 -0
  10. package/skills/frontend-security/references/csp-configuration.md +68 -51
  11. package/skills/frontend-security/references/csrf-protection.md +74 -70
  12. package/skills/frontend-security/references/dom-security.md +36 -29
  13. package/skills/frontend-security/references/file-upload-security.md +101 -69
  14. package/skills/frontend-security/references/framework-patterns.md +42 -40
  15. package/skills/frontend-security/references/input-validation.md +36 -31
  16. package/skills/frontend-security/references/jwt-security.md +68 -84
  17. package/skills/frontend-security/references/nodejs-npm-security.md +63 -55
  18. package/skills/frontend-security/references/xss-prevention.md +38 -36
  19. package/skills/frontend-testing/SKILL.md +31 -38
  20. package/skills/frontend-testing/references/accessibility-testing.md +56 -62
  21. package/skills/frontend-testing/references/aria-snapshots.md +35 -34
  22. package/skills/frontend-testing/references/locator-strategies.md +37 -40
  23. package/skills/frontend-testing/references/visual-regression.md +29 -23
  24. package/skills/npm-publishing-best-practices/SKILL.md +316 -0
  25. package/skills/semantic-html/SKILL.md +5 -21
  26. package/skills/semantic-html/references/heading-patterns.md +1 -5
@@ -18,9 +18,9 @@
18
18
 
19
19
  ```javascript
20
20
  const ALLOWED_EXTENSIONS = {
21
- images: ['.jpg', '.jpeg', '.png', '.gif', '.webp'],
22
- documents: ['.pdf', '.docx', '.xlsx'],
23
- data: ['.csv', '.json']
21
+ images: [".jpg", ".jpeg", ".png", ".gif", ".webp"],
22
+ documents: [".pdf", ".docx", ".xlsx"],
23
+ data: [".csv", ".json"],
24
24
  };
25
25
 
26
26
  function validateExtension(filename, category) {
@@ -34,22 +34,50 @@ function validateExtension(filename, category) {
34
34
  ```javascript
35
35
  const DANGEROUS_EXTENSIONS = [
36
36
  // Server-side execution
37
- '.php', '.php3', '.php4', '.php5', '.phtml',
38
- '.asp', '.aspx', '.ascx', '.ashx',
39
- '.jsp', '.jspx', '.jspa',
40
- '.cgi', '.pl', '.py', '.rb',
37
+ ".php",
38
+ ".php3",
39
+ ".php4",
40
+ ".php5",
41
+ ".phtml",
42
+ ".asp",
43
+ ".aspx",
44
+ ".ascx",
45
+ ".ashx",
46
+ ".jsp",
47
+ ".jspx",
48
+ ".jspa",
49
+ ".cgi",
50
+ ".pl",
51
+ ".py",
52
+ ".rb",
41
53
 
42
54
  // Windows executable
43
- '.exe', '.dll', '.bat', '.cmd', '.com', '.msi', '.ps1',
55
+ ".exe",
56
+ ".dll",
57
+ ".bat",
58
+ ".cmd",
59
+ ".com",
60
+ ".msi",
61
+ ".ps1",
44
62
 
45
63
  // Script files
46
- '.js', '.vbs', '.wsf', '.hta',
64
+ ".js",
65
+ ".vbs",
66
+ ".wsf",
67
+ ".hta",
47
68
 
48
69
  // Config files
49
- '.htaccess', '.htpasswd', '.config', '.ini',
70
+ ".htaccess",
71
+ ".htpasswd",
72
+ ".config",
73
+ ".ini",
50
74
 
51
75
  // Archive (can contain malicious files)
52
- '.zip', '.tar', '.gz', '.rar', '.7z'
76
+ ".zip",
77
+ ".tar",
78
+ ".gz",
79
+ ".rar",
80
+ ".7z",
53
81
  ];
54
82
  ```
55
83
 
@@ -58,7 +86,7 @@ const DANGEROUS_EXTENSIONS = [
58
86
  ```javascript
59
87
  function sanitizeFilename(filename) {
60
88
  // Remove all extensions except the last
61
- const parts = filename.split('.');
89
+ const parts = filename.split(".");
62
90
  if (parts.length > 2) {
63
91
  return `${parts[0]}.${parts[parts.length - 1]}`;
64
92
  }
@@ -74,13 +102,13 @@ function sanitizeFilename(filename) {
74
102
 
75
103
  ```javascript
76
104
  const ALLOWED_MIME_TYPES = {
77
- '.jpg': ['image/jpeg'],
78
- '.jpeg': ['image/jpeg'],
79
- '.png': ['image/png'],
80
- '.gif': ['image/gif'],
81
- '.webp': ['image/webp'],
82
- '.pdf': ['application/pdf'],
83
- '.docx': ['application/vnd.openxmlformats-officedocument.wordprocessingml.document']
105
+ ".jpg": ["image/jpeg"],
106
+ ".jpeg": ["image/jpeg"],
107
+ ".png": ["image/png"],
108
+ ".gif": ["image/gif"],
109
+ ".webp": ["image/webp"],
110
+ ".pdf": ["application/pdf"],
111
+ ".docx": ["application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
84
112
  };
85
113
 
86
114
  function validateMimeType(file) {
@@ -96,18 +124,21 @@ function validateMimeType(file) {
96
124
 
97
125
  ```javascript
98
126
  const FILE_SIGNATURES = {
99
- jpg: Buffer.from([0xFF, 0xD8, 0xFF]),
100
- png: Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),
127
+ jpg: Buffer.from([0xff, 0xd8, 0xff]),
128
+ png: Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
101
129
  gif: Buffer.from([0x47, 0x49, 0x46, 0x38]),
102
130
  pdf: Buffer.from([0x25, 0x50, 0x44, 0x46]),
103
- zip: Buffer.from([0x50, 0x4B, 0x03, 0x04])
131
+ zip: Buffer.from([0x50, 0x4b, 0x03, 0x04]),
104
132
  };
105
133
 
106
134
  async function validateFileSignature(filePath, expectedType) {
107
135
  const buffer = Buffer.alloc(8);
108
- const fd = await fs.open(filePath, 'r');
109
- await fd.read(buffer, 0, 8, 0);
110
- await fd.close();
136
+ const fd = await fs.open(filePath, "r");
137
+ try {
138
+ await fd.read(buffer, 0, 8, 0);
139
+ } finally {
140
+ await fd.close();
141
+ }
111
142
 
112
143
  const signature = FILE_SIGNATURES[expectedType];
113
144
  if (!signature) return false;
@@ -119,17 +150,17 @@ async function validateFileSignature(filePath, expectedType) {
119
150
  ## Safe Storage
120
151
 
121
152
  ```javascript
122
- const multer = require('multer');
123
- const path = require('path');
124
- const crypto = require('crypto');
153
+ const multer = require("multer");
154
+ const path = require("path");
155
+ const crypto = require("crypto");
125
156
 
126
157
  // Store OUTSIDE webroot
127
- const UPLOAD_DIR = '/var/app/uploads'; // Not in /public/
158
+ const UPLOAD_DIR = "/var/app/uploads"; // Not in /public/
128
159
 
129
160
  const storage = multer.diskStorage({
130
161
  destination: (req, file, cb) => {
131
162
  // Organize by date
132
- const date = new Date().toISOString().split('T')[0];
163
+ const date = new Date().toISOString().split("T")[0];
133
164
  const dir = path.join(UPLOAD_DIR, date);
134
165
  fs.mkdirSync(dir, { recursive: true });
135
166
  cb(null, dir);
@@ -137,24 +168,24 @@ const storage = multer.diskStorage({
137
168
  filename: (req, file, cb) => {
138
169
  // Generate random filename
139
170
  const ext = path.extname(file.originalname).toLowerCase();
140
- const name = crypto.randomBytes(16).toString('hex');
171
+ const name = crypto.randomBytes(16).toString("hex");
141
172
  cb(null, `${name}${ext}`);
142
- }
173
+ },
143
174
  });
144
175
 
145
176
  const upload = multer({
146
177
  storage,
147
178
  limits: {
148
- fileSize: 5 * 1024 * 1024, // 5MB
149
- files: 1
179
+ fileSize: 5 * 1024 * 1024, // 5MB
180
+ files: 1,
150
181
  },
151
182
  fileFilter: (req, file, cb) => {
152
183
  if (!validateMimeType(file)) {
153
- cb(new Error('Invalid file type'));
184
+ cb(new Error("Invalid file type"));
154
185
  return;
155
186
  }
156
187
  cb(null, true);
157
- }
188
+ },
158
189
  });
159
190
  ```
160
191
 
@@ -162,20 +193,20 @@ const upload = multer({
162
193
 
163
194
  ```javascript
164
195
  // Serve files through application, not directly
165
- app.get('/files/:id', async (req, res) => {
196
+ app.get("/files/:id", async (req, res) => {
166
197
  // Verify user authorization
167
198
  if (!req.user || !canAccessFile(req.user, req.params.id)) {
168
- return res.status(403).send('Forbidden');
199
+ return res.status(403).send("Forbidden");
169
200
  }
170
201
 
171
202
  // Get file from database (not from user input)
172
203
  const fileRecord = await db.getFile(req.params.id);
173
- if (!fileRecord) return res.status(404).send('Not found');
204
+ if (!fileRecord) return res.status(404).send("Not found");
174
205
 
175
206
  // Set safe headers
176
- res.setHeader('Content-Type', fileRecord.mimeType);
177
- res.setHeader('Content-Disposition', `attachment; filename="${fileRecord.safeName}"`);
178
- res.setHeader('X-Content-Type-Options', 'nosniff');
207
+ res.setHeader("Content-Type", fileRecord.mimeType);
208
+ res.setHeader("Content-Disposition", `attachment; filename="${fileRecord.safeName}"`);
209
+ res.setHeader("X-Content-Type-Options", "nosniff");
179
210
 
180
211
  // Stream file
181
212
  const stream = fs.createReadStream(fileRecord.path);
@@ -188,12 +219,12 @@ app.get('/files/:id', async (req, res) => {
188
219
  Destroy potential malicious content by re-encoding images:
189
220
 
190
221
  ```javascript
191
- const sharp = require('sharp');
222
+ const sharp = require("sharp");
192
223
 
193
224
  async function sanitizeImage(inputPath, outputPath) {
194
225
  await sharp(inputPath)
195
- .rotate() // Apply EXIF orientation
196
- .toFormat('jpeg', { quality: 90 }) // Re-encode
226
+ .rotate() // Apply EXIF orientation
227
+ .toFormat("jpeg", { quality: 90 }) // Re-encode
197
228
  .toFile(outputPath);
198
229
  }
199
230
  ```
@@ -201,8 +232,8 @@ async function sanitizeImage(inputPath, outputPath) {
201
232
  ## ZIP File Handling
202
233
 
203
234
  ```javascript
204
- const AdmZip = require('adm-zip');
205
- const path = require('path');
235
+ const AdmZip = require("adm-zip");
236
+ const path = require("path");
206
237
 
207
238
  function safeExtractZip(zipPath, destDir, maxSize = 100 * 1024 * 1024) {
208
239
  const zip = new AdmZip(zipPath);
@@ -216,20 +247,20 @@ function safeExtractZip(zipPath, destDir, maxSize = 100 * 1024 * 1024) {
216
247
  const resolvedEntry = path.resolve(destDir, entry.entryName);
217
248
  const relativeEntry = path.relative(resolvedDest, resolvedEntry);
218
249
 
219
- if (relativeEntry.startsWith('..') || path.isAbsolute(relativeEntry)) {
220
- throw new Error('Path traversal detected');
250
+ if (relativeEntry.startsWith("..") || path.isAbsolute(relativeEntry)) {
251
+ throw new Error("Path traversal detected");
221
252
  }
222
253
 
223
254
  // Check for zip bomb
224
255
  totalSize += entry.header.size;
225
256
  if (totalSize > maxSize) {
226
- throw new Error('Extracted size exceeds limit');
257
+ throw new Error("Extracted size exceeds limit");
227
258
  }
228
259
 
229
260
  // Check compression ratio (zip bomb indicator)
230
261
  const ratio = entry.header.size / entry.header.compressedSize;
231
262
  if (ratio > 100) {
232
- throw new Error('Suspicious compression ratio');
263
+ throw new Error("Suspicious compression ratio");
233
264
  }
234
265
  }
235
266
 
@@ -240,18 +271,18 @@ function safeExtractZip(zipPath, destDir, maxSize = 100 * 1024 * 1024) {
240
271
  ## Express.js Complete Example
241
272
 
242
273
  ```javascript
243
- const express = require('express');
244
- const multer = require('multer');
245
- const path = require('path');
246
- const crypto = require('crypto');
247
- const fs = require('fs').promises;
274
+ const express = require("express");
275
+ const multer = require("multer");
276
+ const path = require("path");
277
+ const crypto = require("crypto");
278
+ const fs = require("fs").promises;
248
279
 
249
280
  const app = express();
250
281
 
251
282
  // Configuration
252
- const UPLOAD_DIR = '/var/app/uploads';
283
+ const UPLOAD_DIR = "/var/app/uploads";
253
284
  const MAX_FILE_SIZE = 5 * 1024 * 1024;
254
- const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif'];
285
+ const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/gif"];
255
286
 
256
287
  // Multer setup
257
288
  const upload = multer({
@@ -259,26 +290,27 @@ const upload = multer({
259
290
  limits: { fileSize: MAX_FILE_SIZE },
260
291
  fileFilter: (req, file, cb) => {
261
292
  if (!ALLOWED_TYPES.includes(file.mimetype)) {
262
- cb(new multer.MulterError('LIMIT_UNEXPECTED_FILE'));
293
+ cb(new multer.MulterError("LIMIT_UNEXPECTED_FILE"));
263
294
  return;
264
295
  }
265
296
  cb(null, true);
266
- }
297
+ },
267
298
  });
268
299
 
269
300
  // Upload endpoint
270
- app.post('/upload',
271
- requireAuth, // Authentication
272
- verifyToken, // CSRF token
273
- upload.single('file'), // File handling
301
+ app.post(
302
+ "/upload",
303
+ requireAuth, // Authentication
304
+ verifyToken, // CSRF token
305
+ upload.single("file"), // File handling
274
306
  async (req, res) => {
275
307
  try {
276
308
  const file = req.file;
277
- if (!file) return res.status(400).json({ error: 'No file' });
309
+ if (!file) return res.status(400).json({ error: "No file" });
278
310
 
279
311
  // Validate magic bytes
280
312
  if (!validateMagicBytes(file.buffer, file.mimetype)) {
281
- return res.status(400).json({ error: 'Invalid file' });
313
+ return res.status(400).json({ error: "Invalid file" });
282
314
  }
283
315
 
284
316
  // Generate safe filename
@@ -295,15 +327,15 @@ app.post('/upload',
295
327
  filename,
296
328
  originalName: file.originalname,
297
329
  mimeType: file.mimetype,
298
- size: file.size
330
+ size: file.size,
299
331
  });
300
332
 
301
333
  res.json({ id: fileRecord.id });
302
334
  } catch (error) {
303
335
  console.error(error);
304
- res.status(500).json({ error: 'Upload failed' });
336
+ res.status(500).json({ error: "Upload failed" });
305
337
  }
306
- }
338
+ },
307
339
  );
308
340
  ```
309
341
 
@@ -20,18 +20,18 @@ import DOMPurify from 'dompurify';
20
20
 
21
21
  ```jsx
22
22
  // DANGEROUS - javascript: URLs in href
23
- <a href={userInput}>Link</a>
23
+ <a href={userInput}>Link</a>;
24
24
 
25
25
  // SAFE - validate URL protocol
26
26
  function SafeLink({ href, children }) {
27
27
  const safeHref = useMemo(() => {
28
28
  try {
29
29
  const url = new URL(href, window.location.origin);
30
- if (['http:', 'https:', 'mailto:'].includes(url.protocol)) {
30
+ if (["http:", "https:", "mailto:"].includes(url.protocol)) {
31
31
  return href;
32
32
  }
33
33
  } catch {}
34
- return '#';
34
+ return "#";
35
35
  }, [href]);
36
36
 
37
37
  return <a href={safeHref}>{children}</a>;
@@ -55,17 +55,15 @@ function SafeLink({ href, children }) {
55
55
 
56
56
  ```jsx
57
57
  // DANGEROUS - injecting user data into SSR without escaping
58
- <script>
59
- window.__INITIAL_STATE__ = {JSON.stringify(userControlledData)}
60
- </script>
58
+ <script>window.__INITIAL_STATE__ = {JSON.stringify(userControlledData)}</script>;
61
59
 
62
60
  // SAFE - serialize with escaping
63
- import serialize from 'serialize-javascript';
61
+ import serialize from "serialize-javascript";
64
62
  <script
65
63
  dangerouslySetInnerHTML={{
66
- __html: `window.__INITIAL_STATE__ = ${serialize(data, { isJSON: true })}`
64
+ __html: `window.__INITIAL_STATE__ = ${serialize(data, { isJSON: true })}`,
67
65
  }}
68
- />
66
+ />;
69
67
  ```
70
68
 
71
69
  ## Astro Security
@@ -116,20 +114,20 @@ const Component = await loadComponent();
116
114
  // src/pages/api/data.js
117
115
  export async function POST({ request }) {
118
116
  // 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 });
117
+ const contentType = request.headers.get("content-type");
118
+ if (!contentType?.includes("application/json")) {
119
+ return new Response("Invalid content type", { status: 415 });
122
120
  }
123
121
 
124
122
  // Validate and sanitize input
125
123
  const body = await request.json();
126
124
  if (!validateInput(body)) {
127
- return new Response('Invalid input', { status: 400 });
125
+ return new Response("Invalid input", { status: 400 });
128
126
  }
129
127
 
130
128
  // Process request
131
129
  return new Response(JSON.stringify(result), {
132
- headers: { 'Content-Type': 'application/json' }
130
+ headers: { "Content-Type": "application/json" },
133
131
  });
134
132
  }
135
133
  ```
@@ -177,12 +175,12 @@ export async function POST({ request }) {
177
175
  twig:
178
176
  sandbox:
179
177
  policy:
180
- tags: ['if', 'for', 'set']
181
- filters: ['escape', 'upper', 'lower']
178
+ tags: ["if", "for", "set"]
179
+ filters: ["escape", "upper", "lower"]
182
180
  methods:
183
- Symfony\Component\Routing\Generator\UrlGeneratorInterface: ['generate']
181
+ Symfony\Component\Routing\Generator\UrlGeneratorInterface: ["generate"]
184
182
  properties: []
185
- functions: ['path', 'url']
183
+ functions: ["path", "url"]
186
184
  ```
187
185
 
188
186
  ### CSRF in Forms
@@ -207,31 +205,35 @@ Bun.serve({
207
205
  const url = new URL(req.url);
208
206
 
209
207
  // Validate origin for CORS
210
- const origin = req.headers.get('origin');
208
+ const origin = req.headers.get("origin");
211
209
  if (origin && !isAllowedOrigin(origin)) {
212
- return new Response('Forbidden', { status: 403 });
210
+ return new Response("Forbidden", { status: 403 });
213
211
  }
214
212
 
215
213
  // Rate limiting
216
214
  if (isRateLimited(req)) {
217
- return new Response('Too Many Requests', { status: 429 });
215
+ return new Response("Too Many Requests", { status: 429 });
218
216
  }
219
217
 
220
218
  return handleRequest(req);
221
- }
219
+ },
222
220
  });
223
221
  ```
224
222
 
225
223
  ### File Handling
226
224
 
227
225
  ```javascript
226
+ const path = require("path");
227
+
228
228
  // Validate file paths
229
229
  function safeReadFile(userPath) {
230
- const baseDir = '/app/public';
231
- const resolved = Bun.resolveSync(userPath, baseDir);
230
+ const baseDir = "/app/public";
231
+ const safeBaseDir = path.resolve(baseDir);
232
+ const resolved = path.resolve(safeBaseDir, userPath);
233
+ const relativePath = path.relative(safeBaseDir, resolved);
232
234
 
233
- if (!resolved.startsWith(baseDir)) {
234
- throw new Error('Path traversal detected');
235
+ if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
236
+ throw new Error("Path traversal detected");
235
237
  }
236
238
 
237
239
  return Bun.file(resolved).text();
@@ -244,34 +246,34 @@ function safeReadFile(userPath) {
244
246
 
245
247
  ```javascript
246
248
  // NEVER store sensitive data in localStorage
247
- localStorage.setItem('token', jwt); // DANGEROUS
249
+ localStorage.setItem("token", jwt); // DANGEROUS
248
250
 
249
251
  // Use httpOnly cookies for tokens instead
250
252
  // Or store in memory with short expiration
251
253
 
252
254
  // If localStorage is necessary, encrypt
253
- import { encrypt, decrypt } from './crypto';
254
- localStorage.setItem('data', encrypt(sensitiveData, key));
255
+ import { encrypt, decrypt } from "./crypto";
256
+ localStorage.setItem("data", encrypt(sensitiveData, key));
255
257
  ```
256
258
 
257
259
  ### postMessage
258
260
 
259
261
  ```javascript
260
262
  // Always validate origin and data
261
- window.addEventListener('message', (event) => {
263
+ window.addEventListener("message", (event) => {
262
264
  // Validate origin
263
- const allowedOrigins = ['https://trusted.com'];
265
+ const allowedOrigins = ["https://trusted.com"];
264
266
  if (!allowedOrigins.includes(event.origin)) return;
265
267
 
266
268
  // Validate data structure
267
- if (typeof event.data !== 'object') return;
268
- if (!['action1', 'action2'].includes(event.data.type)) return;
269
+ if (typeof event.data !== "object") return;
270
+ if (!["action1", "action2"].includes(event.data.type)) return;
269
271
 
270
272
  handleMessage(event.data);
271
273
  });
272
274
 
273
275
  // Always specify target origin when sending
274
- iframe.contentWindow.postMessage(data, 'https://specific-origin.com');
276
+ iframe.contentWindow.postMessage(data, "https://specific-origin.com");
275
277
  // NEVER use '*' for sensitive data
276
278
  ```
277
279
 
@@ -282,23 +284,23 @@ iframe.contentWindow.postMessage(data, 'https://specific-origin.com');
282
284
  const wss = new WebSocket.Server({
283
285
  server,
284
286
  verifyClient: ({ origin, req }, callback) => {
285
- const allowed = ['https://myapp.com'];
287
+ const allowed = ["https://myapp.com"];
286
288
  callback(allowed.includes(origin));
287
- }
289
+ },
288
290
  });
289
291
 
290
292
  // Validate messages
291
- wss.on('connection', (ws) => {
292
- ws.on('message', (data) => {
293
+ wss.on("connection", (ws) => {
294
+ ws.on("message", (data) => {
293
295
  try {
294
296
  const msg = JSON.parse(data);
295
297
  if (!isValidMessage(msg)) {
296
- ws.close(1008, 'Invalid message');
298
+ ws.close(1008, "Invalid message");
297
299
  return;
298
300
  }
299
301
  handleMessage(msg);
300
302
  } catch {
301
- ws.close(1008, 'Invalid JSON');
303
+ ws.close(1008, "Invalid JSON");
302
304
  }
303
305
  });
304
306
  });