@rimelight/security 0.0.8 → 0.0.9

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.
@@ -1,3 +1,3 @@
1
- import { t as SecurityOptions } from "../types-Bg8w94Fn.mjs";
2
- import { t as buildCspHeader } from "../csp-ESHHTg-n.mjs";
1
+ import { t as SecurityOptions } from "../types-t69O3m_O.mjs";
2
+ import { buildCspHeader } from "../csp.mjs";
3
3
  export { SecurityOptions, buildCspHeader };
@@ -1,2 +1,3 @@
1
- import { t as buildCspHeader } from "../csp-DATUs3cP.mjs";
1
+ import "../types.mjs";
2
+ import { buildCspHeader } from "../csp.mjs";
2
3
  export { buildCspHeader };
package/dist/csp.d.mts ADDED
@@ -0,0 +1,7 @@
1
+ import { t as SecurityOptions } from "./types-t69O3m_O.mjs";
2
+ //#region src/csp.d.ts
3
+ /**
4
+ * Builds the Content-Security-Policy header string dynamically for a given request nonce.
5
+ */
6
+ export declare function buildCspHeader(options?: SecurityOptions, nonce?: string): string;
7
+ //#endregion
@@ -36,6 +36,7 @@ function buildCspHeader(options = {}, nonce) {
36
36
  "'sha256-ZuMSxilKU+4KIM8LWna4lqBEKzd6WaiAjz6gamhm7zM='",
37
37
  ...options.scriptResources || []
38
38
  ])),
