Files
memind/mindspace-public-asset-token.mjs
john c3c487a8fb 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>
2026-07-03 23:02:47 +08:00

57 lines
2.1 KiB
JavaScript

import crypto from 'node:crypto';
function normalizeSecret(secret) {
return String(secret ?? '');
}
export function publicAssetToken(assetId, secret) {
const normalizedAssetId = String(assetId ?? '').trim();
if (!normalizedAssetId) return '';
return crypto
.createHmac('sha256', normalizeSecret(secret))
.update(`mindspace-public-asset:v1:${normalizedAssetId}`)
.digest('base64url');
}
export function verifyPublicAssetToken(assetId, token, secret) {
const value = Array.isArray(token) ? token[0] : token;
const provided = String(value ?? '').trim();
if (!provided) return false;
const expected = publicAssetToken(assetId, secret);
const providedBuffer = Buffer.from(provided);
const expectedBuffer = Buffer.from(expected);
return (
providedBuffer.length === expectedBuffer.length &&
crypto.timingSafeEqual(providedBuffer, expectedBuffer)
);
}
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);
}