nuxt-security-module 4.3.3 → 4.5.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.
Files changed (62) hide show
  1. package/README.md +35 -8
  2. package/dist/constants.js +7 -0
  3. package/dist/index.js +3 -6
  4. package/dist/models/RequestSizeLimiterOptions.d.ts +0 -1
  5. package/dist/models/SsgElement.d.ts +1 -0
  6. package/dist/models/SsgHashSource.d.ts +2 -0
  7. package/dist/models/SsgOptions.d.ts +0 -2
  8. package/dist/runtime/constants.d.ts +1 -3
  9. package/dist/runtime/constants.js +9 -9
  10. package/dist/runtime/nitro/plugins/hidePoweredBy.js +2 -2
  11. package/dist/runtime/nitro/plugins/nonce.js +1 -6
  12. package/dist/runtime/nitro/plugins/routeRules.js +8 -4
  13. package/dist/runtime/nitro/plugins/ssgHashes.js +2 -7
  14. package/dist/runtime/nitro/plugins/subresourceIntegrity.js +1 -6
  15. package/dist/runtime/server/middleware/basicAuth.js +1 -1
  16. package/dist/runtime/server/middleware/corsHandler.js +3 -2
  17. package/dist/runtime/server/middleware/rateLimiter.js +1 -1
  18. package/dist/runtime/server/middleware/requestSizeLimiter.js +5 -3
  19. package/dist/runtime/services/checkIsBase64.d.ts +1 -0
  20. package/dist/runtime/services/checkIsBase64.js +5 -0
  21. package/dist/runtime/services/checkIsBasicAuthorized.js +1 -0
  22. package/dist/runtime/services/checkIsBodyCounted.d.ts +1 -0
  23. package/dist/runtime/services/checkIsBodyCounted.js +4 -0
  24. package/dist/runtime/services/checkIsRequestTooLarge.d.ts +1 -1
  25. package/dist/runtime/services/checkIsRequestTooLarge.js +2 -3
  26. package/dist/runtime/services/checkIsSecurityHeaderKey.d.ts +2 -0
  27. package/dist/runtime/services/checkIsSecurityHeaderKey.js +4 -0
  28. package/dist/runtime/services/checkIsStaticNonce.d.ts +1 -0
  29. package/dist/runtime/services/checkIsStaticNonce.js +4 -0
  30. package/dist/runtime/services/checkIsStringList.d.ts +1 -0
  31. package/dist/runtime/services/checkIsStringList.js +3 -0
  32. package/dist/runtime/services/collectSsgHashes.js +2 -24
  33. package/dist/runtime/services/getBasicAuthCredentials.js +1 -3
  34. package/dist/runtime/services/getBasicAuthOptions.js +1 -1
  35. package/dist/runtime/services/getSsgHashSource.d.ts +4 -0
  36. package/dist/runtime/services/getSsgHashSource.js +26 -0
  37. package/dist/runtime/services/injectSubresourceIntegrity.js +1 -1
  38. package/dist/runtime/services/readHtmlTagAttribute.js +1 -1
  39. package/dist/runtime/services/securityRouteMatcher.d.ts +1 -0
  40. package/dist/runtime/services/securityRouteMatcher.js +9 -2
  41. package/dist/runtime/services/serializeSecurityHeaders.js +2 -10
  42. package/dist/runtime/services/serializeValue.d.ts +1 -0
  43. package/dist/runtime/services/serializeValue.js +11 -0
  44. package/dist/runtime/services/unquote.d.ts +1 -0
  45. package/dist/runtime/services/unquote.js +3 -0
  46. package/dist/runtime/services/updateContentSecurityPolicy.js +1 -1
  47. package/dist/schemas/requestSizeLimiterOptionsSchema.js +1 -4
  48. package/dist/schemas/ssgOptionsSchema.js +1 -3
  49. package/dist/services/computeSubresourceIntegrityHashes.js +10 -15
  50. package/dist/services/constants.js +3 -0
  51. package/dist/services/formatLeafIssue.js +5 -0
  52. package/dist/services/formatZodIssues.js +2 -12
  53. package/dist/services/getDeepestPathLength.js +3 -0
  54. package/dist/services/getDefaultModuleOptions.js +2 -7
  55. package/dist/services/getDefaultSecurityHeaders.js +1 -1
  56. package/dist/services/getLeafIssues.js +11 -0
  57. package/dist/services/mergeDirectives.js +12 -0
  58. package/dist/services/mergeFeatures.js +10 -0
  59. package/dist/services/mergeHeaders.js +14 -0
  60. package/dist/services/mergeModuleOptions.js +1 -27
  61. package/dist/services/mergeSources.js +9 -0
  62. package/package.json +9 -20
package/README.md CHANGED
@@ -5,14 +5,15 @@
5
5
  [![NPM downloads][badge-npm-downloads]][url-npm]
6
6
  [![NPM Unpacked Size (with version)][badge-npm-unpacked-size]][url-npm]
7
7
 
