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:
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user