Files
memind/server/portal-access-policy.mjs
john fde6503bdf
Memind CI / Test, build, and release guards (push) Has been cancelled
feat(mindspace): add SEO/GEO delivery, page template catalog, and admin hooks
Enable optional SEO/GEO injection and discovery routes for confirmed public pages while keeping private pages noindex. Add premium page template skills, portal catalog API, template shop UI, and Baidu push gated by memind_adm config.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 08:06:12 +08:00

427 lines
12 KiB
JavaScript

export const PORTAL_ACCESS_CLASS = Object.freeze({
PUBLIC: 'public',
OPTIONAL_USER: 'optional-user',
AUTHENTICATED: 'authenticated',
INTERNAL: 'internal',
ROUTE_AUTHORIZED: 'route-authorized',
});
export const PORTAL_ACCESS_POLICY_MODE = Object.freeze({
OFF: 'off',
SHADOW: 'shadow',
});
export const PORTAL_ACCESS_ENFORCEMENT_GROUPS = Object.freeze([
'legacy-page-data',
'page-data-public',
'plaza-optional-user',
]);
const DEFAULT_SHADOW_LOG_INTERVAL_MS = 60_000;
const MIN_SHADOW_LOG_INTERVAL_MS = 1_000;
const MAX_SHADOW_LOG_INTERVAL_MS = 60 * 60_000;
function freezeRule(rule) {
return Object.freeze({
...rule,
paths: rule.paths ? Object.freeze([...rule.paths]) : undefined,
methods: rule.methods ? Object.freeze([...rule.methods]) : undefined,
});
}
export const PORTAL_STATIC_ACCESS_RULES = Object.freeze([
freezeRule({
id: 'runtime-status',
accessClass: PORTAL_ACCESS_CLASS.PUBLIC,
legacyDirectBypass: true,
paths: ['/status', '/runtime/status'],
}),
freezeRule({
id: 'blocked-words',
accessClass: PORTAL_ACCESS_CLASS.PUBLIC,
legacyDirectBypass: true,
paths: ['/config/blocked-words'],
}),
freezeRule({
id: 'seo-discovery',
accessClass: PORTAL_ACCESS_CLASS.PUBLIC,
legacyDirectBypass: true,
methods: ['GET'],
paths: ['/robots.txt', '/sitemap.xml', '/llms.txt'],
}),
freezeRule({
id: 'internal-agent',
accessClass: PORTAL_ACCESS_CLASS.INTERNAL,
legacyDirectBypass: true,
pathPrefix: '/internal/agent/',
}),
freezeRule({
id: 'internal-deep-search-llm',
accessClass: PORTAL_ACCESS_CLASS.INTERNAL,
legacyDirectBypass: true,
paths: ['/internal/deep-search/llm'],
}),
freezeRule({
id: 'mindspace-agent-callback',
accessClass: PORTAL_ACCESS_CLASS.INTERNAL,
legacyDirectBypass: true,
paths: [
'/agent/mindspace_page_patch',
'/agent/mindspace_asset_delete',
'/agent/mindspace_asset_download',
'/agent/mindspace_image_generate',
],
}),
freezeRule({
id: 'image-make-runtime-config',
accessClass: PORTAL_ACCESS_CLASS.INTERNAL,
legacyDirectBypass: true,
paths: ['/internal/image-make/runtime-config'],
}),
freezeRule({
id: 'mindspace-asset-download',
accessClass: PORTAL_ACCESS_CLASS.ROUTE_AUTHORIZED,
legacyDirectBypass: true,
methods: ['GET'],
pathPattern: /^\/mindspace\/v1\/assets\/[^/]+\/download$/,
}),
freezeRule({
id: 'plaza-events',
policyGroup: 'plaza-optional-user',
accessClass: PORTAL_ACCESS_CLASS.OPTIONAL_USER,
methods: ['POST'],
paths: ['/plaza/v1/events'],
}),
freezeRule({
id: 'plaza-discovery',
policyGroup: 'plaza-optional-user',
accessClass: PORTAL_ACCESS_CLASS.OPTIONAL_USER,
methods: ['GET'],
paths: ['/plaza/v1/feed', '/plaza/v1/categories', '/plaza/v1/seo/sitemap'],
}),
freezeRule({
id: 'plaza-post-detail',
policyGroup: 'plaza-optional-user',
accessClass: PORTAL_ACCESS_CLASS.OPTIONAL_USER,
methods: ['GET'],
pathPattern: /^\/plaza\/v1\/posts\/[^/]+$/,
}),
freezeRule({
id: 'plaza-post-comments',
policyGroup: 'plaza-optional-user',
accessClass: PORTAL_ACCESS_CLASS.OPTIONAL_USER,
methods: ['GET'],
pathPattern: /^\/plaza\/v1\/posts\/[^/]+\/comments$/,
}),
freezeRule({
id: 'plaza-user-profile',
policyGroup: 'plaza-optional-user',
accessClass: PORTAL_ACCESS_CLASS.OPTIONAL_USER,
methods: ['GET'],
pathPattern: /^\/plaza\/v1\/users\/[^/]+$/,
}),
freezeRule({
id: 'plaza-user-posts',
policyGroup: 'plaza-optional-user',
accessClass: PORTAL_ACCESS_CLASS.OPTIONAL_USER,
methods: ['GET'],
pathPattern: /^\/plaza\/v1\/users\/[^/]+\/posts$/,
}),
]);
function normalizeRequest({ path, method } = {}) {
return {
path: String(path ?? ''),
method: String(method ?? 'GET').trim().toUpperCase() || 'GET',
};
}
function ruleMatches(rule, request) {
if (rule.methods && !rule.methods.includes(request.method)) return false;
if (rule.paths?.includes(request.path)) return true;
if (rule.pathPrefix && request.path.startsWith(rule.pathPrefix)) return true;
return Boolean(rule.pathPattern?.test(request.path));
}
export function findPortalStaticAccessRule(requestInput) {
const request = normalizeRequest(requestInput);
return PORTAL_STATIC_ACCESS_RULES.find((rule) => ruleMatches(rule, request)) ?? null;
}
export function resolvePortalAccessPolicyMode(env = process.env) {
const raw = String(env.MEMIND_PORTAL_ACCESS_POLICY_MODE ?? '').trim().toLowerCase();
return raw === PORTAL_ACCESS_POLICY_MODE.SHADOW
? PORTAL_ACCESS_POLICY_MODE.SHADOW
: PORTAL_ACCESS_POLICY_MODE.OFF;
}
export function resolvePortalAccessShadowLogIntervalMs(env = process.env) {
const parsed = Number(env.MEMIND_PORTAL_ACCESS_SHADOW_LOG_INTERVAL_MS);
if (!Number.isFinite(parsed)) return DEFAULT_SHADOW_LOG_INTERVAL_MS;
return Math.min(
MAX_SHADOW_LOG_INTERVAL_MS,
Math.max(MIN_SHADOW_LOG_INTERVAL_MS, Math.trunc(parsed)),
);
}
function isExplicitlyEnabled(value) {
return String(value ?? '').trim() === '1';
}
export function resolvePortalAccessEnforcementConfig(env = process.env) {
const masterEnabled = isExplicitlyEnabled(
env.MEMIND_PORTAL_ACCESS_POLICY_ENFORCEMENT_ENABLED,
);
const killSwitch = isExplicitlyEnabled(
env.MEMIND_PORTAL_ACCESS_POLICY_KILL_SWITCH,
);
const requestedGroups = [
...new Set(
String(env.MEMIND_PORTAL_ACCESS_POLICY_ENFORCE_GROUPS ?? '')
.split(',')
.map((group) => group.trim())
.filter(Boolean),
),
];
const unknownGroups = requestedGroups.filter(
(group) => !PORTAL_ACCESS_ENFORCEMENT_GROUPS.includes(group),
);
if (masterEnabled && unknownGroups.length > 0) {
throw new Error(
`Unknown Portal access enforcement group(s): ${unknownGroups.join(', ')}`,
);
}
const activeGroups =
masterEnabled && !killSwitch
? requestedGroups.filter((group) =>
PORTAL_ACCESS_ENFORCEMENT_GROUPS.includes(group))
: [];
return Object.freeze({
masterEnabled,
killSwitch,
enabled: activeGroups.length > 0,
requestedGroups: Object.freeze(requestedGroups),
activeGroups: Object.freeze(activeGroups),
unknownGroups: Object.freeze(unknownGroups),
});
}
export function isPortalLegacyDirectGlobalAuthBypass(path, method) {
return Boolean(
findPortalStaticAccessRule({ path, method })?.legacyDirectBypass,
);
}
export function isPortalPlazaOptionalUserPath(path, method) {
return (
findPortalStaticAccessRule({ path, method })?.policyGroup ===
'plaza-optional-user'
);
}
export function resolvePortalApiAccess(
requestInput,
{
isPageDataPublicPath = () => false,
isLegacyPageDataApiPath = () => false,
} = {},
) {
const request = normalizeRequest(requestInput);
const staticRule = findPortalStaticAccessRule(request);
if (staticRule) {
return {
accessClass: staticRule.accessClass,
ruleId: staticRule.id,
policyGroup: staticRule.policyGroup ?? staticRule.id,
};
}
if (isPageDataPublicPath(request.path, request.method)) {
return {
accessClass: PORTAL_ACCESS_CLASS.ROUTE_AUTHORIZED,
ruleId: 'page-data-public',
policyGroup: 'page-data-public',
};
}
if (isLegacyPageDataApiPath(request.path)) {
return {
accessClass: PORTAL_ACCESS_CLASS.PUBLIC,
ruleId: 'legacy-page-data',
policyGroup: 'legacy-page-data',
};
}
return {
accessClass: PORTAL_ACCESS_CLASS.AUTHENTICATED,
ruleId: 'authenticated-default',
policyGroup: 'authenticated-default',
};
}
export function classifyPortalApiAccess(requestInput, predicates = {}) {
return resolvePortalApiAccess(requestInput, predicates).accessClass;
}
export function portalAccessBypassesGlobalAuth(accessClass) {
return accessClass !== PORTAL_ACCESS_CLASS.AUTHENTICATED;
}
export function resolvePortalAccessEnforcementDecision(
requestInput,
predicates,
config,
) {
const proposed = resolvePortalApiAccess(requestInput, predicates);
const activeGroups = config?.activeGroups ?? [];
const selected =
Boolean(config?.enabled) &&
activeGroups.includes(proposed.policyGroup);
let reason = 'group-disabled';
if (config?.killSwitch) reason = 'kill-switch';
else if (!config?.masterEnabled) reason = 'master-disabled';
else if (selected) reason = 'group-enabled';
return {
selected,
bypassGlobalAuth:
selected && portalAccessBypassesGlobalAuth(proposed.accessClass),
reason,
proposedAccessClass: proposed.accessClass,
proposedRuleId: proposed.ruleId,
policyGroup: proposed.policyGroup,
};
}
export function shouldOverridePortalLegacyGlobalAuth(
decision,
currentGlobalAuthBypass,
) {
return (
Boolean(decision?.selected) &&
Boolean(decision?.bypassGlobalAuth) &&
!Boolean(currentGlobalAuthBypass)
);
}
export function comparePortalAccessPolicyShadow(
requestInput,
{
multiUserEnabled = false,
isPageDataPublicPath = () => false,
isLegacyPageDataApiPath = () => false,
} = {},
) {
const request = normalizeRequest(requestInput);
const plazaOptionalUser = isPortalPlazaOptionalUserPath(request.path, request.method);
const pageDataRouteAuthorized = isPageDataPublicPath(request.path, request.method);
const legacyPageDataPublic = isLegacyPageDataApiPath(request.path);
const currentGlobalAuthBypass =
isPortalLegacyDirectGlobalAuthBypass(request.path, request.method) ||
(
Boolean(multiUserEnabled) &&
(plazaOptionalUser || pageDataRouteAuthorized || legacyPageDataPublic)
);
const proposed = resolvePortalApiAccess(request, {
isPageDataPublicPath,
isLegacyPageDataApiPath,
});
const proposedGlobalAuthBypass = portalAccessBypassesGlobalAuth(proposed.accessClass);
return {
path: request.path,
method: request.method,
proposedAccessClass: proposed.accessClass,
proposedRuleId: proposed.ruleId,
policyGroup: proposed.policyGroup,
currentGlobalAuthBypass,
proposedGlobalAuthBypass,
mismatch: currentGlobalAuthBypass !== proposedGlobalAuthBypass,
};
}
export function assessPortalAccessMigrationReadiness(
requests,
{
knownMismatchPolicyGroups = [],
...comparisonOptions
} = {},
) {
const knownGroups = new Set(knownMismatchPolicyGroups);
const comparisons = requests.map((request) =>
comparePortalAccessPolicyShadow(request, comparisonOptions));
const mismatches = comparisons.filter((comparison) => comparison.mismatch);
const knownMismatches = mismatches.filter((comparison) =>
knownGroups.has(comparison.policyGroup));
const unexpectedMismatches = mismatches.filter((comparison) =>
!knownGroups.has(comparison.policyGroup));
return {
contractReady: unexpectedMismatches.length === 0,
safeForBehaviorSwitch: mismatches.length === 0,
total: comparisons.length,
aligned: comparisons.length - mismatches.length,
knownMismatches,
unexpectedMismatches,
};
}
export function createPortalAccessShadowReporter({
intervalMs = DEFAULT_SHADOW_LOG_INTERVAL_MS,
now = Date.now,
emit = () => {},
} = {}) {
const effectiveIntervalMs = Math.min(
MAX_SHADOW_LOG_INTERVAL_MS,
Math.max(MIN_SHADOW_LOG_INTERVAL_MS, Math.trunc(Number(intervalMs) || 0)),
);
const counters = new Map();
function record(event) {
if (!event?.mismatch) return null;
const key = [
event.authMode ?? 'unknown',
event.method ?? 'GET',
event.policyGroup ?? event.proposedRuleId ?? 'unknown',
].join(':');
const timestamp = Number(now());
const counter = counters.get(key) ?? {
key,
totalOccurrences: 0,
suppressedSinceLast: 0,
lastEmittedAt: null,
};
counter.totalOccurrences += 1;
const shouldEmit =
counter.lastEmittedAt === null ||
timestamp - counter.lastEmittedAt >= effectiveIntervalMs;
if (!shouldEmit) {
counter.suppressedSinceLast += 1;
counters.set(key, counter);
return null;
}
const payload = {
...event,
occurrences: counter.totalOccurrences,
suppressedSinceLast: counter.suppressedSinceLast,
shadowLogIntervalMs: effectiveIntervalMs,
};
counter.lastEmittedAt = timestamp;
counter.suppressedSinceLast = 0;
counters.set(key, counter);
emit(payload);
return payload;
}
function snapshot() {
return [...counters.values()].map((counter) => ({ ...counter }));
}
return Object.freeze({ record, snapshot });
}