@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
@@ -15,8 +15,8 @@ function validateUsername(input) {
15
15
 
16
16
  // AVOID: Denylist (block known bad)
17
17
  function validateInput(input) {
18
- const blocked = ['<script>', 'javascript:', 'onerror'];
19
- return !blocked.some(bad => input.includes(bad)); // Easily bypassed
18
+ const blocked = ["<script>", "javascript:", "onerror"];
19
+ return !blocked.some((bad) => input.includes(bad)); // Easily bypassed
20
20
  }
21
21
  ```
22
22
 
@@ -33,8 +33,8 @@ function validateEmail(email) {
33
33
  }
34
34
 
35
35
  // Use built-in browser validation
36
- const input = document.createElement('input');
37
- input.type = 'email';
36
+ const input = document.createElement("input");
37
+ input.type = "email";
38
38
  input.value = email;
39
39
  return input.checkValidity();
40
40
  ```
@@ -45,7 +45,7 @@ return input.checkValidity();
45
45
  function validateUrl(input) {
46
46
  try {
47
47
  const url = new URL(input);
48
- return ['http:', 'https:'].includes(url.protocol);
48
+ return ["http:", "https:"].includes(url.protocol);
49
49
  } catch {
50
50
  return false;
51
51
  }
@@ -57,8 +57,8 @@ function validateSafeUrl(input) {
57
57
  if (!url) return false;
58
58
 
59
59
  // Block data: and javascript: schemes
60
- const dangerous = ['javascript:', 'data:', 'vbscript:'];
61
- return !dangerous.some(scheme => input.toLowerCase().startsWith(scheme));
60
+ const dangerous = ["javascript:", "data:", "vbscript:"];
61
+ return !dangerous.some((scheme) => input.toLowerCase().startsWith(scheme));
62
62
  }
63
63
  ```
64
64
 
@@ -77,7 +77,7 @@ function validateDecimal(input, min, max, decimals) {
77
77
  if (isNaN(num)) return false;
78
78
  if (num < min || num > max) return false;
79
79
 
80
- const parts = input.split('.');
80
+ const parts = input.split(".");
81
81
  if (parts.length > 2) return false;
82
82
  if (parts[1] && parts[1].length > decimals) return false;
83
83
 
@@ -107,7 +107,7 @@ function validateDateRange(input, minDate, maxDate) {
107
107
  function validatePhone(input) {
108
108
  // E.164 format: +[country][number], max 15 digits
109
109
  const pattern = /^\+[1-9]\d{1,14}$/;
110
- return pattern.test(input.replace(/[\s\-()]/g, ''));
110
+ return pattern.test(input.replace(/[\s\-()]/g, ""));
111
111
  }
112
112
  ```
113
113
 
@@ -118,14 +118,14 @@ function validatePhone(input) {
118
118
  ```javascript
119
119
  function escapeHtml(input) {
120
120
  const map = {
121
- '&': '&amp;',
122
- '<': '&lt;',
123
- '>': '&gt;',
124
- '"': '&quot;',
125
- "'": '&#x27;',
126
- '/': '&#x2F;'
121
+ "&": "&amp;",
122
+ "<": "&lt;",
123
+ ">": "&gt;",
124
+ '"': "&quot;",
125
+ "'": "&#x27;",
126
+ "/": "&#x2F;",
127
127
  };
128
- return String(input).replace(/[&<>"'/]/g, char => map[char]);
128
+ return String(input).replace(/[&<>"'/]/g, (char) => map[char]);
129
129
  }
130
130
  ```
131
131
 
@@ -136,14 +136,14 @@ function escapeHtml(input) {
136
136
  const query = `SELECT * FROM users WHERE name = '${userInput}'`;
137
137
 
138
138
  // RIGHT - use parameterized queries
139
- const query = 'SELECT * FROM users WHERE name = ?';
139
+ const query = "SELECT * FROM users WHERE name = ?";
140
140
  db.query(query, [userInput]);
141
141
  ```
142
142
 
143
143
  ### Path Traversal Prevention
144
144
 
145
145
  ```javascript
146
- const path = require('path');
146
+ const path = require("path");
147
147
 
148
148
  function validateFilePath(userPath, baseDir) {
149
149
  const baseCanonical = path.resolve(baseDir);
@@ -151,8 +151,8 @@ function validateFilePath(userPath, baseDir) {
151
151
  const relativePath = path.relative(baseCanonical, resolved);
152
152
 
153
153
  // Ensure resolved path stays inside the base directory
154
- if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
155
- throw new Error('Path traversal detected');
154
+ if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
155
+ throw new Error("Path traversal detected");
156
156
  }
157
157
 
158
158
  return resolved;
@@ -164,13 +164,13 @@ function validateFilePath(userPath, baseDir) {
164
164
  ### Node.js with Joi
165
165
 
166
166
  ```javascript
167
- const Joi = require('joi');
167
+ const Joi = require("joi");
168
168
 
169
169
  const userSchema = Joi.object({
170
170
  username: Joi.string().alphanum().min(3).max(30).required(),
171
171
  email: Joi.string().email().required(),
172
172
  age: Joi.number().integer().min(0).max(150),
173
- website: Joi.string().uri({ scheme: ['http', 'https'] })
173
+ website: Joi.string().uri({ scheme: ["http", "https"] }),
174
174
  });
175
175
 
176
176
  function validateUser(data) {
@@ -183,32 +183,37 @@ function validateUser(data) {
183
183
  ### Express Validator
184
184
 
185
185
  ```javascript
186
- const { body, validationResult } = require('express-validator');
186
+ const { body, validationResult } = require("express-validator");
187
187
 
188
- app.post('/user',
189
- body('email').isEmail().normalizeEmail(),
190
- body('password').isLength({ min: 8 }),
191
- body('age').isInt({ min: 0, max: 150 }),
188
+ app.post(
189
+ "/user",
190
+ body("email").isEmail().normalizeEmail(),
191
+ body("password").isLength({ min: 8 }),
192
+ body("age").isInt({ min: 0, max: 150 }),
192
193
  (req, res) => {
193
194
  const errors = validationResult(req);
194
195
  if (!errors.isEmpty()) {
195
196
  return res.status(400).json({ errors: errors.array() });
196
197
  }
197
198
  // Process valid input
198
- }
199
+ },
199
200
  );
200
201
  ```
201
202
 
202
203
  ### Zod (TypeScript)
203
204
 
204
205
  ```typescript
205
- import { z } from 'zod';
206
+ import { z } from "zod";
206
207
 
207
208
  const UserSchema = z.object({
208
- username: z.string().min(3).max(30).regex(/^[a-zA-Z0-9_]+$/),
209
+ username: z
210
+ .string()
211
+ .min(3)
212
+ .max(30)
213
+ .regex(/^[a-zA-Z0-9_]+$/),
209
214
  email: z.string().email(),
210
215
  age: z.number().int().min(0).max(150).optional(),
211
- website: z.string().url().optional()
216
+ website: z.string().url().optional(),
212
217
  });
213
218
 
214
219
  type User = z.infer<typeof UserSchema>;
@@ -12,7 +12,7 @@ const decoded = jwt.verify(token, secret);
12
12
 
13
13
  // SECURE - explicitly specify allowed algorithms
14
14
  const decoded = jwt.verify(token, secret, {
15
- algorithms: ['HS256'] // Only accept HS256
15
+ algorithms: ["HS256"], // Only accept HS256
16
16
  });
17
17
  ```
18
18
 
@@ -26,7 +26,7 @@ const decoded = jwt.verify(token, publicKey);
26
26
 
27
27
  // SECURE - specify expected algorithm
28
28
  const decoded = jwt.verify(token, publicKey, {
29
- algorithms: ['RS256'] // Only accept RS256
29
+ algorithms: ["RS256"], // Only accept RS256
30
30
  });
31
31
  ```
32
32
 
@@ -36,16 +36,16 @@ Attack: Brute-force weak HMAC secrets.
36
36
 
37
37
  ```javascript
38
38
  // VULNERABLE - weak secret
39
- const token = jwt.sign(payload, 'password123');
39
+ const token = jwt.sign(payload, "password123");
40
40
 
41
41
  // SECURE - strong random secret (256+ bits)
42
- const crypto = require('crypto');
43
- const secret = crypto.randomBytes(32); // 256 bits
42
+ const crypto = require("crypto");
43
+ const secret = crypto.randomBytes(32); // 256 bits
44
44
  const token = jwt.sign(payload, secret);
45
45
 
46
46
  // Or use RSA keys
47
- const privateKey = fs.readFileSync('private.pem');
48
- const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
47
+ const privateKey = fs.readFileSync("private.pem");
48
+ const token = jwt.sign(payload, privateKey, { algorithm: "RS256" });
49
49
  ```
50
50
 
51
51
  ## Token Sidejacking Prevention
@@ -53,46 +53,36 @@ const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
53
53
  Add fingerprint to prevent stolen token reuse:
54
54
 
55
55
  ```javascript
56
- const crypto = require('crypto');
56
+ const crypto = require("crypto");
57
57
 
58
58
  // Generate fingerprint on login
59
59
  function generateFingerprint() {
60
- return crypto.randomBytes(32).toString('hex');
60
+ return crypto.randomBytes(32).toString("hex");
61
61
  }
62
62
 
63
63
  // Create token with fingerprint hash
64
64
  function createToken(userId, fingerprint) {
65
- const fingerprintHash = crypto
66
- .createHash('sha256')
67
- .update(fingerprint)
68
- .digest('hex');
69
-
70
- return jwt.sign(
71
- { sub: userId, fph: fingerprintHash },
72
- secret,
73
- { expiresIn: '15m' }
74
- );
65
+ const fingerprintHash = crypto.createHash("sha256").update(fingerprint).digest("hex");
66
+
67
+ return jwt.sign({ sub: userId, fph: fingerprintHash }, secret, { expiresIn: "15m" });
75
68
  }
76
69
 
77
70
  // Set fingerprint as httpOnly cookie
78
- res.cookie('__Secure-Fgp', fingerprint, {
71
+ res.cookie("__Secure-Fgp", fingerprint, {
79
72
  httpOnly: true,
80
73
  secure: true,
81
- sameSite: 'Strict',
82
- maxAge: 15 * 60 * 1000 // Match token expiry
74
+ sameSite: "Strict",
75
+ maxAge: 15 * 60 * 1000, // Match token expiry
83
76
  });
84
77
 
85
78
  // Validate both token and fingerprint
86
79
  function validateToken(token, fingerprintCookie) {
87
- const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] });
80
+ const decoded = jwt.verify(token, secret, { algorithms: ["HS256"] });
88
81
 
89
- const fingerprintHash = crypto
90
- .createHash('sha256')
91
- .update(fingerprintCookie)
92
- .digest('hex');
82
+ const fingerprintHash = crypto.createHash("sha256").update(fingerprintCookie).digest("hex");
93
83
 
94
84
  if (decoded.fph !== fingerprintHash) {
95
- throw new Error('Invalid fingerprint');
85
+ throw new Error("Invalid fingerprint");
96
86
  }
97
87
 
98
88
  return decoded;
@@ -105,16 +95,16 @@ function validateToken(token, fingerprintCookie) {
105
95
 
106
96
  ```javascript
107
97
  // Store token in sessionStorage
108
- sessionStorage.setItem('token', jwt);
98
+ sessionStorage.setItem("token", jwt);
109
99
 
110
100
  // Clear on logout
111
- sessionStorage.removeItem('token');
101
+ sessionStorage.removeItem("token");
112
102
 
113
103
  // Send in Authorization header
114
- fetch('/api/data', {
104
+ fetch("/api/data", {
115
105
  headers: {
116
- 'Authorization': `Bearer ${sessionStorage.getItem('token')}`
117
- }
106
+ Authorization: `Bearer ${sessionStorage.getItem("token")}`,
107
+ },
118
108
  });
119
109
  ```
120
110
 
@@ -122,11 +112,11 @@ fetch('/api/data', {
122
112
 
123
113
  ```javascript
124
114
  // Server sets cookie
125
- res.cookie('token', jwt, {
126
- httpOnly: true, // Not accessible via JavaScript
127
- secure: true, // HTTPS only
128
- sameSite: 'Strict', // CSRF protection
129
- maxAge: 15 * 60 * 1000
115
+ res.cookie("token", jwt, {
116
+ httpOnly: true, // Not accessible via JavaScript
117
+ secure: true, // HTTPS only
118
+ sameSite: "Strict", // CSRF protection
119
+ maxAge: 15 * 60 * 1000,
130
120
  });
131
121
 
132
122
  // Need CSRF protection with cookie-based auth
@@ -136,50 +126,42 @@ res.cookie('token', jwt, {
136
126
 
137
127
  ```javascript
138
128
  // VULNERABLE - persists after browser close, accessible to XSS
139
- localStorage.setItem('token', jwt); // Not recommended
129
+ localStorage.setItem("token", jwt); // Not recommended
140
130
  ```
141
131
 
142
132
  ## Token Expiration
143
133
 
144
134
  ```javascript
145
135
  // Short-lived access tokens (15-60 minutes)
146
- const accessToken = jwt.sign(payload, secret, { expiresIn: '15m' });
136
+ const accessToken = jwt.sign(payload, secret, { expiresIn: "15m" });
147
137
 
148
138
  // Longer refresh tokens (days/weeks)
149
- const refreshToken = jwt.sign(
150
- { sub: userId, type: 'refresh' },
151
- refreshSecret,
152
- { expiresIn: '7d' }
153
- );
139
+ const refreshToken = jwt.sign({ sub: userId, type: "refresh" }, refreshSecret, { expiresIn: "7d" });
154
140
 
155
141
  // Refresh endpoint
156
- app.post('/refresh', async (req, res) => {
142
+ app.post("/refresh", async (req, res) => {
157
143
  const { refreshToken } = req.body;
158
144
 
159
145
  try {
160
146
  const decoded = jwt.verify(refreshToken, refreshSecret, {
161
- algorithms: ['HS256']
147
+ algorithms: ["HS256"],
162
148
  });
163
149
 
164
- if (decoded.type !== 'refresh') {
165
- throw new Error('Invalid token type');
150
+ if (decoded.type !== "refresh") {
151
+ throw new Error("Invalid token type");
166
152
  }
167
153
 
168
154
  // Check if refresh token is revoked
169
155
  if (await isTokenRevoked(decoded)) {
170
- throw new Error('Token revoked');
156
+ throw new Error("Token revoked");
171
157
  }
172
158
 
173
159
  // Issue new access token
174
- const newAccessToken = jwt.sign(
175
- { sub: decoded.sub },
176
- secret,
177
- { expiresIn: '15m' }
178
- );
160
+ const newAccessToken = jwt.sign({ sub: decoded.sub }, secret, { expiresIn: "15m" });
179
161
 
180
162
  res.json({ accessToken: newAccessToken });
181
163
  } catch (error) {
182
- res.status(401).json({ error: 'Invalid refresh token' });
164
+ res.status(401).json({ error: "Invalid refresh token" });
183
165
  }
184
166
  });
185
167
  ```
@@ -190,15 +172,15 @@ JWTs are stateless, so revocation requires additional mechanisms:
190
172
 
191
173
  ```javascript
192
174
  // Denylist approach
193
- const revokedTokens = new Map(); // Use Redis in production
175
+ const revokedTokens = new Map(); // Use Redis in production
194
176
 
195
177
  function revokeToken(token) {
196
- const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] });
178
+ const decoded = jwt.verify(token, secret, { algorithms: ["HS256"] });
197
179
  const tokenId = decoded.jti;
198
180
  const expiry = decoded.exp * 1000;
199
181
 
200
182
  if (!tokenId || !expiry) {
201
- throw new Error('Token missing required revocation claims');
183
+ throw new Error("Token missing required revocation claims");
202
184
  }
203
185
 
204
186
  // Store until token expires
@@ -213,11 +195,7 @@ function isTokenRevoked(decoded) {
213
195
  }
214
196
 
215
197
  // Include jti (JWT ID) in tokens
216
- const token = jwt.sign(
217
- { sub: userId, jti: crypto.randomUUID() },
218
- secret,
219
- { expiresIn: '15m' }
220
- );
198
+ const token = jwt.sign({ sub: userId, jti: crypto.randomUUID() }, secret, { expiresIn: "15m" });
221
199
  ```
222
200
 
223
201
  ## Token Information Disclosure
@@ -226,17 +204,23 @@ JWTs are base64-encoded, not encrypted. Sensitive data is visible.
226
204
 
227
205
  ```javascript
228
206
  // VULNERABLE - sensitive data in payload
229
- const token = jwt.sign({
230
- sub: userId,
231
- ssn: '123-45-6789', // Visible to anyone!
232
- salary: 100000
233
- }, secret);
207
+ const token = jwt.sign(
208
+ {
209
+ sub: userId,
210
+ ssn: "123-45-6789", // Visible to anyone!
211
+ salary: 100000,
212
+ },
213
+ secret,
214
+ );
234
215
 
235
216
  // SECURE - minimal claims
236
- const token = jwt.sign({
237
- sub: userId,
238
- role: 'user'
239
- }, secret);
217
+ const token = jwt.sign(
218
+ {
219
+ sub: userId,
220
+ role: "user",
221
+ },
222
+ secret,
223
+ );
240
224
 
241
225
  // If sensitive data required, encrypt the token
242
226
  const encryptedToken = encrypt(token, encryptionKey);
@@ -248,38 +232,38 @@ const encryptedToken = encrypt(token, encryptionKey);
248
232
  function authenticateToken(req, res, next) {
249
233
  // Get token from header
250
234
  const authHeader = req.headers.authorization;
251
- const token = authHeader?.split(' ')[1]; // "Bearer TOKEN"
235
+ const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : undefined;
252
236
 
253
237
  if (!token) {
254
- return res.status(401).json({ error: 'Token required' });
238
+ return res.status(401).json({ error: "Token required" });
255
239
  }
256
240
 
257
241
  try {
258
242
  // Verify with explicit algorithm
259
243
  const decoded = jwt.verify(token, secret, {
260
- algorithms: ['HS256'],
261
- issuer: 'myapp',
262
- audience: 'myapp-users'
244
+ algorithms: ["HS256"],
245
+ issuer: "myapp",
246
+ audience: "myapp-users",
263
247
  });
264
248
 
265
249
  // Validate fingerprint if using sidejacking protection
266
- const fingerprint = req.cookies['__Secure-Fgp'];
250
+ const fingerprint = req.cookies["__Secure-Fgp"];
267
251
  if (!validateFingerprint(decoded, fingerprint)) {
268
- throw new Error('Invalid fingerprint');
252
+ throw new Error("Invalid fingerprint");
269
253
  }
270
254
 
271
255
  // Check revocation
272
256
  if (isTokenRevoked(decoded)) {
273
- throw new Error('Token revoked');
257
+ throw new Error("Token revoked");
274
258
  }
275
259
 
276
260
  req.user = decoded;
277
261
  next();
278
262
  } catch (error) {
279
- if (error.name === 'TokenExpiredError') {
280
- return res.status(401).json({ error: 'Token expired' });
263
+ if (error.name === "TokenExpiredError") {
264
+ return res.status(401).json({ error: "Token expired" });
281
265
  }
282
- return res.status(403).json({ error: 'Invalid token' });
266
+ return res.status(403).json({ error: "Invalid token" });
283
267
  }
284
268
  }
285
269
  ```