Address review: drop script/style entirely, guard IndexedDB persistence, fix indentation

Co-authored-by: mroxso <24775431+mroxso@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-09-07 10:20:16 +00:00
committed by GitHub
parent e76ef0b5df
commit 4498a70626
3 changed files with 39 additions and 9 deletions

View File

@@ -468,7 +468,7 @@ export function markdownToDoc(markdown: string): JSONContent {
// Table: header row, | --- | separator, then body rows.
if (/^\|.*\|\s*$/.test(line) && i + 1 < lines.length && /^\|[\s:|-]+\|\s*$/.test(lines[i + 1])) {
flushParagraph();
const parseRow = (row: string, header: boolean): JSONContent => ({
const parseRow = (row: string, header: boolean): JSONContent => ({
type: 'tableRow',
content: row
.replace(/^\|/, '')

View File

@@ -42,6 +42,13 @@ const ALLOWED_ELEMENTS = new Set([
/** class="task-list" / "task-item" and data-checked carry checklist state. */
const ALLOWED_CLASSES = new Set(['task-list', 'task-item']);
/**
* Elements whose entire subtree must be dropped rather than unwrapped: their
* text content is not safe to surface (e.g. `<script>alert(1)</script>` would
* otherwise become a literal "alert(1)" text node).
*/
const REMOVE_ENTIRELY = new Set(['script', 'style']);
const SAFE_PROTOCOLS = new Set(['https:', 'http:', 'mailto:', 'nostr:']);
function sanitizeHref(value: string | null): string | null {
@@ -94,12 +101,19 @@ function sanitizeAttributes(element: Element): void {
}
function sanitizeElement(element: Element): void {
const tag = element.tagName.toLowerCase();
if (REMOVE_ENTIRELY.has(tag)) {
// Drop the element and all of its content — unwrapping would surface
// script/style text as a literal text node.
element.remove();
return;
}
// Children first: a disallowed child is unwrapped before we look up again.
for (const child of [...element.children]) {
sanitizeElement(child);
}
const tag = element.tagName.toLowerCase();
if (!ALLOWED_ELEMENTS.has(tag)) {
// Unwrap: keep the (already sanitized) children, drop the wrapper. This
// keeps the text of a <div> or <span> instead of deleting it.

View File

@@ -28,6 +28,21 @@ function indexedDbAvailable(): boolean {
}
}
/**
* Constructing `IndexeddbPersistence` can throw even when IndexedDB exists
* (e.g. blocked/denied storage in private browsing or locked-down
* environments). Treat persistence as best-effort and degrade to `null`
* (in-memory) rather than crashing the caller.
*/
function createPersistence(documentId: string, doc: Y.Doc): IndexeddbPersistence | null {
if (!indexedDbAvailable()) return null;
try {
return new IndexeddbPersistence(`${DB_PREFIX}${documentId}`, doc);
} catch {
return null;
}
}
/**
* Opens the Yjs document for `documentId` with offline persistence behind it.
*
@@ -49,9 +64,7 @@ export function openDocumentSession(
events: DocumentSessionEvents,
): DocumentSession {
const doc = new Y.Doc();
const persistence = indexedDbAvailable()
? new IndexeddbPersistence(`${DB_PREFIX}${documentId}`, doc)
: null;
const persistence = createPersistence(documentId, doc);
const onUpdate = () => events.onUpdate();
doc.on('update', onUpdate);
@@ -91,13 +104,16 @@ export function openDocumentSession(
/** Removes the persisted body of a document that was deleted from the index. */
export async function deletePersistedDocument(documentId: string): Promise<void> {
if (!indexedDbAvailable()) return;
const doc = new Y.Doc();
const persistence = new IndexeddbPersistence(`${DB_PREFIX}${documentId}`, doc);
try {
await persistence.clearData();
const persistence = createPersistence(documentId, doc);
if (!persistence) return;
try {
await persistence.clearData();
} finally {
persistence.destroy();
}
} finally {
persistence.destroy();
doc.destroy();
}
}