fix: serve MindSpace public assets as same-origin relative URLs

Rewrite absolute tkmind asset download links to /api/... at delivery time
and strip base tags that violate public page CSP, so images load on any host.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-03 23:02:47 +08:00
parent f543a91cc4
commit c3c487a8fb
3 changed files with 65 additions and 13 deletions
+27 -11
View File
@@ -26,15 +26,31 @@ export function verifyPublicAssetToken(assetId, token, secret) {
);
}
export function appendPublicAssetTokens(html, secret) {
const source = String(html ?? '');
if (!source || !source.includes('/api/mindspace/v1/assets/')) return source;
return source.replace(
/((?:https?:\/\/[^"'<>\\\s]+)?\/api\/mindspace\/v1\/assets\/([a-z0-9-]+)\/download)(\?[^"'<>\\\s)]*)?/gi,
(value, base, assetId, query = '') => {
if (/(?:\?|&|&amp;)public_token=/.test(query)) return value;
const separator = query ? (query.includes('&amp;') ? '&amp;' : '&') : '?';
return `${base}${query}${separator}public_token=${publicAssetToken(assetId, secret)}`;
},
);
const PUBLIC_ASSET_DOWNLOAD_PATTERN =
/((?:https?:\/\/[^"'<>\\\s]+)?\/api\/mindspace\/v1\/assets\/([a-z0-9-]+)\/download)(\?[^"'<>\\\s)]*)?/gi;
export function stripPublicHtmlBaseTag(html) {
return String(html ?? '').replace(/<base\b[^>]*>/gi, '');
}
export function normalizePublicAssetReferences(html) {
const source = String(html ?? '');
if (!source.includes('/api/mindspace/v1/assets/')) return source;
return source.replace(PUBLIC_ASSET_DOWNLOAD_PATTERN, (_value, _base, assetId, query = '') => {
return `/api/mindspace/v1/assets/${assetId}/download${query ?? ''}`;
});
}
export function appendPublicAssetTokens(html, secret) {
const source = normalizePublicAssetReferences(html);
if (!source || !source.includes('/api/mindspace/v1/assets/')) return source;
return source.replace(PUBLIC_ASSET_DOWNLOAD_PATTERN, (value, _base, assetId, query = '') => {
if (/(?:\?|&|&amp;)public_token=/.test(query)) return value;
const separator = query ? (query.includes('&amp;') ? '&amp;' : '&') : '?';
return `/api/mindspace/v1/assets/${assetId}/download${query}${separator}public_token=${publicAssetToken(assetId, secret)}`;
});
}
export function preparePublicHtmlAssetDelivery(html, secret) {
return appendPublicAssetTokens(stripPublicHtmlBaseTag(html), secret);
}
+36
View File
@@ -2,7 +2,10 @@ import assert from 'node:assert/strict';
import test from 'node:test';
import {
appendPublicAssetTokens,
normalizePublicAssetReferences,
preparePublicHtmlAssetDelivery,
publicAssetToken,
stripPublicHtmlBaseTag,
verifyPublicAssetToken,
} from './mindspace-public-asset-token.mjs';
@@ -36,3 +39,36 @@ test('appendPublicAssetTokens preserves existing public tokens', () => {
const html = '<img src="/api/mindspace/v1/assets/asset-1/download?inline=1&public_token=keep-me">';
assert.equal(appendPublicAssetTokens(html, SECRET), html);
});
test('normalizePublicAssetReferences rewrites absolute asset urls to same-origin paths', () => {
const html = [
'<img src="https://m.tkmind.cn/api/mindspace/v1/assets/asset-1/download?inline=1">',
'<img src="http://127.0.0.1:5173/api/mindspace/v1/assets/asset-2/download?inline=1&amp;v=3">',
'<meta property="og:image" content="https://mm.tkmind.cn/api/mindspace/v1/assets/asset-3/download?inline=1">',
].join('');
const normalized = normalizePublicAssetReferences(html);
assert.match(normalized, /src="\/api\/mindspace\/v1\/assets\/asset-1\/download\?inline=1"/);
assert.match(normalized, /src="\/api\/mindspace\/v1\/assets\/asset-2\/download\?inline=1&amp;v=3"/);
assert.match(
normalized,
/content="\/api\/mindspace\/v1\/assets\/asset-3\/download\?inline=1"/,
);
});
test('stripPublicHtmlBaseTag removes base tags blocked by public page CSP', () => {
const html = '<head><base href="/"><title>demo</title></head>';
assert.equal(stripPublicHtmlBaseTag(html), '<head><title>demo</title></head>');
});
test('preparePublicHtmlAssetDelivery normalizes asset urls before signing', () => {
const html = '<head><base href="/"></head><img src="https://m.tkmind.cn/api/mindspace/v1/assets/asset-1/download?inline=1">';
const prepared = preparePublicHtmlAssetDelivery(html, SECRET);
assert.doesNotMatch(prepared, /<base\b/i);
assert.match(
prepared,
/src="\/api\/mindspace\/v1\/assets\/asset-1\/download\?inline=1&public_token=[A-Za-z0-9_-]+"/,
);
});
+2 -2
View File
@@ -59,7 +59,7 @@ import {
resolvePublicRequestOrigin,
} from './mindspace-public-page-context.mjs';
import {
appendPublicAssetTokens,
preparePublicHtmlAssetDelivery,
verifyPublicAssetToken,
} from './mindspace-public-asset-token.mjs';
import {
@@ -5427,7 +5427,7 @@ async function sendPublishFile(req, res, filePath) {
res.status(404).json({ message: '文件不存在' });
return;
}
html = appendPublicAssetTokens(html, INTERNAL_AGENT_SECRET);
html = preparePublicHtmlAssetDelivery(html, INTERNAL_AGENT_SECRET);
const embed = isPlazaEmbedRequest(req.query);
if (embed) {
html = preparePublicationHtmlForEmbed(html);