@networkpro/web 1.14.3 → 1.15.1
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/.node-version +1 -1
- package/.nvmrc +1 -1
- package/CHANGELOG.md +87 -2
- package/README.md +6 -4
- package/_redirects +4 -2
- package/netlify.toml +0 -5
- package/package.json +14 -15
- package/scripts/testRedirects.js +84 -0
- package/src/app.html +1 -1
- package/src/hooks.server.js +20 -1
- package/src/lib/components/RedirectPage.svelte +2 -34
- package/src/lib/components/layout/HeaderDefault.svelte +8 -3
- package/src/lib/components/layout/HeaderHome.svelte +8 -3
- package/src/lib/pages/AboutContent.svelte +1 -1
- package/src/lib/pages/LicenseContent.svelte +1 -2
- package/src/lib/pages/PrivacyDashboard.svelte +0 -1
- package/src/lib/styles/css/default.css +22 -0
- package/src/lib/styles/global.min.css +1 -1
- package/src/lib/utils/redirect.js +52 -0
- package/src/lib/utils/utm.js +1 -0
- package/src/routes/consultation/+page.svelte +16 -2
- package/src/routes/contact/+page.svelte +2 -4
- package/src/routes/links/+page.svelte +2 -4
- package/src/routes/posts/+page.svelte +2 -4
- package/src/routes/privacy-rights/+page.svelte +2 -4
- package/static/sitemap.xml +4 -4
- package/tests/unit/{unregisterServiceWorker.test.js → client/lib/unregisterServiceWorker.test.js} +2 -2
- package/tests/unit/client/lib/utils/redirect.test.js +80 -0
- package/tests/unit/{utm.test.js → client/lib/utils/utm.test.js} +1 -1
- package/tests/unit/{routes → client/routes}/page.svelte.test.js +2 -2
- package/tests/unit/{checkEnv.test.js → server/checkEnv.test.js} +2 -2
- package/tests/unit/{checkVersions.test.js → server/checkVersions.test.js} +2 -2
- package/tests/{internal → unit/server/internal}/auditCoverage.test.js +13 -6
- package/tests/unit/{lib → server/lib}/utils/purify.test.js +2 -2
- package/vitest.config.client.js +1 -4
- package/vitest.config.server.js +1 -1
- package/netlify/edge-functions/csp-report.js +0 -160
- package/tests/unit/csp-report.test.js +0 -81
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
src/lib/utils/redirect.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 utm.js
|
|
11
|
+
* @description Determines if the current browser is Firefox and skips redirect
|
|
12
|
+
* visual if true.
|
|
13
|
+
*
|
|
14
|
+
* @module src/lib/utils/
|
|
15
|
+
* @author SunDevil311
|
|
16
|
+
* @updated 2025-07-01
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Checks whether the current browser is Firefox.
|
|
21
|
+
* @returns {boolean} True if the browser is Firefox.
|
|
22
|
+
*/
|
|
23
|
+
function isFirefox() {
|
|
24
|
+
return (
|
|
25
|
+
typeof navigator !== 'undefined' &&
|
|
26
|
+
navigator.userAgent.toLowerCase().includes('firefox')
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Redirects to a URL, immediately in Firefox (to avoid popup heuristics),
|
|
32
|
+
* and with a visual delay in all other browsers.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} to - Destination URL
|
|
35
|
+
* @param {number} delay - Delay in seconds (ignored for Firefox)
|
|
36
|
+
*/
|
|
37
|
+
export function redirectWithBrowserAwareness(to, delay = 2) {
|
|
38
|
+
if (!to) {
|
|
39
|
+
console.warn('⛔ No redirect target provided');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (isFirefox()) {
|
|
44
|
+
console.log('🦊 Firefox detected — redirecting immediately');
|
|
45
|
+
window.location.replace(to);
|
|
46
|
+
} else {
|
|
47
|
+
console.log(`✅ Delayed redirect to: ${to} after ${delay}s`);
|
|
48
|
+
setTimeout(() => {
|
|
49
|
+
window.location.replace(to);
|
|
50
|
+
}, delay * 1000);
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/lib/utils/utm.js
CHANGED
|
@@ -34,6 +34,7 @@ export function appendUTM(url) {
|
|
|
34
34
|
else if (pathname.startsWith('/links')) campaign = 'links';
|
|
35
35
|
else if (pathname.startsWith('/posts')) campaign = 'posts';
|
|
36
36
|
else if (pathname.startsWith('/privacy-rights')) campaign = 'prights';
|
|
37
|
+
else if (pathname.startsWith('/consultation')) campaign = 'consult';
|
|
37
38
|
// add more if needed
|
|
38
39
|
|
|
39
40
|
const utmParams = new URLSearchParams({
|
|
@@ -9,6 +9,10 @@ This file is part of Network Pro.
|
|
|
9
9
|
<script>
|
|
10
10
|
import RedirectPage from '$lib/components/RedirectPage.svelte';
|
|
11
11
|
import { appendUTM } from '$lib/utils/utm.js';
|
|
12
|
+
import { getUTMParams } from '$lib/utils/getUTMParams.js';
|
|
13
|
+
import { trackingEnabled } from '$lib/stores/trackingPreferences';
|
|
14
|
+
import posthog from 'posthog-js';
|
|
15
|
+
import { get } from 'svelte/store';
|
|
12
16
|
import { onMount } from 'svelte';
|
|
13
17
|
import { browser } from '$app/environment';
|
|
14
18
|
|
|
@@ -21,10 +25,20 @@ This file is part of Network Pro.
|
|
|
21
25
|
onMount(() => {
|
|
22
26
|
if (!browser) return;
|
|
23
27
|
|
|
24
|
-
|
|
28
|
+
const url = appendUTM(
|
|
25
29
|
'https://cloud.neteng.pro/index.php/apps/appointments/pub/8clCqQrt3AtGbNrr/form',
|
|
26
30
|
);
|
|
27
|
-
|
|
31
|
+
|
|
32
|
+
if (get(trackingEnabled)) {
|
|
33
|
+
const utm = getUTMParams(url);
|
|
34
|
+
posthog.capture('redirect_to_consult', {
|
|
35
|
+
target_url: url,
|
|
36
|
+
...utm,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
target = url;
|
|
41
|
+
show = true; // Immediately show RedirectPage
|
|
28
42
|
});
|
|
29
43
|
</script>
|
|
30
44
|
|
package/static/sitemap.xml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<!-- Sitemap last updated on 2025
|
|
2
|
+
<!-- Sitemap last updated on July 12, 2025 -->
|
|
3
3
|
|
|
4
4
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
5
5
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
<loc>https://netwk.pro</loc>
|
|
9
9
|
|
|
10
|
-
<lastmod>2025-
|
|
10
|
+
<lastmod>2025-07-12</lastmod>
|
|
11
11
|
|
|
12
12
|
<changefreq>weekly</changefreq>
|
|
13
13
|
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
|
|
44
44
|
<loc>https://netwk.pro/privacy-dashboard</loc>
|
|
45
45
|
|
|
46
|
-
<lastmod>2025-
|
|
46
|
+
<lastmod>2025-06-30</lastmod>
|
|
47
47
|
|
|
48
48
|
<changefreq>monthly</changefreq>
|
|
49
49
|
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
|
|
56
56
|
<loc>https://netwk.pro/privacy</loc>
|
|
57
57
|
|
|
58
|
-
<lastmod>2025-06-
|
|
58
|
+
<lastmod>2025-06-30</lastmod>
|
|
59
59
|
|
|
60
60
|
<changefreq>monthly</changefreq>
|
|
61
61
|
|
package/tests/unit/{unregisterServiceWorker.test.js → client/lib/unregisterServiceWorker.test.js}
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* ==========================================================================
|
|
2
|
-
tests/unit/unregisterServiceWorker.test.js
|
|
2
|
+
tests/unit/client/lib/unregisterServiceWorker.test.js
|
|
3
3
|
|
|
4
4
|
Copyright © 2025 Network Pro Strategies (Network Pro™)
|
|
5
5
|
SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
|
|
@@ -7,7 +7,7 @@ This file is part of Network Pro.
|
|
|
7
7
|
========================================================================== */
|
|
8
8
|
|
|
9
9
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
10
|
-
import { unregisterServiceWorker } from '
|
|
10
|
+
import { unregisterServiceWorker } from '../../../../src/lib/unregisterServiceWorker.js';
|
|
11
11
|
|
|
12
12
|
describe('unregisterServiceWorker()', () => {
|
|
13
13
|
beforeEach(() => {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
tests/unit/client/lib/utils/redirect.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 redirect.test.js
|
|
11
|
+
* @description Unit test for src/lib/utils/redirect.js
|
|
12
|
+
* @module tests/unit/lib/util/
|
|
13
|
+
* @author SunDevil311
|
|
14
|
+
* @updated 2025-07-01
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { redirectWithBrowserAwareness } from '$lib/utils/redirect.js';
|
|
18
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
19
|
+
|
|
20
|
+
describe('redirectWithBrowserAwareness', () => {
|
|
21
|
+
let originalLocation;
|
|
22
|
+
let originalNavigator;
|
|
23
|
+
|
|
24
|
+
/** @type {{ url: string; delay: number }} */
|
|
25
|
+
const commonMocks = {
|
|
26
|
+
url: 'https://example.com',
|
|
27
|
+
delay: 1,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
// Preserve globals
|
|
32
|
+
originalLocation = window.location;
|
|
33
|
+
originalNavigator = global.navigator;
|
|
34
|
+
|
|
35
|
+
// Stub window.location.replace
|
|
36
|
+
delete window.location;
|
|
37
|
+
window.location = {
|
|
38
|
+
replace: vi.fn(),
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
// Restore globals
|
|
44
|
+
window.location = originalLocation;
|
|
45
|
+
global.navigator = originalNavigator;
|
|
46
|
+
vi.clearAllMocks();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should redirect immediately in Firefox', () => {
|
|
50
|
+
global.navigator = {
|
|
51
|
+
userAgent:
|
|
52
|
+
'Mozilla/5.0 (Windows NT 10.0; rv:99.0) Gecko/20100101 Firefox/99.0',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
redirectWithBrowserAwareness(commonMocks.url, commonMocks.delay);
|
|
56
|
+
|
|
57
|
+
expect(window.location.replace).toHaveBeenCalledWith(commonMocks.url);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should redirect after delay in non-Firefox', () => {
|
|
61
|
+
vi.useFakeTimers();
|
|
62
|
+
|
|
63
|
+
global.navigator = {
|
|
64
|
+
userAgent:
|
|
65
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0 Safari/537.36',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
redirectWithBrowserAwareness(commonMocks.url, commonMocks.delay);
|
|
69
|
+
|
|
70
|
+
expect(window.location.replace).not.toHaveBeenCalled();
|
|
71
|
+
|
|
72
|
+
vi.advanceTimersByTime(commonMocks.delay * 1000);
|
|
73
|
+
|
|
74
|
+
expect(window.location.replace).toHaveBeenCalledWith(commonMocks.url);
|
|
75
|
+
|
|
76
|
+
vi.useRealTimers();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// cspell:ignore khtml
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* ==========================================================================
|
|
2
|
-
tests/unit/routes/page.svelte.test.js
|
|
2
|
+
tests/unit/client/routes/page.svelte.test.js
|
|
3
3
|
|
|
4
4
|
Copyright © 2025 Network Pro Strategies (Network Pro™)
|
|
5
5
|
SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
|
|
@@ -9,7 +9,7 @@ This file is part of Network Pro.
|
|
|
9
9
|
import '@testing-library/jest-dom/vitest';
|
|
10
10
|
import { render, screen } from '@testing-library/svelte';
|
|
11
11
|
import { describe, expect, test } from 'vitest';
|
|
12
|
-
import Page from '
|
|
12
|
+
import Page from '../../../../src/routes/+page.svelte';
|
|
13
13
|
|
|
14
14
|
describe('/+page.svelte', () => {
|
|
15
15
|
test('should render the home page section', () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* ==========================================================================
|
|
2
|
-
tests/unit/checkEnv.test.js
|
|
2
|
+
tests/unit/server/checkEnv.test.js
|
|
3
3
|
|
|
4
4
|
Copyright © 2025 Network Pro Strategies (Network Pro™)
|
|
5
5
|
SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
|
|
@@ -11,7 +11,7 @@ This file is part of Network Pro.
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
14
|
-
import { checkEnv } from '
|
|
14
|
+
import { checkEnv } from '../../../scripts/checkEnv.js';
|
|
15
15
|
|
|
16
16
|
describe('checkEnv()', () => {
|
|
17
17
|
const originalEnv = process.env.ENV_MODE;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* ==========================================================================
|
|
2
|
-
tests/unit/checkVersions.test.js
|
|
2
|
+
tests/unit/server/checkVersions.test.js
|
|
3
3
|
|
|
4
4
|
Copyright © 2025 Network Pro Strategies (Network Pro™)
|
|
5
5
|
SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
|
|
@@ -11,7 +11,7 @@ This file is part of Network Pro.
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { describe, expect, it } from 'vitest';
|
|
14
|
-
import { checkVersions } from '
|
|
14
|
+
import { checkVersions } from '../../../scripts/checkVersions.js';
|
|
15
15
|
|
|
16
16
|
describe('checkVersions()', () => {
|
|
17
17
|
it('should match current Node and NPM versions to engine ranges', () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* ==========================================================================
|
|
2
|
-
tests/internal/auditCoverage.test.js
|
|
2
|
+
tests/unit/server/internal/auditCoverage.test.js
|
|
3
3
|
|
|
4
4
|
Copyright © 2025 Network Pro Strategies (Network Pro™)
|
|
5
5
|
SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
|
|
@@ -8,10 +8,11 @@ This file is part of Network Pro.
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* @file auditCoverage.test.js
|
|
11
|
-
* @description Scans all .js files in src/ and scripts/ for matching unit
|
|
11
|
+
* @description Scans all .js files in src/ and scripts/ for matching unit
|
|
12
|
+
* tests
|
|
12
13
|
* @module tests/internal
|
|
13
14
|
* @author SunDevil311
|
|
14
|
-
* @updated 2025-
|
|
15
|
+
* @updated 2025-07-01
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
import fs from 'fs';
|
|
@@ -60,20 +61,26 @@ describe('auditCoverage', () => {
|
|
|
60
61
|
const allFiles = [...srcFiles, ...scriptsFiles].map((f) =>
|
|
61
62
|
path
|
|
62
63
|
.relative(process.cwd(), f)
|
|
63
|
-
.replace(/\\/g, '/')
|
|
64
|
+
.replace(/\\/g, '/')
|
|
64
65
|
.replace(/^src\//, '')
|
|
65
66
|
.replace(/^scripts\//, '')
|
|
66
67
|
.replace(/\.js$/, ''),
|
|
67
68
|
);
|
|
68
69
|
|
|
69
|
-
const
|
|
70
|
+
const clientTests = getAllJsFiles(path.resolve('tests/unit/client'), {
|
|
71
|
+
includeTests: true,
|
|
72
|
+
});
|
|
73
|
+
const serverTests = getAllJsFiles(path.resolve('tests/unit/server'), {
|
|
70
74
|
includeTests: true,
|
|
71
75
|
});
|
|
76
|
+
const testFiles = [...clientTests, ...serverTests];
|
|
72
77
|
|
|
73
78
|
const testFilesNormalized = testFiles.map((f) =>
|
|
74
79
|
path
|
|
75
|
-
.relative(
|
|
80
|
+
.relative(process.cwd(), f)
|
|
76
81
|
.replace(/\\/g, '/')
|
|
82
|
+
.replace(/^tests\/unit\/client\//, '')
|
|
83
|
+
.replace(/^tests\/unit\/server\//, '')
|
|
77
84
|
.replace(/\.test\.js$|\.spec\.js$/, ''),
|
|
78
85
|
);
|
|
79
86
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* ==========================================================================
|
|
2
|
-
tests/unit/lib/utils/purify.test.js
|
|
2
|
+
tests/unit/server/lib/utils/purify.test.js
|
|
3
3
|
|
|
4
4
|
Copyright © 2025 Network Pro Strategies (Network Pro™)
|
|
5
5
|
SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
|
|
@@ -15,7 +15,7 @@ This file is part of Network Pro.
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { describe, expect, it } from 'vitest';
|
|
18
|
-
import { sanitizeHtml } from '
|
|
18
|
+
import { sanitizeHtml } from '../../../../../src/lib/utils/purify.js';
|
|
19
19
|
|
|
20
20
|
describe('sanitizeHtml', () => {
|
|
21
21
|
it('removes dangerous tags like <script>', async () => {
|
package/vitest.config.client.js
CHANGED
|
@@ -26,10 +26,7 @@ export default defineConfig({
|
|
|
26
26
|
name: 'client',
|
|
27
27
|
environment: 'jsdom',
|
|
28
28
|
clearMocks: true,
|
|
29
|
-
include: [
|
|
30
|
-
'tests/unit/**/*.test.{js,mjs,svelte}',
|
|
31
|
-
'tests/internal/**/*.test.{js,mjs,svelte}',
|
|
32
|
-
],
|
|
29
|
+
include: ['tests/unit/client/**/*.test.{js,mjs,svelte}'],
|
|
33
30
|
exclude: [],
|
|
34
31
|
setupFiles: ['./vitest-setup-client.js'],
|
|
35
32
|
reporters: ['default', 'json'],
|
package/vitest.config.server.js
CHANGED
|
@@ -23,7 +23,7 @@ export default defineConfig({
|
|
|
23
23
|
test: {
|
|
24
24
|
name: 'server',
|
|
25
25
|
environment: 'node',
|
|
26
|
-
include: ['tests/unit/**/*.test.{js,mjs}'],
|
|
26
|
+
include: ['tests/unit/server/**/*.test.{js,mjs}'],
|
|
27
27
|
exclude: ['tests/unit/**/*.svelte.test.{js,mjs}'],
|
|
28
28
|
reporters: ['default', 'json'],
|
|
29
29
|
testTimeout: 10000,
|
|
@@ -1,160 +0,0 @@
|
|
|
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 noisy or unactionable reports
|
|
47
|
-
const ignored = [
|
|
48
|
-
violated.startsWith('img-src'),
|
|
49
|
-
blockedUri === '',
|
|
50
|
-
blockedUri === 'eval',
|
|
51
|
-
blockedUri === 'about',
|
|
52
|
-
blockedUri.startsWith('chrome-extension://'),
|
|
53
|
-
blockedUri.startsWith('moz-extension://'),
|
|
54
|
-
!report['source-file'],
|
|
55
|
-
!report['document-uri'],
|
|
56
|
-
].some(Boolean);
|
|
57
|
-
|
|
58
|
-
if (ignored) {
|
|
59
|
-
console.log('[CSP-Edge] Ignored low-value violation:', {
|
|
60
|
-
directive: violated,
|
|
61
|
-
uri: blockedUri,
|
|
62
|
-
});
|
|
63
|
-
return new Response(null, { status: 204 });
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Send alert for high-risk directives
|
|
67
|
-
await sendToNtfy(violated, blockedUri, report);
|
|
68
|
-
|
|
69
|
-
// Log useful violations
|
|
70
|
-
console.log('[CSP-Edge] Violation:', {
|
|
71
|
-
directive: violated,
|
|
72
|
-
uri: blockedUri,
|
|
73
|
-
referrer: report['referrer'],
|
|
74
|
-
source: report['source-file'],
|
|
75
|
-
line: report['line-number'],
|
|
76
|
-
});
|
|
77
|
-
} catch (err) {
|
|
78
|
-
console.warn('[CSP-Edge] Failed to parse CSP report:', err.message);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return new Response(null, { status: 204 });
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const recentViolations = new Map();
|
|
85
|
-
const VIOLATION_TTL_MS = 60_000;
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Sends a high-priority alert to your ntfy topic for high-risk CSP violations.
|
|
89
|
-
* Applies rate-limiting to avoid sending duplicate alerts within 60 seconds.
|
|
90
|
-
*
|
|
91
|
-
* @param {string} violated - The violated CSP directive
|
|
92
|
-
* @param {string} blockedUri - The URI that was blocked
|
|
93
|
-
* @param {Record<string, any>} report - The full CSP report object
|
|
94
|
-
*/
|
|
95
|
-
async function sendToNtfy(violated, blockedUri, report) {
|
|
96
|
-
const highRiskDirectives = [
|
|
97
|
-
'script-src',
|
|
98
|
-
'form-action',
|
|
99
|
-
'frame-ancestors',
|
|
100
|
-
'base-uri',
|
|
101
|
-
];
|
|
102
|
-
|
|
103
|
-
const directiveKey = violated.split(' ')[0]; // strip fallback values or sources
|
|
104
|
-
const isHighRisk = highRiskDirectives.includes(directiveKey);
|
|
105
|
-
console.log(
|
|
106
|
-
`[CSP-Edge] Directive ${directiveKey} is ${isHighRisk ? '' : 'not '}high-risk`,
|
|
107
|
-
);
|
|
108
|
-
if (!isHighRisk) return;
|
|
109
|
-
|
|
110
|
-
const key = `${violated}|${blockedUri}`;
|
|
111
|
-
const now = Date.now();
|
|
112
|
-
|
|
113
|
-
// Skip and log if violation was reported recently
|
|
114
|
-
if (
|
|
115
|
-
recentViolations.has(key) &&
|
|
116
|
-
now - recentViolations.get(key) < VIOLATION_TTL_MS
|
|
117
|
-
) {
|
|
118
|
-
console.log(`[CSP-Edge] Skipped duplicate alert for ${key}`);
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Record the current timestamp
|
|
123
|
-
recentViolations.set(key, now);
|
|
124
|
-
|
|
125
|
-
// Cleanup old entries (memory-safe for low volume)
|
|
126
|
-
for (const [k, t] of recentViolations.entries()) {
|
|
127
|
-
if (now - t > VIOLATION_TTL_MS) {
|
|
128
|
-
recentViolations.delete(k);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const topicUrl = 'https://ntfy.neteng.pro/csp-alerts';
|
|
133
|
-
|
|
134
|
-
const message = [
|
|
135
|
-
`🚨 CSP Violation Detected`,
|
|
136
|
-
`Directive: ${violated}`,
|
|
137
|
-
`Blocked URI: ${blockedUri}`,
|
|
138
|
-
`Referrer: ${report.referrer || 'N/A'}`,
|
|
139
|
-
`Source: ${report['source-file'] || 'N/A'}`,
|
|
140
|
-
`Line: ${report['line-number'] || 'N/A'}`,
|
|
141
|
-
].join('\n');
|
|
142
|
-
|
|
143
|
-
await fetch(topicUrl, {
|
|
144
|
-
method: 'POST',
|
|
145
|
-
headers: {
|
|
146
|
-
'Content-Type': 'text/plain',
|
|
147
|
-
'X-Title': 'High-Risk CSP Violation',
|
|
148
|
-
'X-Priority': '5',
|
|
149
|
-
},
|
|
150
|
-
body: message,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Configuration block for the Edge Function.
|
|
156
|
-
* This sets the endpoint route to /api/csp-report
|
|
157
|
-
*/
|
|
158
|
-
export const config = {
|
|
159
|
-
path: '/api/csp-report',
|
|
160
|
-
};
|
|
@@ -1,81 +0,0 @@
|
|
|
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
|
-
});
|