Add page data delivery and publication guards
This commit is contained in:
+70
-37
@@ -13,8 +13,9 @@ import {
|
||||
sessionCookie,
|
||||
} from './auth.mjs';
|
||||
import { createDbPool, initSchema, isDatabaseConfigured } from './db.mjs';
|
||||
import { createAgentRunGateway } from './agent-run-gateway.mjs';
|
||||
import { createWorkspacePageDeliverService } from './mindspace-workspace-page-deliver.mjs';
|
||||
import { createToolGateway } from './tool-gateway.mjs';
|
||||
import { createAgentRunGateway } from './agent-run-gateway.mjs';
|
||||
import {
|
||||
createAgentRunEventsHandler,
|
||||
createGetAgentRunHandler,
|
||||
@@ -58,6 +59,8 @@ import {
|
||||
} from './mindspace-public-route.mjs';
|
||||
import {
|
||||
buildPublishedHtmlViewContext,
|
||||
injectPublishedPageDataContext,
|
||||
parseMindSpacePublishFilePath,
|
||||
resolvePublicRequestOrigin,
|
||||
} from './mindspace-public-page-context.mjs';
|
||||
import {
|
||||
@@ -65,6 +68,7 @@ import {
|
||||
verifyPublicAssetToken,
|
||||
} from './mindspace-public-asset-token.mjs';
|
||||
import {
|
||||
collectInlineScriptHashes,
|
||||
decorateMindSpacePublishedHtml,
|
||||
handleMindSpaceLongImageDownload,
|
||||
} from './mindspace-public-delivery.mjs';
|
||||
@@ -102,9 +106,9 @@ import {
|
||||
allowPlazaEmbedFrame,
|
||||
preparePublicationHtmlForEmbed,
|
||||
isPlazaEmbedRequest,
|
||||
publishedPageCspForEmbed,
|
||||
stripPublicationHtmlCspMeta,
|
||||
} from './plaza-embed.mjs';
|
||||
import { publishedPageCsp } from './mindspace-published-page-csp.mjs';
|
||||
import { createMindSpaceAgentRunner } from './mindspace-agent-runner.mjs';
|
||||
import {
|
||||
analyzeChatMessageForSave,
|
||||
@@ -173,7 +177,7 @@ import { createFeedbackService } from './user-feedback.mjs';
|
||||
import { startScheduleReminderWorker } from './schedule-reminder-worker.mjs';
|
||||
import { createLlmProviderService, RELAY_BOOTSTRAP } from './llm-providers.mjs';
|
||||
import { createDirectChatService, isDirectChatSessionId, isPortalDirectChatSnapshot, sendDirectChatSessionEvents, shouldExpirePortalDirectChatSnapshot } from './direct-chat-service.mjs';
|
||||
import { repairSessionConversationFromDb } from './conversation-repair.mjs';
|
||||
import { filterUserVisibleConversation, repairSessionConversationFromDb } from './conversation-repair.mjs';
|
||||
import { filterNonemptyUserVisibleMessages } from './conversation-transcript-persist.mjs';
|
||||
import { createSessionStreamStore } from './session-stream-store.mjs';
|
||||
import { isSessionStreamReplayEnabled } from './session-stream.mjs';
|
||||
@@ -345,6 +349,7 @@ let mindSpacePageLiveEdit = null;
|
||||
let mindSpaceAssetAgent = null;
|
||||
let mindSpacePageEditSession = null;
|
||||
let mindSpacePublications = null;
|
||||
let workspacePageDeliver = null;
|
||||
let plazaPosts = null;
|
||||
let plazaEvents = null;
|
||||
let plazaRecommend = null;
|
||||
@@ -434,6 +439,13 @@ async function bootstrapUserAuth() {
|
||||
mindSpacePageLiveEdit = mindSpaceRuntimeAdapter.pageLiveEditService;
|
||||
mindSpaceAssetAgent = mindSpaceRuntimeAdapter.assetAgentService;
|
||||
mindSpacePublications = mindSpaceRuntimeAdapter.publicationService;
|
||||
workspacePageDeliver = createWorkspacePageDeliverService({
|
||||
pool,
|
||||
pageService: mindSpacePages,
|
||||
publicationService: mindSpacePublications,
|
||||
pageSyncService: mindSpacePageSync,
|
||||
logger: console,
|
||||
});
|
||||
const resolveUserIdByDirKey = async (dirKey) => {
|
||||
let userId = dirKey;
|
||||
if (!PUBLISH_KEY_UUID.test(dirKey)) {
|
||||
@@ -634,6 +646,7 @@ async function bootstrapUserAuth() {
|
||||
chatIntentRouter = createManagedChatIntentRouter({
|
||||
llmProviderService,
|
||||
memoryV2,
|
||||
conversationMemoryService,
|
||||
configService: memoryV2ConfigService,
|
||||
});
|
||||
// GOOSED PROXY BOUNDARY: H5 chat → goosed 唯一入口(Patch 5, goosed-proxy-boundary.mjs)
|
||||
@@ -668,6 +681,9 @@ async function bootstrapUserAuth() {
|
||||
chatIntentRouter,
|
||||
sessionSnapshotService,
|
||||
conversationMemoryService,
|
||||
syncUserPagesOnSuccess: async ({ userId }) => {
|
||||
await syncUserGeneratedPages(userId);
|
||||
},
|
||||
autoDispatch: ['1', 'true', 'yes', 'on'].includes(
|
||||
String(process.env.MEMIND_AGENT_RUN_AUTODISPATCH ?? '1').trim().toLowerCase(),
|
||||
),
|
||||
@@ -1997,7 +2013,7 @@ async function loadUserVisibleConversation(sessionId, userId) {
|
||||
if (authPool && userId) {
|
||||
session = await repairSessionConversationFromDb(authPool, session, sessionId, userId);
|
||||
}
|
||||
return (session?.conversation ?? []).filter((message) => message?.metadata?.userVisible);
|
||||
return filterUserVisibleConversation(session?.conversation ?? []);
|
||||
}
|
||||
|
||||
async function syncUserMemoriesIntoSession(userId, sessionId) {
|
||||
@@ -2006,6 +2022,19 @@ async function syncUserMemoriesIntoSession(userId, sessionId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function resolveUserMemoryItems(userId, { sessionId = null, limit = 200 } = {}) {
|
||||
const resolved = await memoryV2.resolve({
|
||||
userId,
|
||||
sessionId,
|
||||
limit,
|
||||
});
|
||||
const memories = Array.isArray(resolved?.memories) ? resolved.memories : [];
|
||||
if (memories.length || !conversationMemoryService?.listMemories) {
|
||||
return memories;
|
||||
}
|
||||
return conversationMemoryService.listMemories(userId, { limit }).catch(() => []);
|
||||
}
|
||||
|
||||
api.post('/user-memory/v1/remember-recent', async (req, res) => {
|
||||
const memoryStatus = await memoryV2?.getStatus?.().catch(() => null);
|
||||
if (!memoryStatus?.enabled) {
|
||||
@@ -2034,12 +2063,10 @@ api.post('/user-memory/v1/remember-recent', async (req, res) => {
|
||||
messages,
|
||||
});
|
||||
const syncedToSession = await syncUserMemoriesIntoSession(req.currentUser.id, sessionId);
|
||||
const resolved = await memoryV2.resolve({
|
||||
userId: req.currentUser.id,
|
||||
const memories = await resolveUserMemoryItems(req.currentUser.id, {
|
||||
sessionId,
|
||||
limit: 200,
|
||||
});
|
||||
const memories = Array.isArray(resolved?.memories) ? resolved.memories : [];
|
||||
return res.json({
|
||||
ok: true,
|
||||
analyzed: result.analyzed ?? 0,
|
||||
@@ -2075,12 +2102,10 @@ api.post('/user-memory/v1/sync', async (req, res) => {
|
||||
sessionId,
|
||||
});
|
||||
const syncedToSession = await syncUserMemoriesIntoSession(req.currentUser.id, sessionId);
|
||||
const resolved = await memoryV2.resolve({
|
||||
userId: req.currentUser.id,
|
||||
const memories = await resolveUserMemoryItems(req.currentUser.id, {
|
||||
sessionId,
|
||||
limit: 200,
|
||||
});
|
||||
const memories = Array.isArray(resolved?.memories) ? resolved.memories : [];
|
||||
return res.json({
|
||||
ok: true,
|
||||
analyzed: result.analyzed ?? 0,
|
||||
@@ -3099,7 +3124,12 @@ const SAVE_TARGET_CATEGORIES = new Set(['draft', 'oa', 'public']);
|
||||
|
||||
// REGRESSION GUARD: mindspace-page-sync-thumbnail — remote 也经 pageSyncService RPC 同步 public HTML
|
||||
async function syncUserGeneratedPages(userId) {
|
||||
if (!mindSpacePageSync || !userId) return;
|
||||
if (!userId) return;
|
||||
if (workspacePageDeliver?.syncAndDeliver) {
|
||||
await workspacePageDeliver.syncAndDeliver(userId);
|
||||
return;
|
||||
}
|
||||
if (!mindSpacePageSync) return;
|
||||
await mindSpacePageSync.syncUserGeneratedPages(userId);
|
||||
}
|
||||
|
||||
@@ -5005,31 +5035,6 @@ app.use(`/${PUBLISH_ROOT_DIR}`, async (req, res, next) => {
|
||||
});
|
||||
app.use('/mindspace', api);
|
||||
|
||||
function scriptSrcDirective({ inline = false, urls = [], hashes = [] } = {}) {
|
||||
const parts = [];
|
||||
if (inline) parts.push("'unsafe-inline'");
|
||||
for (const hash of hashes) parts.push(`'sha256-${hash}'`);
|
||||
for (const url of urls) parts.push(url);
|
||||
return parts.length ? `script-src ${parts.join(' ')}` : "script-src 'none'";
|
||||
}
|
||||
|
||||
function publishedPageCsp(html, { embed = false, raw = false, wechatShare = false, scriptHashes = [] } = {}) {
|
||||
const isFullHtml = /^\s*<!doctype html/i.test(html) || /^\s*<html[\s>]/i.test(html);
|
||||
if (embed && isFullHtml) {
|
||||
return publishedPageCspForEmbed(true);
|
||||
}
|
||||
if (wechatShare && isFullHtml) {
|
||||
return "default-src 'none'; style-src 'unsafe-inline' https:; img-src 'self' data: http: https:; font-src 'self' https: data:; connect-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'self'; script-src 'unsafe-inline' https://res.wx.qq.com";
|
||||
}
|
||||
if (raw && isFullHtml) {
|
||||
return "default-src 'none'; style-src 'unsafe-inline' https:; img-src 'self' data: http: https:; font-src 'self' https: data:; connect-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'self'; script-src 'unsafe-inline'";
|
||||
}
|
||||
if (isFullHtml) {
|
||||
return `default-src 'none'; style-src 'unsafe-inline' https:; img-src 'self' data: http: https:; font-src 'self' https: data:; connect-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'self'; ${scriptSrcDirective({ hashes: scriptHashes })}`;
|
||||
}
|
||||
return "default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; connect-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'self'";
|
||||
}
|
||||
|
||||
function extractOgImageUrl(html) {
|
||||
return (
|
||||
String(html ?? '').match(/<meta[^>]+property=["']og:image["'][^>]+content=["']([^"']+)["']/i)?.[1] ||
|
||||
@@ -5412,6 +5417,10 @@ async function sendPublishedPage(req, res, result, { embed = false, raw = false,
|
||||
} else if (raw) {
|
||||
html = stripPublicationHtmlCspMeta(html);
|
||||
}
|
||||
html = injectPublishedPageDataContext(html, {
|
||||
pageSource: result.pageSource,
|
||||
publication: result.publication,
|
||||
});
|
||||
if (!embed) {
|
||||
try {
|
||||
html = injectOgTags(html, { origin, pageUrl, pageDirUrl });
|
||||
@@ -5482,7 +5491,15 @@ async function sendPublishedPage(req, res, result, { embed = false, raw = false,
|
||||
return res.send(shellHtml);
|
||||
}
|
||||
res.set('Content-Type', 'text/html; charset=utf-8');
|
||||
res.set('Content-Security-Policy', publishedPageCsp(html, { embed, raw, wechatShare }));
|
||||
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',
|
||||
@@ -5829,10 +5846,26 @@ async function sendPublishFile(req, res, filePath, { isOwner = true } = {}) {
|
||||
filePath,
|
||||
thumbnailPngPathForSvg,
|
||||
});
|
||||
let pageDataContext = null;
|
||||
if (mindSpacePages) {
|
||||
const parsed = parseMindSpacePublishFilePath(filePath, __dirname);
|
||||
if (parsed?.userId && parsed.relativePath) {
|
||||
const page = await mindSpacePages
|
||||
.findPageByRelativePath(parsed.userId, parsed.relativePath)
|
||||
.catch(() => null);
|
||||
if (page?.id) {
|
||||
pageDataContext = {
|
||||
pageId: page.id,
|
||||
accessMode: page.publicationAccessMode ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
const decorated = decorateMindSpacePublishedHtml({
|
||||
html,
|
||||
embed,
|
||||
isOwner,
|
||||
pageDataContext,
|
||||
context,
|
||||
htmlFilePath: filePath,
|
||||
userAgent: req.get('user-agent') || '',
|
||||
|
||||
Reference in New Issue
Block a user