diff --git a/mindspace-public-asset-token.mjs b/mindspace-public-asset-token.mjs
index 1d0b2a5..0a00bd6 100644
--- a/mindspace-public-asset-token.mjs
+++ b/mindspace-public-asset-token.mjs
@@ -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 (/(?:\?|&|&)public_token=/.test(query)) return value;
- const separator = query ? (query.includes('&') ? '&' : '&') : '?';
- 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(/]*>/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 (/(?:\?|&|&)public_token=/.test(query)) return value;
+ const separator = query ? (query.includes('&') ? '&' : '&') : '?';
+ return `/api/mindspace/v1/assets/${assetId}/download${query}${separator}public_token=${publicAssetToken(assetId, secret)}`;
+ });
+}
+
+export function preparePublicHtmlAssetDelivery(html, secret) {
+ return appendPublicAssetTokens(stripPublicHtmlBaseTag(html), secret);
}
diff --git a/mindspace-public-asset-token.test.mjs b/mindspace-public-asset-token.test.mjs
index 49cec9f..578bfe8 100644
--- a/mindspace-public-asset-token.test.mjs
+++ b/mindspace-public-asset-token.test.mjs
@@ -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 = '
';
assert.equal(appendPublicAssetTokens(html, SECRET), html);
});
+
+test('normalizePublicAssetReferences rewrites absolute asset urls to same-origin paths', () => {
+ const html = [
+ '
',
+ '
',
+ '',
+ ].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&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 = '
demo';
+ assert.equal(stripPublicHtmlBaseTag(html), 'demo');
+});
+
+test('preparePublicHtmlAssetDelivery normalizes asset urls before signing', () => {
+ const html = '
';
+ const prepared = preparePublicHtmlAssetDelivery(html, SECRET);
+
+ assert.doesNotMatch(prepared, /