@runtypelabs/persona 2.1.0 → 2.3.0
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/LICENSE +21 -0
- package/dist/index.cjs +41 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +130 -1
- package/dist/index.d.ts +130 -1
- package/dist/index.global.js +68 -64
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +41 -41
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/widget.css +40 -10
- package/package.json +3 -1
- package/src/client.ts +0 -1
- package/src/components/artifact-pane.ts +8 -1
- package/src/components/composer-builder.ts +1 -0
- package/src/components/header-builder.ts +1 -0
- package/src/components/header-layouts.ts +41 -1
- package/src/components/message-bubble.test.ts +97 -0
- package/src/components/message-bubble.ts +22 -2
- package/src/components/panel.ts +2 -0
- package/src/index.ts +19 -1
- package/src/install-config.test.ts +38 -0
- package/src/install.ts +3 -1
- package/src/postprocessors.test.ts +84 -0
- package/src/presets.ts +127 -0
- package/src/styles/widget.css +40 -10
- package/src/types/theme.ts +41 -0
- package/src/types.ts +29 -1
- package/src/ui.ts +25 -8
- package/src/utils/actions.test.ts +114 -0
- package/src/utils/sanitize.test.ts +114 -0
- package/src/utils/sanitize.ts +83 -0
- package/src/utils/tokens.ts +54 -0
package/dist/install.global.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var SiteAgentInstaller=(()=>{var w=Object.defineProperty;var
|
|
1
|
+
"use strict";var SiteAgentInstaller=(()=>{var w=Object.defineProperty;var P=Object.getOwnPropertyDescriptor;var j=Object.getOwnPropertyNames;var W=Object.prototype.hasOwnProperty;var T=(s,i,d,t)=>{if(i&&typeof i=="object"||typeof i=="function")for(let a of j(i))!W.call(s,a)&&a!==d&&w(s,a,{get:()=>i[a],enumerable:!(t=P(i,a))||t.enumerable});return s};var _=s=>T(w({},"__esModule",{value:!0}),s);var F={};(function(){"use strict";if(window.__siteAgentInstallerLoaded)return;window.__siteAgentInstallerLoaded=!0;let i=(()=>{let n=document.currentScript;if(!n)return{};let e={},o=n.getAttribute("data-config");if(o)try{let c=JSON.parse(o);if(c.config){let{__proto__:E,constructor:M,prototype:$,...v}=c;Object.assign(e,v)}else e.config=c}catch(c){console.error("Failed to parse data-config JSON:",c)}let r=n.getAttribute("data-runtype-token");r&&(e.clientToken=r);let l=n.getAttribute("data-flow-id");l&&(e.flowId=l);let f=n.getAttribute("data-api-url");f&&(e.apiUrl=f);let p=n.getAttribute("data-preview-param");return p&&(e.previewQueryParam=p),e})(),t={...window.siteAgentConfig||{},...i};if(!(()=>{if(!t.previewQueryParam)return!0;let e=new URLSearchParams(window.location.search).get(t.previewQueryParam);return e!==null&&e!==""&&e.toLowerCase()!=="false"&&e!=="0"})())return;let m=t.version||"latest",A=t.cdn||"jsdelivr",C=t.autoInit!==!1,h=()=>{if(t.cssUrl&&t.jsUrl)return{cssUrl:t.cssUrl,jsUrl:t.jsUrl};let e=`/npm/@runtypelabs/persona@${m}/dist`;return A==="unpkg"?{cssUrl:`https://unpkg.com${e}/widget.css`,jsUrl:`https://unpkg.com${e}/index.global.js`}:{cssUrl:`https://cdn.jsdelivr.net${e}/widget.css`,jsUrl:`https://cdn.jsdelivr.net${e}/index.global.js`}},{cssUrl:g,jsUrl:u}=h(),y=()=>!!document.head.querySelector("link[data-persona]")||!!document.head.querySelector('link[href*="widget.css"]'),k=()=>!!window.AgentWidget,S=n=>{let e=!1,o=()=>{e||(e=!0,n())},r=()=>{typeof requestIdleCallback!="undefined"?requestIdleCallback(()=>{requestAnimationFrame(()=>{requestAnimationFrame(o)})},{timeout:3e3}):setTimeout(o,300)};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",r):r()},U=()=>new Promise((n,e)=>{if(y()){n();return}let o=document.createElement("link");o.rel="stylesheet",o.href=g,o.setAttribute("data-persona","true"),o.onload=()=>n(),o.onerror=()=>e(new Error(`Failed to load CSS from ${g}`)),document.head.appendChild(o)}),b=()=>new Promise((n,e)=>{if(k()){n();return}let o=document.createElement("script");o.src=u,o.async=!0,o.onload=()=>n(),o.onerror=()=>e(new Error(`Failed to load JS from ${u}`)),document.head.appendChild(o)}),I=()=>{var r;if(!window.AgentWidget||!window.AgentWidget.initAgentWidget){console.warn("AgentWidget not available. Make sure the script loaded successfully.");return}let n=t.target||"body",e={...t.config};if(t.apiUrl&&!e.apiUrl&&(e.apiUrl=t.apiUrl),t.clientToken&&!e.clientToken&&(e.clientToken=t.clientToken),t.flowId&&!e.flowId&&(e.flowId=t.flowId),!(!(e.apiUrl||e.clientToken)&&Object.keys(e).length===0)){!e.postprocessMessage&&window.AgentWidget.markdownPostprocessor&&(e.postprocessMessage=({text:l})=>window.AgentWidget.markdownPostprocessor(l));try{window.AgentWidget.initAgentWidget({target:n,config:e,useShadowDom:(r=t.useShadowDom)!=null?r:!1})}catch(l){console.error("Failed to initialize AgentWidget:",l)}}};S(async()=>{try{await U(),await b(),C&&(t.config||t.apiUrl||t.clientToken)&&setTimeout(I,0)}catch(n){console.error("Failed to install AgentWidget:",n)}})})();return _(F);})();
|
|
2
2
|
//# sourceMappingURL=install.global.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/install.ts"],"sourcesContent":["/**\n * Standalone installer script for easy script tag installation\n * This script automatically loads CSS and JS, then initializes the widget\n * if configuration is provided via window.siteAgentConfig\n */\n\nexport {};\n\ninterface SiteAgentInstallConfig {\n version?: string;\n cdn?: \"unpkg\" | \"jsdelivr\";\n cssUrl?: string;\n jsUrl?: string;\n target?: string | HTMLElement;\n config?: any;\n autoInit?: boolean;\n // Client token mode options (can also be set via data attributes)\n clientToken?: string;\n flowId?: string;\n apiUrl?: string;\n // Optional query param key that gates widget installation in preview mode\n previewQueryParam?: string;\n // Shadow DOM option (defaults to false for better CSS compatibility)\n useShadowDom?: boolean;\n}\n\ndeclare global {\n interface Window {\n siteAgentConfig?: SiteAgentInstallConfig;\n AgentWidget?: any;\n }\n}\n\n(function() {\n \"use strict\";\n\n // Prevent double installation\n if ((window as any).__siteAgentInstallerLoaded) {\n return;\n }\n (window as any).__siteAgentInstallerLoaded = true;\n\n /**\n * Read configuration from data attributes on the current script tag.\n * Supports: data-config (JSON), data-runtype-token, data-flow-id, data-api-url\n */\n const getConfigFromScript = (): Partial<SiteAgentInstallConfig> => {\n // Try to get the current script element\n const script = document.currentScript as HTMLScriptElement | null;\n if (!script) return {};\n\n const scriptConfig: Partial<SiteAgentInstallConfig> = {};\n\n // Full config from data-config attribute (JSON string)\n const configJson = script.getAttribute('data-config');\n if (configJson) {\n try {\n const parsedConfig = JSON.parse(configJson);\n // If it has nested 'config' property, use it; otherwise treat as widget config\n if (parsedConfig.config) {\n Object.assign(scriptConfig, parsedConfig);\n } else {\n // Treat the entire object as widget config\n scriptConfig.config = parsedConfig;\n }\n } catch (e) {\n console.error(\"Failed to parse data-config JSON:\", e);\n }\n }\n\n // Client token from data attribute (primary method for client token mode)\n const token = script.getAttribute('data-runtype-token');\n if (token) {\n scriptConfig.clientToken = token;\n }\n\n // Optional flow ID\n const flowId = script.getAttribute('data-flow-id');\n if (flowId) {\n scriptConfig.flowId = flowId;\n }\n\n // Optional API URL override\n const apiUrl = script.getAttribute('data-api-url');\n if (apiUrl) {\n scriptConfig.apiUrl = apiUrl;\n }\n\n // Optional preview query param gate\n const previewQueryParam = script.getAttribute('data-preview-param');\n if (previewQueryParam) {\n scriptConfig.previewQueryParam = previewQueryParam;\n }\n\n return scriptConfig;\n };\n\n // Get config from script attributes (must be called synchronously during script execution)\n const scriptConfig = getConfigFromScript();\n\n // Merge script attributes with window config (script attributes take precedence)\n const windowConfig: SiteAgentInstallConfig = window.siteAgentConfig || {};\n const config: SiteAgentInstallConfig = { ...windowConfig, ...scriptConfig };\n\n const isPreviewModeEnabled = (): boolean => {\n if (!config.previewQueryParam) {\n return true;\n }\n\n const params = new URLSearchParams(window.location.search);\n const value = params.get(config.previewQueryParam);\n return value !== null && value !== \"\" && value.toLowerCase() !== \"false\" && value !== \"0\";\n };\n\n if (!isPreviewModeEnabled()) {\n return;\n }\n \n const version = config.version || \"latest\";\n const cdn = config.cdn || \"jsdelivr\";\n const autoInit = config.autoInit !== false; // Default to true\n\n // Determine CDN base URL\n const getCdnBase = () => {\n if (config.cssUrl && config.jsUrl) {\n return { cssUrl: config.cssUrl, jsUrl: config.jsUrl };\n }\n \n const packageName = \"@runtypelabs/persona\";\n const basePath = `/npm/${packageName}@${version}/dist`;\n \n if (cdn === \"unpkg\") {\n return {\n cssUrl: `https://unpkg.com${basePath}/widget.css`,\n jsUrl: `https://unpkg.com${basePath}/index.global.js`\n };\n } else {\n return {\n cssUrl: `https://cdn.jsdelivr.net${basePath}/widget.css`,\n jsUrl: `https://cdn.jsdelivr.net${basePath}/index.global.js`\n };\n }\n };\n\n const { cssUrl, jsUrl } = getCdnBase();\n\n // Check if CSS is already loaded\n const isCssLoaded = () => {\n return !!document.head.querySelector('link[data-persona]') ||\n !!document.head.querySelector(`link[href*=\"widget.css\"]`);\n };\n\n // Check if JS is already loaded\n const isJsLoaded = () => {\n return !!(window as any).AgentWidget;\n };\n\n /**\n * Wait for framework hydration to complete (Next.js, Nuxt, etc.)\n * This prevents the framework from removing dynamically added CSS during reconciliation.\n * Uses requestIdleCallback + double requestAnimationFrame for reliable detection.\n */\n const waitForHydration = (callback: () => void): void => {\n let executed = false;\n \n const execute = () => {\n if (executed) return;\n executed = true;\n callback();\n };\n\n const afterDom = () => {\n // Strategy 1: Use requestIdleCallback if available (best for detecting idle after hydration)\n if (typeof requestIdleCallback !== 'undefined') {\n requestIdleCallback(() => {\n // Double requestAnimationFrame ensures at least one full paint cycle completed\n requestAnimationFrame(() => {\n requestAnimationFrame(execute);\n });\n }, { timeout: 3000 }); // Max wait 3 seconds, then proceed anyway\n } else {\n // Strategy 2: Fallback for Safari (no requestIdleCallback)\n // 300ms is typically enough for hydration on most pages\n setTimeout(execute, 300);\n }\n };\n\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', afterDom);\n } else {\n // DOM already ready, but still wait for potential hydration\n afterDom();\n }\n };\n\n // Load CSS\n const loadCSS = (): Promise<void> => {\n return new Promise((resolve, reject) => {\n if (isCssLoaded()) {\n resolve();\n return;\n }\n\n const link = document.createElement(\"link\");\n link.rel = \"stylesheet\";\n link.href = cssUrl;\n link.setAttribute(\"data-persona\", \"true\");\n \n link.onload = () => resolve();\n link.onerror = () => reject(new Error(`Failed to load CSS from ${cssUrl}`));\n document.head.appendChild(link);\n });\n };\n\n // Load JS\n const loadJS = (): Promise<void> => {\n return new Promise((resolve, reject) => {\n if (isJsLoaded()) {\n resolve();\n return;\n }\n\n const script = document.createElement(\"script\");\n script.src = jsUrl;\n script.async = true;\n script.onload = () => resolve();\n script.onerror = () => reject(new Error(`Failed to load JS from ${jsUrl}`));\n document.head.appendChild(script);\n });\n };\n\n // Initialize widget\n const initWidget = () => {\n if (!window.AgentWidget || !window.AgentWidget.initAgentWidget) {\n console.warn(\"AgentWidget not available. Make sure the script loaded successfully.\");\n return;\n }\n\n const target = config.target || \"body\";\n // Merge top-level config options into widget config\n const widgetConfig = { ...config.config };\n \n // Merge apiUrl from top-level config into widget config if present\n if (config.apiUrl && !widgetConfig.apiUrl) {\n widgetConfig.apiUrl = config.apiUrl;\n }\n \n // Merge clientToken from top-level config into widget config if present\n if (config.clientToken && !widgetConfig.clientToken) {\n widgetConfig.clientToken = config.clientToken;\n }\n \n // Merge flowId from top-level config into widget config if present\n if (config.flowId && !widgetConfig.flowId) {\n widgetConfig.flowId = config.flowId;\n }\n\n // Only initialize if we have either apiUrl OR clientToken (or other config)\n const hasApiConfig = widgetConfig.apiUrl || widgetConfig.clientToken;\n if (!hasApiConfig && Object.keys(widgetConfig).length === 0) {\n return;\n }\n\n // Auto-apply markdown postprocessor if not explicitly set and available\n if (!widgetConfig.postprocessMessage && window.AgentWidget.markdownPostprocessor) {\n widgetConfig.postprocessMessage = ({ text }: { text: string }) => \n window.AgentWidget.markdownPostprocessor(text);\n }\n\n try {\n window.AgentWidget.initAgentWidget({\n target,\n config: widgetConfig,\n // Explicitly disable shadow DOM for better CSS compatibility with host page\n useShadowDom: config.useShadowDom ?? false\n });\n } catch (error) {\n console.error(\"Failed to initialize AgentWidget:\", error);\n }\n };\n\n // Main installation flow (called after hydration completes)\n const install = async () => {\n try {\n await loadCSS();\n await loadJS();\n \n // Auto-init if we have config OR apiUrl OR clientToken\n const shouldAutoInit = autoInit && (\n config.config || \n config.apiUrl || \n config.clientToken\n );\n \n if (shouldAutoInit) {\n // Wait a tick to ensure AgentWidget is fully initialized\n setTimeout(initWidget, 0);\n }\n } catch (error) {\n console.error(\"Failed to install AgentWidget:\", error);\n }\n };\n\n // Start installation after hydration completes\n // This prevents Next.js/Nuxt/etc. from removing dynamically added CSS\n waitForHydration(install);\n})();\n\n"],"mappings":"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","names":["install_exports","scriptConfig","script","configJson","parsedConfig","e","token","flowId","apiUrl","previewQueryParam","config","value","version","cdn","autoInit","getCdnBase","basePath","cssUrl","jsUrl","isCssLoaded","isJsLoaded","waitForHydration","callback","executed","execute","afterDom","loadCSS","resolve","reject","link","loadJS","initWidget","_a","target","widgetConfig","text","error"]}
|
|
1
|
+
{"version":3,"sources":["../src/install.ts"],"sourcesContent":["/**\n * Standalone installer script for easy script tag installation\n * This script automatically loads CSS and JS, then initializes the widget\n * if configuration is provided via window.siteAgentConfig\n */\n\nexport {};\n\ninterface SiteAgentInstallConfig {\n version?: string;\n cdn?: \"unpkg\" | \"jsdelivr\";\n cssUrl?: string;\n jsUrl?: string;\n target?: string | HTMLElement;\n config?: any;\n autoInit?: boolean;\n // Client token mode options (can also be set via data attributes)\n clientToken?: string;\n flowId?: string;\n apiUrl?: string;\n // Optional query param key that gates widget installation in preview mode\n previewQueryParam?: string;\n // Shadow DOM option (defaults to false for better CSS compatibility)\n useShadowDom?: boolean;\n}\n\ndeclare global {\n interface Window {\n siteAgentConfig?: SiteAgentInstallConfig;\n AgentWidget?: any;\n }\n}\n\n(function() {\n \"use strict\";\n\n // Prevent double installation\n if ((window as any).__siteAgentInstallerLoaded) {\n return;\n }\n (window as any).__siteAgentInstallerLoaded = true;\n\n /**\n * Read configuration from data attributes on the current script tag.\n * Supports: data-config (JSON), data-runtype-token, data-flow-id, data-api-url\n */\n const getConfigFromScript = (): Partial<SiteAgentInstallConfig> => {\n // Try to get the current script element\n const script = document.currentScript as HTMLScriptElement | null;\n if (!script) return {};\n\n const scriptConfig: Partial<SiteAgentInstallConfig> = {};\n\n // Full config from data-config attribute (JSON string)\n const configJson = script.getAttribute('data-config');\n if (configJson) {\n try {\n const parsedConfig = JSON.parse(configJson);\n // If it has nested 'config' property, use it; otherwise treat as widget config\n if (parsedConfig.config) {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { __proto__: _a, constructor: _b, prototype: _c, ...safeConfig } = parsedConfig;\n Object.assign(scriptConfig, safeConfig);\n } else {\n // Treat the entire object as widget config\n scriptConfig.config = parsedConfig;\n }\n } catch (e) {\n console.error(\"Failed to parse data-config JSON:\", e);\n }\n }\n\n // Client token from data attribute (primary method for client token mode)\n const token = script.getAttribute('data-runtype-token');\n if (token) {\n scriptConfig.clientToken = token;\n }\n\n // Optional flow ID\n const flowId = script.getAttribute('data-flow-id');\n if (flowId) {\n scriptConfig.flowId = flowId;\n }\n\n // Optional API URL override\n const apiUrl = script.getAttribute('data-api-url');\n if (apiUrl) {\n scriptConfig.apiUrl = apiUrl;\n }\n\n // Optional preview query param gate\n const previewQueryParam = script.getAttribute('data-preview-param');\n if (previewQueryParam) {\n scriptConfig.previewQueryParam = previewQueryParam;\n }\n\n return scriptConfig;\n };\n\n // Get config from script attributes (must be called synchronously during script execution)\n const scriptConfig = getConfigFromScript();\n\n // Merge script attributes with window config (script attributes take precedence)\n const windowConfig: SiteAgentInstallConfig = window.siteAgentConfig || {};\n const config: SiteAgentInstallConfig = { ...windowConfig, ...scriptConfig };\n\n const isPreviewModeEnabled = (): boolean => {\n if (!config.previewQueryParam) {\n return true;\n }\n\n const params = new URLSearchParams(window.location.search);\n const value = params.get(config.previewQueryParam);\n return value !== null && value !== \"\" && value.toLowerCase() !== \"false\" && value !== \"0\";\n };\n\n if (!isPreviewModeEnabled()) {\n return;\n }\n \n const version = config.version || \"latest\";\n const cdn = config.cdn || \"jsdelivr\";\n const autoInit = config.autoInit !== false; // Default to true\n\n // Determine CDN base URL\n const getCdnBase = () => {\n if (config.cssUrl && config.jsUrl) {\n return { cssUrl: config.cssUrl, jsUrl: config.jsUrl };\n }\n \n const packageName = \"@runtypelabs/persona\";\n const basePath = `/npm/${packageName}@${version}/dist`;\n \n if (cdn === \"unpkg\") {\n return {\n cssUrl: `https://unpkg.com${basePath}/widget.css`,\n jsUrl: `https://unpkg.com${basePath}/index.global.js`\n };\n } else {\n return {\n cssUrl: `https://cdn.jsdelivr.net${basePath}/widget.css`,\n jsUrl: `https://cdn.jsdelivr.net${basePath}/index.global.js`\n };\n }\n };\n\n const { cssUrl, jsUrl } = getCdnBase();\n\n // Check if CSS is already loaded\n const isCssLoaded = () => {\n return !!document.head.querySelector('link[data-persona]') ||\n !!document.head.querySelector(`link[href*=\"widget.css\"]`);\n };\n\n // Check if JS is already loaded\n const isJsLoaded = () => {\n return !!(window as any).AgentWidget;\n };\n\n /**\n * Wait for framework hydration to complete (Next.js, Nuxt, etc.)\n * This prevents the framework from removing dynamically added CSS during reconciliation.\n * Uses requestIdleCallback + double requestAnimationFrame for reliable detection.\n */\n const waitForHydration = (callback: () => void): void => {\n let executed = false;\n \n const execute = () => {\n if (executed) return;\n executed = true;\n callback();\n };\n\n const afterDom = () => {\n // Strategy 1: Use requestIdleCallback if available (best for detecting idle after hydration)\n if (typeof requestIdleCallback !== 'undefined') {\n requestIdleCallback(() => {\n // Double requestAnimationFrame ensures at least one full paint cycle completed\n requestAnimationFrame(() => {\n requestAnimationFrame(execute);\n });\n }, { timeout: 3000 }); // Max wait 3 seconds, then proceed anyway\n } else {\n // Strategy 2: Fallback for Safari (no requestIdleCallback)\n // 300ms is typically enough for hydration on most pages\n setTimeout(execute, 300);\n }\n };\n\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', afterDom);\n } else {\n // DOM already ready, but still wait for potential hydration\n afterDom();\n }\n };\n\n // Load CSS\n const loadCSS = (): Promise<void> => {\n return new Promise((resolve, reject) => {\n if (isCssLoaded()) {\n resolve();\n return;\n }\n\n const link = document.createElement(\"link\");\n link.rel = \"stylesheet\";\n link.href = cssUrl;\n link.setAttribute(\"data-persona\", \"true\");\n \n link.onload = () => resolve();\n link.onerror = () => reject(new Error(`Failed to load CSS from ${cssUrl}`));\n document.head.appendChild(link);\n });\n };\n\n // Load JS\n const loadJS = (): Promise<void> => {\n return new Promise((resolve, reject) => {\n if (isJsLoaded()) {\n resolve();\n return;\n }\n\n const script = document.createElement(\"script\");\n script.src = jsUrl;\n script.async = true;\n script.onload = () => resolve();\n script.onerror = () => reject(new Error(`Failed to load JS from ${jsUrl}`));\n document.head.appendChild(script);\n });\n };\n\n // Initialize widget\n const initWidget = () => {\n if (!window.AgentWidget || !window.AgentWidget.initAgentWidget) {\n console.warn(\"AgentWidget not available. Make sure the script loaded successfully.\");\n return;\n }\n\n const target = config.target || \"body\";\n // Merge top-level config options into widget config\n const widgetConfig = { ...config.config };\n \n // Merge apiUrl from top-level config into widget config if present\n if (config.apiUrl && !widgetConfig.apiUrl) {\n widgetConfig.apiUrl = config.apiUrl;\n }\n \n // Merge clientToken from top-level config into widget config if present\n if (config.clientToken && !widgetConfig.clientToken) {\n widgetConfig.clientToken = config.clientToken;\n }\n \n // Merge flowId from top-level config into widget config if present\n if (config.flowId && !widgetConfig.flowId) {\n widgetConfig.flowId = config.flowId;\n }\n\n // Only initialize if we have either apiUrl OR clientToken (or other config)\n const hasApiConfig = widgetConfig.apiUrl || widgetConfig.clientToken;\n if (!hasApiConfig && Object.keys(widgetConfig).length === 0) {\n return;\n }\n\n // Auto-apply markdown postprocessor if not explicitly set and available\n if (!widgetConfig.postprocessMessage && window.AgentWidget.markdownPostprocessor) {\n widgetConfig.postprocessMessage = ({ text }: { text: string }) => \n window.AgentWidget.markdownPostprocessor(text);\n }\n\n try {\n window.AgentWidget.initAgentWidget({\n target,\n config: widgetConfig,\n // Explicitly disable shadow DOM for better CSS compatibility with host page\n useShadowDom: config.useShadowDom ?? false\n });\n } catch (error) {\n console.error(\"Failed to initialize AgentWidget:\", error);\n }\n };\n\n // Main installation flow (called after hydration completes)\n const install = async () => {\n try {\n await loadCSS();\n await loadJS();\n \n // Auto-init if we have config OR apiUrl OR clientToken\n const shouldAutoInit = autoInit && (\n config.config || \n config.apiUrl || \n config.clientToken\n );\n \n if (shouldAutoInit) {\n // Wait a tick to ensure AgentWidget is fully initialized\n setTimeout(initWidget, 0);\n }\n } catch (error) {\n console.error(\"Failed to install AgentWidget:\", error);\n }\n };\n\n // Start installation after hydration completes\n // This prevents Next.js/Nuxt/etc. from removing dynamically added CSS\n waitForHydration(install);\n})();\n\n"],"mappings":"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","names":["install_exports","scriptConfig","script","configJson","parsedConfig","_a","_b","_c","safeConfig","e","token","flowId","apiUrl","previewQueryParam","config","value","version","cdn","autoInit","getCdnBase","basePath","cssUrl","jsUrl","isCssLoaded","isJsLoaded","waitForHydration","callback","executed","execute","afterDom","loadCSS","resolve","reject","link","loadJS","initWidget","target","widgetConfig","text","error"]}
|
package/dist/widget.css
CHANGED
|
@@ -2064,9 +2064,9 @@
|
|
|
2064
2064
|
display: inline-flex;
|
|
2065
2065
|
align-items: center;
|
|
2066
2066
|
justify-content: center;
|
|
2067
|
-
padding: 0.25rem;
|
|
2068
|
-
border-radius: var(--persona-radius-md, 0.375rem);
|
|
2069
|
-
border: 1px solid var(--persona-border, #e5e7eb);
|
|
2067
|
+
padding: var(--persona-artifact-toolbar-icon-padding, 0.25rem);
|
|
2068
|
+
border-radius: var(--persona-artifact-toolbar-icon-radius, var(--persona-radius-md, 0.375rem));
|
|
2069
|
+
border: var(--persona-artifact-toolbar-icon-border, 1px solid var(--persona-border, #e5e7eb));
|
|
2070
2070
|
background: var(--persona-surface, #ffffff);
|
|
2071
2071
|
color: var(--persona-artifact-doc-toolbar-icon-color, var(--persona-text, #111827));
|
|
2072
2072
|
cursor: pointer;
|
|
@@ -2074,7 +2074,8 @@
|
|
|
2074
2074
|
}
|
|
2075
2075
|
|
|
2076
2076
|
#persona-root .persona-artifact-toolbar-document .persona-artifact-doc-icon-btn:hover {
|
|
2077
|
-
|
|
2077
|
+
color: var(--persona-artifact-toolbar-icon-hover-color, inherit);
|
|
2078
|
+
background: var(--persona-artifact-toolbar-icon-hover-bg, var(--persona-container, #f3f4f6));
|
|
2078
2079
|
}
|
|
2079
2080
|
|
|
2080
2081
|
#persona-root .persona-artifact-toolbar-document .persona-artifact-doc-icon-btn[aria-pressed="true"] {
|
|
@@ -2086,24 +2087,53 @@
|
|
|
2086
2087
|
display: inline-flex;
|
|
2087
2088
|
align-items: center;
|
|
2088
2089
|
gap: 0.35rem;
|
|
2089
|
-
padding: 0.25rem 0.5rem;
|
|
2090
|
-
border-radius: var(--persona-radius-md, 0.375rem);
|
|
2091
|
-
border: 1px solid var(--persona-border, #e5e7eb);
|
|
2092
|
-
background: var(--persona-surface, #ffffff);
|
|
2093
|
-
color: var(--persona-artifact-doc-toolbar-icon-color, var(--persona-text, #111827));
|
|
2090
|
+
padding: var(--persona-artifact-toolbar-copy-padding, 0.25rem 0.5rem);
|
|
2091
|
+
border-radius: var(--persona-artifact-toolbar-copy-radius, var(--persona-radius-md, 0.375rem));
|
|
2092
|
+
border: var(--persona-artifact-toolbar-copy-border, 1px solid var(--persona-border, #e5e7eb));
|
|
2093
|
+
background: var(--persona-artifact-toolbar-copy-bg, var(--persona-surface, #ffffff));
|
|
2094
|
+
color: var(--persona-artifact-toolbar-copy-color, var(--persona-artifact-doc-toolbar-icon-color, var(--persona-text, #111827)));
|
|
2094
2095
|
cursor: pointer;
|
|
2095
2096
|
font-size: 0.75rem;
|
|
2096
2097
|
line-height: 1.25;
|
|
2097
2098
|
}
|
|
2098
2099
|
|
|
2099
2100
|
#persona-root .persona-artifact-toolbar-document .persona-artifact-doc-copy-btn:hover {
|
|
2100
|
-
background: var(--persona-container, #f3f4f6);
|
|
2101
|
+
background: var(--persona-artifact-toolbar-icon-hover-bg, var(--persona-container, #f3f4f6));
|
|
2101
2102
|
}
|
|
2102
2103
|
|
|
2103
2104
|
#persona-root .persona-artifact-toolbar-document .persona-artifact-doc-copy-label {
|
|
2104
2105
|
font-weight: 500;
|
|
2105
2106
|
}
|
|
2106
2107
|
|
|
2108
|
+
/* Copy menu dropdown theming */
|
|
2109
|
+
#persona-root .persona-artifact-doc-copy-menu {
|
|
2110
|
+
background: var(--persona-artifact-toolbar-copy-menu-bg, var(--persona-surface, #fff));
|
|
2111
|
+
border: var(--persona-artifact-toolbar-copy-menu-border, 1px solid var(--persona-border, #e5e7eb));
|
|
2112
|
+
box-shadow: var(--persona-artifact-toolbar-copy-menu-shadow, 0 4px 6px -1px rgba(0,0,0,.1));
|
|
2113
|
+
border-radius: var(--persona-artifact-toolbar-copy-menu-radius, 0.375rem);
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
#persona-root .persona-artifact-doc-copy-menu button:hover {
|
|
2117
|
+
background: var(--persona-artifact-toolbar-copy-menu-item-hover-bg, var(--persona-container, #f3f4f6));
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
/* Artifact tab theming */
|
|
2121
|
+
#persona-root .persona-artifact-tab {
|
|
2122
|
+
background: var(--persona-artifact-tab-bg, transparent);
|
|
2123
|
+
border-radius: var(--persona-artifact-tab-radius, 0.5rem);
|
|
2124
|
+
color: var(--persona-artifact-tab-color, inherit);
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
#persona-root .persona-artifact-tab.persona-bg-persona-container {
|
|
2128
|
+
background: var(--persona-artifact-tab-active-bg, var(--persona-container, #f3f4f6));
|
|
2129
|
+
border-color: var(--persona-artifact-tab-active-border, var(--persona-border, #e5e7eb));
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
/* Artifact toolbar background theming */
|
|
2133
|
+
#persona-root .persona-artifact-toolbar {
|
|
2134
|
+
background: var(--persona-artifact-toolbar-bg, var(--persona-surface, #fff));
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2107
2137
|
/* Draggable split handle (desktop split only; hidden in drawer / narrow host / small viewport) */
|
|
2108
2138
|
#persona-root .persona-artifact-split-handle {
|
|
2109
2139
|
width: 6px;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runtypelabs/persona",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Themeable, pluggable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"src"
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
|
+
"dompurify": "^3.3.3",
|
|
26
27
|
"idiomorph": "^0.7.4",
|
|
27
28
|
"lucide": "^0.552.0",
|
|
28
29
|
"marked": "^12.0.2",
|
|
@@ -47,6 +48,7 @@
|
|
|
47
48
|
"engines": {
|
|
48
49
|
"node": ">=18.17.0"
|
|
49
50
|
},
|
|
51
|
+
"author": "Runtype",
|
|
50
52
|
"license": "MIT",
|
|
51
53
|
"keywords": [
|
|
52
54
|
"ai",
|
package/src/client.ts
CHANGED
|
@@ -45,7 +45,6 @@ const DEFAULT_CLIENT_API_BASE = "https://api.runtype.com";
|
|
|
45
45
|
* Check if a message has valid (non-empty) content for sending to the API.
|
|
46
46
|
* Filters out messages with empty content that would cause validation errors.
|
|
47
47
|
*
|
|
48
|
-
* @see https://github.com/anthropics/claude-code/issues/XXX - Empty assistant messages from failed requests
|
|
49
48
|
*/
|
|
50
49
|
const hasValidContent = (message: AgentWidgetMessage): boolean => {
|
|
51
50
|
// Check contentParts (multi-modal content)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createElement } from "../utils/dom";
|
|
2
2
|
import type { AgentWidgetConfig, AgentWidgetMessage, PersonaArtifactRecord } from "../types";
|
|
3
3
|
import { escapeHtml, createMarkdownProcessorFromConfig } from "../postprocessors";
|
|
4
|
+
import { resolveSanitizer } from "../utils/sanitize";
|
|
4
5
|
import { componentRegistry, type ComponentContext } from "./registry";
|
|
5
6
|
import { renderLucideIcon } from "../utils/icons";
|
|
6
7
|
|
|
@@ -102,7 +103,11 @@ export function createArtifactPane(
|
|
|
102
103
|
const panePadding = layout?.panePadding?.trim();
|
|
103
104
|
|
|
104
105
|
const md = config.markdown ? createMarkdownProcessorFromConfig(config.markdown) : null;
|
|
105
|
-
const
|
|
106
|
+
const sanitize = resolveSanitizer(config.sanitize);
|
|
107
|
+
const toHtml = (text: string) => {
|
|
108
|
+
const raw = md ? md(text) : escapeHtml(text);
|
|
109
|
+
return sanitize ? sanitize(raw) : raw;
|
|
110
|
+
};
|
|
106
111
|
|
|
107
112
|
const backdrop =
|
|
108
113
|
typeof document !== "undefined"
|
|
@@ -127,6 +132,7 @@ export function createArtifactPane(
|
|
|
127
132
|
"aside",
|
|
128
133
|
"persona-artifact-pane persona-flex persona-flex-col persona-min-h-0 persona-min-w-0 persona-bg-persona-surface persona-text-persona-primary persona-border-l persona-border-persona-border"
|
|
129
134
|
);
|
|
135
|
+
shell.setAttribute("data-persona-theme-zone", "artifact-pane");
|
|
130
136
|
if (documentChrome) {
|
|
131
137
|
shell.classList.add("persona-artifact-pane-document");
|
|
132
138
|
}
|
|
@@ -135,6 +141,7 @@ export function createArtifactPane(
|
|
|
135
141
|
"div",
|
|
136
142
|
"persona-artifact-toolbar persona-flex persona-items-center persona-justify-between persona-gap-2 persona-px-2 persona-py-2 persona-border-b persona-border-persona-border persona-shrink-0"
|
|
137
143
|
);
|
|
144
|
+
toolbar.setAttribute("data-persona-theme-zone", "artifact-toolbar");
|
|
138
145
|
if (documentChrome) {
|
|
139
146
|
toolbar.classList.add("persona-artifact-toolbar-document");
|
|
140
147
|
}
|
|
@@ -68,6 +68,7 @@ export const buildComposer = (context: ComposerBuildContext): ComposerElements =
|
|
|
68
68
|
"div",
|
|
69
69
|
"persona-widget-footer persona-border-t-persona-divider persona-bg-persona-surface persona-px-6 persona-py-4"
|
|
70
70
|
);
|
|
71
|
+
footer.setAttribute("data-persona-theme-zone", "composer");
|
|
71
72
|
|
|
72
73
|
const suggestions = createElement(
|
|
73
74
|
"div",
|
|
@@ -31,6 +31,7 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
|
|
|
31
31
|
"div",
|
|
32
32
|
"persona-widget-header persona-flex persona-items-center persona-gap-3 persona-px-6 persona-py-5"
|
|
33
33
|
);
|
|
34
|
+
header.setAttribute("data-persona-theme-zone", "header");
|
|
34
35
|
header.style.backgroundColor = 'var(--persona-header-bg, var(--persona-surface, #ffffff))';
|
|
35
36
|
header.style.borderBottomWidth = '1px';
|
|
36
37
|
header.style.borderBottomStyle = 'solid';
|
|
@@ -24,12 +24,32 @@ export type HeaderLayoutRenderer = (context: HeaderLayoutContext) => HeaderEleme
|
|
|
24
24
|
* Full header with icon, title, subtitle, clear chat, and close button
|
|
25
25
|
*/
|
|
26
26
|
export const buildDefaultHeader: HeaderLayoutRenderer = (context) => {
|
|
27
|
-
|
|
27
|
+
const elements = buildHeader({
|
|
28
28
|
config: context.config,
|
|
29
29
|
showClose: context.showClose,
|
|
30
30
|
onClose: context.onClose,
|
|
31
31
|
onClearChat: context.onClearChat
|
|
32
32
|
});
|
|
33
|
+
|
|
34
|
+
// Make the title/subtitle area clickable when onTitleClick is provided
|
|
35
|
+
const onTitleClick = context.layoutHeaderConfig?.onTitleClick;
|
|
36
|
+
if (onTitleClick) {
|
|
37
|
+
const headerCopy = elements.headerTitle.parentElement;
|
|
38
|
+
if (headerCopy) {
|
|
39
|
+
headerCopy.style.cursor = "pointer";
|
|
40
|
+
headerCopy.setAttribute("role", "button");
|
|
41
|
+
headerCopy.setAttribute("tabindex", "0");
|
|
42
|
+
headerCopy.addEventListener("click", () => onTitleClick());
|
|
43
|
+
headerCopy.addEventListener("keydown", (e) => {
|
|
44
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
onTitleClick();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return elements;
|
|
33
53
|
};
|
|
34
54
|
|
|
35
55
|
/**
|
|
@@ -68,6 +88,7 @@ export const buildMinimalHeader: HeaderLayoutRenderer = (context) => {
|
|
|
68
88
|
"div",
|
|
69
89
|
"persona-flex persona-items-center persona-justify-between persona-bg-persona-surface persona-px-6 persona-py-4 persona-border-b-persona-divider"
|
|
70
90
|
);
|
|
91
|
+
header.setAttribute("data-persona-theme-zone", "header");
|
|
71
92
|
|
|
72
93
|
const titleRow = createElement(
|
|
73
94
|
"div",
|
|
@@ -85,6 +106,25 @@ export const buildMinimalHeader: HeaderLayoutRenderer = (context) => {
|
|
|
85
106
|
layoutHeaderConfig?.onAction ?? onHeaderAction
|
|
86
107
|
);
|
|
87
108
|
|
|
109
|
+
// Make title row clickable when onTitleClick is provided
|
|
110
|
+
if (layoutHeaderConfig?.onTitleClick) {
|
|
111
|
+
titleRow.style.cursor = "pointer";
|
|
112
|
+
titleRow.setAttribute("role", "button");
|
|
113
|
+
titleRow.setAttribute("tabindex", "0");
|
|
114
|
+
const handleTitleClick = layoutHeaderConfig.onTitleClick;
|
|
115
|
+
titleRow.addEventListener("click", (e) => {
|
|
116
|
+
// Skip if the click was on a trailing action button
|
|
117
|
+
if ((e.target as HTMLElement).closest("button")) return;
|
|
118
|
+
handleTitleClick();
|
|
119
|
+
});
|
|
120
|
+
titleRow.addEventListener("keydown", (e) => {
|
|
121
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
handleTitleClick();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
88
128
|
header.appendChild(titleRow);
|
|
89
129
|
|
|
90
130
|
// Close button
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
import { createStandardBubble, isSafeImageSrc } from "./message-bubble";
|
|
4
|
+
import type { AgentWidgetMessage } from "../types";
|
|
5
|
+
|
|
6
|
+
const makeMessage = (overrides: Partial<AgentWidgetMessage> = {}): AgentWidgetMessage => ({
|
|
7
|
+
id: "msg-1",
|
|
8
|
+
role: "assistant",
|
|
9
|
+
content: "",
|
|
10
|
+
createdAt: new Date().toISOString(),
|
|
11
|
+
...overrides,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("isSafeImageSrc", () => {
|
|
15
|
+
it("allows https URLs", () => {
|
|
16
|
+
expect(isSafeImageSrc("https://example.com/img.png")).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("allows http URLs", () => {
|
|
20
|
+
expect(isSafeImageSrc("http://example.com/img.png")).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("allows blob URLs", () => {
|
|
24
|
+
expect(isSafeImageSrc("blob:http://example.com/abc-123")).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("allows data:image/png URIs", () => {
|
|
28
|
+
expect(isSafeImageSrc("data:image/png;base64,abc123")).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("allows data:image/jpeg URIs", () => {
|
|
32
|
+
expect(isSafeImageSrc("data:image/jpeg;base64,abc123")).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("allows data:image/gif URIs", () => {
|
|
36
|
+
expect(isSafeImageSrc("data:image/gif;base64,abc123")).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("allows data:image/webp URIs", () => {
|
|
40
|
+
expect(isSafeImageSrc("data:image/webp;base64,abc123")).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("blocks data:image/svg+xml URIs", () => {
|
|
44
|
+
expect(isSafeImageSrc("data:image/svg+xml,<svg onload=alert(1)>")).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("blocks data:image/svg+xml with base64", () => {
|
|
48
|
+
expect(isSafeImageSrc("data:image/svg+xml;base64,PHN2Zz4=")).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("blocks mixed-case SVG data URIs", () => {
|
|
52
|
+
expect(isSafeImageSrc("data:image/Svg+xml,<svg onload=alert(1)>")).toBe(false);
|
|
53
|
+
expect(isSafeImageSrc("data:image/SVG+XML,<svg>")).toBe(false);
|
|
54
|
+
expect(isSafeImageSrc("data:Image/SVG+XML;base64,abc")).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("blocks javascript: URIs", () => {
|
|
58
|
+
expect(isSafeImageSrc("javascript:alert(1)")).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("blocks data:text/html URIs", () => {
|
|
62
|
+
expect(isSafeImageSrc("data:text/html,<script>alert(1)</script>")).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("allows relative paths (no colon)", () => {
|
|
66
|
+
expect(isSafeImageSrc("relative/path.png")).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("allows dot-relative paths", () => {
|
|
70
|
+
expect(isSafeImageSrc("./image.png")).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("allows empty string", () => {
|
|
74
|
+
expect(isSafeImageSrc("")).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("createStandardBubble", () => {
|
|
79
|
+
it("skips rendering blocked image previews while keeping safe ones", () => {
|
|
80
|
+
const bubble = createStandardBubble(
|
|
81
|
+
makeMessage({
|
|
82
|
+
content: "Image attachments",
|
|
83
|
+
contentParts: [
|
|
84
|
+
{ type: "image", image: "https://example.com/safe.png", alt: "Safe image" },
|
|
85
|
+
{ type: "image", image: "data:image/svg+xml,<svg onload=alert(1)>", alt: "Blocked image" },
|
|
86
|
+
],
|
|
87
|
+
}),
|
|
88
|
+
({ text }) => text
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const previewImages = bubble.querySelectorAll('[data-message-attachments="images"] img');
|
|
92
|
+
|
|
93
|
+
expect(previewImages).toHaveLength(1);
|
|
94
|
+
expect(previewImages[0]?.getAttribute("src")).toBe("https://example.com/safe.png");
|
|
95
|
+
expect(previewImages[0]?.getAttribute("alt")).toBe("Safe image");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -12,6 +12,17 @@ import {
|
|
|
12
12
|
import { renderLucideIcon } from "../utils/icons";
|
|
13
13
|
import { IMAGE_ONLY_MESSAGE_FALLBACK_TEXT } from "../utils/content";
|
|
14
14
|
|
|
15
|
+
/** Validate that an image src URL uses a safe scheme (blocks javascript: and SVG data URIs). */
|
|
16
|
+
export const isSafeImageSrc = (src: string): boolean => {
|
|
17
|
+
const lower = src.toLowerCase();
|
|
18
|
+
if (lower.startsWith("data:image/svg+xml")) return false;
|
|
19
|
+
if (/^(?:https?|blob):/i.test(src)) return true;
|
|
20
|
+
if (lower.startsWith("data:image/")) return true;
|
|
21
|
+
// Relative URLs are safe
|
|
22
|
+
if (!src.includes(":")) return true;
|
|
23
|
+
return false;
|
|
24
|
+
};
|
|
25
|
+
|
|
15
26
|
export type LoadingIndicatorRenderer = (context: LoadingIndicatorRenderContext) => HTMLElement | null;
|
|
16
27
|
|
|
17
28
|
export type MessageTransform = (context: {
|
|
@@ -100,8 +111,15 @@ const createMessageImagePreviews = (
|
|
|
100
111
|
settled = true;
|
|
101
112
|
});
|
|
102
113
|
|
|
103
|
-
|
|
104
|
-
|
|
114
|
+
if (isSafeImageSrc(imagePart.image)) {
|
|
115
|
+
imageElement.src = imagePart.image;
|
|
116
|
+
container.appendChild(imageElement);
|
|
117
|
+
} else {
|
|
118
|
+
// Treat blocked images like load errors so fallback triggers correctly
|
|
119
|
+
settled = true;
|
|
120
|
+
visiblePreviewCount = Math.max(0, visiblePreviewCount - 1);
|
|
121
|
+
imageElement.remove();
|
|
122
|
+
}
|
|
105
123
|
});
|
|
106
124
|
|
|
107
125
|
if (visiblePreviewCount === 0) {
|
|
@@ -470,6 +488,8 @@ export const createStandardBubble = (
|
|
|
470
488
|
bubble.id = `bubble-${message.id}`;
|
|
471
489
|
bubble.setAttribute("data-message-id", message.id);
|
|
472
490
|
|
|
491
|
+
bubble.setAttribute("data-persona-theme-zone", message.role === "user" ? "user-message" : "assistant-message");
|
|
492
|
+
|
|
473
493
|
// Apply component-level color overrides via CSS variables
|
|
474
494
|
if (message.role === "user") {
|
|
475
495
|
bubble.style.backgroundColor = 'var(--persona-message-user-bg, var(--persona-accent))';
|
package/src/components/panel.ts
CHANGED
|
@@ -116,6 +116,7 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
|
|
|
116
116
|
"div",
|
|
117
117
|
"persona-widget-container persona-flex persona-h-full persona-w-full persona-flex-1 persona-min-h-0 persona-flex-col persona-bg-persona-surface persona-text-persona-primary persona-rounded-2xl persona-overflow-hidden persona-border persona-border-persona-border"
|
|
118
118
|
);
|
|
119
|
+
container.setAttribute("data-persona-theme-zone", "container");
|
|
119
120
|
|
|
120
121
|
// Build header using layout config if available, otherwise use standard builder
|
|
121
122
|
const headerLayoutConfig = config?.layout?.header;
|
|
@@ -130,6 +131,7 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
|
|
|
130
131
|
"persona-widget-body persona-flex persona-flex-1 persona-min-h-0 persona-flex-col persona-gap-6 persona-overflow-y-auto persona-bg-persona-container persona-px-6 persona-py-6"
|
|
131
132
|
);
|
|
132
133
|
body.id = "persona-scroll-container";
|
|
134
|
+
body.setAttribute("data-persona-theme-zone", "messages");
|
|
133
135
|
|
|
134
136
|
const introCard = createElement(
|
|
135
137
|
"div",
|
package/src/index.ts
CHANGED
|
@@ -121,6 +121,11 @@ export {
|
|
|
121
121
|
createDirectivePostprocessor
|
|
122
122
|
} from "./postprocessors";
|
|
123
123
|
export type { MarkdownProcessorOptions } from "./postprocessors";
|
|
124
|
+
export {
|
|
125
|
+
createDefaultSanitizer,
|
|
126
|
+
resolveSanitizer
|
|
127
|
+
} from "./utils/sanitize";
|
|
128
|
+
export type { SanitizeFunction } from "./utils/sanitize";
|
|
124
129
|
export {
|
|
125
130
|
createPlainTextParser,
|
|
126
131
|
createJsonStreamParser,
|
|
@@ -189,8 +194,10 @@ export {
|
|
|
189
194
|
DEFAULT_PALETTE,
|
|
190
195
|
DEFAULT_SEMANTIC,
|
|
191
196
|
DEFAULT_COMPONENTS,
|
|
192
|
-
validateTheme
|
|
197
|
+
validateTheme,
|
|
198
|
+
THEME_ZONES
|
|
193
199
|
} from "./utils/tokens";
|
|
200
|
+
export type { ThemeZone } from "./utils/tokens";
|
|
194
201
|
export {
|
|
195
202
|
accessibilityPlugin,
|
|
196
203
|
animationsPlugin,
|
|
@@ -219,6 +226,9 @@ export type {
|
|
|
219
226
|
SemanticSpacing,
|
|
220
227
|
SemanticTypography,
|
|
221
228
|
ComponentTokens,
|
|
229
|
+
ArtifactToolbarTokens,
|
|
230
|
+
ArtifactTabTokens,
|
|
231
|
+
ArtifactPaneTokens,
|
|
222
232
|
ThemeValidationResult,
|
|
223
233
|
ThemeValidationError
|
|
224
234
|
} from "./types/theme";
|
|
@@ -245,6 +255,14 @@ export {
|
|
|
245
255
|
DEFAULT_DARK_THEME,
|
|
246
256
|
mergeWithDefaults
|
|
247
257
|
} from "./defaults";
|
|
258
|
+
export {
|
|
259
|
+
PRESETS,
|
|
260
|
+
getPreset,
|
|
261
|
+
PRESET_SHOP,
|
|
262
|
+
PRESET_MINIMAL,
|
|
263
|
+
PRESET_FULLSCREEN
|
|
264
|
+
} from "./presets";
|
|
265
|
+
export type { WidgetPreset } from "./presets";
|
|
248
266
|
|
|
249
267
|
// Layout system exports
|
|
250
268
|
export {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
describe("prototype pollution guard", () => {
|
|
4
|
+
it("strips __proto__, constructor, and prototype from parsed config", () => {
|
|
5
|
+
// Simulates the destructuring pattern used in install.ts
|
|
6
|
+
const malicious = JSON.parse(
|
|
7
|
+
'{"config":{"apiUrl":"http://localhost"},"__proto__":{"polluted":true},"constructor":{"polluted":true},"prototype":{"polluted":true}}'
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
11
|
+
const { __proto__: _a, constructor: _b, prototype: _c, ...safeConfig } = malicious;
|
|
12
|
+
|
|
13
|
+
const target: Record<string, unknown> = {};
|
|
14
|
+
Object.assign(target, safeConfig);
|
|
15
|
+
|
|
16
|
+
// The dangerous keys should not be on the target
|
|
17
|
+
expect(Object.keys(target)).toEqual(["config"]);
|
|
18
|
+
expect((target as any).__proto__).toBe(Object.prototype); // normal prototype, not polluted
|
|
19
|
+
expect((target as any).constructor).toBe(Object); // normal constructor
|
|
20
|
+
|
|
21
|
+
// Global prototype should not be polluted
|
|
22
|
+
expect(({} as any).polluted).toBeUndefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("preserves safe config properties", () => {
|
|
26
|
+
const parsed = JSON.parse(
|
|
27
|
+
'{"config":{"apiUrl":"http://localhost"},"clientToken":"tok_123","flowId":"flow-1"}'
|
|
28
|
+
);
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
30
|
+
const { __proto__: _a, constructor: _b, prototype: _c, ...safeConfig } = parsed;
|
|
31
|
+
|
|
32
|
+
expect(safeConfig).toEqual({
|
|
33
|
+
config: { apiUrl: "http://localhost" },
|
|
34
|
+
clientToken: "tok_123",
|
|
35
|
+
flowId: "flow-1",
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
package/src/install.ts
CHANGED
|
@@ -58,7 +58,9 @@ declare global {
|
|
|
58
58
|
const parsedConfig = JSON.parse(configJson);
|
|
59
59
|
// If it has nested 'config' property, use it; otherwise treat as widget config
|
|
60
60
|
if (parsedConfig.config) {
|
|
61
|
-
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
62
|
+
const { __proto__: _a, constructor: _b, prototype: _c, ...safeConfig } = parsedConfig;
|
|
63
|
+
Object.assign(scriptConfig, safeConfig);
|
|
62
64
|
} else {
|
|
63
65
|
// Treat the entire object as widget config
|
|
64
66
|
scriptConfig.config = parsedConfig;
|