8
- Nuxt security module for [Nitro 3](https://nitro.build) and h3 2. It sets security headers on rendered pages, a per-request nonce Content Security Policy with the nonce written onto the page's own tags, subresource integrity, a prerender policy as a `<meta>` element, the framework's `X-Powered-By` header removed, CORS, allowed methods, basic auth and a request size limit.
8
+ This package is a Nuxt security module for [Nitro 3](https://nitro.build) and h3 2 that sets security headers on rendered pages. It also provides a per-request nonce Content Security Policy, subresource integrity, CORS, allowed methods, basic auth and a request size limit.
9
9
 
10
- It replaces [nuxt-security](https://github.com/Baroshem/nuxt-security). Its upstream audit, every issue it was held to, and the defaults it ships are on the documentation page: [nuxt-security-module](https://github.com/Esposter/Esposter/blob/main/openspec/specs/nuxt-security-module/README.md).
10
+ Design notes live in [the specs](https://github.com/Esposter/Esposter/tree/main/openspec/specs/nuxt-security-module).
11
11
 
12
12
  ## Table of Contents
13
13
 
14
14
  - 🚀 [Getting Started](#getting-started)
15
- - 📖 [Documentation](#documentation)
15
+ - 🧩 [Key Exports](#key-exports)
16
+ - 📝 [Usage](#usage)
16
17
  - ⚖️ [License](#license)
17
18
 
18
19
  ---
@@ -28,17 +29,43 @@ pnpm i nuxt-security-module
28
29
  export default defineNuxtConfig({ modules: ["nuxt-security-module"] });
29
30
  ```
30
31
 
31
- ## <a name="documentation">📖 Documentation</a>
32
+ ## <a name="key-exports">🧩 Key Exports</a>
32
33
 
33
- We highly recommend you take a look at the [documentation](https://github.com/Esposter/Esposter/tree/main/openspec/specs/nuxt-security-module) to level up.
34
+ | Export | Kind | Purpose |
35
+ | :----------------------- | :-------- | :----------------------------------------------------------------------------------------------------------------------- |
36
+ | `createRouteRulesPlugin` | function | Exported from `./runtime/nitro/plugins/routeRules`: the plugin that matches each request against the build's route rules |
37
+ | `default` | const | The Nuxt module, set up from the `security` config key; each feature registers only when its option is on |
38
+ | `ModuleOptions` | interface | The module's options, one key per feature, each on or off; the `security` config key takes a partial of them |
39
+ | `rateLimiter` | const | Default export of `./runtime/server/middleware/rateLimiter`: refuses a request with `429` once its key's window is spent |
34
40
 
35
- The module's option reference, its upstream audit and its defaults review live in the repository's [specs](https://github.com/Esposter/Esposter/tree/main/openspec/specs/nuxt-security-module). The package's status and the steps not yet built are in its `README.md` there.
41
+ ## <a name="usage">📝 Usage</a>
36
42
 
37
- ---
43
+ ```ts
44
+ // nuxt.config.ts
45
+ export default defineNuxtConfig({
46
+ modules: ["nuxt-security-module"],
47
+ security: {
48
+ // Refuses a client with 429 after 100 requests in each minute-long window
49
+ rateLimiter: { intervalInMilliseconds: 60000, tokensPerInterval: 100 },
50
+ },
51
+ });
52
+ ```
53
+
54
+ A request's body limit is the one its route sets, whatever its `Content-Type` claims, since a client writes that header. A route that takes a larger body names its own limit in its route rule:
55
+
56
+ ```ts
57
+ // nuxt.config.ts
58
+ export default defineNuxtConfig({
59
+ routeRules: {
60
+ // Lets a request to this route carry a 10 MB body, where the rest of the app takes the 2 MB default
61
+ "/api/import/**": { security: { requestSizeLimiter: { maxRequestSizeInBytes: 10_000_000 } } },
62
+ },
63
+ });
64
+ ```
38
65
 
39
66
  ## <a name="license">⚖️ License</a>
40
67
 
41
- Apache-2.0
68
+ Licensed under [Apache-2.0][url-license].
42
69
 
43
70
  [badge-license]: https://img.shields.io/github/license/Esposter/Esposter.svg?color=blue
44
71
  [url-license]: https://github.com/Esposter/Esposter/blob/main/LICENSE
@@ -0,0 +1,7 @@
1
+ import { MODULE_NAME } from "./runtime/constants.js";
2
+ // src/constants.ts
3
+ const ROUTE_RULES_TEMPLATE_FILENAME = `#${MODULE_NAME}/route-rules.mjs`;
4
+ const SRI_HASHES_TEMPLATE_FILENAME = `#${MODULE_NAME}/sri-hashes.mjs`;
5
+ const ROUTE_RULES_PLUGIN_FILENAME = `#${MODULE_NAME}/route-rules-plugin.mjs`;
6
+ const SRI_PLUGIN_FILENAME = `#${MODULE_NAME}/sri-plugin.mjs`;
7
+ export { ROUTE_RULES_PLUGIN_FILENAME, ROUTE_RULES_TEMPLATE_FILENAME, SRI_HASHES_TEMPLATE_FILENAME, SRI_PLUGIN_FILENAME };
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { ROUTE_RULES_PLUGIN_FILENAME, ROUTE_RULES_TEMPLATE_FILENAME, SRI_HASHES_TEMPLATE_FILENAME, SRI_PLUGIN_FILENAME } from "./constants.js";
1
2
  import { MODULE_NAME } from "./runtime/constants.js";
2
3
  import { checkIsRecord } from "./runtime/services/checkIsRecord.js";
3
4
  import { computeSubresourceIntegrityHashes } from "./services/computeSubresourceIntegrityHashes.js";
@@ -5,18 +6,14 @@ import { getDefaultModuleOptions } from "./services/getDefaultModuleOptions.js";
5
6
  import { getPluginTemplate } from "./services/getPluginTemplate.js";
6
7
  import { getRemoveLoggersViteConfig } from "./services/getRemoveLoggersViteConfig.js";
7
8
  import { getRouteRulesTemplate } from "./services/getRouteRulesTemplate.js";
8
- import { mergeModuleOptions } from "./services/mergeModuleOptions.js";
9
9
  import { getRouteSecurityRules } from "./services/getRouteSecurityRules.js";
10
10
  import { getStaticResourceHeaders } from "./services/getStaticResourceHeaders.js";
11
11
  import { getTypeTemplate } from "./services/getTypeTemplate.js";
12
+ import { mergeModuleOptions } from "./services/mergeModuleOptions.js";
12
13
  import { parseModuleOptions } from "./services/parseModuleOptions.js";
13
14
  import { defu } from "defu";
14
15
  import { addNitroPlugin, addServerHandler, addServerTemplate, addTypeTemplate, createResolver, defineNuxtModule } from "@nuxt/kit";
15
16
  // src/module.ts
16
- const ROUTE_RULES_TEMPLATE_FILENAME = `#${MODULE_NAME}/route-rules.mjs`;
17
- const SRI_HASHES_TEMPLATE_FILENAME = `#${MODULE_NAME}/sri-hashes.mjs`;
18
- const ROUTE_RULES_PLUGIN_FILENAME = `#${MODULE_NAME}/route-rules-plugin.mjs`;
19
- const SRI_PLUGIN_FILENAME = `#${MODULE_NAME}/sri-plugin.mjs`;
20
17
  const securityModule = defineNuxtModule({
21
18
  meta: {
22
19
  compatibility: { nuxt: ">=5.0.0-0" },
@@ -88,7 +85,7 @@ const securityModule = defineNuxtModule({
88
85
  filename: SRI_HASHES_TEMPLATE_FILENAME,
89
86
  getContents: () => `export default ${JSON.stringify(sriHashes)}\n`
90
87
  });
91
- nuxt.hook("nitro:build:before", async (nitro) => {
88
+ nuxt.hook("nitro:build:public-assets", async (nitro) => {
92
89
  sriHashes = await computeSubresourceIntegrityHashes(nitro);
93
90
  });
94
91
  }
@@ -1,4 +1,3 @@
1
1
  export interface RequestSizeLimiterOptions {
2
2
  maxRequestSizeInBytes: number;
3
- maxUploadFileRequestInBytes: number;
4
3
  }
@@ -0,0 +1 @@
1
+ export type SsgElement = [name: string, attributes: string, body: string | undefined];
@@ -0,0 +1,2 @@
1
+ import { SecurityState } from "../runtime/services/getSecurityState.js";
2
+ export type SsgHashSource = [kind: keyof SecurityState["hashes"], source: string];
@@ -1,7 +1,5 @@
1
1
  export interface SsgOptions {
2
- exportToPresets: boolean;
3
2
  hashScripts: boolean;
4
3
  hashStyles: boolean;
5
4
  meta: boolean;
6
- nitroHeaders: boolean;
7
5
  }
@@ -11,12 +11,10 @@ export declare const NONCE_BYTE_LENGTH = 18;
11
11
  export declare const NONCE_PLACEHOLDER = "'nonce-{{nonce}}'";
12
12
  export declare const SRI_HASH_ALGORITHM = "SHA-384";
13
13
  export declare const CSP_HASH_ALGORITHM = "SHA-256";
14
- export declare const PRERENDERED_HEADERS_DIRECTORY_NAME = "nuxt-security";
15
- export declare const PRERENDERED_HEADERS_FILE_NAME = "headers.json";
16
14
  export declare const HTML_ATTRIBUTE_REGEX: RegExp;
17
15
  export declare const HTML_TAG_REGEX: RegExp;
18
16
  export declare const NONCE_TAG_NAMES: readonly string[];
19
- export declare const FILE_UPLOAD_CONTENT_TYPE = "multipart/form-data";
17
+ export declare const HTML_SECTIONS: readonly ["head", "bodyPrepend", "body", "bodyAppend"];
20
18
  export declare const SIZE_LIMITED_METHODS: readonly string[];
21
19
  export declare const DEFAULT_BASIC_AUTH_REALM = "Please enter username and password";
22
20
  export declare const BASIC_AUTHORIZATION_SCHEME = "Basic";
@@ -19,19 +19,18 @@ const SECURITY_HEADER_NAMES = {
19
19
  const CONTENT_SECURITY_POLICY_REPORT_ONLY_HEADER_NAME = "Content-Security-Policy-Report-Only";
20
20
  const MAX_RATE_LIMIT_ENTRIES = 1e4;
21
21
  const UNKNOWN_CLIENT_KEY = "unknown";
22
- const CSRF_UNSAFE_METHODS = [
22
+ const STATE_CHANGING_METHODS = [
23
23
  "DELETE",
24
24
  "PATCH",
25
25
  "POST",
26
26
  "PUT"
27
27
  ];
28
+ const CSRF_UNSAFE_METHODS = STATE_CHANGING_METHODS;
28
29
  const CSRF_TOKEN_BYTE_LENGTH = 32;
29
30
  const NONCE_BYTE_LENGTH = 18;
30
31
  const NONCE_PLACEHOLDER = "'nonce-{{nonce}}'";
31
32
  const SRI_HASH_ALGORITHM = "SHA-384";
32
33
  const CSP_HASH_ALGORITHM = "SHA-256";
33
- const PRERENDERED_HEADERS_DIRECTORY_NAME = "nuxt-security";
34
- const PRERENDERED_HEADERS_FILE_NAME = "headers.json";
35
34
  const HTML_ATTRIBUTE_PATTERN = String.raw`\s+[^\s"'<>/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'<>=]+))?`;
36
35
  const HTML_ATTRIBUTE_REGEX = /\s+(?<name>[^\s"'<>/=]+)(?<assignment>\s*=\s*(?<value>"[^"]*"|'[^']*'|[^\s"'<>=]+))?/gu;
37
36
  const HTML_TAG_REGEX = new RegExp(String.raw`<(script|style)\b((?:${HTML_ATTRIBUTE_PATTERN})*)(\s*\/?)>([\s\S]*?)<\/\1\s*>|<([a-z][\w:-]*)((?:${HTML_ATTRIBUTE_PATTERN})*)(\s*\/?)>`, "giu");
@@ -40,12 +39,13 @@ const NONCE_TAG_NAMES = [
40
39
  "script",
41
40
  "style"
42
41
  ];
43
- const FILE_UPLOAD_CONTENT_TYPE = "multipart/form-data";
44
- const SIZE_LIMITED_METHODS = [
45
- "DELETE",
46
- "POST",
47
- "PUT"
42
+ const HTML_SECTIONS = [
43
+ "head",
44
+ "bodyPrepend",
45
+ "body",
46
+ "bodyAppend"
48
47
  ];
48
+ const SIZE_LIMITED_METHODS = STATE_CHANGING_METHODS;
49
49
  const DEFAULT_BASIC_AUTH_REALM = "Please enter username and password";
50
50
  const BASIC_AUTHORIZATION_SCHEME = "Basic";
51
- export { BASIC_AUTHORIZATION_SCHEME, CONTENT_SECURITY_POLICY_REPORT_ONLY_HEADER_NAME, CSP_HASH_ALGORITHM, CSRF_TOKEN_BYTE_LENGTH, CSRF_UNSAFE_METHODS, DEFAULT_BASIC_AUTH_REALM, FILE_UPLOAD_CONTENT_TYPE, HTML_ATTRIBUTE_REGEX, HTML_TAG_REGEX, MAX_RATE_LIMIT_ENTRIES, MODULE_NAME, NONCE_BYTE_LENGTH, NONCE_PLACEHOLDER, NONCE_TAG_NAMES, PRERENDERED_HEADERS_DIRECTORY_NAME, PRERENDERED_HEADERS_FILE_NAME, ROUTE_RULES_HOOK, SECURITY_HEADER_NAMES, SIZE_LIMITED_METHODS, SRI_HASH_ALGORITHM, UNKNOWN_CLIENT_KEY };
51
+ export { BASIC_AUTHORIZATION_SCHEME, CONTENT_SECURITY_POLICY_REPORT_ONLY_HEADER_NAME, CSP_HASH_ALGORITHM, CSRF_TOKEN_BYTE_LENGTH, CSRF_UNSAFE_METHODS, DEFAULT_BASIC_AUTH_REALM, HTML_ATTRIBUTE_REGEX, HTML_SECTIONS, HTML_TAG_REGEX, MAX_RATE_LIMIT_ENTRIES, MODULE_NAME, NONCE_BYTE_LENGTH, NONCE_PLACEHOLDER, NONCE_TAG_NAMES, ROUTE_RULES_HOOK, SECURITY_HEADER_NAMES, SIZE_LIMITED_METHODS, SRI_HASH_ALGORITHM, UNKNOWN_CLIENT_KEY };
@@ -3,10 +3,10 @@ import { definePlugin } from "nitro";
3
3
  import { isEvent } from "nitro/h3";
4
4
  // src/runtime/nitro/plugins/hidePoweredBy.ts
5
5
  const hidePoweredBy = definePlugin((nitroApp) => {
6
- nitroApp.hooks.hook("response", (_response, event) => {
6
+ nitroApp.hooks.hook("response", (response, event) => {
7
7
  if (!isEvent(event)) return;
8
8
  const rules = getSecurityRules(event);
9
- if (rules?.enabled && rules.hidePoweredBy) event.res.headers.delete("x-powered-by");
9
+ if (rules?.enabled && rules.hidePoweredBy) response.headers.delete("x-powered-by");
10
10
  });
11
11
  });
12
12
  export { hidePoweredBy as default };
@@ -1,15 +1,10 @@
1
+ import { HTML_SECTIONS } from "../../constants.js";
1
2
  import { generateNonce } from "../../services/generateNonce.js";
2
3
  import { getSecurityRules } from "../../services/getSecurityRules.js";
3
4
  import { getSecurityState } from "../../services/getSecurityState.js";
4
5
  import { injectNonce } from "../../services/injectNonce.js";
5
6
  import { definePlugin } from "nitro";
6
7
  // src/runtime/nitro/plugins/nonce.ts
7
- const HTML_SECTIONS = [
8
- "body",
9
- "bodyAppend",
10
- "bodyPrepend",
11
- "head"
12
- ];
13
8
  const noncePlugin = definePlugin((nitroApp) => {
14
9
  if (import.meta.prerender) return;
15
10
  nitroApp.hooks.hook("render:route", (_context, { event }) => {
@@ -1,14 +1,18 @@
1
1
  import { ROUTE_RULES_HOOK } from "../../constants.js";
2
- import { setSecurityRouteMatcher } from "../../services/securityRouteMatcher.js";
2
+ import { setSecurityRouteMatcher, setSecurityRouteMatcherFailure } from "../../services/securityRouteMatcher.js";
3
3
  import { definePlugin } from "nitro";
4
4
  // src/runtime/nitro/plugins/routeRules.ts
5
5
  const createRouteRulesPlugin = (routeRules) => definePlugin((nitroApp) => {
6
+ const resolveRouteRules = async () => {
7
+ const routes = structuredClone(routeRules);
8
+ await nitroApp.hooks.callHook(ROUTE_RULES_HOOK, routes);
9
+ setSecurityRouteMatcher(routes);
10
+ };
6
11
  let routesResolved;
7
12
  nitroApp.hooks.hook("request", () => {
8
13
  routesResolved ??= (async () => {
9
- const routes = structuredClone(routeRules);
10
- await nitroApp.hooks.callHook(ROUTE_RULES_HOOK, routes);
11
- setSecurityRouteMatcher(routes);
14
+ const [resolution] = await Promise.allSettled([resolveRouteRules()]);
15
+ if (resolution.status === "rejected") setSecurityRouteMatcherFailure(resolution.reason);
12
16
  })();
13
17
  return routesResolved;
14
18
  });
@@ -1,3 +1,4 @@
1
+ import { HTML_SECTIONS } from "../../constants.js";
1
2
  import { collectSsgHashes } from "../../services/collectSsgHashes.js";
2
3
  import { getSecurityRules } from "../../services/getSecurityRules.js";
3
4
  import { getSecurityState } from "../../services/getSecurityState.js";
@@ -5,12 +6,6 @@ import { insertContentSecurityPolicyMeta } from "../../services/insertContentSec
5
6
  import { updateContentSecurityPolicy } from "../../services/updateContentSecurityPolicy.js";
6
7
  import { definePlugin } from "nitro";
7
8
  // src/runtime/nitro/plugins/ssgHashes.ts
8
- const HTML_SECTIONS = [
9
- "head",
10
- "bodyPrepend",
11
- "body",
12
- "bodyAppend"
13
- ];
14
9
  const ssgHashes = definePlugin((nitroApp) => {
15
10
  if (!import.meta.prerender) return;
16
11
  nitroApp.hooks.hook("render:html", async (html, { event }) => {
@@ -18,7 +13,7 @@ const ssgHashes = definePlugin((nitroApp) => {
18
13
  if (!rules?.enabled || !rules.ssg || !rules.headers || !rules.headers.contentSecurityPolicy) return;
19
14
  const state = getSecurityState(event);
20
15
  state.hashes = await collectSsgHashes(HTML_SECTIONS.map((section) => html[section].join("")).join(""), rules.ssg);
21
- if (!rules.ssg.meta) return;
16
+ if (!rules.ssg.meta || rules.contentSecurityPolicyReportOnly) return;
22
17
  const policy = updateContentSecurityPolicy(rules.headers.contentSecurityPolicy, state);
23
18
  html.head = [insertContentSecurityPolicyMeta(html.head.join(""), policy)];
24
19
  });
@@ -1,13 +1,8 @@
1
+ import { HTML_SECTIONS } from "../../constants.js";
1
2
  import { getSecurityRules } from "../../services/getSecurityRules.js";
2
3
  import { injectSubresourceIntegrity } from "../../services/injectSubresourceIntegrity.js";
3
4
  import { definePlugin } from "nitro";
4
5
  // src/runtime/nitro/plugins/subresourceIntegrity.ts
5
- const HTML_SECTIONS = [
6
- "body",
7
- "bodyAppend",
8
- "bodyPrepend",
9
- "head"
10
- ];
11
6
  const createSubresourceIntegrityPlugin = (sriHashes) => definePlugin((nitroApp) => {
12
7
  nitroApp.hooks.hook("render:html", (html, { event }) => {
13
8
  const rules = getSecurityRules(event);
@@ -13,8 +13,8 @@ const basicAuthMiddleware = defineMiddleware((event) => {
13
13
  const { pathname } = event.url;
14
14
  if (!checkIsChallenged(pathname, basicAuth)) return;
15
15
  if (checkIsBasicAuthorized(event.req.headers.get("authorization"), basicAuth)) return;
16
- event.res.headers.set("WWW-Authenticate", serializeBasicAuthChallenge(basicAuth.message || "Please enter username and password"));
17
16
  throw new HTTPError({
17
+ headers: { "WWW-Authenticate": serializeBasicAuthChallenge(basicAuth.message || "Please enter username and password") },
18
18
  status: 401,
19
19
  statusText: "Access denied"
20
20
  });
@@ -4,9 +4,9 @@ import { handleCors } from "nitro/h3";
4
4
  // src/runtime/server/middleware/corsHandler.ts
5
5
  const corsHandler = defineMiddleware((event) => {
6
6
  const rules = getSecurityRules(event);
7
- if (!rules?.enabled || !rules.corsHandler) return;
7
+ if (!rules?.enabled || !rules.corsHandler) return void 0;
8
8
  const { allowHeaders, credentials, exposeHeaders, maxAge, methods, origin, preflight } = rules.corsHandler;
9
- handleCors(event, {
9
+ const response = handleCors(event, {
10
10
  allowHeaders,
11
11
  credentials,
12
12
  exposeHeaders,
@@ -15,5 +15,6 @@ const corsHandler = defineMiddleware((event) => {
15
15
  origin: [origin].flat(),
16
16
  preflight
17
17
  });
18
+ return response === false ? void 0 : response;
18
19
  });
19
20
  export { corsHandler as default };
@@ -12,8 +12,8 @@ const rateLimiter = defineMiddleware(async (event) => {
12
12
  const key = getRequestIP(event, { xForwardedFor: false }) ?? "unknown";
13
13
  const retryAfterSeconds = await getRetryAfterSeconds(rateLimiterStore, key, rules.rateLimiter);
14
14
  if (retryAfterSeconds === void 0) return;
15
- event.res.headers.set("Retry-After", String(retryAfterSeconds));
16
15
  throw new HTTPError({
16
+ headers: { "Retry-After": String(retryAfterSeconds) },
17
17
  status: 429,
18
18
  statusText: "Too Many Requests"
19
19
  });
@@ -1,16 +1,18 @@
1
+ import { checkIsBodyCounted } from "../../services/checkIsBodyCounted.js";
1
2
  import { checkIsRequestTooLarge } from "../../services/checkIsRequestTooLarge.js";
2
3
  import { getSecurityRules } from "../../services/getSecurityRules.js";
3
4
  import { defineMiddleware } from "nitro";
4
- import { HTTPError } from "nitro/h3";
5
+ import { HTTPError, assertBodySize } from "nitro/h3";
5
6
  // src/runtime/server/middleware/requestSizeLimiter.ts
6
7
  const requestSizeLimiter = defineMiddleware((event) => {
7
8
  const rules = getSecurityRules(event);
8
9
  if (!rules?.enabled || !rules.requestSizeLimiter) return;
10
+ const { method } = event.req;
9
11
  const contentLength = event.req.headers.get("content-length");
10
- const contentType = event.req.headers.get("content-type");
11
- if (checkIsRequestTooLarge(event.req.method, contentLength, contentType, rules.requestSizeLimiter)) throw new HTTPError({
12
+ if (checkIsRequestTooLarge(method, contentLength, rules.requestSizeLimiter)) throw new HTTPError({
12
13
  status: 413,
13
14
  statusText: "Payload Too Large"
14
15
  });
16
+ if (checkIsBodyCounted(method, contentLength)) assertBodySize(event, rules.requestSizeLimiter.maxRequestSizeInBytes);
15
17
  });
16
18
  export { requestSizeLimiter as default };
@@ -0,0 +1 @@
1
+ export declare const checkIsBase64: (value: string) => boolean;
@@ -0,0 +1,5 @@
1
+ // src/runtime/services/checkIsBase64.ts
2
+ const BASE64_REGEX = /^[\d+/A-Za-z]*={0,2}$/u;
3
+ const BASE64_QUANTUM_LENGTH = 4;
4
+ const checkIsBase64 = (value) => BASE64_REGEX.test(value) && (value.endsWith("=") ? value.length % BASE64_QUANTUM_LENGTH === 0 : value.length % BASE64_QUANTUM_LENGTH !== 1);
5
+ export { checkIsBase64 };
@@ -2,6 +2,7 @@ import { checkIsEqualInConstantTime } from "./checkIsEqualInConstantTime.js";
2
2
  import { getBasicAuthCredentials } from "./getBasicAuthCredentials.js";
3
3
  // src/runtime/services/checkIsBasicAuthorized.ts
4
4
  const checkIsBasicAuthorized = (authorization, { name, pass }) => {
5
+ if (!name || !pass) return false;
5
6
  const credentials = getBasicAuthCredentials(authorization);
6
7
  if (!credentials) return false;
7
8
  const isNameEqual = checkIsEqualInConstantTime(credentials.name, name);
@@ -0,0 +1 @@
1
+ export declare const checkIsBodyCounted: (method: string, contentLength: null | string) => boolean;
@@ -0,0 +1,4 @@
1
+ import { SIZE_LIMITED_METHODS } from "../constants.js";
2
+ // src/runtime/services/checkIsBodyCounted.ts
3
+ const checkIsBodyCounted = (method, contentLength) => contentLength === null && SIZE_LIMITED_METHODS.includes(method);
4
+ export { checkIsBodyCounted };
@@ -1,2 +1,2 @@
1
1
  import { RequestSizeLimiterOptions } from "../../models/RequestSizeLimiterOptions.js";
2
- export declare const checkIsRequestTooLarge: (method: string, contentLength: null | string, contentType: null | string, { maxRequestSizeInBytes, maxUploadFileRequestInBytes }: RequestSizeLimiterOptions) => boolean;
2
+ export declare const checkIsRequestTooLarge: (method: string, contentLength: null | string, { maxRequestSizeInBytes }: RequestSizeLimiterOptions) => boolean;
@@ -1,8 +1,7 @@
1
1
  import { SIZE_LIMITED_METHODS } from "../constants.js";
2
2
  // src/runtime/services/checkIsRequestTooLarge.ts
3
- const checkIsRequestTooLarge = (method, contentLength, contentType, { maxRequestSizeInBytes, maxUploadFileRequestInBytes }) => {
3
+ const checkIsRequestTooLarge = (method, contentLength, { maxRequestSizeInBytes }) => {
4
4
  if (!SIZE_LIMITED_METHODS.includes(method) || contentLength === null) return false;
5
- const limit = contentType?.includes("multipart/form-data") ?? false ? maxUploadFileRequestInBytes : maxRequestSizeInBytes;
6
- return Math.trunc(Number(contentLength)) >= limit;
5
+ return Math.trunc(Number(contentLength)) > maxRequestSizeInBytes;
7
6
  };
8
7
  export { checkIsRequestTooLarge };
@@ -0,0 +1,2 @@
1
+ import { SecurityHeaders } from "../../models/SecurityHeaders.js";
2
+ export declare const checkIsSecurityHeaderKey: (key: string) => key is keyof SecurityHeaders;
@@ -0,0 +1,4 @@
1
+ import { SECURITY_HEADER_NAMES } from "../constants.js";
2
+ // src/runtime/services/checkIsSecurityHeaderKey.ts
3
+ const checkIsSecurityHeaderKey = (key) => Object.hasOwn(SECURITY_HEADER_NAMES, key);
4
+ export { checkIsSecurityHeaderKey };
@@ -0,0 +1 @@
1
+ export declare const checkIsStaticNonce: (source: string) => boolean;
@@ -0,0 +1,4 @@
1
+ import "../constants.js";
2
+ // src/runtime/services/checkIsStaticNonce.ts
3
+ const checkIsStaticNonce = (source) => source.startsWith("'nonce-") && source !== "'nonce-{{nonce}}'";
4
+ export { checkIsStaticNonce };
@@ -0,0 +1 @@
1
+ export declare const checkIsStringList: (value: unknown) => value is string[];
@@ -0,0 +1,3 @@
1
+ // src/runtime/services/checkIsStringList.ts
2
+ const checkIsStringList = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
3
+ export { checkIsStringList };
@@ -1,28 +1,6 @@
1
- import { CSP_HASH_ALGORITHM, HTML_TAG_REGEX } from "../constants.js";
2
- import { generateHash } from "./generateHash.js";
3
- import { readHtmlTagAttribute } from "./readHtmlTagAttribute.js";
1
+ import { HTML_TAG_REGEX } from "../constants.js";
2
+ import { getSsgHashSource } from "./getSsgHashSource.js";
4
3
  // src/runtime/services/collectSsgHashes.ts
5
- const SCRIPT_PRELOAD_TYPES = /* @__PURE__ */ new Set([
6
- "audioworklet",
7
- "paintworklet",
8
- "script",
9
- "xslt"
10
- ]);
11
- const getSsgHashSource = async ([name, attributes, body], { hashScripts, hashStyles }) => {
12
- const integrity = readHtmlTagAttribute(attributes, "integrity");
13
- if (name === "script" && hashScripts) {
14
- const source = readHtmlTagAttribute(attributes, "src");
15
- if (source !== void 0 && integrity !== void 0) return ["script", `'${integrity}'`];
16
- if (source === void 0 && body) return ["script", `'${await generateHash(body, CSP_HASH_ALGORITHM)}'`];
17
- return;
18
- }
19
- if (name === "style" && hashStyles && body) return ["style", `'${await generateHash(body, CSP_HASH_ALGORITHM)}'`];
20
- if (name !== "link" || integrity === void 0) return void 0;
21
- const rel = readHtmlTagAttribute(attributes, "rel");
22
- const as = readHtmlTagAttribute(attributes, "as") ?? "";
23
- if (rel === "stylesheet" && hashStyles) return ["style", `'${integrity}'`];
24
- if (hashScripts && (rel === "modulepreload" || rel === "preload" && SCRIPT_PRELOAD_TYPES.has(as))) return ["script", `'${integrity}'`];
25
- };
26
4
  const collectSsgHashes = async (html, options) => {
27
5
  const elements = Array.from(html.matchAll(HTML_TAG_REGEX), ([, elementName, elementAttributes, , body, tagName, tagAttributes]) => [
28
6
  (elementName ?? tagName ?? "").toLowerCase(),
@@ -1,8 +1,6 @@
1
1
  import { BASIC_AUTHORIZATION_SCHEME } from "../constants.js";
2
+ import { checkIsBase64 } from "./checkIsBase64.js";
2
3
  // src/runtime/services/getBasicAuthCredentials.ts
3
- const BASE64_REGEX = /^[\d+/A-Za-z]*={0,2}$/u;
4
- const BASE64_QUANTUM_LENGTH = 4;
5
- const checkIsBase64 = (value) => BASE64_REGEX.test(value) && (value.endsWith("=") ? value.length % BASE64_QUANTUM_LENGTH === 0 : value.length % BASE64_QUANTUM_LENGTH !== 1);
6
4
  const getBasicAuthCredentials = (authorization) => {
7
5
  if (!authorization?.startsWith(`Basic `)) return void 0;
8
6
  const encoded = authorization.slice(BASIC_AUTHORIZATION_SCHEME.length + 1).trim();
@@ -1,6 +1,6 @@
1
1
  import { checkIsRecord } from "./checkIsRecord.js";
2
+ import { checkIsStringList } from "./checkIsStringList.js";
2
3
  // src/runtime/services/getBasicAuthOptions.ts
3
- const checkIsStringList = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
4
4
  const getBasicAuthOptions = (privateRuntimeConfig) => {
5
5
  if (!checkIsRecord(privateRuntimeConfig) || !checkIsRecord(privateRuntimeConfig.basicAuth)) return void 0;
6
6
  const { exclude, include, message, name, pass } = privateRuntimeConfig.basicAuth;
@@ -0,0 +1,4 @@
1
+ import { SsgElement } from "../../models/SsgElement.js";
2
+ import { SsgHashSource } from "../../models/SsgHashSource.js";
3
+ import { SsgOptions } from "../../models/SsgOptions.js";
4
+ export declare const getSsgHashSource: ([name, attributes, body]: SsgElement, { hashScripts, hashStyles }: SsgOptions) => Promise<SsgHashSource | undefined>;
@@ -0,0 +1,26 @@
1
+ import { CSP_HASH_ALGORITHM } from "../constants.js";
2
+ import { generateHash } from "./generateHash.js";
3
+ import { readHtmlTagAttribute } from "./readHtmlTagAttribute.js";
4
+ // src/runtime/services/getSsgHashSource.ts
5
+ const SCRIPT_PRELOAD_TYPES = /* @__PURE__ */ new Set([
6
+ "audioworklet",
7
+ "paintworklet",
8
+ "script",
9
+ "xslt"
10
+ ]);
11
+ const getSsgHashSource = async ([name, attributes, body], { hashScripts, hashStyles }) => {
12
+ const integrity = readHtmlTagAttribute(attributes, "integrity");
13
+ if (name === "script" && hashScripts) {
14
+ const source = readHtmlTagAttribute(attributes, "src");
15
+ if (source !== void 0 && integrity !== void 0) return ["script", `'${integrity}'`];
16
+ if (source === void 0 && body) return ["script", `'${await generateHash(body, CSP_HASH_ALGORITHM)}'`];
17
+ return;
18
+ }
19
+ if (name === "style" && hashStyles && body) return ["style", `'${await generateHash(body, CSP_HASH_ALGORITHM)}'`];
20
+ if (name !== "link" || integrity === void 0) return void 0;
21
+ const rel = readHtmlTagAttribute(attributes, "rel");
22
+ const as = readHtmlTagAttribute(attributes, "as") ?? "";
23
+ if (rel === "stylesheet" && hashStyles) return ["style", `'${integrity}'`];
24
+ if (hashScripts && (rel === "modulepreload" || rel === "preload" && SCRIPT_PRELOAD_TYPES.has(as))) return ["script", `'${integrity}'`];
25
+ };
26
+ export { getSsgHashSource };
@@ -14,7 +14,7 @@ const injectSubresourceIntegrity = (html, hashes) => rewriteHtmlTags(html, (tagN
14
14
  const resourceAttributeName = RESOURCE_ATTRIBUTE_NAMES[tagName.toLowerCase()];
15
15
  if (!resourceAttributeName || readHtmlTagAttribute(attributes, "integrity") !== void 0) return void 0;
16
16
  const resource = readHtmlTagAttribute(attributes, resourceAttributeName);
17
- if (resource === void 0 || hashes[resource] === void 0) return void 0;
17
+ if (resource === void 0 || !Object.hasOwn(hashes, resource)) return void 0;
18
18
  if (tagName.toLowerCase() === "link" && !INTEGRITY_LINK_RELS.has(readHtmlTagAttribute(attributes, "rel") ?? "")) return void 0;
19
19
  return ` integrity="${hashes[resource]}"${attributes}`;
20
20
  });
@@ -1,7 +1,7 @@
1
1
  import { HTML_ATTRIBUTE_REGEX } from "../constants.js";
2
+ import { unquote } from "./unquote.js";
2
3
  // src/runtime/services/readHtmlTagAttribute.ts
3
4
  const readHtmlTagAttribute = (attributes, attributeName) => {
4
5
  for (const [, name, , value] of attributes.matchAll(HTML_ATTRIBUTE_REGEX)) if (name?.toLowerCase() === attributeName) return unquote(value ?? "");
5
6
  };
6
- const unquote = (value) => value.replace(/^(?<quote>["'])(?<inner>.*)\k<quote>$/su, "$<inner>");
7
7
  export { readHtmlTagAttribute };
@@ -1,3 +1,4 @@
1
1
  import { SecurityRules } from "../../models/SecurityRules.js";
2
2
  export declare const setSecurityRouteMatcher: (routes: Record<string, SecurityRules>) => void;
3
+ export declare const setSecurityRouteMatcherFailure: (cause: unknown) => void;
3
4
  export declare const findSecurityRules: (pathname: string) => SecurityRules[] | undefined;
@@ -1,10 +1,17 @@
1
1
  import { addRoute, createRouter, findAllRoutes } from "rou3";
2
2
  // src/runtime/services/securityRouteMatcher.ts
3
3
  let securityRouter;
4
+ let securityRouteFailure;
4
5
  const setSecurityRouteMatcher = (routes) => {
5
6
  const router = createRouter();
6
7
  for (const [pattern, rules] of Object.entries(routes)) addRoute(router, "", pattern, rules);
7
8
  securityRouter = router;
8
9
  };
9
- const findSecurityRules = (pathname) => securityRouter && findAllRoutes(securityRouter, "", pathname).flatMap(({ data }) => data ? [data] : []);
10
- export { findSecurityRules, setSecurityRouteMatcher };
10
+ const setSecurityRouteMatcherFailure = (cause) => {
11
+ securityRouteFailure = new TypeError("nuxt-security-module: the security route rules did not resolve", { cause });
12
+ };
13
+ const findSecurityRules = (pathname) => {
14
+ if (securityRouteFailure) throw securityRouteFailure;
15
+ return securityRouter && findAllRoutes(securityRouter, "", pathname).flatMap(({ data }) => data ? [data] : []);
16
+ };
17
+ export { findSecurityRules, setSecurityRouteMatcher, setSecurityRouteMatcherFailure };
@@ -1,18 +1,10 @@
1
1
  import { SECURITY_HEADER_NAMES } from "../constants.js";
2
- import { serializeContentSecurityPolicy } from "./serializeContentSecurityPolicy.js";
3
- import { serializePermissionsPolicy } from "./serializePermissionsPolicy.js";
4
- import { serializeStrictTransportSecurity } from "./serializeStrictTransportSecurity.js";
2
+ import { checkIsSecurityHeaderKey } from "./checkIsSecurityHeaderKey.js";
3
+ import { serializeValue } from "./serializeValue.js";
5
4
  // src/runtime/services/serializeSecurityHeaders.ts
6
- const checkIsSecurityHeaderKey = (key) => Object.hasOwn(SECURITY_HEADER_NAMES, key);
7
5
  const serializeSecurityHeaders = (headers) => Object.fromEntries(Object.keys(headers).flatMap((key) => {
8
6
  if (!checkIsSecurityHeaderKey(key)) return [];
9
7
  const value = headers[key];
10
8
  return value === false || value === void 0 ? [] : [[SECURITY_HEADER_NAMES[key], serializeValue(key, value)]];
11
9
  }));
12
- const serializeValue = (key, value) => {
13
- if (key === "contentSecurityPolicy") return serializeContentSecurityPolicy(value);
14
- if (key === "permissionsPolicy") return serializePermissionsPolicy(value);
15
- if (key === "strictTransportSecurity") return serializeStrictTransportSecurity(value);
16
- return String(value);
17
- };
18
10
  export { serializeSecurityHeaders };
@@ -0,0 +1 @@
1
+ export declare const serializeValue: (key: string, value: unknown) => string;
@@ -0,0 +1,11 @@
1
+ import { serializeContentSecurityPolicy } from "./serializeContentSecurityPolicy.js";
2
+ import { serializePermissionsPolicy } from "./serializePermissionsPolicy.js";
3
+ import { serializeStrictTransportSecurity } from "./serializeStrictTransportSecurity.js";
4
+ // src/runtime/services/serializeValue.ts
5
+ const serializeValue = (key, value) => {
6
+ if (key === "contentSecurityPolicy") return serializeContentSecurityPolicy(value);
7
+ if (key === "permissionsPolicy") return serializePermissionsPolicy(value);
8
+ if (key === "strictTransportSecurity") return serializeStrictTransportSecurity(value);
9
+ return String(value);
10
+ };
11
+ export { serializeValue };
@@ -0,0 +1 @@
1
+ export declare const unquote: (value: string) => string;
@@ -0,0 +1,3 @@
1
+ // src/runtime/services/unquote.ts
2
+ const unquote = (value) => value.replace(/^(?<quote>["'])(?<inner>.*)\k<quote>$/su, "$<inner>");
3
+ export { unquote };
@@ -1,4 +1,5 @@
1
1
  import { NONCE_PLACEHOLDER } from "../constants.js";
2
+ import { checkIsStaticNonce } from "./checkIsStaticNonce.js";
2
3
  // src/runtime/services/updateContentSecurityPolicy.ts
3
4
  const SCRIPT_DIRECTIVES = /* @__PURE__ */ new Set(["script-src", "script-src-elem"]);
4
5
  const STYLE_DIRECTIVES = /* @__PURE__ */ new Set(["style-src", "style-src-elem"]);
@@ -10,5 +11,4 @@ const updateContentSecurityPolicy = (policy, { hashes, nonce }) => Object.fromEn
10
11
  if (STYLE_DIRECTIVES.has(directive)) sources.push(...hashes.style);
11
12
  return [directive, sources];
12
13
  }));
13
- const checkIsStaticNonce = (source) => source.startsWith("'nonce-") && source !== "'nonce-{{nonce}}'";
14
14
  export { updateContentSecurityPolicy };
@@ -1,7 +1,4 @@
1
1
  import { z } from "zod";
2
2
  // src/schemas/requestSizeLimiterOptionsSchema.ts
3
- const requestSizeLimiterOptionsSchema = z.strictObject({
4
- maxRequestSizeInBytes: z.int("must be a whole number of bytes").positive("must be more than zero bytes"),
5
- maxUploadFileRequestInBytes: z.int("must be a whole number of bytes").positive("must be more than zero bytes")
6
- });
3
+ const requestSizeLimiterOptionsSchema = z.strictObject({ maxRequestSizeInBytes: z.int("must be a whole number of bytes").positive("must be more than zero bytes") });
7
4
  export { requestSizeLimiterOptionsSchema };
@@ -1,10 +1,8 @@
1
1
  import { z } from "zod";
2
2
  // src/schemas/ssgOptionsSchema.ts
3
3
  const ssgOptionsSchema = z.strictObject({
4
- exportToPresets: z.boolean(),
5
4
  hashScripts: z.boolean(),
6
5
  hashStyles: z.boolean(),
7
- meta: z.boolean(),
8
- nitroHeaders: z.boolean()
6
+ meta: z.boolean()
9
7
  });
10
8
  export { ssgOptionsSchema };
@@ -6,22 +6,17 @@ import { join, posix, relative, sep } from "node:path";
6
6
  // src/services/computeSubresourceIntegrityHashes.ts
7
7
  const computeSubresourceIntegrityHashes = async (nitro) => {
8
8
  const { baseURL: appBaseUrl, cdnURL: appCdnUrl } = nitro.options.runtimeConfig.app;
9
- const existingPublicAssets = nitro.options.publicAssets.filter(({ dir: directory }) => existsSync(directory));
10
- const filesByPublicAsset = await Promise.all(existingPublicAssets.map(async ({ baseURL = "", dir: directory }) => {
11
- return (await readdir(directory, {
12
- recursive: true,
13
- withFileTypes: true
14
- })).filter((entry) => entry.isFile()).map((entry) => {
15
- const filePath = join(entry.parentPath, entry.name);
16
- return {
17
- filePath,
18
- fullPath: posix.join(baseURL, ...relative(directory, filePath).split(sep))
19
- };
20
- });
21
- }));
22
- const hashEntries = await Promise.all(filesByPublicAsset.flat().map(async ({ filePath, fullPath }) => {
9
+ const { publicDir } = nitro.options.output;
10
+ if (!existsSync(publicDir)) return {};
11
+ const entries = await readdir(publicDir, {
12
+ recursive: true,
13
+ withFileTypes: true
14
+ });
15
+ const hashEntries = await Promise.all(entries.filter((entry) => entry.isFile()).map(async (entry) => {
16
+ const filePath = join(entry.parentPath, entry.name);
23
17
  const hash = await generateHash(await readFile(filePath), SRI_HASH_ALGORITHM);
24
- return [appCdnUrl ? new URL(posix.isAbsolute(fullPath) ? fullPath.slice(1) : fullPath, appCdnUrl.endsWith("/") ? appCdnUrl : `${appCdnUrl}/`).href : posix.join("/", appBaseUrl, fullPath), hash];
18
+ const fullPath = posix.join(...relative(publicDir, filePath).split(sep));
19
+ return [appCdnUrl ? new URL(fullPath, appCdnUrl.endsWith("/") ? appCdnUrl : `${appCdnUrl}/`).href : posix.join("/", appBaseUrl, fullPath), hash];
25
20
  }));
26
21
  return Object.fromEntries(hashEntries);
27
22
  };
@@ -0,0 +1,3 @@
1
+ // src/services/constants.ts
2
+ const NONE_SOURCE = "'none'";
3
+ export { NONE_SOURCE };
@@ -0,0 +1,5 @@
1
+ // src/services/formatLeafIssue.ts
2
+ const formatLeafIssue = ({ issue, path }) => {
3
+ return `${path.map(String).join(".") || "(options)"}: ${issue.message}`;
4
+ };
5
+ export { formatLeafIssue };
@@ -1,15 +1,5 @@
1
+ import { formatLeafIssue } from "./formatLeafIssue.js";
2
+ import { getLeafIssues } from "./getLeafIssues.js";
1
3
  // src/services/formatZodIssues.ts
2
- const getDeepestPathLength = (leafIssues) => Math.max(-1, ...leafIssues.map(({ path }) => path.length));
3
- const getLeafIssues = (issue, parentPath) => {
4
- const path = [...parentPath, ...issue.path];
5
- if (issue.code !== "invalid_union") return [{
6
- issue,
7
- path
8
- }];
9
- return issue.errors.map((branch) => branch.flatMap((branchIssue) => getLeafIssues(branchIssue, path))).reduce((deepest, branch) => getDeepestPathLength(branch) > getDeepestPathLength(deepest) ? branch : deepest);
10
- };
11
- const formatLeafIssue = ({ issue, path }) => {
12
- return `${path.map(String).join(".") || "(options)"}: ${issue.message}`;
13
- };
14
4
  const formatZodIssues = (issues) => issues.flatMap((issue) => getLeafIssues(issue, [])).map((leafIssue) => formatLeafIssue(leafIssue)).join("; ");
15
5
  export { formatZodIssues };
@@ -0,0 +1,3 @@
1
+ // src/services/getDeepestPathLength.ts
2
+ const getDeepestPathLength = (leafIssues) => Math.max(-1, ...leafIssues.map(({ path }) => path.length));
3
+ export { getDeepestPathLength };
@@ -23,17 +23,12 @@ const getDefaultModuleOptions = (isDevelopment, devServerUrl, isStrict) => ({
23
23
  nonce: true,
24
24
  rateLimiter: false,
25
25
  removeLoggers: false,
26
- requestSizeLimiter: {
27
- maxRequestSizeInBytes: 2e6,
28
- maxUploadFileRequestInBytes: 8e6
29
- },
26
+ requestSizeLimiter: { maxRequestSizeInBytes: 2e6 },
30
27
  sri: true,
31
28
  ssg: {
32
- exportToPresets: true,
33
29
  hashScripts: true,
34
30
  hashStyles: false,
35
- meta: true,
36
- nitroHeaders: true
31
+ meta: true
37
32
  },
38
33
  strict: isStrict
39
34
  });
@@ -1,6 +1,6 @@
1
- import { NONCE_PLACEHOLDER } from "../runtime/constants.js";
2
1
  import { CrossOriginEmbedderPolicy } from "../models/CrossOriginEmbedderPolicy.js";
3
2
  import { XFrameOptions } from "../models/XFrameOptions.js";
3
+ import { NONCE_PLACEHOLDER } from "../runtime/constants.js";
4
4
  // src/services/getDefaultSecurityHeaders.ts
5
5
  const getDefaultSecurityHeaders = (isDevelopment, isStrict) => ({
6
6
  contentSecurityPolicy: {
@@ -0,0 +1,11 @@
1
+ import { getDeepestPathLength } from "./getDeepestPathLength.js";
2
+ // src/services/getLeafIssues.ts
3
+ const getLeafIssues = (issue, parentPath) => {
4
+ const path = [...parentPath, ...issue.path];
5
+ if (issue.code !== "invalid_union") return [{
6
+ issue,
7
+ path
8
+ }];
9
+ return issue.errors.map((branch) => branch.flatMap((branchIssue) => getLeafIssues(branchIssue, path))).reduce((deepest, branch) => getDeepestPathLength(branch) > getDeepestPathLength(deepest) ? branch : deepest);
10
+ };
11
+ export { getLeafIssues };
@@ -0,0 +1,12 @@
1
+ import { mergeSources } from "./mergeSources.js";
2
+ // src/services/mergeDirectives.ts
3
+ const mergeDirectives = (defaults, overrides) => {
4
+ if (overrides === false || defaults === false) return overrides ?? defaults;
5
+ if (overrides === void 0) return defaults;
6
+ const merged = { ...defaults };
7
+ for (const [directive, value] of Object.entries(overrides)) if (value === false) delete merged[directive];
8
+ else if (Array.isArray(value)) merged[directive] = mergeSources(merged[directive], value);
9
+ else merged[directive] = value;
10
+ return merged;
11
+ };
12
+ export { mergeDirectives };
@@ -0,0 +1,10 @@
1
+ // src/services/mergeFeatures.ts
2
+ const mergeFeatures = (defaults, overrides) => {
3
+ if (overrides === false || defaults === false) return overrides ?? defaults;
4
+ if (overrides === void 0) return defaults;
5
+ const merged = { ...defaults };
6
+ for (const [feature, sources] of Object.entries(overrides)) if (sources === false) delete merged[feature];
7
+ else merged[feature] = [.../* @__PURE__ */ new Set([...merged[feature] || [], ...sources || []])];
8
+ return merged;
9
+ };
10
+ export { mergeFeatures };
@@ -0,0 +1,14 @@
1
+ import { mergeDirectives } from "./mergeDirectives.js";
2
+ import { mergeFeatures } from "./mergeFeatures.js";
3
+ // src/services/mergeHeaders.ts
4
+ const mergeHeaders = (defaults, overrides) => {
5
+ if (overrides === false || defaults === false) return overrides ?? defaults;
6
+ if (overrides === void 0) return defaults;
7
+ return {
8
+ ...defaults,
9
+ ...overrides,
10
+ contentSecurityPolicy: mergeDirectives(defaults.contentSecurityPolicy, overrides.contentSecurityPolicy),
11
+ permissionsPolicy: mergeFeatures(defaults.permissionsPolicy, overrides.permissionsPolicy)
12
+ };
13
+ };
14
+ export { mergeHeaders };
@@ -1,3 +1,4 @@
1
+ import { mergeHeaders } from "./mergeHeaders.js";
1
2
  // src/services/mergeModuleOptions.ts
2
3
  const mergeModuleOptions = (defaults, overrides) => ({
3
4
  ...defaults,
@@ -8,31 +9,4 @@ const mergeModuleOptions = (defaults, overrides) => ({
8
9
  ...overrides.ssg
9
10
  }
10
11
  });
11
- const mergeHeaders = (defaults, overrides) => {
12
- if (overrides === false || defaults === false) return overrides ?? defaults;
13
- if (overrides === void 0) return defaults;
14
- return {
15
- ...defaults,
16
- ...overrides,
17
- contentSecurityPolicy: mergeDirectives(defaults.contentSecurityPolicy, overrides.contentSecurityPolicy),
18
- permissionsPolicy: mergeFeatures(defaults.permissionsPolicy, overrides.permissionsPolicy)
19
- };
20
- };
21
- const mergeDirectives = (defaults, overrides) => {
22
- if (overrides === false || defaults === false) return overrides ?? defaults;
23
- if (overrides === void 0) return defaults;
24
- const merged = { ...defaults };
25
- for (const [directive, value] of Object.entries(overrides)) if (value === false) delete merged[directive];
26
- else if (Array.isArray(value)) merged[directive] = [.../* @__PURE__ */ new Set([...Array.isArray(merged[directive]) ? merged[directive] : [], ...value])];
27
- else merged[directive] = value;
28
- return merged;
29
- };
30
- const mergeFeatures = (defaults, overrides) => {
31
- if (overrides === false || defaults === false) return overrides ?? defaults;
32
- if (overrides === void 0) return defaults;
33
- const merged = { ...defaults };
34
- for (const [feature, sources] of Object.entries(overrides)) if (sources === false) delete merged[feature];
35
- else merged[feature] = [.../* @__PURE__ */ new Set([...merged[feature] || [], ...sources || []])];
36
- return merged;
37
- };
38
12
  export { mergeModuleOptions };
@@ -0,0 +1,9 @@
1
+ import { NONE_SOURCE } from "./constants.js";
2
+ // src/services/mergeSources.ts
3
+ const mergeSources = (defaultSources, sources) => {
4
+ if (sources.length === 1 && sources[0] === "'none'") return sources;
5
+ const mergedSources = /* @__PURE__ */ new Set([...Array.isArray(defaultSources) ? defaultSources : [], ...sources]);
6
+ if (mergedSources.size > 1) mergedSources.delete(NONE_SOURCE);
7
+ return [...mergedSources];
8
+ };
9
+ export { mergeSources };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-security-module",
3
- "version": "4.3.3",
3
+ "version": "4.5.0",
4
4
  "description": "Nuxt security module for Nitro 3 and h3 2 — security headers, a per-request nonce Content Security Policy, subresource integrity, CORS, allowed methods, basic auth and a request size limit.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Jimmy Chen",
@@ -19,19 +19,8 @@
19
19
  },
20
20
  "exports": {
21
21
  ".": "./dist/index.js",
22
- "./runtime/constants": "./dist/runtime/constants.js",
23
- "./runtime/nitro/plugins/hidePoweredBy": "./dist/runtime/nitro/plugins/hidePoweredBy.js",
24
- "./runtime/nitro/plugins/nonce": "./dist/runtime/nitro/plugins/nonce.js",
25
22
  "./runtime/nitro/plugins/routeRules": "./dist/runtime/nitro/plugins/routeRules.js",
26
- "./runtime/nitro/plugins/securityHeaders": "./dist/runtime/nitro/plugins/securityHeaders.js",
27
- "./runtime/nitro/plugins/ssgHashes": "./dist/runtime/nitro/plugins/ssgHashes.js",
28
- "./runtime/nitro/plugins/subresourceIntegrity": "./dist/runtime/nitro/plugins/subresourceIntegrity.js",
29
- "./runtime/server/middleware/allowedMethodsRestricter": "./dist/runtime/server/middleware/allowedMethodsRestricter.js",
30
- "./runtime/server/middleware/basicAuth": "./dist/runtime/server/middleware/basicAuth.js",
31
- "./runtime/server/middleware/corsHandler": "./dist/runtime/server/middleware/corsHandler.js",
32
- "./runtime/server/middleware/csrf": "./dist/runtime/server/middleware/csrf.js",
33
23
  "./runtime/server/middleware/rateLimiter": "./dist/runtime/server/middleware/rateLimiter.js",
34
- "./runtime/server/middleware/requestSizeLimiter": "./dist/runtime/server/middleware/requestSizeLimiter.js",
35
24
  "./package.json": "./package.json"
36
25
  },
37
26
  "scripts": {
@@ -40,7 +29,7 @@
40
29
  "lint": "eslint .",
41
30
  "lint:fix": "eslint --fix .",
42
31
  "test": "vitest",
43
- "typecheck": "tsc"
32
+ "typecheck": "tsrs --fast"
44
33
  },
45
34
  "dependencies": {
46
35
  "@nuxt/kit": "npm:@nuxt/kit-nightly@5x",
@@ -50,16 +39,16 @@
50
39
  "zod": "^4.6.7"
51
40
  },
52
41
  "devDependencies": {
53
- "@esposter/configuration": "4.3.3",
54
- "@esposter/shared-node": "4.3.3",
42
+ "@esposter/configuration": "4.5.0",
43
+ "@esposter/shared-node": "4.5.0",
55
44
  "@nuxt/nitro-server": "npm:@nuxt/nitro-server-nightly@5x",
56
- "@types/node": "^26.6.5",
57
- "h3": "^2.0.7",
58
- "tsdown": "^0.23.1"
45
+ "@types/node": "^26.6.6",
46
+ "h3": "^2.0.8",
47
+ "tsdown": "^0.23.5"
59
48
  },
60
49
  "peerDependencies": {
61
- "nitro": "npm:nitro-nightly@3.0.1-20261018-005730-bb9894fe",
50
+ "nitro": "npm:nitro-nightly@latest",
62
51
  "nuxt": "npm:nuxt-nightly@5x"
63
52
  },
64
- "gitHead": "955bbc130ad9b28877fd4ae8efc73a045141ce01"
53
+ "gitHead": "e30c001f3b4b15693e75fb53092d62a97ade4d2b"
65
54
  }