nuxt-csp-report 1.0.0-alpha.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025-present - Gonzo17
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,176 @@
1
+ # Nuxt CSP Report
2
+
3
+ [![npm version][npm-version-src]][npm-version-href]
4
+ [![npm downloads][npm-downloads-src]][npm-downloads-href]
5
+ [![License][license-src]][license-href]
6
+ [![Nuxt][nuxt-src]][nuxt-href]
7
+
8
+ A Nuxt module for collecting, normalizing, and persisting Content Security Policy (CSP) reports.
9
+
10
+ [✨ Release Notes](/CHANGELOG.md)
11
+
12
+ ## What is CSP and CSP reports?
13
+
14
+ The CSP is a HTTP response header that allows you to control which resources a document is allowed to load. For example setting `Content-Security-Policy: script-src example.com;` will prevent any script tag from loading a source that is not from `example.com`. Any violation will be logged in the console of the browser. Additionally, a reporting endpoint can be set in the CSP header where the browser will send the CSP report to.
15
+
16
+ Once you decide to secure your website with CSP, you most likely want to analyze on production if your CSP headers are configured properly. That can be tricky the more external resources are loaded. Especially dynamically loaded scripts, e.g. depending on your country or your consent, are not always the same for every user. That's where the CSP reports are helpful, because they show the real CSP violations that users experience in their browsers.
17
+
18
+ ## Features
19
+
20
+ - 📋 Registers a POST endpoint for CSP reports
21
+ - 📡 Adds the `Reporting-Endpoints` header to your responses for `report-to` support
22
+ - 🔄 Supports both legacy `report-uri` and `report-to` format reports
23
+ - ✅ Validates and normalizes reports with Zod
24
+ - 💾 Persists reports via unstorage
25
+ - 📝 Full TypeScript support with proper type exports
26
+
27
+ ## Quick Setup
28
+
29
+ Install the module to your Nuxt application:
30
+
31
+ ```bash
32
+ npm install nuxt-csp-report
33
+ ```
34
+
35
+ Add it to your `nuxt.config.ts`:
36
+
37
+ ```typescript
38
+ export default defineNuxtConfig({
39
+ modules: ['nuxt-csp-report'],
40
+ cspReport: {
41
+ // Module options
42
+ },
43
+ })
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ The module is ready to go with the defaults.
49
+ In most use cases simple logs are sufficient. If you want to analyze CSP reports, you can use the `storage` option to persist the reports in a KV store.
50
+
51
+ ### nuxt-security
52
+
53
+ The Content Security Policy is set through specific headers. You can handle that yourself with Nuxt/Nitro, but I highly recommend using [nuxt-security](https://github.com/Baroshem/nuxt-security).
54
+ Here is a minimal example of how to use the two moduls in combination:
55
+
56
+ ```typescript
57
+ export default defineNuxtConfig({
58
+ modules: ['nuxt-security', 'nuxt-csp-report'],
59
+ security: {
60
+ headers: {
61
+ contentSecurityPolicy: {
62
+ 'report-uri': '/api/csp-report',
63
+ // your CSP headers
64
+ },
65
+ },
66
+ },
67
+ })
68
+ ```
69
+
70
+ ### Advanced: Access reports
71
+
72
+ Depending on your use case you might want to access the CSP reports. You can do that with `useStorage`:
73
+
74
+ ```typescript
75
+ export default defineNuxtConfig({
76
+ modules: ['nuxt-csp-report'],
77
+ cspReport: {
78
+ storage: {
79
+ driver: {
80
+ name: 'redis',
81
+ options: {
82
+ // Your redis configuration
83
+ }
84
+ }
85
+ },
86
+ },
87
+ })
88
+ ```
89
+
90
+ ```typescript
91
+ import { type NormalizedCspReport } from 'nuxt-csp-report'
92
+
93
+ const storage = useStorage<NormalizedCspReport>('csp-report-storage')
94
+ ```
95
+
96
+ ## Options
97
+
98
+ ### endpoint
99
+ * Type: `string`
100
+ * Default: `/api/csp-report`
101
+ * Description: Optional. Path for the CSP report endpoint.
102
+
103
+ ### reportingEndpointsHeader
104
+ * Type: `boolean`
105
+ * Default: `false`
106
+ * Description: Optional. Adds the `Reporting-Endpoints` header to your HTML responses, using `'csp-endpoint'` as the key and `endpoint` from the configuration as the value. This header is needed if you want to use `report-to csp-endpoint` in your CSP configuration.
107
+
108
+ ### console
109
+ * Type: `'summary' | 'full' | false`
110
+ * Default: `'summary'`
111
+ * Description: Optional. Log reports to console on server. `'full'` will print the `NormalizedCspReport` object.
112
+
113
+ ### storage
114
+ * Type: See fields below.
115
+ * Description: Optional. Sets up a storage using `unstorage`, which is part of Nitro and Nuxt.
116
+
117
+ ### storage.driver
118
+ * Type: `BuiltinDriverOptions`
119
+ * Description: Defines the driver from `unstorage`. You can use the same notation and drivers as in Nuxt:
120
+ * https://nuxt.com/docs/4.x/directory-structure/server#server-storage
121
+ * https://nitro.build/guide/storage
122
+ * https://unstorage.unjs.io/drivers
123
+
124
+ ### storage.keyPrefix
125
+ * Type: `string`
126
+ * Default: `csp-report`
127
+ * Description: Optional. Key prefix for the stored reports.
128
+
129
+
130
+ ## Contribution
131
+
132
+ <details>
133
+ <summary>Local development</summary>
134
+
135
+ ```bash
136
+ # Install dependencies
137
+ pnpm install
138
+
139
+ # Generate type stubs
140
+ pnpm run dev:prepare
141
+
142
+ # Develop with the playground
143
+ pnpm run dev
144
+
145
+ # Build the playground
146
+ pnpm run dev:build
147
+
148
+ # Run ESLint
149
+ pnpm run lint
150
+
151
+ # Run Vitest
152
+ pnpm run test
153
+ pnpm run test:watch
154
+
155
+ # Build the module
156
+ pnpm run prepack
157
+
158
+ # Release new version
159
+ pnpm run release
160
+ ```
161
+
162
+ </details>
163
+
164
+
165
+ <!-- Badges -->
166
+ [npm-version-src]: https://img.shields.io/npm/v/nuxt-csp-report/latest.svg?style=flat&colorA=020420&colorB=00DC82
167
+ [npm-version-href]: https://npmjs.com/package/nuxt-csp-report
168
+
169
+ [npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-csp-report.svg?style=flat&colorA=020420&colorB=00DC82
170
+ [npm-downloads-href]: https://npm.chart.dev/nuxt-csp-report
171
+
172
+ [license-src]: https://img.shields.io/npm/l/nuxt-csp-report.svg?style=flat&colorA=020420&colorB=00DC82
173
+ [license-href]: https://npmjs.com/package/nuxt-csp-report
174
+
175
+ [nuxt-src]: https://img.shields.io/badge/Nuxt-020420?logo=nuxt.js
176
+ [nuxt-href]: https://nuxt.com
@@ -0,0 +1,41 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+ import { BuiltinDriverOptions } from 'unstorage';
3
+ import { ReportToEntryFormat, ReportUriFormat } from '../dist/runtime/server/utils/normalizeCspReport.js';
4
+ export { ReportToEntryFormat, ReportToFormat, ReportUriFormat } from '../dist/runtime/server/utils/normalizeCspReport.js';
5
+
6
+ interface NuxtCspReportModuleOptions {
7
+ endpoint: string;
8
+ reportingEndpointsHeader?: boolean;
9
+ console: 'summary' | 'full' | false;
10
+ storage?: {
11
+ keyPrefix?: string;
12
+ driver: {
13
+ [driverName in keyof BuiltinDriverOptions]: {
14
+ name: driverName;
15
+ options?: BuiltinDriverOptions[driverName];
16
+ };
17
+ }[keyof BuiltinDriverOptions];
18
+ };
19
+ }
20
+ declare module 'nuxt/schema' {
21
+ interface RuntimeConfig {
22
+ cspReport?: NuxtCspReportModuleOptions;
23
+ }
24
+ }
25
+
26
+ interface NormalizedCspReport {
27
+ timestamp: number;
28
+ documentURL: string;
29
+ blockedURL: string;
30
+ directive: string;
31
+ sourceFile?: string;
32
+ line?: number;
33
+ column?: number;
34
+ disposition: 'enforce' | 'report';
35
+ raw: ReportToEntryFormat | ReportUriFormat;
36
+ }
37
+
38
+ declare const _default: _nuxt_schema.NuxtModule<NuxtCspReportModuleOptions, NuxtCspReportModuleOptions, false>;
39
+
40
+ export { _default as default };
41
+ export type { NormalizedCspReport, NuxtCspReportModuleOptions };
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "nuxt-csp-report",
3
+ "configKey": "cspReport",
4
+ "version": "1.0.0-alpha.1",
5
+ "builder": {
6
+ "@nuxt/module-builder": "1.0.2",
7
+ "unbuild": "3.6.1"
8
+ }
9
+ }
@@ -0,0 +1,53 @@
1
+ import { defineNuxtModule, createResolver, addServerHandler } from '@nuxt/kit';
2
+ import { defu } from 'defu';
3
+
4
+ const module$1 = defineNuxtModule({
5
+ meta: {
6
+ name: "nuxt-csp-report",
7
+ configKey: "cspReport"
8
+ },
9
+ defaults: {
10
+ endpoint: "/api/csp-report",
11
+ console: "summary",
12
+ reportingEndpointsHeader: false
13
+ },
14
+ setup(moduleOptions, nuxt) {
15
+ const resolver = createResolver(import.meta.url);
16
+ nuxt.options.runtimeConfig.cspReport = {
17
+ endpoint: moduleOptions.endpoint,
18
+ reportingEndpointsHeader: moduleOptions.reportingEndpointsHeader,
19
+ console: moduleOptions.console,
20
+ storage: moduleOptions.storage ? {
21
+ keyPrefix: moduleOptions.storage.keyPrefix || "csp-report",
22
+ driver: moduleOptions.storage.driver
23
+ } : void 0
24
+ };
25
+ addServerHandler({
26
+ route: moduleOptions.endpoint,
27
+ method: "post",
28
+ handler: resolver.resolve("./runtime/server/api/csp-report.post")
29
+ });
30
+ nuxt.hook("nitro:config", (config) => {
31
+ const cspReportConfig = nuxt.options.runtimeConfig.cspReport;
32
+ if (!cspReportConfig) return;
33
+ if (cspReportConfig.reportingEndpointsHeader) {
34
+ config.plugins = config.plugins || [];
35
+ config.plugins.push(resolver.resolve("./runtime/nitro/plugin/reporting-endpoints-header"));
36
+ }
37
+ const storageDriver = cspReportConfig.storage?.driver;
38
+ if (!storageDriver) return;
39
+ const { name, options = {} } = storageDriver;
40
+ config.storage = defu(
41
+ {
42
+ "csp-report-storage": {
43
+ driver: name,
44
+ ...options
45
+ }
46
+ },
47
+ config.storage
48
+ );
49
+ });
50
+ }
51
+ });
52
+
53
+ export { module$1 as default };
@@ -0,0 +1,2 @@
1
+ declare const _default: import("nitropack/types").NitroAppPlugin;
2
+ export default _default;
@@ -0,0 +1,15 @@
1
+ import { defineNitroPlugin } from "nitropack/runtime";
2
+ import { useRuntimeConfig } from "#imports";
3
+ import { setResponseHeader, getRequestHost, getRequestProtocol } from "h3";
4
+ export default defineNitroPlugin((nitroApp) => {
5
+ const cspReportConfig = useRuntimeConfig().cspReport;
6
+ if (!cspReportConfig?.reportingEndpointsHeader) {
7
+ return;
8
+ }
9
+ nitroApp.hooks.hook("render:response", (response, { event }) => {
10
+ const protocol = getRequestProtocol(event, { xForwardedProto: true });
11
+ const host = getRequestHost(event, { xForwardedHost: true });
12
+ const endpoint = `${protocol}://${host}${cspReportConfig.endpoint}`;
13
+ setResponseHeader(event, "Reporting-Endpoints", `csp-endpoint="${endpoint}"`);
14
+ });
15
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,32 @@
1
+ import { useRuntimeConfig, useStorage, readBody, defineEventHandler } from "#imports";
2
+ import { normalizeCspReport } from "../utils/normalizeCspReport.js";
3
+ import { formatCspLog } from "../utils/formatCspLog.js";
4
+ const runtimeConfig = useRuntimeConfig();
5
+ const storage = runtimeConfig.cspReport?.storage ? useStorage("csp-report-storage") : null;
6
+ export default defineEventHandler(async (event) => {
7
+ try {
8
+ const config = runtimeConfig.cspReport;
9
+ const body = await readBody(event).catch(() => null);
10
+ const reports = normalizeCspReport(body);
11
+ for (const report of reports) {
12
+ switch (config?.console) {
13
+ case "summary":
14
+ console.info("[nuxt-csp-report]", formatCspLog(report));
15
+ break;
16
+ case "full":
17
+ console.info("[nuxt-csp-report]", report);
18
+ break;
19
+ }
20
+ if (storage) {
21
+ const suffix = Math.random().toString(36).slice(2, 8);
22
+ const timestamp = Date.now();
23
+ const key = `${config.storage.keyPrefix}:${timestamp}:${suffix}`;
24
+ await storage.setItem(key, report);
25
+ }
26
+ }
27
+ return { ok: true };
28
+ } catch (err) {
29
+ console.error("[nuxt-csp-report]", err);
30
+ return { ok: true };
31
+ }
32
+ });
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "../../../.nuxt/tsconfig.server.json",
3
+ }
@@ -0,0 +1,2 @@
1
+ import type { NormalizedCspReport } from '../../../types/report.js';
2
+ export declare function formatCspLog(report: NormalizedCspReport): string;
@@ -0,0 +1,12 @@
1
+ export function formatCspLog(report) {
2
+ const parts = [];
3
+ if (report.directive) parts.push(`directive=${report.directive}`);
4
+ if (report.documentURL) parts.push(`document=${report.documentURL}`);
5
+ if (report.blockedURL) parts.push(`blocked=${report.blockedURL}`);
6
+ if (report.sourceFile) {
7
+ const loc = [report.sourceFile, report.line, report.column].filter((v) => v != null).join(":");
8
+ parts.push(`source=${loc}`);
9
+ }
10
+ if (report.disposition) parts.push(`mode=${report.disposition}`);
11
+ return parts.join(" | ");
12
+ }
@@ -0,0 +1,376 @@
1
+ import { z } from 'zod';
2
+ import type { NormalizedCspReport } from '../../../types/report.js';
3
+ declare const reportUriSchema: z.ZodObject<{
4
+ 'csp-report': z.ZodObject<{
5
+ 'blocked-uri': z.ZodString;
6
+ disposition: z.ZodEnum<["enforce", "report"]>;
7
+ 'document-uri': z.ZodString;
8
+ 'effective-directive': z.ZodString;
9
+ 'line-number': z.ZodOptional<z.ZodNumber>;
10
+ 'original-policy': z.ZodOptional<z.ZodString>;
11
+ referrer: z.ZodOptional<z.ZodString>;
12
+ 'script-sample': z.ZodOptional<z.ZodString>;
13
+ 'source-file': z.ZodOptional<z.ZodString>;
14
+ 'status-code': z.ZodOptional<z.ZodNumber>;
15
+ 'violated-directive': z.ZodString;
16
+ }, "strip", z.ZodTypeAny, {
17
+ disposition: "report" | "enforce";
18
+ 'blocked-uri': string;
19
+ 'document-uri': string;
20
+ 'effective-directive': string;
21
+ 'violated-directive': string;
22
+ referrer?: string | undefined;
23
+ 'line-number'?: number | undefined;
24
+ 'original-policy'?: string | undefined;
25
+ 'script-sample'?: string | undefined;
26
+ 'source-file'?: string | undefined;
27
+ 'status-code'?: number | undefined;
28
+ }, {
29
+ disposition: "report" | "enforce";
30
+ 'blocked-uri': string;
31
+ 'document-uri': string;
32
+ 'effective-directive': string;
33
+ 'violated-directive': string;
34
+ referrer?: string | undefined;
35
+ 'line-number'?: number | undefined;
36
+ 'original-policy'?: string | undefined;
37
+ 'script-sample'?: string | undefined;
38
+ 'source-file'?: string | undefined;
39
+ 'status-code'?: number | undefined;
40
+ }>;
41
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
42
+ 'csp-report': z.ZodObject<{
43
+ 'blocked-uri': z.ZodString;
44
+ disposition: z.ZodEnum<["enforce", "report"]>;
45
+ 'document-uri': z.ZodString;
46
+ 'effective-directive': z.ZodString;
47
+ 'line-number': z.ZodOptional<z.ZodNumber>;
48
+ 'original-policy': z.ZodOptional<z.ZodString>;
49
+ referrer: z.ZodOptional<z.ZodString>;
50
+ 'script-sample': z.ZodOptional<z.ZodString>;
51
+ 'source-file': z.ZodOptional<z.ZodString>;
52
+ 'status-code': z.ZodOptional<z.ZodNumber>;
53
+ 'violated-directive': z.ZodString;
54
+ }, "strip", z.ZodTypeAny, {
55
+ disposition: "report" | "enforce";
56
+ 'blocked-uri': string;
57
+ 'document-uri': string;
58
+ 'effective-directive': string;
59
+ 'violated-directive': string;
60
+ referrer?: string | undefined;
61
+ 'line-number'?: number | undefined;
62
+ 'original-policy'?: string | undefined;
63
+ 'script-sample'?: string | undefined;
64
+ 'source-file'?: string | undefined;
65
+ 'status-code'?: number | undefined;
66
+ }, {
67
+ disposition: "report" | "enforce";
68
+ 'blocked-uri': string;
69
+ 'document-uri': string;
70
+ 'effective-directive': string;
71
+ 'violated-directive': string;
72
+ referrer?: string | undefined;
73
+ 'line-number'?: number | undefined;
74
+ 'original-policy'?: string | undefined;
75
+ 'script-sample'?: string | undefined;
76
+ 'source-file'?: string | undefined;
77
+ 'status-code'?: number | undefined;
78
+ }>;
79
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
80
+ 'csp-report': z.ZodObject<{
81
+ 'blocked-uri': z.ZodString;
82
+ disposition: z.ZodEnum<["enforce", "report"]>;
83
+ 'document-uri': z.ZodString;
84
+ 'effective-directive': z.ZodString;
85
+ 'line-number': z.ZodOptional<z.ZodNumber>;
86
+ 'original-policy': z.ZodOptional<z.ZodString>;
87
+ referrer: z.ZodOptional<z.ZodString>;
88
+ 'script-sample': z.ZodOptional<z.ZodString>;
89
+ 'source-file': z.ZodOptional<z.ZodString>;
90
+ 'status-code': z.ZodOptional<z.ZodNumber>;
91
+ 'violated-directive': z.ZodString;
92
+ }, "strip", z.ZodTypeAny, {
93
+ disposition: "report" | "enforce";
94
+ 'blocked-uri': string;
95
+ 'document-uri': string;
96
+ 'effective-directive': string;
97
+ 'violated-directive': string;
98
+ referrer?: string | undefined;
99
+ 'line-number'?: number | undefined;
100
+ 'original-policy'?: string | undefined;
101
+ 'script-sample'?: string | undefined;
102
+ 'source-file'?: string | undefined;
103
+ 'status-code'?: number | undefined;
104
+ }, {
105
+ disposition: "report" | "enforce";
106
+ 'blocked-uri': string;
107
+ 'document-uri': string;
108
+ 'effective-directive': string;
109
+ 'violated-directive': string;
110
+ referrer?: string | undefined;
111
+ 'line-number'?: number | undefined;
112
+ 'original-policy'?: string | undefined;
113
+ 'script-sample'?: string | undefined;
114
+ 'source-file'?: string | undefined;
115
+ 'status-code'?: number | undefined;
116
+ }>;
117
+ }, z.ZodTypeAny, "passthrough">>;
118
+ export type ReportUriFormat = z.infer<typeof reportUriSchema>;
119
+ declare const reportToEntrySchema: z.ZodObject<{
120
+ age: z.ZodNumber;
121
+ body: z.ZodObject<{
122
+ blockedURL: z.ZodString;
123
+ columnNumber: z.ZodOptional<z.ZodNumber>;
124
+ disposition: z.ZodEnum<["enforce", "report"]>;
125
+ documentURL: z.ZodString;
126
+ effectiveDirective: z.ZodString;
127
+ lineNumber: z.ZodOptional<z.ZodNumber>;
128
+ originalPolicy: z.ZodString;
129
+ referrer: z.ZodOptional<z.ZodString>;
130
+ sample: z.ZodOptional<z.ZodString>;
131
+ sourceFile: z.ZodOptional<z.ZodString>;
132
+ statusCode: z.ZodNumber;
133
+ }, "strip", z.ZodTypeAny, {
134
+ statusCode: number;
135
+ blockedURL: string;
136
+ disposition: "report" | "enforce";
137
+ documentURL: string;
138
+ effectiveDirective: string;
139
+ originalPolicy: string;
140
+ referrer?: string | undefined;
141
+ columnNumber?: number | undefined;
142
+ lineNumber?: number | undefined;
143
+ sample?: string | undefined;
144
+ sourceFile?: string | undefined;
145
+ }, {
146
+ statusCode: number;
147
+ blockedURL: string;
148
+ disposition: "report" | "enforce";
149
+ documentURL: string;
150
+ effectiveDirective: string;
151
+ originalPolicy: string;
152
+ referrer?: string | undefined;
153
+ columnNumber?: number | undefined;
154
+ lineNumber?: number | undefined;
155
+ sample?: string | undefined;
156
+ sourceFile?: string | undefined;
157
+ }>;
158
+ type: z.ZodString;
159
+ url: z.ZodString;
160
+ user_agent: z.ZodString;
161
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
162
+ age: z.ZodNumber;
163
+ body: z.ZodObject<{
164
+ blockedURL: z.ZodString;
165
+ columnNumber: z.ZodOptional<z.ZodNumber>;
166
+ disposition: z.ZodEnum<["enforce", "report"]>;
167
+ documentURL: z.ZodString;
168
+ effectiveDirective: z.ZodString;
169
+ lineNumber: z.ZodOptional<z.ZodNumber>;
170
+ originalPolicy: z.ZodString;
171
+ referrer: z.ZodOptional<z.ZodString>;
172
+ sample: z.ZodOptional<z.ZodString>;
173
+ sourceFile: z.ZodOptional<z.ZodString>;
174
+ statusCode: z.ZodNumber;
175
+ }, "strip", z.ZodTypeAny, {
176
+ statusCode: number;
177
+ blockedURL: string;
178
+ disposition: "report" | "enforce";
179
+ documentURL: string;
180
+ effectiveDirective: string;
181
+ originalPolicy: string;
182
+ referrer?: string | undefined;
183
+ columnNumber?: number | undefined;
184
+ lineNumber?: number | undefined;
185
+ sample?: string | undefined;
186
+ sourceFile?: string | undefined;
187
+ }, {
188
+ statusCode: number;
189
+ blockedURL: string;
190
+ disposition: "report" | "enforce";
191
+ documentURL: string;
192
+ effectiveDirective: string;
193
+ originalPolicy: string;
194
+ referrer?: string | undefined;
195
+ columnNumber?: number | undefined;
196
+ lineNumber?: number | undefined;
197
+ sample?: string | undefined;
198
+ sourceFile?: string | undefined;
199
+ }>;
200
+ type: z.ZodString;
201
+ url: z.ZodString;
202
+ user_agent: z.ZodString;
203
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
204
+ age: z.ZodNumber;
205
+ body: z.ZodObject<{
206
+ blockedURL: z.ZodString;
207
+ columnNumber: z.ZodOptional<z.ZodNumber>;
208
+ disposition: z.ZodEnum<["enforce", "report"]>;
209
+ documentURL: z.ZodString;
210
+ effectiveDirective: z.ZodString;
211
+ lineNumber: z.ZodOptional<z.ZodNumber>;
212
+ originalPolicy: z.ZodString;
213
+ referrer: z.ZodOptional<z.ZodString>;
214
+ sample: z.ZodOptional<z.ZodString>;
215
+ sourceFile: z.ZodOptional<z.ZodString>;
216
+ statusCode: z.ZodNumber;
217
+ }, "strip", z.ZodTypeAny, {
218
+ statusCode: number;
219
+ blockedURL: string;
220
+ disposition: "report" | "enforce";
221
+ documentURL: string;
222
+ effectiveDirective: string;
223
+ originalPolicy: string;
224
+ referrer?: string | undefined;
225
+ columnNumber?: number | undefined;
226
+ lineNumber?: number | undefined;
227
+ sample?: string | undefined;
228
+ sourceFile?: string | undefined;
229
+ }, {
230
+ statusCode: number;
231
+ blockedURL: string;
232
+ disposition: "report" | "enforce";
233
+ documentURL: string;
234
+ effectiveDirective: string;
235
+ originalPolicy: string;
236
+ referrer?: string | undefined;
237
+ columnNumber?: number | undefined;
238
+ lineNumber?: number | undefined;
239
+ sample?: string | undefined;
240
+ sourceFile?: string | undefined;
241
+ }>;
242
+ type: z.ZodString;
243
+ url: z.ZodString;
244
+ user_agent: z.ZodString;
245
+ }, z.ZodTypeAny, "passthrough">>;
246
+ declare const reportToSchema: z.ZodArray<z.ZodObject<{
247
+ age: z.ZodNumber;
248
+ body: z.ZodObject<{
249
+ blockedURL: z.ZodString;
250
+ columnNumber: z.ZodOptional<z.ZodNumber>;
251
+ disposition: z.ZodEnum<["enforce", "report"]>;
252
+ documentURL: z.ZodString;
253
+ effectiveDirective: z.ZodString;
254
+ lineNumber: z.ZodOptional<z.ZodNumber>;
255
+ originalPolicy: z.ZodString;
256
+ referrer: z.ZodOptional<z.ZodString>;
257
+ sample: z.ZodOptional<z.ZodString>;
258
+ sourceFile: z.ZodOptional<z.ZodString>;
259
+ statusCode: z.ZodNumber;
260
+ }, "strip", z.ZodTypeAny, {
261
+ statusCode: number;
262
+ blockedURL: string;
263
+ disposition: "report" | "enforce";
264
+ documentURL: string;
265
+ effectiveDirective: string;
266
+ originalPolicy: string;
267
+ referrer?: string | undefined;
268
+ columnNumber?: number | undefined;
269
+ lineNumber?: number | undefined;
270
+ sample?: string | undefined;
271
+ sourceFile?: string | undefined;
272
+ }, {
273
+ statusCode: number;
274
+ blockedURL: string;
275
+ disposition: "report" | "enforce";
276
+ documentURL: string;
277
+ effectiveDirective: string;
278
+ originalPolicy: string;
279
+ referrer?: string | undefined;
280
+ columnNumber?: number | undefined;
281
+ lineNumber?: number | undefined;
282
+ sample?: string | undefined;
283
+ sourceFile?: string | undefined;
284
+ }>;
285
+ type: z.ZodString;
286
+ url: z.ZodString;
287
+ user_agent: z.ZodString;
288
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
289
+ age: z.ZodNumber;
290
+ body: z.ZodObject<{
291
+ blockedURL: z.ZodString;
292
+ columnNumber: z.ZodOptional<z.ZodNumber>;
293
+ disposition: z.ZodEnum<["enforce", "report"]>;
294
+ documentURL: z.ZodString;
295
+ effectiveDirective: z.ZodString;
296
+ lineNumber: z.ZodOptional<z.ZodNumber>;
297
+ originalPolicy: z.ZodString;
298
+ referrer: z.ZodOptional<z.ZodString>;
299
+ sample: z.ZodOptional<z.ZodString>;
300
+ sourceFile: z.ZodOptional<z.ZodString>;
301
+ statusCode: z.ZodNumber;
302
+ }, "strip", z.ZodTypeAny, {
303
+ statusCode: number;
304
+ blockedURL: string;
305
+ disposition: "report" | "enforce";
306
+ documentURL: string;
307
+ effectiveDirective: string;
308
+ originalPolicy: string;
309
+ referrer?: string | undefined;
310
+ columnNumber?: number | undefined;
311
+ lineNumber?: number | undefined;
312
+ sample?: string | undefined;
313
+ sourceFile?: string | undefined;
314
+ }, {
315
+ statusCode: number;
316
+ blockedURL: string;
317
+ disposition: "report" | "enforce";
318
+ documentURL: string;
319
+ effectiveDirective: string;
320
+ originalPolicy: string;
321
+ referrer?: string | undefined;
322
+ columnNumber?: number | undefined;
323
+ lineNumber?: number | undefined;
324
+ sample?: string | undefined;
325
+ sourceFile?: string | undefined;
326
+ }>;
327
+ type: z.ZodString;
328
+ url: z.ZodString;
329
+ user_agent: z.ZodString;
330
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
331
+ age: z.ZodNumber;
332
+ body: z.ZodObject<{
333
+ blockedURL: z.ZodString;
334
+ columnNumber: z.ZodOptional<z.ZodNumber>;
335
+ disposition: z.ZodEnum<["enforce", "report"]>;
336
+ documentURL: z.ZodString;
337
+ effectiveDirective: z.ZodString;
338
+ lineNumber: z.ZodOptional<z.ZodNumber>;
339
+ originalPolicy: z.ZodString;
340
+ referrer: z.ZodOptional<z.ZodString>;
341
+ sample: z.ZodOptional<z.ZodString>;
342
+ sourceFile: z.ZodOptional<z.ZodString>;
343
+ statusCode: z.ZodNumber;
344
+ }, "strip", z.ZodTypeAny, {
345
+ statusCode: number;
346
+ blockedURL: string;
347
+ disposition: "report" | "enforce";
348
+ documentURL: string;
349
+ effectiveDirective: string;
350
+ originalPolicy: string;
351
+ referrer?: string | undefined;
352
+ columnNumber?: number | undefined;
353
+ lineNumber?: number | undefined;
354
+ sample?: string | undefined;
355
+ sourceFile?: string | undefined;
356
+ }, {
357
+ statusCode: number;
358
+ blockedURL: string;
359
+ disposition: "report" | "enforce";
360
+ documentURL: string;
361
+ effectiveDirective: string;
362
+ originalPolicy: string;
363
+ referrer?: string | undefined;
364
+ columnNumber?: number | undefined;
365
+ lineNumber?: number | undefined;
366
+ sample?: string | undefined;
367
+ sourceFile?: string | undefined;
368
+ }>;
369
+ type: z.ZodString;
370
+ url: z.ZodString;
371
+ user_agent: z.ZodString;
372
+ }, z.ZodTypeAny, "passthrough">>, "many">;
373
+ export type ReportToEntryFormat = z.infer<typeof reportToEntrySchema>;
374
+ export type ReportToFormat = z.infer<typeof reportToSchema>;
375
+ export declare function normalizeCspReport(input: unknown): NormalizedCspReport[];
376
+ export {};
@@ -0,0 +1,73 @@
1
+ import { z } from "zod";
2
+ const reportUriSchema = z.object({
3
+ "csp-report": z.object({
4
+ "blocked-uri": z.string(),
5
+ "disposition": z.enum(["enforce", "report"]),
6
+ "document-uri": z.string(),
7
+ "effective-directive": z.string(),
8
+ "line-number": z.number().optional(),
9
+ "original-policy": z.string().optional(),
10
+ "referrer": z.string().optional(),
11
+ "script-sample": z.string().optional(),
12
+ "source-file": z.string().optional(),
13
+ "status-code": z.number().optional(),
14
+ "violated-directive": z.string()
15
+ })
16
+ }).passthrough();
17
+ const reportToEntrySchema = z.object({
18
+ age: z.number(),
19
+ body: z.object({
20
+ blockedURL: z.string(),
21
+ columnNumber: z.number().optional(),
22
+ disposition: z.enum(["enforce", "report"]),
23
+ documentURL: z.string(),
24
+ effectiveDirective: z.string(),
25
+ lineNumber: z.number().optional(),
26
+ originalPolicy: z.string(),
27
+ referrer: z.string().optional(),
28
+ sample: z.string().optional(),
29
+ sourceFile: z.string().optional(),
30
+ statusCode: z.number()
31
+ }),
32
+ type: z.string(),
33
+ url: z.string(),
34
+ user_agent: z.string()
35
+ }).passthrough();
36
+ const reportToSchema = reportToEntrySchema.array();
37
+ export function normalizeCspReport(input) {
38
+ if (!input) {
39
+ return [];
40
+ }
41
+ try {
42
+ if (typeof input === "object" && "csp-report" in input) {
43
+ const parsed = reportUriSchema.parse(input);
44
+ return [{
45
+ raw: parsed,
46
+ timestamp: Date.now(),
47
+ documentURL: parsed["csp-report"]["document-uri"],
48
+ blockedURL: parsed["csp-report"]["blocked-uri"],
49
+ directive: parsed["csp-report"]["violated-directive"],
50
+ sourceFile: parsed["csp-report"]["source-file"],
51
+ line: parsed["csp-report"]["line-number"],
52
+ disposition: parsed["csp-report"]["disposition"]
53
+ }];
54
+ }
55
+ if (Array.isArray(input)) {
56
+ const parsed = reportToSchema.parse(input);
57
+ return parsed.map((item) => {
58
+ return {
59
+ raw: item,
60
+ timestamp: Date.now(),
61
+ documentURL: item.body.documentURL,
62
+ blockedURL: item.body.blockedURL,
63
+ directive: item.body.effectiveDirective,
64
+ sourceFile: item.body.sourceFile,
65
+ line: item.body.lineNumber,
66
+ disposition: item.body.disposition
67
+ };
68
+ });
69
+ }
70
+ } catch {
71
+ }
72
+ return [];
73
+ }
@@ -0,0 +1,11 @@
1
+ import type { NuxtModule } from '@nuxt/schema'
2
+
3
+ import type { default as Module } from './module.mjs'
4
+
5
+ export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
6
+
7
+ export { type ReportToEntryFormat, type ReportToFormat, type ReportUriFormat } from '../dist/runtime/server/utils/normalizeCspReport.js'
8
+
9
+ export { default } from './module.mjs'
10
+
11
+ export { type NormalizedCspReport, type NuxtCspReportModuleOptions } from './module.mjs'
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "nuxt-csp-report",
3
+ "version": "1.0.0-alpha.1",
4
+ "description": "A Nuxt module for collecting, normalizing, and persisting Content Security Policy reports",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/Gonzo17/nuxt-csp-report.git"
8
+ },
9
+ "homepage": "https://github.com/Gonzo17/nuxt-csp-report",
10
+ "license": "MIT",
11
+ "keywords": [
12
+ "nuxt",
13
+ "security",
14
+ "csp",
15
+ "content-security-policy",
16
+ "content-security-policy-report",
17
+ "report-to",
18
+ "report-uri"
19
+ ],
20
+ "type": "module",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/types.d.mts",
24
+ "import": "./dist/module.mjs"
25
+ }
26
+ },
27
+ "main": "./dist/module.mjs",
28
+ "typesVersions": {
29
+ "*": {
30
+ ".": [
31
+ "./dist/types.d.mts"
32
+ ]
33
+ }
34
+ },
35
+ "files": [
36
+ "dist"
37
+ ],
38
+ "scripts": {
39
+ "prepack": "nuxt-module-build build",
40
+ "dev": "npm run dev:prepare && nuxi dev playground",
41
+ "dev:build": "nuxi build playground",
42
+ "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
43
+ "release": "npm run lint && npm run test && npm run prepack && changelogen --release --prerelease alpha && npm publish --tag alpha && git push --follow-tags",
44
+ "lint": "eslint .",
45
+ "test": "vitest run",
46
+ "test:watch": "vitest watch",
47
+ "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
48
+ },
49
+ "dependencies": {
50
+ "@nuxt/kit": "^4.2.1",
51
+ "defu": "^6.1.4",
52
+ "unstorage": "^1.17.3",
53
+ "zod": "^3.22.4"
54
+ },
55
+ "devDependencies": {
56
+ "@nuxt/devtools": "^3.1.0",
57
+ "@nuxt/eslint-config": "^1.10.0",
58
+ "@nuxt/module-builder": "^1.0.2",
59
+ "@nuxt/schema": "^4.2.1",
60
+ "@nuxt/test-utils": "^3.20.1",
61
+ "@types/node": "latest",
62
+ "changelogen": "^0.6.2",
63
+ "eslint": "^9.39.1",
64
+ "nuxt": "^4.2.1",
65
+ "nuxt-security": "^2.5.0",
66
+ "typescript": "~5.9.3",
67
+ "vitest": "^4.0.13",
68
+ "vue-tsc": "^3.1.5"
69
+ }
70
+ }