@@ -175,6 +175,36 @@ function isAuthRejectionError(error) {
175175 }
176176}
177177
178+ /**
179+ * Format an auth rejection error with actionable information for users.
180+ * Includes the backend URL and a hint to sign in via VS Code command palette.
181+ */
182+ function formatAuthRejectionError ( originalError , backendUrl ) {
183+ const originalMsg =
184+ ( originalError && typeof originalError . message === "string" && originalError . message ) ||
185+ ( typeof originalError === "string" ? originalError : String ( originalError || "Unknown auth error" ) ) ;
186+
187+ const serverInfo = backendUrl ? ` (server: ${ backendUrl } )` : "" ;
188+ const hint = "Run 'Context Engine: Sign In' from the VS Code command palette to authenticate." ;
189+
190+ return `Authentication failed${ serverInfo } : ${ originalMsg } . ${ hint } ` ;
191+ }
192+
193+ /**
194+ * Emit a special log line that the VS Code extension can detect to show a notification toast.
195+ * Format: [ctxce:auth-error] JSON payload
196+ */
197+ function emitAuthErrorNotification ( backendUrl , originalError ) {
198+ const payload = {
199+ type : "auth_rejection" ,
200+ backend : backendUrl || "unknown" ,
201+ message : String ( originalError ?. message || originalError || "Authentication failed" ) ,
202+ hint : "Run 'Context Engine: Sign In' from the VS Code command palette" ,
203+ } ;
204+ // This special prefix allows the VS Code extension to detect auth errors in stderr
205+ debugLog ( `[ctxce:auth-error] ${ JSON . stringify ( payload ) } ` ) ;
206+ }
207+
178208function getBridgeRetryAttempts ( ) {
179209 try {
180210 const raw = process . env . CTXCE_TOOL_RETRY_ATTEMPTS ;
@@ -520,8 +550,21 @@ async function createBridgeServer(options) {
520550 sessionId = resolveSessionId ( ) ;
521551 }
522552
553+ // Only fall back to deterministic session if auth is not configured
554+ // If auth backend is configured but no session found, log warning instead of creating deterministic session
523555 if ( ! sessionId ) {
524- sessionId = `ctxce-${ Buffer . from ( workspace ) . toString ( "hex" ) . slice ( 0 , 24 ) } ` ;
556+ if ( authBackendUrl ) {
557+ // Auth is configured but no valid session - don't use deterministic fallback
558+ debugLog ( `[ctxce] WARNING: Auth backend configured (${ authBackendUrl } ) but no valid session found.` ) ;
559+ debugLog ( "[ctxce] To authenticate, run 'Context Engine: Sign In' from the VS Code command palette, or run `ctxce auth login` from the terminal." ) ;
560+ debugLog ( "[ctxce] Continuing with deterministic session for backward compatibility, but this may fail if backend requires auth." ) ;
561+ // Emit notification for VS Code extension
562+ emitAuthErrorNotification ( authBackendUrl , { message : "No valid session found - authentication required" } ) ;
563+ sessionId = `ctxce-${ Buffer . from ( workspace ) . toString ( "hex" ) . slice ( 0 , 24 ) } ` ;
564+ } else {
565+ // No auth configured - use deterministic session for local-only operation
566+ sessionId = `ctxce-${ Buffer . from ( workspace ) . toString ( "hex" ) . slice ( 0 , 24 ) } ` ;
567+ }
525568 }
526569
527570 // Best-effort: inform the indexer of default collection and session.
@@ -531,6 +574,17 @@ async function createBridgeServer(options) {
531574 defaultsPayload . collection = defaultCollection ;
532575 }
533576
577+ // Include org context from auth entry if available (for org-scoped collection isolation)
578+ try {
579+ const authEntry = backendHint ? loadAuthEntry ( backendHint ) : null ;
580+ if ( authEntry && authEntry . org_id ) {
581+ defaultsPayload . org_id = authEntry . org_id ;
582+ defaultsPayload . org_slug = authEntry . org_slug ;
583+ }
584+ } catch {
585+ // ignore auth entry lookup failures
586+ }
587+
534588 const repoName = detectRepoName ( workspace , config ) ;
535589
536590 try {
@@ -782,10 +836,13 @@ async function createBridgeServer(options) {
782836
783837 // Backend auth rejection (mcp_auth.py ValidationError) - expire local auth
784838 if ( isAuthRejectionError ( err ) ) {
839+ const serverUrl = backendHint || uploadServiceUrl || "unknown server" ;
785840 debugLog (
786- " [ctxce] tools/call: backend auth rejection; marking local session as expired: " +
841+ ` [ctxce] tools/call: backend auth rejection from ${ serverUrl } ; marking local session as expired: ` +
787842 String ( err ) ,
788843 ) ;
844+ // Emit special notification for VS Code extension to detect and show toast
845+ emitAuthErrorNotification ( serverUrl , err ) ;
789846 if ( backendHint ) {
790847 try {
791848 const entry = loadAuthEntry ( backendHint ) ;
@@ -796,6 +853,13 @@ async function createBridgeServer(options) {
796853 // ignore failures
797854 }
798855 }
856+ // Enhance error with actionable message before throwing
857+ if ( ! isTransientToolError ( err ) || attempt === maxAttempts - 1 ) {
858+ const enhancedMessage = formatAuthRejectionError ( err , serverUrl ) ;
859+ const enhancedError = new Error ( enhancedMessage ) ;
860+ enhancedError . cause = err ;
861+ throw enhancedError ;
862+ }
799863 }
800864
801865 if ( ! isTransientToolError ( err ) || attempt === maxAttempts - 1 ) {
@@ -908,10 +972,42 @@ export async function runHttpMcpServer(options) {
908972 // Check Bearer token for MCP endpoint (accept /mcp and /mcp/ for compatibility)
909973 if ( parsedUrl . pathname === "/mcp" || parsedUrl . pathname === "/mcp/" ) {
910974 const authHeader = req . headers [ "authorization" ] || "" ;
911- const token = authHeader . startsWith ( "Bearer " ) ? authHeader . slice ( 7 ) : null ;
912-
913- // TODO: Validate token and inject session
914- // For now, allow unauthenticated (backward compatible)
975+ const bearerToken = authHeader . startsWith ( "Bearer " ) ? authHeader . slice ( 7 ) : null ;
976+
977+ // ----------------------------------------------------------------
978+ // AUTHENTICATION DESIGN: Permissive by default for backward compatibility
979+ // ----------------------------------------------------------------
980+ // The condition `bearerToken && hasTokenStore()` is INTENTIONALLY permissive:
981+ //
982+ // 1. PRE-EXISTING USERS (v3.0.0, local/dev mode):
983+ // - No OAuth flow occurs → tokenStore remains empty
984+ // - hasTokenStore() returns false → auth check skipped entirely
985+ // - Requests proceed without authentication (local dev experience)
986+ //
987+ // 2. SAAS PLATFORM USERS (multi-tenant):
988+ // - User completes OAuth flow → token stored in tokenStore
989+ // - hasTokenStore() returns true → bearer token validation required
990+ // - Invalid/missing tokens are rejected with 401
991+ //
992+ // WHY NOT `hasTokenStore() && !bearerToken` (require token when store exists)?
993+ // - Mixed environments: Some clients may be local (no auth) while others
994+ // are authenticated. Requiring auth globally after first login would
995+ // break local dev workflows in hybrid setups.
996+ // - The current design: "validate if provided, but don't require"
997+ //
998+ // SECURITY NOTE: If strict authentication is required for all clients once
999+ // any user authenticates, add an environment flag like CTXCE_REQUIRE_AUTH=1
1000+ // and check it here to enforce bearer tokens regardless of token store state.
1001+ // ----------------------------------------------------------------
1002+ if ( bearerToken && oauthHandler . hasTokenStore ( ) ) {
1003+ const sessionId = oauthHandler . lookupToken ( bearerToken ) ;
1004+ if ( ! sessionId ) {
1005+ // Token provided but invalid - reject
1006+ res . writeHead ( 401 , { "Content-Type" : "application/json" } ) ;
1007+ res . end ( JSON . stringify ( { jsonrpc : "2.0" , error : { code : - 32000 , message : "Invalid or expired bearer token" } , id : null } ) ) ;
1008+ return ;
1009+ }
1010+ }
9151011
9161012 if ( req . method !== "POST" ) {
9171013 res . statusCode = 405 ;
0 commit comments