/** * Session Broker v0 — thin facade over userAuth session ownership + goosed target mapping. * * Does NOT own message history, tool state, memory, or SSE replay. * See docs/h5-session-architecture-20260706.md Patch 1. */ import { createSessionBrokerMetrics, isSessionBrokerMetricsEnabled } from './session-broker-metrics.mjs'; function envFlag(value, fallback = false) { const raw = String(value ?? '').trim().toLowerCase(); if (!raw) return fallback; return ['1', 'true', 'yes', 'on'].includes(raw); } export function isSessionBrokerEnabled(env = process.env) { return envFlag(env.MEMIND_SESSION_BROKER_ENABLED, false); } function requireUserAuthFn(userAuth, name) { if (typeof userAuth?.[name] !== 'function') { throw new Error(`userAuth.${name} is required for Session Broker`); } } export function createSessionBroker({ userAuth, logger = console, metricsEnabled = isSessionBrokerMetricsEnabled(), } = {}) { requireUserAuthFn(userAuth, 'registerAgentSession'); requireUserAuthFn(userAuth, 'getSessionTarget'); requireUserAuthFn(userAuth, 'ownsSession'); requireUserAuthFn(userAuth, 'unregisterAgentSession'); const metrics = createSessionBrokerMetrics({ logger, enabled: metricsEnabled }); async function validateOwnership(userId, sessionId) { if (!userId || !sessionId) return false; const owned = await userAuth.ownsSession(userId, sessionId); if (!owned) { metrics.ownershipDenied({ userId, sessionId }); } return owned; } async function registerSession({ userId, sessionId, target = 0, origin = 'h5' } = {}) { if (!userId || !sessionId) { throw new Error('registerSession requires userId and sessionId'); } if (await userAuth.ownsSession(userId, sessionId)) { metrics.registerDuplicate({ userId, sessionId, target, origin }); } await userAuth.registerAgentSession(userId, sessionId, target); if (typeof userAuth.setSessionOrigin === 'function' && (origin === 'h5' || origin === 'wechat')) { await userAuth.setSessionOrigin(sessionId, origin); } } async function unregisterSession({ userId, sessionId } = {}) { if (!userId || !sessionId) return; await userAuth.unregisterAgentSession(userId, sessionId); } async function resolveSessionTarget(sessionId) { if (!sessionId) return { target: null, node: 0 }; const resolved = await userAuth.getSessionTarget(sessionId); if (!resolved?.target && Number(resolved?.node ?? 0) > 0) { metrics.resolveTargetMiss({ sessionId, node: Number(resolved.node), reason: 'legacy_node_fallback', }); } return resolved; } async function resolveSession(sessionId) { if (!sessionId) return null; const { target, node } = await userAuth.getSessionTarget(sessionId); let origin = 'h5'; if (typeof userAuth.getSessionOrigins === 'function') { const origins = await userAuth.getSessionOrigins([sessionId]); origin = origins.get(sessionId) ?? 'h5'; } return { sessionId, target, node, origin }; } return { validateOwnership, registerSession, unregisterSession, resolveSession, resolveSessionTarget, }; } /** * Unified session ownership/target access with optional broker routing. * When disabled, delegates to userAuth with identical behavior (Patch 2 fallback). */ export function createSessionAccess({ userAuth, enabled = isSessionBrokerEnabled(), broker = null, logger = console, metricsEnabled = isSessionBrokerMetricsEnabled(), } = {}) { if (!userAuth) { throw new Error('createSessionAccess requires userAuth'); } const useBroker = Boolean(enabled); const sessionBroker = useBroker ? (broker ?? createSessionBroker({ userAuth, logger, metricsEnabled })) : null; async function validateOwnership(userId, sessionId) { if (useBroker) return sessionBroker.validateOwnership(userId, sessionId); if (!userId || !sessionId) return false; return userAuth.ownsSession(userId, sessionId); } async function ownsSession(userId, sessionId) { return validateOwnership(userId, sessionId); } async function registerSession({ userId, sessionId, target = 0, origin = 'h5' } = {}) { if (useBroker) return sessionBroker.registerSession({ userId, sessionId, target, origin }); await userAuth.registerAgentSession(userId, sessionId, target); if ( typeof userAuth.setSessionOrigin === 'function' && (origin === 'h5' || origin === 'wechat') ) { await userAuth.setSessionOrigin(sessionId, origin); } } async function registerAgentSession(userId, sessionId, target = 0) { return registerSession({ userId, sessionId, target, origin: 'h5' }); } async function unregisterSession({ userId, sessionId } = {}) { if (useBroker) return sessionBroker.unregisterSession({ userId, sessionId }); if (!userId || !sessionId) return; await userAuth.unregisterAgentSession(userId, sessionId); } async function resolveSessionTarget(sessionId) { if (useBroker) return sessionBroker.resolveSessionTarget(sessionId); if (!sessionId) return { target: null, node: 0 }; return userAuth.getSessionTarget(sessionId); } async function getSessionTarget(sessionId) { return resolveSessionTarget(sessionId); } async function resolveSession(sessionId) { if (useBroker) return sessionBroker.resolveSession(sessionId); if (!sessionId) return null; const { target, node } = await userAuth.getSessionTarget(sessionId); let origin = 'h5'; if (typeof userAuth.getSessionOrigins === 'function') { const origins = await userAuth.getSessionOrigins([sessionId]); origin = origins.get(sessionId) ?? 'h5'; } return { sessionId, target, node, origin }; } return { enabled: useBroker, broker: sessionBroker, validateOwnership, ownsSession, registerSession, registerAgentSession, unregisterSession, resolveSession, resolveSessionTarget, getSessionTarget, }; } const noopSessionAccess = { enabled: false, broker: null, async validateOwnership() { return true; }, async ownsSession() { return true; }, async registerSession() {}, async registerAgentSession() {}, async unregisterSession() {}, async resolveSession() { return null; }, async resolveSessionTarget() { return { target: null, node: 0 }; }, async getSessionTarget() { return { target: null, node: 0 }; }, }; /** Resolve shared session access for server wiring and tests. */ export function resolveSessionAccess({ userAuth, sessionAccess = null, enabled = isSessionBrokerEnabled() } = {}) { if (sessionAccess) return sessionAccess; if (userAuth) return createSessionAccess({ userAuth, enabled }); return noopSessionAccess; }