@schalkneethling/toolkit 0.5.0 → 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 +9 -9
  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
@@ -8,21 +8,21 @@ Generate unique per-session tokens and validate on state-changing requests:
8
8
 
9
9
  ```javascript
10
10
  // Server-side token generation (Node.js)
11
- const crypto = require('crypto');
11
+ const crypto = require("crypto");
12
12
 
13
13
  function generateCSRFToken(session) {
14
- const token = crypto.randomBytes(32).toString('hex');
14
+ const token = crypto.randomBytes(32).toString("hex");
15
15
  session.csrfToken = token;
16
16
  return token;
17
17
  }
18
18
 
19
19
  // Middleware validation
20
20
  function validateCSRF(req, res, next) {
21
- const token = req.headers['x-csrf-token'] || req.body._csrf;
21
+ const token = req.headers["x-csrf-token"] || req.body._csrf;
22
22
  const sessionToken = req.session.csrfToken;
23
23
 
24
- if (typeof token !== 'string' || typeof sessionToken !== 'string') {
25
- return res.status(403).json({ error: 'Invalid CSRF token' });
24
+ if (typeof token !== "string" || typeof sessionToken !== "string") {
25
+ return res.status(403).json({ error: "Invalid CSRF token" });
26
26
  }
27
27
 
28
28
  const tokenBuffer = Buffer.from(token);
@@ -32,7 +32,7 @@ function validateCSRF(req, res, next) {
32
32
  tokenBuffer.length !== sessionTokenBuffer.length ||
33
33
  !crypto.timingSafeEqual(tokenBuffer, sessionTokenBuffer)
34
34
  ) {
35
- return res.status(403).json({ error: 'Invalid CSRF token' });
35
+ return res.status(403).json({ error: "Invalid CSRF token" });
36
36
  }
37
37
  next();
38
38
  }
@@ -42,24 +42,24 @@ function validateCSRF(req, res, next) {
42
42
 
43
43
  ```javascript
44
44
  // Set CSRF cookie
