/** * Per-session activation gate. A plugin declares whether it is session-scoped (the default); the * operator then activates it for all sessions (`['*']`) or an explicit set. A global plugin * (`sessionScoped === false`, e.g. a metrics logger) ignores this and is always active. * * A non-session-attributed event (no `sessionId`) is never gated — the plugin chose to register that * hook, and there is no number to scope it to. */ export function isPluginActiveForSession( sessionScoped: boolean, activeSessions: string[], sessionId: string | undefined, ): boolean { if (!sessionScoped) return true; if (sessionId === undefined) return true; if (activeSessions.includes('*')) return true; return activeSessions.includes(sessionId); } const isPlainObject = (v: unknown): v is Record => typeof v === 'object' && v !== null && !Array.isArray(v); /** * Recursively merge `override` over `base`: nested plain objects merge key-by-key so an override that * sets only some keys inherits the rest from the base (incl. nested secrets); arrays and scalars are * replaced wholesale. Returns fresh objects; never mutates the inputs. */ function deepMerge(base: Record, override: Record): Record { const out: Record = { ...base }; for (const [key, value] of Object.entries(override)) { const baseValue = out[key]; out[key] = isPlainObject(baseValue) && isPlainObject(value) ? deepMerge(baseValue, value) : value; } return out; } /** * Resolve the config a plugin sees for a given session: the per-session override (if any) deep-merged * over the base ('*') config, so an override only changes the keys it specifies (at any depth) and * inherits the rest from the base — a sparse override can't drop a base key (e.g. a nested secret) it * didn't touch. A global plugin and a non-session-attributed event (no sessionId) get the base * unchanged. Returns a fresh object on merge; never mutates the inputs. */ export function resolvePluginConfig( base: Record, sessionConfig: Record> | undefined, sessionId: string | undefined, sessionScoped: boolean, ): Record { if (!sessionScoped || sessionId === undefined || !sessionConfig) return base; const override = sessionConfig[sessionId]; return override ? deepMerge(base, override) : base; }