@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 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",
3
+ "version": "0.0.5",
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",
@@ -14,11 +29,18 @@
14
29
  "access": "public"
15
30
  },
16
31
  "devDependencies": {
17
- "astro": "7.1.3",
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.1"
38
+ "astro": ">=7.0.0"
39
+ },
40
+ "engines": {
41
+ "node": ">=26.7.0"
22
42
  },
23
- "packageManager": "pnpm@11.9.0"
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
+ }
@@ -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,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 (import.meta.env.PROD && proto === "http") {
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 (import.meta.env.DEV) {
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(/&#x0*27;/gi, "'")
123
138
  .replace(/&quot;/g, '"')
124
139
  .replace(/&amp;/g, "&")
125
- newHeaders.set("content-security-policy", cspHeader)
126
- }
127
140
 
128
- 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}' `)
129
143
 
130
- if (cspHeader && nonceValue) {
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
@@ -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
- }