fix: allow inline page scripts in MindSpace public page CSP

Collect sha256 hashes from all inline scripts in published HTML so agent-generated
fade-in animations are not blocked while keeping the share-button CSP model.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-03 22:42:29 +08:00
parent cd2cbdbe78
commit f543a91cc4
2 changed files with 47 additions and 6 deletions
+17 -1
View File
@@ -1,5 +1,20 @@
import crypto from 'node:crypto';
import path from 'node:path'; import path from 'node:path';
const INLINE_SCRIPT_PATTERN = /<script\b(?![^>]*\bsrc\b)[^>]*>([\s\S]*?)<\/script>/gi;
export function collectInlineScriptHashes(html) {
const hashes = [];
const seen = new Set();
for (const match of String(html ?? '').matchAll(INLINE_SCRIPT_PATTERN)) {
const hash = crypto.createHash('sha256').update(match[1] ?? '').digest('base64');
if (seen.has(hash)) continue;
seen.add(hash);
hashes.push(hash);
}
return hashes;
}
export async function handleMindSpaceLongImageDownload({ export async function handleMindSpaceLongImageDownload({
query, query,
filePath, filePath,
@@ -63,12 +78,13 @@ export function decorateMindSpacePublishedHtml({
? injectPublicFileShareButton(nextHtml) ? injectPublicFileShareButton(nextHtml)
: { html: nextHtml, scriptHashes: [] }; : { html: nextHtml, scriptHashes: [] };
nextHtml = shareInjection.html; nextHtml = shareInjection.html;
const scriptHashes = collectInlineScriptHashes(nextHtml);
return { return {
html: nextHtml, html: nextHtml,
csp: publishedPageCsp(nextHtml, { csp: publishedPageCsp(nextHtml, {
embed, embed,
wechatShare, wechatShare,
scriptHashes: shareInjection.scriptHashes, scriptHashes,
}), }),
allowEmbedFrame, allowEmbedFrame,
}; };
+30 -5
View File
@@ -1,6 +1,22 @@
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { decorateMindSpacePublishedHtml, handleMindSpaceLongImageDownload } from './mindspace-public-delivery.mjs'; import {
collectInlineScriptHashes,
decorateMindSpacePublishedHtml,
handleMindSpaceLongImageDownload,
} from './mindspace-public-delivery.mjs';
test('collectInlineScriptHashes returns sha256 hashes for inline scripts only', () => {
const html = `<html><body>
<script>console.log('page')</script>
<script src="https://cdn.example/app.js"></script>
<script>console.log('share')</script>
</body></html>`;
const hashes = collectInlineScriptHashes(html);
assert.equal(hashes.length, 2);
assert.match(hashes[0], /^[A-Za-z0-9+/]+=*$/);
assert.notEqual(hashes[0], hashes[1]);
});
test('handleMindSpaceLongImageDownload renders and downloads when requested', async () => { test('handleMindSpaceLongImageDownload renders and downloads when requested', async () => {
const calls = []; const calls = [];
@@ -79,8 +95,10 @@ test('handleMindSpaceLongImageDownload falls back to local html path without pag
}); });
test('decorateMindSpacePublishedHtml returns decorated html and csp', () => { test('decorateMindSpacePublishedHtml returns decorated html and csp', () => {
const pageScript = "document.body.dataset.ready='1'";
const shareScript = "document.body.dataset.share='1'";
const result = decorateMindSpacePublishedHtml({ const result = decorateMindSpacePublishedHtml({
html: '<html><body>demo</body></html>', html: `<html><body><script>${pageScript}</script>demo</body></html>`,
embed: false, embed: false,
context: { context: {
origin: 'https://mm.tkmind.cn', origin: 'https://mm.tkmind.cn',
@@ -92,16 +110,23 @@ test('decorateMindSpacePublishedHtml returns decorated html and csp', () => {
preparePublicationHtmlForEmbed: (value) => value, preparePublicationHtmlForEmbed: (value) => value,
injectOgTags: (value, meta) => `${value}<!--og:${meta.pageUrl}-->`, injectOgTags: (value, meta) => `${value}<!--og:${meta.pageUrl}-->`,
injectWechatShareBridge: (value, meta) => `${value}<!--wechat:${meta.pageUrl}-->`, injectWechatShareBridge: (value, meta) => `${value}<!--wechat:${meta.pageUrl}-->`,
injectPublicFileShareButton: (value) => ({ html: `${value}<!--share-->`, scriptHashes: ['abc'] }), injectPublicFileShareButton: (value) => ({
html: value.replace('</body>', `<script>${shareScript}</script></body>`),
scriptHashes: ['ignored'],
}),
publishedPageCsp: (_value, options) => JSON.stringify(options), publishedPageCsp: (_value, options) => JSON.stringify(options),
isWechatUserAgent: () => true, isWechatUserAgent: () => true,
}); });
assert.match(result.html, /og:/); assert.match(result.html, /og:/);
assert.match(result.html, /wechat:/); assert.match(result.html, /wechat:/);
assert.match(result.html, /share/); assert.match(result.html, /dataset\.share/);
assert.equal(result.allowEmbedFrame, false); assert.equal(result.allowEmbedFrame, false);
assert.equal(result.csp, JSON.stringify({ embed: false, wechatShare: true, scriptHashes: ['abc'] })); const options = JSON.parse(result.csp);
assert.equal(options.embed, false);
assert.equal(options.wechatShare, true);
assert.deepEqual(options.scriptHashes, collectInlineScriptHashes(result.html));
assert.equal(options.scriptHashes.length, 2);
}); });
test('decorateMindSpacePublishedHtml supports embed mode without share injection', () => { test('decorateMindSpacePublishedHtml supports embed mode without share injection', () => {