UNPKG

@assistant-ui/react

Version:

Open-source TypeScript/React library for building production-grade AI chat experiences

92 lines (91 loc) • 3.39 kB
// src/mcp-apps/csp.ts const isAllowedOrigin = (value, allowWebSocket) => { if (!(allowWebSocket ? /^(?:https?|wss?):\/\//i : /^https?:\/\//i).test(value) || /[\s;'"`,]/.test(value)) return false; try { const url = new URL(value.replace("://*.", "://wildcard.")); return url.username === "" && url.password === "" && url.pathname === "/" && url.search === "" && url.hash === ""; } catch { return false; } }; const getAllowedOrigins = (values, allowWebSocket = false) => Array.isArray(values) ? [...new Set(values.filter((value) => typeof value === "string" && isAllowedOrigin(value, allowWebSocket)))] : []; const directive = (name, values) => `${name} ${values.join(" ")}`; const buildMcpAppCsp = (csp) => { const connectDomains = getAllowedOrigins(csp?.connectDomains, true); const resourceDomains = getAllowedOrigins(csp?.resourceDomains); const frameDomains = getAllowedOrigins(csp?.frameDomains); const baseUriDomains = getAllowedOrigins(csp?.baseUriDomains); return [ "default-src 'none'", directive("script-src", [ "'self'", "'unsafe-inline'", ...resourceDomains ]), directive("style-src", [ "'self'", "'unsafe-inline'", ...resourceDomains ]), directive("connect-src", connectDomains.length > 0 ? ["'self'", ...connectDomains] : ["'none'"]), directive("img-src", [ "'self'", "data:", ...resourceDomains ]), directive("font-src", ["'self'", ...resourceDomains]), directive("media-src", [ "'self'", "data:", ...resourceDomains ]), directive("frame-src", frameDomains.length > 0 ? frameDomains : ["'none'"]), directive("base-uri", baseUriDomains.length > 0 ? baseUriDomains : ["'self'"]), "object-src 'none'", "form-action 'none'" ].join("; "); }; const findDoctypeEnd = (html) => { let offset = 0; while (offset < html.length) { const whitespace = html.slice(offset).match(/^[\t\n\f\r ]+/)?.[0]; if (whitespace) { offset += whitespace.length; continue; } if (html.startsWith("<!--", offset)) { if (html.startsWith("<!-->", offset)) { offset += 5; continue; } if (html.startsWith("<!--->", offset)) { offset += 6; continue; } const commentEnd = html.indexOf("-->", offset + 4); const bangCommentEnd = html.indexOf("--!>", offset + 4); if (commentEnd === -1 && bangCommentEnd === -1) return 0; offset = bangCommentEnd !== -1 && (commentEnd === -1 || bangCommentEnd < commentEnd) ? bangCommentEnd + 4 : commentEnd + 3; continue; } if (html.slice(offset, offset + 9).toLowerCase() === "<!doctype") break; if (html.startsWith("<?", offset) || html.startsWith("<!", offset)) { const bogusCommentEnd = html.indexOf(">", offset + 2); if (bogusCommentEnd === -1) return 0; offset = bogusCommentEnd + 1; continue; } break; } if (html.slice(offset, offset + 9).toLowerCase() !== "<!doctype") return offset; const end = html.indexOf(">", offset + 9); return end === -1 ? 0 : end + 1; }; const applyMcpAppCsp = (html, csp) => { const normalizedHtml = html.charCodeAt(0) === 65279 ? html.slice(1) : html; const meta = `<meta http-equiv="Content-Security-Policy" content="${buildMcpAppCsp(csp)}">`; const insertionPoint = findDoctypeEnd(normalizedHtml); return `${normalizedHtml.slice(0, insertionPoint)}${meta}${normalizedHtml.slice(insertionPoint)}`; }; export { applyMcpAppCsp, buildMcpAppCsp }; //# sourceMappingURL=csp.js.map