feat(voyage): vendor DOMPurify >=3.1.1 + sanitize annotation-content
This commit is contained in:
parent
e839ba2a7a
commit
fc8c9eecdd
5 changed files with 105 additions and 6 deletions
|
|
@ -1,9 +1,10 @@
|
|||
{
|
||||
"generated_at": "2026-05-09T13:16:03.483Z",
|
||||
"generated_at": "2026-05-10T15:59:53.379Z",
|
||||
"pins": {
|
||||
"markdown-it": "14.1.0",
|
||||
"markdown-it-front-matter": "0.2.4",
|
||||
"highlight.js": "11.11.1"
|
||||
"highlight.js": "11.11.1",
|
||||
"dompurify": "3.2.6"
|
||||
},
|
||||
"highlight_languages": [
|
||||
"yaml",
|
||||
|
|
@ -16,6 +17,7 @@
|
|||
"output_files": [
|
||||
"markdown-it.min.js",
|
||||
"markdown-it-front-matter.min.js",
|
||||
"highlight.min.js"
|
||||
"highlight.min.js",
|
||||
"dompurify.min.js"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
3
plugins/voyage/playground/lib/dompurify.min.js
vendored
Normal file
3
plugins/voyage/playground/lib/dompurify.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1135,6 +1135,10 @@
|
|||
<script src="lib/markdown-it.min.js"></script>
|
||||
<script src="lib/markdown-it-front-matter.min.js"></script>
|
||||
<script src="lib/highlight.min.js"></script>
|
||||
<!-- v4.3 Step 24 — DOMPurify ≥ 3.1.1 (UMD bundle exposes window.DOMPurify).
|
||||
Used by sanitizeAnnotation() to scrub annotation rich-text before DOM
|
||||
insertion. Pinned via scripts/vendor-playground-libs.mjs. -->
|
||||
<script src="lib/dompurify.min.js"></script>
|
||||
|
||||
<!-- Sample plan inlined for Step 8 first-run experience.
|
||||
Same content as tests/fixtures/annotation/annotation-plan.md (truncated). -->
|
||||
|
|
@ -1244,6 +1248,27 @@ playground first-run shows a complete round-trip-able artifact.
|
|||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// v4.3 Step 24 — DOMPurify-backed annotation-content sanitizer.
|
||||
// Strips <script>, inline styles, and event-handler attributes; keeps
|
||||
// a small allowlist of inline-formatting tags so users can paste basic
|
||||
// rich-text (bold/italic/code) without breaking export round-trip.
|
||||
// Falls back to escapeHtml when DOMPurify is unavailable (file:// in a
|
||||
// browser without the vendored bundle, or test environments).
|
||||
function sanitizeAnnotation(html) {
|
||||
var input = String(html == null ? '' : html);
|
||||
if (typeof window !== 'undefined' && window.DOMPurify && typeof window.DOMPurify.sanitize === 'function') {
|
||||
return window.DOMPurify.sanitize(input, {
|
||||
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'code'],
|
||||
ALLOWED_ATTR: [],
|
||||
FORBID_TAGS: ['style', 'script'],
|
||||
FORBID_ATTR: ['style', 'onerror', 'onload'],
|
||||
});
|
||||
}
|
||||
// Defensive fallback — never inject untrusted HTML if DOMPurify
|
||||
// failed to load.
|
||||
return escapeHtml(input);
|
||||
}
|
||||
|
||||
// Parse frontmatter yourself (cheap line-walk) so deriveStorageKey can
|
||||
// see slug/type without wire-tapping the plugin.
|
||||
function quickParseFrontmatter(text) {
|
||||
|
|
@ -2306,7 +2331,11 @@ playground first-run shows a complete round-trip-able artifact.
|
|||
(annot.line ? '<span class="critique-card__line">linje ' + annot.line + '</span>' : '') +
|
||||
'</div>' +
|
||||
(annot.snippet ? '<div class="critique-card__snippet">' + escapeHtmlInline(annot.snippet) + '</div>' : '') +
|
||||
'<div class="critique-card__comment">' + escapeHtmlInline(annot.comment || '') + '</div>' +
|
||||
// v4.3 Step 24 — comment is user-entered rich-text; route through
|
||||
// sanitizeAnnotation (DOMPurify-backed) so basic inline-formatting
|
||||
// tags survive while <script>, inline styles, and event-handler
|
||||
// attributes are stripped before DOM insertion.
|
||||
'<div class="critique-card__comment">' + sanitizeAnnotation(annot.comment || '') + '</div>' +
|
||||
'<div class="critique-card__status' + (annot.exported ? ' critique-card__status--exported' : '') + '">' +
|
||||
(annot.exported ? 'Eksportert' : 'Pending') +
|
||||
'</div>';
|
||||
|
|
|
|||
|
|
@ -9,9 +9,10 @@
|
|||
// - markdown-it@14.1.0 (UMD bundle copied verbatim)
|
||||
// - markdown-it-front-matter@0.2.4 (CommonJS module wrapped in IIFE)
|
||||
// - highlight.js@11.11.1 (5-lang bundle assembled from CommonJS sources)
|
||||
// - dompurify@3.2.6 (UMD bundle copied verbatim) — v4.3 Step 24
|
||||
//
|
||||
// Output: playground/lib/{markdown-it.min.js, markdown-it-front-matter.min.js,
|
||||
// highlight.min.js}
|
||||
// highlight.min.js, dompurify.min.js}
|
||||
//
|
||||
// All three output files are zero-network browser-loadable scripts that
|
||||
// expose globals (`window.markdownit`, `window.markdownitFrontMatter`,
|
||||
|
|
@ -32,6 +33,9 @@ const PINS = {
|
|||
'markdown-it': '14.1.0',
|
||||
'markdown-it-front-matter': '0.2.4',
|
||||
'highlight.js': '11.11.1',
|
||||
// v4.3 Step 24 — pinned ≥ 3.1.1 (PortSwigger HTML-comment mutation-XSS bypass
|
||||
// was fixed in 3.1.x; 3.2.6 is the current stable line as of 2026-05-10).
|
||||
'dompurify': '3.2.6',
|
||||
};
|
||||
|
||||
const HL_LANGS = ['yaml', 'json', 'javascript', 'bash', 'markdown', 'diff'];
|
||||
|
|
@ -77,7 +81,20 @@ function vendor() {
|
|||
writeFileSync(join(OUT, 'highlight.min.js'), hlBundle);
|
||||
log(`wrote ${join(OUT, 'highlight.min.js')} (${HL_LANGS.length} langs)`);
|
||||
|
||||
// 4. MANIFEST — record the vendored versions for audit
|
||||
// 4. dompurify — copy UMD min bundle directly (v4.3 Step 24).
|
||||
// Mirrors markdown-it-vendoring: npm pack → tar xzf → copy
|
||||
// dist/purify.min.js → playground/lib/dompurify.min.js. The UMD bundle
|
||||
// exposes `window.DOMPurify` for browser-loadable use.
|
||||
log('packing dompurify@' + PINS['dompurify']);
|
||||
execSync(`npm pack dompurify@${PINS['dompurify']} --silent`, { cwd: tmp });
|
||||
execSync(`tar xzf dompurify-${PINS['dompurify']}.tgz`, { cwd: tmp });
|
||||
copyFileSync(
|
||||
join(tmp, 'package', 'dist', 'purify.min.js'),
|
||||
join(OUT, 'dompurify.min.js'),
|
||||
);
|
||||
log(`wrote ${join(OUT, 'dompurify.min.js')}`);
|
||||
|
||||
// 5. MANIFEST — record the vendored versions for audit
|
||||
const manifest = {
|
||||
generated_at: new Date().toISOString(),
|
||||
pins: PINS,
|
||||
|
|
@ -86,6 +103,7 @@ function vendor() {
|
|||
'markdown-it.min.js',
|
||||
'markdown-it-front-matter.min.js',
|
||||
'highlight.min.js',
|
||||
'dompurify.min.js',
|
||||
],
|
||||
};
|
||||
writeFileSync(
|
||||
|
|
|
|||
|
|
@ -413,3 +413,50 @@ test('docs/screenshots/README.md documents mappestruktur + hooks (v4.3 Step 23)'
|
|||
assert.match(text, /window\.__voyage\.scheduleRender/, 'scheduleRender hook documented');
|
||||
assert.match(text, /window\.__voyage\.getProjectArtifacts/, 'getProjectArtifacts hook documented');
|
||||
});
|
||||
|
||||
// v4.3 Step 24 — vendor DOMPurify + sanitize annotation-content
|
||||
test('playground/lib/dompurify.min.js is vendored (v4.3 Step 24)', () => {
|
||||
const path = join(PLAYGROUND, 'lib', 'dompurify.min.js');
|
||||
assert.equal(existsSync(path), true, 'playground/lib/dompurify.min.js must exist (run scripts/vendor-playground-libs.mjs)');
|
||||
const size = statSync(path).size;
|
||||
// Sanity floor — DOMPurify min bundle is ~22 KB; reject empty/0-byte
|
||||
assert.ok(size > 5000, 'dompurify.min.js too small (' + size + ' bytes) — vendor script may have failed');
|
||||
});
|
||||
|
||||
test('playground/lib/VENDOR-MANIFEST.json pins dompurify >= 3.1.1 (v4.3 Step 24)', () => {
|
||||
const path = join(PLAYGROUND, 'lib', 'VENDOR-MANIFEST.json');
|
||||
const manifest = JSON.parse(readFileSync(path, 'utf-8'));
|
||||
assert.ok(manifest.pins && manifest.pins.dompurify, 'manifest must pin dompurify');
|
||||
// semver compare on major.minor: must be >= 3.1.1
|
||||
const m = String(manifest.pins.dompurify).match(/^(\d+)\.(\d+)\.(\d+)/);
|
||||
assert.ok(m, 'invalid dompurify pin format: ' + manifest.pins.dompurify);
|
||||
const [, maj, min] = m;
|
||||
assert.ok(Number(maj) > 3 || (Number(maj) === 3 && Number(min) >= 1), 'dompurify pin must be >= 3.1.1, got ' + manifest.pins.dompurify);
|
||||
assert.ok(manifest.output_files.includes('dompurify.min.js'), 'manifest output_files must list dompurify.min.js');
|
||||
});
|
||||
|
||||
test('voyage-playground.html loads dompurify.min.js (v4.3 Step 24)', () => {
|
||||
const text = readFileSync(HTML, 'utf-8');
|
||||
assert.match(text, /<script src="lib\/dompurify\.min\.js">/, 'lib/dompurify.min.js script tag required');
|
||||
});
|
||||
|
||||
test('voyage-playground.html declares sanitizeAnnotation function with allowlist (v4.3 Step 24)', () => {
|
||||
const text = readFileSync(HTML, 'utf-8');
|
||||
assert.match(text, /function\s+sanitizeAnnotation\s*\(/, 'sanitizeAnnotation() function required');
|
||||
// Must call DOMPurify.sanitize with an ALLOWED_TAGS allowlist
|
||||
assert.match(text, /DOMPurify\.sanitize/, 'DOMPurify.sanitize call required');
|
||||
assert.match(text, /ALLOWED_TAGS:\s*\[/, 'ALLOWED_TAGS allowlist required');
|
||||
});
|
||||
|
||||
test('voyage-playground.html bundle stays under 460 KB HALT-gate (v4.3 Step 24)', () => {
|
||||
// Sums voyage-playground.html + every playground/lib/*.js file. Per plan
|
||||
// critic finding 18 — must be < 460000 bytes (40 KB margin under the
|
||||
// 500 KB NFR).
|
||||
const htmlSize = statSync(HTML).size;
|
||||
const libDir = join(PLAYGROUND, 'lib');
|
||||
const libFiles = readdirSync(libDir).filter((f) => f.endsWith('.js') || f.endsWith('.mjs'));
|
||||
let libTotal = 0;
|
||||
for (const f of libFiles) libTotal += statSync(join(libDir, f)).size;
|
||||
const total = htmlSize + libTotal;
|
||||
assert.ok(total < 460000, 'bundle size ' + total + ' bytes exceeds 460 KB HALT-gate (' + libFiles.length + ' lib files)');
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue