@networkpro/web 1.8.3 β 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -51
- package/package.json +4 -4
- package/scripts/generateTest.js +61 -0
- package/src/lib/components/foss/FossItemContent.svelte +28 -30
- package/src/lib/pages/AboutContent.svelte +15 -5
- package/src/lib/pages/LicenseContent.svelte +2 -2
- package/src/lib/types/fossTypes.js +23 -0
- package/src/lib/utils/purify.js +74 -0
- package/tests/internal/auditCoverage.test.js +99 -0
- package/tests/unit/lib/utils/purify.test.js +50 -0
- package/vitest.config.client.js +5 -1
- package/vitest.config.server.js +1 -0
- package/tests/unit/auditScripts.test.js +0 -43
package/README.md
CHANGED
|
@@ -51,34 +51,41 @@ All infrastructure and data flows are designed with **maximum transparency, self
|
|
|
51
51
|
## π Repository Structure
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
|
-
.
|
|
55
|
-
βββ .github/
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
β βββ
|
|
59
|
-
β βββ extensions.
|
|
60
|
-
β
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
β
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
β βββ
|
|
69
|
-
β βββ hooks.
|
|
70
|
-
β βββ
|
|
71
|
-
β
|
|
72
|
-
βββ
|
|
73
|
-
β βββ
|
|
74
|
-
β
|
|
75
|
-
β
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
β
|
|
79
|
-
βββ
|
|
80
|
-
βββ
|
|
81
|
-
βββ
|
|
54
|
+
.
|
|
55
|
+
βββ .github/
|
|
56
|
+
β βββ workflows/ # CI workflows (e.g. test, deploy)
|
|
57
|
+
βββ .vscode/
|
|
58
|
+
β βββ customData.json # Custom CSS IntelliSense (e.g. FontAwesome)
|
|
59
|
+
β βββ extensions.json # Recommended VS Code / VSCodium extensions
|
|
60
|
+
β βββ extensions.jsonc # Commented version of extensions.json
|
|
61
|
+
β βββ settings.json # Workspace settings
|
|
62
|
+
βββ netlify/
|
|
63
|
+
β βββ edge-functions/
|
|
64
|
+
β β βββ csp-report.js # Receives CSP violation reports
|
|
65
|
+
β βββ netlify.toml # Netlify configuration
|
|
66
|
+
βββ scripts/ # General-purpose utility scripts
|
|
67
|
+
βββ src/
|
|
68
|
+
β βββ app.html # Entry HTML (CSP meta, bootstrapping)
|
|
69
|
+
β βββ hooks.client.ts # PWA install prompt & client-side logging
|
|
70
|
+
β βββ hooks.server.js # Injects CSP headers and permissions policy
|
|
71
|
+
β βββ lib/ # Components, utilities, types, styles
|
|
72
|
+
β β βββ components/ # Svelte components
|
|
73
|
+
β β βββ data/ # Custom data (e.g. JSON, metadata, constants)
|
|
74
|
+
β β βββ utils/ # Helper utilities
|
|
75
|
+
β βββ routes/ # SvelteKit pages (+page.svelte, +server.js)
|
|
76
|
+
β βββ service-worker.js # Custom PWA service worker
|
|
77
|
+
βββ static/ # Public assets served at site root
|
|
78
|
+
β βββ disableSw.js # Service worker bypass (via ?nosw param)
|
|
79
|
+
β βββ manifest.json # PWA metadata
|
|
80
|
+
β βββ robots.txt # SEO: allow/disallow crawlers
|
|
81
|
+
β βββ sitemap.xml # SEO: full site map
|
|
82
|
+
βββ tests/
|
|
83
|
+
β βββ e2e/ # Playwright end-to-end tests
|
|
84
|
+
β βββ internal/ # Internal audit/test helpers
|
|
85
|
+
β β βββ auditCoverage.test.js # Warns about untested source modules
|
|
86
|
+
β βββ unit/ # Vitest unit tests
|
|
87
|
+
βββ _redirects # Netlify redirect rules
|
|
88
|
+
βββ package.json # Project manifest (scripts, deps, etc.)
|
|
82
89
|
```
|
|
83
90
|
|
|
84
91
|
|
|
@@ -307,7 +314,7 @@ To use:
|
|
|
307
314
|
https://netwk.pro/?nosw
|
|
308
315
|
```
|
|
309
316
|
|
|
310
|
-
> π‘ `disableSw.js` is loaded from the static directory
|
|
317
|
+
> π‘ `disableSw.js` is loaded via a `<script>` tag in `app.html` from the `static` directory. This ensures the `__DISABLE_SW__` flag is set before any service worker logic runs.
|
|
311
318
|
|
|
312
319
|
|
|
313
320
|
|
|
@@ -415,8 +422,14 @@ npm run test:coverage # Collect code coverage reports
|
|
|
415
422
|
npm run test:e2e # Runs Playwright E2E tests (with one retry on failure)
|
|
416
423
|
```
|
|
417
424
|
|
|
425
|
+
<!-- markdownlint-disable MD028 -->
|
|
426
|
+
|
|
427
|
+
> The unit test suite includes a coverage audit (`auditCoverage.test.js`) that warns when source files in `src/` or `scripts/` do not have corresponding unit tests. This helps track test completeness without failing CI.
|
|
428
|
+
|
|
418
429
|
> Playwright will retry failed tests once `(--retries=1)` to reduce false negatives from transient flakiness (network, render delay, etc.).
|
|
419
430
|
|
|
431
|
+
<!-- markdownlint-enable MD028 -->
|
|
432
|
+
|
|
420
433
|
Audit your app using Lighthouse:
|
|
421
434
|
|
|
422
435
|
```bash
|
|
@@ -550,14 +563,14 @@ The following CLI commands are available via `npm run <script>` or `pnpm run <sc
|
|
|
550
563
|
|
|
551
564
|
### β
Pre-check / Sync
|
|
552
565
|
|
|
553
|
-
| Script | Description
|
|
554
|
-
| ------------- |
|
|
555
|
-
| `prepare` | Run SvelteKit sync
|
|
556
|
-
| `check` | Run SvelteKit sync and type check with `svelte-check`
|
|
557
|
-
| `check:watch` | Watch mode for type checks
|
|
558
|
-
| `check:node` | Validate Node & npm versions match package.json `engines`
|
|
559
|
-
| `checkout` | Full local validation: check versions, test, lint, typecheck |
|
|
560
|
-
| `verify` | Alias for `checkout`
|
|
566
|
+
| Script | Description |
|
|
567
|
+
| ------------- | ----------------------------------------------------------------------------------- |
|
|
568
|
+
| `prepare` | Run SvelteKit sync |
|
|
569
|
+
| `check` | Run SvelteKit sync and type check with `svelte-check` |
|
|
570
|
+
| `check:watch` | Watch mode for type checks |
|
|
571
|
+
| `check:node` | Validate Node & npm versions match package.json `engines` |
|
|
572
|
+
| `checkout` | Full local validation: check versions, test (incl. audit coverage), lint, typecheck |
|
|
573
|
+
| `verify` | Alias for `checkout` |
|
|
561
574
|
|
|
562
575
|
|
|
563
576
|
|
|
@@ -577,15 +590,15 @@ The following CLI commands are available via `npm run <script>` or `pnpm run <sc
|
|
|
577
590
|
|
|
578
591
|
<!-- markdownlint-enable MD024 -->
|
|
579
592
|
|
|
580
|
-
| Script | Description
|
|
581
|
-
| --------------- |
|
|
582
|
-
| `test` | Alias for `test:all`
|
|
583
|
-
| `test:all` | Run both client and server test suites
|
|
584
|
-
| `test:client` | Run client tests with Vitest
|
|
585
|
-
| `test:server` | Run server-side tests with Vitest
|
|
586
|
-
| `test:watch` | Watch mode for client tests
|
|
587
|
-
| `test:coverage` | Collect coverage from both client and server
|
|
588
|
-
| `test:e2e` | Runs E2E tests with up to 1 automatic retry on failure
|
|
593
|
+
| Script | Description |
|
|
594
|
+
| --------------- | ------------------------------------------------------------- |
|
|
595
|
+
| `test` | Alias for `test:all` |
|
|
596
|
+
| `test:all` | Run both client and server test suites (incl. audit coverage) |
|
|
597
|
+
| `test:client` | Run client tests with Vitest |
|
|
598
|
+
| `test:server` | Run server-side tests with Vitest |
|
|
599
|
+
| `test:watch` | Watch mode for client tests |
|
|
600
|
+
| `test:coverage` | Collect coverage from both client and server |
|
|
601
|
+
| `test:e2e` | Runs E2E tests with up to 1 automatic retry on failure |
|
|
589
602
|
|
|
590
603
|
|
|
591
604
|
|
|
@@ -615,11 +628,11 @@ The following CLI commands are available via `npm run <script>` or `pnpm run <sc
|
|
|
615
628
|
|
|
616
629
|
### π Audits / Validation
|
|
617
630
|
|
|
618
|
-
| Script
|
|
619
|
-
|
|
|
620
|
-
| `audit:
|
|
621
|
-
| `head:flatten`
|
|
622
|
-
| `head:validate`
|
|
631
|
+
| Script | Description |
|
|
632
|
+
| ---------------- | ---------------------------------------------------- |
|
|
633
|
+
| `audit:coverage` | Warn about untested modules in `src/` and `scripts/` |
|
|
634
|
+
| `head:flatten` | Flatten headers for Netlify |
|
|
635
|
+
| `head:validate` | Validate headers file against project config |
|
|
623
636
|
|
|
624
637
|
|
|
625
638
|
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"sideEffects": [
|
|
5
5
|
"./.netlify/shims.js"
|
|
6
6
|
],
|
|
7
|
-
"version": "1.
|
|
7
|
+
"version": "1.9.0",
|
|
8
8
|
"description": "Locking Down Networks, Unlocking Confidence | Security, Networking, Privacy β Network Pro Strategies",
|
|
9
9
|
"keywords": [
|
|
10
10
|
"advisory",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
|
50
50
|
"check:env": "node scripts/checkEnv.js",
|
|
51
51
|
"check:node": "node scripts/checkNode.js",
|
|
52
|
-
"checkout": "npm run check:node && npm run test:all && npm run lint:all && npm run check
|
|
52
|
+
"checkout": "npm run check:node && npm run test:all && npm run lint:all && npm run check",
|
|
53
53
|
"verify": "npm run checkout",
|
|
54
54
|
"delete": "rm -rf build .svelte-kit node_modules package-lock.json",
|
|
55
55
|
"clean": "npm run delete && npm cache clean --force && npm install",
|
|
@@ -71,13 +71,13 @@
|
|
|
71
71
|
"format:fix": "prettier --write .",
|
|
72
72
|
"lhci": "lhci",
|
|
73
73
|
"lhci:run": "lhci autorun --config=.lighthouserc.cjs",
|
|
74
|
-
"audit:
|
|
74
|
+
"audit:coverage": "vitest run tests/internal/auditCoverage.test.js",
|
|
75
75
|
"head:flatten": "node scripts/flattenHeaders.js",
|
|
76
76
|
"head:validate": "node scripts/validateHeaders.js",
|
|
77
77
|
"postinstall": "npm run check:node"
|
|
78
78
|
},
|
|
79
79
|
"dependencies": {
|
|
80
|
-
"
|
|
80
|
+
"dompurify": "^3.2.6",
|
|
81
81
|
"posthog-js": "^1.249.0",
|
|
82
82
|
"semver": "^7.7.2",
|
|
83
83
|
"svelte": "5.33.11"
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/* ==========================================================================
|
|
4
|
+
scripts/generateTest.js
|
|
5
|
+
|
|
6
|
+
Copyright Β© 2025 Network Pro Strategies (Network Proβ’)
|
|
7
|
+
SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
|
|
8
|
+
This file is part of Network Pro.
|
|
9
|
+
========================================================================== */
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @file generateTest.js
|
|
13
|
+
* @description Auto-generates a *.test.js scaffold for utilities and
|
|
14
|
+
* components.
|
|
15
|
+
* @module scripts
|
|
16
|
+
* @author SunDevil311
|
|
17
|
+
* @updated 2025-06-01
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from "fs";
|
|
21
|
+
import path from "path";
|
|
22
|
+
|
|
23
|
+
const [, , targetFile] = process.argv;
|
|
24
|
+
|
|
25
|
+
if (!targetFile) {
|
|
26
|
+
console.error("Usage: node generateTest.js <path/to/yourFile.js>");
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const absolutePath = path.resolve(targetFile);
|
|
31
|
+
const parsed = path.parse(absolutePath);
|
|
32
|
+
|
|
33
|
+
const testFileName = `${parsed.name}.test.js`;
|
|
34
|
+
const testFilePath = path.join(
|
|
35
|
+
parsed.dir.replace("src", "tests/unit"),
|
|
36
|
+
testFileName,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Example scaffold content
|
|
40
|
+
const scaffold = `/**
|
|
41
|
+
* Unit tests for ${parsed.base}
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { describe, it, expect } from "vitest";
|
|
45
|
+
import * as Module from "${path.relative(path.dirname(testFilePath), absolutePath).replace(/\\/g, "/")}";
|
|
46
|
+
|
|
47
|
+
describe("${parsed.name}", () => {
|
|
48
|
+
it("should have tests", () => {
|
|
49
|
+
expect(true).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
fs.mkdirSync(path.dirname(testFilePath), { recursive: true });
|
|
55
|
+
|
|
56
|
+
if (fs.existsSync(testFilePath)) {
|
|
57
|
+
console.warn(`Test file already exists at: ${testFilePath}`);
|
|
58
|
+
} else {
|
|
59
|
+
fs.writeFileSync(testFilePath, scaffold);
|
|
60
|
+
console.log(`β
Test scaffold created: ${testFilePath}`);
|
|
61
|
+
}
|
|
@@ -7,8 +7,11 @@ This file is part of Network Pro.
|
|
|
7
7
|
========================================================================== -->
|
|
8
8
|
|
|
9
9
|
<script>
|
|
10
|
+
/* at-html is sanitized by DOMPurify */
|
|
10
11
|
/* eslint-disable svelte/no-at-html-tags */
|
|
11
12
|
|
|
13
|
+
import { onMount } from "svelte";
|
|
14
|
+
import { sanitizeHtml } from "$lib/utils/purify.js";
|
|
12
15
|
import FossFeatures from "$lib/components/foss/FossFeatures.svelte";
|
|
13
16
|
// Import directly from $lib by way of image utility
|
|
14
17
|
import { obtainiumPng, obtainiumWbp } from "$lib";
|
|
@@ -26,30 +29,9 @@ This file is part of Network Pro.
|
|
|
26
29
|
/** @type {"lazy"} */
|
|
27
30
|
const loading = "lazy";
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
* title: string,
|
|
33
|
-
* images: {
|
|
34
|
-
* webp: string,
|
|
35
|
-
* png: string
|
|
36
|
-
* },
|
|
37
|
-
* imgAlt: string,
|
|
38
|
-
* headline: string,
|
|
39
|
-
* headlineDescription: string,
|
|
40
|
-
* detailsDescription: string,
|
|
41
|
-
* features: any[],
|
|
42
|
-
* notes: string[],
|
|
43
|
-
* links: Array<{
|
|
44
|
-
* label?: string,
|
|
45
|
-
* href?: string,
|
|
46
|
-
* imgAlt?: string,
|
|
47
|
-
* downloadText?: string,
|
|
48
|
-
* downloadHref?: string,
|
|
49
|
-
* hideLabels?: boolean
|
|
50
|
-
* }>
|
|
51
|
-
* }}
|
|
52
|
-
*/
|
|
32
|
+
/// <reference path="$lib/types/fossTypes.js" />
|
|
33
|
+
|
|
34
|
+
/** @type {FossItem} */
|
|
53
35
|
export let fossItem;
|
|
54
36
|
|
|
55
37
|
/**
|
|
@@ -58,6 +40,18 @@ This file is part of Network Pro.
|
|
|
58
40
|
* @type {boolean}
|
|
59
41
|
*/
|
|
60
42
|
export let isFirst = false;
|
|
43
|
+
|
|
44
|
+
let safeHeadlineDescription = "";
|
|
45
|
+
let safeDetailsDescription = "";
|
|
46
|
+
/** @type {string[]} */
|
|
47
|
+
let safeNotes = [];
|
|
48
|
+
|
|
49
|
+
// Sanitize everything on mount
|
|
50
|
+
onMount(async () => {
|
|
51
|
+
safeHeadlineDescription = await sanitizeHtml(fossItem.headlineDescription);
|
|
52
|
+
safeDetailsDescription = await sanitizeHtml(fossItem.detailsDescription);
|
|
53
|
+
safeNotes = await Promise.all((fossItem.notes ?? []).map(sanitizeHtml));
|
|
54
|
+
});
|
|
61
55
|
</script>
|
|
62
56
|
|
|
63
57
|
<!-- BEGIN FOSS ITEMS -->
|
|
@@ -88,17 +82,21 @@ This file is part of Network Pro.
|
|
|
88
82
|
|
|
89
83
|
<h3>{fossItem.headline}</h3>
|
|
90
84
|
|
|
91
|
-
<!--
|
|
92
|
-
|
|
85
|
+
<!-- Sanitized input from DOMPurify -->
|
|
86
|
+
<div class="headline-description">
|
|
87
|
+
{@html safeHeadlineDescription}
|
|
88
|
+
</div>
|
|
93
89
|
|
|
94
90
|
<FossFeatures features={fossItem.features} />
|
|
95
91
|
|
|
96
|
-
<!--
|
|
97
|
-
|
|
92
|
+
<!-- Sanitized input from DOMPurify -->
|
|
93
|
+
<div class="details-description">
|
|
94
|
+
{@html safeDetailsDescription}
|
|
95
|
+
</div>
|
|
98
96
|
|
|
99
|
-
{#each
|
|
97
|
+
{#each safeNotes as note}
|
|
100
98
|
<blockquote class="bquote">
|
|
101
|
-
<!--
|
|
99
|
+
<!-- Sanitized input from DOMPurify -->
|
|
102
100
|
{@html note}
|
|
103
101
|
</blockquote>
|
|
104
102
|
{/each}
|
|
@@ -8,6 +8,16 @@ This file is part of Network Pro.
|
|
|
8
8
|
|
|
9
9
|
<script>
|
|
10
10
|
import { pgpContact, pgpSupport, vcfSrc } from "$lib";
|
|
11
|
+
import { base } from "$app/paths";
|
|
12
|
+
|
|
13
|
+
// Log the base path to verify its value
|
|
14
|
+
//console.log("Base path:", base);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* URL to the Contact Form route, using the base path
|
|
18
|
+
* @type {string}
|
|
19
|
+
*/
|
|
20
|
+
const contactLink = `${base}/contact`;
|
|
11
21
|
|
|
12
22
|
/**
|
|
13
23
|
* Security attribute for external links
|
|
@@ -196,7 +206,7 @@ This file is part of Network Pro.
|
|
|
196
206
|
<div class="spacer"></div>
|
|
197
207
|
|
|
198
208
|
<p>
|
|
199
|
-
<a href=
|
|
209
|
+
<a href={contactLink} target="_self">Let's connect</a>
|
|
200
210
|
to discuss how we can help secure and strengthen your business today.
|
|
201
211
|
</p>
|
|
202
212
|
|
|
@@ -236,8 +246,7 @@ This file is part of Network Pro.
|
|
|
236
246
|
type="application/pgp-keys"
|
|
237
247
|
download
|
|
238
248
|
target={tgtBlank}
|
|
239
|
-
>asc
|
|
240
|
-
<span class="fas fa-file-arrow-down"></span></a
|
|
249
|
+
>asc <span class="fas fa-file-arrow-down"></span></a
|
|
241
250
|
></strong>
|
|
242
251
|
</p>
|
|
243
252
|
<p
|
|
@@ -262,7 +271,7 @@ This file is part of Network Pro.
|
|
|
262
271
|
type="application/pgp-keys"
|
|
263
272
|
download
|
|
264
273
|
target={tgtBlank}
|
|
265
|
-
>asc
|
|
274
|
+
>asc <span class="fas fa-file-arrow-down"></span></a
|
|
266
275
|
></strong>
|
|
267
276
|
</p>
|
|
268
277
|
<p
|
|
@@ -300,7 +309,8 @@ This file is part of Network Pro.
|
|
|
300
309
|
type="text/vcard"
|
|
301
310
|
download
|
|
302
311
|
target={tgtBlank}>
|
|
303
|
-
<strong
|
|
312
|
+
<strong
|
|
313
|
+
>vcf <span class="fas fa-file-arrow-down"></span></strong>
|
|
304
314
|
</a>
|
|
305
315
|
</p>
|
|
306
316
|
</td>
|
|
@@ -218,7 +218,7 @@ This file is part of Network Pro.
|
|
|
218
218
|
</ul>
|
|
219
219
|
{:else if link.id === "cc-by"}
|
|
220
220
|
<p class={constants.classSmall}>
|
|
221
|
-
|
|
221
|
+
Download:
|
|
222
222
|
<a
|
|
223
223
|
href="/assets/license/CC-BY-4.0.html"
|
|
224
224
|
download
|
|
@@ -321,7 +321,7 @@ This file is part of Network Pro.
|
|
|
321
321
|
</code>
|
|
322
322
|
{:else if link.id === "gnu-gpl"}
|
|
323
323
|
<p class={constants.classSmall}>
|
|
324
|
-
|
|
324
|
+
Download:
|
|
325
325
|
<a
|
|
326
326
|
href="/assets/license/COPYING.html"
|
|
327
327
|
download
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} FossLink
|
|
3
|
+
* @property {string} [label]
|
|
4
|
+
* @property {string} [href]
|
|
5
|
+
* @property {string} [imgAlt]
|
|
6
|
+
* @property {string} [downloadText]
|
|
7
|
+
* @property {string} [downloadHref]
|
|
8
|
+
* @property {boolean} [hideLabels]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} FossItem
|
|
13
|
+
* @property {string} id
|
|
14
|
+
* @property {string} title
|
|
15
|
+
* @property {{ webp: string, png: string }} images
|
|
16
|
+
* @property {string} imgAlt
|
|
17
|
+
* @property {string} headline
|
|
18
|
+
* @property {string} headlineDescription
|
|
19
|
+
* @property {string} detailsDescription
|
|
20
|
+
* @property {Array<any>} features
|
|
21
|
+
* @property {Array<string>} notes
|
|
22
|
+
* @property {Array<FossLink>} links
|
|
23
|
+
*/
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
src/lib/utils/purify.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 purify.js
|
|
11
|
+
* @description Universal DOMPurify instance for SSR + client with safe build support.
|
|
12
|
+
* Secures untrusted HTML before injecting it into the DOM.
|
|
13
|
+
* @module src/lib/utils
|
|
14
|
+
* @author SunDevil311
|
|
15
|
+
* @updated 2025-06-01
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import createDOMPurify from "dompurify";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {ReturnType<import('dompurify').default>} DOMPurifyInstance
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** @type {import('dompurify').DOMPurify | null} */
|
|
25
|
+
let DOMPurifyInstance = null;
|
|
26
|
+
|
|
27
|
+
/** @type {import('jsdom').JSDOM['window'] | null} */
|
|
28
|
+
let jsdomWindow = null;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* SSR-safe + Vite-compatible init of DOMPurify.
|
|
32
|
+
*
|
|
33
|
+
* Caches DOMPurify across multiple calls to improve performance in tests or SSR environments.
|
|
34
|
+
*
|
|
35
|
+
* @returns {Promise<DOMPurifyInstance>}
|
|
36
|
+
*/
|
|
37
|
+
export async function getDOMPurify() {
|
|
38
|
+
if (DOMPurifyInstance) return DOMPurifyInstance;
|
|
39
|
+
|
|
40
|
+
if (typeof window !== "undefined") {
|
|
41
|
+
// β
Client-side: use native window
|
|
42
|
+
DOMPurifyInstance = createDOMPurify(window);
|
|
43
|
+
} else {
|
|
44
|
+
// β
SSR: dynamically import jsdom to avoid bundling
|
|
45
|
+
const { JSDOM } = await import("jsdom");
|
|
46
|
+
jsdomWindow = jsdomWindow || new JSDOM("").window;
|
|
47
|
+
DOMPurifyInstance = createDOMPurify(jsdomWindow);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return DOMPurifyInstance;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Sanitizes HTML content to prevent XSS.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} dirtyHtml
|
|
57
|
+
* @returns {Promise<string>} - A Promise resolving to sanitized HTML
|
|
58
|
+
*/
|
|
59
|
+
export async function sanitizeHtml(dirtyHtml) {
|
|
60
|
+
const DOMPurify = await getDOMPurify();
|
|
61
|
+
return DOMPurify.sanitize(dirtyHtml, {
|
|
62
|
+
USE_PROFILES: { html: true },
|
|
63
|
+
ALLOW_DATA_ATTR: false,
|
|
64
|
+
ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto):)/i,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Optional helper to reset cache (for test isolation).
|
|
70
|
+
*/
|
|
71
|
+
export function resetDOMPurifyCache() {
|
|
72
|
+
DOMPurifyInstance = null;
|
|
73
|
+
jsdomWindow = null;
|
|
74
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
tests/internal/auditCoverage.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
|
+
* @file auditCoverage.test.js
|
|
11
|
+
* @description Scans all .js files in src/ and scripts/ for matching unit test
|
|
12
|
+
* @module tests/internal
|
|
13
|
+
* @author SunDevil311
|
|
14
|
+
* @updated 2025-06-01
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from "fs";
|
|
18
|
+
import path from "path";
|
|
19
|
+
import { describe, expect, it } from "vitest";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Recursively get all .js files in a directory
|
|
23
|
+
* @param {string} dir
|
|
24
|
+
* @param {object} [opts]
|
|
25
|
+
* @param {boolean} [opts.includeTests=false]
|
|
26
|
+
* @returns {string[]}
|
|
27
|
+
*/
|
|
28
|
+
function getAllJsFiles(dir, { includeTests = false } = {}) {
|
|
29
|
+
let results = [];
|
|
30
|
+
const list = fs.readdirSync(dir);
|
|
31
|
+
for (const file of list) {
|
|
32
|
+
const fullPath = path.join(dir, file);
|
|
33
|
+
const stat = fs.statSync(fullPath);
|
|
34
|
+
if (stat.isDirectory()) {
|
|
35
|
+
results = results.concat(getAllJsFiles(fullPath, { includeTests }));
|
|
36
|
+
} else if (file.endsWith(".js")) {
|
|
37
|
+
if (
|
|
38
|
+
!includeTests &&
|
|
39
|
+
(file.endsWith(".test.js") || file.endsWith(".spec.js"))
|
|
40
|
+
) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
results.push(fullPath);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return results;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe("auditCoverage", () => {
|
|
50
|
+
it("should have corresponding test files for all JS modules", () => {
|
|
51
|
+
const allowList = new Set([
|
|
52
|
+
"checkNode.js",
|
|
53
|
+
"auditScripts.js",
|
|
54
|
+
"vite.config.js",
|
|
55
|
+
"svelte.config.js",
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
const srcFiles = getAllJsFiles(path.resolve("src"));
|
|
59
|
+
const scriptsFiles = getAllJsFiles(path.resolve("scripts"));
|
|
60
|
+
const allFiles = [...srcFiles, ...scriptsFiles].map((f) =>
|
|
61
|
+
path
|
|
62
|
+
.relative(process.cwd(), f)
|
|
63
|
+
.replace(/\\/g, "/") // Normalize Windows slashes
|
|
64
|
+
.replace(/^src\//, "")
|
|
65
|
+
.replace(/^scripts\//, "")
|
|
66
|
+
.replace(/\.js$/, ""),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const testFiles = getAllJsFiles(path.resolve("tests/unit"), {
|
|
70
|
+
includeTests: true,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const testFilesNormalized = testFiles.map((f) =>
|
|
74
|
+
path
|
|
75
|
+
.relative(path.resolve("tests/unit"), f)
|
|
76
|
+
.replace(/\\/g, "/")
|
|
77
|
+
.replace(/\.test\.js$|\.spec\.js$/, ""),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const testedNames = new Set(testFilesNormalized);
|
|
81
|
+
|
|
82
|
+
const untested = allFiles.filter((file) => {
|
|
83
|
+
if (file.startsWith("tests/")) return false;
|
|
84
|
+
if ([...allowList].some((allowed) => file.endsWith(allowed)))
|
|
85
|
+
return false;
|
|
86
|
+
return !testedNames.has(file);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (untested.length > 0) {
|
|
90
|
+
console.warn(
|
|
91
|
+
"\n[WARN] The following JS modules do not have corresponding tests:\n" +
|
|
92
|
+
untested.map((f) => ` - ${f}`).join("\n"),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// β
Always pass β warn only
|
|
97
|
+
expect(true).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
tests/unit/lib/utils/purify.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
|
+
* @file purify.test.js
|
|
11
|
+
* @description Unit test for src/lib/utils/purify.js
|
|
12
|
+
* @module tests/unit/lib/util
|
|
13
|
+
* @author SunDevil311
|
|
14
|
+
* @updated 2025-06-01
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, expect, it } from "vitest";
|
|
18
|
+
import { sanitizeHtml } from "../../../../src/lib/utils/purify.js";
|
|
19
|
+
|
|
20
|
+
describe("sanitizeHtml", () => {
|
|
21
|
+
it("removes dangerous tags like <script>", async () => {
|
|
22
|
+
const dirty = `<div>Hello <script>alert("XSS")</script> world!</div>`;
|
|
23
|
+
const clean = await sanitizeHtml(dirty);
|
|
24
|
+
expect(clean).toBe("<div>Hello world!</div>");
|
|
25
|
+
}); // timeout in ms
|
|
26
|
+
|
|
27
|
+
it("preserves safe markup like <strong>", async () => {
|
|
28
|
+
const dirty = `<p>This is <strong>important</strong>.</p>`;
|
|
29
|
+
const clean = await sanitizeHtml(dirty);
|
|
30
|
+
expect(clean).toBe("<p>This is <strong>important</strong>.</p>");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("removes dangerous attributes like onerror", async () => {
|
|
34
|
+
const dirty = `<img src="x" onerror="alert(1)">`;
|
|
35
|
+
const clean = await sanitizeHtml(dirty);
|
|
36
|
+
expect(clean).toBe('<img src="x">');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("keeps valid external links", async () => {
|
|
40
|
+
const dirty = `<a href="https://example.com">Click</a>`;
|
|
41
|
+
const clean = await sanitizeHtml(dirty);
|
|
42
|
+
expect(clean).toBe('<a href="https://example.com">Click</a>');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("blocks javascript: URLs", async () => {
|
|
46
|
+
const dirty = `<a href="javascript:alert('XSS')">bad</a>`;
|
|
47
|
+
const clean = await sanitizeHtml(dirty);
|
|
48
|
+
expect(clean).toBe("<a>bad</a>");
|
|
49
|
+
});
|
|
50
|
+
});
|
package/vitest.config.client.js
CHANGED
|
@@ -26,10 +26,14 @@ export default defineConfig({
|
|
|
26
26
|
name: "client",
|
|
27
27
|
environment: "jsdom",
|
|
28
28
|
clearMocks: true,
|
|
29
|
-
include: [
|
|
29
|
+
include: [
|
|
30
|
+
"tests/unit/**/*.test.{js,mjs,svelte}",
|
|
31
|
+
"tests/internal/**/*.test.{js,mjs,svelte}",
|
|
32
|
+
],
|
|
30
33
|
exclude: [],
|
|
31
34
|
setupFiles: ["./vitest-setup-client.js"],
|
|
32
35
|
reporters: ["default", "json"],
|
|
36
|
+
testTimeout: 10000,
|
|
33
37
|
outputFile: {
|
|
34
38
|
json: "./reports/client/results.json",
|
|
35
39
|
},
|
package/vitest.config.server.js
CHANGED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/* ==========================================================================
|
|
2
|
-
tests/unit/auditScripts.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
|
-
* Unit test for scripts/auditScripts.js
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import fs from "fs";
|
|
14
|
-
import path from "path";
|
|
15
|
-
import { describe, expect, it } from "vitest";
|
|
16
|
-
|
|
17
|
-
describe("auditScripts.js", () => {
|
|
18
|
-
it("should identify untested scripts correctly", () => {
|
|
19
|
-
const scriptsDir = path.resolve("./scripts");
|
|
20
|
-
const testsDir = path.resolve("./tests");
|
|
21
|
-
|
|
22
|
-
const allowList = new Set(["checkNode.js", "auditScripts.js"]);
|
|
23
|
-
|
|
24
|
-
const scriptFiles = fs
|
|
25
|
-
.readdirSync(scriptsDir)
|
|
26
|
-
.filter((file) => file.endsWith(".js"));
|
|
27
|
-
|
|
28
|
-
const testFiles = fs
|
|
29
|
-
.readdirSync(testsDir)
|
|
30
|
-
.filter((file) => file.endsWith(".test.js") || file.endsWith(".spec.js"));
|
|
31
|
-
|
|
32
|
-
const testedModules = new Set(
|
|
33
|
-
testFiles.map((f) => f.replace(/\.test\.js$|\.spec\.js$/, "")),
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
const untested = scriptFiles.filter((file) => {
|
|
37
|
-
const base = file.replace(/\.js$/, "");
|
|
38
|
-
return !allowList.has(file) && !testedModules.has(base);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
expect(untested).not.toContain("auditScripts.js");
|
|
42
|
-
});
|
|
43
|
-
});
|