39
+ "script-src-attr": ["'none'"],
39
40
  "style-src": Array.from(/* @__PURE__ */ new Set([
40
41
  "'self'",
41
42
  "'sha256-SKuaOGnks7NAUq37nvw1PfGEE0lOAs5ERbiYRbGFIbw='",
@@ -60,4 +61,4 @@ function buildCspHeader(options = {}, nonce) {
60
61
  }).join("; ");
61
62
  }
62
63
  //#endregion
63
- export { buildCspHeader as t };
64
+ export { buildCspHeader };
package/dist/index.d.mts CHANGED
@@ -1,27 +1,4 @@
1
- import { t as SecurityOptions } from "./types-Bg8w94Fn.mjs";
2
- import { t as buildCspHeader } from "./csp-ESHHTg-n.mjs";
3
- //#region src/vite.d.ts
4
- export interface RimelightSecurityPlugin {
5
- name: string;
6
- enforce?: "pre" | "post";
7
- resolveId?: (id: string) => string | null | undefined;
8
- load?: (id: string) => string | null | undefined;
9
- buildStart?: () => Promise<void> | void;
10
- generateBundle?: (options: any, bundle: Record<string, any>) => Promise<void> | void;
11
- [key: string]: any;
12
- }
13
- export interface SecurityPluginOptions extends SecurityOptions {
14
- /**
15
- * Additional external script/style URLs to fetch and hash for SRI at build time.
16
- */
17
- resources?: string[];
18
- }
19
- /**
20
- * Pure Vite Security & SRI Plugin. 1. Accepts framework-agnostic security options (domain, imgSrc,
21
- * etc.) and exposes virtual:rimelight-security-config. 2. Fetches and hashes external 3rd-party
22
- * scripts/styles at build time. 3. Emits SRI hashes for locally bundled JS/CSS chunks
23
- * (Nuxt-security style). 4. Exposes virtual:sri-manifest with a unified URL -> hash mapping.
24
- */
25
- export declare function security(options?: SecurityPluginOptions): RimelightSecurityPlugin;
26
- //#endregion
27
- export { SecurityOptions, buildCspHeader, security as sri };
1
+ import { t as SecurityOptions } from "./types-t69O3m_O.mjs";
2
+ import { buildCspHeader } from "./csp.mjs";
3
+ import { RimelightSecurityPlugin, SecurityPluginOptions, security } from "./vite.mjs";
4
+ export { RimelightSecurityPlugin, SecurityOptions, SecurityPluginOptions, buildCspHeader, security, security as sri };
package/dist/index.mjs CHANGED
@@ -1,66 +1,4 @@
1
- import { t as buildCspHeader } from "./csp-DATUs3cP.mjs";
2
- import { createHash } from "node:crypto";
3
- //#region src/vite.ts
4
- const DEFAULT_RESOURCES = ["https://betterlytics.io/analytics.js"];
5
- function hashContent(buffer) {
6
- return `sha384-${createHash("sha384").update(buffer).digest("base64")}`;
7
- }
8
- /**
9
- * Pure Vite Security & SRI Plugin. 1. Accepts framework-agnostic security options (domain, imgSrc,
10
- * etc.) and exposes virtual:rimelight-security-config. 2. Fetches and hashes external 3rd-party
11
- * scripts/styles at build time. 3. Emits SRI hashes for locally bundled JS/CSS chunks
12
- * (Nuxt-security style). 4. Exposes virtual:sri-manifest with a unified URL -> hash mapping.
13
- */
14
- function security(options = {}) {
15
- const sriManifest = {};
16
- const externalUrls = Array.from(/* @__PURE__ */ new Set([...DEFAULT_RESOURCES, ...options.resources || []]));
17
- const sriVirtualId = "virtual:sri-manifest";
18
- const resolvedSriVirtualId = "\0" + sriVirtualId;
19
- const configVirtualId = "virtual:rimelight-security-config";
20
- const resolvedConfigVirtualId = "\0" + configVirtualId;
21
- return {
22
- name: "vite-plugin-rimelight-security",
23
- enforce: "post",
24
- async buildStart() {
25
- if (process.env["NODE_ENV"] === "development") return;
26
- await Promise.all(externalUrls.map(async (url) => {
27
- if (!url.startsWith("https://")) return;
28
- try {
29
- const parsed = new URL(url);
30
- if (parsed.hostname.includes("*")) return;
31
- const pathname = parsed.pathname;
32
- if (pathname === "/" || pathname === "" || pathname.endsWith("/")) return;
33
- const response = await fetch(url);
34
- if (!response.ok) return;
35
- const buffer = await response.arrayBuffer();
36
- sriManifest[url] = hashContent(buffer);
37
- } catch {}
38
- }));
39
- },
40
- generateBundle(_outputOptions, bundle) {
41
- for (const [fileName, chunk] of Object.entries(bundle)) {
42
- if (!fileName.endsWith(".js") && !fileName.endsWith(".css")) continue;
43
- let content;
44
- if (chunk.type === "chunk") content = chunk.code;
45
- else if (chunk.type === "asset") content = chunk.source;
46
- if (content) {
47
- const hash = hashContent(content);
48
- sriManifest[`/${fileName.replace(/^\/+/, "")}`] = hash;
49
- sriManifest[fileName] = hash;
50
- }
51
- }
52
- },
53
- resolveId(id) {
54
- if (id === sriVirtualId) return resolvedSriVirtualId;
55
- if (id === configVirtualId) return resolvedConfigVirtualId;
56
- return null;
57
- },
58
- load(id) {
59
- if (id === resolvedSriVirtualId) return `export const manifest = ${JSON.stringify(sriManifest)};`;
60
- if (id === resolvedConfigVirtualId) return `export const config = ${JSON.stringify(options)};`;
61
- return null;
62
- }
63
- };
64
- }
65
- //#endregion
1
+ import "./types.mjs";
2
+ import { buildCspHeader } from "./csp.mjs";
3
+ import { security } from "./vite.mjs";
66
4
  export { buildCspHeader, security, security as sri };
@@ -0,0 +1,13 @@
1
+ //#region src/middleware/dev-only.d.ts
2
+ /**
3
+ * Hono middleware that blocks any path containing `/dev/` in non-development environments. Mount
4
+ * this early in your Hono app (before API routes and Astro pages) so it covers both `/dev/` page
5
+ * routes and `/api/dev/` API routes in a single place.
6
+ *
7
+ * @example
8
+ * // fetch.ts
9
+ * import { devOnly } from "@rimelight/security/middleware"
10
+ * app.use(devOnly)
11
+ */
12
+ export declare const devOnly: (c: any, next: any) => Promise<Response>;
13
+ //#endregion
@@ -0,0 +1,17 @@
1
+ //#region src/middleware/dev-only.ts
2
+ /**
3
+ * Hono middleware that blocks any path containing `/dev/` in non-development environments. Mount
4
+ * this early in your Hono app (before API routes and Astro pages) so it covers both `/dev/` page
5
+ * routes and `/api/dev/` API routes in a single place.
6
+ *
7
+ * @example
8
+ * // fetch.ts
9
+ * import { devOnly } from "@rimelight/security/middleware"
10
+ * app.use(devOnly)
11
+ */
12
+ const devOnly = async (c, next) => {
13
+ if (!import.meta.env.DEV && c.req.path.includes("/dev/")) return c.notFound();
14
+ return next();
15
+ };
16
+ //#endregion
17
+ export { devOnly };
@@ -1,21 +1,3 @@
1
- import { t as SecurityOptions } from "../types-Bg8w94Fn.mjs";
2
- //#region src/middleware/security.d.ts
3
- /**
4
- * Universal Security Middleware for Hono and Web Standards (Fetch API). Injects security headers,
5
- * CSP with per-request nonce, and SRI attributes.
6
- */
7
- export declare function security(options?: SecurityOptions): (c: any, next: any) => Promise<Response | void>;
8
- //#endregion
9
- //#region src/middleware/dev-only.d.ts
10
- /**
11
- * Hono middleware that blocks any path containing `/dev/` in non-development environments. Mount
12
- * this early in your Hono app (before API routes and Astro pages) so it covers both `/dev/` page
13
- * routes and `/api/dev/` API routes in a single place.
14
- *
15
- * @example
16
- * // fetch.ts
17
- * import { devOnly } from "@rimelight/security/middleware"
18
- * app.use(devOnly)
19
- */
20
- export declare const devOnly: (c: any, next: any) => Promise<Response>;
21
- //#endregion
1
+ import { devOnly } from "./dev-only.mjs";
2
+ import { security } from "./security.mjs";
3
+ export { devOnly, security };
@@ -1,196 +1,3 @@
1
- import { t as buildCspHeader } from "../csp-DATUs3cP.mjs";
2
- import { manifest } from "virtual:sri-manifest";
3
- import { config } from "virtual:rimelight-security-config";
4
- //#region src/middleware/security.ts
5
- function isManifestRecord(obj) {
6
- return typeof obj === "object" && obj !== null && !Array.isArray(obj);
7
- }
8
- function isConfigRecord(obj) {
9
- return typeof obj === "object" && obj !== null && !Array.isArray(obj);
10
- }
11
- const manifest$1 = isManifestRecord(manifest) ? manifest : {};
12
- const defaultPluginConfig = isConfigRecord(config) ? config : {};
13
- function applySecurityHeaders(response, options, origin, nonce, reportOnlyOptions) {
14
- const cspHeader = buildCspHeader(options, nonce);
15
- const reportOnlyHeader = reportOnlyOptions ? buildCspHeader({
16
- ...reportOnlyOptions,
17
- directives: {
18
- ...reportOnlyOptions.directives,
19
- "upgrade-insecure-requests": false
20
- }
21
- }, nonce) : void 0;
22
- const headers = new Headers(response.headers);
23
- headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
24
- headers.set("X-Content-Type-Options", "nosniff");
25
- headers.set("X-Frame-Options", "DENY");
26
- headers.set("Cross-Origin-Resource-Policy", "same-origin");
27
- headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload");
28
- headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=(), usb=()");
29
- headers.set("No-Vary-Search", "except=(\"q\" \"search\" \"locale\"), params=?1");
30
- if (cspHeader) headers.set("Content-Security-Policy", cspHeader);
31
- if (reportOnlyHeader) headers.set("Content-Security-Policy-Report-Only", reportOnlyHeader);
32
- if (origin) headers.set("Link", [
33
- `<${origin}/sitemap-index.xml>; rel="sitemap"`,
34
- `<${origin}/llms.txt>; rel="llms"`,
35
- `<${origin}/.well-known/api-catalog>; rel="api-catalog"`,
36
- `<${origin}/rss.xml>; rel="alternate"; type="application/rss+xml"`
37
- ].join(", "));
38
- if (options.headers) for (const [key, value] of Object.entries(options.headers)) headers.set(key, value);
39
- return new Response(response.body, {
40
- status: response.status,
41
- statusText: response.statusText,
42
- headers
43
- });
44
- }
45
- /**
46
- * Universal Security Middleware for Hono and Web Standards (Fetch API). Injects security headers,
47
- * CSP with per-request nonce, and SRI attributes.
48
- */
49
- function security(options = {}) {
50
- const mergedOptions = {
51
- ...defaultPluginConfig,
52
- ...options,
53
- allowedDomains: [...defaultPluginConfig.allowedDomains || [], ...options.allowedDomains || []],
54
- imgSrc: [...defaultPluginConfig.imgSrc || [], ...options.imgSrc || []],
55
- connectSrc: [...defaultPluginConfig.connectSrc || [], ...options.connectSrc || []],
56
- frameSrc: [...defaultPluginConfig.frameSrc || [], ...options.frameSrc || []],
57
- scriptResources: [...defaultPluginConfig.scriptResources || [], ...options.scriptResources || []],
58
- styleResources: [...defaultPluginConfig.styleResources || [], ...options.styleResources || []],
59
- directives: {
60
- ...defaultPluginConfig.directives,
61
- ...options.directives
62
- },
63
- headers: {
64
- ...defaultPluginConfig.headers,
65
- ...options.headers
66
- }
67
- };
68
- return async (c, next) => {
69
- const isProd = import.meta.env?.PROD ?? process.env["NODE_ENV"] === "production";
70
- const isDev = import.meta.env?.DEV ?? process.env["NODE_ENV"] === "development";
71
- const req = c.req?.raw || c.request || c.req;
72
- const proto = req?.headers?.get("x-forwarded-proto");
73
- if (isProd && proto === "http" && req?.url) {
74
- const httpsUrl = req.url.replace(/^http:/, "https:");
75
- return Response.redirect(httpsUrl, 301);
76
- }
77
- await next();
78
- const response = c.res;
79
- if (!response) return;
80
- const contentType = response.headers.get("content-type") || "";
81
- const origin = req?.url ? new URL(req.url).origin : void 0;
82
- if (!contentType.includes("text/html")) {
83
- c.res = applySecurityHeaders(response, mergedOptions, origin);
84
- return c.res;
85
- }
86
- if (isDev) {
87
- const devDirectives = {
88
- ...mergedOptions.directives,
89
- "default-src": false,
90
- "script-src": [
91
- "'self'",
92
- "'unsafe-inline'",
93
- "'unsafe-eval'",
94
- "https:",
95
- "http:",
96
- "ws:",
97
- "wss:"
98
- ],
99
- "style-src": [
100
- "'self'",
101
- "'unsafe-inline'",
102
- "https:",
103
- "http:"
104
- ],
105
- "connect-src": [
106
- "'self'",
107
- "ws:",
108
- "wss:",
109
- "http:",
110
- "https:"
111
- ],
112
- "img-src": [
113
- "'self'",
114
- "data:",
115
- "blob:",
116
- "https:",
117
- "http:"
118
- ]
119
- };
120
- const reportOnlySimulation = {
121
- ...mergedOptions,
122
- directives: {
123
- ...mergedOptions.directives,
124
- "style-src": ["'self'", "'unsafe-inline'"],
125
- "script-src": [
126
- "'self'",
127
- "'unsafe-inline'",
128
- "'unsafe-eval'"
129
- ]
130
- }
131
- };
132
- c.res = applySecurityHeaders(response, {
133
- ...mergedOptions,
134
- directives: devDirectives
135
- }, origin, void 0, reportOnlySimulation);
136
- return c.res;
137
- }
138
- const nonce = crypto.randomUUID().replace(/-/g, "");
139
- if (typeof HTMLRewriter !== "undefined") {
140
- let rewriter = new HTMLRewriter();
141
- rewriter = rewriter.on("script[src]", { element(el) {
142
- const src = el.getAttribute("src");
143
- if (src && manifest$1[src]) {
144
- el.setAttribute("integrity", manifest$1[src]);
145
- el.setAttribute("crossorigin", "anonymous");
146
- }
147
- } }).on("link[rel=\"stylesheet\"][href]", { element(el) {
148
- const href = el.getAttribute("href");
149
- if (href && manifest$1[href]) {
150
- el.setAttribute("integrity", manifest$1[href]);
151
- el.setAttribute("crossorigin", "anonymous");
152
- }
153
- } }).on("script", { element(el) {
154
- el.setAttribute("nonce", nonce);
155
- } }).on("head", { element(el) {
156
- el.append(`<meta name="csp-nonce" content="${nonce}">`, { html: true });
157
- } });
158
- c.res = applySecurityHeaders(rewriter.transform(response), mergedOptions, origin, nonce);
159
- return c.res;
160
- }
161
- let modifiedHtml = await response.text();
162
- modifiedHtml = modifiedHtml.replace(/(<head(?:\s[^>]*)?>)/i, `$1<meta name="csp-nonce" content="${nonce}">`);
163
- modifiedHtml = modifiedHtml.replace(/<script(\s[^>]*)?>/gi, (_match, attrs = "") => `<script${attrs} nonce="${nonce}">`);
164
- for (const [url, hash] of Object.entries(manifest$1)) {
165
- if (!hash) continue;
166
- const scriptRegex = new RegExp(`(<script[^>]+src=["']${url}["'][^>]*)(/?>)`, "g");
167
- const linkRegex = new RegExp(`(<link[^>]+href=["']${url}["'][^>]*)(/?>)`, "g");
168
- modifiedHtml = modifiedHtml.replace(scriptRegex, `$1 integrity="${hash}" crossorigin="anonymous"$2`);
169
- modifiedHtml = modifiedHtml.replace(linkRegex, `$1 integrity="${hash}" crossorigin="anonymous"$2`);
170
- }
171
- c.res = applySecurityHeaders(new Response(modifiedHtml, {
172
- status: response.status,
173
- statusText: response.statusText,
174
- headers: response.headers
175
- }), mergedOptions, origin, nonce);
176
- return c.res;
177
- };
178
- }
179
- //#endregion
180
- //#region src/middleware/dev-only.ts
181
- /**
182
- * Hono middleware that blocks any path containing `/dev/` in non-development environments. Mount
183
- * this early in your Hono app (before API routes and Astro pages) so it covers both `/dev/` page
184
- * routes and `/api/dev/` API routes in a single place.
185
- *
186
- * @example
187
- * // fetch.ts
188
- * import { devOnly } from "@rimelight/security/middleware"
189
- * app.use(devOnly)
190
- */
191
- const devOnly = async (c, next) => {
192
- if (!import.meta.env.DEV && c.req.path.includes("/dev/")) return c.notFound();
193
- return next();
194
- };
195
- //#endregion
1
+ import { devOnly } from "./dev-only.mjs";
2
+ import { security } from "./security.mjs";
196
3
  export { devOnly, security };
@@ -0,0 +1,8 @@
1
+ import { t as SecurityOptions } from "../types-t69O3m_O.mjs";
2
+ //#region src/middleware/security.d.ts
3
+ /**
4
+ * Universal Security Middleware for Hono and Web Standards (Fetch API). Injects security headers,
5
+ * CSP with per-request nonce, and SRI attributes.
6
+ */
7
+ export declare function security(options?: SecurityOptions): (c: any, next: any) => Promise<Response | void>;
8
+ //#endregion
@@ -0,0 +1,184 @@
1
+ import { buildCspHeader } from "../csp.mjs";
2
+ import { manifest } from "virtual:sri-manifest";
3
+ import { config } from "virtual:rimelight-security-config";
4
+ //#region src/middleware/security.ts
5
+ function isManifestRecord(obj) {
6
+ return typeof obj === "object" && obj !== null && !Array.isArray(obj);
7
+ }
8
+ function isConfigRecord(obj) {
9
+ return typeof obj === "object" && obj !== null && !Array.isArray(obj);
10
+ }
11
+ const manifest$1 = isManifestRecord(manifest) ? manifest : {};
12
+ const defaultPluginConfig = isConfigRecord(config) ? config : {};
13
+ function applySecurityHeaders(response, options, origin, nonce, reportOnlyOptions) {
14
+ const cspHeader = buildCspHeader(options, nonce);
15
+ const reportOnlyHeader = reportOnlyOptions ? buildCspHeader({
16
+ ...reportOnlyOptions,
17
+ directives: {
18
+ ...reportOnlyOptions.directives,
19
+ "upgrade-insecure-requests": false
20
+ }
21
+ }, nonce) : void 0;
22
+ const headers = new Headers(response.headers);
23
+ headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
24
+ headers.set("X-Content-Type-Options", "nosniff");
25
+ headers.set("X-Frame-Options", "DENY");
26
+ if (options.crossOriginResourcePolicy !== false) headers.set("Cross-Origin-Resource-Policy", typeof options.crossOriginResourcePolicy === "string" ? options.crossOriginResourcePolicy : "same-origin");
27
+ if (options.crossOriginOpenerPolicy !== false) headers.set("Cross-Origin-Opener-Policy", typeof options.crossOriginOpenerPolicy === "string" ? options.crossOriginOpenerPolicy : "same-origin");
28
+ if (options.crossOriginEmbedderPolicy !== false) headers.set("Cross-Origin-Embedder-Policy", typeof options.crossOriginEmbedderPolicy === "string" ? options.crossOriginEmbedderPolicy : "credentialless");
29
+ headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload");
30
+ headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=()");
31
+ headers.set("No-Vary-Search", "except=(\"q\" \"search\" \"locale\"), params=?1");
32
+ if (cspHeader) headers.set("Content-Security-Policy", cspHeader);
33
+ if (reportOnlyHeader) headers.set("Content-Security-Policy-Report-Only", reportOnlyHeader);
34
+ if (origin) headers.set("Link", [
35
+ `<${origin}/sitemap-index.xml>; rel="sitemap"`,
36
+ `<${origin}/llms.txt>; rel="llms"`,
37
+ `<${origin}/.well-known/api-catalog>; rel="api-catalog"`,
38
+ `<${origin}/rss.xml>; rel="alternate"; type="application/rss+xml"`
39
+ ].join(", "));
40
+ if (options.headers) for (const [key, value] of Object.entries(options.headers)) headers.set(key, value);
41
+ return new Response(response.body, {
42
+ status: response.status,
43
+ statusText: response.statusText,
44
+ headers
45
+ });
46
+ }
47
+ /**
48
+ * Universal Security Middleware for Hono and Web Standards (Fetch API). Injects security headers,
49
+ * CSP with per-request nonce, and SRI attributes.
50
+ */
51
+ function security(options = {}) {
52
+ const mergedOptions = {
53
+ ...defaultPluginConfig,
54
+ ...options,
55
+ allowedDomains: [...defaultPluginConfig.allowedDomains || [], ...options.allowedDomains || []],
56
+ imgSrc: [...defaultPluginConfig.imgSrc || [], ...options.imgSrc || []],
57
+ connectSrc: [...defaultPluginConfig.connectSrc || [], ...options.connectSrc || []],
58
+ frameSrc: [...defaultPluginConfig.frameSrc || [], ...options.frameSrc || []],
59
+ scriptResources: [...defaultPluginConfig.scriptResources || [], ...options.scriptResources || []],
60
+ styleResources: [...defaultPluginConfig.styleResources || [], ...options.styleResources || []],
61
+ directives: {
62
+ ...defaultPluginConfig.directives,
63
+ ...options.directives
64
+ },
65
+ headers: {
66
+ ...defaultPluginConfig.headers,
67
+ ...options.headers
68
+ }
69
+ };
70
+ return async (c, next) => {
71
+ const isProd = import.meta.env?.PROD ?? process.env["NODE_ENV"] === "production";
72
+ const isDev = import.meta.env?.DEV ?? process.env["NODE_ENV"] === "development";
73
+ const req = c.req?.raw || c.request || c.req;
74
+ const proto = req?.headers?.get("x-forwarded-proto");
75
+ if (isProd && proto === "http" && req?.url) {
76
+ const httpsUrl = req.url.replace(/^http:/, "https:");
77
+ return Response.redirect(httpsUrl, 301);
78
+ }
79
+ await next();
80
+ const response = c.res;
81
+ if (!response) return;
82
+ const contentType = response.headers.get("content-type") || "";
83
+ const origin = req?.url ? new URL(req.url).origin : void 0;
84
+ if (!contentType.includes("text/html")) {
85
+ c.res = applySecurityHeaders(response, mergedOptions, origin);
86
+ return c.res;
87
+ }
88
+ if (isDev) {
89
+ const devDirectives = {
90
+ ...mergedOptions.directives,
91
+ "default-src": false,
92
+ "script-src": [
93
+ "'self'",
94
+ "'unsafe-inline'",
95
+ "'unsafe-eval'",
96
+ "https:",
97
+ "http:",
98
+ "ws:",
99
+ "wss:"
100
+ ],
101
+ "script-src-attr": ["'unsafe-inline'"],
102
+ "style-src": [
103
+ "'self'",
104
+ "'unsafe-inline'",
105
+ "https:",
106
+ "http:"
107
+ ],
108
+ "connect-src": [
109
+ "'self'",
110
+ "ws:",
111
+ "wss:",
112
+ "http:",
113
+ "https:"
114
+ ],
115
+ "img-src": [
116
+ "'self'",
117
+ "data:",
118
+ "blob:",
119
+ "https:",
120
+ "http:"
121
+ ]
122
+ };
123
+ const reportOnlySimulation = {
124
+ ...mergedOptions,
125
+ directives: {
126
+ ...mergedOptions.directives,
127
+ "script-src-attr": ["'none'"],
128
+ "style-src": ["'self'", "'unsafe-inline'"],
129
+ "script-src": [
130
+ "'self'",
131
+ "'unsafe-inline'",
132
+ "'unsafe-eval'"
133
+ ]
134
+ }
135
+ };
136
+ c.res = applySecurityHeaders(response, {
137
+ ...mergedOptions,
138
+ directives: devDirectives
139
+ }, origin, void 0, reportOnlySimulation);
140
+ return c.res;
141
+ }
142
+ const nonce = crypto.randomUUID().replace(/-/g, "");
143
+ if (typeof HTMLRewriter !== "undefined") {
144
+ let rewriter = new HTMLRewriter();
145
+ rewriter = rewriter.on("script[src]", { element(el) {
146
+ const src = el.getAttribute("src");
147
+ if (src && manifest$1[src]) {
148
+ el.setAttribute("integrity", manifest$1[src]);
149
+ el.setAttribute("crossorigin", "anonymous");
150
+ }
151
+ } }).on("link[rel=\"stylesheet\"][href]", { element(el) {
152
+ const href = el.getAttribute("href");
153
+ if (href && manifest$1[href]) {
154
+ el.setAttribute("integrity", manifest$1[href]);
155
+ el.setAttribute("crossorigin", "anonymous");
156
+ }
157
+ } }).on("script", { element(el) {
158
+ el.setAttribute("nonce", nonce);
159
+ } }).on("head", { element(el) {
160
+ el.append(`<meta name="csp-nonce" content="${nonce}">`, { html: true });
161
+ } });
162
+ c.res = applySecurityHeaders(rewriter.transform(response), mergedOptions, origin, nonce);
163
+ return c.res;
164
+ }
165
+ let modifiedHtml = await response.text();
166
+ modifiedHtml = modifiedHtml.replace(/(<head(?:\s[^>]*)?>)/i, `$1<meta name="csp-nonce" content="${nonce}">`);
167
+ modifiedHtml = modifiedHtml.replace(/<script(\s[^>]*)?>/gi, (_match, attrs = "") => `<script${attrs} nonce="${nonce}">`);
168
+ for (const [url, hash] of Object.entries(manifest$1)) {
169
+ if (!hash) continue;
170
+ const scriptRegex = new RegExp(`(<script[^>]+src=["']${url}["'][^>]*)(/?>)`, "g");
171
+ const linkRegex = new RegExp(`(<link[^>]+href=["']${url}["'][^>]*)(/?>)`, "g");
172
+ modifiedHtml = modifiedHtml.replace(scriptRegex, `$1 integrity="${hash}" crossorigin="anonymous"$2`);
173
+ modifiedHtml = modifiedHtml.replace(linkRegex, `$1 integrity="${hash}" crossorigin="anonymous"$2`);
174
+ }
175
+ c.res = applySecurityHeaders(new Response(modifiedHtml, {
176
+ status: response.status,
177
+ statusText: response.statusText,
178
+ headers: response.headers
179
+ }), mergedOptions, origin, nonce);
180
+ return c.res;
181
+ };
182
+ }
183
+ //#endregion
184
+ export { security };
@@ -33,6 +33,22 @@ interface SecurityOptions {
33
33
  * Enables the 'require-trusted-types-for' directive
34
34
  */
35
35
  trustedTypes?: boolean;
36
+ /**
37
+ * Cross-Origin-Embedder-Policy header value. Allowed values: "require-corp", "credentialless",
38
+ * "unsafe-none" or false to disable. Defaults to "credentialless".
39
+ */
40
+ crossOriginEmbedderPolicy?: "require-corp" | "credentialless" | "unsafe-none" | (string & {}) | false;
41
+ /**
42
+ * Cross-Origin-Opener-Policy header value. Allowed values: "same-origin",
43
+ * "same-origin-allow-popups", "noopener-allow-popups", "unsafe-none" or false to disable.
44
+ * Defaults to "same-origin".
45
+ */
46
+ crossOriginOpenerPolicy?: "same-origin" | "same-origin-allow-popups" | "noopener-allow-popups" | "unsafe-none" | (string & {}) | false;
47
+ /**
48
+ * Cross-Origin-Resource-Policy header value. Allowed values: "same-origin", "same-site",
49
+ * "cross-origin" or false to disable. Defaults to "same-origin".
50
+ */
51
+ crossOriginResourcePolicy?: "same-origin" | "same-site" | "cross-origin" | (string & {}) | false;
36
52
  /**
37
53
  * Granular CSP directive overrides. Values are arrays of directive tokens, e.g.: {
38
54
  * "frame-ancestors": ["'self'"] }
@@ -0,0 +1,2 @@
1
+ import { t as SecurityOptions } from "./types-t69O3m_O.mjs";
2
+ export { SecurityOptions };
package/dist/types.mjs ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,26 @@
1
+ import { t as SecurityOptions } from "./types-t69O3m_O.mjs";
2
+ //#region src/vite.d.ts
3
+ export interface RimelightSecurityPlugin {
4
+ name: string;
5
+ enforce?: "pre" | "post";
6
+ resolveId?: (id: string) => string | null | undefined;
7
+ load?: (id: string) => string | null | undefined;
8
+ buildStart?: () => Promise<void> | void;
9
+ generateBundle?: (options: any, bundle: Record<string, any>) => Promise<void> | void;
10
+ [key: string]: any;
11
+ }
12
+ export interface SecurityPluginOptions extends SecurityOptions {
13
+ /**
14
+ * Additional external script/style URLs to fetch and hash for SRI at build time.
15
+ */
16
+ resources?: string[];
17
+ }
18
+ /**
19
+ * Pure Vite Security & SRI Plugin. 1. Accepts framework-agnostic security options (domain, imgSrc,
20
+ * etc.) and exposes virtual:rimelight-security-config. 2. Fetches and hashes external 3rd-party
21
+ * scripts/styles at build time. 3. Emits SRI hashes for locally bundled JS/CSS chunks
22
+ * (Nuxt-security style). 4. Exposes virtual:sri-manifest with a unified URL -> hash mapping.
23
+ */
24
+ export declare function security(options?: SecurityPluginOptions): RimelightSecurityPlugin;
25
+ //#endregion
26
+ export { security as sri };
package/dist/vite.mjs ADDED
@@ -0,0 +1,65 @@
1
+ import { createHash } from "node:crypto";
2
+ //#region src/vite.ts
3
+ const DEFAULT_RESOURCES = ["https://betterlytics.io/analytics.js"];
4
+ function hashContent(buffer) {
5
+ return `sha384-${createHash("sha384").update(buffer).digest("base64")}`;
6
+ }
7
+ /**
8
+ * Pure Vite Security & SRI Plugin. 1. Accepts framework-agnostic security options (domain, imgSrc,
9
+ * etc.) and exposes virtual:rimelight-security-config. 2. Fetches and hashes external 3rd-party
10
+ * scripts/styles at build time. 3. Emits SRI hashes for locally bundled JS/CSS chunks
11
+ * (Nuxt-security style). 4. Exposes virtual:sri-manifest with a unified URL -> hash mapping.
12
+ */
13
+ function security(options = {}) {
14
+ const sriManifest = {};
15
+ const externalUrls = Array.from(/* @__PURE__ */ new Set([...DEFAULT_RESOURCES, ...options.resources || []]));
16
+ const sriVirtualId = "virtual:sri-manifest";
17
+ const resolvedSriVirtualId = "\0" + sriVirtualId;
18
+ const configVirtualId = "virtual:rimelight-security-config";
19
+ const resolvedConfigVirtualId = "\0" + configVirtualId;
20
+ return {
21
+ name: "vite-plugin-rimelight-security",
22
+ enforce: "post",
23
+ async buildStart() {
24
+ if (process.env["NODE_ENV"] === "development") return;
25
+ await Promise.all(externalUrls.map(async (url) => {
26
+ if (!url.startsWith("https://")) return;
27
+ try {
28
+ const parsed = new URL(url);
29
+ if (parsed.hostname.includes("*")) return;
30
+ const pathname = parsed.pathname;
31
+ if (pathname === "/" || pathname === "" || pathname.endsWith("/")) return;
32
+ const response = await fetch(url);
33
+ if (!response.ok) return;
34
+ const buffer = await response.arrayBuffer();
35
+ sriManifest[url] = hashContent(buffer);
36
+ } catch {}
37
+ }));
38
+ },
39
+ generateBundle(_outputOptions, bundle) {
40
+ for (const [fileName, chunk] of Object.entries(bundle)) {
41
+ if (!fileName.endsWith(".js") && !fileName.endsWith(".css")) continue;
42
+ let content;
43
+ if (chunk.type === "chunk") content = chunk.code;
44
+ else if (chunk.type === "asset") content = chunk.source;
45
+ if (content) {
46
+ const hash = hashContent(content);
47
+ sriManifest[`/${fileName.replace(/^\/+/, "")}`] = hash;
48
+ sriManifest[fileName] = hash;
49
+ }
50
+ }
51
+ },
52
+ resolveId(id) {
53
+ if (id === sriVirtualId) return resolvedSriVirtualId;
54
+ if (id === configVirtualId) return resolvedConfigVirtualId;
55
+ return null;
56
+ },
57
+ load(id) {
58
+ if (id === resolvedSriVirtualId) return `export const manifest = ${JSON.stringify(sriManifest)};`;
59
+ if (id === resolvedConfigVirtualId) return `export const config = ${JSON.stringify(options)};`;
60
+ return null;
61
+ }
62
+ };
63
+ }
64
+ //#endregion
65
+ export { security, security as sri };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimelight/security",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "private": false,
5
5
  "description": "Rimelight Entertainment's Security Package",
6
6
  "homepage": "https://rimelight.com/docs",
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "devDependencies": {
42
42
  "@astrojs/check": "0.9.10",
43
- "@rimelight/config": "0.0.5",
43
+ "@rimelight/config": "0.0.6",
44
44
  "astro": "7.3.2",
45
45
  "typescript": "6.0.3"
46
46
  },
@@ -48,7 +48,7 @@
48
48
  "astro": ">=7.0.0"
49
49
  },
50
50
  "engines": {
51
- "node": ">=26.7.0"
51
+ "node": ">=26.8.2"
52
52
  },
53
53
  "scripts": {
54
54
  "build": "vp pack",
@@ -1,8 +0,0 @@
1
- import { t as SecurityOptions } from "./types-Bg8w94Fn.mjs";
2
- //#region src/csp.d.ts
3
- /**
4
- * Builds the Content-Security-Policy header string dynamically for a given request nonce.
5
- */
6
- declare function buildCspHeader(options?: SecurityOptions, nonce?: string): string;
7
- //#endregion
8
- export { buildCspHeader as t };