@rimelight/ui 0.0.16 → 0.0.17

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimelight/ui",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "private": false,
5
5
  "description": "Rimelight Entertainment UI Library.",
6
6
  "homepage": "https://rimelight.com/docs",
@@ -1,92 +1,92 @@
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
+ 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,2 +1 @@
1
- export * from "./sri"
2
1
  export * from "./security"
@@ -1,22 +1,92 @@
1
1
  import { defineMiddleware } from "astro:middleware"
2
+ // @ts-ignore - virtual module generated by the SRI integration
3
+ import { manifest } from "virtual:sri-manifest"
2
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): Response {
12
+ const newResponse = new Response(response.body, response)
13
+ newResponse.headers.set("Referrer-Policy", "strict-origin-when-cross-origin")
14
+ newResponse.headers.set("X-Content-Type-Options", "nosniff")
15
+ newResponse.headers.set("X-Frame-Options", "DENY")
16
+ newResponse.headers.set("Cross-Origin-Resource-Policy", "same-origin")
17
+ newResponse.headers.set(
18
+ "Strict-Transport-Security",
19
+ "max-age=31536000; includeSubDomains; preload"
20
+ )
21
+ return newResponse
22
+ }
23
+
24
+ /**
25
+ * Security Middleware: Injects SRI integrity attributes and security headers.
26
+ */
3
27
  export const security = defineMiddleware(async (context, next) => {
4
- // HTTP to HTTPS redirect
5
- if (context.request.url.startsWith("http://")) {
28
+ // HTTP to HTTPS redirect only in production
29
+ if (import.meta.env.PROD && context.request.url.startsWith("http://")) {
6
30
  const httpsUrl = context.request.url.replace(/^http:/, "https:")
7
31
  return Response.redirect(httpsUrl, 301)
8
32
  }
9
33
 
10
34
  const response = await next()
35
+ const contentType = response.headers.get("content-type") || ""
11
36
 
12
- const newResponse = new Response(response.body, response)
37
+ if (!contentType.includes("text/html")) {
38
+ return addSecurityHeaders(response)
39
+ }
13
40
 
14
- // Standard Security Headers
15
- newResponse.headers.set("Referrer-Policy", "strict-origin-when-cross-origin")
16
- newResponse.headers.set("X-Content-Type-Options", "nosniff")
17
- newResponse.headers.set("X-Frame-Options", "DENY")
18
- newResponse.headers.set("Cross-Origin-Resource-Policy", "same-origin")
19
- newResponse.headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
41
+ if (typeof HTMLRewriter !== "undefined") {
42
+ return new HTMLRewriter()
43
+ .on("script[src]", {
44
+ element(el: any) {
45
+ const src = el.getAttribute("src")
46
+ if (src && manifest[src]) {
47
+ el.setAttribute("integrity", manifest[src])
48
+ el.setAttribute("crossorigin", "anonymous")
49
+ }
50
+ }
51
+ })
52
+ .on('link[rel="stylesheet"][href]', {
53
+ element(el: any) {
54
+ const href = el.getAttribute("href")
55
+ if (href && manifest[href]) {
56
+ el.setAttribute("integrity", manifest[href])
57
+ el.setAttribute("crossorigin", "anonymous")
58
+ }
59
+ }
60
+ })
61
+ .transform(response)
62
+ }
20
63
 
21
- return newResponse
22
- })
64
+ let modifiedHtml = await response.text()
65
+
66
+ assertManifest(manifest)
67
+
68
+ const entries = Object.entries(manifest)
69
+ for (const [url, hash] of entries) {
70
+ if (!hash) continue
71
+
72
+ const scriptRegex = new RegExp(`(<script[^>]+src=["']${url}["'][^>]*)(/?>)`, "g")
73
+ modifiedHtml = modifiedHtml.replace(
74
+ scriptRegex,
75
+ `$1 integrity="${hash}" crossorigin="anonymous"$2`
76
+ )
77
+
78
+ const linkRegex = new RegExp(`(<link[^>]+href=["']${url}["'][^>]*)(/?>)`, "g")
79
+ modifiedHtml = modifiedHtml.replace(
80
+ linkRegex,
81
+ `$1 integrity="${hash}" crossorigin="anonymous"$2`
82
+ )
83
+ }
84
+
85
+ return addSecurityHeaders(
86
+ new Response(modifiedHtml, {
87
+ status: response.status,
88
+ statusText: response.statusText,
89
+ headers: new Headers(response.headers)
90
+ })
91
+ )
92
+ })
@@ -1,132 +1,132 @@
1
- /// <reference path="./locals.d.ts" />
2
- import type { StarlightPlugin, StarlightUserConfig } from "@astrojs/starlight/types"
3
- import virtual from "vite-plugin-virtual"
4
- import path from "node:path"
5
- import { fileURLToPath } from "node:url"
6
- import { z } from "astro/zod"
7
-
8
- import {
9
- StarlightSidebarTopicsConfigSchema,
10
- StarlightSidebarTopicsOptionsSchema,
11
- type StarlightSidebarTopicsUserConfig,
12
- type StarlightSidebarTopicsUserOptions
13
- } from "./libs/config"
14
- import { throwPluginError } from "./libs/plugin"
15
- import { vitePluginStarlightSidebarTopics } from "./libs/vite"
16
-
17
- export type {
18
- StarlightSidebarTopicsConfig,
19
- StarlightSidebarTopicsUserConfig,
20
- StarlightSidebarTopicsOptions,
21
- StarlightSidebarTopicsUserOptions
22
- } from "./libs/config"
23
-
24
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
25
-
26
- export interface StarlightAddonsConfig {
27
- copy?: boolean
28
- share?: boolean
29
- packageManagers?: string[]
30
- // Merged config for Topics
31
- topics?: StarlightSidebarTopicsUserConfig
32
- topicOptions?: StarlightSidebarTopicsUserOptions
33
- }
34
-
35
- export default function starlightAddons(userConfig?: StarlightAddonsConfig): StarlightPlugin {
36
- // 1. Setup default and merged config for Addons
37
- const defaultAddonConfig: StarlightAddonsConfig = {
38
- copy: true,
39
- share: true,
40
- packageManagers: ["npm"]
41
- }
42
-
43
- const addonConfig = { ...defaultAddonConfig, ...userConfig }
44
-
45
- // 2. Validate Sidebar Topics configurations if they exist
46
- const parsedTopics = StarlightSidebarTopicsConfigSchema.safeParse(addonConfig.topics || [])
47
- if (!parsedTopics.success) {
48
- throwPluginError(
49
- `Invalid topics config: ${parsedTopics.error.issues.map((i) => i.message).join("\n")}`
50
- )
51
- }
52
-
53
- const parsedOptions = StarlightSidebarTopicsOptionsSchema.safeParse(addonConfig.topicOptions)
54
- if (!parsedOptions.success) {
55
- throwPluginError(`Invalid topic options: ${z.prettifyError(parsedOptions.error)}`)
56
- }
57
-
58
- const topicsData = parsedTopics.data
59
- const optionsData = parsedOptions.data
60
-
61
- return {
62
- name: "rimelight-ui-starlight-addons",
63
- hooks: {
64
- "config:setup"({
65
- addIntegration,
66
- updateConfig,
67
- logger,
68
- addRouteMiddleware,
69
- command,
70
- config: starlightConfig
71
- }) {
72
- // --- Sidebar Topics Logic ---
73
- if ((command === "dev" || command === "build") && topicsData.length > 0) {
74
- if (starlightConfig.sidebar) {
75
- throwPluginError(
76
- "Sidebar detected. To use topics, move your sidebar items into the topics config."
77
- )
78
- }
79
-
80
- const topicSidebar: StarlightUserConfig["sidebar"] = topicsData.map((topic, index) => {
81
- return "items" in topic
82
- ? { label: String(index), items: topic.items }
83
- : { label: String(index), items: [] }
84
- })
85
-
86
- updateConfig({
87
- sidebar: topicSidebar,
88
- components: {
89
- Sidebar: `starlight-sidebar-topics/overrides/Sidebar.astro`
90
- }
91
- })
92
- }
93
-
94
- // --- Addons Logic ---
95
- if (
96
- !addonConfig.copy &&
97
- !addonConfig.share &&
98
- (!addonConfig.packageManagers || addonConfig.packageManagers.length === 0)
99
- ) {
100
- logger.warn("StarlightAddons: No features enabled.")
101
- }
102
-
103
- updateConfig({
104
- components: {
105
- // This will merge with the Sidebar override if both exist
106
- PageTitle: path.join(__dirname, "PageTitle.astro")
107
- }
108
- })
109
-
110
- // --- Unified Integration ---
111
- addIntegration({
112
- name: "rimelight-ui-starlight-addons-integration",
113
- hooks: {
114
- "astro:config:setup": ({ updateConfig: updateAstroConfig }) => {
115
- updateAstroConfig({
116
- vite: {
117
- plugins: [
118
- virtual({
119
- "virtual:rimelight-starlight-addons-config": `export default ${JSON.stringify(addonConfig)}`
120
- }),
121
- // Pass the validated topic data to the vite plugin
122
- vitePluginStarlightSidebarTopics(topicsData, optionsData)
123
- ]
124
- }
125
- } as Record<string, unknown>)
126
- }
127
- }
128
- })
129
- }
130
- }
131
- }
132
- }
1
+ /// <reference path="./locals.d.ts" />
2
+ import type { StarlightPlugin, StarlightUserConfig } from "@astrojs/starlight/types"
3
+ import virtual from "vite-plugin-virtual"
4
+ import path from "node:path"
5
+ import { fileURLToPath } from "node:url"
6
+ import { z } from "astro/zod"
7
+
8
+ import {
9
+ StarlightSidebarTopicsConfigSchema,
10
+ StarlightSidebarTopicsOptionsSchema,
11
+ type StarlightSidebarTopicsUserConfig,
12
+ type StarlightSidebarTopicsUserOptions
13
+ } from "./libs/config"
14
+ import { throwPluginError } from "./libs/plugin"
15
+ import { vitePluginStarlightSidebarTopics } from "./libs/vite"
16
+
17
+ export type {
18
+ StarlightSidebarTopicsConfig,
19
+ StarlightSidebarTopicsUserConfig,
20
+ StarlightSidebarTopicsOptions,
21
+ StarlightSidebarTopicsUserOptions
22
+ } from "./libs/config"
23
+
24
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
25
+
26
+ export interface StarlightAddonsConfig {
27
+ copy?: boolean
28
+ share?: boolean
29
+ packageManagers?: string[]
30
+ // Merged config for Topics
31
+ topics?: StarlightSidebarTopicsUserConfig
32
+ topicOptions?: StarlightSidebarTopicsUserOptions
33
+ }
34
+
35
+ export default function starlightAddons(userConfig?: StarlightAddonsConfig): StarlightPlugin {
36
+ // 1. Setup default and merged config for Addons
37
+ const defaultAddonConfig: StarlightAddonsConfig = {
38
+ copy: true,
39
+ share: true,
40
+ packageManagers: ["npm"]
41
+ }
42
+
43
+ const addonConfig = { ...defaultAddonConfig, ...userConfig }
44
+
45
+ // 2. Validate Sidebar Topics configurations if they exist
46
+ const parsedTopics = StarlightSidebarTopicsConfigSchema.safeParse(addonConfig.topics || [])
47
+ if (!parsedTopics.success) {
48
+ throwPluginError(
49
+ `Invalid topics config: ${parsedTopics.error.issues.map((i) => i.message).join("\n")}`
50
+ )
51
+ }
52
+
53
+ const parsedOptions = StarlightSidebarTopicsOptionsSchema.safeParse(addonConfig.topicOptions)
54
+ if (!parsedOptions.success) {
55
+ throwPluginError(`Invalid topic options: ${z.prettifyError(parsedOptions.error)}`)
56
+ }
57
+
58
+ const topicsData = parsedTopics.data
59
+ const optionsData = parsedOptions.data
60
+
61
+ return {
62
+ name: "rimelight-ui-starlight-addons",
63
+ hooks: {
64
+ "config:setup"({
65
+ addIntegration,
66
+ updateConfig,
67
+ logger,
68
+ addRouteMiddleware,
69
+ command,
70
+ config: starlightConfig
71
+ }) {
72
+ // --- Sidebar Topics Logic ---
73
+ if ((command === "dev" || command === "build") && topicsData.length > 0) {
74
+ if (starlightConfig.sidebar) {
75
+ throwPluginError(
76
+ "Sidebar detected. To use topics, move your sidebar items into the topics config."
77
+ )
78
+ }
79
+
80
+ const topicSidebar: StarlightUserConfig["sidebar"] = topicsData.map((topic, index) => {
81
+ return "items" in topic
82
+ ? { label: String(index), items: topic.items }
83
+ : { label: String(index), items: [] }
84
+ })
85
+
86
+ updateConfig({
87
+ sidebar: topicSidebar,
88
+ components: {
89
+ Sidebar: `starlight-sidebar-topics/overrides/Sidebar.astro`
90
+ }
91
+ })
92
+ }
93
+
94
+ // --- Addons Logic ---
95
+ if (
96
+ !addonConfig.copy &&
97
+ !addonConfig.share &&
98
+ (!addonConfig.packageManagers || addonConfig.packageManagers.length === 0)
99
+ ) {
100
+ logger.warn("StarlightAddons: No features enabled.")
101
+ }
102
+
103
+ updateConfig({
104
+ components: {
105
+ // This will merge with the Sidebar override if both exist
106
+ PageTitle: path.join(__dirname, "PageTitle.astro")
107
+ }
108
+ })
109
+
110
+ // --- Unified Integration ---
111
+ addIntegration({
112
+ name: "rimelight-ui-starlight-addons-integration",
113
+ hooks: {
114
+ "astro:config:setup": ({ updateConfig: updateAstroConfig }) => {
115
+ updateAstroConfig({
116
+ vite: {
117
+ plugins: [
118
+ virtual({
119
+ "virtual:rimelight-starlight-addons-config": `export default ${JSON.stringify(addonConfig)}`
120
+ }),
121
+ // Pass the validated topic data to the vite plugin
122
+ vitePluginStarlightSidebarTopics(topicsData, optionsData)
123
+ ]
124
+ }
125
+ } as Record<string, unknown>)
126
+ }
127
+ }
128
+ })
129
+ }
130
+ }
131
+ }
132
+ }