Parcourir la source

webapi: replace persistent OSCAR bridge store with in-memory session fields

also fix session management, the server sent redundant departure
messages on signout.
Mike il y a 2 semaines
Parent
commit
faf161f60f

+ 1 - 2
cmd/server/factory.go

@@ -583,8 +583,7 @@ func WebAPI(deps Container) *webapi.Server {
 		// Phase 2 additions
 		// Phase 2 additions
 		BuddyBroadcaster: oscarBuddyBroadcaster,
 		BuddyBroadcaster: oscarBuddyBroadcaster,
 		// Phase 4 additions for OSCAR Bridge
 		// Phase 4 additions for OSCAR Bridge
-		OSCARBridgeStore: deps.sqLiteUserStore.NewOSCARBridgeStore(),
-		OSCARConfig:      webapi.NewOSCARConfigAdapter(deps.cfg),
+		OSCARConfig: webapi.NewOSCARConfigAdapter(deps.cfg),
 		// Phase 5 additions for buddy list and messaging
 		// Phase 5 additions for buddy list and messaging
 		BuddyListManager:   buddyListManager,
 		BuddyListManager:   buddyListManager,
 		ChatSessionManager: deps.chatSessionManager,
 		ChatSessionManager: deps.chatSessionManager,

+ 1 - 2
server/webapi/handler.go

@@ -23,8 +23,7 @@ type Handler struct {
 	// Phase 2 additions
 	// Phase 2 additions
 	BuddyBroadcaster BuddyBroadcaster
 	BuddyBroadcaster BuddyBroadcaster
 	// Phase 4 additions for OSCAR Bridge
 	// Phase 4 additions for OSCAR Bridge
-	OSCARBridgeStore OSCARBridgeStore
-	OSCARConfig      OSCARConfig
+	OSCARConfig OSCARConfig
 	// Phase 5 additions for buddy list and messaging
 	// Phase 5 additions for buddy list and messaging
 	BuddyListManager   interface{}
 	BuddyListManager   interface{}
 	RecalcWarning      func(ctx context.Context, instance *state.SessionInstance) error
 	RecalcWarning      func(ctx context.Context, instance *state.SessionInstance) error

+ 11 - 28
server/webapi/handlers/oscar_bridge.go

@@ -22,7 +22,6 @@ type OSCARBridgeHandler struct {
 	SessionManager   *state.WebAPISessionManager
 	SessionManager   *state.WebAPISessionManager
 	OSCARAuthService OSCARAuthService
 	OSCARAuthService OSCARAuthService
 	CookieBaker      CookieBaker
 	CookieBaker      CookieBaker
-	BridgeStore      OSCARBridgeStore
 	Config           OSCARConfig
 	Config           OSCARConfig
 	Logger           *slog.Logger
 	Logger           *slog.Logger
 }
 }
@@ -45,16 +44,6 @@ type CookieBaker interface {
 	Crack(data []byte) ([]byte, error)
 	Crack(data []byte) ([]byte, error)
 }
 }
 
 
