From c5658429be31429939ebe013adb4ffd604319f43 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 22 Sep 2026 04:16:01 +0000 Subject: [PATCH 1/5] fix(web): require auth for public SaaS Ask APIs Co-authored-by: Michael Sukkarieh --- .../web/src/app/api/(server)/ee/chat/route.ts | 6 +++ .../web/src/app/api/(server)/ee/mcp/route.ts | 13 ++++--- .../web/src/ee/features/mcp/askCodebase.ts | 6 +++ .../web/src/features/chat/askAuth.test.ts | 39 +++++++++++++++++++ packages/web/src/features/chat/askAuth.ts | 14 +++++++ 5 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 packages/web/src/features/chat/askAuth.test.ts create mode 100644 packages/web/src/features/chat/askAuth.ts diff --git a/packages/web/src/app/api/(server)/ee/chat/route.ts b/packages/web/src/app/api/(server)/ee/chat/route.ts index c8a34aa8f..b3a5bbdbf 100644 --- a/packages/web/src/app/api/(server)/ee/chat/route.ts +++ b/packages/web/src/app/api/(server)/ee/chat/route.ts @@ -12,6 +12,7 @@ import { getAISDKLanguageModelAndOptions } from "@/features/chat/llm.server"; import { resolveContextWindow } from "@/features/chat/modelContextWindow.server"; import { materializeCommandMessageTexts } from "@/ee/features/chat/skills/commandResolution"; import { getAskSkillAvailabilityAnalytics, getAskSkillTurnCompletedAnalytics } from "@/ee/features/chat/skills/skillAnalytics.server"; +import { checkAskAuthentication } from "@/features/chat/askAuth"; import { apiHandler } from "@/lib/apiHandler"; import { ErrorCode } from "@/lib/errorCodes"; import { captureEvent } from "@/lib/posthog"; @@ -50,6 +51,11 @@ export const POST = apiHandler(async (req: NextRequest) => { const response = await sew(() => withOptionalAuth(async ({ org, user, prisma }) => { + const authError = checkAskAuthentication(user); + if (authError) { + return authError; + } + // Gate the generative path behind the `ask` entitlement. The client // also gates this, but server-side enforcement can't be bypassed. const askError = await checkAskEntitlement(); diff --git a/packages/web/src/app/api/(server)/ee/mcp/route.ts b/packages/web/src/app/api/(server)/ee/mcp/route.ts index d5e57224d..56ac91bad 100644 --- a/packages/web/src/app/api/(server)/ee/mcp/route.ts +++ b/packages/web/src/app/api/(server)/ee/mcp/route.ts @@ -2,9 +2,10 @@ import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { createMcpServer } from '@/ee/features/mcp/server'; import { MCP_PAID_PLAN_REQUIRED_MESSAGE } from '@/ee/features/mcp/constants'; +import { checkAskAuthentication } from '@/features/chat/askAuth'; import { withOptionalAuth } from '@/middleware/withAuth'; import { isServiceError } from '@/lib/utils'; -import { notAuthenticated, serviceErrorResponse, ServiceError } from '@/lib/serviceError'; +import { serviceErrorResponse, ServiceError } from '@/lib/serviceError'; import { ErrorCode } from '@/lib/errorCodes'; import { StatusCodes } from 'http-status-codes'; import { NextRequest } from 'next/server'; @@ -85,8 +86,9 @@ export const POST = apiHandler(async (request: NextRequest) => { const response = await sew(() => withOptionalAuth(async ({ user, principal }) => { - if (env.EXPERIMENT_ASK_GH_ENABLED === 'true' && !user) { - return notAuthenticated(); + const authError = checkAskAuthentication(user); + if (authError) { + return authError; } const ownerId = user?.id ?? null; const sessionId = request.headers.get(MCP_SESSION_ID_HEADER); @@ -151,8 +153,9 @@ export const DELETE = apiHandler(async (request: NextRequest) => { const result = await sew(() => withOptionalAuth(async ({ user }) => { - if (env.EXPERIMENT_ASK_GH_ENABLED === 'true' && !user) { - return notAuthenticated(); + const authError = checkAskAuthentication(user); + if (authError) { + return authError; } const ownerId = user?.id ?? null; const sessionId = request.headers.get(MCP_SESSION_ID_HEADER); diff --git a/packages/web/src/ee/features/mcp/askCodebase.ts b/packages/web/src/ee/features/mcp/askCodebase.ts index 35337d29f..7ff405441 100644 --- a/packages/web/src/ee/features/mcp/askCodebase.ts +++ b/packages/web/src/ee/features/mcp/askCodebase.ts @@ -4,6 +4,7 @@ import { generateChatNameFromMessage } from "@/ee/features/chat/llm.server"; import { getAISDKLanguageModelAndOptions } from "@/features/chat/llm.server"; import { resolveContextWindow } from "@/features/chat/modelContextWindow.server"; import { LanguageModelInfo, SBChatMessage, SearchScope } from "@/features/chat/types"; +import { checkAskAuthentication } from "@/features/chat/askAuth"; import { convertLLMOutputToPortableMarkdown, getAnswerPartFromAssistantMessage, getLanguageModelKey } from "@/features/chat/utils"; import { resolveModelCapabilities } from "@/features/chat/modelCapabilities.server"; import { ErrorCode } from "@/lib/errorCodes"; @@ -49,6 +50,11 @@ const blockStreamUntilFinish = async => sew(() => withOptionalAuth(async ({ org, user, prisma }) => { + const authError = checkAskAuthentication(user); + if (authError) { + return authError; + } + // Ask Sourcebot is a paid feature. askCodebase() is the single choke point // for the programmatic ask path (the MCP `ask_codebase` tool and the // /api/chat/blocking route both wrap it), so gating here covers both without diff --git a/packages/web/src/features/chat/askAuth.test.ts b/packages/web/src/features/chat/askAuth.test.ts new file mode 100644 index 000000000..a40c25203 --- /dev/null +++ b/packages/web/src/features/chat/askAuth.test.ts @@ -0,0 +1,39 @@ +import { beforeEach, describe, expect, test, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + env: { + EXPERIMENT_ASK_GH_ENABLED: "false", + }, +})); + +vi.mock("@sourcebot/shared", () => ({ + env: mocks.env, +})); + +const { checkAskAuthentication } = await import("./askAuth"); + +beforeEach(() => { + mocks.env.EXPERIMENT_ASK_GH_ENABLED = "false"; +}); + +describe("checkAskAuthentication", () => { + test("rejects anonymous Ask requests when Public SaaS is enabled", () => { + mocks.env.EXPERIMENT_ASK_GH_ENABLED = "true"; + + expect(checkAskAuthentication(undefined)).toEqual({ + statusCode: 401, + errorCode: "NOT_AUTHENTICATED", + message: "Not authenticated", + }); + }); + + test("allows authenticated Ask requests when Public SaaS is enabled", () => { + mocks.env.EXPERIMENT_ASK_GH_ENABLED = "true"; + + expect(checkAskAuthentication({ id: "user-1" })).toBeNull(); + }); + + test("allows anonymous Ask requests when Public SaaS is disabled", () => { + expect(checkAskAuthentication(undefined)).toBeNull(); + }); +}); diff --git a/packages/web/src/features/chat/askAuth.ts b/packages/web/src/features/chat/askAuth.ts new file mode 100644 index 000000000..4914d3e9b --- /dev/null +++ b/packages/web/src/features/chat/askAuth.ts @@ -0,0 +1,14 @@ +import { notAuthenticated, type ServiceError } from "@/lib/serviceError"; +import { env } from "@sourcebot/shared"; + +/** + * Public SaaS requires an authenticated user for Ask requests. Self-hosted + * deployments retain their existing anonymous-access behavior. + */ +export const checkAskAuthentication = (user: object | undefined): ServiceError | null => { + if (env.EXPERIMENT_ASK_GH_ENABLED === "true" && !user) { + return notAuthenticated(); + } + + return null; +}; From 0653ccef06a2fc749fabeda4207e954f2da3c654 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 22 Sep 2026 04:16:20 +0000 Subject: [PATCH 2/5] docs: add Ask API auth changelog entry Co-authored-by: Michael Sukkarieh --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d67263859..6f2a7b5f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- Require authentication for the streaming and blocking Ask APIs in Public SaaS deployments. [#1679](https://github.com/sourcebot-dev/sourcebot/pull/1679) + ## [5.1.14] - 2026-09-17 ### Added From b9eda6a5ae414b9044634fe7fd00241495d2eecf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 23 Sep 2026 03:07:43 +0000 Subject: [PATCH 3/5] refactor(web): generalize auth override helper Co-authored-by: Michael Sukkarieh --- packages/web/src/app/api/(server)/ee/chat/route.ts | 4 ++-- packages/web/src/app/api/(server)/ee/mcp/route.ts | 6 +++--- packages/web/src/ee/features/mcp/askCodebase.ts | 4 ++-- packages/web/src/features/chat/askAuth.test.ts | 10 +++++----- packages/web/src/features/chat/askAuth.ts | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/web/src/app/api/(server)/ee/chat/route.ts b/packages/web/src/app/api/(server)/ee/chat/route.ts index b3a5bbdbf..ff57e882f 100644 --- a/packages/web/src/app/api/(server)/ee/chat/route.ts +++ b/packages/web/src/app/api/(server)/ee/chat/route.ts @@ -12,7 +12,7 @@ import { getAISDKLanguageModelAndOptions } from "@/features/chat/llm.server"; import { resolveContextWindow } from "@/features/chat/modelContextWindow.server"; import { materializeCommandMessageTexts } from "@/ee/features/chat/skills/commandResolution"; import { getAskSkillAvailabilityAnalytics, getAskSkillTurnCompletedAnalytics } from "@/ee/features/chat/skills/skillAnalytics.server"; -import { checkAskAuthentication } from "@/features/chat/askAuth"; +import { checkAuthenticationRequiredOverride } from "@/features/chat/askAuth"; import { apiHandler } from "@/lib/apiHandler"; import { ErrorCode } from "@/lib/errorCodes"; import { captureEvent } from "@/lib/posthog"; @@ -51,7 +51,7 @@ export const POST = apiHandler(async (req: NextRequest) => { const response = await sew(() => withOptionalAuth(async ({ org, user, prisma }) => { - const authError = checkAskAuthentication(user); + const authError = checkAuthenticationRequiredOverride(user); if (authError) { return authError; } diff --git a/packages/web/src/app/api/(server)/ee/mcp/route.ts b/packages/web/src/app/api/(server)/ee/mcp/route.ts index 56ac91bad..7dc3485f9 100644 --- a/packages/web/src/app/api/(server)/ee/mcp/route.ts +++ b/packages/web/src/app/api/(server)/ee/mcp/route.ts @@ -2,7 +2,7 @@ import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { createMcpServer } from '@/ee/features/mcp/server'; import { MCP_PAID_PLAN_REQUIRED_MESSAGE } from '@/ee/features/mcp/constants'; -import { checkAskAuthentication } from '@/features/chat/askAuth'; +import { checkAuthenticationRequiredOverride } from '@/features/chat/askAuth'; import { withOptionalAuth } from '@/middleware/withAuth'; import { isServiceError } from '@/lib/utils'; import { serviceErrorResponse, ServiceError } from '@/lib/serviceError'; @@ -86,7 +86,7 @@ export const POST = apiHandler(async (request: NextRequest) => { const response = await sew(() => withOptionalAuth(async ({ user, principal }) => { - const authError = checkAskAuthentication(user); + const authError = checkAuthenticationRequiredOverride(user); if (authError) { return authError; } @@ -153,7 +153,7 @@ export const DELETE = apiHandler(async (request: NextRequest) => { const result = await sew(() => withOptionalAuth(async ({ user }) => { - const authError = checkAskAuthentication(user); + const authError = checkAuthenticationRequiredOverride(user); if (authError) { return authError; } diff --git a/packages/web/src/ee/features/mcp/askCodebase.ts b/packages/web/src/ee/features/mcp/askCodebase.ts index 7ff405441..c0826055a 100644 --- a/packages/web/src/ee/features/mcp/askCodebase.ts +++ b/packages/web/src/ee/features/mcp/askCodebase.ts @@ -4,7 +4,7 @@ import { generateChatNameFromMessage } from "@/ee/features/chat/llm.server"; import { getAISDKLanguageModelAndOptions } from "@/features/chat/llm.server"; import { resolveContextWindow } from "@/features/chat/modelContextWindow.server"; import { LanguageModelInfo, SBChatMessage, SearchScope } from "@/features/chat/types"; -import { checkAskAuthentication } from "@/features/chat/askAuth"; +import { checkAuthenticationRequiredOverride } from "@/features/chat/askAuth"; import { convertLLMOutputToPortableMarkdown, getAnswerPartFromAssistantMessage, getLanguageModelKey } from "@/features/chat/utils"; import { resolveModelCapabilities } from "@/features/chat/modelCapabilities.server"; import { ErrorCode } from "@/lib/errorCodes"; @@ -50,7 +50,7 @@ const blockStreamUntilFinish = async => sew(() => withOptionalAuth(async ({ org, user, prisma }) => { - const authError = checkAskAuthentication(user); + const authError = checkAuthenticationRequiredOverride(user); if (authError) { return authError; } diff --git a/packages/web/src/features/chat/askAuth.test.ts b/packages/web/src/features/chat/askAuth.test.ts index a40c25203..42ace92f6 100644 --- a/packages/web/src/features/chat/askAuth.test.ts +++ b/packages/web/src/features/chat/askAuth.test.ts @@ -10,17 +10,17 @@ vi.mock("@sourcebot/shared", () => ({ env: mocks.env, })); -const { checkAskAuthentication } = await import("./askAuth"); +const { checkAuthenticationRequiredOverride } = await import("./askAuth"); beforeEach(() => { mocks.env.EXPERIMENT_ASK_GH_ENABLED = "false"; }); -describe("checkAskAuthentication", () => { +describe("checkAuthenticationRequiredOverride", () => { test("rejects anonymous Ask requests when Public SaaS is enabled", () => { mocks.env.EXPERIMENT_ASK_GH_ENABLED = "true"; - expect(checkAskAuthentication(undefined)).toEqual({ + expect(checkAuthenticationRequiredOverride(undefined)).toEqual({ statusCode: 401, errorCode: "NOT_AUTHENTICATED", message: "Not authenticated", @@ -30,10 +30,10 @@ describe("checkAskAuthentication", () => { test("allows authenticated Ask requests when Public SaaS is enabled", () => { mocks.env.EXPERIMENT_ASK_GH_ENABLED = "true"; - expect(checkAskAuthentication({ id: "user-1" })).toBeNull(); + expect(checkAuthenticationRequiredOverride({ id: "user-1" })).toBeNull(); }); test("allows anonymous Ask requests when Public SaaS is disabled", () => { - expect(checkAskAuthentication(undefined)).toBeNull(); + expect(checkAuthenticationRequiredOverride(undefined)).toBeNull(); }); }); diff --git a/packages/web/src/features/chat/askAuth.ts b/packages/web/src/features/chat/askAuth.ts index 4914d3e9b..a147b3336 100644 --- a/packages/web/src/features/chat/askAuth.ts +++ b/packages/web/src/features/chat/askAuth.ts @@ -5,7 +5,7 @@ import { env } from "@sourcebot/shared"; * Public SaaS requires an authenticated user for Ask requests. Self-hosted * deployments retain their existing anonymous-access behavior. */ -export const checkAskAuthentication = (user: object | undefined): ServiceError | null => { +export const checkAuthenticationRequiredOverride = (user: object | undefined): ServiceError | null => { if (env.EXPERIMENT_ASK_GH_ENABLED === "true" && !user) { return notAuthenticated(); } From 270a8474bde562e28767eea0927ca2979358d869 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 23 Sep 2026 03:10:40 +0000 Subject: [PATCH 4/5] refactor(web): make auth override a boolean gate Co-authored-by: Michael Sukkarieh --- .../web/src/app/api/(server)/ee/chat/route.ts | 9 +++--- .../web/src/app/api/(server)/ee/mcp/route.ts | 14 ++++----- .../web/src/ee/features/mcp/askCodebase.ts | 9 +++--- .../web/src/features/chat/askAuth.test.ts | 31 +++++++------------ packages/web/src/features/chat/askAuth.ts | 9 +----- 5 files changed, 27 insertions(+), 45 deletions(-) diff --git a/packages/web/src/app/api/(server)/ee/chat/route.ts b/packages/web/src/app/api/(server)/ee/chat/route.ts index ff57e882f..b69c77274 100644 --- a/packages/web/src/app/api/(server)/ee/chat/route.ts +++ b/packages/web/src/app/api/(server)/ee/chat/route.ts @@ -12,11 +12,11 @@ import { getAISDKLanguageModelAndOptions } from "@/features/chat/llm.server"; import { resolveContextWindow } from "@/features/chat/modelContextWindow.server"; import { materializeCommandMessageTexts } from "@/ee/features/chat/skills/commandResolution"; import { getAskSkillAvailabilityAnalytics, getAskSkillTurnCompletedAnalytics } from "@/ee/features/chat/skills/skillAnalytics.server"; -import { checkAuthenticationRequiredOverride } from "@/features/chat/askAuth"; +import { isAuthRequiredOverrideEnabled } from "@/features/chat/askAuth"; import { apiHandler } from "@/lib/apiHandler"; import { ErrorCode } from "@/lib/errorCodes"; import { captureEvent } from "@/lib/posthog"; -import { notFound, requestBodySchemaValidationError, ServiceError, serviceErrorResponse } from "@/lib/serviceError"; +import { notAuthenticated, notFound, requestBodySchemaValidationError, ServiceError, serviceErrorResponse } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; import { withOptionalAuth } from "@/middleware/withAuth"; import * as Sentry from "@sentry/nextjs"; @@ -51,9 +51,8 @@ export const POST = apiHandler(async (req: NextRequest) => { const response = await sew(() => withOptionalAuth(async ({ org, user, prisma }) => { - const authError = checkAuthenticationRequiredOverride(user); - if (authError) { - return authError; + if (isAuthRequiredOverrideEnabled && !user) { + return notAuthenticated(); } // Gate the generative path behind the `ask` entitlement. The client diff --git a/packages/web/src/app/api/(server)/ee/mcp/route.ts b/packages/web/src/app/api/(server)/ee/mcp/route.ts index 7dc3485f9..298af202b 100644 --- a/packages/web/src/app/api/(server)/ee/mcp/route.ts +++ b/packages/web/src/app/api/(server)/ee/mcp/route.ts @@ -2,10 +2,10 @@ import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { createMcpServer } from '@/ee/features/mcp/server'; import { MCP_PAID_PLAN_REQUIRED_MESSAGE } from '@/ee/features/mcp/constants'; -import { checkAuthenticationRequiredOverride } from '@/features/chat/askAuth'; +import { isAuthRequiredOverrideEnabled } from '@/features/chat/askAuth'; import { withOptionalAuth } from '@/middleware/withAuth'; import { isServiceError } from '@/lib/utils'; -import { serviceErrorResponse, ServiceError } from '@/lib/serviceError'; +import { notAuthenticated, serviceErrorResponse, ServiceError } from '@/lib/serviceError'; import { ErrorCode } from '@/lib/errorCodes'; import { StatusCodes } from 'http-status-codes'; import { NextRequest } from 'next/server'; @@ -86,9 +86,8 @@ export const POST = apiHandler(async (request: NextRequest) => { const response = await sew(() => withOptionalAuth(async ({ user, principal }) => { - const authError = checkAuthenticationRequiredOverride(user); - if (authError) { - return authError; + if (isAuthRequiredOverrideEnabled && !user) { + return notAuthenticated(); } const ownerId = user?.id ?? null; const sessionId = request.headers.get(MCP_SESSION_ID_HEADER); @@ -153,9 +152,8 @@ export const DELETE = apiHandler(async (request: NextRequest) => { const result = await sew(() => withOptionalAuth(async ({ user }) => { - const authError = checkAuthenticationRequiredOverride(user); - if (authError) { - return authError; + if (isAuthRequiredOverrideEnabled && !user) { + return notAuthenticated(); } const ownerId = user?.id ?? null; const sessionId = request.headers.get(MCP_SESSION_ID_HEADER); diff --git a/packages/web/src/ee/features/mcp/askCodebase.ts b/packages/web/src/ee/features/mcp/askCodebase.ts index c0826055a..6a12213dc 100644 --- a/packages/web/src/ee/features/mcp/askCodebase.ts +++ b/packages/web/src/ee/features/mcp/askCodebase.ts @@ -4,11 +4,11 @@ import { generateChatNameFromMessage } from "@/ee/features/chat/llm.server"; import { getAISDKLanguageModelAndOptions } from "@/features/chat/llm.server"; import { resolveContextWindow } from "@/features/chat/modelContextWindow.server"; import { LanguageModelInfo, SBChatMessage, SearchScope } from "@/features/chat/types"; -import { checkAuthenticationRequiredOverride } from "@/features/chat/askAuth"; +import { isAuthRequiredOverrideEnabled } from "@/features/chat/askAuth"; import { convertLLMOutputToPortableMarkdown, getAnswerPartFromAssistantMessage, getLanguageModelKey } from "@/features/chat/utils"; import { resolveModelCapabilities } from "@/features/chat/modelCapabilities.server"; import { ErrorCode } from "@/lib/errorCodes"; -import { ServiceError, ServiceErrorException } from "@/lib/serviceError"; +import { notAuthenticated, ServiceError, ServiceErrorException } from "@/lib/serviceError"; import { withOptionalAuth } from "@/middleware/withAuth"; import { ChatVisibility, Prisma } from "@sourcebot/db"; import { createLogger, env } from "@sourcebot/shared"; @@ -50,9 +50,8 @@ const blockStreamUntilFinish = async => sew(() => withOptionalAuth(async ({ org, user, prisma }) => { - const authError = checkAuthenticationRequiredOverride(user); - if (authError) { - return authError; + if (isAuthRequiredOverrideEnabled && !user) { + return notAuthenticated(); } // Ask Sourcebot is a paid feature. askCodebase() is the single choke point diff --git a/packages/web/src/features/chat/askAuth.test.ts b/packages/web/src/features/chat/askAuth.test.ts index 42ace92f6..961da9bfa 100644 --- a/packages/web/src/features/chat/askAuth.test.ts +++ b/packages/web/src/features/chat/askAuth.test.ts @@ -10,30 +10,23 @@ vi.mock("@sourcebot/shared", () => ({ env: mocks.env, })); -const { checkAuthenticationRequiredOverride } = await import("./askAuth"); - -beforeEach(() => { - mocks.env.EXPERIMENT_ASK_GH_ENABLED = "false"; -}); - -describe("checkAuthenticationRequiredOverride", () => { - test("rejects anonymous Ask requests when Public SaaS is enabled", () => { - mocks.env.EXPERIMENT_ASK_GH_ENABLED = "true"; - - expect(checkAuthenticationRequiredOverride(undefined)).toEqual({ - statusCode: 401, - errorCode: "NOT_AUTHENTICATED", - message: "Not authenticated", - }); +describe("isAuthRequiredOverrideEnabled", () => { + beforeEach(() => { + vi.resetModules(); + mocks.env.EXPERIMENT_ASK_GH_ENABLED = "false"; }); - test("allows authenticated Ask requests when Public SaaS is enabled", () => { + test("is enabled for Public SaaS deployments", async () => { mocks.env.EXPERIMENT_ASK_GH_ENABLED = "true"; - expect(checkAuthenticationRequiredOverride({ id: "user-1" })).toBeNull(); + const { isAuthRequiredOverrideEnabled } = await import("./askAuth"); + + expect(isAuthRequiredOverrideEnabled).toBe(true); }); - test("allows anonymous Ask requests when Public SaaS is disabled", () => { - expect(checkAuthenticationRequiredOverride(undefined)).toBeNull(); + test("is disabled for self-hosted deployments", async () => { + const { isAuthRequiredOverrideEnabled } = await import("./askAuth"); + + expect(isAuthRequiredOverrideEnabled).toBe(false); }); }); diff --git a/packages/web/src/features/chat/askAuth.ts b/packages/web/src/features/chat/askAuth.ts index a147b3336..55552b06d 100644 --- a/packages/web/src/features/chat/askAuth.ts +++ b/packages/web/src/features/chat/askAuth.ts @@ -1,14 +1,7 @@ -import { notAuthenticated, type ServiceError } from "@/lib/serviceError"; import { env } from "@sourcebot/shared"; /** * Public SaaS requires an authenticated user for Ask requests. Self-hosted * deployments retain their existing anonymous-access behavior. */ -export const checkAuthenticationRequiredOverride = (user: object | undefined): ServiceError | null => { - if (env.EXPERIMENT_ASK_GH_ENABLED === "true" && !user) { - return notAuthenticated(); - } - - return null; -}; +export const isAuthRequiredOverrideEnabled = env.EXPERIMENT_ASK_GH_ENABLED === "true"; From 30c22687b2d223757864ad7065fc0b35d9d7f931 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 23 Sep 2026 03:28:47 +0000 Subject: [PATCH 5/5] refactor(web): inline Ask auth override checks Co-authored-by: Michael Sukkarieh --- .../web/src/app/api/(server)/ee/chat/route.ts | 3 +- .../web/src/app/api/(server)/ee/mcp/route.ts | 5 ++- .../web/src/ee/features/mcp/askCodebase.ts | 3 +- .../web/src/features/chat/askAuth.test.ts | 32 ------------------- packages/web/src/features/chat/askAuth.ts | 7 ---- 5 files changed, 4 insertions(+), 46 deletions(-) delete mode 100644 packages/web/src/features/chat/askAuth.test.ts delete mode 100644 packages/web/src/features/chat/askAuth.ts diff --git a/packages/web/src/app/api/(server)/ee/chat/route.ts b/packages/web/src/app/api/(server)/ee/chat/route.ts index b69c77274..58f85f1b8 100644 --- a/packages/web/src/app/api/(server)/ee/chat/route.ts +++ b/packages/web/src/app/api/(server)/ee/chat/route.ts @@ -12,7 +12,6 @@ import { getAISDKLanguageModelAndOptions } from "@/features/chat/llm.server"; import { resolveContextWindow } from "@/features/chat/modelContextWindow.server"; import { materializeCommandMessageTexts } from "@/ee/features/chat/skills/commandResolution"; import { getAskSkillAvailabilityAnalytics, getAskSkillTurnCompletedAnalytics } from "@/ee/features/chat/skills/skillAnalytics.server"; -import { isAuthRequiredOverrideEnabled } from "@/features/chat/askAuth"; import { apiHandler } from "@/lib/apiHandler"; import { ErrorCode } from "@/lib/errorCodes"; import { captureEvent } from "@/lib/posthog"; @@ -51,7 +50,7 @@ export const POST = apiHandler(async (req: NextRequest) => { const response = await sew(() => withOptionalAuth(async ({ org, user, prisma }) => { - if (isAuthRequiredOverrideEnabled && !user) { + if (env.EXPERIMENT_ASK_GH_ENABLED === 'true' && !user) { return notAuthenticated(); } diff --git a/packages/web/src/app/api/(server)/ee/mcp/route.ts b/packages/web/src/app/api/(server)/ee/mcp/route.ts index 298af202b..d5e57224d 100644 --- a/packages/web/src/app/api/(server)/ee/mcp/route.ts +++ b/packages/web/src/app/api/(server)/ee/mcp/route.ts @@ -2,7 +2,6 @@ import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { createMcpServer } from '@/ee/features/mcp/server'; import { MCP_PAID_PLAN_REQUIRED_MESSAGE } from '@/ee/features/mcp/constants'; -import { isAuthRequiredOverrideEnabled } from '@/features/chat/askAuth'; import { withOptionalAuth } from '@/middleware/withAuth'; import { isServiceError } from '@/lib/utils'; import { notAuthenticated, serviceErrorResponse, ServiceError } from '@/lib/serviceError'; @@ -86,7 +85,7 @@ export const POST = apiHandler(async (request: NextRequest) => { const response = await sew(() => withOptionalAuth(async ({ user, principal }) => { - if (isAuthRequiredOverrideEnabled && !user) { + if (env.EXPERIMENT_ASK_GH_ENABLED === 'true' && !user) { return notAuthenticated(); } const ownerId = user?.id ?? null; @@ -152,7 +151,7 @@ export const DELETE = apiHandler(async (request: NextRequest) => { const result = await sew(() => withOptionalAuth(async ({ user }) => { - if (isAuthRequiredOverrideEnabled && !user) { + if (env.EXPERIMENT_ASK_GH_ENABLED === 'true' && !user) { return notAuthenticated(); } const ownerId = user?.id ?? null; diff --git a/packages/web/src/ee/features/mcp/askCodebase.ts b/packages/web/src/ee/features/mcp/askCodebase.ts index 6a12213dc..40cf3cb62 100644 --- a/packages/web/src/ee/features/mcp/askCodebase.ts +++ b/packages/web/src/ee/features/mcp/askCodebase.ts @@ -4,7 +4,6 @@ import { generateChatNameFromMessage } from "@/ee/features/chat/llm.server"; import { getAISDKLanguageModelAndOptions } from "@/features/chat/llm.server"; import { resolveContextWindow } from "@/features/chat/modelContextWindow.server"; import { LanguageModelInfo, SBChatMessage, SearchScope } from "@/features/chat/types"; -import { isAuthRequiredOverrideEnabled } from "@/features/chat/askAuth"; import { convertLLMOutputToPortableMarkdown, getAnswerPartFromAssistantMessage, getLanguageModelKey } from "@/features/chat/utils"; import { resolveModelCapabilities } from "@/features/chat/modelCapabilities.server"; import { ErrorCode } from "@/lib/errorCodes"; @@ -50,7 +49,7 @@ const blockStreamUntilFinish = async => sew(() => withOptionalAuth(async ({ org, user, prisma }) => { - if (isAuthRequiredOverrideEnabled && !user) { + if (env.EXPERIMENT_ASK_GH_ENABLED === 'true' && !user) { return notAuthenticated(); } diff --git a/packages/web/src/features/chat/askAuth.test.ts b/packages/web/src/features/chat/askAuth.test.ts deleted file mode 100644 index 961da9bfa..000000000 --- a/packages/web/src/features/chat/askAuth.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { beforeEach, describe, expect, test, vi } from "vitest"; - -const mocks = vi.hoisted(() => ({ - env: { - EXPERIMENT_ASK_GH_ENABLED: "false", - }, -})); - -vi.mock("@sourcebot/shared", () => ({ - env: mocks.env, -})); - -describe("isAuthRequiredOverrideEnabled", () => { - beforeEach(() => { - vi.resetModules(); - mocks.env.EXPERIMENT_ASK_GH_ENABLED = "false"; - }); - - test("is enabled for Public SaaS deployments", async () => { - mocks.env.EXPERIMENT_ASK_GH_ENABLED = "true"; - - const { isAuthRequiredOverrideEnabled } = await import("./askAuth"); - - expect(isAuthRequiredOverrideEnabled).toBe(true); - }); - - test("is disabled for self-hosted deployments", async () => { - const { isAuthRequiredOverrideEnabled } = await import("./askAuth"); - - expect(isAuthRequiredOverrideEnabled).toBe(false); - }); -}); diff --git a/packages/web/src/features/chat/askAuth.ts b/packages/web/src/features/chat/askAuth.ts deleted file mode 100644 index 55552b06d..000000000 --- a/packages/web/src/features/chat/askAuth.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { env } from "@sourcebot/shared"; - -/** - * Public SaaS requires an authenticated user for Ask requests. Self-hosted - * deployments retain their existing anonymous-access behavior. - */ -export const isAuthRequiredOverrideEnabled = env.EXPERIMENT_ASK_GH_ENABLED === "true";