@rimelight/security 0.0.8 → 0.0.10
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/dist/config/index.d.mts +2 -2
- package/dist/config/index.mjs +2 -1
- package/dist/csp.d.mts +7 -0
- package/dist/{csp-DATUs3cP.mjs → csp.mjs} +2 -1
- package/dist/index.d.mts +4 -27
- package/dist/index.mjs +3 -65
- package/dist/middleware/dev-only.d.mts +13 -0
- package/dist/middleware/dev-only.mjs +17 -0
- package/dist/middleware/index.d.mts +3 -21
- package/dist/middleware/index.mjs +2 -195
- package/dist/middleware/security.d.mts +8 -0
- package/dist/middleware/security.mjs +184 -0
- package/dist/{types-Bg8w94Fn.d.mts → types-t69O3m_O.d.mts} +16 -0
- package/dist/types.d.mts +2 -0
- package/dist/types.mjs +1 -0
- package/dist/vite.d.mts +26 -0
- package/dist/vite.mjs +65 -0
- package/package.json +3 -3
- package/dist/csp-ESHHTg-n.d.mts +0 -8
package/dist/config/index.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as SecurityOptions } from "../types-
|
|
2
|
-
import {
|
|
1
|
+
import { t as SecurityOptions } from "../types-t69O3m_O.mjs";
|
|
2
|
+
import { buildCspHeader } from "../csp.mjs";
|
|
3
3
|
export { SecurityOptions, buildCspHeader };
|
package/dist/config/index.mjs
CHANGED
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
|
|
64
|
+
export { buildCspHeader };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,27 +1,4 @@
|
|
|
1
|
-
import { t as SecurityOptions } from "./types-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
export
|
|
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
|
|
2
|
-
import {
|
|
3
|
-
|
|
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 {
|
|
2
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
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'"] }
|
package/dist/types.d.mts
ADDED
package/dist/types.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/vite.d.mts
ADDED
|
@@ -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.
|
|
3
|
+
"version": "0.0.10",
|
|
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.
|
|
43
|
+
"@rimelight/config": "0.0.7",
|
|
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.
|
|
51
|
+
"node": ">=26.8.2"
|
|
52
52
|
},
|
|
53
53
|
"scripts": {
|
|
54
54
|
"build": "vp pack",
|
package/dist/csp-ESHHTg-n.d.mts
DELETED
|
@@ -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 };
|