fix(page-data): harden structural delivery and Portal base URL

Reject /api/page-data/ legacy passthrough, verify live API before send,
and resolve delivery links to Portal (8081) instead of Vite (5173).
Add local repair/verify scripts for daily-register Page Data flow.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-13 09:35:40 +08:00
parent 249b0697cb
commit 804f13bc04
11 changed files with 712 additions and 51 deletions
+42 -21
View File
@@ -1,6 +1,7 @@
import fs from 'node:fs/promises';
import {
detectPageDataDatasetUsageFromHtml,
htmlUsesForbiddenLegacyPageDataApi,
inferPageDataBindAccessMode,
} from './page-data-html-detect.mjs';
import { listPageAccessPolicies, readPageAccessPolicy } from './page-data-policy-store.mjs';
@@ -107,12 +108,23 @@ export async function assessPageDataHtmlBinding({
findPageByRelativePath,
}) {
const usage = detectPageDataDatasetUsageFromHtml(html);
const reasons = [];
if (htmlUsesForbiddenLegacyPageDataApi(html)) {
reasons.push('forbidden_legacy_page_data_api');
}
if (!usage.size) {
return { bound: true, reasons: [], pageId: null };
return {
bound: reasons.length === 0,
reasons: [...new Set(reasons)],
pageId: null,
policy: null,
};
}
const workspace = assessWorkspacePageDataReadiness({ publishDir, relativePath, html });
const reasons = [...workspace.reasons];
reasons.push(...workspace.reasons);
let pageId = null;
if (typeof findPageByRelativePath === 'function') {
@@ -147,6 +159,13 @@ export async function assessPageDataHtmlBinding({
}
}
if (pool && pageId) {
const publication = await queryOnlinePublication(pool, pageId);
const expectedAccessMode = inferPageDataBindAccessMode(relativePath, html);
if (publication && String(publication.access_mode ?? '').trim() !== expectedAccessMode) {
reasons.push('publication_access_mode_mismatch');
}
}
}
return {
@@ -219,12 +238,30 @@ export async function verifyPageDataDeliveryArtifacts({
}) {
const failures = [];
for (const artifact of artifacts) {
if (artifact.isAdmin) continue;
const relativePath = artifact.relativePath;
const html = await fs.readFile(artifact.localPath, 'utf8').catch(() => '');
const usage = detectPageDataDatasetUsageFromHtml(html);
const insertDataset = [...usage.entries()].find(([, perms]) => perms.insert)?.[0] ?? null;
if (!insertDataset) continue;
const assessment = await assessPageDataHtmlBinding({
pool,
userId,
publishDir,
relativePath,
html,
findPageByRelativePath,
});
if (!assessment.bound) {
failures.push({
relativePath,
stage: 'binding',
reason: assessment.reasons.join(',') || 'not_bound',
bindingReasons: assessment.reasons,
});
continue;
}
if (artifact.isAdmin || !insertDataset) continue;
const injection = await fetchWorkspaceHtmlInjection({ url: artifact.url, fetchImpl });
if (!injection.ok) {
@@ -234,15 +271,7 @@ export async function verifyPageDataDeliveryArtifacts({
const pageId = injection.pageId;
const policy =
readPageAccessPolicy(publishDir, pageId) ??
(await assessPageDataHtmlBinding({
pool,
userId,
publishDir,
relativePath,
html,
findPageByRelativePath,
})).policy;
readPageAccessPolicy(publishDir, pageId) ?? assessment.policy;
const smoke = await smokeTestPageDataInsert({
apiBase,
@@ -255,14 +284,6 @@ export async function verifyPageDataDeliveryArtifacts({
continue;
}
const assessment = await assessPageDataHtmlBinding({
pool,
userId,
publishDir,
relativePath,
html,
findPageByRelativePath,
});
failures.push({
relativePath,
stage: 'insert_smoke',