-// OSCARBridgeStore manages the persistence of OSCAR bridge sessions.
-type OSCARBridgeStore interface {
-	// SaveBridgeSession stores the mapping between WebAPI and OSCAR sessions
-	SaveBridgeSession(ctx context.Context, webSessionID string, oscarCookie []byte, bosHost string, bosPort int) error
-	// GetBridgeSession retrieves bridge session details
-	GetBridgeSession(ctx context.Context, webSessionID string) (*state.OSCARBridgeSession, error)
-	// DeleteBridgeSession removes a bridge session
-	DeleteBridgeSession(ctx context.Context, webSessionID string) error
-}
-
 // OSCARConfig provides configuration for OSCAR services.
 // OSCARConfig provides configuration for OSCAR services.
 type OSCARConfig interface {
 type OSCARConfig interface {
 	// GetBOSAddress returns the BOS server address for client connections
 	// GetBOSAddress returns the BOS server address for client connections
@@ -218,15 +207,12 @@ func (h *OSCARBridgeHandler) StartOSCARSession(w http.ResponseWriter, r *http.Re
 		host, port = h.Config.GetBOSAddress()
 		host, port = h.Config.GetBOSAddress()
 	}
 	}
 
 
-	// Store bridge session in database
-	if h.BridgeStore != nil {
-		if err := h.BridgeStore.SaveBridgeSession(ctx, aimsid, cookie, host, port); err != nil {
-			h.Logger.Error("failed to save bridge session",
-				"error", err,
-				"aimsid", aimsid)
-			// Continue anyway - the bridge will work without persistence
-		}
-	}
+	// Record the bridge details on the session so a repeat startOSCARSession
+	// can return the same connection details via returnExistingBridge.
+	session.OSCARCookie = cookie
+	session.BOSHost = host
+	session.BOSPort = port
+	session.UseSSL = useSSL
 
 
 	// Prepare response
 	// Prepare response
 	resp := h.buildResponse(host, port, cookie, useSSL, compress)
 	resp := h.buildResponse(host, port, cookie, useSSL, compress)
@@ -292,14 +278,11 @@ func (h *OSCARBridgeHandler) parseBoolParam(value string) bool {
 
 
 // returnExistingBridge returns details for an existing OSCAR bridge.
 // returnExistingBridge returns details for an existing OSCAR bridge.
 func (h *OSCARBridgeHandler) returnExistingBridge(w http.ResponseWriter, r *http.Request, session *state.WebAPISession) {
 func (h *OSCARBridgeHandler) returnExistingBridge(w http.ResponseWriter, r *http.Request, session *state.WebAPISession) {
-	// Retrieve existing bridge details from store
-	if h.BridgeStore != nil {
-		bridge, err := h.BridgeStore.GetBridgeSession(r.Context(), session.AimSID)
-		if err == nil && bridge != nil {
-			resp := h.buildResponse(bridge.BOSHost, bridge.BOSPort, bridge.OSCARCookie, bridge.UseSSL, false)
-			h.sendResponse(w, r, resp)
-			return
-		}
+	// Reuse the bridge details recorded on the session by StartOSCARSession.
+	if len(session.OSCARCookie) > 0 {
+		resp := h.buildResponse(session.BOSHost, session.BOSPort, session.OSCARCookie, session.UseSSL, false)
+		h.sendResponse(w, r, resp)
+		return
 	}
 	}
 
 
 	// If we can't retrieve the bridge, return an error
 	// If we can't retrieve the bridge, return an error

+ 32 - 31
server/webapi/handlers/session.go

@@ -225,15 +225,14 @@ func (h *SessionHandler) StartSession(w http.ResponseWriter, r *http.Request) {
 	if authToken != "" && h.OSCARSessionManager != nil {
 	if authToken != "" && h.OSCARSessionManager != nil {
 		fnCfg := func(sess *state.Session) {
 		fnCfg := func(sess *state.Session) {
 			sess.OnSessionClose(func() {
 			sess.OnSessionClose(func() {
-				if !shuttingDown(ctx) {
-					if err := h.BuddyBroadcaster.BroadcastBuddyDeparted(ctx, sess.IdentScreenName()); err != nil {
-						h.Logger.ErrorContext(ctx, "error sending buddy departure notifications", "err", err.Error())
-					}
-				}
-
 				ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
 				ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
 				defer cancel()
 				defer cancel()
 
 
+				// todo - a better way to detect server shutdowns
+				if err := h.BuddyBroadcaster.BroadcastBuddyDeparted(ctx, sess.IdentScreenName()); err != nil {
+					h.Logger.ErrorContext(ctx, "error sending buddy departure notifications", "err", err.Error())
+				}
+
 				// buddy list must be cleared before session is closed, otherwise
 				// buddy list must be cleared before session is closed, otherwise
 				// there will be a race condition that could cause the buddy list
 				// there will be a race condition that could cause the buddy list
 				// be prematurely deleted.
 				// be prematurely deleted.
@@ -637,31 +636,33 @@ func (h *SessionHandler) EndSession(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 
 
-	// Clean up OSCAR session if present
-	if session.OSCARSession != nil && h.OSCARSessionManager != nil {
-		// Broadcast departure to OSCAR clients
-		if h.BuddyBroadcaster != nil {
-			if err := h.BuddyBroadcaster.BroadcastBuddyDeparted(ctx, session.OSCARSession.IdentScreenName()); err != nil {
-				h.Logger.ErrorContext(ctx, "failed to broadcast buddy departure", "err", err.Error())
-			}
-		}
-
-		// Unregister buddy list
-		if h.BuddyListRegistry != nil {
-			if err := h.BuddyListRegistry.UnregisterBuddyList(ctx, session.ScreenName.IdentScreenName()); err != nil {
-				h.Logger.ErrorContext(ctx, "failed to unregister buddy list", "err", err.Error())
-			}
-		}
-
-		h.OSCARSessionManager.RemoveSession(session.OSCARSession.Session())
-	}
-
-	// Remove session
-	if err := h.SessionManager.RemoveSession(r.Context(), aimsid); err != nil {
-		h.Logger.ErrorContext(ctx, "failed to remove session", "err", err.Error())
-		h.sendError(w, r, http.StatusInternalServerError, "failed to end session")
-		return
-	}
+	//// Clean up OSCAR session if present
+	//if session.OSCARSession != nil && h.OSCARSessionManager != nil {
+	//	// Broadcast departure to OSCAR clients
+	//	if h.BuddyBroadcaster != nil {
+	//		if err := h.BuddyBroadcaster.BroadcastBuddyDeparted(ctx, session.OSCARSession.IdentScreenName()); err != nil {
+	//			h.Logger.ErrorContext(ctx, "failed to broadcast buddy departure", "err", err.Error())
+	//		}
+	//	}
+	//
+	//	// Unregister buddy list
+	//	if h.BuddyListRegistry != nil {
+	//		if err := h.BuddyListRegistry.UnregisterBuddyList(ctx, session.ScreenName.IdentScreenName()); err != nil {
+	//			h.Logger.ErrorContext(ctx, "failed to unregister buddy list", "err", err.Error())
+	//		}
+	//	}
+	//
+	//	h.OSCARSessionManager.RemoveSession(session.OSCARSession.Session())
+	//}
+	//
+	//// Remove session
+	//if err := h.SessionManager.RemoveSession(r.Context(), aimsid); err != nil {
+	//	h.Logger.ErrorContext(ctx, "failed to remove session", "err", err.Error())
+	//	h.sendError(w, r, http.StatusInternalServerError, "failed to end session")
+	//	return
+	//}
+
+	session.OSCARSession.CloseInstance()
 
 
 	// Send response
 	// Send response
 	resp := EndSessionResponse{}
 	resp := EndSessionResponse{}

+ 0 - 1
server/webapi/server.go

@@ -81,7 +81,6 @@ func NewServer(listeners []string, logger *slog.Logger, handler Handler, apiKeyV
 		SessionManager:   sessionManager,
 		SessionManager:   sessionManager,
 		OSCARAuthService: handler.AuthService,
 		OSCARAuthService: handler.AuthService,
 		CookieBaker:      handler.CookieBaker,
 		CookieBaker:      handler.CookieBaker,
-		BridgeStore:      handler.OSCARBridgeStore,
 		Config:           handler.OSCARConfig,
 		Config:           handler.OSCARConfig,
 		Logger:           logger,
 		Logger:           logger,
 	}
 	}

+ 0 - 8
server/webapi/types.go

@@ -89,14 +89,6 @@ type BuddyBroadcaster interface {
 
 
 // Phase 4: OSCAR Bridge interfaces
 // Phase 4: OSCAR Bridge interfaces
 
 
-// OSCARBridgeStore manages the persistence of OSCAR bridge sessions.
-type OSCARBridgeStore interface {
-	SaveBridgeSession(ctx context.Context, webSessionID string, oscarCookie []byte, bosHost string, bosPort int) error
-	SaveBridgeSessionWithDetails(ctx context.Context, session *state.OSCARBridgeSession) error
-	GetBridgeSession(ctx context.Context, webSessionID string) (*state.OSCARBridgeSession, error)
-	DeleteBridgeSession(ctx context.Context, webSessionID string) error
-}
-
 // OSCARConfig provides configuration for OSCAR services.
 // OSCARConfig provides configuration for OSCAR services.
 type OSCARConfig interface {
 type OSCARConfig interface {
 	GetBOSAddress() (host string, port int)
 	GetBOSAddress() (host string, port int)

+ 17 - 0
state/migrations/0041_drop_oscar_bridge_sessions.down.sql

@@ -0,0 +1,17 @@
+-- Rollback: recreate the WebAPI-to-OSCAR bridge sessions table (see 0018).
+
+CREATE TABLE IF NOT EXISTS oscar_bridge_sessions (
+    web_session_id VARCHAR(64) PRIMARY KEY,
+    oscar_cookie BLOB NOT NULL,
+    bos_host VARCHAR(255) NOT NULL,
+    bos_port INTEGER NOT NULL,
+    use_ssl BOOLEAN DEFAULT FALSE,
+    screen_name VARCHAR(97) NOT NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    last_accessed TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    client_name VARCHAR(255),
+    client_version VARCHAR(50)
+);
+
+CREATE INDEX IF NOT EXISTS idx_oscar_bridge_screen_name ON oscar_bridge_sessions(screen_name);
+CREATE INDEX IF NOT EXISTS idx_oscar_bridge_last_accessed ON oscar_bridge_sessions(last_accessed);

+ 3 - 0
state/migrations/0041_drop_oscar_bridge_sessions.up.sql

@@ -0,0 +1,3 @@
+-- The WebAPI-to-OSCAR bridge mapping (aimsid -> cookie/host/port) is now held
+-- in-memory on the WebAPISession, so this table is no longer used.
+DROP TABLE IF EXISTS oscar_bridge_sessions;

+ 0 - 383
state/webapi_oscar_bridge.go

@@ -1,383 +0,0 @@
-package state
-
-import (
-	"context"
-	"database/sql"
-	"encoding/hex"
-	"errors"
-	"fmt"
-	"time"
-)
-
-// OSCARBridgeStore manages the persistence of OSCAR bridge sessions in the database.
-// It provides methods to store, retrieve, and manage the mapping between WebAPI
-// sessions and OSCAR authentication cookies.
-type OSCARBridgeStore struct {
-	store *SQLiteUserStore
-}
-
-// NewOSCARBridgeStore creates a new OSCAR bridge store instance.
-func (s *SQLiteUserStore) NewOSCARBridgeStore() *OSCARBridgeStore {
-	return &OSCARBridgeStore{store: s}
-}
-
-// OSCARBridgeSession represents a bridge between WebAPI and OSCAR sessions.
-type OSCARBridgeSession struct {
-	WebSessionID  string    // WebAPI session identifier
-	OSCARCookie   []byte    // OSCAR authentication cookie
-	BOSHost       string    // BOS server hostname
-	BOSPort       int       // BOS server port
-	UseSSL        bool      // Whether to use SSL connection
-	ScreenName    string    // Screen name associated with the session
-	ClientName    string    // Client application name
-	ClientVersion string    // Client application version
-	CreatedAt     time.Time // Bridge creation timestamp
-	LastAccessed  time.Time // Last access timestamp
-}
-
-// SaveBridgeSession stores the mapping between WebAPI and OSCAR sessions.
-func (s *OSCARBridgeStore) SaveBridgeSession(ctx context.Context, webSessionID string,
-	oscarCookie []byte, bosHost string, bosPort int) error {
-
-	query := `
-		INSERT INTO oscar_bridge_sessions 
-		(web_session_id, oscar_cookie, bos_host, bos_port, screen_name, created_at, last_accessed)
-		VALUES (?, ?, ?, ?, ?, ?, ?)
-		ON CONFLICT(web_session_id) DO UPDATE SET
-			oscar_cookie = excluded.oscar_cookie,
-			bos_host = excluded.bos_host,
-			bos_port = excluded.bos_port,
-			last_accessed = excluded.last_accessed
-	`
-
-	now := time.Now()
-	// Note: We'll need to get the screen name from the session manager
-	// For now, using a placeholder
-	screenName := "" // This should be passed from the handler
-
-	_, err := s.store.db.ExecContext(ctx, query,
-		webSessionID, oscarCookie, bosHost, bosPort, screenName, now, now)
-	if err != nil {
-		return fmt.Errorf("failed to save bridge session: %w", err)
-	}
-
-	return nil
-}
-
-// SaveBridgeSessionWithDetails stores a complete bridge session with all details.
-func (s *OSCARBridgeStore) SaveBridgeSessionWithDetails(ctx context.Context, session *OSCARBridgeSession) error {
-	query := `
-		INSERT INTO oscar_bridge_sessions 
-		(web_session_id, oscar_cookie, bos_host, bos_port, use_ssl, screen_name, 
-		 client_name, client_version, created_at, last_accessed)
-		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
-		ON CONFLICT(web_session_id) DO UPDATE SET
-			oscar_cookie = excluded.oscar_cookie,
-			bos_host = excluded.bos_host,
-			bos_port = excluded.bos_port,
-			use_ssl = excluded.use_ssl,
-			last_accessed = excluded.last_accessed
-	`
-
-	_, err := s.store.db.ExecContext(ctx, query,
-		session.WebSessionID, session.OSCARCookie, session.BOSHost, session.BOSPort,
-		session.UseSSL, session.ScreenName, session.ClientName, session.ClientVersion,
-		session.CreatedAt, session.LastAccessed)
-	if err != nil {
-		return fmt.Errorf("failed to save bridge session: %w", err)
-	}
-
-	return nil
-}
-
-// GetBridgeSession retrieves bridge session details by WebAPI session ID.
-func (s *OSCARBridgeStore) GetBridgeSession(ctx context.Context, webSessionID string) (*OSCARBridgeSession, error) {
-	query := `
-		SELECT web_session_id, oscar_cookie, bos_host, bos_port, use_ssl, screen_name,
-		       client_name, client_version, created_at, last_accessed
-		FROM oscar_bridge_sessions
-		WHERE web_session_id = ?
-	`
-
-	var session OSCARBridgeSession
-	var clientName, clientVersion sql.NullString
-
-	err := s.store.db.QueryRowContext(ctx, query, webSessionID).Scan(
-		&session.WebSessionID,
-		&session.OSCARCookie,
-		&session.BOSHost,
-		&session.BOSPort,
-		&session.UseSSL,
-		&session.ScreenName,
-		&clientName,
-		&clientVersion,
-		&session.CreatedAt,
-		&session.LastAccessed,
-	)
-
-	if err != nil {
-		if errors.Is(err, sql.ErrNoRows) {
-			return nil, fmt.Errorf("bridge session not found")
-		}
-		return nil, fmt.Errorf("failed to get bridge session: %w", err)
-	}
-
-	// Handle nullable fields
-	if clientName.Valid {
-		session.ClientName = clientName.String
-	}
-	if clientVersion.Valid {
-		session.ClientVersion = clientVersion.String
-	}
-
-	// Update last accessed time
-	go s.touchSession(context.Background(), webSessionID)
-
-	return &session, nil
-}
-
-// GetBridgeSessionByScreenName retrieves bridge sessions by screen name.
-func (s *OSCARBridgeStore) GetBridgeSessionByScreenName(ctx context.Context, screenName string) ([]*OSCARBridgeSession, error) {
-	query := `
-		SELECT web_session_id, oscar_cookie, bos_host, bos_port, use_ssl, screen_name,
-		       client_name, client_version, created_at, last_accessed
-		FROM oscar_bridge_sessions
-		WHERE screen_name = ?
-		ORDER BY last_accessed DESC
-	`
-
-	rows, err := s.store.db.QueryContext(ctx, query, screenName)
-	if err != nil {
-		return nil, fmt.Errorf("failed to query bridge sessions: %w", err)
-	}
-	defer rows.Close()
-
-	var sessions []*OSCARBridgeSession
-
-	for rows.Next() {
-		var session OSCARBridgeSession
-		var clientName, clientVersion sql.NullString
-
-		err := rows.Scan(
-			&session.WebSessionID,
-			&session.OSCARCookie,
-			&session.BOSHost,
-			&session.BOSPort,
-			&session.UseSSL,
-			&session.ScreenName,
-			&clientName,
-			&clientVersion,
-			&session.CreatedAt,
-			&session.LastAccessed,
-		)
-
-		if err != nil {
-			return nil, fmt.Errorf("failed to scan bridge session: %w", err)
-		}
-
-		// Handle nullable fields
-		if clientName.Valid {
-			session.ClientName = clientName.String
-		}
-		if clientVersion.Valid {
-			session.ClientVersion = clientVersion.String
-		}
-
-		sessions = append(sessions, &session)
-	}
-
-	if err := rows.Err(); err != nil {
-		return nil, fmt.Errorf("error iterating bridge sessions: %w", err)
-	}
-
-	return sessions, nil
-}
-
-// DeleteBridgeSession removes a bridge session.
-func (s *OSCARBridgeStore) DeleteBridgeSession(ctx context.Context, webSessionID string) error {
-	query := `DELETE FROM oscar_bridge_sessions WHERE web_session_id = ?`
-
-	result, err := s.store.db.ExecContext(ctx, query, webSessionID)
-	if err != nil {
-		return fmt.Errorf("failed to delete bridge session: %w", err)
-	}
-
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return fmt.Errorf("failed to get rows affected: %w", err)
-	}
-
-	if rowsAffected == 0 {
-		return fmt.Errorf("bridge session not found")
-	}
-
-	return nil
-}
-
-// CleanupExpiredSessions removes bridge sessions that haven't been accessed recently.
-func (s *OSCARBridgeStore) CleanupExpiredSessions(ctx context.Context, maxAge time.Duration) (int, error) {
-	cutoff := time.Now().Add(-maxAge)
-
-	query := `DELETE FROM oscar_bridge_sessions WHERE last_accessed < ?`
-
-	result, err := s.store.db.ExecContext(ctx, query, cutoff)
-	if err != nil {
-		return 0, fmt.Errorf("failed to cleanup expired sessions: %w", err)
-	}
-
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return 0, fmt.Errorf("failed to get rows affected: %w", err)
-	}
-
-	return int(rowsAffected), nil
-}
-
-// touchSession updates the last accessed time for a session (internal helper).
-func (s *OSCARBridgeStore) touchSession(ctx context.Context, webSessionID string) {
-	query := `UPDATE oscar_bridge_sessions SET last_accessed = ? WHERE web_session_id = ?`
-	_, _ = s.store.db.ExecContext(ctx, query, time.Now(), webSessionID)
-}
-
-// GetAllBridgeSessions returns all active bridge sessions (for monitoring/admin).
-func (s *OSCARBridgeStore) GetAllBridgeSessions(ctx context.Context) ([]*OSCARBridgeSession, error) {
-	query := `
-		SELECT web_session_id, oscar_cookie, bos_host, bos_port, use_ssl, screen_name,
-		       client_name, client_version, created_at, last_accessed
-		FROM oscar_bridge_sessions
-		ORDER BY last_accessed DESC
-	`
-
-	rows, err := s.store.db.QueryContext(ctx, query)
-	if err != nil {
-		return nil, fmt.Errorf("failed to query all bridge sessions: %w", err)
-	}
-	defer rows.Close()
-
-	var sessions []*OSCARBridgeSession
-
-	for rows.Next() {
-		var session OSCARBridgeSession
-		var clientName, clientVersion sql.NullString
-
-		err := rows.Scan(
-			&session.WebSessionID,
-			&session.OSCARCookie,
-			&session.BOSHost,
-			&session.BOSPort,
-			&session.UseSSL,
-			&session.ScreenName,
-			&clientName,
-			&clientVersion,
-			&session.CreatedAt,
-			&session.LastAccessed,
-		)
-
-		if err != nil {
-			return nil, fmt.Errorf("failed to scan bridge session: %w", err)
-		}
-
-		// Handle nullable fields
-		if clientName.Valid {
-			session.ClientName = clientName.String
-		}
-		if clientVersion.Valid {
-			session.ClientVersion = clientVersion.String
-		}
-
-		sessions = append(sessions, &session)
-	}
-
-	if err := rows.Err(); err != nil {
-		return nil, fmt.Errorf("error iterating bridge sessions: %w", err)
-	}
-
-	return sessions, nil
-}
-
-// GetStatistics returns statistics about bridge sessions.
-func (s *OSCARBridgeStore) GetStatistics(ctx context.Context) (map[string]interface{}, error) {
-	stats := make(map[string]interface{})
-
-	// Total sessions
-	var totalCount int
-	err := s.store.db.QueryRowContext(ctx,
-		`SELECT COUNT(*) FROM oscar_bridge_sessions`).Scan(&totalCount)
-	if err != nil {
-		return nil, fmt.Errorf("failed to get total count: %w", err)
-	}
-	stats["total_sessions"] = totalCount
-
-	// Active sessions (accessed in last hour)
-	var activeCount int
-	oneHourAgo := time.Now().Add(-time.Hour)
-	err = s.store.db.QueryRowContext(ctx,
-		`SELECT COUNT(*) FROM oscar_bridge_sessions WHERE last_accessed > ?`,
-		oneHourAgo).Scan(&activeCount)
-	if err != nil {
-		return nil, fmt.Errorf("failed to get active count: %w", err)
-	}
-	stats["active_sessions"] = activeCount
-
-	// SSL vs non-SSL
-	var sslCount int
-	err = s.store.db.QueryRowContext(ctx,
-		`SELECT COUNT(*) FROM oscar_bridge_sessions WHERE use_ssl = true`).Scan(&sslCount)
-	if err != nil {
-		return nil, fmt.Errorf("failed to get SSL count: %w", err)
-	}
-	stats["ssl_sessions"] = sslCount
-	stats["non_ssl_sessions"] = totalCount - sslCount
-
-	return stats, nil
-}
-
-// ValidateOSCARCookie checks if an OSCAR cookie exists in the bridge store.
-// This can be used to validate incoming OSCAR connections.
-func (s *OSCARBridgeStore) ValidateOSCARCookie(ctx context.Context, cookie []byte) (*OSCARBridgeSession, error) {
-	// Convert cookie to hex for comparison
-	cookieHex := hex.EncodeToString(cookie)
-
-	query := `
-		SELECT web_session_id, oscar_cookie, bos_host, bos_port, use_ssl, screen_name,
-		       client_name, client_version, created_at, last_accessed
-		FROM oscar_bridge_sessions
-		WHERE hex(oscar_cookie) = ?
-	`
-
-	var session OSCARBridgeSession
-	var clientName, clientVersion sql.NullString
-
-	err := s.store.db.QueryRowContext(ctx, query, cookieHex).Scan(
-		&session.WebSessionID,
-		&session.OSCARCookie,
-		&session.BOSHost,
-		&session.BOSPort,
-		&session.UseSSL,
-		&session.ScreenName,
-		&clientName,
-		&clientVersion,
-		&session.CreatedAt,
-		&session.LastAccessed,
-	)
-
-	if err != nil {
-		if errors.Is(err, sql.ErrNoRows) {
-			return nil, fmt.Errorf("cookie not found")
-		}
-		return nil, fmt.Errorf("failed to validate cookie: %w", err)
-	}
-
-	// Handle nullable fields
-	if clientName.Valid {
-		session.ClientName = clientName.String
-	}
-	if clientVersion.Valid {
-		session.ClientVersion = clientVersion.String
-	}
-
-	// Update last accessed time
-	go s.touchSession(context.Background(), session.WebSessionID)
-
-	return &session, nil
-}

+ 4 - 19
state/webapi_session.go

@@ -27,6 +27,10 @@ type WebAPISession struct {
 	AimSID              string                                         // Unique session ID for web client
 	AimSID              string                                         // Unique session ID for web client
 	ScreenName          DisplayScreenName                              // User identity
 	ScreenName          DisplayScreenName                              // User identity
 	OSCARSession        *SessionInstance                               // Bridge to existing OSCAR session
 	OSCARSession        *SessionInstance                               // Bridge to existing OSCAR session
+	OSCARCookie         []byte                                         // OSCAR auth cookie for the startOSCARSession handoff
+	BOSHost             string                                         // BOS host advertised to the web client
+	BOSPort             int                                            // BOS port advertised to the web client
+	UseSSL              bool                                           // Whether the handoff advertised an SSL BOS connection
 	Events              []string                                       // Subscribed event types
 	Events              []string                                       // Subscribed event types
 	EventQueue          *types.EventQueue                              // Per-session event queue
 	EventQueue          *types.EventQueue                              // Per-session event queue
 	DevID               string                                         // Developer ID that created this session
 	DevID               string                                         // Developer ID that created this session
@@ -468,25 +472,6 @@ func (m *WebAPISessionManager) GetAllSessions(ctx context.Context) []*WebAPISess
 	return sessions
 	return sessions
 }
 }
 
 
-// GetSessionsByScreenName returns all sessions for a given screen name.
-func (m *WebAPISessionManager) GetSessionsByScreenName(ctx context.Context, screenName DisplayScreenName) []*WebAPISession {
-	m.mu.RLock()
-	defer m.mu.RUnlock()
-
-	var sessions []*WebAPISession
-	identScreenName := screenName.IdentScreenName()
-
-	// Check both the byUser map and iterate through all sessions
-	// since a user might have multiple sessions
-	for _, session := range m.sessions {
-		if session.ScreenName.IdentScreenName() == identScreenName {
-			sessions = append(sessions, session)
-		}
-	}
-
-	return sessions
-}
-
 // cleanupExpiredSessions periodically removes expired sessions.
 // cleanupExpiredSessions periodically removes expired sessions.
 func (m *WebAPISessionManager) cleanupExpiredSessions() {
 func (m *WebAPISessionManager) cleanupExpiredSessions() {
 	for {
 	for {