@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.
Files changed (44) hide show
  1. package/package.json +24 -20
  2. package/src/components/astro/RLAButton.astro +58 -26
  3. package/src/components/astro/RLACarousel.astro +202 -26
  4. package/src/components/astro/RLALink.astro +10 -12
  5. package/src/components/astro/RLALocaleSelector.astro +2 -1
  6. package/src/components/astro/RLAPopover.astro +99 -5
  7. package/src/components/astro/RLAScrollToTop.astro +10 -19
  8. package/src/components/astro/RLASelect.astro +52 -47
  9. package/src/components/astro/RLASidebar.astro +1 -1
  10. package/src/components/astro/RLATableOfContents.astro +76 -27
  11. package/src/components/astro/RLAThemeSelector.astro +1 -0
  12. package/src/components/astro/RLAToast.astro +2 -2
  13. package/src/components/vue/RLVScrollToTop.vue +12 -10
  14. package/src/components/vue/RLVToast.vue +1 -1
  15. package/src/components/vue/RLVToaster.vue +15 -3
  16. package/src/config/index.ts +0 -1
  17. package/src/integrations/index.ts +0 -1
  18. package/src/integrations/ui.ts +12 -11
  19. package/src/themes/link-group.theme.ts +1 -1
  20. package/src/themes/locale-selector.theme.ts +1 -1
  21. package/src/themes/popover.theme.ts +2 -5
  22. package/src/themes/select.theme.ts +1 -1
  23. package/src/themes/toast.theme.ts +9 -3
  24. package/src/types/components/button.ts +2 -1
  25. package/src/types/components/link.ts +0 -10
  26. package/src/types/components/sidebar.ts +1 -1
  27. package/src/types/components/table-of-contents.ts +5 -0
  28. package/src/types/components/toast.ts +1 -1
  29. package/src/utils/index.ts +2 -0
  30. package/src/utils/merge.ts +62 -0
  31. package/src/utils/scroll.ts +41 -0
  32. package/src/utils/toast.ts +37 -0
  33. package/src/utils/useUi.ts +2 -2
  34. package/src/components/HttpObservatory.astro +0 -103
  35. package/src/components/LighthouseScores.astro +0 -204
  36. package/src/composables/index.ts +0 -4
  37. package/src/composables/useHeaderStore.ts +0 -14
  38. package/src/composables/useScrollToTop.ts +0 -62
  39. package/src/composables/useShortcuts.ts +0 -11
  40. package/src/composables/useToast.ts +0 -43
  41. package/src/config/security.ts +0 -227
  42. package/src/integrations/sri.ts +0 -92
  43. package/src/middleware/index.ts +0 -1
  44. package/src/middleware/security.ts +0 -210
@@ -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
- }
@@ -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
- }
@@ -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 &#39; or &#x27; to ') that Astro's native CSP generation mistakenly puts in the header
76
- cspHeader = cspHeader
77
- .replace(/&#0*39;/g, "'")
78
- .replace(/&#x0*27;/gi, "'")
79
- .replace(/&quot;/g, '"')
80
- .replace(/&amp;/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
- })