c7718c683b
Memind CI / Test, build, and release guards (push) Successful in 1m36s
Publication routes and share shells were missing the Rybbit tracker, and bootstrap now reuses the stored MindSpace analytics secret when Rybbit env secret is absent. Co-authored-by: Cursor <cursoragent@cursor.com>
397 lines
9.7 KiB
JavaScript
397 lines
9.7 KiB
JavaScript
import {
|
|
injectPublishedPageDataContext,
|
|
resolveMindSpacePageDataContext,
|
|
} from '../mindspace-public-page-context.mjs';
|
|
import {
|
|
collectInlineScriptHashes,
|
|
} from '../mindspace-public-delivery.mjs';
|
|
import {
|
|
injectMindSpaceAnalytics,
|
|
resolveAnalyticsOwnerLabel,
|
|
resolveAnalyticsOwnerSegment,
|
|
resolveAnalyticsPlan,
|
|
} from '../mindspace-analytics.mjs';
|
|
import {
|
|
injectMindSpaceRybbit,
|
|
} from '../mindspace-rybbit.mjs';
|
|
import {
|
|
rewriteBrokenMindSpacePublicImageUrls,
|
|
rewritePublicationCanonicalAssetUrls,
|
|
} from '../mindspace-publications.mjs';
|
|
import {
|
|
allowPlazaEmbedFrame,
|
|
preparePublicationHtmlForEmbed,
|
|
stripPublicationHtmlCspMeta,
|
|
} from '../plaza-embed.mjs';
|
|
import {
|
|
publishedPageCsp,
|
|
} from '../mindspace-published-page-csp.mjs';
|
|
import {
|
|
injectOgTags,
|
|
injectWechatShareBridge,
|
|
} from '../mindspace-og-tags.mjs';
|
|
import {
|
|
isLongImageDownloadRequest,
|
|
renderLongImageBuffer,
|
|
} from '../mindspace-long-image.mjs';
|
|
import { isWechatUserAgent } from '../wechat-oauth.mjs';
|
|
import {
|
|
appendQueryParam,
|
|
detectPublishedPageTitle,
|
|
extractOgImageUrl,
|
|
extractShareMetaFromPageHtml,
|
|
publishedPageShellHtml,
|
|
removeQueryParam,
|
|
resolveRequestOrigin,
|
|
} from './portal-publication-shell.mjs';
|
|
|
|
async function decoratePublicationHtmlAnalytics(
|
|
html,
|
|
{
|
|
result,
|
|
analyticsConfig,
|
|
rybbitConfig,
|
|
getAuthPool = () => null,
|
|
getUserAuth = () => null,
|
|
getMindSpacePages = () => null,
|
|
resolvePageDataContext =
|
|
resolveMindSpacePageDataContext,
|
|
logger = console,
|
|
} = {},
|
|
) {
|
|
const source = String(html ?? '');
|
|
const isFullHtml =
|
|
/^\s*<!doctype html/i.test(source) ||
|
|
/^\s*<html[\s>]/i.test(source);
|
|
if (!isFullHtml) return source;
|
|
|
|
const userAuth = getUserAuth?.();
|
|
const pageOwner =
|
|
result?.ownerId && userAuth
|
|
? await userAuth
|
|
.getUserById(result.ownerId)
|
|
.catch(() => null)
|
|
: null;
|
|
let pageDataContext = null;
|
|
const mindSpacePages = getMindSpacePages?.();
|
|
if (
|
|
result?.ownerId &&
|
|
result?.workspaceRelativePath &&
|
|
mindSpacePages
|
|
) {
|
|
pageDataContext = await resolvePageDataContext({
|
|
pool: getAuthPool?.(),
|
|
pageService: mindSpacePages,
|
|
userId: result.ownerId,
|
|
relativePath: result.workspaceRelativePath,
|
|
logger,
|
|
}).catch(() => null);
|
|
}
|
|
|
|
const ownerSegment = resolveAnalyticsOwnerSegment(
|
|
pageOwner ?? {},
|
|
);
|
|
const ownerLabel = resolveAnalyticsOwnerLabel(
|
|
pageOwner ?? {},
|
|
);
|
|
const pageId =
|
|
pageDataContext?.pageId ??
|
|
result?.pageSource?.pageId ??
|
|
'';
|
|
const publicationId =
|
|
pageDataContext?.publicationId ??
|
|
pageDataContext?.publication_id ??
|
|
result?.publication?.id ??
|
|
'';
|
|
|
|
let decorated = injectMindSpaceAnalytics(source, {
|
|
ownerId: result?.ownerId ?? '',
|
|
ownerSegment,
|
|
ownerLabel,
|
|
planType: resolveAnalyticsPlan(pageOwner ?? {}),
|
|
generatedAt: pageDataContext?.generatedAt ?? '',
|
|
pageId,
|
|
publicationId,
|
|
config: analyticsConfig,
|
|
});
|
|
decorated = injectMindSpaceRybbit(decorated, {
|
|
ownerId: result?.ownerId ?? '',
|
|
ownerSegment,
|
|
ownerLabel,
|
|
pageId,
|
|
publicationId,
|
|
channel: 'publication',
|
|
config: rybbitConfig,
|
|
});
|
|
return decorated;
|
|
}
|
|
|
|
export function createPortalPublishedPageDelivery({
|
|
registerLongImageArtifact,
|
|
renderLongImage =
|
|
renderLongImageBuffer,
|
|
analyticsConfig = { enabled: false },
|
|
rybbitConfig = { enabled: false },
|
|
getAuthPool = () => null,
|
|
getUserAuth = () => null,
|
|
getMindSpacePages = () => null,
|
|
logger = console,
|
|
} = {}) {
|
|
if (
|
|
typeof registerLongImageArtifact !==
|
|
'function'
|
|
) {
|
|
throw new Error(
|
|
'createPortalPublishedPageDelivery requires artifact registration',
|
|
);
|
|
}
|
|
|
|
return async function sendPublishedPage(
|
|
req,
|
|
res,
|
|
result,
|
|
{
|
|
embed = false,
|
|
raw = false,
|
|
ownerSlug = null,
|
|
} = {},
|
|
) {
|
|
let html =
|
|
rewriteBrokenMindSpacePublicImageUrls(
|
|
result.html,
|
|
);
|
|
if (ownerSlug) {
|
|
html =
|
|
rewritePublicationCanonicalAssetUrls(
|
|
html,
|
|
ownerSlug,
|
|
);
|
|
}
|
|
const origin = resolveRequestOrigin(req);
|
|
const originalPath =
|
|
req.originalUrl || req.url || '';
|
|
const sharePath = removeQueryParam(
|
|
removeQueryParam(
|
|
originalPath,
|
|
'download',
|
|
),
|
|
'export',
|
|
);
|
|
const pageUrl = originalPath
|
|
? new URL(
|
|
sharePath,
|
|
origin || 'http://localhost',
|
|
)
|
|
.toString()
|
|
.split('#')[0]
|
|
: '';
|
|
const pageDirUrl = pageUrl
|
|
? pageUrl.slice(
|
|
0,
|
|
pageUrl.lastIndexOf('/') + 1,
|
|
)
|
|
: '';
|
|
const wechatShare =
|
|
!embed &&
|
|
isWechatUserAgent(
|
|
req.get('user-agent') || '',
|
|
);
|
|
if (embed) {
|
|
html =
|
|
preparePublicationHtmlForEmbed(html);
|
|
allowPlazaEmbedFrame(res);
|
|
} else if (raw) {
|
|
html =
|
|
stripPublicationHtmlCspMeta(html);
|
|
}
|
|
html = injectPublishedPageDataContext(
|
|
html,
|
|
{
|
|
pageSource: result.pageSource,
|
|
publication: result.publication,
|
|
},
|
|
);
|
|
html = await decoratePublicationHtmlAnalytics(html, {
|
|
result,
|
|
analyticsConfig,
|
|
rybbitConfig,
|
|
getAuthPool,
|
|
getUserAuth,
|
|
getMindSpacePages,
|
|
logger,
|
|
});
|
|
if (!embed) {
|
|
try {
|
|
html = injectOgTags(html, {
|
|
origin,
|
|
pageUrl,
|
|
pageDirUrl,
|
|
});
|
|
if (wechatShare) {
|
|
html = injectWechatShareBridge(
|
|
html,
|
|
{ pageUrl },
|
|
);
|
|
}
|
|
} catch {
|
|
// Never block delivery on share metadata.
|
|
}
|
|
}
|
|
const isFullHtml =
|
|
/^\s*<!doctype html/i.test(html) ||
|
|
/^\s*<html[\s>]/i.test(html);
|
|
if (
|
|
!embed &&
|
|
!raw &&
|
|
isFullHtml &&
|
|
isLongImageDownloadRequest(req.query)
|
|
) {
|
|
try {
|
|
const rawUrl = new URL(
|
|
appendQueryParam(
|
|
sharePath || originalPath,
|
|
'view',
|
|
'raw',
|
|
),
|
|
origin || 'http://localhost',
|
|
);
|
|
const image =
|
|
await renderLongImage({
|
|
url: rawUrl.toString(),
|
|
});
|
|
const longImageUrl = new URL(
|
|
appendQueryParam(
|
|
sharePath || originalPath,
|
|
'download',
|
|
'long-image',
|
|
),
|
|
origin || 'http://localhost',
|
|
).toString();
|
|
await registerLongImageArtifact({
|
|
result,
|
|
image,
|
|
canonicalUrl: longImageUrl,
|
|
});
|
|
res.set('Content-Type', 'image/png');
|
|
res.set(
|
|
'Content-Disposition',
|
|
'attachment; filename="mindspace-public-page.long.png"',
|
|
);
|
|
res.set(
|
|
'Cache-Control',
|
|
'no-store',
|
|
);
|
|
return res.send(image);
|
|
} catch (error) {
|
|
return res
|
|
.status(500)
|
|
.type(
|
|
'text/plain; charset=utf-8',
|
|
)
|
|
.send(
|
|
`长图生成失败:${
|
|
error?.message || '未知错误'
|
|
}`,
|
|
);
|
|
}
|
|
}
|
|
const canWrapWithShell =
|
|
!embed &&
|
|
!raw &&
|
|
isFullHtml &&
|
|
result.publication?.accessMode !==
|
|
'password';
|
|
if (canWrapWithShell) {
|
|
const title =
|
|
detectPublishedPageTitle(html);
|
|
const rawUrl = appendQueryParam(
|
|
sharePath || originalPath,
|
|
'view',
|
|
'raw',
|
|
);
|
|
const longImageUrl = appendQueryParam(
|
|
sharePath || originalPath,
|
|
'download',
|
|
'long-image',
|
|
);
|
|
let shellHtml = publishedPageShellHtml({
|
|
iframeUrl: rawUrl,
|
|
shareUrl: pageUrl,
|
|
title,
|
|
longImageUrl,
|
|
});
|
|
try {
|
|
shellHtml = injectOgTags(shellHtml, {
|
|
origin,
|
|
pageUrl,
|
|
pageDirUrl,
|
|
fallbackImageUrl:
|
|
extractOgImageUrl(html),
|
|
meta:
|
|
extractShareMetaFromPageHtml(
|
|
html,
|
|
),
|
|
});
|
|
if (wechatShare) {
|
|
shellHtml =
|
|
injectWechatShareBridge(
|
|
shellHtml,
|
|
{ pageUrl },
|
|
);
|
|
}
|
|
} catch {
|
|
// Keep the share shell usable.
|
|
}
|
|
shellHtml = await decoratePublicationHtmlAnalytics(shellHtml, {
|
|
result,
|
|
analyticsConfig,
|
|
rybbitConfig,
|
|
getAuthPool,
|
|
getUserAuth,
|
|
getMindSpacePages,
|
|
logger,
|
|
});
|
|
res.set(
|
|
'Content-Type',
|
|
'text/html; charset=utf-8',
|
|
);
|
|
res.set(
|
|
'Content-Security-Policy',
|
|
wechatShare
|
|
? "default-src 'none'; style-src 'unsafe-inline'; img-src data: https:; font-src 'none'; connect-src 'self' https://cdn.jsdelivr.net; script-src 'unsafe-inline' 'self' https://cdn.jsdelivr.net https://res.wx.qq.com; frame-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'"
|
|
: "default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; connect-src 'self' https://cdn.jsdelivr.net; script-src 'unsafe-inline' 'self' https://cdn.jsdelivr.net; frame-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'",
|
|
);
|
|
res.set(
|
|
'Cache-Control',
|
|
result.publication.accessMode ===
|
|
'public'
|
|
? 'public, max-age=60'
|
|
: 'private, no-store',
|
|
);
|
|
return res.send(shellHtml);
|
|
}
|
|
res.set(
|
|
'Content-Type',
|
|
'text/html; charset=utf-8',
|
|
);
|
|
res.set(
|
|
'Content-Security-Policy',
|
|
publishedPageCsp(html, {
|
|
embed,
|
|
raw,
|
|
wechatShare,
|
|
scriptHashes:
|
|
collectInlineScriptHashes(html),
|
|
}),
|
|
);
|
|
res.set(
|
|
'Cache-Control',
|
|
result.publication.accessMode ===
|
|
'public'
|
|
? 'public, max-age=60'
|
|
: 'private, no-store',
|
|
);
|
|
return res.send(html);
|
|
};
|
|
}
|