@networkpro/web 1.7.6 β†’ 1.8.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.
package/README.md CHANGED
@@ -17,6 +17,8 @@ This file is part of Network Pro.
17
17
  [![Code Style: Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat)](https://github.com/prettier/prettier) [![stylelint](https://img.shields.io/badge/stylelint-%23747474?style=flat&logo=stylelint&logoSize=auto&labelColor=%23263238)](https://stylelint.io/)
18
18
  [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](https://github.com/netwk-pro/netwk-pro.github.io/blob/master/CODE_OF_CONDUCT.md)
19
19
 
20
+ <section id="top">
21
+
20
22
  ## πŸš€ Project Overview
21
23
 
22
24
  This GitHub repository powers the official web presence of **[Network Pro Strategies](https://netwk.pro/about)** β€” a privacy-first consultancy specializing in cybersecurity, network engineering, and information security. We also lead public advocacy efforts promoting digital privacy and responsible cyber policy.
@@ -26,7 +28,27 @@ Built with [SvelteKit](https://kit.svelte.dev/) and deployed via [Netlify](https
26
28
 
27
29
  All infrastructure and data flows are designed with **maximum transparency, self-hosting, and user privacy** in mind.
28
30
 
29
- ### πŸ“ Repository Structure
31
+ </section>
32
+
33
+ ### Table of Contents
34
+
35
+ - [Repository Structure](#structure)
36
+ - [Getting Started](#getting-started)
37
+ - [Configuration](#configuration)
38
+ - [Service Worker Utilities](#sw-utilities)
39
+ - [CSP Report Handler](#cspreport)
40
+ - [Testing](#testing)
41
+ - [Recommended Toolchain](#toolchain)
42
+ - [Tooling Configuration](#toolconfig)
43
+ - [Available Scripts](#scripts)
44
+ - [License](#license)
45
+ - [Questions](#questions)
46
+
47
+ ---
48
+
49
+ <section id="structure">
50
+
51
+ ## πŸ“ Repository Structure
30
52
 
31
53
  ```bash
32
54
  .
@@ -36,8 +58,9 @@ All infrastructure and data flows are designed with **maximum transparency, self
36
58
  β”‚ β”œβ”€β”€ extensions.json # Recommended VSCodium / VS Code extensions
37
59
  β”‚ β”œβ”€β”€ extensions.jsonc # Commented version of extensions.json for reference
38
60
  β”‚ └── settings.json # User settings for VSCodium / VS Code
39
- β”œβ”€β”€ netlify-functions/
40
- β”‚ └── cspReport.js # Serverless function to receive and log CSP violation reports
61
+ β”œβ”€β”€ netlify/
62
+ β”‚ └── edge-functions/ # Netlify Edge Functions directory
63
+ β”‚ └── csp-report.js # Edge Function to receive and log CSP violation reports
41
64
  β”œβ”€β”€ scripts/ # General utility scripts
42
65
  β”œβ”€β”€ src/
43
66
  β”‚ β”œβ”€β”€ lib/ # Reusable components, styles, utilities
@@ -74,8 +97,14 @@ tests/
74
97
  └── ...
75
98
  ```
76
99
 
100
+ </section>
101
+
102
+ <sub>[Back to top](#top)</sub>
103
+
77
104
  ---
78
105
 
106
+ <section id="getting-started">
107
+
79
108
  ## πŸ›  Getting Started
80
109
 
81
110
  ### πŸ“¦ Environment Setup
@@ -119,9 +148,9 @@ npm install
119
148
  > You can also use `bootstrap.local.sh` to automate the steps above and more (optional).
120
149
  > `ENV_MODE` controls local tooling behavior β€” it is not used by the app runtime directly.
121
150
 
122
- ---
151
+ &nbsp;
123
152
 
124
- #### πŸ’Ύ Version Enforcement
153
+ ### πŸ’Ύ Version Enforcement
125
154
 
126
155
  To ensure consistent environments across contributors and CI systems, this project enforces specific Node.js and npm versions via the `"engines"` field in `package.json`:
127
156
 
@@ -185,7 +214,13 @@ node -v # Should fall within engines.node
185
214
  npm -v # Should fall within engines.npm
186
215
  ```
187
216
 
188
- &nbsp;
217
+ </section>
218
+
219
+ <sub>[Back to top](#top)</sub>
220
+
221
+ ---
222
+
223
+ <section id="configuration">
189
224
 
190
225
  ## πŸ›‘οΈ Configuration
191
226
 
@@ -208,36 +243,117 @@ To re-enable nonce generation for inline scripts in the future:
208
243
 
209
244
  > πŸ’‘ The `[headers]` block in `netlify.toml` has been deprecated β€” all headers are now set dynamically from within SvelteKit.
210
245
 
211
- ---
246
+ &nbsp;
212
247
 
213
248
  ### 🧭 `hooks.client.ts`
214
249
 
215
- This lightweight hook enhances client experience:
250
+ Located at `src/hooks.client.ts`, this file is currently limited to handling uncaught client-side errors via the `handleError()` lifecycle hook.
216
251
 
217
- - Handles the `beforeinstallprompt` event to support progressive web app (PWA) install flows
218
- - Provides a `handleError()` hook that logs uncaught client-side errors
252
+ Client-side PWA logic (such as handling the `beforeinstallprompt` event, checking browser compatibility, and registering the service worker) has been moved to `src/lib/registerServiceWorker.js` for better modularity and testability.
219
253
 
220
- Located at `src/hooks.client.ts`, it is automatically used by the SvelteKit runtime during client boot.
254
+ > πŸ’‘ This separation ensures that error handling is isolated from PWA lifecycle logic, making both concerns easier to maintain.
255
+
256
+ </section>
257
+
258
+ <sub>[Back to top](#top)</sub>
221
259
 
222
260
  ---
223
261
 
224
- ### πŸ“£ CSP Report Handler
262
+ <section id="sw-utilities">
263
+
264
+ ## βš™οΈ Service Worker Utilities
265
+
266
+ This project includes modular service worker management to support PWA functionality, update lifecycles, and debugging workflows.
267
+
268
+ ### βœ… `registerServiceWorker.js`
269
+
270
+ Located at `src/lib/registerServiceWorker.js`, this module handles:
271
+
272
+ - **Service worker registration** (`service-worker.js`)
273
+ - **Update lifecycle**: prompts users when new content is available
274
+ - **Cache hygiene**: removes unexpected caches not prefixed with `cache-`
275
+ - **Install prompt support**: dispatches a `pwa-install-available` event for custom handling
276
+ - **Firefox compatibility**: skips registration in Firefox during localhost development
277
+
278
+ This function is typically called during client boot from `+layout.svelte` or another root-level component.
279
+
280
+ > ℹ️ The service worker will not register if the `?nosw` flag is present or if `window.__DISABLE_SW__` is set (see below).
281
+
282
+ &nbsp;
283
+
284
+ ### 🧹 `unregisterServiceWorker.js`
285
+
286
+ Located at `src/lib/unregisterServiceWorker.js`, this utility allows for manual deactivation of service workers during debugging or user opt-out flows.
287
+
288
+ It unregisters **all active service worker registrations** and logs the result.
289
+
290
+ &nbsp;
291
+
292
+ ### 🚫 `disableSw.js`
293
+
294
+ Located at `static/disableSw.js`, this file sets a global flag if the URL contains the `?nosw` query parameter:
295
+
296
+ ```js
297
+ if (location.search.includes("nosw")) {
298
+ window.__DISABLE_SW__ = true;
299
+ }
300
+ ```
301
+
302
+ This flag is used by `registerServiceWorker.js` to bypass registration. It's helpful for testing environments, browser compatibility checks, or simulating first-load conditions without service worker interference.
303
+
304
+ To use:
305
+
306
+ ```bash
307
+ https://netwk.pro/?nosw
308
+ ```
309
+
310
+ > πŸ’‘ `disableSw.js` is loaded from the static directory and runs early, ensuring the `__DISABLE_SW__` flag is available before service worker logic executes.
311
+
312
+ &nbsp;
313
+
314
+ #### πŸ”§ Example Usage
315
+
316
+ To register the service worker conditionally, call the function from client code:
317
+
318
+ ```js
319
+ import { registerServiceWorker } from "$lib/registerServiceWorker.js";
320
+
321
+ registerServiceWorker();
322
+ ```
323
+
324
+ You can optionally import unregisterServiceWorker() in a debug menu or settings panel to let users opt out of offline behavior.
325
+
326
+ </section>
327
+
328
+ <sub>[Back to top](#top)</sub>
329
+
330
+ ---
331
+
332
+ <section id="cspreport">
333
+
334
+ ## πŸ“£ CSP Report Handler
225
335
 
226
336
  To receive and inspect CSP violation reports in development or production, the repo includes a Netlify-compatible function at:
227
337
 
228
338
  ```bash
229
- netlify-functions/cspReport.js
339
+ netlify/edge-functions/csp-report.js
230
340
  ```
231
341
 
232
- This function receives reports sent to `/functions/cspReport` and logs them to the console. You can later integrate with logging tools or alerts (e.g., via email, Slack, or SIEM ingestion).
342
+ This Edge Function receives Content Security Policy (CSP) violation reports at `/api/csp-report` and logs relevant details to the console. High-risk violations (e.g., `script-src`, `form-action`) also trigger real-time alerts via `ntfy`. You can further integrate with logging tools, SIEM platforms, or notification systems as needed.
233
343
 
234
344
  Make sure to include the `report-uri` directive in your CSP header:
235
345
 
236
346
  ```bash
237
- Content-Security-Policy: ...; report-uri /.netlify/functions/cspReport;
347
+ Content-Security-Policy: ...; report-uri /api/csp-report;
238
348
  ```
239
349
 
240
- &nbsp;
350
+ </section>
351
+
352
+ <sub>[Back to top](#top)</sub>
353
+
354
+ ---
355
+
356
+ <section id="testing">
241
357
 
242
358
  ## πŸ§ͺ Testing
243
359
 
@@ -332,8 +448,14 @@ You can also audit locally using Chrome DevTools β†’ Lighthouse tab for on-the-f
332
448
 
333
449
  <!-- markdownlint-disable MD028 -->
334
450
 
451
+ </section>
452
+
453
+ <sub>[Back to top](#top)</sub>
454
+
335
455
  ---
336
456
 
457
+ <section id="toolchain">
458
+
337
459
  ## πŸ›  Recommended Toolchain
338
460
 
339
461
  To streamline development and align with project conventions, we recommend the following setup β€” especially for contributors without a strong existing preference.
@@ -371,8 +493,14 @@ npm run lint:fix
371
493
  npm run format:fix
372
494
  ```
373
495
 
496
+ </section>
497
+
498
+ <sub>[Back to top](#top)</sub>
499
+
374
500
  ---
375
501
 
502
+ <section id="toolconfig">
503
+
376
504
  ## βš™οΈ Tooling Configuration
377
505
 
378
506
  All linting, formatting, and version settings are defined in versioned project config files:
@@ -395,8 +523,14 @@ These are the same rules used by CI and automation, so aligning your local setup
395
523
 
396
524
  > Note: `.vscode/extensions.json` defines a minimal recommended dev stack for VSCodium / VS Code. These extensions are **optional but thoughtfully curated** to improve developer experience without introducing bloat.
397
525
 
526
+ </section>
527
+
528
+ <sub>[Back to top](#top)</sub>
529
+
398
530
  ---
399
531
 
532
+ <section id="scripts">
533
+
400
534
  ## πŸ“œ Available Scripts
401
535
 
402
536
  The following CLI commands are available via `npm run <script>` or `pnpm run <script>`.
@@ -412,7 +546,7 @@ The following CLI commands are available via `npm run <script>` or `pnpm run <sc
412
546
  | `build:netlify` | Build using Netlify CLI |
413
547
  | `css:bundle` | Bundle and minify CSS |
414
548
 
415
- ---
549
+ &nbsp;
416
550
 
417
551
  ### βœ… Pre-check / Sync
418
552
 
@@ -425,7 +559,7 @@ The following CLI commands are available via `npm run <script>` or `pnpm run <sc
425
559
  | `checkout` | Full local validation: check versions, test, lint, typecheck |
426
560
  | `verify` | Alias for `checkout` |
427
561
 
428
- ---
562
+ &nbsp;
429
563
 
430
564
  ### 🧹 Cleanup & Maintenance
431
565
 
@@ -435,7 +569,7 @@ The following CLI commands are available via `npm run <script>` or `pnpm run <sc
435
569
  | `clean` | Fully reset environment and reinstall |
436
570
  | `upgrade` | Update all dependencies via `npm-check-updates` |
437
571
 
438
- ---
572
+ &nbsp;
439
573
 
440
574
  <!-- markdownlint-disable MD024 -->
441
575
 
@@ -453,7 +587,7 @@ The following CLI commands are available via `npm run <script>` or `pnpm run <sc
453
587
  | `test:coverage` | Collect coverage from both client and server |
454
588
  | `test:e2e` | Runs E2E tests with up to 1 automatic retry on failure |
455
589
 
456
- ---
590
+ &nbsp;
457
591
 
458
592
  ### 🧼 Linting & Formatting
459
593
 
@@ -468,7 +602,7 @@ The following CLI commands are available via `npm run <script>` or `pnpm run <sc
468
602
  | `format` | Run Prettier formatting check |
469
603
  | `format:fix` | Auto-format code using Prettier |
470
604
 
471
- ---
605
+ &nbsp;
472
606
 
473
607
  ### πŸ’‘ Lighthouse / Performance
474
608
 
@@ -477,7 +611,7 @@ The following CLI commands are available via `npm run <script>` or `pnpm run <sc
477
611
  | `lhci` | Alias for Lighthouse CI |
478
612
  | `lhci:run` | Run Lighthouse CI autorun |
479
613
 
480
- ---
614
+ &nbsp;
481
615
 
482
616
  ### πŸ“‹ Audits / Validation
483
617
 
@@ -487,7 +621,7 @@ The following CLI commands are available via `npm run <script>` or `pnpm run <sc
487
621
  | `head:flatten` | Flatten headers for Netlify |
488
622
  | `head:validate` | Validate headers file against project config |
489
623
 
490
- ---
624
+ &nbsp;
491
625
 
492
626
  ### πŸ”„ Lifecycle Hooks
493
627
 
@@ -495,10 +629,14 @@ The following CLI commands are available via `npm run <script>` or `pnpm run <sc
495
629
  | ------------- | ----------------------------------- |
496
630
  | `postinstall` | Ensures version check after install |
497
631
 
498
- &nbsp;
632
+ </section>
633
+
634
+ <sub>[Back to top](#top)</sub>
499
635
 
500
636
  ---
501
637
 
638
+ <section id="license">
639
+
502
640
  ## 🧾 License
503
641
 
504
642
  This project is licensed under:
@@ -509,11 +647,21 @@ This project is licensed under:
509
647
 
510
648
  Source code, branding, and visual assets are subject to reuse and distribution terms specified on our [Legal, Copyright, and Licensing page](https://netwk.pro/license).
511
649
 
512
- &nbsp;
650
+ </section>
651
+
652
+ <sub>[Back to top](#top)</sub>
653
+
654
+ ---
655
+
656
+ <section id="questions">
513
657
 
514
658
  ## πŸ™‹β€β™‚οΈQuestions?
515
659
 
516
- Reach out via [netwk.pro/contact](https://netwk.pro/contact), open an issue on this repo, or email us directly at `contact (at) s.neteng.pro`.
660
+ Reach out via our [Contact Form](https://netwk.pro/contact), open an issue on this repo, or email us directly at `support (at) neteng.pro`.
661
+
662
+ </section>
663
+
664
+ <sub>[Back to top](#top)</sub>
517
665
 
518
666
  &nbsp;
519
667
 
@@ -522,7 +670,7 @@ _Designed for professionals. Hardened for privacy. Built with intent._
522
670
 
523
671
  ---
524
672
 
525
- <div style="font-size: 12px; text-align: center;">
673
+ <span style="font-size: 12px; text-align: center;">
526
674
 
527
675
  Copyright &copy; 2025
528
676
  **[Network Pro Strategies](https://netwk.pro) (Network Pro&trade;)**
@@ -531,4 +679,6 @@ Network Pro&trade;, the shield logo, and the "Locking Down Networks&trade;" slog
531
679
 
532
680
  Licensed under **[CC BY 4.0](https://netwk.pro/license#cc-by)** and the **[GNU GPL](https://netwk.pro/license#gnu-gpl)**, as published by the [Free Software Foundation](https://www.fsf.org), either version 3 of the License, or (at your option) any later version.
533
681
 
534
- </div>
682
+ </span>
683
+
684
+ <!-- cspell:ignore cspreport toolconfig -->
package/_redirects CHANGED
@@ -1,2 +1,5 @@
1
1
  https://www.netwk.pro/* https://netwk.pro/:splat 301
2
2
  /privacy-policy /privacy 301
3
+
4
+ # New redirect for Netlify function proxy
5
+ /api/* /.netlify/functions/:splat 200
package/cspell.json CHANGED
@@ -19,6 +19,7 @@
19
19
  "homescreen",
20
20
  "Izzy",
21
21
  "lhci",
22
+ "lifecycles",
22
23
  "lighthouseci",
23
24
  "lighthouserc",
24
25
  "lightningcss",
@@ -35,6 +36,7 @@
35
36
  "nosniff",
36
37
  "nosw",
37
38
  "npmjs",
39
+ "ntfy",
38
40
  "obtainium",
39
41
  "posthog",
40
42
  "SIEM",
@@ -45,6 +47,7 @@
45
47
  "subsites",
46
48
  "Supercookie",
47
49
  "supercookies",
50
+ "unregisters",
48
51
  "urlcheck",
49
52
  "vcard",
50
53
  "vite",
@@ -0,0 +1,151 @@
1
+ /* ==========================================================================
2
+ edge-functions/csp-report.js
3
+
4
+ Copyright Β© 2025 Network Pro Strategies (Network Proβ„’)
5
+ SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
6
+ This file is part of Network Pro.
7
+ ========================================================================== */
8
+
9
+ /**
10
+ * @file csp-report.js
11
+ * @description Netlify Edge Function to handle CSP violation reports.
12
+ *
13
+ * Accepts POST requests to /api/csp-report and logs relevant CSP reports.
14
+ * Filters out common low-value reports (e.g., img-src) to reduce invocation
15
+ * cost. Alerts on high-risk violations via ntfy topic.
16
+ *
17
+ * @module netlify/edge-functions
18
+ * @author SunDevil311
19
+ * @updated 2025-05-31
20
+ */
21
+
22
+ /**
23
+ * Netlify Edge Function entry point for CSP reporting.
24
+ *
25
+ * @param {Request} request - The incoming HTTP request object
26
+ * @param {import('@netlify/edge-functions').Context} _context - The Netlify Edge Function context (unused)
27
+ * @returns {Promise<Response>} HTTP Response with status 204 or 405
28
+ */
29
+ export default async (request, _context) => {
30
+ if (request.method !== "POST") {
31
+ return new Response("Method Not Allowed", { status: 405 });
32
+ }
33
+
34
+ try {
35
+ const body = await request.json();
36
+ const report = body["csp-report"];
37
+
38
+ // Ignore if report is missing or malformed
39
+ if (!report || typeof report !== "object") {
40
+ return new Response(null, { status: 204 });
41
+ }
42
+
43
+ const violated = report["violated-directive"] ?? "";
44
+ const blockedUri = report["blocked-uri"] ?? "";
45
+
46
+ // Filter: Skip img-src violations and empty URIs
47
+ const ignored = [
48
+ violated.startsWith("img-src"),
49
+ blockedUri === "",
50
+ blockedUri === "about",
51
+ blockedUri.startsWith("chrome-extension://"),
52
+ blockedUri.startsWith("moz-extension://"),
53
+ ].some(Boolean);
54
+
55
+ if (ignored) {
56
+ return new Response(null, { status: 204 });
57
+ }
58
+
59
+ // Send alert for high-risk directives
60
+ await sendToNtfy(violated, blockedUri, report);
61
+
62
+ // Log useful violations
63
+ console.log("[CSP-Edge] Violation:", {
64
+ directive: violated,
65
+ uri: blockedUri,
66
+ referrer: report["referrer"],
67
+ source: report["source-file"],
68
+ line: report["line-number"],
69
+ });
70
+ } catch (err) {
71
+ console.warn("[CSP-Edge] Failed to parse CSP report:", err.message);
72
+ }
73
+
74
+ return new Response(null, { status: 204 });
75
+ };
76
+
77
+ const recentViolations = new Map();
78
+ const VIOLATION_TTL_MS = 60_000;
79
+
80
+ /**
81
+ * Sends a high-priority alert to your ntfy topic for high-risk CSP violations.
82
+ * Applies rate-limiting to avoid sending duplicate alerts within 60 seconds.
83
+ *
84
+ * @param {string} violated - The violated CSP directive
85
+ * @param {string} blockedUri - The URI that was blocked
86
+ * @param {Record<string, any>} report - The full CSP report object
87
+ */
88
+ async function sendToNtfy(violated, blockedUri, report) {
89
+ const highRiskDirectives = [
90
+ "script-src",
91
+ "form-action",
92
+ "frame-ancestors",
93
+ "base-uri",
94
+ ];
95
+
96
+ const directiveKey = violated.split(" ")[0]; // strip fallback values or sources
97
+ const isHighRisk = highRiskDirectives.includes(directiveKey);
98
+ console.log(`[CSP-Edge] Checking directive: ${directiveKey}`);
99
+ if (!isHighRisk) return;
100
+
101
+ const key = `${violated}|${blockedUri}`;
102
+ const now = Date.now();
103
+
104
+ // Skip and log if violation was reported recently
105
+ if (
106
+ recentViolations.has(key) &&
107
+ now - recentViolations.get(key) < VIOLATION_TTL_MS
108
+ ) {
109
+ console.log(`[CSP-Edge] Skipped duplicate alert for ${key}`);
110
+ return;
111
+ }
112
+
113
+ // Record the current timestamp
114
+ recentViolations.set(key, now);
115
+
116
+ // Cleanup old entries (memory-safe for low volume)
117
+ for (const [k, t] of recentViolations.entries()) {
118
+ if (now - t > VIOLATION_TTL_MS) {
119
+ recentViolations.delete(k);
120
+ }
121
+ }
122
+
123
+ const topicUrl = "https://ntfy.neteng.pro/csp-alerts";
124
+
125
+ const message = [
126
+ `🚨 CSP Violation Detected`,
127
+ `Directive: ${violated}`,
128
+ `Blocked URI: ${blockedUri}`,
129
+ `Referrer: ${report.referrer || "N/A"}`,
130
+ `Source: ${report["source-file"] || "N/A"}`,
131
+ `Line: ${report["line-number"] || "N/A"}`,
132
+ ].join("\n");
133
+
134
+ await fetch(topicUrl, {
135
+ method: "POST",
136
+ headers: {
137
+ "Content-Type": "text/plain",
138
+ "X-Title": "High-Risk CSP Violation",
139
+ "X-Priority": "5",
140
+ },
141
+ body: message,
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Configuration block for the Edge Function.
147
+ * This sets the endpoint route to /api/csp-report
148
+ */
149
+ export const config = {
150
+ path: "/api/csp-report",
151
+ };
package/netlify.toml CHANGED
@@ -11,9 +11,9 @@
11
11
  targetPort = 5173
12
12
  port = 8888
13
13
 
14
- [functions."*"]
15
- node_bundler = "esbuild"
16
- included_files = ["netlify-functions/**"]
14
+ [[edge_functions]]
15
+ path = "/api/csp-report"
16
+ function = "csp-report"
17
17
 
18
18
  [[plugins]]
19
19
  package = "netlify-plugin-submit-sitemap"
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "sideEffects": [
5
5
  "./.netlify/shims.js"
6
6
  ],
7
- "version": "1.7.6",
7
+ "version": "1.8.3",
8
8
  "description": "Locking Down Networks, Unlocking Confidence | Security, Networking, Privacy β€” Network Pro Strategies",
9
9
  "keywords": [
10
10
  "advisory",
@@ -80,7 +80,7 @@
80
80
  "nodemailer": "^7.0.3",
81
81
  "posthog-js": "^1.249.0",
82
82
  "semver": "^7.7.2",
83
- "svelte": "5.33.10"
83
+ "svelte": "5.33.11"
84
84
  },
85
85
  "devDependencies": {
86
86
  "@eslint/compat": "^1.2.9",
@@ -97,7 +97,7 @@
97
97
  "browserslist": "^4.25.0",
98
98
  "eslint": "^9.28.0",
99
99
  "eslint-config-prettier": "^10.1.5",
100
- "eslint-plugin-jsdoc": "^50.6.17",
100
+ "eslint-plugin-jsdoc": "^50.7.0",
101
101
  "eslint-plugin-svelte": "^3.9.0",
102
102
  "globals": "^16.2.0",
103
103
  "jsdom": "^26.1.0",
@@ -21,14 +21,11 @@ export async function handle({ event, resolve }) {
21
21
  const isProdEnvironment =
22
22
  process.env.NODE_ENV === "production" || process.env.ENV_MODE === "prod";
23
23
 
24
- if (!isProdEnvironment) {
25
- console.log("ENV_MODE:", process.env.ENV_MODE);
26
- }
24
+ console.log("[CSP Debug] NODE_ENV:", process.env.NODE_ENV);
25
+ console.log("[CSP Debug] ENV_MODE:", process.env.ENV_MODE);
27
26
 
28
27
  // Determine report URI
29
- const reportUri = isProdEnvironment
30
- ? "/.netlify/functions/cspReport"
31
- : "/api/mock-csp";
28
+ const reportUri = isProdEnvironment ? "/api/csp-report" : "/api/mock-csp";
32
29
 
33
30
  // Construct base policy
34
31
  const cspDirectives = [
@@ -79,7 +79,7 @@ This file is part of Network Pro.
79
79
  {#if label === "blog"}
80
80
  <br />
81
81
  {:else}
82
- <span class="separator">|</span>
82
+ <span class="goldseparator">|</span>
83
83
  {/if}
84
84
  {/if}
85
85
  {/each}
@@ -77,7 +77,7 @@ This file is part of Network Pro.
77
77
  {#if label === "blog"}
78
78
  <br />
79
79
  {:else}
80
- <span class="separator">|</span>
80
+ <span class="goldseparator">|</span>
81
81
  {/if}
82
82
  {/if}
83
83
  {/each}
@@ -46,7 +46,7 @@ This file is part of Network Pro.
46
46
  */
47
47
  const pageInfo = {
48
48
  title: "FOSS Spotlight",
49
- lastUpdated: "May 26, 2025",
49
+ lastUpdated: "May 30, 2025",
50
50
  };
51
51
 
52
52
  /** @type {any} */
@@ -67,8 +67,7 @@ This file is part of Network Pro.
67
67
  href="https://spdx.dev/learn/handling-license-info"
68
68
  target={constants.targetBlank}>
69
69
  SPDX License Identifier
70
- </a>
71
- : &nbsp;<code>CC-BY-4.0 OR GPL-3.0-or-later</code>
70
+ </a>: &nbsp;<code>CC-BY-4.0 OR GPL-3.0-or-later</code>
72
71
  </span>
73
72
  </section>
74
73
 
@@ -439,8 +439,8 @@ footer .container {
439
439
 
440
440
  .center-nav {
441
441
  padding: 5px;
442
- font-size: 0.875rem;
443
- line-height: 1.125rem;
442
+ font-size: 1rem;
443
+ line-height: 1.5rem;
444
444
  text-align: center;
445
445
  }
446
446
 
@@ -497,3 +497,12 @@ footer .container {
497
497
  font-family: inherit; /* Ensure it uses the same font-family as normal text */
498
498
  font-style: normal; /* Remove italic for the description */
499
499
  }
500
+
501
+ .goldseparator {
502
+ margin: 0 0.5rem;
503
+ color: #FFC627;
504
+ }
505
+
506
+ .gold {
507
+ color: #FFC627;
508
+ }
@@ -3,4 +3,4 @@ Copyright Β© 2025 Network Pro Strategies (Network Proβ„’)
3
3
  SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
4
4
  This file is part of Network Pro.
5
5
  ========================================================================== */
6
- html{-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{margin:.67em 0;font-size:2em}hr{box-sizing:content-box}pre{font-family:monospace;font-size:1em}a{background-color:#0000}abbr[title]{border-bottom:none;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:100%;line-height:1.15}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted buttontext}fieldset{padding:.35em .75em .625em}legend{color:inherit;box-sizing:border-box;white-space:normal;max-width:100%;padding:0;display:table}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}details{display:block}summary{display:list-item}template{display:none}html{color:#222;scroll-behavior:smooth;font-size:1em;line-height:1.4}::-moz-selection{text-shadow:none;background:#191919}::selection{text-shadow:none;background:#191919}hr{border:0;border-top:1px solid #ccc;height:1px;margin:1em 0;padding:0;display:block;overflow:visible}audio,canvas,iframe,img,svg,video{vertical-align:middle}fieldset{border:0;margin:0;padding:0}textarea{resize:vertical}body{color:#fafafa;background-color:#191919;margin:10px;font-family:Arial,Helvetica,sans-serif}a{text-decoration:none}a:link{color:#ffc627}a:hover,a:active{color:#ffc627;text-decoration:underline}a:focus{color:#191919;background-color:#ffc627}a:visited,a:visited:hover{color:#7f6227}.hidden,[hidden]{display:none!important}.visually-hidden{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.visually-hidden.focusable:active,.visually-hidden.focusable:focus{clip:auto;width:auto;height:auto;white-space:inherit;margin:0;position:static;overflow:visible}.invisible{visibility:hidden}.clearfix:before,.clearfix:after{content:"";display:table}.clearfix:after{clear:both}@media print{*,:before,:after{color:#000!important;box-shadow:none!important;text-shadow:none!important;background:#fff!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href)")"}abbr[title]:after{content:" (" attr(title)")"}a[href^=\#]:after,a[href^=javascript\:]:after{content:""}pre{white-space:pre-wrap!important}pre,blockquote{page-break-inside:avoid;border:1px solid #999}tr,img{page-break-inside:avoid}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}.full-width-section{background-position:50%;background-size:cover;width:100%;max-width:1920px;margin:0 auto}.container{max-width:1200px;margin:0 auto;padding:0 12px}.readable{max-width:900px;margin:0 auto}header,footer{width:100%}header .container,footer .container{max-width:1200px;margin:0 auto;padding:20px 12px}.gh{border-collapse:collapse;border-spacing:0;margin:0 auto}.gh td,.gh th{border-collapse:collapse;word-break:normal;padding:10px 5px;overflow:hidden}.gh .gh-tcell{text-align:center;vertical-align:middle}@media screen and (width<=767px){.gh,.gh col{width:auto!important}.gh-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.soc{border-collapse:collapse;border-spacing:0;margin:0 auto}.soc td,.soc th{border-collapse:collapse;word-break:normal;padding:8px;overflow:hidden}.soc .soc-fa{text-align:center;vertical-align:middle}@media screen and (width<=767px){.soc,.soc col{width:auto!important}.soc-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.foss{border-collapse:collapse;border-spacing:0}.foss td,.foss th{border-collapse:collapse;word-break:normal;padding:10px 5px;overflow:hidden}.foss .foss-cell{text-align:center;vertical-align:middle}@media screen and (width<=767px){.foss,.foss col{width:auto!important}.foss-wrap{-webkit-overflow-scrolling:touch;overflow-x:auto}}.bnav{text-align:center;border-collapse:collapse;border-spacing:0;margin:0 auto}.bnav td,.bnav th{text-align:center;vertical-align:middle;word-break:normal;border-style:none;padding:10px;font-size:.875rem;font-weight:700;line-height:1.125rem;overflow:hidden}.bnav .bnav-cell{text-align:center;vertical-align:middle;align-content:center}@media screen and (width<=767px){.bnav,.bnav col{width:auto!important}.bnav-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.bnav2{border-collapse:collapse;border-spacing:0;margin:0 auto}.bnav2 td{word-break:normal;border-style:none;padding:10px;font-size:.875rem;font-weight:700;line-height:1.125rem;overflow:hidden}.bnav2 th{word-break:normal;border-style:none;padding:12px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.bnav2 .bnav2-cell{text-align:center;vertical-align:middle;align-content:center}@media screen and (width<=767px){.bnav2,.bnav2 col{width:auto!important}.bnav2-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.pgp{border-collapse:collapse;border-spacing:0;margin:0 auto}.pgp td{word-break:normal;border-style:none;padding:10px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.pgp th{word-break:normal;border:1px solid #000;padding:10px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.pgp .pgp-col1{text-align:right;vertical-align:middle;padding-right:1rem}.pgp .pgp-col2{text-align:left;vertical-align:middle;padding-left:1rem}@media screen and (width<=767px){.pgp,.pgp col{width:auto!important}.pgp-wrap{-webkit-overflow-scrolling:touch;margin:2rem 0 auto;overflow-x:auto}}.logo{margin-left:auto;margin-right:auto;display:block}.index-title1{text-align:center;font-style:italic;font-weight:700}.index-title2{letter-spacing:-.015em;text-align:center;font-variant:small-caps;font-size:1.25rem;line-height:1.625rem}.index1{letter-spacing:-.035em;text-align:center;font-style:italic;font-weight:700;line-height:2.125rem}.index2{letter-spacing:-.035em;text-align:center;font-variant:small-caps;font-size:1.5rem;line-height:1.75rem}.index3{letter-spacing:-.035em;text-align:center;font-size:1.5rem;line-height:1.75rem}.index4{letter-spacing:-.035em;text-align:center;font-size:1.5rem;line-height:1.75rem;text-decoration:underline}.subhead{letter-spacing:-.035em;font-variant:small-caps;font-size:1.5rem;line-height:1.75rem}.bold{font-weight:700}.emphasis{font-style:italic}.uline{text-decoration:underline}.bolditalic{font-style:italic;font-weight:700}.bquote{border-left:3px solid #9e9e9e;margin-left:30px;padding-left:10px;font-style:italic}.small-text{font-size:.75rem;line-height:1.125rem}.large-text-center{text-align:center;font-size:1.25rem;line-height:1.75rem}.prewrap{white-space:pre-wrap;display:block}.hr-styled{width:75%;margin:auto}.center-text{text-align:center}.copyright{text-align:center;font-size:.75rem;line-height:1.125rem}.visited{color:#7f6227}.center-nav{text-align:center;padding:5px;font-size:.875rem;line-height:1.125rem}.block{resize:none;background:0 0;border:none;border-radius:0;outline:none;width:100%;font-size:.75rem;line-height:1.125rem}.fingerprint{white-space:pre-line;font-weight:700;display:block}.pgp-image{width:150px;height:150px}.spacer{margin:2rem 0}.separator{margin:0 .5rem}.emoji{margin-right:8px}.headline{margin-bottom:4px;font-style:italic;font-weight:700;display:block}.label{font-family:inherit;font-weight:700}.description{font-family:inherit;font-style:normal;font-weight:400;display:inline}
6
+ html{-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{margin:.67em 0;font-size:2em}hr{box-sizing:content-box}pre{font-family:monospace;font-size:1em}a{background-color:#0000}abbr[title]{border-bottom:none;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:100%;line-height:1.15}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted buttontext}fieldset{padding:.35em .75em .625em}legend{color:inherit;box-sizing:border-box;white-space:normal;max-width:100%;padding:0;display:table}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}details{display:block}summary{display:list-item}template{display:none}html{color:#222;scroll-behavior:smooth;font-size:1em;line-height:1.4}::-moz-selection{text-shadow:none;background:#191919}::selection{text-shadow:none;background:#191919}hr{border:0;border-top:1px solid #ccc;height:1px;margin:1em 0;padding:0;display:block;overflow:visible}audio,canvas,iframe,img,svg,video{vertical-align:middle}fieldset{border:0;margin:0;padding:0}textarea{resize:vertical}body{color:#fafafa;background-color:#191919;margin:10px;font-family:Arial,Helvetica,sans-serif}a{text-decoration:none}a:link{color:#ffc627}a:hover,a:active{color:#ffc627;text-decoration:underline}a:focus{color:#191919;background-color:#ffc627}a:visited,a:visited:hover{color:#7f6227}.hidden,[hidden]{display:none!important}.visually-hidden{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.visually-hidden.focusable:active,.visually-hidden.focusable:focus{clip:auto;width:auto;height:auto;white-space:inherit;margin:0;position:static;overflow:visible}.invisible{visibility:hidden}.clearfix:before,.clearfix:after{content:"";display:table}.clearfix:after{clear:both}@media print{*,:before,:after{color:#000!important;box-shadow:none!important;text-shadow:none!important;background:#fff!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href)")"}abbr[title]:after{content:" (" attr(title)")"}a[href^=\#]:after,a[href^=javascript\:]:after{content:""}pre{white-space:pre-wrap!important}pre,blockquote{page-break-inside:avoid;border:1px solid #999}tr,img{page-break-inside:avoid}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}.full-width-section{background-position:50%;background-size:cover;width:100%;max-width:1920px;margin:0 auto}.container{max-width:1200px;margin:0 auto;padding:0 12px}.readable{max-width:900px;margin:0 auto}header,footer{width:100%}header .container,footer .container{max-width:1200px;margin:0 auto;padding:20px 12px}.gh{border-collapse:collapse;border-spacing:0;margin:0 auto}.gh td,.gh th{border-collapse:collapse;word-break:normal;padding:10px 5px;overflow:hidden}.gh .gh-tcell{text-align:center;vertical-align:middle}@media screen and (width<=767px){.gh,.gh col{width:auto!important}.gh-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.soc{border-collapse:collapse;border-spacing:0;margin:0 auto}.soc td,.soc th{border-collapse:collapse;word-break:normal;padding:8px;overflow:hidden}.soc .soc-fa{text-align:center;vertical-align:middle}@media screen and (width<=767px){.soc,.soc col{width:auto!important}.soc-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.foss{border-collapse:collapse;border-spacing:0}.foss td,.foss th{border-collapse:collapse;word-break:normal;padding:10px 5px;overflow:hidden}.foss .foss-cell{text-align:center;vertical-align:middle}@media screen and (width<=767px){.foss,.foss col{width:auto!important}.foss-wrap{-webkit-overflow-scrolling:touch;overflow-x:auto}}.bnav{text-align:center;border-collapse:collapse;border-spacing:0;margin:0 auto}.bnav td,.bnav th{text-align:center;vertical-align:middle;word-break:normal;border-style:none;padding:10px;font-size:.875rem;font-weight:700;line-height:1.125rem;overflow:hidden}.bnav .bnav-cell{text-align:center;vertical-align:middle;align-content:center}@media screen and (width<=767px){.bnav,.bnav col{width:auto!important}.bnav-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.bnav2{border-collapse:collapse;border-spacing:0;margin:0 auto}.bnav2 td{word-break:normal;border-style:none;padding:10px;font-size:.875rem;font-weight:700;line-height:1.125rem;overflow:hidden}.bnav2 th{word-break:normal;border-style:none;padding:12px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.bnav2 .bnav2-cell{text-align:center;vertical-align:middle;align-content:center}@media screen and (width<=767px){.bnav2,.bnav2 col{width:auto!important}.bnav2-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.pgp{border-collapse:collapse;border-spacing:0;margin:0 auto}.pgp td{word-break:normal;border-style:none;padding:10px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.pgp th{word-break:normal;border:1px solid #000;padding:10px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.pgp .pgp-col1{text-align:right;vertical-align:middle;padding-right:1rem}.pgp .pgp-col2{text-align:left;vertical-align:middle;padding-left:1rem}@media screen and (width<=767px){.pgp,.pgp col{width:auto!important}.pgp-wrap{-webkit-overflow-scrolling:touch;margin:2rem 0 auto;overflow-x:auto}}.logo{margin-left:auto;margin-right:auto;display:block}.index-title1{text-align:center;font-style:italic;font-weight:700}.index-title2{letter-spacing:-.015em;text-align:center;font-variant:small-caps;font-size:1.25rem;line-height:1.625rem}.index1{letter-spacing:-.035em;text-align:center;font-style:italic;font-weight:700;line-height:2.125rem}.index2{letter-spacing:-.035em;text-align:center;font-variant:small-caps;font-size:1.5rem;line-height:1.75rem}.index3{letter-spacing:-.035em;text-align:center;font-size:1.5rem;line-height:1.75rem}.index4{letter-spacing:-.035em;text-align:center;font-size:1.5rem;line-height:1.75rem;text-decoration:underline}.subhead{letter-spacing:-.035em;font-variant:small-caps;font-size:1.5rem;line-height:1.75rem}.bold{font-weight:700}.emphasis{font-style:italic}.uline{text-decoration:underline}.bolditalic{font-style:italic;font-weight:700}.bquote{border-left:3px solid #9e9e9e;margin-left:30px;padding-left:10px;font-style:italic}.small-text{font-size:.75rem;line-height:1.125rem}.large-text-center{text-align:center;font-size:1.25rem;line-height:1.75rem}.prewrap{white-space:pre-wrap;display:block}.hr-styled{width:75%;margin:auto}.center-text{text-align:center}.copyright{text-align:center;font-size:.75rem;line-height:1.125rem}.visited{color:#7f6227}.center-nav{text-align:center;padding:5px;font-size:1rem;line-height:1.5rem}.block{resize:none;background:0 0;border:none;border-radius:0;outline:none;width:100%;font-size:.75rem;line-height:1.125rem}.fingerprint{white-space:pre-line;font-weight:700;display:block}.pgp-image{width:150px;height:150px}.spacer{margin:2rem 0}.separator{margin:0 .5rem}.emoji{margin-right:8px}.headline{margin-bottom:4px;font-style:italic;font-weight:700;display:block}.label{font-family:inherit;font-weight:700}.description{font-family:inherit;font-style:normal;font-weight:400;display:inline}.goldseparator{color:#ffc627;margin:0 .5rem}.gold{color:#ffc627}
@@ -0,0 +1,32 @@
1
+ /* ==========================================================================
2
+ src/routes/status/+page.server.js
3
+
4
+ Copyright Β© 2025 Network Pro Strategies (Network Proβ„’)
5
+ SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
6
+ This file is part of Network Pro.
7
+ ========================================================================== */
8
+
9
+ /**
10
+ * Server-side load function for /status route.
11
+ *
12
+ * Logs runtime environment variables for debugging purposes.
13
+ * This function executes on the server during SSR and is used to verify
14
+ * that environment variables (e.g., ENV_MODE and NODE_ENV) are properly
15
+ * injected and available during runtime in Netlify's SSR context.
16
+ *
17
+ * @module src/routes/status
18
+ * @author SunDevil311
19
+ * @updated 2025-05-31
20
+ */
21
+
22
+ export const prerender = false;
23
+
24
+ /**
25
+ * @function load
26
+ * @returns {{ ok: boolean }} Response indicating successful execution
27
+ */
28
+ export function load() {
29
+ console.log("[Status Debug] ENV_MODE:", process.env.ENV_MODE);
30
+ console.log("[Status Debug] NODE_ENV:", process.env.NODE_ENV);
31
+ return { ok: true };
32
+ }
@@ -0,0 +1,81 @@
1
+ /* ==========================================================================
2
+ tests/unit/csp-report.test.js
3
+
4
+ Copyright Β© 2025 Network Pro Strategies (Network Proβ„’)
5
+ SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
6
+ This file is part of Network Pro.
7
+ ========================================================================== */
8
+
9
+ /**
10
+ * Tests the edge-functions/csp-report.js CSP reporting endpoint
11
+ *
12
+ * @module tests/unit
13
+ * @author SunDevil311
14
+ * @updated 2025-05-31
15
+ */
16
+
17
+ /** @file Unit tests for edge-functions/csp-report.js using Vitest */
18
+ /** @typedef {import('vitest').TestContext} TestContext */
19
+
20
+ import { beforeEach, describe, expect, it, vi } from "vitest";
21
+ import handler from "../../netlify/edge-functions/csp-report.js";
22
+
23
+ // πŸ§ͺ Mock fetch used by sendToNtfy inside the Edge Function
24
+ global.fetch = vi.fn(() =>
25
+ Promise.resolve({ ok: true, status: 200, text: () => "OK" }),
26
+ );
27
+
28
+ describe("csp-report.js", () => {
29
+ beforeEach(() => {
30
+ vi.clearAllMocks();
31
+ });
32
+
33
+ it("should handle a valid CSP report", async () => {
34
+ const req = new Request("http://localhost/api/csp-report", {
35
+ method: "POST",
36
+ headers: { "Content-Type": "application/json" },
37
+ body: JSON.stringify({
38
+ "csp-report": {
39
+ "document-uri": "https://example.com",
40
+ "violated-directive": "script-src",
41
+ "blocked-uri": "https://malicious.site",
42
+ },
43
+ }),
44
+ });
45
+
46
+ const res = await handler(req, {});
47
+ expect(res.status).toBe(204);
48
+ });
49
+
50
+ it("should reject non-POST requests", async () => {
51
+ const req = new Request("http://localhost/api/csp-report", {
52
+ method: "GET",
53
+ });
54
+
55
+ const res = await handler(req, {});
56
+ const text = await res.text();
57
+ expect(res.status).toBe(405);
58
+ expect(text).toContain("Method Not Allowed");
59
+ });
60
+
61
+ it("should handle malformed JSON", async () => {
62
+ const badJson = "{ invalid json }";
63
+ const req = new Request("http://localhost/api/csp-report", {
64
+ method: "POST",
65
+ headers: { "Content-Type": "application/json" },
66
+ body: badJson,
67
+ });
68
+
69
+ const res = await handler(req, {});
70
+ expect(res.status).toBe(204); // The current handler swallows errors silently
71
+ });
72
+
73
+ it("should handle missing body", async () => {
74
+ const req = new Request("http://localhost/api/csp-report", {
75
+ method: "POST",
76
+ });
77
+
78
+ const res = await handler(req, {});
79
+ expect(res.status).toBe(204); // No body is also treated silently
80
+ });
81
+ });
@@ -1,76 +0,0 @@
1
- /* ==========================================================================
2
- netlify-functions/cspReport.js
3
-
4
- Copyright Β© 2025 Network Pro Strategies (Network Proβ„’)
5
- SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
6
- This file is part of Network Pro.
7
- ========================================================================== */
8
-
9
- /**
10
- * @file cspReport.js
11
- * @description Sets up a CSP reporting endpoint with email notifications, to be deployed as a Netlify function.
12
- * @module netlify-functions
13
- * @author SunDevil311
14
- * @updated 2025-05-29
15
- */
16
-
17
- import nodemailer from "nodemailer";
18
-
19
- /**
20
- * Netlify Function: CSP violation report handler
21
- *
22
- * @param {import('@netlify/functions').HandlerEvent} event - Incoming Netlify request
23
- * @returns {Promise<import('@netlify/functions').HandlerResponse>} - Netlify-compatible HTTP response
24
- */
25
- export async function handler(event) {
26
- try {
27
- if (event.httpMethod !== "POST") {
28
- return {
29
- statusCode: 405,
30
- body: "Method Not Allowed",
31
- };
32
- }
33
-
34
- if (!event.body) {
35
- return {
36
- statusCode: 400,
37
- body: "No body provided",
38
- };
39
- }
40
-
41
- /** @type {Record<string, any>} */
42
- const report = JSON.parse(event.body);
43
- const violation = report["csp-report"] || report;
44
-
45
- const shouldSendEmail =
46
- process.env.MAIL_ENABLED !== "false" && process.env.NODE_ENV !== "test";
47
-
48
- if (shouldSendEmail) {
49
- const transporter = nodemailer.createTransport({
50
- host: process.env.SMTP_HOST,
51
- port: 465,
52
- secure: true,
53
- auth: {
54
- user: process.env.SMTP_USER,
55
- pass: process.env.SMTP_PASS,
56
- },
57
- });
58
-
59
- await transporter.sendMail({
60
- from: `"CSP Reporter" <${process.env.SMTP_USER}>`,
61
- to: process.env.NOTIFY_EMAIL,
62
- subject: "🚨 CSP Violation Detected",
63
- text: JSON.stringify(violation, null, 2),
64
- });
65
- }
66
-
67
- return {
68
- statusCode: 204,
69
- };
70
- } catch (error) {
71
- return {
72
- statusCode: 400,
73
- body: `Invalid JSON: ${error instanceof Error ? error.message : "Unknown error"}`,
74
- };
75
- }
76
- }
@@ -1,81 +0,0 @@
1
- /* ==========================================================================
2
- tests/unit/cspReport.test.js
3
-
4
- Copyright Β© 2025 Network Pro Strategies (Network Proβ„’)
5
- SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
6
- This file is part of Network Pro.
7
- ========================================================================== */
8
-
9
- /** @file Unit tests for netlify-functions/cspReport.js using Vitest */
10
- /** @typedef {import('vitest').TestContext} TestContext */
11
-
12
- import { beforeEach, describe, expect, it, vi } from "vitest";
13
- import { handler } from "../../netlify-functions/cspReport.js";
14
-
15
- // πŸ§ͺ Force test mode
16
- process.env.NODE_ENV = "test";
17
- process.env.MAIL_ENABLED = "true"; // Still ignored due to NODE_ENV === test
18
-
19
- // πŸ§ͺ Mock nodemailer to prevent real email sending
20
- vi.mock("nodemailer", async () => {
21
- return {
22
- default: {
23
- createTransport: () => ({
24
- sendMail: vi.fn().mockResolvedValue({}),
25
- }),
26
- },
27
- };
28
- });
29
-
30
- describe("cspReport.js", () => {
31
- beforeEach(() => {
32
- vi.clearAllMocks(); // reset mocks if needed
33
- });
34
-
35
- it("should handle valid CSP report", async () => {
36
- /** @type {import('netlify/functions').HandlerEvent} */
37
- const event = {
38
- httpMethod: "POST",
39
- body: JSON.stringify({
40
- "csp-report": {
41
- "document-uri": "https://example.com",
42
- "violated-directive": "script-src",
43
- },
44
- }),
45
- };
46
-
47
- const response = await handler(event);
48
- expect(response.statusCode).toBe(204);
49
- });
50
-
51
- it("should reject GET requests", async () => {
52
- /** @type {import('netlify/functions').HandlerEvent} */
53
- const event = { httpMethod: "GET" };
54
- const response = await handler(event);
55
- expect(response.statusCode).toBe(405);
56
- expect(response.body).toContain("Method Not Allowed");
57
- });
58
-
59
- it("should handle malformed JSON", async () => {
60
- /** @type {import('netlify/functions').HandlerEvent} */
61
- const event = {
62
- httpMethod: "POST",
63
- body: "{ bad json }",
64
- };
65
-
66
- const response = await handler(event);
67
- expect(response.statusCode).toBe(400);
68
- expect(response.body).toContain("Invalid JSON");
69
- });
70
-
71
- it("should handle missing body", async () => {
72
- /** @type {import('netlify/functions').HandlerEvent} */
73
- const event = {
74
- httpMethod: "POST",
75
- };
76
-
77
- const response = await handler(event);
78
- expect(response.statusCode).toBe(400);
79
- expect(response.body).toContain("No body provided");
80
- });
81
- });