user-scoped channel sessions, whatsapp disconnect cleanup, browser relay token fix
Channel session IDs now include userId (channel-{provider}-{userId}-{contextId})
to prevent cross-user contamination in multi-user setups. WhatsApp disconnect
properly logs out and clears cached auth. Browser relay uses server-derived token
directly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -38,8 +38,8 @@ export async function buildRelayWsUrl(host, port, gatewayToken) {
|
||||
"Missing relay token in extension settings (chrome.storage.local.relayToken)",
|
||||
);
|
||||
}
|
||||
const relayToken = await deriveRelayToken(token, port);
|
||||
return `ws://${host}:${port}/extension?token=${encodeURIComponent(relayToken)}`;
|
||||
// Token is already derived server-side — use it directly
|
||||
return `ws://${host}:${port}/extension?token=${encodeURIComponent(token)}`;
|
||||
}
|
||||
|
||||
export function isRetryableReconnectError(err) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { deriveRelayToken } from './background-utils.js'
|
||||
import { classifyRelayCheckException, classifyRelayCheckResponse } from './options-validation.js'
|
||||
|
||||
const DEFAULT_PORT = 18792
|
||||
@@ -32,11 +31,11 @@ async function checkRelayReachable(host, port, token) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const relayToken = await deriveRelayToken(trimmedToken, port)
|
||||
// Token is already derived server-side — use it directly
|
||||
const res = await chrome.runtime.sendMessage({
|
||||
type: 'relayCheck',
|
||||
url,
|
||||
token: relayToken,
|
||||
token: trimmedToken,
|
||||
})
|
||||
const result = classifyRelayCheckResponse(res, host, port)
|
||||
if (result.action === 'throw') throw new Error(result.error)
|
||||
|
||||
@@ -153,7 +153,7 @@ async function handleCommand(ctx: CommandContext): Promise<boolean> {
|
||||
await channel.send('No models available.');
|
||||
return true;
|
||||
}
|
||||
const current = getSessionModel('discord', discordId);
|
||||
const current = getSessionModel('discord', ctx.userId, discordId);
|
||||
const grouped = new Map<string, string[]>();
|
||||
for (const m of models) {
|
||||
const list = grouped.get(m.provider) ?? [];
|
||||
@@ -170,7 +170,7 @@ async function handleCommand(ctx: CommandContext): Promise<boolean> {
|
||||
}
|
||||
|
||||
if (lower === '!model') {
|
||||
const current = getSessionModel('discord', discordId);
|
||||
const current = getSessionModel('discord', ctx.userId, discordId);
|
||||
await channel.send(current ? `Current model: **${current}**` : 'No active session yet — the default model will be used on your next message.');
|
||||
return true;
|
||||
}
|
||||
@@ -178,7 +178,7 @@ async function handleCommand(ctx: CommandContext): Promise<boolean> {
|
||||
if (lower.startsWith('!model ')) {
|
||||
const requested = content.slice('!model '.length).trim();
|
||||
if (!requested) {
|
||||
const current = getSessionModel('discord', discordId);
|
||||
const current = getSessionModel('discord', ctx.userId, discordId);
|
||||
await channel.send(current ? `Current model: **${current}**` : 'No active session yet.');
|
||||
return true;
|
||||
}
|
||||
@@ -188,7 +188,7 @@ async function handleCommand(ctx: CommandContext): Promise<boolean> {
|
||||
await channel.send(`Model not found: \`${requested}\`\nUse \`!models\` to see available models.`);
|
||||
return true;
|
||||
}
|
||||
setSessionModel('discord', discordId, match.id);
|
||||
setSessionModel('discord', ctx.userId, discordId, match.id);
|
||||
await channel.send(`Model switched to **${match.id}**. The new model will be used on your next message.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,10 @@ const sessionCallbacks = new Map<string, EventCallback>();
|
||||
// Channel model overrides — survive session eviction/recreation
|
||||
const channelModelOverrides = new Map<string, string>();
|
||||
|
||||
function buildSessionId(context: string, userId: number, contextId: string): string {
|
||||
return `channel-${context}-${userId}-${contextId}`;
|
||||
}
|
||||
|
||||
async function getUserDefaultModel(userId: number): Promise<string | null> {
|
||||
try {
|
||||
const settings = await getUserSettings(userId);
|
||||
@@ -49,14 +53,14 @@ async function getUserDefaultModel(userId: number): Promise<string | null> {
|
||||
}
|
||||
}
|
||||
|
||||
export function getSessionModel(context: string, contextId: string): string | null {
|
||||
const sessionId = `channel-${context}-${contextId}`;
|
||||
export function getSessionModel(context: string, userId: number, contextId: string): string | null {
|
||||
const sessionId = buildSessionId(context, userId, contextId);
|
||||
const session = sessionManager.getSession(sessionId);
|
||||
return session?.model ?? channelModelOverrides.get(sessionId) ?? null;
|
||||
}
|
||||
|
||||
export function setSessionModel(context: string, contextId: string, model: string): void {
|
||||
const sessionId = `channel-${context}-${contextId}`;
|
||||
export function setSessionModel(context: string, userId: number, contextId: string, model: string): void {
|
||||
const sessionId = buildSessionId(context, userId, contextId);
|
||||
// Store override independently of session — survives idle eviction
|
||||
channelModelOverrides.set(sessionId, model);
|
||||
|
||||
@@ -76,8 +80,8 @@ export function setSessionModel(context: string, contextId: string, model: strin
|
||||
}
|
||||
|
||||
export async function sendAndAwait(params: SendAndAwaitParams): Promise<SendAndAwaitResult> {
|
||||
const { context, contextId } = params;
|
||||
const sessionId = `channel-${context}-${contextId}`;
|
||||
const { userId, context, contextId } = params;
|
||||
const sessionId = buildSessionId(context, userId, contextId);
|
||||
|
||||
// Serialize per session — if two messages arrive at once, second waits for first
|
||||
const existing = sessionLocks.get(sessionId) ?? Promise.resolve();
|
||||
@@ -112,19 +116,20 @@ async function doSend(sessionId: string, params: SendAndAwaitParams): Promise<Se
|
||||
const homeDir = getHomeDir(email);
|
||||
const cwd = homeDir;
|
||||
|
||||
// Resolve model: explicit param > channel override > existing session model > user default > system default
|
||||
// Resolve model: explicit param > !model override > user default > existing session > system default
|
||||
const existingSession = sessionManager.getSession(sessionId);
|
||||
let model = params.model ?? channelModelOverrides.get(sessionId) ?? existingSession?.model;
|
||||
const override = channelModelOverrides.get(sessionId);
|
||||
let model = params.model ?? override;
|
||||
if (!model) {
|
||||
const userDefault = await getUserDefaultModel(userId);
|
||||
model = userDefault ?? DEFAULT_MODEL;
|
||||
model = userDefault ?? existingSession?.model ?? DEFAULT_MODEL;
|
||||
}
|
||||
|
||||
const session = sessionManager.getOrCreate(sessionId, email, cwd, model, null, context, contextId);
|
||||
session.model = model;
|
||||
session.meta.model = model;
|
||||
session.userId = userId;
|
||||
logger.info('Channel doSend', { sessionId, model, hasProcess: !!session.piProcess });
|
||||
logger.info('Channel doSend', { sessionId, model, hasProcess: !!session.piProcess, userId, email });
|
||||
|
||||
return new Promise<SendAndAwaitResult>((resolve, reject) => {
|
||||
let resultText = '';
|
||||
@@ -296,7 +301,7 @@ async function doSend(sessionId: string, params: SendAndAwaitParams): Promise<Se
|
||||
}
|
||||
});
|
||||
|
||||
logger.info('Spawned Pi for channel session', { sessionId, model, context });
|
||||
logger.info('Spawned Pi for channel session', { sessionId, model, context, userId, email });
|
||||
}
|
||||
|
||||
// Add user message
|
||||
|
||||
@@ -153,7 +153,7 @@ async function handleCommand(ctx: CommandContext): Promise<boolean> {
|
||||
await send('No models available.');
|
||||
return true;
|
||||
}
|
||||
const current = getSessionModel('telegram', telegramId);
|
||||
const current = getSessionModel('telegram', ctx.userId, telegramId);
|
||||
const grouped = new Map<string, string[]>();
|
||||
for (const m of models) {
|
||||
const list = grouped.get(m.provider) ?? [];
|
||||
@@ -170,7 +170,7 @@ async function handleCommand(ctx: CommandContext): Promise<boolean> {
|
||||
}
|
||||
|
||||
if (lower === '!model') {
|
||||
const current = getSessionModel('telegram', telegramId);
|
||||
const current = getSessionModel('telegram', ctx.userId, telegramId);
|
||||
await send(
|
||||
current
|
||||
? `Current model: *${current}*`
|
||||
@@ -182,7 +182,7 @@ async function handleCommand(ctx: CommandContext): Promise<boolean> {
|
||||
if (lower.startsWith('!model ')) {
|
||||
const requested = content.slice('!model '.length).trim();
|
||||
if (!requested) {
|
||||
const current = getSessionModel('telegram', telegramId);
|
||||
const current = getSessionModel('telegram', ctx.userId, telegramId);
|
||||
await send(current ? `Current model: *${current}*` : 'No active session yet.');
|
||||
return true;
|
||||
}
|
||||
@@ -192,7 +192,7 @@ async function handleCommand(ctx: CommandContext): Promise<boolean> {
|
||||
await send(`Model not found: \`${requested}\`\nUse \`!models\` to see available models.`);
|
||||
return true;
|
||||
}
|
||||
setSessionModel('telegram', telegramId, match.id);
|
||||
setSessionModel('telegram', ctx.userId, telegramId, match.id);
|
||||
await send(`Model switched to *${match.id}*. The new model will be used on your next message.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { join } from 'path';
|
||||
import { rmSync } from 'node:fs';
|
||||
import { Client, LocalAuth } from 'whatsapp-web.js';
|
||||
import { getServerIntegration, upsertServerIntegration } from 'officerdb';
|
||||
import { DATA_PATH } from '@@/data-path';
|
||||
@@ -117,6 +118,18 @@ export async function startWhatsAppBotIfConfigured(): Promise<void> {
|
||||
}
|
||||
|
||||
export async function disconnectWhatsApp(): Promise<void> {
|
||||
if (client) {
|
||||
try {
|
||||
await client.logout();
|
||||
} catch {
|
||||
// May fail if not authenticated
|
||||
}
|
||||
}
|
||||
await stopWhatsAppBot();
|
||||
|
||||
// Remove cached session so a new QR is shown on next connect
|
||||
const authPath = join(DATA_PATH, '.wwebjs_auth');
|
||||
rmSync(authPath, { recursive: true, force: true });
|
||||
|
||||
await upsertServerIntegration('whatsapp', {}, false);
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ async function handleCommand(ctx: CommandContext): Promise<boolean> {
|
||||
await send('No models available.');
|
||||
return true;
|
||||
}
|
||||
const current = getSessionModel('whatsapp', whatsappId);
|
||||
const current = getSessionModel('whatsapp', ctx.userId, whatsappId);
|
||||
const grouped = new Map<string, string[]>();
|
||||
for (const m of models) {
|
||||
const list = grouped.get(m.provider) ?? [];
|
||||
@@ -171,7 +171,7 @@ async function handleCommand(ctx: CommandContext): Promise<boolean> {
|
||||
}
|
||||
|
||||
if (lower === '!model') {
|
||||
const current = getSessionModel('whatsapp', whatsappId);
|
||||
const current = getSessionModel('whatsapp', ctx.userId, whatsappId);
|
||||
await send(
|
||||
current
|
||||
? `Current model: *${current}*`
|
||||
@@ -183,7 +183,7 @@ async function handleCommand(ctx: CommandContext): Promise<boolean> {
|
||||
if (lower.startsWith('!model ')) {
|
||||
const requested = content.slice('!model '.length).trim();
|
||||
if (!requested) {
|
||||
const current = getSessionModel('whatsapp', whatsappId);
|
||||
const current = getSessionModel('whatsapp', ctx.userId, whatsappId);
|
||||
await send(current ? `Current model: *${current}*` : 'No active session yet.');
|
||||
return true;
|
||||
}
|
||||
@@ -193,7 +193,7 @@ async function handleCommand(ctx: CommandContext): Promise<boolean> {
|
||||
await send(`Model not found: \`${requested}\`\nUse \`!models\` to see available models.`);
|
||||
return true;
|
||||
}
|
||||
setSessionModel('whatsapp', whatsappId, match.id);
|
||||
setSessionModel('whatsapp', ctx.userId, whatsappId, match.id);
|
||||
await send(`Model switched to *${match.id}*. The new model will be used on your next message.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user