45
- res.cookie('csrf_token', token, {
46
- httpOnly: false, // Must be readable by JavaScript
45
+ res.cookie("csrf_token", token, {
46
+ httpOnly: false, // Must be readable by JavaScript
47
47
  secure: true,
48
- sameSite: 'Strict'
48
+ sameSite: "Strict",
49
49
  });
50
50
 
51
51
  // Client sends token in header
52
- fetch('/api/action', {
53
- method: 'POST',
52
+ fetch("/api/action", {
53
+ method: "POST",
54
54
  headers: {
55
- 'X-CSRF-Token': getCookie('csrf_token')
56
- }
55
+ "X-CSRF-Token": getCookie("csrf_token"),
56
+ },
57
57
  });
58
58
 
59
59
  // Server validates cookie matches header
60
60
  function validateDoubleSubmit(req) {
61
61
  const cookieToken = req.cookies.csrf_token;
62
- const headerToken = req.headers['x-csrf-token'];
62
+ const headerToken = req.headers["x-csrf-token"];
63
63
  return cookieToken && cookieToken === headerToken;
64
64
  }
65
65
  ```
@@ -68,43 +68,43 @@ function validateDoubleSubmit(req) {
68
68
 
69
69
  ```javascript
70
70
  // Strict - never sent cross-site
71
- res.cookie('session', value, { sameSite: 'Strict', secure: true, httpOnly: true });
71
+ res.cookie("session", value, { sameSite: "Strict", secure: true, httpOnly: true });
72
72
 
73
73
  // Lax - sent for top-level GET navigations (default in modern browsers)
74
- res.cookie('session', value, { sameSite: 'Lax', secure: true, httpOnly: true });
74
+ res.cookie("session", value, { sameSite: "Lax", secure: true, httpOnly: true });
75
75
 
76
76
  // None - requires Secure flag, sent cross-site
77
- res.cookie('session', value, { sameSite: 'None', secure: true, httpOnly: true });
77
+ res.cookie("session", value, { sameSite: "None", secure: true, httpOnly: true });
78
78
  ```
79
79
 
80
80
  **Recommendation**: Use `SameSite=Strict` for session cookies when possible, `Lax` as minimum.
81
81
 
82
82
  ## Fetch Metadata Headers
83
83
 
84
- Validate request origin using Sec-Fetch-* headers:
84
+ Validate request origin using Sec-Fetch-\* headers:
85
85
 
86
86
  ```javascript
87
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'];
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
91
  const method = req.method;
92
92
 
93
93
  // Allow same-origin requests
94
- if (site === 'same-origin') return next();
94
+ if (site === "same-origin") return next();
95
95
 
96
96
  // Allow user-initiated browser navigations
97
- if (site === 'none') return next();
97
+ if (site === "none") return next();
98
98
 
99
99
  // Allow same-site top-level navigations and safe methods
100
100
  if (
101
- site === 'same-site' &&
102
- (mode === 'navigate' || ['GET', 'HEAD', 'OPTIONS'].includes(method))
101
+ site === "same-site" &&
102
+ (mode === "navigate" || ["GET", "HEAD", "OPTIONS"].includes(method))
103
103
  ) {
104
104
  return next();
105
105
  }
106
106
 
107
- return res.status(403).json({ error: 'Fetch metadata validation failed' });
107
+ return res.status(403).json({ error: "Fetch metadata validation failed" });
108
108
  }
109
109
  ```
110
110
 
@@ -113,20 +113,24 @@ function validateFetchMetadata(req, res, next) {
113
113
  ### Express.js with Signed Double-Submit Tokens
114
114
 
115
115
  ```javascript
116
- const crypto = require('crypto');
116
+ const crypto = require("crypto");
117
+
118
+ const CSRF_COOKIE_NAME = "csrf_token";
119
+ if (!/^[a-f0-9]{64,}$/i.test(process.env.CSRF_SECRET || "")) {
120
+ throw new Error("CSRF_SECRET must be a hex-encoded secret with at least 32 bytes of entropy");
121
+ }
117
122
 
118
- const CSRF_COOKIE_NAME = 'csrf_token';
119
- const CSRF_SECRET = Buffer.from(process.env.CSRF_SECRET, 'hex');
123
+ const CSRF_SECRET = Buffer.from(process.env.CSRF_SECRET, "hex");
120
124
 
121
125
  function signToken(sessionId, nonce) {
122
126
  return crypto
123
- .createHmac('sha256', CSRF_SECRET)
127
+ .createHmac("sha256", CSRF_SECRET)
124
128
  .update(`${sessionId}:${nonce}`)
125
- .digest('base64url');
129
+ .digest("base64url");
126
130
  }
127
131
 
128
132
  function constantTimeEqual(value, expected) {
129
- if (typeof value !== 'string' || typeof expected !== 'string') return false;
133
+ if (typeof value !== "string" || typeof expected !== "string") return false;
130
134
 
131
135
  const valueBuffer = Buffer.from(value);
132
136
  const expectedBuffer = Buffer.from(expected);
@@ -138,7 +142,7 @@ function constantTimeEqual(value, expected) {
138
142
  }
139
143
 
140
144
  function generateToken(req) {
141
- const nonce = crypto.randomBytes(32).toString('base64url');
145
+ const nonce = crypto.randomBytes(32).toString("base64url");
142
146
  const signature = signToken(req.session.id, nonce);
143
147
  return `${nonce}.${signature}`;
144
148
  }
@@ -149,7 +153,7 @@ function sendToken(req, res, next) {
149
153
  res.cookie(CSRF_COOKIE_NAME, token, {
150
154
  httpOnly: true,
151
155
  secure: true,
152
- sameSite: 'Strict'
156
+ sameSite: "Strict",
153
157
  });
154
158
 
155
159
  res.locals.csrfToken = token;
@@ -157,30 +161,30 @@ function sendToken(req, res, next) {
157
161
  }
158
162
 
159
163
  function verifyToken(req, res, next) {
160
- const token = req.headers['x-csrf-token'] || req.body._csrf;
164
+ const token = req.headers["x-csrf-token"] || req.body._csrf;
161
165
  const cookieToken = req.cookies[CSRF_COOKIE_NAME];
162
166
 
163
167
  if (!constantTimeEqual(token, cookieToken)) {
164
- return res.status(403).json({ error: 'Invalid CSRF token' });
168
+ return res.status(403).json({ error: "Invalid CSRF token" });
165
169
  }
166
170
 
167
- const [nonce, signature, extra] = cookieToken.split('.');
171
+ const [nonce, signature, extra] = cookieToken.split(".");
168
172
  if (extra || !nonce || !signature) {
169
- return res.status(403).json({ error: 'Invalid CSRF token' });
173
+ return res.status(403).json({ error: "Invalid CSRF token" });
170
174
  }
171
175
 
172
176
  if (!constantTimeEqual(signature, signToken(req.session.id, nonce))) {
173
- return res.status(403).json({ error: 'Invalid CSRF token' });
177
+ return res.status(403).json({ error: "Invalid CSRF token" });
174
178
  }
175
179
 
176
180
  next();
177
181
  }
178
182
 
179
- app.get('/form', sendToken, (req, res) => {
180
- res.render('form', { csrfToken: res.locals.csrfToken });
183
+ app.get("/form", sendToken, (req, res) => {
184
+ res.render("form", { csrfToken: res.locals.csrfToken });
181
185
  });
182
186
 
183
- app.post('/submit', verifyToken, (req, res) => {
187
+ app.post("/submit", verifyToken, (req, res) => {
184
188
  res.json({ ok: true });
185
189
  });
186
190
  ```
@@ -188,16 +192,16 @@ app.post('/submit', verifyToken, (req, res) => {
188
192
  Example tests for token issuance and verification:
189
193
 
190
194
  ```javascript
191
- const assert = require('node:assert/strict');
192
- const test = require('node:test');
195
+ const assert = require("node:assert/strict");
196
+ const test = require("node:test");
193
197
 
194
- test('sendToken issues a cookie and exposes a form token', () => {
195
- const req = { session: { id: 'session-123' } };
198
+ test("sendToken issues a cookie and exposes a form token", () => {
199
+ const req = { session: { id: "session-123" } };
196
200
  const res = {
197
201
  locals: {},
198
202
  cookie(name, value, options) {
199
203
  this.cookieArgs = { name, value, options };
200
- }
204
+ },
201
205
  };
202
206
 
203
207
  sendToken(req, res, () => {});
@@ -205,36 +209,36 @@ test('sendToken issues a cookie and exposes a form token', () => {
205
209
  assert.equal(res.cookieArgs.name, CSRF_COOKIE_NAME);
206
210
  assert.equal(res.locals.csrfToken, res.cookieArgs.value);
207
211
  assert.equal(res.cookieArgs.options.httpOnly, true);
208
- assert.equal(res.cookieArgs.options.sameSite, 'Strict');
212
+ assert.equal(res.cookieArgs.options.sameSite, "Strict");
209
213
  });
210
214
 
211
- test('verifyToken accepts a matching signed token', () => {
212
- const req = { session: { id: 'session-123' } };
215
+ test("verifyToken accepts a matching signed token", () => {
216
+ const req = { session: { id: "session-123" } };
213
217
  const token = generateToken(req);
214
218
  let called = false;
215
219
 
216
220
  verifyToken(
217
221
  {
218
222
  ...req,
219
- headers: { 'x-csrf-token': token },
223
+ headers: { "x-csrf-token": token },
220
224
  body: {},
221
- cookies: { [CSRF_COOKIE_NAME]: token }
225
+ cookies: { [CSRF_COOKIE_NAME]: token },
222
226
  },
223
227
  {},
224
228
  () => {
225
229
  called = true;
226
- }
230
+ },
227
231
  );
228
232
 
229
233
  assert.equal(called, true);
230
234
  });
231
235
 
232
- test('verifyToken rejects mismatched or tampered tokens', () => {
236
+ test("verifyToken rejects mismatched or tampered tokens", () => {
233
237
  const req = {
234
- session: { id: 'session-123' },
235
- headers: { 'x-csrf-token': 'tampered.token' },
238
+ session: { id: "session-123" },
239
+ headers: { "x-csrf-token": "tampered.token" },
236
240
  body: {},
237
- cookies: { [CSRF_COOKIE_NAME]: generateToken({ session: { id: 'session-123' } }) }
241
+ cookies: { [CSRF_COOKIE_NAME]: generateToken({ session: { id: "session-123" } }) },
238
242
  };
239
243
  const res = {
240
244
  status(code) {
@@ -243,13 +247,13 @@ test('verifyToken rejects mismatched or tampered tokens', () => {
243
247
  },
244
248
  json(body) {
245
249
  this.body = body;
246
- }
250
+ },
247
251
  };
248
252
 
249
253
  verifyToken(req, res, () => {});
250
254
 
251
255
  assert.equal(res.statusCode, 403);
252
- assert.deepEqual(res.body, { error: 'Invalid CSRF token' });
256
+ assert.deepEqual(res.body, { error: "Invalid CSRF token" });
253
257
  });
254
258
  ```
255
259
 
@@ -259,13 +263,13 @@ test('verifyToken rejects mismatched or tampered tokens', () => {
259
263
  function Form({ csrfToken }) {
260
264
  const handleSubmit = async (e) => {
261
265
  e.preventDefault();
262
- await fetch('/api/submit', {
263
- method: 'POST',
266
+ await fetch("/api/submit", {
267
+ method: "POST",
264
268
  headers: {
265
- 'Content-Type': 'application/json',
266
- 'X-CSRF-Token': csrfToken
269
+ "Content-Type": "application/json",
270
+ "X-CSRF-Token": csrfToken,
267
271
  },
268
- body: JSON.stringify(formData)
272
+ body: JSON.stringify(formData),
269
273
  });
270
274
  };
271
275
 
@@ -293,10 +297,10 @@ Protect against CSRF in single-page applications:
293
297
 
294
298
  ```javascript
295
299
  // Set up axios defaults
296
- import axios from 'axios';
300
+ import axios from "axios";
297
301
 
298
- axios.defaults.xsrfCookieName = 'csrf_token';
299
- axios.defaults.xsrfHeaderName = 'X-CSRF-Token';
302
+ axios.defaults.xsrfCookieName = "csrf_token";
303
+ axios.defaults.xsrfHeaderName = "X-CSRF-Token";
300
304
  axios.defaults.withCredentials = true;
301
305
 
302
306
  // Or with fetch
@@ -305,11 +309,11 @@ async function secureFetch(url, options = {}) {
305
309
 
306
310
  return fetch(url, {
307
311
  ...options,
308
- credentials: 'same-origin',
312
+ credentials: "same-origin",
309
313
  headers: {
310
314
  ...options.headers,
311
- 'X-CSRF-Token': csrfToken
312
- }
315
+ "X-CSRF-Token": csrfToken,
316
+ },
313
317
  });
314
318
  }
315
319
  ```
@@ -8,13 +8,13 @@ Untrusted data in HTML element content:
8
8
 
9
9
  ```javascript
10
10
  // UNSAFE
11
- element.innerHTML = '<div>' + userInput + '</div>';
11
+ element.innerHTML = "<div>" + userInput + "</div>";
12
12
 
13
13
  // SAFE - use textContent
14
14
  element.textContent = userInput;
15
15
 
16
16
  // SAFE - create elements programmatically
17
- const div = document.createElement('div');
17
+ const div = document.createElement("div");
18
18
  div.textContent = userInput;
19
19
  parent.appendChild(div);
20
20
  ```
@@ -39,14 +39,14 @@ const value = element.dataset.value;
39
39
 
40
40
  ```javascript
41
41
  // UNSAFE - event handlers
42
- element.setAttribute('onclick', userInput);
42
+ element.setAttribute("onclick", userInput);
43
43
 
44
44
  // UNSAFE - dangerous attributes
45
- element.setAttribute('href', userInput); // javascript: URLs
46
- element.setAttribute('src', userInput); // script injection
45
+ element.setAttribute("href", userInput); // javascript: URLs
46
+ element.setAttribute("src", userInput); // script injection
47
47
 
48
48
  // SAFE - safe attributes only
49
- const safeAttributes = ['title', 'alt', 'class', 'id', 'name'];
49
+ const safeAttributes = ["title", "alt", "class", "id", "name"];
50
50
  if (safeAttributes.includes(attributeName)) {
51
51
  element.setAttribute(attributeName, userInput);
52
52
  }
@@ -57,7 +57,7 @@ if (safeAttributes.includes(attributeName)) {
57
57
  ```javascript
58
58
  // UNSAFE - expression injection
59
59
  element.style.cssText = userInput;
60
- element.setAttribute('style', userInput);
60
+ element.setAttribute("style", userInput);
61
61
 
62
62
  // SAFE - set specific properties
63
63
  element.style.backgroundColor = sanitizeColor(userInput);
@@ -65,7 +65,7 @@ element.style.backgroundColor = sanitizeColor(userInput);
65
65
  function sanitizeColor(input) {
66
66
  // Only allow safe color values
67
67
  const colorRegex = /^#[0-9A-Fa-f]{6}$|^#[0-9A-Fa-f]{3}$|^rgb\(\d{1,3},\s*\d{1,3},\s*\d{1,3}\)$/;
68
- return colorRegex.test(input) ? input : 'inherit';
68
+ return colorRegex.test(input) ? input : "inherit";
69
69
  }
70
70
  ```
71
71
 
@@ -75,13 +75,13 @@ function sanitizeColor(input) {
75
75
  // UNSAFE - unvalidated URLs
76
76
  location.href = userInput;
77
77
  window.open(userInput);
78
- element.setAttribute('href', userInput);
78
+ element.setAttribute("href", userInput);
79
79
 
80
80
  // SAFE - validate URL protocol
81
81
  function validateUrl(input) {
82
82
  try {
83
83
  const url = new URL(input, window.location.origin);
84
- const allowedProtocols = ['http:', 'https:', 'mailto:'];
84
+ const allowedProtocols = ["http:", "https:", "mailto:"];
85
85
  if (!allowedProtocols.includes(url.protocol)) {
86
86
  return null;
87
87
  }
@@ -105,7 +105,7 @@ Named elements (id, name attributes) create global variables:
105
105
 
106
106
  ```html
107
107
  <!-- This creates window.config -->
108
- <img id="config" src="x">
108
+ <img id="config" src="x" />
109
109
 
110
110
  <!-- JavaScript that assumes config is an object will break -->
111
111
  <script>
@@ -117,12 +117,17 @@ Named elements (id, name attributes) create global variables:
117
117
 
118
118
  ```javascript
119
119
  // 1. Use Object.hasOwn() or hasOwnProperty()
120
- if (Object.hasOwn(window, 'config') && typeof config === 'object') {
121
- // Safe to use config
120
+ if (
121
+ Object.hasOwn(window, "config") &&
122
+ typeof window.config === "object" &&
123
+ window.config !== null &&
124
+ !(window.config instanceof Element)
125
+ ) {
126
+ // Safe to use window.config
122
127
  }
123
128
 
124
129
  // 2. Access through document methods
125
- const element = document.getElementById('config');
130
+ const element = document.getElementById("config");
126
131
 
127
132
  // 3. Use Map instead of objects for user-controlled keys
128
133
  const userConfig = new Map();
@@ -133,7 +138,7 @@ Object.freeze(window.config);
133
138
 
134
139
  // 5. Use nullish coalescing with type checking
135
140
  const config = window.config ?? {};
136
- if (typeof config.apiKey === 'string') {
141
+ if (typeof config.apiKey === "string") {
137
142
  // Safe to use
138
143
  }
139
144
  ```
@@ -143,8 +148,8 @@ if (typeof config.apiKey === 'string') {
143
148
  ```javascript
144
149
  // DOMPurify with clobbering protection
145
150
  const clean = DOMPurify.sanitize(dirty, {
146
- SANITIZE_DOM: true, // Remove clobbering vectors
147
- SANITIZE_NAMED_PROPS: true
151
+ SANITIZE_DOM: true, // Remove clobbering vectors
152
+ SANITIZE_NAMED_PROPS: true,
148
153
  });
149
154
  ```
150
155
 
@@ -158,7 +163,7 @@ element.textContent = userInput;
158
163
  document.createTextNode(userInput);
159
164
 
160
165
  // Attribute manipulation (for safe attributes)
161
- element.setAttribute('data-value', userInput);
166
+ element.setAttribute("data-value", userInput);
162
167
  element.classList.add(sanitizedClass);
163
168
 
164
169
  // Query selectors (read-only)
@@ -173,11 +178,11 @@ document.querySelectorAll(selector);
173
178
  element.innerHTML = sanitized;
174
179
  element.outerHTML = sanitized;
175
180
  element.insertAdjacentHTML(position, sanitized);
176
- document.write(sanitized); // Avoid entirely
181
+ document.write(sanitized); // Avoid entirely
177
182
 
178
183
  // Script execution
179
- eval(); // Never use with user input
180
- new Function(); // Never use with user input
184
+ eval(); // Never use with user input
185
+ new Function(); // Never use with user input
181
186
  setTimeout(string); // Never pass strings
182
187
  setInterval(string); // Never pass strings
183
188
  ```
@@ -186,17 +191,17 @@ setInterval(string); // Never pass strings
186
191
 
187
192
  ```javascript
188
193
  // Sender - specify exact origin
189
- targetWindow.postMessage(data, 'https://trusted-domain.com');
194
+ targetWindow.postMessage(data, "https://trusted-domain.com");
190
195
 
191
196
  // Receiver - always validate origin
192
- window.addEventListener('message', (event) => {
197
+ window.addEventListener("message", (event) => {
193
198
  // Validate origin
194
- if (event.origin !== 'https://trusted-domain.com') {
199
+ if (event.origin !== "https://trusted-domain.com") {
195
200
  return;
196
201
  }
197
202
 
198
203
  // Validate data structure
199
- if (typeof event.data !== 'object' || !event.data.type) {
204
+ if (event.data === null || typeof event.data !== "object" || !event.data.type) {
200
205
  return;
201
206
  }
202
207
 
@@ -212,14 +217,16 @@ window.addEventListener('message', (event) => {
212
217
  // Content-Security-Policy: require-trusted-types-for 'script'
213
218
 
214
219
  // Create a policy
215
- const policy = trustedTypes.createPolicy('default', {
220
+ const policy = trustedTypes.createPolicy("default", {
216
221
  createHTML: (input) => DOMPurify.sanitize(input),
217
- createScript: () => { throw new Error('Scripts not allowed'); },
222
+ createScript: () => {
223
+ throw new Error("Scripts not allowed");
224
+ },
218
225
  createScriptURL: (input) => {
219
226
  const url = new URL(input, location.origin);
220
227
  if (url.origin === location.origin) return input;
221
- throw new Error('Invalid script URL');
222
- }
228
+ throw new Error("Invalid script URL");
229
+ },
223
230
  });
224
231
 
225
232
  // Usage