@rimelight/security 0.0.3 → 0.0.5
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/README.md +19 -0
- package/package.json +28 -6
- package/src/components/SecurityHead.astro +117 -0
- package/src/middleware/dev-only.ts +16 -0
- package/src/middleware/index.ts +1 -0
- package/src/middleware/security.ts +51 -57
- package/src/env.d.ts +0 -5
- package/tsconfig.json +0 -17
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Rimelight Entertainment Workspace
|
|
2
|
+
|
|
3
|
+
## Structure
|
|
4
|
+
|
|
5
|
+
### Apps (`/packages`)
|
|
6
|
+
|
|
7
|
+
- **`rimelight.com`**: The main company website.
|
|
8
|
+
- **`starter.rimelight.com`**: Our standardized starter template for Astro websites.
|
|
9
|
+
|
|
10
|
+
### Packages (`/packages`)
|
|
11
|
+
|
|
12
|
+
- **`@rimelight/auth`**: Authentication and authorization utilities for the Rimelight ecosystem.
|
|
13
|
+
- **`@rimelight/cli`**: The command line interface for managing Rimelight projects.
|
|
14
|
+
- **`@rimelight/cms`**: Enterprise content management, block rendering, and wiki engine.
|
|
15
|
+
- **`@rimelight/docs`**: Documentation components and utilities.
|
|
16
|
+
- **`@rimelight/i18n`**: Internationalization and localization tools.
|
|
17
|
+
- **`@rimelight/security`**: Astro security integration (CSP, SRI, and more).
|
|
18
|
+
- **`@rimelight/seo`**: SEO utilities including sitemap, robots, and meta components.
|
|
19
|
+
- **`@rimelight/ui`**: Our component library used in all our web projects.
|
package/package.json
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rimelight/security",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "Rimelight Entertainment Security
|
|
5
|
+
"description": "Rimelight Entertainment's Security Package",
|
|
6
|
+
"homepage": "https://rimelight.com/docs",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/Rimelight-Entertainment/rimelight/issues"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"author": {
|
|
12
|
+
"name": "Rimelight Entertainment"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/Rimelight-Entertainment/rimelight.git"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
6
21
|
"type": "module",
|
|
7
22
|
"exports": {
|
|
8
23
|
".": "./src/integrations/index.ts",
|
|
@@ -14,11 +29,18 @@
|
|
|
14
29
|
"access": "public"
|
|
15
30
|
},
|
|
16
31
|
"devDependencies": {
|
|
17
|
-
"
|
|
32
|
+
"@astrojs/check": "0.9.10",
|
|
33
|
+
"@rimelight/config": "0.0.3",
|
|
34
|
+
"astro": "7.2.7",
|
|
18
35
|
"typescript": "6.0.3"
|
|
19
36
|
},
|
|
20
37
|
"peerDependencies": {
|
|
21
|
-
"astro": ">=7.0.
|
|
38
|
+
"astro": ">=7.0.0"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=26.7.0"
|
|
22
42
|
},
|
|
23
|
-
"
|
|
24
|
-
|
|
43
|
+
"scripts": {
|
|
44
|
+
"check": "vp check --fix && astro check"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* SecurityHead — Security infrastructure scripts for CSP Nonce patching & Trusted Types.
|
|
4
|
+
* Provided by @rimelight/security.
|
|
5
|
+
*/
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<!-- Trusted Types Policy -->
|
|
9
|
+
<script>
|
|
10
|
+
(function() {
|
|
11
|
+
const w = window as any;
|
|
12
|
+
if (w.trustedTypes && w.trustedTypes.createPolicy) {
|
|
13
|
+
w.trustedTypes.createPolicy('default', {
|
|
14
|
+
createHTML: function(input: string) { return input; },
|
|
15
|
+
createScript: function(input: string) { return input; },
|
|
16
|
+
createScriptURL: function(input: string) { return input; }
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
})();
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<!-- CSP Nonce Patching -->
|
|
23
|
+
<script>
|
|
24
|
+
(function() {
|
|
25
|
+
const w = window as any;
|
|
26
|
+
const doc = document as any;
|
|
27
|
+
const elemProto = Element.prototype as any;
|
|
28
|
+
|
|
29
|
+
if (!w.initialCspNonce) {
|
|
30
|
+
w.initialCspNonce = document.querySelector('meta[name="csp-nonce"]')?.getAttribute('content') ||
|
|
31
|
+
Array.from(document.querySelectorAll('script')).find(function(s: any) { return s.nonce; })?.nonce ||
|
|
32
|
+
document.querySelector('script[nonce]')?.getAttribute('nonce') ||
|
|
33
|
+
document.querySelector('style[nonce]')?.getAttribute('nonce');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
var orig = doc.createElement;
|
|
37
|
+
doc.createElement = function(tag: string, opts?: ElementCreationOptions) {
|
|
38
|
+
var el = orig.call(document, tag, opts);
|
|
39
|
+
var lowTag = tag.toLowerCase();
|
|
40
|
+
if (lowTag === 'script' || lowTag === 'style') {
|
|
41
|
+
var nonce = w.initialCspNonce;
|
|
42
|
+
if (nonce) {
|
|
43
|
+
el.setAttribute('nonce', nonce);
|
|
44
|
+
el.nonce = nonce;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return el;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
var origAppend = elemProto.appendChild;
|
|
51
|
+
elemProto.appendChild = function(child: any) {
|
|
52
|
+
if (child && (child.tagName === 'SCRIPT' || child.tagName === 'STYLE')) {
|
|
53
|
+
var nonce = w.initialCspNonce;
|
|
54
|
+
if (nonce && !child.getAttribute('nonce')) {
|
|
55
|
+
child.setAttribute('nonce', nonce);
|
|
56
|
+
child.nonce = nonce;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return origAppend.apply(this, [child]);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
var origInsert = elemProto.insertBefore;
|
|
63
|
+
elemProto.insertBefore = function(newChild: any, refChild: any) {
|
|
64
|
+
if (newChild && (newChild.tagName === 'SCRIPT' || newChild.tagName === 'STYLE')) {
|
|
65
|
+
var nonce = w.initialCspNonce;
|
|
66
|
+
if (nonce && !newChild.getAttribute('nonce')) {
|
|
67
|
+
newChild.setAttribute('nonce', nonce);
|
|
68
|
+
newChild.nonce = nonce;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return origInsert.apply(this, [newChild, refChild]);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
var origAppendMethod = elemProto.append;
|
|
75
|
+
if (origAppendMethod) {
|
|
76
|
+
elemProto.append = function(...nodes: (string | Node)[]) {
|
|
77
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
78
|
+
var arg = nodes[i] as any;
|
|
79
|
+
if (arg && (arg.tagName === 'SCRIPT' || arg.tagName === 'STYLE')) {
|
|
80
|
+
var nonce = w.initialCspNonce;
|
|
81
|
+
if (nonce && !arg.getAttribute('nonce')) {
|
|
82
|
+
arg.setAttribute('nonce', nonce);
|
|
83
|
+
arg.nonce = nonce;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return origAppendMethod.apply(this, nodes);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
var origPrependMethod = elemProto.prepend;
|
|
92
|
+
if (origPrependMethod) {
|
|
93
|
+
elemProto.prepend = function(...nodes: (string | Node)[]) {
|
|
94
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
95
|
+
var arg = nodes[i] as any;
|
|
96
|
+
if (arg && (arg.tagName === 'SCRIPT' || arg.tagName === 'STYLE')) {
|
|
97
|
+
var nonce = w.initialCspNonce;
|
|
98
|
+
if (nonce && !arg.getAttribute('nonce')) {
|
|
99
|
+
arg.setAttribute('nonce', nonce);
|
|
100
|
+
arg.nonce = nonce;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return origPrependMethod.apply(this, nodes);
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
document.addEventListener('astro:before-swap', function(e: any) {
|
|
109
|
+
var incomingNonce =
|
|
110
|
+
e.newDocument?.querySelector('meta[name="csp-nonce"]')?.getAttribute('content') ||
|
|
111
|
+
Array.from((e.newDocument?.querySelectorAll('script[nonce]') || []) as any[]).find(function(s) { return s.getAttribute('nonce'); })?.getAttribute('nonce');
|
|
112
|
+
if (incomingNonce) {
|
|
113
|
+
w.initialCspNonce = incomingNonce;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
})();
|
|
117
|
+
</script>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hono middleware that blocks any path containing `/dev/` in non-development environments. Mount
|
|
3
|
+
* this early in your Hono app (before API routes and Astro pages) so it covers both `/dev/` page
|
|
4
|
+
* routes and `/api/dev/` API routes in a single place.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* // fetch.ts
|
|
8
|
+
* import { devOnly } from "@rimelight/security/middleware"
|
|
9
|
+
* app.use(devOnly)
|
|
10
|
+
*/
|
|
11
|
+
export const devOnly = async (c: any, next: any): Promise<Response> => {
|
|
12
|
+
if (!import.meta.env.DEV && c.req.path.includes("/dev/")) {
|
|
13
|
+
return c.notFound()
|
|
14
|
+
}
|
|
15
|
+
return next()
|
|
16
|
+
}
|
package/src/middleware/index.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
import type { APIContext, MiddlewareNext } from "astro"
|
|
1
2
|
import { defineMiddleware } from "astro:middleware"
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
// @ts-ignore virtual module resolved by SRI Vite plugin
|
|
5
|
+
import { manifest as sriManifest } from "virtual:sri-manifest"
|
|
6
|
+
|
|
7
|
+
function isManifestRecord(obj: unknown): obj is Record<string, string> {
|
|
8
|
+
return typeof obj === "object" && obj !== null && !Array.isArray(obj)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const manifest: Record<string, string> = isManifestRecord(sriManifest) ? sriManifest : {}
|
|
4
12
|
|
|
5
13
|
declare const HTMLRewriter: any
|
|
6
14
|
|
|
@@ -78,10 +86,12 @@ function addSecurityHeaders(response: Response, origin?: string): Response {
|
|
|
78
86
|
/**
|
|
79
87
|
* Security Middleware: Injects SRI integrity attributes and security headers.
|
|
80
88
|
*/
|
|
81
|
-
export const security = defineMiddleware(async (context, next) => {
|
|
89
|
+
export const security = defineMiddleware(async (context: APIContext, next: MiddlewareNext) => {
|
|
82
90
|
// HTTP to HTTPS redirect only in production based on x-forwarded-proto
|
|
91
|
+
const isProd = import.meta.env.PROD
|
|
92
|
+
const isDev = import.meta.env.DEV
|
|
83
93
|
const proto = context.request.headers.get("x-forwarded-proto")
|
|
84
|
-
if (
|
|
94
|
+
if (isProd && proto === "http") {
|
|
85
95
|
const httpsUrl = context.request.url.replace(/^http:/, "https:")
|
|
86
96
|
return Response.redirect(httpsUrl, 301)
|
|
87
97
|
}
|
|
@@ -94,7 +104,7 @@ export const security = defineMiddleware(async (context, next) => {
|
|
|
94
104
|
return addSecurityHeaders(response, context.url.origin)
|
|
95
105
|
}
|
|
96
106
|
|
|
97
|
-
if (
|
|
107
|
+
if (isDev) {
|
|
98
108
|
const cleanHeaders = new Headers(response.headers)
|
|
99
109
|
cleanHeaders.delete("content-security-policy")
|
|
100
110
|
let modifiedHtml = await response.text()
|
|
@@ -112,6 +122,11 @@ export const security = defineMiddleware(async (context, next) => {
|
|
|
112
122
|
)
|
|
113
123
|
}
|
|
114
124
|
|
|
125
|
+
// Generate a per-request nonce. Cloudflare reads 'nonce-*' from the CSP header and
|
|
126
|
+
// automatically applies it to any scripts it injects (e.g. challenge-platform).
|
|
127
|
+
// The nonce is also stamped on every <script> tag so Astro's own scripts still load.
|
|
128
|
+
const nonce = crypto.randomUUID().replace(/-/g, "")
|
|
129
|
+
|
|
115
130
|
let cspHeader = response.headers.get("content-security-policy")
|
|
116
131
|
const newHeaders = new Headers(response.headers)
|
|
117
132
|
|
|
@@ -122,24 +137,11 @@ export const security = defineMiddleware(async (context, next) => {
|
|
|
122
137
|
.replace(/�*27;/gi, "'")
|
|
123
138
|
.replace(/"/g, '"')
|
|
124
139
|
.replace(/&/g, "&")
|
|
125
|
-
newHeaders.set("content-security-policy", cspHeader)
|
|
126
|
-
}
|
|
127
140
|
|
|
128
|
-
|
|
141
|
+
// Inject the nonce into script-src (additive alongside existing hashes)
|
|
142
|
+
cspHeader = cspHeader.replace(/(script-src\s)/i, `$1'nonce-${nonce}' `)
|
|
129
143
|
|
|
130
|
-
|
|
131
|
-
let newCsp = cspHeader
|
|
132
|
-
if (newCsp.includes("script-src ")) {
|
|
133
|
-
newCsp = newCsp.replace("script-src ", `script-src 'nonce-${nonceValue}' `)
|
|
134
|
-
} else {
|
|
135
|
-
newCsp = newCsp + `; script-src 'nonce-${nonceValue}'`
|
|
136
|
-
}
|
|
137
|
-
if (newCsp.includes("style-src ")) {
|
|
138
|
-
newCsp = newCsp.replace("style-src ", `style-src 'nonce-${nonceValue}' `)
|
|
139
|
-
} else {
|
|
140
|
-
newCsp = newCsp + `; style-src 'nonce-${nonceValue}'`
|
|
141
|
-
}
|
|
142
|
-
newHeaders.set("content-security-policy", newCsp)
|
|
144
|
+
newHeaders.set("content-security-policy", cspHeader)
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
if (typeof HTMLRewriter !== "undefined") {
|
|
@@ -168,33 +170,27 @@ export const security = defineMiddleware(async (context, next) => {
|
|
|
168
170
|
})
|
|
169
171
|
}
|
|
170
172
|
|
|
173
|
+
// Stamp nonce on every <script> tag so Astro's own scripts are still allowed
|
|
174
|
+
rewriter = rewriter.on("script", {
|
|
175
|
+
element(el: any) {
|
|
176
|
+
el.setAttribute("nonce", nonce)
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
// Inject a <meta name="csp-nonce"> so client JS can reliably read the
|
|
181
|
+
// current page's nonce (especially after View Transition navigations).
|
|
182
|
+
rewriter = rewriter.on("head", {
|
|
183
|
+
element(el: any) {
|
|
184
|
+
el.append(`<meta name="csp-nonce" content="${nonce}">`, { html: true })
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
|
|
171
188
|
rewriter = rewriter.on('meta[http-equiv="content-security-policy" i]', {
|
|
172
189
|
element(el: any) {
|
|
173
190
|
el.remove()
|
|
174
191
|
}
|
|
175
192
|
})
|
|
176
193
|
|
|
177
|
-
if (nonceValue) {
|
|
178
|
-
rewriter = rewriter
|
|
179
|
-
.on("head", {
|
|
180
|
-
element(el: any) {
|
|
181
|
-
el.append(`<meta name="csp-nonce" content="${nonceValue}">`, { html: true })
|
|
182
|
-
}
|
|
183
|
-
})
|
|
184
|
-
.on("script", {
|
|
185
|
-
element(el: any) {
|
|
186
|
-
if (!el.getAttribute("nonce")) {
|
|
187
|
-
el.setAttribute("nonce", nonceValue)
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
})
|
|
191
|
-
.on("style", {
|
|
192
|
-
element(el: any) {
|
|
193
|
-
el.setAttribute("nonce", nonceValue)
|
|
194
|
-
}
|
|
195
|
-
})
|
|
196
|
-
}
|
|
197
|
-
|
|
198
194
|
return addSecurityHeaders(
|
|
199
195
|
rewriter.transform(
|
|
200
196
|
new Response(response.body, {
|
|
@@ -215,6 +211,19 @@ export const security = defineMiddleware(async (context, next) => {
|
|
|
215
211
|
""
|
|
216
212
|
)
|
|
217
213
|
|
|
214
|
+
// Inject a <meta name="csp-nonce"> into <head> so client JS can reliably
|
|
215
|
+
// read the current page's nonce after View Transition navigations.
|
|
216
|
+
modifiedHtml = modifiedHtml.replace(
|
|
217
|
+
/(<head(?:\s[^>]*)?>)/i,
|
|
218
|
+
`$1<meta name="csp-nonce" content="${nonce}">`
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
// Stamp nonce on every <script> tag (regex fallback path)
|
|
222
|
+
modifiedHtml = modifiedHtml.replace(
|
|
223
|
+
/<script(\s[^>]*)?>/gi,
|
|
224
|
+
(_match, attrs = "") => `<script${attrs} nonce="${nonce}">`
|
|
225
|
+
)
|
|
226
|
+
|
|
218
227
|
// Use pre-compiled regexes
|
|
219
228
|
for (const { hash, scriptRegex, linkRegex } of manifestRegexes) {
|
|
220
229
|
if (!hash) continue
|
|
@@ -230,21 +239,6 @@ export const security = defineMiddleware(async (context, next) => {
|
|
|
230
239
|
)
|
|
231
240
|
}
|
|
232
241
|
|
|
233
|
-
if (nonceValue) {
|
|
234
|
-
modifiedHtml = modifiedHtml.replace(
|
|
235
|
-
/<head([^>]*)>/i,
|
|
236
|
-
`<head$1>\n<meta name="csp-nonce" content="${nonceValue}">`
|
|
237
|
-
)
|
|
238
|
-
modifiedHtml = modifiedHtml.replace(
|
|
239
|
-
/<script(?![^>]*nonce=)([^>]*)>/gi,
|
|
240
|
-
`<script$1 nonce="${nonceValue}">`
|
|
241
|
-
)
|
|
242
|
-
modifiedHtml = modifiedHtml.replace(
|
|
243
|
-
/<style(?![^>]*nonce=)([^>]*)>/gi,
|
|
244
|
-
`<style$1 nonce="${nonceValue}">`
|
|
245
|
-
)
|
|
246
|
-
}
|
|
247
|
-
|
|
248
242
|
return addSecurityHeaders(
|
|
249
243
|
new Response(modifiedHtml, {
|
|
250
244
|
status: response.status,
|
package/src/env.d.ts
DELETED
package/tsconfig.json
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "astro/tsconfigs/strictest",
|
|
3
|
-
"include": [".astro/types.d.ts", "**/*"],
|
|
4
|
-
"exclude": ["dist"],
|
|
5
|
-
"compilerOptions": {
|
|
6
|
-
"plugins": [
|
|
7
|
-
{
|
|
8
|
-
"name": "@astrojs/ts-plugin"
|
|
9
|
-
}
|
|
10
|
-
],
|
|
11
|
-
"paths": {
|
|
12
|
-
"@/*": ["./src/*"]
|
|
13
|
-
},
|
|
14
|
-
"allowArbitraryExtensions": true,
|
|
15
|
-
"jsx": "preserve"
|
|
16
|
-
}
|
|
17
|
-
}
|