html-preview-sandbox 0.1.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/CHANGELOG.md +45 -0
- package/LICENSE +21 -0
- package/README.md +163 -0
- package/SECURITY.md +40 -0
- package/THREAT_MODEL.md +51 -0
- package/dist/bridge.d.ts +3 -0
- package/dist/decode.d.ts +12 -0
- package/dist/document.browser.d.ts +2 -0
- package/dist/document.d.ts +2 -0
- package/dist/errors.d.ts +12 -0
- package/dist/index.browser.d.ts +9 -0
- package/dist/index.browser.js +988 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +990 -0
- package/dist/index.js.map +1 -0
- package/dist/policy.d.ts +10 -0
- package/dist/renderer-core.d.ts +4 -0
- package/dist/renderer.browser.d.ts +1 -0
- package/dist/renderer.d.ts +1 -0
- package/dist/sanitize-core.d.ts +5 -0
- package/dist/sanitize.browser.d.ts +4 -0
- package/dist/sanitize.d.ts +4 -0
- package/dist/scrollbar.d.ts +2 -0
- package/dist/types.d.ts +84 -0
- package/docs/BRANCHING.md +94 -0
- package/docs/BROWSER_SUPPORT.md +42 -0
- package/docs/INTEGRATION.md +145 -0
- package/docs/PROJECT_STRUCTURE.md +121 -0
- package/docs/ROADMAP.md +184 -0
- package/docs/SANITIZER_DECISION.md +78 -0
- package/docs/SECURITY_MODEL.md +100 -0
- package/docs//344/275/277/347/224/250/346/226/207/346/241/243.zh-CN.md +204 -0
- package/package.json +86 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/decode.ts","../src/sanitize.ts","../src/sanitize-core.ts","../src/policy.ts","../src/bridge.ts","../src/scrollbar.ts","../src/document.ts","../src/renderer-core.ts","../src/renderer.ts"],"sourcesContent":["import type { PreviewErrorCode } from './types.js';\n\nexport class PreviewError extends Error {\n code: PreviewErrorCode;\n cause?: unknown;\n\n constructor(code: PreviewErrorCode, message: string, cause?: unknown) {\n super(message);\n this.name = 'PreviewError';\n this.code = code;\n this.cause = cause;\n }\n}\n\nexport const ERROR_CODES = {\n OVERSIZED: 'OVERSIZED',\n DECODE_FAILED: 'DECODE_FAILED',\n EMPTY_AFTER_SANITIZE: 'EMPTY_AFTER_SANITIZE',\n RENDER_FAILED: 'RENDER_FAILED',\n} as const satisfies Record<string, PreviewErrorCode>;\n","import { ERROR_CODES, PreviewError } from './errors.js';\nimport type { PreviewInput } from './types.js';\n\nexport const DEFAULT_MAX_BYTES = 100 * 1024 * 1024;\n\nconst BOM_UTF8 = [0xef, 0xbb, 0xbf];\nconst BOM_UTF16_LE = [0xff, 0xfe];\nconst BOM_UTF16_BE = [0xfe, 0xff];\n\nexport interface DecodeResult {\n html: string;\n encoding: string;\n usedBom: boolean;\n size: number;\n}\n\nfunction startsWithBytes(bytes: Uint8Array, prefix: number[]): boolean {\n if (bytes.length < prefix.length) return false;\n return prefix.every((byte, index) => bytes[index] === byte);\n}\n\nfunction decodeWith(bytes: Uint8Array, encoding: string, fatal = false): string | null {\n try {\n return new TextDecoder(encoding, { fatal }).decode(bytes);\n } catch {\n return null;\n }\n}\n\nfunction findDeclaredCharset(head: string): string | null {\n const metaCharset = head.match(/<meta[^>]+charset\\s*=\\s*[\"']?([\\w-]+)/i);\n if (metaCharset?.[1]) return metaCharset[1].toLowerCase();\n\n const contentType = head.match(/charset\\s*=\\s*[\"']?([\\w-]+)/i);\n if (contentType?.[1]) return contentType[1].toLowerCase();\n\n return null;\n}\n\nexport async function normalizeInput(input: PreviewInput, options: { maxBytes?: number } = {}): Promise<DecodeResult> {\n const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n\n if (typeof input === 'string') {\n const size = new TextEncoder().encode(input).byteLength;\n if (size > maxBytes) {\n throw new PreviewError(ERROR_CODES.OVERSIZED, `HTML input exceeds ${maxBytes} bytes`);\n }\n return { html: input, encoding: 'utf-8', usedBom: false, size };\n }\n\n let bytes: Uint8Array;\n if (input instanceof Uint8Array) {\n bytes = input;\n } else if (input instanceof ArrayBuffer) {\n bytes = new Uint8Array(input);\n } else if (typeof Blob !== 'undefined' && input instanceof Blob) {\n // Check Blob.size before reading — arrayBuffer() would materialize the whole\n // file into memory first, so an oversized Blob must be rejected up front.\n if (input.size > maxBytes) {\n throw new PreviewError(ERROR_CODES.OVERSIZED, `HTML input exceeds ${maxBytes} bytes`);\n }\n bytes = new Uint8Array(await input.arrayBuffer());\n } else {\n throw new PreviewError(ERROR_CODES.DECODE_FAILED, 'Unsupported preview input');\n }\n\n if (bytes.byteLength > maxBytes) {\n throw new PreviewError(ERROR_CODES.OVERSIZED, `HTML input exceeds ${maxBytes} bytes`);\n }\n\n return decodeHtmlBytes(bytes);\n}\n\nexport function decodeHtmlBytes(input: ArrayBuffer | Uint8Array): DecodeResult {\n const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);\n\n if (startsWithBytes(bytes, BOM_UTF8)) {\n return {\n html: decodeWith(bytes.subarray(BOM_UTF8.length), 'utf-8') ?? '',\n encoding: 'utf-8',\n usedBom: true,\n size: bytes.byteLength,\n };\n }\n\n if (startsWithBytes(bytes, BOM_UTF16_LE)) {\n return {\n html: decodeWith(bytes.subarray(BOM_UTF16_LE.length), 'utf-16le') ?? '',\n encoding: 'utf-16le',\n usedBom: true,\n size: bytes.byteLength,\n };\n }\n\n if (startsWithBytes(bytes, BOM_UTF16_BE)) {\n return {\n html: decodeWith(bytes.subarray(BOM_UTF16_BE.length), 'utf-16be') ?? '',\n encoding: 'utf-16be',\n usedBom: true,\n size: bytes.byteLength,\n };\n }\n\n const strictUtf8 = decodeWith(bytes, 'utf-8', true);\n if (strictUtf8 !== null) {\n return { html: strictUtf8, encoding: 'utf-8', usedBom: false, size: bytes.byteLength };\n }\n\n const head = decodeWith(bytes.subarray(0, 4096), 'iso-8859-1') ?? '';\n const declared = findDeclaredCharset(head);\n\n if (declared && declared !== 'utf-8' && declared !== 'utf8') {\n const decoded = decodeWith(bytes, declared);\n if (decoded !== null) {\n return { html: decoded, encoding: declared, usedBom: false, size: bytes.byteLength };\n }\n }\n\n return {\n html: decodeWith(bytes, 'utf-8') ?? '',\n encoding: 'utf-8',\n usedBom: false,\n size: bytes.byteLength,\n };\n}\n","import createDOMPurify from 'dompurify';\nimport { JSDOM } from 'jsdom';\nimport { createSanitizer } from './sanitize-core.js';\n\nconst nodeWindow = new JSDOM('').window;\n\nexport const sanitizeHtml = createSanitizer(nodeWindow, createDOMPurify);\n","import type { SanitizeOptions, SanitizeReport } from './types.js';\n\nconst DEFAULT_ALLOWED_TAGS = [\n 'html', 'head', 'body', 'title', 'meta', 'link', 'style', 'script',\n 'main', 'section', 'article', 'aside', 'header', 'footer', 'nav',\n 'div', 'span', 'p', 'br', 'hr', 'pre', 'code', 'blockquote',\n 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',\n 'ul', 'ol', 'li', 'dl', 'dt', 'dd',\n 'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'caption', 'colgroup', 'col',\n 'a', 'strong', 'b', 'em', 'i', 'u', 's', 'small', 'mark', 'sub', 'sup',\n 'img', 'picture', 'source', 'audio', 'video', 'canvas',\n 'form', 'label', 'input', 'button', 'select', 'option', 'optgroup', 'textarea',\n 'fieldset', 'legend', 'details', 'summary',\n 'svg', 'g', 'defs', 'symbol', 'use', 'desc',\n 'rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'path',\n 'text', 'tspan', 'textPath', 'linearGradient', 'radialGradient', 'stop',\n 'pattern', 'mask', 'clipPath', 'filter', 'feGaussianBlur', 'feColorMatrix',\n 'feOffset', 'feMerge', 'feMergeNode', 'feBlend', 'feFlood', 'feComposite',\n 'feTurbulence', 'feDisplacementMap', 'feDropShadow', 'marker',\n 'animate', 'animateTransform', 'animateMotion', 'set', 'foreignObject',\n];\n\nconst DISALLOWED_TAGS = ['base', 'object', 'embed', 'applet'];\n\nconst GLOBAL_ATTRS = [\n 'class', 'id', 'style', 'title', 'lang', 'dir', 'role', 'tabindex', 'hidden',\n 'aria-*', 'data-*',\n];\n\nconst SAFE_INLINE_EVENTS = [\n 'onclick', 'onauxclick', 'ondblclick',\n 'oninput', 'onchange', 'onsubmit', 'onreset',\n 'onkeydown', 'onkeyup', 'onkeypress',\n 'onfocusin', 'onfocusout',\n];\n\nconst SVG_ATTRS = [\n 'x', 'y', 'cx', 'cy', 'r', 'rx', 'ry', 'x1', 'y1', 'x2', 'y2',\n 'width', 'height', 'd', 'points', 'transform', 'fill', 'fill-opacity',\n 'fill-rule', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin',\n 'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity', 'stroke-miterlimit',\n 'opacity', 'color', 'visibility', 'display', 'vector-effect',\n 'viewBox', 'preserveAspectRatio', 'xmlns', 'xmlns:xlink',\n 'href', 'xlink:href', 'offset', 'gradientUnits', 'gradientTransform',\n 'spreadMethod', 'stop-color', 'stop-opacity', 'text-anchor',\n 'dominant-baseline', 'alignment-baseline', 'font-family', 'font-size',\n 'font-weight', 'font-style', 'letter-spacing', 'word-spacing',\n 'text-decoration', 'dx', 'dy', 'rotate', 'startOffset', 'textLength',\n 'lengthAdjust', 'clip-path', 'clip-rule', 'mask', 'filter', 'stdDeviation',\n 'in', 'in2', 'result', 'mode', 'flood-color', 'flood-opacity', 'edgeMode',\n 'patternUnits', 'patternTransform', 'patternContentUnits', 'markerWidth',\n 'markerHeight', 'refX', 'refY', 'orient', 'markerUnits', 'marker-start',\n 'marker-mid', 'marker-end', 'attributeName', 'attributeType', 'from', 'to',\n 'by', 'values', 'dur', 'repeatCount', 'repeatDur', 'begin', 'end',\n 'calcMode', 'keyTimes', 'keySplines', 'additive', 'accumulate',\n 'fill-mode', 'restart',\n];\n\nconst ATTRS_BY_TAG: Record<string, string[]> = {\n a: ['href', 'name', 'target', 'rel', 'download'],\n link: ['rel', 'href', 'as', 'crossorigin', 'integrity', 'referrerpolicy', 'type', 'media', 'sizes'],\n meta: ['charset', 'name', 'content', 'http-equiv', 'property'],\n img: ['src', 'alt', 'width', 'height', 'loading', 'decoding', 'srcset', 'sizes'],\n source: ['src', 'srcset', 'type', 'media'],\n input: ['type', 'name', 'value', 'placeholder', 'min', 'max', 'step', 'required', 'checked', 'disabled', 'readonly', 'pattern', 'maxlength'],\n button: ['type', 'name', 'value', 'disabled'],\n select: ['name', 'value', 'multiple', 'disabled', 'required'],\n option: ['value', 'selected', 'disabled'],\n textarea: ['name', 'value', 'placeholder', 'rows', 'cols', 'maxlength', 'required', 'disabled', 'readonly'],\n form: ['method', 'enctype', 'novalidate'],\n label: ['for'],\n audio: ['src', 'controls', 'autoplay', 'loop', 'muted', 'preload'],\n video: ['src', 'controls', 'autoplay', 'loop', 'muted', 'preload', 'poster', 'width', 'height'],\n script: ['src', 'type', 'async', 'defer', 'crossorigin', 'integrity', 'referrerpolicy', 'nomodule', 'nonce'],\n};\n\nconst URL_ATTRS = new Set(['href', 'src', 'srcset', 'cite', 'xlink:href', 'action']);\nconst DEFAULT_SCHEMES = new Set(['http:', 'https:', 'mailto:', 'tel:']);\nconst MEDIA_TAGS = new Set(['img', 'audio', 'video', 'source', 'picture']);\n\nfunction emptyReport(): SanitizeReport {\n return {\n removedTags: [],\n removedAttributes: [],\n removedSchemes: [],\n strippedAll: false,\n };\n}\n\ntype CountedRecord = { count: number; [key: string]: unknown };\n\nfunction increment<T extends CountedRecord>(list: T[], predicate: (item: T) => boolean, create: () => Omit<T, 'count'>): void {\n const existing = list.find(predicate);\n if (existing) {\n existing.count += 1;\n return;\n }\n list.push({ ...create(), count: 1 } as T);\n}\n\nfunction reportTag(report: SanitizeReport, tag: string): void {\n increment(report.removedTags, (item) => item.tag === tag, () => ({ tag }));\n}\n\nfunction reportAttr(report: SanitizeReport, tag: string, attr: string): void {\n increment(report.removedAttributes, (item) => item.tag === tag && item.attr === attr, () => ({ tag, attr }));\n}\n\nfunction reportScheme(report: SanitizeReport, scheme: string): void {\n increment(report.removedSchemes, (item) => item.scheme === scheme, () => ({ scheme }));\n}\n\nfunction normalizeName(name: unknown): string {\n return String(name || '').toLowerCase();\n}\n\nfunction matchesPattern(attr: string, pattern: string): boolean {\n const normalizedPattern = normalizeName(pattern);\n return normalizedPattern.endsWith('*')\n ? attr.startsWith(normalizedPattern.slice(0, -1))\n : attr === normalizedPattern;\n}\n\nfunction getAllowedTags(options: SanitizeOptions = {}): Set<string> {\n const tags = new Set(DEFAULT_ALLOWED_TAGS.map(normalizeName));\n if (options.allowScripts === false) tags.delete('script');\n for (const tag of options.extraTags ?? []) tags.add(normalizeName(tag));\n for (const tag of options.dropTags ?? []) tags.delete(normalizeName(tag));\n return tags;\n}\n\nfunction getAllowedAttrs(options: SanitizeOptions = {}): string[] {\n return [\n ...GLOBAL_ATTRS,\n ...(options.allowInlineEvents === false ? [] : SAFE_INLINE_EVENTS),\n ...SVG_ATTRS,\n ...Object.values(ATTRS_BY_TAG).flat(),\n ...(options.extraAttributes?.['*'] ?? []),\n ...Object.values(options.extraAttributes ?? {}).flat(),\n ];\n}\n\nfunction isAllowedAttr(tag: string, attr: string, options: SanitizeOptions = {}): boolean {\n if (GLOBAL_ATTRS.some((pattern) => matchesPattern(attr, pattern))) return true;\n if (options.allowInlineEvents !== false && SAFE_INLINE_EVENTS.map(normalizeName).includes(attr)) return true;\n if (SVG_ATTRS.map(normalizeName).includes(attr)) return true;\n if ((ATTRS_BY_TAG[tag] ?? []).map(normalizeName).includes(attr)) return true;\n if ((options.extraAttributes?.['*'] ?? []).map(normalizeName).includes(attr)) return true;\n if ((options.extraAttributes?.[tag] ?? []).map(normalizeName).includes(attr)) return true;\n return false;\n}\n\nfunction getScheme(raw: unknown): string {\n const trimmed = String(raw || '').trim();\n const match = trimmed.match(/^([a-zA-Z][\\w+.-]*):/);\n return match ? `${match[1].toLowerCase()}:` : '';\n}\n\nfunction isAllowedUrl(raw: unknown, tag: string, options: SanitizeOptions = {}): boolean {\n const scheme = getScheme(raw);\n if (!scheme) return true;\n if (DEFAULT_SCHEMES.has(scheme)) return true;\n if (MEDIA_TAGS.has(tag) && (scheme === 'data:' || scheme === 'blob:')) return true;\n return (options.extraSchemes ?? [])\n .map((item) => (item.endsWith(':') ? item.toLowerCase() : `${item.toLowerCase()}:`))\n .includes(scheme);\n}\n\nfunction splitSrcsetCandidates(raw: unknown): string[] {\n return String(raw || '')\n .split(',')\n .map((candidate) => candidate.trim())\n .filter(Boolean)\n .map((candidate) => candidate.split(/\\s+/)[0])\n .filter(Boolean);\n}\n\nfunction disallowedSrcsetSchemes(raw: unknown, tag: string, options: SanitizeOptions = {}): string[] {\n const schemes = new Set<string>();\n for (const url of splitSrcsetCandidates(raw)) {\n if (!isAllowedUrl(url, tag, options)) {\n schemes.add(getScheme(url) || 'invalid');\n }\n }\n return [...schemes];\n}\n\nfunction isDangerousMeta(node: Element): boolean {\n if (normalizeName(node.nodeName) !== 'meta') return false;\n const equiv = normalizeName(node.getAttribute('http-equiv'));\n return equiv === 'refresh' || equiv === 'content-security-policy';\n}\n\n// SVG animation elements can rewrite URL attributes at runtime\n// (<animate attributeName=\"href\" values=\"javascript:...\">), and the animation\n// value attributes (values/from/to/by) do not go through URL scheme filtering.\n// Any animation element that targets a URL attribute is removed entirely.\nconst ANIMATION_TAGS = new Set(['animate', 'set', 'animatemotion', 'animatetransform']);\nconst URL_TARGET_ATTRS = new Set(['href', 'xlink:href', 'src']);\n\nfunction isUrlTargetingAnimation(node: Element): boolean {\n if (!ANIMATION_TAGS.has(normalizeName(node.nodeName))) return false;\n const target = normalizeName(node.getAttribute?.('attributeName'));\n return URL_TARGET_ATTRS.has(target);\n}\n\n// Resource-hint <link rel> values that can open a connection or fetch a resource\n// outside consistent CSP governance (browsers vary on whether preconnect/dns-prefetch\n// are subject to CSP). They are stripped at the sanitizer layer rather than relying on\n// CSP alone. Stylesheet/preload/modulepreload/icon remain allowed — those are governed\n// by the matching CSP directive (style-src / script-src / img-src).\nconst BLOCKED_LINK_RELS = new Set(['dns-prefetch', 'preconnect', 'prefetch', 'prerender']);\n\nfunction isBlockedResourceHintLink(node: Element): boolean {\n if (normalizeName(node.nodeName) !== 'link') return false;\n const rel = normalizeName(node.getAttribute?.('rel'));\n if (!rel) return false;\n return rel.split(/\\s+/).some((token) => BLOCKED_LINK_RELS.has(token));\n}\n\nfunction applyProjectHooks(purifier: any, report: SanitizeReport, options: SanitizeOptions): void {\n // Nodes flagged for removal during uponSanitizeElement (where their original\n // attributes are still readable) but detached later in afterSanitizeElements,\n // because detaching mid-walk breaks DOMPurify's tree traversal.\n const pendingRemoval = new WeakSet<Element>();\n\n purifier.addHook('uponSanitizeElement', (node: Element, data: { tagName?: string }) => {\n const tag = normalizeName(data.tagName || node.nodeName);\n if (DISALLOWED_TAGS.includes(tag)) reportTag(report, tag);\n if (isDangerousMeta(node)) reportAttr(report, 'meta', 'http-equiv');\n // Detect URL-targeting SVG animations here, before DOMPurify strips their\n // attributeName/values attributes (which would hide the vector from later hooks).\n if (isUrlTargetingAnimation(node)) {\n reportTag(report, tag);\n pendingRemoval.add(node);\n }\n // Drop connection/prefetch resource-hint links (see BLOCKED_LINK_RELS).\n if (isBlockedResourceHintLink(node)) {\n reportTag(report, tag);\n pendingRemoval.add(node);\n }\n });\n\n purifier.addHook('afterSanitizeElements', (node: Element) => {\n if (isDangerousMeta(node) || pendingRemoval.has(node)) {\n node.remove();\n }\n });\n\n purifier.addHook('afterSanitizeAttributes', (node: Element) => {\n const tag = normalizeName(node.nodeName);\n if (!node.attributes) return;\n\n for (const attrNode of [...node.attributes]) {\n const attr = normalizeName(attrNode.name);\n if (!isAllowedAttr(tag, attr, options)) {\n reportAttr(report, tag, attr);\n node.removeAttribute(attrNode.name);\n continue;\n }\n\n if (URL_ATTRS.has(attr) && !isAllowedUrl(attrNode.value, tag, options)) {\n reportAttr(report, tag, attr);\n reportScheme(report, getScheme(attrNode.value) || 'invalid');\n node.removeAttribute(attrNode.name);\n continue;\n }\n\n if (attr === 'srcset') {\n const blockedSchemes = disallowedSrcsetSchemes(attrNode.value, tag, options);\n if (blockedSchemes.length) {\n reportAttr(report, tag, attr);\n for (const scheme of blockedSchemes) reportScheme(report, scheme);\n node.removeAttribute(attrNode.name);\n }\n }\n }\n });\n}\n\nfunction collectDomPurifyRemoved(purifier: any, report: SanitizeReport): void {\n for (const item of purifier.removed ?? []) {\n if (item.element) {\n reportTag(report, normalizeName(item.element.nodeName));\n }\n if (item.attribute) {\n reportAttr(report, normalizeName(item.from?.nodeName || '*'), normalizeName(item.attribute.name));\n if (URL_ATTRS.has(normalizeName(item.attribute.name))) {\n reportScheme(report, getScheme(item.attribute.value) || 'invalid');\n }\n }\n }\n}\n\nexport function createSanitizer(runtimeWindow: any, createDOMPurify: any) {\n return function sanitizeHtml(rawHtml: string, options: SanitizeOptions = {}): { html: string; report: SanitizeReport } {\n const purifier = createDOMPurify(runtimeWindow);\n const report = emptyReport();\n const allowedTags = [...getAllowedTags(options)];\n\n applyProjectHooks(purifier, report, options);\n\n const html = purifier.sanitize(rawHtml, {\n WHOLE_DOCUMENT: true,\n // ALLOWED_TAGS/ALLOWED_ATTR are authoritative (they replace DOMPurify's\n // defaults). Per-tag attribute tightening happens in the afterSanitizeAttributes\n // hook; DISALLOWED_TAGS is the hard deny-list.\n ALLOWED_TAGS: allowedTags,\n ALLOWED_ATTR: getAllowedAttrs(options),\n FORBID_TAGS: DISALLOWED_TAGS,\n KEEP_CONTENT: true,\n ALLOW_DATA_ATTR: true,\n ALLOW_ARIA_ATTR: true,\n });\n\n collectDomPurifyRemoved(purifier, report);\n\n const parser = new runtimeWindow.DOMParser();\n const doc = parser.parseFromString(html, 'text/html');\n report.strippedAll = !doc.body.textContent?.trim() && !doc.body.querySelector('script,style,img,svg,canvas,video,audio');\n\n return { html, report };\n };\n}\n","import type { CspPolicy, CspPreset } from './types.js';\n\nconst DEFAULT_SCRIPT_HOSTS = [\n 'https://cdnjs.cloudflare.com',\n 'https://cdn.jsdelivr.net',\n 'https://cdn.tailwindcss.com',\n 'https://code.jquery.com',\n];\n\nconst DEFAULT_STYLE_HOSTS = [\n 'https://cdnjs.cloudflare.com',\n 'https://cdn.jsdelivr.net',\n 'https://cdn.tailwindcss.com',\n 'https://fonts.googleapis.com',\n];\n\nconst DEFAULT_FONT_HOSTS = ['https://fonts.gstatic.com'];\n\nexport const DEFAULT_SANDBOX_TOKENS = [\n 'allow-scripts',\n 'allow-forms',\n 'allow-popups',\n 'allow-popups-to-escape-sandbox',\n 'allow-modals',\n];\n\nexport const UNSAFE_SANDBOX_TOKENS = [\n 'allow-same-origin',\n 'allow-top-navigation',\n 'allow-top-navigation-by-user-activation',\n 'allow-downloads',\n];\n\nexport const DEFAULT_ALLOWED_EXTERNAL_PROTOCOLS = ['http:', 'https:', 'mailto:', 'tel:'];\n\nfunction normalizePolicy(policy?: CspPreset | CspPolicy): CspPolicy {\n if (!policy) return { preset: 'strict' };\n if (typeof policy === 'string') return { preset: policy };\n return { preset: 'strict', ...policy };\n}\n\nfunction join(values: string[]): string {\n return [...new Set(values.filter(Boolean))].join(' ');\n}\n\n// Presets are layered by exfiltration capability (egress), not resource loading (ingress):\n// offline — no network at all, nothing in or out.\n// strict — default. Blocks attacker-readable exfiltration channels (connect-src 'none',\n// form-action 'none', no wildcard img/media hosts). Fixed static CDN/font hosts\n// may be loaded; inline script and 'unsafe-eval' are permitted because they add\n// no attacker capability beyond already-permitted inline scripts and provide no\n// attacker-readable exfiltration sink.\n// balanced — compatibility-first. Opens https: images/media and connect-src https:,\n// which IS a general-purpose exfiltration surface; for semi-trusted content only.\nexport function buildCsp(policy?: CspPreset | CspPolicy): string {\n const normalized = normalizePolicy(policy);\n const preset = normalized.preset ?? 'strict';\n\n const scriptHosts = [...DEFAULT_SCRIPT_HOSTS, ...(normalized.scriptHosts ?? [])];\n const styleHosts = [...DEFAULT_STYLE_HOSTS, ...(normalized.styleHosts ?? [])];\n const fontHosts = [...DEFAULT_FONT_HOSTS, ...(normalized.fontHosts ?? [])];\n const imgHosts = normalized.imgHosts ?? [];\n const connectHosts = normalized.connectHosts ?? [];\n\n const directives: Record<string, string> = {\n 'default-src': \"'none'\",\n 'script-src': join([\"'unsafe-inline'\", \"'unsafe-eval'\", ...scriptHosts]),\n 'style-src': join([\"'unsafe-inline'\", ...styleHosts]),\n 'img-src': join(['blob:', 'data:', ...imgHosts]),\n 'media-src': 'blob: data:',\n 'font-src': join(['data:', ...fontHosts]),\n 'connect-src': \"'none'\",\n 'worker-src': \"'none'\",\n 'frame-src': 'blob:',\n 'object-src': \"'none'\",\n 'base-uri': \"'none'\",\n 'form-action': \"'none'\",\n 'upgrade-insecure-requests': '',\n 'block-all-mixed-content': '',\n };\n\n if (preset === 'balanced') {\n directives['script-src'] = join([\"'unsafe-inline'\", \"'unsafe-eval'\", ...scriptHosts, 'https://unpkg.com']);\n directives['style-src'] = join([\"'unsafe-inline'\", ...styleHosts, 'https://unpkg.com']);\n directives['img-src'] = join(['https:', 'blob:', 'data:', ...imgHosts]);\n directives['media-src'] = join(['https:', 'blob:', 'data:']);\n directives['connect-src'] = connectHosts.length ? join(['https:', ...connectHosts]) : 'https:';\n }\n\n if (preset === 'offline') {\n directives['script-src'] = \"'unsafe-inline'\";\n directives['style-src'] = \"'unsafe-inline'\";\n directives['img-src'] = 'blob: data:';\n directives['media-src'] = 'blob: data:';\n directives['font-src'] = 'data:';\n directives['connect-src'] = \"'none'\";\n }\n\n Object.assign(directives, normalized.directives ?? {});\n\n return Object.entries(directives)\n .map(([name, value]) => (value ? `${name} ${value}` : name))\n .join('; ');\n}\n\nexport function injectCspMeta(html: string, csp: string): string {\n const escaped = csp.replace(/\"/g, '"');\n const meta = `<meta http-equiv=\"Content-Security-Policy\" content=\"${escaped}\">`;\n\n if (/<head[^>]*>/i.test(html)) {\n return html.replace(/<head[^>]*>/i, (match: string) => `${match}${meta}`);\n }\n\n if (/<html[^>]*>/i.test(html)) {\n return html.replace(/<html[^>]*>/i, (match: string) => `${match}<head>${meta}</head>`);\n }\n\n return `<!doctype html><html><head>${meta}</head><body>${html}</body></html>`;\n}\n\nexport function getSandboxAttribute(tokens = DEFAULT_SANDBOX_TOKENS, options: { allowUnsafeTokens?: boolean } = {}): string {\n const allowUnsafeTokens = options.allowUnsafeTokens === true;\n return [...new Set(tokens)]\n .filter((token) => allowUnsafeTokens || !UNSAFE_SANDBOX_TOKENS.includes(token))\n .join(' ');\n}\n\nexport function isAllowedExternalUrl(url: string, protocols = DEFAULT_ALLOWED_EXTERNAL_PROTOCOLS): boolean {\n try {\n const parsed = new URL(url);\n const allowedProtocols = protocols.map((protocol) => {\n const normalized = String(protocol || '').toLowerCase();\n return normalized.endsWith(':') ? normalized : `${normalized}:`;\n });\n return allowedProtocols.includes(parsed.protocol);\n } catch {\n return false;\n }\n}\n","export const BRIDGE_OPEN_EXTERNAL = 'html-preview-sandbox:openExternal';\nexport const BRIDGE_CSP_VIOLATION = 'html-preview-sandbox:cspViolation';\n\nconst CSP_VIOLATION_LIMIT = 30;\n\nfunction bridgeScript(): string {\n return `<script>\n(() => {\n const OPEN_EXTERNAL = ${JSON.stringify(BRIDGE_OPEN_EXTERNAL)};\n const CSP_VIOLATION = ${JSON.stringify(BRIDGE_CSP_VIOLATION)};\n const LIMIT = ${CSP_VIOLATION_LIMIT};\n\n function post(type, payload) {\n try {\n window.parent.postMessage(Object.assign({ type }, payload), '*');\n } catch (_) {}\n }\n\n function postExternal(href, source) {\n post(OPEN_EXTERNAL, { href: String(href || ''), source });\n }\n\n document.addEventListener('click', (event) => {\n let element = event.target;\n while (element && element.nodeName !== 'A') element = element.parentElement;\n if (!element) return;\n\n const href = element.getAttribute('href');\n const resolved = element.href || '';\n\n if (href == null || href === '' || href.charAt(0) === '#') {\n event.preventDefault();\n event.stopPropagation();\n const fragment = href && href.length > 1 ? href.slice(1) : '';\n if (fragment) {\n let target = null;\n try {\n target = document.getElementById(fragment) || document.querySelector('a[name=\"' + CSS.escape(fragment) + '\"]');\n } catch (_) {}\n if (target && target.scrollIntoView) target.scrollIntoView({ behavior: 'smooth', block: 'start' });\n } else if (href === '#') {\n try { window.scrollTo({ top: 0, behavior: 'smooth' }); } catch (_) {}\n }\n return;\n }\n\n // Defense in depth: even if a relaxed custom sanitizer let a javascript: URL\n // through, the bridge must not allow the default click to run it.\n if (/^javascript:/i.test(resolved)) {\n event.preventDefault();\n event.stopPropagation();\n return;\n }\n\n try {\n const url = new URL(resolved);\n if (url.protocol === 'about:') return;\n } catch (_) {\n return;\n }\n\n event.preventDefault();\n event.stopPropagation();\n postExternal(resolved, 'link');\n }, true);\n\n window.open = (url) => {\n if (url) postExternal(url, 'window.open');\n return null;\n };\n\n const seen = new Set();\n let sent = 0;\n document.addEventListener('securitypolicyviolation', (event) => {\n if (sent >= LIMIT) return;\n const key = (event.effectiveDirective || event.violatedDirective || '') + '|' + (event.blockedURI || '');\n if (seen.has(key)) return;\n seen.add(key);\n sent += 1;\n post(CSP_VIOLATION, {\n blockedURI: String(event.blockedURI || ''),\n violatedDirective: String(event.violatedDirective || ''),\n effectiveDirective: String(event.effectiveDirective || ''),\n sourceFile: String(event.sourceFile || ''),\n lineNumber: Number(event.lineNumber || 0),\n columnNumber: Number(event.columnNumber || 0),\n disposition: String(event.disposition || ''),\n sample: String(event.sample || '').slice(0, 120),\n });\n });\n})();\n</script>`;\n}\n\nexport function injectBridgeScript(html: string): string {\n const script = bridgeScript();\n const cspMeta = /<meta[^>]*http-equiv\\s*=\\s*[\"']Content-Security-Policy[\"'][^>]*>/i;\n\n if (cspMeta.test(html)) {\n return html.replace(cspMeta, (match: string) => `${match}${script}`);\n }\n\n if (/<\\/head>/i.test(html)) {\n return html.replace(/<\\/head>/i, `${script}</head>`);\n }\n\n if (/<body[^>]*>/i.test(html)) {\n return html.replace(/<body[^>]*>/i, (match: string) => `${match}${script}`);\n }\n\n return `${html}${script}`;\n}\n","const AUTHOR_SCROLLBAR_RE = /::-webkit-scrollbar|scrollbar-width|scrollbar-color/i;\n\nconst SCROLLBAR_STYLE = `<style data-html-preview-sandbox-scrollbar>\n:root {\n color-scheme: light;\n}\nhtml, body {\n min-height: 100%;\n}\n* {\n scrollbar-width: thin;\n scrollbar-color: rgba(96, 112, 128, 0.45) transparent;\n}\n*::-webkit-scrollbar {\n width: 10px;\n height: 10px;\n}\n*::-webkit-scrollbar-thumb {\n background: rgba(96, 112, 128, 0.38);\n border-radius: 999px;\n border: 2px solid transparent;\n background-clip: content-box;\n}\n</style>`;\n\nexport function hasAuthorScrollbarStyle(html: string): boolean {\n return AUTHOR_SCROLLBAR_RE.test(html || '');\n}\n\nexport function injectScrollbarStyle(html: string): string {\n if (hasAuthorScrollbarStyle(html)) return html;\n\n if (/<\\/head>/i.test(html)) {\n return html.replace(/<\\/head>/i, `${SCROLLBAR_STYLE}</head>`);\n }\n\n if (/<body[^>]*>/i.test(html)) {\n return html.replace(/<body[^>]*>/i, (match: string) => `${match}${SCROLLBAR_STYLE}`);\n }\n\n return `${html}${SCROLLBAR_STYLE}`;\n}\n","import { normalizeInput } from './decode.js';\nimport { sanitizeHtml } from './sanitize.js';\nimport { buildCsp, injectCspMeta } from './policy.js';\nimport { injectBridgeScript } from './bridge.js';\nimport { injectScrollbarStyle } from './scrollbar.js';\nimport { ERROR_CODES, PreviewError } from './errors.js';\nimport type { PreviewInput, PreviewOptions, RenderResult } from './types.js';\n\nexport async function createHtmlDocument(input: PreviewInput, options: PreviewOptions = {}): Promise<RenderResult> {\n const decoded = await normalizeInput(input, { maxBytes: options.maxBytes });\n const sanitized = sanitizeHtml(decoded.html, options.sanitize);\n\n if (!sanitized.html.trim()) {\n sanitized.report.strippedAll = true;\n throw new PreviewError(ERROR_CODES.EMPTY_AFTER_SANITIZE, 'HTML is empty after sanitization');\n }\n\n const csp = buildCsp(options.csp);\n const withCsp = injectCspMeta(sanitized.html, csp);\n const withBridge = injectBridgeScript(withCsp);\n const html = options.injectScrollbarStyle === false ? withBridge : injectScrollbarStyle(withBridge);\n\n return {\n html,\n encoding: decoded.encoding,\n sanitizeReport: sanitized.report,\n };\n}\n","import { BRIDGE_CSP_VIOLATION, BRIDGE_OPEN_EXTERNAL } from './bridge.js';\nimport { DEFAULT_SANDBOX_TOKENS, getSandboxAttribute, isAllowedExternalUrl } from './policy.js';\nimport { ERROR_CODES, PreviewError } from './errors.js';\nimport type { OpenExternalSource, PreviewErrorShape, PreviewHandle, PreviewInput, PreviewOptions, RenderResult } from './types.js';\n\nfunction toMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction toPreviewError(error: unknown): PreviewErrorShape {\n return error instanceof PreviewError ? error : new PreviewError(ERROR_CODES.RENDER_FAILED, toMessage(error), error);\n}\n\ntype OpenExternalContext = { source: OpenExternalSource };\ntype CreateHtmlDocument = (input: PreviewInput, options?: PreviewOptions) => Promise<RenderResult>;\n\n// The Node and browser entrypoints share this renderer; they only differ in\n// which createHtmlDocument implementation (jsdom-backed vs real DOM) they inject.\nexport function createPreviewFactory(createHtmlDocument: CreateHtmlDocument) {\n return function createPreview(container: HTMLElement, options: PreviewOptions = {}): PreviewHandle {\n if (!container || typeof container.appendChild !== 'function') {\n throw new TypeError('createPreview requires a container HTMLElement');\n }\n\n let currentOptions = { ...options };\n let iframe: HTMLIFrameElement | null = null;\n let destroyed = false;\n let lastHtml = '';\n\n function removeIframe(): void {\n if (iframe?.parentNode) iframe.parentNode.removeChild(iframe);\n iframe = null;\n }\n\n function ensureIframe(): HTMLIFrameElement {\n removeIframe();\n iframe = document.createElement('iframe');\n iframe.setAttribute('sandbox', getSandboxAttribute(currentOptions.sandboxTokens ?? DEFAULT_SANDBOX_TOKENS, {\n allowUnsafeTokens: currentOptions.allowUnsafeSandboxTokens,\n }));\n iframe.setAttribute('referrerpolicy', 'no-referrer');\n iframe.setAttribute('title', 'HTML preview sandbox');\n iframe.style.width = '100%';\n iframe.style.height = '100%';\n iframe.style.border = '0';\n iframe.style.display = 'block';\n iframe.style.background = '#fff';\n container.appendChild(iframe);\n return iframe;\n }\n\n function handleMessage(event: MessageEvent): void {\n const data = event?.data;\n if (!data || typeof data !== 'object') return;\n if (!iframe || event.source !== iframe.contentWindow) return;\n\n if ('type' in data && data.type === BRIDGE_CSP_VIOLATION) {\n currentOptions.onCspViolation?.({\n effectiveDirective: String(data.effectiveDirective || data.violatedDirective || ''),\n blockedURI: String(data.blockedURI || ''),\n sourceFile: String(data.sourceFile || ''),\n lineNumber: Number(data.lineNumber || 0),\n sample: String(data.sample || ''),\n });\n return;\n }\n\n if ('type' in data && data.type === BRIDGE_OPEN_EXTERNAL) {\n const href = String(data.href || '');\n const source: OpenExternalSource = data.source === 'window.open' ? 'window.open' : 'link';\n const context: OpenExternalContext = { source };\n if (!isAllowedExternal(href, context)) {\n currentOptions.logger?.warn?.(`Blocked external URL: ${href}`);\n return;\n }\n currentOptions.onOpenExternal?.(href, context);\n }\n }\n\n function isAllowedExternal(url: string, context: OpenExternalContext): boolean {\n if (!isAllowedExternalUrl(url, currentOptions.externalProtocols)) return false;\n if (!currentOptions.allowExternalUrl) return true;\n try {\n return currentOptions.allowExternalUrl(url, context) === true;\n } catch (error) {\n currentOptions.logger?.warn?.(`External URL policy threw: ${toMessage(error)}`);\n return false;\n }\n }\n\n function remountLastHtml(): void {\n if (!lastHtml) return;\n const target = ensureIframe();\n target.srcdoc = lastHtml;\n }\n\n window.addEventListener('message', handleMessage);\n\n return {\n async render(input) {\n if (destroyed) throw new Error('Preview has been destroyed');\n try {\n const result = await createHtmlDocument(input, currentOptions);\n currentOptions.onSanitize?.(result.sanitizeReport);\n lastHtml = result.html;\n const target = ensureIframe();\n target.srcdoc = result.html;\n return result;\n } catch (error) {\n currentOptions.onError?.(toPreviewError(error));\n throw error;\n }\n },\n updateOptions(patch) {\n currentOptions = { ...currentOptions, ...patch };\n },\n notifyNavigationAttempt(url, context = { source: 'navigation' as const }) {\n const href = String(url || '');\n if (!href) return;\n currentOptions.onNavigationAttempt?.(href, context);\n remountLastHtml();\n if (isAllowedExternal(href, context)) {\n currentOptions.onOpenExternal?.(href, context);\n } else {\n currentOptions.logger?.warn?.(`Blocked navigation URL: ${href}`);\n }\n },\n destroy() {\n destroyed = true;\n window.removeEventListener('message', handleMessage);\n removeIframe();\n lastHtml = '';\n },\n get iframe() {\n return iframe;\n },\n };\n };\n}\n","import { createHtmlDocument } from './document.js';\nimport { createPreviewFactory } from './renderer-core.js';\n\nexport const createPreview = createPreviewFactory(createHtmlDocument);\n"],"mappings":"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","names":["createDOMPurify","sanitizeHtml","createHtmlDocument","createPreview"]}
|
package/dist/policy.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CspPolicy, CspPreset } from './types.js';
|
|
2
|
+
export declare const DEFAULT_SANDBOX_TOKENS: string[];
|
|
3
|
+
export declare const UNSAFE_SANDBOX_TOKENS: string[];
|
|
4
|
+
export declare const DEFAULT_ALLOWED_EXTERNAL_PROTOCOLS: string[];
|
|
5
|
+
export declare function buildCsp(policy?: CspPreset | CspPolicy): string;
|
|
6
|
+
export declare function injectCspMeta(html: string, csp: string): string;
|
|
7
|
+
export declare function getSandboxAttribute(tokens?: string[], options?: {
|
|
8
|
+
allowUnsafeTokens?: boolean;
|
|
9
|
+
}): string;
|
|
10
|
+
export declare function isAllowedExternalUrl(url: string, protocols?: string[]): boolean;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { PreviewHandle, PreviewInput, PreviewOptions, RenderResult } from './types.js';
|
|
2
|
+
type CreateHtmlDocument = (input: PreviewInput, options?: PreviewOptions) => Promise<RenderResult>;
|
|
3
|
+
export declare function createPreviewFactory(createHtmlDocument: CreateHtmlDocument): (container: HTMLElement, options?: PreviewOptions) => PreviewHandle;
|
|
4
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const createPreview: (container: HTMLElement, options?: import("./types.js").PreviewOptions) => import("./types.js").PreviewHandle;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const createPreview: (container: HTMLElement, options?: import("./types.js").PreviewOptions) => import("./types.js").PreviewHandle;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export type PreviewInput = string | ArrayBuffer | Uint8Array | Blob | File;
|
|
2
|
+
export type CspPreset = 'strict' | 'balanced' | 'offline';
|
|
3
|
+
export type OpenExternalSource = 'link' | 'window.open' | 'navigation';
|
|
4
|
+
export interface CspPolicy {
|
|
5
|
+
preset?: CspPreset;
|
|
6
|
+
scriptHosts?: string[];
|
|
7
|
+
styleHosts?: string[];
|
|
8
|
+
imgHosts?: string[];
|
|
9
|
+
fontHosts?: string[];
|
|
10
|
+
connectHosts?: string[];
|
|
11
|
+
directives?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
export interface SanitizeOptions {
|
|
14
|
+
allowScripts?: boolean;
|
|
15
|
+
allowInlineEvents?: boolean;
|
|
16
|
+
extraTags?: string[];
|
|
17
|
+
extraAttributes?: Record<string, string[]>;
|
|
18
|
+
extraSchemes?: string[];
|
|
19
|
+
dropTags?: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface SanitizeReport {
|
|
22
|
+
removedTags: Array<{
|
|
23
|
+
tag: string;
|
|
24
|
+
count: number;
|
|
25
|
+
}>;
|
|
26
|
+
removedAttributes: Array<{
|
|
27
|
+
tag: string;
|
|
28
|
+
attr: string;
|
|
29
|
+
count: number;
|
|
30
|
+
}>;
|
|
31
|
+
removedSchemes: Array<{
|
|
32
|
+
scheme: string;
|
|
33
|
+
count: number;
|
|
34
|
+
}>;
|
|
35
|
+
strippedAll: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface CspViolationReport {
|
|
38
|
+
effectiveDirective: string;
|
|
39
|
+
blockedURI: string;
|
|
40
|
+
sourceFile?: string;
|
|
41
|
+
lineNumber?: number;
|
|
42
|
+
sample?: string;
|
|
43
|
+
}
|
|
44
|
+
export type PreviewErrorCode = 'OVERSIZED' | 'DECODE_FAILED' | 'EMPTY_AFTER_SANITIZE' | 'RENDER_FAILED';
|
|
45
|
+
export interface PreviewErrorShape extends Error {
|
|
46
|
+
code: PreviewErrorCode;
|
|
47
|
+
cause?: unknown;
|
|
48
|
+
}
|
|
49
|
+
export interface PreviewOptions {
|
|
50
|
+
csp?: CspPreset | CspPolicy;
|
|
51
|
+
sanitize?: SanitizeOptions;
|
|
52
|
+
maxBytes?: number;
|
|
53
|
+
sandboxTokens?: string[];
|
|
54
|
+
allowUnsafeSandboxTokens?: boolean;
|
|
55
|
+
externalProtocols?: string[];
|
|
56
|
+
allowExternalUrl?: (url: string, context: {
|
|
57
|
+
source: OpenExternalSource;
|
|
58
|
+
}) => boolean;
|
|
59
|
+
injectScrollbarStyle?: boolean;
|
|
60
|
+
logger?: Pick<Console, 'info' | 'warn' | 'error'>;
|
|
61
|
+
onOpenExternal?: (url: string, context: {
|
|
62
|
+
source: OpenExternalSource;
|
|
63
|
+
}) => void;
|
|
64
|
+
onCspViolation?: (report: CspViolationReport) => void;
|
|
65
|
+
onSanitize?: (report: SanitizeReport) => void;
|
|
66
|
+
onNavigationAttempt?: (url: string, context: {
|
|
67
|
+
source: OpenExternalSource;
|
|
68
|
+
}) => void;
|
|
69
|
+
onError?: (error: PreviewErrorShape) => void;
|
|
70
|
+
}
|
|
71
|
+
export interface RenderResult {
|
|
72
|
+
html: string;
|
|
73
|
+
encoding: string;
|
|
74
|
+
sanitizeReport: SanitizeReport;
|
|
75
|
+
}
|
|
76
|
+
export interface PreviewHandle {
|
|
77
|
+
render(input: PreviewInput): Promise<RenderResult>;
|
|
78
|
+
updateOptions(patch: Partial<PreviewOptions>): void;
|
|
79
|
+
notifyNavigationAttempt(url: string, context?: {
|
|
80
|
+
source: OpenExternalSource;
|
|
81
|
+
}): void;
|
|
82
|
+
destroy(): void;
|
|
83
|
+
readonly iframe: HTMLIFrameElement | null;
|
|
84
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Branching & Release Management
|
|
2
|
+
|
|
3
|
+
This project uses a **trunk-based** model with tag-triggered releases. It is
|
|
4
|
+
intentionally simple so a small team (or a single maintainer) can keep it green
|
|
5
|
+
and shippable at all times.
|
|
6
|
+
|
|
7
|
+
## Branches
|
|
8
|
+
|
|
9
|
+
| Branch | Purpose | Rules |
|
|
10
|
+
|--------|---------|-------|
|
|
11
|
+
| `main` | The single long-lived branch. Always releasable, always green. | Protected: changes land via PR; CI must pass; at least one review when more than one maintainer exists. Never commit directly to `main` for non-trivial work. |
|
|
12
|
+
| `feat/<topic>` | New features | Branch from `main`, open a PR back into `main`. |
|
|
13
|
+
| `fix/<topic>` | Bug fixes | Same as above. |
|
|
14
|
+
| `docs/<topic>` | Documentation-only changes | Same as above. |
|
|
15
|
+
| `chore/<topic>` | Tooling, deps, CI | Same as above. |
|
|
16
|
+
|
|
17
|
+
Security fixes are developed privately (see [Security Fixes](#security-fixes)),
|
|
18
|
+
not on public branches, until an advisory is ready.
|
|
19
|
+
|
|
20
|
+
## Pull Request Flow
|
|
21
|
+
|
|
22
|
+
1. Branch from up-to-date `main`.
|
|
23
|
+
2. Make the change; keep it focused (unrelated refactors go in separate PRs).
|
|
24
|
+
3. Run the local gate: `npm run check && npm run test:browser`.
|
|
25
|
+
4. Open a PR. The template checklist must pass, including:
|
|
26
|
+
- independent-implementation boundary respected;
|
|
27
|
+
- security-affecting changes carry regression tests and fixtures;
|
|
28
|
+
- docs updated and consistent with actual behavior.
|
|
29
|
+
5. CI runs type check, Node tests, browser tests, CodeQL, and `pack:dry`.
|
|
30
|
+
6. Squash-merge into `main` once green and reviewed.
|
|
31
|
+
|
|
32
|
+
## Versioning
|
|
33
|
+
|
|
34
|
+
Semantic Versioning. During `0.x` the public API may change between **minor**
|
|
35
|
+
versions — breaking changes are allowed but must be called out in the PR and in
|
|
36
|
+
`CHANGELOG.md`. After the API stabilizes, cut `1.0.0` and follow strict SemVer.
|
|
37
|
+
|
|
38
|
+
- **patch** (`0.1.0 → 0.1.1`): bug fixes, security fixes, doc-only shipped changes.
|
|
39
|
+
- **minor** (`0.1.x → 0.2.0`): new features; in `0.x`, also allowed breaking changes.
|
|
40
|
+
- **major** (`→ 1.0.0` and beyond): first stable API; thereafter breaking changes only.
|
|
41
|
+
|
|
42
|
+
## Release Process
|
|
43
|
+
|
|
44
|
+
Releases are automated by [`.github/workflows/release.yml`](../.github/workflows/release.yml),
|
|
45
|
+
triggered by pushing a `v*` tag. To cut a release:
|
|
46
|
+
|
|
47
|
+
1. Ensure `main` is green and `CHANGELOG.md` has the release notes under a new
|
|
48
|
+
version heading (move items out of `Unreleased`).
|
|
49
|
+
2. Bump `version` in `package.json` to match (e.g. `0.1.1`).
|
|
50
|
+
3. Commit: `chore: release v0.1.1`.
|
|
51
|
+
4. Tag and push:
|
|
52
|
+
```bash
|
|
53
|
+
git tag v0.1.1
|
|
54
|
+
git push origin main --tags
|
|
55
|
+
```
|
|
56
|
+
5. The release workflow runs the full test suite, verifies the tag matches
|
|
57
|
+
`package.json` version, and publishes to npm with `--provenance`.
|
|
58
|
+
|
|
59
|
+
The tag/version consistency check will fail the release if the `v*` tag does not
|
|
60
|
+
match `package.json`, preventing accidental mismatched publishes.
|
|
61
|
+
|
|
62
|
+
### Prerequisites (one-time repository setup)
|
|
63
|
+
|
|
64
|
+
Confirm the identity triple matches everywhere before the first publish:
|
|
65
|
+
|
|
66
|
+
- GitHub org: `preview-sandbox`
|
|
67
|
+
- GitHub repo: `html-preview-sandbox`
|
|
68
|
+
- npm package: `html-preview-sandbox`
|
|
69
|
+
|
|
70
|
+
Then set up the repository:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git remote add origin git@github.com:preview-sandbox/html-preview-sandbox.git
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- [ ] `NPM_TOKEN` (npm automation token) stored as a repository secret; the account must own or maintain the `html-preview-sandbox` npm package.
|
|
77
|
+
- [ ] GitHub Security Advisories enabled (private vulnerability reporting).
|
|
78
|
+
- [ ] `main` branch protection: require PRs, require CI to pass, and require review once there is more than one maintainer.
|
|
79
|
+
- [ ] CI (`ci.yml`), CodeQL (`codeql.yml`), and the release workflow (`release.yml`) present on the default branch.
|
|
80
|
+
|
|
81
|
+
## Security Fixes
|
|
82
|
+
|
|
83
|
+
Do **not** open public issues, PRs, or branches for vulnerabilities. Follow
|
|
84
|
+
[SECURITY.md](../SECURITY.md): report privately via GitHub Security Advisories.
|
|
85
|
+
Fixes are developed in the advisory's temporary private fork, then merged to
|
|
86
|
+
`main` and released as a patch version alongside the published advisory.
|
|
87
|
+
|
|
88
|
+
## Changelog
|
|
89
|
+
|
|
90
|
+
`CHANGELOG.md` is maintained manually in [Keep a Changelog](https://keepachangelog.com/)
|
|
91
|
+
format. Every user-facing change adds an entry under `Unreleased`; cutting a
|
|
92
|
+
release moves those entries under the new version heading. (Adopting an automated
|
|
93
|
+
tool such as changesets is an open question in [ROADMAP.md](ROADMAP.md) but is not
|
|
94
|
+
needed while the project has a single maintainer.)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Browser Support
|
|
2
|
+
|
|
3
|
+
`html-preview-sandbox` targets modern evergreen browsers and recent Electron. There is no polyfill layer; the library relies directly on the platform features listed below.
|
|
4
|
+
|
|
5
|
+
## Supported Baseline
|
|
6
|
+
|
|
7
|
+
| Environment | Minimum |
|
|
8
|
+
|-------------|---------|
|
|
9
|
+
| Chrome / Edge | 90+ |
|
|
10
|
+
| Firefox | 90+ |
|
|
11
|
+
| Safari | 14+ |
|
|
12
|
+
| Electron | 12+ (Chromium 89+) |
|
|
13
|
+
| Node (for the jsdom-backed sanitizer path and tests) | 18+ |
|
|
14
|
+
|
|
15
|
+
The binding constraint is `Blob.prototype.arrayBuffer()` (Safari 14, 2020). Everything else the library uses is available earlier.
|
|
16
|
+
|
|
17
|
+
## Platform Features Used
|
|
18
|
+
|
|
19
|
+
Host side (`createPreview` / renderer):
|
|
20
|
+
|
|
21
|
+
- **Sandboxed `<iframe>` with `srcdoc`** — the core isolation primitive. The default sandbox omits `allow-same-origin`, giving the document an opaque origin.
|
|
22
|
+
- **`postMessage` / `message` events** — the bridge channel between the sandboxed document and the host.
|
|
23
|
+
- **`TextDecoder`** — charset decoding in `decode` (UTF-8, UTF-16, and declared legacy charsets such as GBK).
|
|
24
|
+
- **`Blob.prototype.arrayBuffer()`** — reading `Blob`/`File` inputs.
|
|
25
|
+
- **`URL`** — external-link protocol parsing and validation.
|
|
26
|
+
|
|
27
|
+
Injected bridge (runs inside the sandboxed document):
|
|
28
|
+
|
|
29
|
+
- **`securitypolicyviolation` event** — CSP violation reporting, since `report-uri`/`report-to` are ignored under a `<meta>` CSP.
|
|
30
|
+
- **`CSS.escape`** — safe fragment lookup for in-page anchor scrolling.
|
|
31
|
+
- Modern syntax (arrow functions, `const`/`let`, template literals, `Set`) — the bridge is injected as ES2020-level source and runs in the target browser.
|
|
32
|
+
|
|
33
|
+
Sanitizer:
|
|
34
|
+
|
|
35
|
+
- **Browser entrypoint**: DOMPurify against the real `window`.
|
|
36
|
+
- **Node entrypoint**: DOMPurify against a jsdom `window`.
|
|
37
|
+
|
|
38
|
+
## Notes
|
|
39
|
+
|
|
40
|
+
- CSP is delivered as a `<meta http-equiv="Content-Security-Policy">` tag. All supported browsers honor meta CSP for the directives this library uses; `report-uri`/`report-to` are intentionally not used (they are ignored in meta CSP), which is why violation reporting goes through the `securitypolicyviolation` event and the bridge instead.
|
|
41
|
+
- The `strict`/`balanced`/`offline` presets rely only on CSP directives that are broadly supported across the baseline. `upgrade-insecure-requests` and `block-all-mixed-content` degrade gracefully where unsupported.
|
|
42
|
+
- Older browsers are not tested and not supported. If a wider baseline is required, that would be a deliberate future scope decision (see `ROADMAP.md`).
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Integration Guide
|
|
2
|
+
|
|
3
|
+
This guide describes how to embed `html-preview-sandbox` in a host application.
|
|
4
|
+
|
|
5
|
+
## Basic Web Integration
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { createPreview } from 'html-preview-sandbox';
|
|
9
|
+
|
|
10
|
+
const preview = createPreview(document.querySelector('#preview'), {
|
|
11
|
+
csp: 'strict',
|
|
12
|
+
onOpenExternal(url) {
|
|
13
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
14
|
+
},
|
|
15
|
+
onSanitize(report) {
|
|
16
|
+
console.info(report);
|
|
17
|
+
},
|
|
18
|
+
onCspViolation(report) {
|
|
19
|
+
console.warn(report);
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
await preview.render(fileOrHtmlString);
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Modern bundlers should resolve the package `browser` export automatically. Repository examples run against `dist/index.browser.js`; run `npm run build` before opening them locally.
|
|
27
|
+
|
|
28
|
+
If your bundler or runtime does not honor the `browser` condition, import the explicit browser subpath:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
import { createPreview } from 'html-preview-sandbox/browser';
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The sanitizer is DOMPurify in both runtimes. Browser integrations use the real browser `window`; Node integrations use jsdom.
|
|
35
|
+
|
|
36
|
+
The default iframe sandbox omits `allow-same-origin`. Previewed content receives an opaque origin and cannot read host cookies, storage, or DOM.
|
|
37
|
+
|
|
38
|
+
## External Links
|
|
39
|
+
|
|
40
|
+
Links and `window.open()` calls inside the sandbox are forwarded to `onOpenExternal`.
|
|
41
|
+
|
|
42
|
+
If `onOpenExternal` is not provided, external requests are discarded. This is intentional fail-closed behavior.
|
|
43
|
+
|
|
44
|
+
Always validate or route external URLs in the host application. The core package allows only `http:`, `https:`, `mailto:`, and `tel:` by default.
|
|
45
|
+
|
|
46
|
+
Use `externalProtocols` to narrow the default protocol allowlist:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
const preview = createPreview(container, {
|
|
50
|
+
externalProtocols: ['https:'],
|
|
51
|
+
onOpenExternal(url) {
|
|
52
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Use `allowExternalUrl` for host-specific policy. If this callback is provided, it must return `true` to allow the URL:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
const preview = createPreview(container, {
|
|
61
|
+
externalProtocols: ['https:'],
|
|
62
|
+
allowExternalUrl(url, context) {
|
|
63
|
+
const parsed = new URL(url);
|
|
64
|
+
return context.source === 'link' && parsed.hostname.endsWith('.example.com');
|
|
65
|
+
},
|
|
66
|
+
onOpenExternal(url) {
|
|
67
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The same external URL policy is used for bridge-forwarded links, `window.open`, and host-reported navigation attempts.
|
|
73
|
+
|
|
74
|
+
## Navigation Attempts
|
|
75
|
+
|
|
76
|
+
Pure Web pages cannot reliably intercept all `window.location` assignments from a sandboxed iframe. Hosts that can observe iframe navigation, such as Electron main-process code, should call:
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
preview.notifyNavigationAttempt(url);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The preview will restore the last generated `srcdoc` and route the URL through the same external-link allowlist.
|
|
83
|
+
|
|
84
|
+
See [`examples/electron/`](../examples/electron/) for a complete main-process interception example (`did-start-navigation` / `will-frame-navigate` → IPC → `notifyNavigationAttempt`), and [`examples/web-component/`](../examples/web-component/) for a `<safe-html-preview>` custom-element wrapper.
|
|
85
|
+
|
|
86
|
+
## Examples
|
|
87
|
+
|
|
88
|
+
- [`examples/web/`](../examples/web/) — minimal string-input integration.
|
|
89
|
+
- [`examples/file-upload/`](../examples/file-upload/) — `<input type=file>` + drag & drop, `render(file)`, showing detected encoding, sanitizer removals, and the `OVERSIZED` error state. The shape for attachment / upload / netdisk previews.
|
|
90
|
+
- [`examples/web-component/`](../examples/web-component/) — `<safe-html-preview>` custom element (framework-agnostic).
|
|
91
|
+
- [`examples/electron/`](../examples/electron/) — desktop host with main-process navigation interception.
|
|
92
|
+
- [`examples/node-create-document/`](../examples/node-create-document/) — `createHtmlDocument` (full pipeline, no iframe) for self-managed webviews, SSR pre-processing, or CLI conversion.
|
|
93
|
+
|
|
94
|
+
## Presets
|
|
95
|
+
|
|
96
|
+
Presets are layered by exfiltration capability, not by which resources they load:
|
|
97
|
+
|
|
98
|
+
- `strict`: default. Blocks attacker-readable exfiltration (`connect-src 'none'`, `form-action 'none'`, no wildcard `img-src`/`media-src`). Allows inline script, `unsafe-eval`, and fixed static CDN/font hosts, so most self-contained reports render without opening a data-exit channel.
|
|
99
|
+
- `balanced`: opens HTTPS images/media and `connect-src https:` for semi-trusted reports that need remote resources or API calls. This is a general-purpose exfiltration surface.
|
|
100
|
+
- `offline`: no network at all — local `data:`/`blob:` resources only.
|
|
101
|
+
|
|
102
|
+
Use `strict` unless users explicitly expect remote images/media or API calls, in which case use `balanced`. See `../THREAT_MODEL.md` for the residual side channels that remain under `strict`.
|
|
103
|
+
|
|
104
|
+
## Sandbox Overrides
|
|
105
|
+
|
|
106
|
+
The default sandbox omits high-risk permissions. If you pass custom `sandboxTokens`, the package still filters these tokens by default:
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
allow-same-origin allow-top-navigation allow-top-navigation-by-user-activation allow-downloads
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Only enable `allowUnsafeSandboxTokens` for controlled, trusted content. For untrusted HTML previews, keeping an opaque origin is a core part of the security model.
|
|
113
|
+
|
|
114
|
+
## Large Files
|
|
115
|
+
|
|
116
|
+
The default input size limit is 100 MB. Override it with `maxBytes` only after testing performance in your host application.
|
|
117
|
+
|
|
118
|
+
## Type Safety
|
|
119
|
+
|
|
120
|
+
The package is implemented in TypeScript and publishes generated declarations. Public options such as `externalProtocols`, `allowExternalUrl`, and `allowUnsafeSandboxTokens` are covered by the same API surface used by TypeScript consumers.
|
|
121
|
+
|
|
122
|
+
Run this before changing public API behavior:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npm run check:types
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Playground
|
|
129
|
+
|
|
130
|
+
The repository Playground runs from the built browser bundle:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
npm run build
|
|
134
|
+
npm run serve
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Open `http://localhost:4173/playground/`. The workbench includes sample payloads, CSP preset switching, external protocol controls, host suffix filtering, and live inspector panels for sanitizer removals, CSP violations, external requests, and current policy.
|
|
138
|
+
|
|
139
|
+
## Recommended Host Behavior
|
|
140
|
+
|
|
141
|
+
- Show a clear empty/error state when sanitization removes all content.
|
|
142
|
+
- Route external links through a user-visible action.
|
|
143
|
+
- Keep `allow-same-origin` disabled unless you fully understand the risk.
|
|
144
|
+
- Prefer `strict` or `offline` for untrusted attachments.
|
|
145
|
+
- Add host-level navigation interception if your platform supports it.
|