3d4ad664b3
Batches 6-9: behaviour-preserving extractions of testable pure cores plus the tests they unblock, and a fix for the broken client test environment. Full suites green: server 113 suites / 1117 + 1 todo, client 30 files / 338. client (R0 infra): - vitest.setup.ts: in-memory localStorage/sessionStorage Storage stub wired via setupFiles. Unblocks menu-items.gating.test.ts (was 9 failing) -> client suite fully green. + menu-items.suggestions.test.ts (getSuggestionItems filter/sort). share: - extract buildShareMetaHtml (share-seo.util.ts) from the SEO controller; tests for reflected-XSS escaping in <title>/og/twitter meta, noindex, truncation; extractPageSlugId; updateAttachmentAttr; prepareContentForShare comment-strip (anonymous-viewer metadata-leak guard). ai-chat (security extractions): - selectAccessibleHits: CASL post-filter for semantic search (restricted page in an accessible space must NOT leak to the agent). - validateResolvedAddresses: SSRF connect-time guard (block if ANY resolved address is private). - resolveAudioFormat: mime whitelist (dead `?? 'webm'` fallback dropped, set unchanged). + mcp-servers toView header-leak guard, MCP tool namespacing. collaboration (data-loss area): - extract computeHistoryJob (pins the "agent delay MUST stay 0" invariant) and resolveSource. Integration: onAuthenticate read-only matrix (collab auth bypass), HistoryProcessor (contributor restore on save failure), onStoreDocument Approach-A boundary snapshot (human revision pinned before agent overwrite). Reviewed (APPROVE WITH SUGGESTIONS): extractions behaviour-preserving, security tests mutation-resistant. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
41 lines
1.6 KiB
TypeScript
41 lines
1.6 KiB
TypeScript
import { htmlEscape } from '../../common/helpers/html-escaper';
|
|
|
|
/**
|
|
* Build the SEO-enriched index HTML for a publicly shared page.
|
|
*
|
|
* This is the pure, side-effect-free core of ShareSeoController.getShare: given
|
|
* the raw index.html and the share's title + searchIndexing flag, it returns the
|
|
* transformed HTML with the <title> replaced and the og:/twitter:/robots meta
|
|
* tags injected at the <!--meta-tags--> marker.
|
|
*
|
|
* SECURITY: the title is attacker-influenceable (it is the shared page title),
|
|
* so it MUST be htmlEscape'd before being interpolated into both the <title>
|
|
* element and the content="..." attributes of the meta tags. Removing the
|
|
* escaping would allow a page title to break out of the attribute / element and
|
|
* inject markup into the share origin.
|
|
*/
|
|
export function buildShareMetaHtml(
|
|
indexHtml: string,
|
|
opts: { title: string | null; searchIndexing: boolean },
|
|
): string {
|
|
// Escape FIRST, then truncate, so the truncation acts on the safe string and
|
|
// can never split a multi-char HTML entity (matches the original controller).
|
|
const rawTitle = htmlEscape(opts.title ?? 'untitled');
|
|
const metaTitle =
|
|
rawTitle.length > 80 ? `${rawTitle.slice(0, 77)}…` : rawTitle;
|
|
|
|
const metaTagVar = '<!--meta-tags-->';
|
|
|
|
const metaTags = [
|
|
`<meta property="og:title" content="${metaTitle}" />`,
|
|
`<meta property="twitter:title" content="${metaTitle}" />`,
|
|
!opts.searchIndexing ? `<meta name="robots" content="noindex" />` : '',
|
|
]
|
|
.filter(Boolean)
|
|
.join('\n ');
|
|
|
|
return indexHtml
|
|
.replace(/<title>[\s\S]*?<\/title>/i, `<title>${metaTitle}</title>`)
|
|
.replace(metaTagVar, metaTags);
|
|
}
|