@rimelight/ui 0.0.36 → 0.0.37
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/package.json +24 -20
- package/src/components/astro/RLAButton.astro +58 -26
- package/src/components/astro/RLACarousel.astro +202 -26
- package/src/components/astro/RLALink.astro +10 -12
- package/src/components/astro/RLALocaleSelector.astro +2 -1
- package/src/components/astro/RLAPopover.astro +99 -5
- package/src/components/astro/RLAScrollToTop.astro +10 -19
- package/src/components/astro/RLASelect.astro +52 -47
- package/src/components/astro/RLASidebar.astro +1 -1
- package/src/components/astro/RLATableOfContents.astro +76 -27
- package/src/components/astro/RLAThemeSelector.astro +1 -0
- package/src/components/astro/RLAToast.astro +2 -2
- package/src/components/vue/RLVScrollToTop.vue +12 -10
- package/src/components/vue/RLVToast.vue +1 -1
- package/src/components/vue/RLVToaster.vue +15 -3
- package/src/config/index.ts +0 -1
- package/src/integrations/index.ts +0 -1
- package/src/integrations/ui.ts +12 -11
- package/src/themes/link-group.theme.ts +1 -1
- package/src/themes/locale-selector.theme.ts +1 -1
- package/src/themes/popover.theme.ts +2 -5
- package/src/themes/select.theme.ts +1 -1
- package/src/themes/toast.theme.ts +9 -3
- package/src/types/components/button.ts +2 -1
- package/src/types/components/link.ts +0 -10
- package/src/types/components/sidebar.ts +1 -1
- package/src/types/components/table-of-contents.ts +5 -0
- package/src/types/components/toast.ts +1 -1
- package/src/utils/index.ts +2 -0
- package/src/utils/merge.ts +62 -0
- package/src/utils/scroll.ts +41 -0
- package/src/utils/toast.ts +37 -0
- package/src/utils/useUi.ts +2 -2
- package/src/components/HttpObservatory.astro +0 -103
- package/src/components/LighthouseScores.astro +0 -204
- package/src/composables/index.ts +0 -4
- package/src/composables/useHeaderStore.ts +0 -14
- package/src/composables/useScrollToTop.ts +0 -62
- package/src/composables/useShortcuts.ts +0 -11
- package/src/composables/useToast.ts +0 -43
- package/src/config/security.ts +0 -227
- package/src/integrations/sri.ts +0 -92
- package/src/middleware/index.ts +0 -1
- package/src/middleware/security.ts +0 -210
package/src/config/security.ts
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
import type { AstroUserConfig } from "astro"
|
|
2
|
-
|
|
3
|
-
export type AstroSecurityConfig = NonNullable<AstroUserConfig["security"]>
|
|
4
|
-
export type AstroCspConfig = Exclude<NonNullable<AstroSecurityConfig["csp"]>, boolean>
|
|
5
|
-
|
|
6
|
-
export interface SecurityConfigOptions {
|
|
7
|
-
/**
|
|
8
|
-
* The base domain name for this project (e.g., "starter.rimelight.com") used to autoconfigure
|
|
9
|
-
* standard allowedDomains and img-src CDN targets.
|
|
10
|
-
*/
|
|
11
|
-
domain: string
|
|
12
|
-
/**
|
|
13
|
-
* Additional Allowed Domains (appended to the default wildcard subdomains of `domain`)
|
|
14
|
-
*/
|
|
15
|
-
allowedDomains?: AstroSecurityConfig["allowedDomains"]
|
|
16
|
-
/**
|
|
17
|
-
* Additional sources to allow for img-src directive
|
|
18
|
-
*/
|
|
19
|
-
imgSrc?: string[]
|
|
20
|
-
/**
|
|
21
|
-
* Additional sources to allow for connect-src directive
|
|
22
|
-
*/
|
|
23
|
-
connectSrc?: string[]
|
|
24
|
-
/**
|
|
25
|
-
* Additional sources to allow for frame-src directive
|
|
26
|
-
*/
|
|
27
|
-
frameSrc?: string[]
|
|
28
|
-
/**
|
|
29
|
-
* Additional script resources
|
|
30
|
-
*/
|
|
31
|
-
scriptResources?: string[]
|
|
32
|
-
/**
|
|
33
|
-
* Additional style resources
|
|
34
|
-
*/
|
|
35
|
-
styleResources?: string[]
|
|
36
|
-
/**
|
|
37
|
-
* Enables the 'require-trusted-types-for' directive
|
|
38
|
-
*/
|
|
39
|
-
trustedTypes?: boolean
|
|
40
|
-
/**
|
|
41
|
-
* Direct overrides for any security config fields
|
|
42
|
-
*/
|
|
43
|
-
overrides?: Partial<Omit<AstroSecurityConfig, "csp">> & {
|
|
44
|
-
csp?: Partial<AstroCspConfig>
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Generates a standard, strictly configured Security/CSP object for Astro configurations with
|
|
50
|
-
* support for easy customization and additions per project.
|
|
51
|
-
*
|
|
52
|
-
* @example
|
|
53
|
-
* ;```ts
|
|
54
|
-
* // Default returned shape:
|
|
55
|
-
* {
|
|
56
|
-
* checkOrigin: true,
|
|
57
|
-
* allowedDomains: [
|
|
58
|
-
* {
|
|
59
|
-
* hostname: "**.domain.com",
|
|
60
|
-
* protocol: "https"
|
|
61
|
-
* }
|
|
62
|
-
* ],
|
|
63
|
-
* csp: {
|
|
64
|
-
* algorithm: "SHA-384",
|
|
65
|
-
* directives: [
|
|
66
|
-
* "default-src 'none'",
|
|
67
|
-
* "img-src 'self' data: https://cdn.domain.com https://i3.ytimg.com https://www.youtube.com https://www.youtube-nocookie.com",
|
|
68
|
-
* "font-src 'self'",
|
|
69
|
-
* "connect-src 'self' https://cloudflareinsights.com",
|
|
70
|
-
* "frame-ancestors 'none'",
|
|
71
|
-
* "frame-src https://www.youtube.com https://www.youtube-nocookie.com",
|
|
72
|
-
* "upgrade-insecure-requests",
|
|
73
|
-
* "base-uri 'self'",
|
|
74
|
-
* "form-action 'self'"
|
|
75
|
-
* ],
|
|
76
|
-
* scriptDirective: {
|
|
77
|
-
* resources: [
|
|
78
|
-
* "'self'",
|
|
79
|
-
* "https://static.cloudflareinsights.com",
|
|
80
|
-
* "https://betterlytics.io/analytics.js",
|
|
81
|
-
* "'inline-speculation-rules'"
|
|
82
|
-
* ]
|
|
83
|
-
* },
|
|
84
|
-
* styleDirective: {
|
|
85
|
-
* resources: [
|
|
86
|
-
* "'self'",
|
|
87
|
-
* "'sha256-SKuaOGnks7NAUq37nvw1PfGEE0lOAs5ERbiYRbGFIbw='"
|
|
88
|
-
* ]
|
|
89
|
-
* }
|
|
90
|
-
* }
|
|
91
|
-
* }
|
|
92
|
-
* ```
|
|
93
|
-
*/
|
|
94
|
-
export function defineSecurity(options: SecurityConfigOptions): AstroSecurityConfig {
|
|
95
|
-
const { domain, overrides } = options
|
|
96
|
-
const domainWithWildcard = `**.${domain}`
|
|
97
|
-
|
|
98
|
-
// 1. Build default allowed domains
|
|
99
|
-
const allowedDomains = [
|
|
100
|
-
{
|
|
101
|
-
hostname: domainWithWildcard,
|
|
102
|
-
protocol: "https" as const
|
|
103
|
-
},
|
|
104
|
-
...(options.allowedDomains || [])
|
|
105
|
-
]
|
|
106
|
-
|
|
107
|
-
// 2. Build CSP directives
|
|
108
|
-
const imgSources = Array.from(
|
|
109
|
-
new Set([
|
|
110
|
-
"'self'",
|
|
111
|
-
"data:",
|
|
112
|
-
`https://cdn.${domain}`,
|
|
113
|
-
"https://i3.ytimg.com",
|
|
114
|
-
"https://www.youtube.com",
|
|
115
|
-
"https://www.youtube-nocookie.com",
|
|
116
|
-
...(options.imgSrc || [])
|
|
117
|
-
])
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
const connectSources = Array.from(
|
|
121
|
-
new Set(["'self'", "https://cloudflareinsights.com", ...(options.connectSrc || [])])
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
const frameSources = Array.from(
|
|
125
|
-
new Set([
|
|
126
|
-
"https://www.youtube.com",
|
|
127
|
-
"https://www.youtube-nocookie.com",
|
|
128
|
-
...(options.frameSrc || [])
|
|
129
|
-
])
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
const baseDirectives: AstroCspConfig["directives"] = [
|
|
133
|
-
"default-src 'none'",
|
|
134
|
-
`img-src ${imgSources.join(" ")}`,
|
|
135
|
-
"font-src 'self'",
|
|
136
|
-
`connect-src ${connectSources.join(" ")}`,
|
|
137
|
-
"frame-ancestors 'none'",
|
|
138
|
-
`frame-src ${frameSources.join(" ")}`,
|
|
139
|
-
"upgrade-insecure-requests",
|
|
140
|
-
"base-uri 'self'",
|
|
141
|
-
"form-action 'self'",
|
|
142
|
-
"object-src 'none'",
|
|
143
|
-
"manifest-src 'self'",
|
|
144
|
-
...(options.trustedTypes ? ["require-trusted-types-for 'script'" as const] : [])
|
|
145
|
-
]
|
|
146
|
-
|
|
147
|
-
// 3. Build script & style directives
|
|
148
|
-
const scriptResources = Array.from(
|
|
149
|
-
new Set([
|
|
150
|
-
"'self'",
|
|
151
|
-
"https://static.cloudflareinsights.com",
|
|
152
|
-
"https://betterlytics.io/analytics.js",
|
|
153
|
-
"'inline-speculation-rules'",
|
|
154
|
-
// Astro ClientRouter (View Transitions)
|
|
155
|
-
"'sha256-ZuMSxilKU+4KIM8LWna4lqBEKzd6WaiAjz6gamhm7zM='",
|
|
156
|
-
...(options.scriptResources || [])
|
|
157
|
-
])
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
const styleResources = Array.from(
|
|
161
|
-
new Set([
|
|
162
|
-
"'self'",
|
|
163
|
-
// Astro ClientRouter (View Transitions)
|
|
164
|
-
"'sha256-SKuaOGnks7NAUq37nvw1PfGEE0lOAs5ERbiYRbGFIbw='",
|
|
165
|
-
...(options.styleResources || [])
|
|
166
|
-
])
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
// 4. Build base SecurityConfig
|
|
170
|
-
const securityConfig: AstroSecurityConfig = {
|
|
171
|
-
checkOrigin: true,
|
|
172
|
-
allowedDomains,
|
|
173
|
-
csp: {
|
|
174
|
-
algorithm: "SHA-384",
|
|
175
|
-
directives: baseDirectives,
|
|
176
|
-
scriptDirective: {
|
|
177
|
-
resources: scriptResources
|
|
178
|
-
},
|
|
179
|
-
styleDirective: {
|
|
180
|
-
resources: styleResources
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// 5. Merge overrides
|
|
186
|
-
if (overrides) {
|
|
187
|
-
const { csp, ...restOverrides } = overrides
|
|
188
|
-
Object.assign(securityConfig, restOverrides)
|
|
189
|
-
if (csp && typeof securityConfig.csp === "object" && securityConfig.csp !== null) {
|
|
190
|
-
const currentCsp = securityConfig.csp
|
|
191
|
-
const mergedCsp: AstroCspConfig = {
|
|
192
|
-
...currentCsp,
|
|
193
|
-
...csp
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (csp.scriptDirective || currentCsp.scriptDirective) {
|
|
197
|
-
mergedCsp.scriptDirective = {
|
|
198
|
-
...currentCsp.scriptDirective,
|
|
199
|
-
...csp.scriptDirective,
|
|
200
|
-
resources: Array.from(
|
|
201
|
-
new Set([
|
|
202
|
-
...(currentCsp.scriptDirective?.resources || []),
|
|
203
|
-
...(csp.scriptDirective?.resources || [])
|
|
204
|
-
])
|
|
205
|
-
)
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (csp.styleDirective || currentCsp.styleDirective) {
|
|
210
|
-
mergedCsp.styleDirective = {
|
|
211
|
-
...currentCsp.styleDirective,
|
|
212
|
-
...csp.styleDirective,
|
|
213
|
-
resources: Array.from(
|
|
214
|
-
new Set([
|
|
215
|
-
...(currentCsp.styleDirective?.resources || []),
|
|
216
|
-
...(csp.styleDirective?.resources || [])
|
|
217
|
-
])
|
|
218
|
-
)
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
securityConfig.csp = mergedCsp
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return securityConfig
|
|
227
|
-
}
|
package/src/integrations/sri.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import type { AstroIntegration } from "astro"
|
|
2
|
-
import { createHash } from "node:crypto"
|
|
3
|
-
|
|
4
|
-
interface SRIConfig {
|
|
5
|
-
security?: {
|
|
6
|
-
csp?: {
|
|
7
|
-
scriptDirective?: {
|
|
8
|
-
resources?: string[]
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function assertSRIConfig(obj: unknown): asserts obj is SRIConfig {
|
|
15
|
-
if (!obj || typeof obj !== "object") throw new Error("Expected config")
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Astro SRI Integration Fetches external scripts at build-time and generates hashes for the SSR
|
|
20
|
-
* middleware.
|
|
21
|
-
*/
|
|
22
|
-
export function sri(): AstroIntegration {
|
|
23
|
-
const sriManifest: Record<string, string> = {}
|
|
24
|
-
|
|
25
|
-
return {
|
|
26
|
-
name: "sri",
|
|
27
|
-
hooks: {
|
|
28
|
-
"astro:config:setup": async ({ updateConfig, config, logger }) => {
|
|
29
|
-
logger.info("Initializing SRI Discovery...")
|
|
30
|
-
|
|
31
|
-
// 1. Identify External Scripts from CSP config
|
|
32
|
-
// We look into the custom 'security' block in astro.config.mjs
|
|
33
|
-
assertSRIConfig(config)
|
|
34
|
-
|
|
35
|
-
const security = config.security
|
|
36
|
-
const externalUrls: string[] = []
|
|
37
|
-
|
|
38
|
-
if (security?.csp?.scriptDirective?.resources) {
|
|
39
|
-
for (const resource of security.csp.scriptDirective.resources) {
|
|
40
|
-
if (resource.startsWith("https://")) {
|
|
41
|
-
externalUrls.push(resource)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// 2. Fetch and Hash External Scripts
|
|
47
|
-
await Promise.all(
|
|
48
|
-
externalUrls.map(async (url) => {
|
|
49
|
-
try {
|
|
50
|
-
logger.info(`Fetching ${url} for SRI hashing...`)
|
|
51
|
-
const response = await fetch(url)
|
|
52
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
53
|
-
|
|
54
|
-
const buffer = await response.arrayBuffer()
|
|
55
|
-
const hash = createHash("sha384").update(Buffer.from(buffer)).digest("base64")
|
|
56
|
-
|
|
57
|
-
sriManifest[url] = `sha384-${hash}`
|
|
58
|
-
logger.info(`Successfully hashed ${url}`)
|
|
59
|
-
} catch (err: unknown) {
|
|
60
|
-
const message = err instanceof Error ? err.message : String(err)
|
|
61
|
-
logger.error(`Failed to fetch SRI for ${url}: ${message}`)
|
|
62
|
-
}
|
|
63
|
-
})
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
// 3. Provide hashes to the runtime via Vite's virtual module
|
|
67
|
-
const virtualModuleId = "virtual:sri-manifest"
|
|
68
|
-
const resolvedVirtualModuleId = "\0" + virtualModuleId
|
|
69
|
-
|
|
70
|
-
updateConfig({
|
|
71
|
-
vite: {
|
|
72
|
-
plugins: [
|
|
73
|
-
{
|
|
74
|
-
name: "vite-plugin-sri-manifest",
|
|
75
|
-
resolveId(id: string) {
|
|
76
|
-
if (id === virtualModuleId) return resolvedVirtualModuleId
|
|
77
|
-
return null
|
|
78
|
-
},
|
|
79
|
-
load(id: string) {
|
|
80
|
-
if (id === resolvedVirtualModuleId) {
|
|
81
|
-
return `export const manifest = ${JSON.stringify(sriManifest)};`
|
|
82
|
-
}
|
|
83
|
-
return null
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
]
|
|
87
|
-
}
|
|
88
|
-
})
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
package/src/middleware/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./security"
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
import { defineMiddleware } from "astro:middleware"
|
|
2
|
-
// @ts-ignore - virtual module generated by the SRI integration
|
|
3
|
-
import { manifest } from "virtual:sri-manifest"
|
|
4
|
-
|
|
5
|
-
declare const HTMLRewriter: any
|
|
6
|
-
|
|
7
|
-
function assertManifest(obj: unknown): asserts obj is Record<string, string> {
|
|
8
|
-
if (!obj || typeof obj !== "object") throw new Error("Expected manifest")
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function addSecurityHeaders(response: Response, origin?: string): Response {
|
|
12
|
-
const headers = new Headers(response.headers)
|
|
13
|
-
headers.set("Referrer-Policy", "strict-origin-when-cross-origin")
|
|
14
|
-
headers.set("X-Content-Type-Options", "nosniff")
|
|
15
|
-
headers.set("X-Frame-Options", "DENY")
|
|
16
|
-
headers.set("Cross-Origin-Resource-Policy", "same-origin")
|
|
17
|
-
headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")
|
|
18
|
-
headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=(), usb=()")
|
|
19
|
-
if (origin) {
|
|
20
|
-
headers.set(
|
|
21
|
-
"Link",
|
|
22
|
-
[
|
|
23
|
-
`<${origin}/sitemap-index.xml>; rel="sitemap"`,
|
|
24
|
-
`<${origin}/llms.txt>; rel="llms"`,
|
|
25
|
-
`<${origin}/.well-known/api-catalog>; rel="api-catalog"`,
|
|
26
|
-
`<${origin}/rss.xml>; rel="alternate"; type="application/rss+xml"`
|
|
27
|
-
].join(", ")
|
|
28
|
-
)
|
|
29
|
-
}
|
|
30
|
-
return new Response(response.body, {
|
|
31
|
-
status: response.status,
|
|
32
|
-
statusText: response.statusText,
|
|
33
|
-
headers
|
|
34
|
-
})
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Security Middleware: Injects SRI integrity attributes and security headers.
|
|
39
|
-
*/
|
|
40
|
-
export const security = defineMiddleware(async (context, next) => {
|
|
41
|
-
// HTTP to HTTPS redirect only in production
|
|
42
|
-
if (import.meta.env.PROD && context.request.url.startsWith("http://")) {
|
|
43
|
-
const httpsUrl = context.request.url.replace(/^http:/, "https:")
|
|
44
|
-
return Response.redirect(httpsUrl, 301)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const response = await next()
|
|
48
|
-
const contentType = response.headers.get("content-type") || ""
|
|
49
|
-
|
|
50
|
-
if (import.meta.env.DEV) {
|
|
51
|
-
if (!contentType.includes("text/html")) {
|
|
52
|
-
return addSecurityHeaders(response, context.url.origin)
|
|
53
|
-
}
|
|
54
|
-
const cleanHeaders = new Headers(response.headers)
|
|
55
|
-
cleanHeaders.delete("content-security-policy")
|
|
56
|
-
let modifiedHtml = await response.text()
|
|
57
|
-
modifiedHtml = modifiedHtml.replace(
|
|
58
|
-
/<meta\s[^>]*http-equiv\s*=\s*["']?content-security-policy[^>]*>/gi,
|
|
59
|
-
""
|
|
60
|
-
)
|
|
61
|
-
return addSecurityHeaders(
|
|
62
|
-
new Response(modifiedHtml, {
|
|
63
|
-
status: response.status,
|
|
64
|
-
statusText: response.statusText,
|
|
65
|
-
headers: cleanHeaders
|
|
66
|
-
}),
|
|
67
|
-
context.url.origin
|
|
68
|
-
)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
let cspHeader = response.headers.get("content-security-policy")
|
|
72
|
-
const newHeaders = new Headers(response.headers)
|
|
73
|
-
|
|
74
|
-
if (cspHeader) {
|
|
75
|
-
// Unescape HTML entities (like ' or ' to ') that Astro's native CSP generation mistakenly puts in the header
|
|
76
|
-
cspHeader = cspHeader
|
|
77
|
-
.replace(/�*39;/g, "'")
|
|
78
|
-
.replace(/�*27;/gi, "'")
|
|
79
|
-
.replace(/"/g, '"')
|
|
80
|
-
.replace(/&/g, "&")
|
|
81
|
-
newHeaders.set("content-security-policy", cspHeader)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const nonceValue = cspHeader ? crypto.randomUUID().replace(/-/g, "") : null
|
|
85
|
-
|
|
86
|
-
if (cspHeader && nonceValue) {
|
|
87
|
-
let newCsp = cspHeader
|
|
88
|
-
if (newCsp.includes("script-src ")) {
|
|
89
|
-
newCsp = newCsp.replace("script-src ", `script-src 'nonce-${nonceValue}' `)
|
|
90
|
-
} else {
|
|
91
|
-
newCsp = newCsp + `; script-src 'nonce-${nonceValue}'`
|
|
92
|
-
}
|
|
93
|
-
if (newCsp.includes("style-src ")) {
|
|
94
|
-
newCsp = newCsp.replace("style-src ", `style-src 'nonce-${nonceValue}' `)
|
|
95
|
-
} else {
|
|
96
|
-
newCsp = newCsp + `; style-src 'nonce-${nonceValue}'`
|
|
97
|
-
}
|
|
98
|
-
newHeaders.set("content-security-policy", newCsp)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (typeof HTMLRewriter !== "undefined") {
|
|
102
|
-
let rewriter = new HTMLRewriter()
|
|
103
|
-
.on("script[src]", {
|
|
104
|
-
element(el: any) {
|
|
105
|
-
const src = el.getAttribute("src")
|
|
106
|
-
if (src && manifest[src]) {
|
|
107
|
-
el.setAttribute("integrity", manifest[src])
|
|
108
|
-
el.setAttribute("crossorigin", "anonymous")
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
})
|
|
112
|
-
.on('link[rel="stylesheet"][href]', {
|
|
113
|
-
element(el: any) {
|
|
114
|
-
const href = el.getAttribute("href")
|
|
115
|
-
if (href && manifest[href]) {
|
|
116
|
-
el.setAttribute("integrity", manifest[href])
|
|
117
|
-
el.setAttribute("crossorigin", "anonymous")
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
})
|
|
121
|
-
.on('meta[http-equiv="content-security-policy" i]', {
|
|
122
|
-
element(el: any) {
|
|
123
|
-
el.remove()
|
|
124
|
-
}
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
if (nonceValue) {
|
|
128
|
-
rewriter = rewriter
|
|
129
|
-
.on("head", {
|
|
130
|
-
element(el: any) {
|
|
131
|
-
el.append(`<meta name="csp-nonce" content="${nonceValue}">`, { html: true })
|
|
132
|
-
}
|
|
133
|
-
})
|
|
134
|
-
.on("script", {
|
|
135
|
-
element(el: any) {
|
|
136
|
-
if (!el.getAttribute("nonce")) {
|
|
137
|
-
el.setAttribute("nonce", nonceValue)
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
})
|
|
141
|
-
.on("style", {
|
|
142
|
-
element(el: any) {
|
|
143
|
-
el.setAttribute("nonce", nonceValue)
|
|
144
|
-
}
|
|
145
|
-
})
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return addSecurityHeaders(
|
|
149
|
-
rewriter.transform(
|
|
150
|
-
new Response(response.body, {
|
|
151
|
-
status: response.status,
|
|
152
|
-
statusText: response.statusText,
|
|
153
|
-
headers: newHeaders
|
|
154
|
-
})
|
|
155
|
-
),
|
|
156
|
-
context.url.origin
|
|
157
|
-
)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
let modifiedHtml = await response.text()
|
|
161
|
-
|
|
162
|
-
// Remove Astro-injected CSP meta tag to prevent duplicate CSP headers and HTML escaping errors
|
|
163
|
-
modifiedHtml = modifiedHtml.replace(
|
|
164
|
-
/<meta\s[^>]*http-equiv\s*=\s*["']?content-security-policy[^>]*>/gi,
|
|
165
|
-
""
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
assertManifest(manifest)
|
|
169
|
-
|
|
170
|
-
const entries = Object.entries(manifest)
|
|
171
|
-
for (const [url, hash] of entries) {
|
|
172
|
-
if (!hash) continue
|
|
173
|
-
|
|
174
|
-
const scriptRegex = new RegExp(`(<script[^>]+src=["']${url}["'][^>]*)(/?>)`, "g")
|
|
175
|
-
modifiedHtml = modifiedHtml.replace(
|
|
176
|
-
scriptRegex,
|
|
177
|
-
`$1 integrity="${hash}" crossorigin="anonymous"$2`
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
const linkRegex = new RegExp(`(<link[^>]+href=["']${url}["'][^>]*)(/?>)`, "g")
|
|
181
|
-
modifiedHtml = modifiedHtml.replace(
|
|
182
|
-
linkRegex,
|
|
183
|
-
`$1 integrity="${hash}" crossorigin="anonymous"$2`
|
|
184
|
-
)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (nonceValue) {
|
|
188
|
-
modifiedHtml = modifiedHtml.replace(
|
|
189
|
-
/<head([^>]*)>/i,
|
|
190
|
-
`<head$1>\n<meta name="csp-nonce" content="${nonceValue}">`
|
|
191
|
-
)
|
|
192
|
-
modifiedHtml = modifiedHtml.replace(
|
|
193
|
-
/<script(?![^>]*nonce=)([^>]*)>/gi,
|
|
194
|
-
`<script$1 nonce="${nonceValue}">`
|
|
195
|
-
)
|
|
196
|
-
modifiedHtml = modifiedHtml.replace(
|
|
197
|
-
/<style(?![^>]*nonce=)([^>]*)>/gi,
|
|
198
|
-
`<style$1 nonce="${nonceValue}">`
|
|
199
|
-
)
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return addSecurityHeaders(
|
|
203
|
-
new Response(modifiedHtml, {
|
|
204
|
-
status: response.status,
|
|
205
|
-
statusText: response.statusText,
|
|
206
|
-
headers: newHeaders
|
|
207
|
-
}),
|
|
208
|
-
context.url.origin
|
|
209
|
-
)
|
|
210
|
-
})
|