@rimelight/security 0.0.2 → 0.0.4

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 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.2",
3
+ "version": "0.0.4",
4
4
  "private": false,
5
- "description": "Rimelight Entertainment Security Middleware and Utilities.",
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",
@@ -13,12 +28,20 @@
13
28
  "publishConfig": {
14
29
  "access": "public"
15
30
  },
31
+ "scripts": {
32
+ "check": "pnpm audit --audit-level=moderate && vp check --fix && astro check"
33
+ },
16
34
  "devDependencies": {
17
- "astro": "7.1.0",
35
+ "@astrojs/check": "0.9.10",
36
+ "@rimelight/config": "workspace:*",
37
+ "astro": "7.2.2",
18
38
  "typescript": "6.0.3"
19
39
  },
20
40
  "peerDependencies": {
21
- "astro": ">=7.0.1"
41
+ "astro": ">=7.0.0"
42
+ },
43
+ "engines": {
44
+ "node": ">=26.7.0"
22
45
  },
23
- "packageManager": "pnpm@11.9.0"
46
+ "packageManager": "pnpm@11.22.0"
24
47
  }
@@ -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
+ }
@@ -1 +1,2 @@
1
1
  export * from "./security"
2
+ export * from "./dev-only"
@@ -1,6 +1,14 @@
1
+ import type { APIContext, MiddlewareNext } from "astro"
1
2
  import { defineMiddleware } from "astro:middleware"
2
- // @ts-ignore - virtual module generated by the SRI integration
3
- import { manifest } from "virtual:sri-manifest"
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,9 +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) => {
82
- // HTTP to HTTPS redirect only in production
83
- if (import.meta.env.PROD && context.request.url.startsWith("http://")) {
89
+ export const security = defineMiddleware(async (context: APIContext, next: MiddlewareNext) => {
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
93
+ const proto = context.request.headers.get("x-forwarded-proto")
94
+ if (isProd && proto === "http") {
84
95
  const httpsUrl = context.request.url.replace(/^http:/, "https:")
85
96
  return Response.redirect(httpsUrl, 301)
86
97
  }
@@ -93,7 +104,7 @@ export const security = defineMiddleware(async (context, next) => {
93
104
  return addSecurityHeaders(response, context.url.origin)
94
105
  }
95
106
 
96
- if (import.meta.env.DEV) {
107
+ if (isDev) {
97
108
  const cleanHeaders = new Headers(response.headers)
98
109
  cleanHeaders.delete("content-security-policy")
99
110
  let modifiedHtml = await response.text()
@@ -111,6 +122,11 @@ export const security = defineMiddleware(async (context, next) => {
111
122
  )
112
123
  }
113
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
+
114
130
  let cspHeader = response.headers.get("content-security-policy")
115
131
  const newHeaders = new Headers(response.headers)
116
132
 
@@ -121,24 +137,11 @@ export const security = defineMiddleware(async (context, next) => {
121
137
  .replace(/&#x0*27;/gi, "'")
122
138
  .replace(/&quot;/g, '"')
123
139
  .replace(/&amp;/g, "&")
124
- newHeaders.set("content-security-policy", cspHeader)
125
- }
126
140
 
127
- const nonceValue = cspHeader ? crypto.randomUUID().replace(/-/g, "") : null
141
+ // Inject the nonce into script-src (additive alongside existing hashes)
142
+ cspHeader = cspHeader.replace(/(script-src\s)/i, `$1'nonce-${nonce}' `)
128
143
 
129
- if (cspHeader && nonceValue) {
130
- let newCsp = cspHeader
131
- if (newCsp.includes("script-src ")) {
132
- newCsp = newCsp.replace("script-src ", `script-src 'nonce-${nonceValue}' `)
133
- } else {
134
- newCsp = newCsp + `; script-src 'nonce-${nonceValue}'`
135
- }
136
- if (newCsp.includes("style-src ")) {
137
- newCsp = newCsp.replace("style-src ", `style-src 'nonce-${nonceValue}' `)
138
- } else {
139
- newCsp = newCsp + `; style-src 'nonce-${nonceValue}'`
140
- }
141
- newHeaders.set("content-security-policy", newCsp)
144
+ newHeaders.set("content-security-policy", cspHeader)
142
145
  }
143
146
 
144
147
  if (typeof HTMLRewriter !== "undefined") {
@@ -167,33 +170,27 @@ export const security = defineMiddleware(async (context, next) => {
167
170
  })
168
171
  }
169
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
+
170
188
  rewriter = rewriter.on('meta[http-equiv="content-security-policy" i]', {
171
189
  element(el: any) {
172
190
  el.remove()
173
191
  }
174
192
  })
175
193
 
176
- if (nonceValue) {
177
- rewriter = rewriter
178
- .on("head", {
179
- element(el: any) {
180
- el.append(`<meta name="csp-nonce" content="${nonceValue}">`, { html: true })
181
- }
182
- })
183
- .on("script", {
184
- element(el: any) {
185
- if (!el.getAttribute("nonce")) {
186
- el.setAttribute("nonce", nonceValue)
187
- }
188
- }
189
- })
190
- .on("style", {
191
- element(el: any) {
192
- el.setAttribute("nonce", nonceValue)
193
- }
194
- })
195
- }
196
-
197
194
  return addSecurityHeaders(
198
195
  rewriter.transform(
199
196
  new Response(response.body, {
@@ -214,6 +211,19 @@ export const security = defineMiddleware(async (context, next) => {
214
211
  ""
215
212
  )
216
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
+
217
227
  // Use pre-compiled regexes
218
228
  for (const { hash, scriptRegex, linkRegex } of manifestRegexes) {
219
229
  if (!hash) continue
@@ -229,21 +239,6 @@ export const security = defineMiddleware(async (context, next) => {
229
239
  )
230
240
  }
231
241
 
232
- if (nonceValue) {
233
- modifiedHtml = modifiedHtml.replace(
234
- /<head([^>]*)>/i,
235
- `<head$1>\n<meta name="csp-nonce" content="${nonceValue}">`
236
- )
237
- modifiedHtml = modifiedHtml.replace(
238
- /<script(?![^>]*nonce=)([^>]*)>/gi,
239
- `<script$1 nonce="${nonceValue}">`
240
- )
241
- modifiedHtml = modifiedHtml.replace(
242
- /<style(?![^>]*nonce=)([^>]*)>/gi,
243
- `<style$1 nonce="${nonceValue}">`
244
- )
245
- }
246
-
247
242
  return addSecurityHeaders(
248
243
  new Response(modifiedHtml, {
249
244
  status: response.status,
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Rimelight Entertainment
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
package/src/env.d.ts DELETED
@@ -1,5 +0,0 @@
1
- /// <reference types="astro/client" />
2
-
3
- declare module "virtual:sri-manifest" {
4
- export const manifest: Record<string, string>
5
- }
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
- }