Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/web/eslint.config.mjs
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
import nextCoreWebVitals from 'eslint-config-next/core-web-vitals';
import tseslint from 'typescript-eslint';
import tanstackQuery from '@tanstack/eslint-plugin-query';
import authzLocal from './tools/eslint-plugin-local/index.mjs';

const config = [
...nextCoreWebVitals,
...tseslint.configs.recommended,
...tanstackQuery.configs['flat/recommended'],
{
plugins: {
authz: authzLocal,
},
rules: {
'authz/require-auth-wrapper': 'error',
},
},
{
rules: {
// New react-hooks v7 rules disabled as too strict for this codebase's existing patterns.
Expand Down
5 changes: 5 additions & 0 deletions packages/web/src/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -817,6 +817,7 @@ export const getOrgAccountRequests = async () => sew(() =>
}));
}));

// eslint-disable-next-line authz/require-auth-wrapper -- calls getAuthenticatedUser() directly; runs pre-org-membership so cannot use withAuth
export const createAccountRequest = async () => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand DownExpand Up@@ -920,6 +921,7 @@ export const createAccountRequest = async () => sew(async () => {
}
});

// eslint-disable-next-line authz/require-auth-wrapper -- public org-config bit consulted on login/signup screens before any session exists
export const getMemberApprovalRequired = async (): Promise<boolean | ServiceError> => sew(async () => {
const org = await __unsafePrisma.org.findUnique({
where: {
Expand DownExpand Up@@ -1181,6 +1183,7 @@ export const getRepoImage = async (repoId: number): Promise<ArrayBuffer | Servic
})
});

// eslint-disable-next-line authz/require-auth-wrapper -- public org-config bit consulted before authentication to decide whether to gate the UI
export const getAnonymousAccessStatus = async (): Promise<boolean | ServiceError> => sew(async () => {
const org = await __unsafePrisma.org.findUnique({
where: { id: SINGLE_TENANT_ORG_ID },
Expand DownExpand Up@@ -1244,6 +1247,7 @@ export const setAnonymousAccessStatus = async (enabled: boolean): Promise<Servic
});
});

// eslint-disable-next-line authz/require-auth-wrapper -- UI-only preference cookie, no DB access
export const setAgenticSearchTutorialDismissedCookie = async (dismissed: boolean) => sew(async () => {
const cookieStore = await cookies();
cookieStore.set(AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME, dismissed ? "true" : "false", {
Expand All@@ -1253,6 +1257,7 @@ export const setAgenticSearchTutorialDismissedCookie = async (dismissed: boolean
return true;
});

// eslint-disable-next-line authz/require-auth-wrapper -- UI-only preference cookie, no DB access
export const dismissMobileUnsupportedSplashScreen = async () => sew(async () => {
const cookieStore = await cookies();
cookieStore.set(MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, 'true');
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/[...slug]/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,4 +10,5 @@ const handler = () => {
});
}

// eslint-disable-next-line authz/require-auth-wrapper -- 404 catch-all for unknown API endpoints, returns no user data
export { handler as GET, handler as POST, handler as PUT, handler as PATCH, handler as DELETE }
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
import { handlers } from "@/auth";
// eslint-disable-next-line authz/require-auth-wrapper -- NextAuth's own auth-flow handlers, not user-data endpoints
export const { GET, POST } = handlers;
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/blame/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getFileBlame() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = Object.fromEntries(
Object.keys(fileBlameRequestSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/chat/blocking/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,7 @@ const blockingChatRequestSchema = z.object({
* The chat session is persisted to the database, allowing users to view the full
* conversation (including tool calls and reasoning) in the web UI.
*/
// eslint-disable-next-line authz/require-auth-wrapper -- delegates to askCodebase() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const requestBody = await request.json();
const parsed = await blockingChatRequestSchema.safeParseAsync(requestBody);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/commit/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getCommit() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(getCommitQueryParamsSchema.shape).map(key => [
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { serviceErrorResponse, queryParamsSchemaValidationError } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to listCommitAuthors() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(listCommitAuthorsQueryParamsSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/commits/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { serviceErrorResponse, queryParamsSchemaValidationError } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to listCommits() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(listCommitsQueryParamsSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/diff/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getDiff() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(getDiffRequestSchema.shape).map(key => [
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants

// RFC 8414: OAuth 2.0 Authorization Server Metadata
// @see: https://datatracker.ietf.org/doc/html/rfc8414
// eslint-disable-next-line authz/require-auth-wrapper -- RFC 8414 public metadata endpoint
export const GET = oauthApiHandler(async () => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,7 @@ const PROTECTED_RESOURCES = new Set([
'api/mcp'
]);

// eslint-disable-next-line authz/require-auth-wrapper -- RFC 9728 public metadata endpoint
export const GET = oauthApiHandler(async (_request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,7 @@ const queryParamsSchema = z.object({
jobId: z.string(),
});

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getAccountSyncStatus() which calls withAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = {
jobId: request.nextUrl.searchParams.get('jobId') ?? undefined,
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/ee/audit/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ const auditQueryParamsSchema = auditQueryParamsBaseSchema.refine(
{ message: "'since' must be before 'until'", path: ["since"] }
);

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to fetchAuditRecords() which calls withAuth + withMinimumOrgRole(OWNER)
export const GET = apiHandler(async (request: NextRequest) => {
const entitlements = getEntitlements();
if (!entitlements.includes('audit')) {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ const registerRequestSchema = z.object({
logo_uri: z.string().url().nullish(),
});

// eslint-disable-next-line authz/require-auth-wrapper -- RFC 7591 dynamic client registration, intentionally unauthenticated
export const POST = oauthApiHandler(async (request: NextRequest) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants
// RFC 7009: OAuth 2.0 Token Revocation
// Always returns 200 regardless of whether the token existed.
// @see: https://datatracker.ietf.org/doc/html/rfc7009
// eslint-disable-next-line authz/require-auth-wrapper -- RFC 7009 token revocation, no user session required
export const POST = oauthApiHandler(async (request: NextRequest) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/ee/oauth/token/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants
// OAuth 2.0 Token Endpoint
// Supports grant_type=authorization_code with PKCE (RFC 7636).
// @see: https://datatracker.ietf.org/doc/html/rfc6749#section-3.2
// eslint-disable-next-line authz/require-auth-wrapper -- OAuth token endpoint, authenticated via PKCE code / refresh token, not user session
export const POST = oauthApiHandler(async (request: NextRequest) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@ import { getPermissionSyncStatus } from "./api";
* Returns whether a user has a account that has it's permissions
* synced for the first time.
*/
// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getPermissionSyncStatus() which calls withAuth
export const GET = apiHandler(async () => {
const result = await getPermissionSyncStatus();

Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/files/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getFiles() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await getFilesRequestSchema.safeParseAsync(body);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to findSearchBasedSymbolDefinitions() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await findRelatedSymbolsRequestSchema.safeParseAsync(body);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to findSearchBasedSymbolReferences() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await findRelatedSymbolsRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/health/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { createLogger } from "@sourcebot/shared";

const logger = createLogger('health-check');

// eslint-disable-next-line authz/require-auth-wrapper -- public health check, no user data returned
export const GET = apiHandler(async () => {
logger.debug('health check');
return Response.json({ status: 'ok' });
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/mcp/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -133,6 +133,7 @@ export const DELETE = apiHandler(async (request: NextRequest) => {
// supported. Per the MCP Streamable HTTP spec, servers that do not offer a GET SSE
// stream MUST return 405 Method Not Allowed.
// @see: https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#listening-for-messages-from-the-server
// eslint-disable-next-line authz/require-auth-wrapper -- MCP spec mandates 405 for GET when SSE stream is unsupported; no user data
export const GET = apiHandler(async (_request: NextRequest) => {
return new Response(null, {
status: StatusCodes.METHOD_NOT_ALLOWED,
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/openapi.json/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ async function loadOpenApiDocument() {
return JSON.parse(await fs.readFile(openApiPath, 'utf8'));
}

// eslint-disable-next-line authz/require-auth-wrapper -- public OpenAPI spec, intentionally unauthenticated
export const GET = apiHandler(async () => {
const document = await loadOpenApiDocument();

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { serviceErrorResponse } from "@/lib/serviceError";
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getRepoInfo() which calls withOptionalAuth
export const GET = apiHandler(async (
_request: NextRequest,
{ params }: { params: Promise<{ repoId: string }> }
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/repos/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";
import { listRepos } from "./listReposApi";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to listRepos() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = Object.fromEntries(
Object.keys(listReposQueryParamsSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/search/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to search() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await searchRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/source/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getFileSource() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = Object.fromEntries(
Object.keys(fileSourceRequestSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/stream_search/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from '@/lib/se
import { isServiceError } from '@/lib/utils';
import { NextRequest } from 'next/server';

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to streamSearch() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await searchRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/tree/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getTree() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await getTreeRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/version/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ import { GetVersionResponse } from "@/lib/types";
// @see: https://nextjs.org/docs/14/app/building-your-application/routing/route-handlers#caching
export const dynamic = "force-dynamic";

// eslint-disable-next-line authz/require-auth-wrapper -- public Sourcebot version string, no user data
export const GET = apiHandler(async () => {
return Response.json({
version: SOURCEBOT_VERSION,
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/webhook/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,6 +130,7 @@ if (env.GITLAB_REVIEW_AGENT_TOKEN) {
}
}

// eslint-disable-next-line authz/require-auth-wrapper -- authenticated via GitHub App / GitLab webhook secrets, not user session
export const POST = async (request: NextRequest) => {
const body = await request.json();
const headers = Object.fromEntries(Array.from(request.headers.entries(), ([key, value]) => [key.toLowerCase(), value]));
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/minidenticon/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { apiHandler } from '@/lib/apiHandler';

// Generates a minidenticon avatar PNG from an email address.
// Used as a fallback avatar in emails where data URIs aren't supported.
// eslint-disable-next-line authz/require-auth-wrapper -- public identicon generator, no user data returned
export const GET = apiHandler(async (request: NextRequest) => {
const email = request.nextUrl.searchParams.get('email');
if (!email) {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/repos/[repoId]/image/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ import { apiHandler } from "@/lib/apiHandler";
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getRepoImage() action which calls withOptionalAuth
export const GET = apiHandler(async (
_request: NextRequest,
{ params }: { params: Promise<{ repoId: string }> }
Expand Down
3 changes: 3 additions & 0 deletions packages/web/src/app/invite/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ import { getAuditService } from "@/ee/features/audit/factory";

const auditService = getAuditService();

// eslint-disable-next-line authz/require-auth-wrapper -- runs pre-org-membership; uses getAuthenticatedUser() directly since withAuth requires a user-to-org link this call is establishing
export const joinOrganization = async (inviteLinkId?: string) => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand DownExpand Up@@ -71,6 +72,7 @@ export const joinOrganization = async (inviteLinkId?: string) => sew(async () =>
}
});

// eslint-disable-next-line authz/require-auth-wrapper -- runs pre-org-membership; uses getAuthenticatedUser() directly since withAuth requires a user-to-org link this call is establishing
export const redeemInvite = async (inviteId: string): Promise<{ success: boolean; } | ServiceError> => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand DownExpand Up@@ -161,6 +163,7 @@ export const redeemInvite = async (inviteId: string): Promise<{ success: boolean
});


// eslint-disable-next-line authz/require-auth-wrapper -- runs pre-org-membership; uses getAuthenticatedUser() directly since the invitee is not yet a member
export const getInviteInfo = async (inviteId: string) => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/ee/features/sso/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -100,6 +100,7 @@ export const unlinkLinkedAccountProvider = async (provider: string) => sew(() =>
)
);

// eslint-disable-next-line authz/require-auth-wrapper -- UI-only preference cookie, no DB access
export const skipOptionalProvidersLink = async () => sew(async () => {
const cookieStore = await cookies();
cookieStore.set(OPTIONAL_PROVIDERS_LINK_SKIPPED_COOKIE_NAME, 'true', {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/features/chat/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -550,6 +550,7 @@ export const submitFeedback = async ({
})
)

// eslint-disable-next-line authz/require-auth-wrapper -- returns identity provider metadata for the login wall, consulted before auth
export const getAskGhLoginWallData = async () => sew(async () => {
const isEnabled = env.EXPERIMENT_ASK_GH_ENABLED === 'true';
if (!isEnabled) {
Expand Down
12 changes: 12 additions & 0 deletions packages/web/tools/eslint-plugin-local/index.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import requireAuthWrapper from './rules/requireAuthWrapper.mjs';

const plugin = {
meta: {
name: 'eslint-plugin-authz-local',
},
rules: {
'require-auth-wrapper': requireAuthWrapper,
},
};

export default plugin;
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/web/eslint.config.mjs
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
import nextCoreWebVitals from 'eslint-config-next/core-web-vitals';
import tseslint from 'typescript-eslint';
import tanstackQuery from '@tanstack/eslint-plugin-query';
import authzLocal from './tools/eslint-plugin-local/index.mjs';

const config = [
...nextCoreWebVitals,
...tseslint.configs.recommended,
...tanstackQuery.configs['flat/recommended'],
{
plugins: {
authz: authzLocal,
},
rules: {
'authz/require-auth-wrapper': 'error',
},
},
{
rules: {
// New react-hooks v7 rules disabled as too strict for this codebase's existing patterns.
Expand Down
5 changes: 5 additions & 0 deletions packages/web/src/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -817,6 +817,7 @@ export const getOrgAccountRequests = async () => sew(() =>
}));
}));

// eslint-disable-next-line authz/require-auth-wrapper -- calls getAuthenticatedUser() directly; runs pre-org-membership so cannot use withAuth
export const createAccountRequest = async () => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand DownExpand Up@@ -920,6 +921,7 @@ export const createAccountRequest = async () => sew(async () => {
}
});

// eslint-disable-next-line authz/require-auth-wrapper -- public org-config bit consulted on login/signup screens before any session exists
export const getMemberApprovalRequired = async (): Promise<boolean | ServiceError> => sew(async () => {
const org = await __unsafePrisma.org.findUnique({
where: {
Expand DownExpand Up@@ -1181,6 +1183,7 @@ export const getRepoImage = async (repoId: number): Promise<ArrayBuffer | Servic
})
});

// eslint-disable-next-line authz/require-auth-wrapper -- public org-config bit consulted before authentication to decide whether to gate the UI
export const getAnonymousAccessStatus = async (): Promise<boolean | ServiceError> => sew(async () => {
const org = await __unsafePrisma.org.findUnique({
where: { id: SINGLE_TENANT_ORG_ID },
Expand DownExpand Up@@ -1244,6 +1247,7 @@ export const setAnonymousAccessStatus = async (enabled: boolean): Promise<Servic
});
});

// eslint-disable-next-line authz/require-auth-wrapper -- UI-only preference cookie, no DB access
export const setAgenticSearchTutorialDismissedCookie = async (dismissed: boolean) => sew(async () => {
const cookieStore = await cookies();
cookieStore.set(AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME, dismissed ? "true" : "false", {
Expand All@@ -1253,6 +1257,7 @@ export const setAgenticSearchTutorialDismissedCookie = async (dismissed: boolean
return true;
});

// eslint-disable-next-line authz/require-auth-wrapper -- UI-only preference cookie, no DB access
export const dismissMobileUnsupportedSplashScreen = async () => sew(async () => {
const cookieStore = await cookies();
cookieStore.set(MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, 'true');
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/[...slug]/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,4 +10,5 @@ const handler = () => {
});
}

// eslint-disable-next-line authz/require-auth-wrapper -- 404 catch-all for unknown API endpoints, returns no user data
export { handler as GET, handler as POST, handler as PUT, handler as PATCH, handler as DELETE }
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
import { handlers } from "@/auth";
// eslint-disable-next-line authz/require-auth-wrapper -- NextAuth's own auth-flow handlers, not user-data endpoints
export const { GET, POST } = handlers;
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/blame/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getFileBlame() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = Object.fromEntries(
Object.keys(fileBlameRequestSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/chat/blocking/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,7 @@ const blockingChatRequestSchema = z.object({
* The chat session is persisted to the database, allowing users to view the full
* conversation (including tool calls and reasoning) in the web UI.
*/
// eslint-disable-next-line authz/require-auth-wrapper -- delegates to askCodebase() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const requestBody = await request.json();
const parsed = await blockingChatRequestSchema.safeParseAsync(requestBody);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/commit/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getCommit() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(getCommitQueryParamsSchema.shape).map(key => [
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { serviceErrorResponse, queryParamsSchemaValidationError } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to listCommitAuthors() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(listCommitAuthorsQueryParamsSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/commits/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { serviceErrorResponse, queryParamsSchemaValidationError } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to listCommits() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(listCommitsQueryParamsSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/diff/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getDiff() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(getDiffRequestSchema.shape).map(key => [
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants

// RFC 8414: OAuth 2.0 Authorization Server Metadata
// @see: https://datatracker.ietf.org/doc/html/rfc8414
// eslint-disable-next-line authz/require-auth-wrapper -- RFC 8414 public metadata endpoint
export const GET = oauthApiHandler(async () => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,7 @@ const PROTECTED_RESOURCES = new Set([
'api/mcp'
]);

// eslint-disable-next-line authz/require-auth-wrapper -- RFC 9728 public metadata endpoint
export const GET = oauthApiHandler(async (_request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,7 @@ const queryParamsSchema = z.object({
jobId: z.string(),
});

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getAccountSyncStatus() which calls withAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = {
jobId: request.nextUrl.searchParams.get('jobId') ?? undefined,
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/ee/audit/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ const auditQueryParamsSchema = auditQueryParamsBaseSchema.refine(
{ message: "'since' must be before 'until'", path: ["since"] }
);

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to fetchAuditRecords() which calls withAuth + withMinimumOrgRole(OWNER)
export const GET = apiHandler(async (request: NextRequest) => {
const entitlements = getEntitlements();
if (!entitlements.includes('audit')) {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ const registerRequestSchema = z.object({
logo_uri: z.string().url().nullish(),
});

// eslint-disable-next-line authz/require-auth-wrapper -- RFC 7591 dynamic client registration, intentionally unauthenticated
export const POST = oauthApiHandler(async (request: NextRequest) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants
// RFC 7009: OAuth 2.0 Token Revocation
// Always returns 200 regardless of whether the token existed.
// @see: https://datatracker.ietf.org/doc/html/rfc7009
// eslint-disable-next-line authz/require-auth-wrapper -- RFC 7009 token revocation, no user session required
export const POST = oauthApiHandler(async (request: NextRequest) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/ee/oauth/token/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants
// OAuth 2.0 Token Endpoint
// Supports grant_type=authorization_code with PKCE (RFC 7636).
// @see: https://datatracker.ietf.org/doc/html/rfc6749#section-3.2
// eslint-disable-next-line authz/require-auth-wrapper -- OAuth token endpoint, authenticated via PKCE code / refresh token, not user session
export const POST = oauthApiHandler(async (request: NextRequest) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@ import { getPermissionSyncStatus } from "./api";
* Returns whether a user has a account that has it's permissions
* synced for the first time.
*/
// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getPermissionSyncStatus() which calls withAuth
export const GET = apiHandler(async () => {
const result = await getPermissionSyncStatus();

Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/files/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getFiles() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await getFilesRequestSchema.safeParseAsync(body);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to findSearchBasedSymbolDefinitions() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await findRelatedSymbolsRequestSchema.safeParseAsync(body);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to findSearchBasedSymbolReferences() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await findRelatedSymbolsRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/health/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { createLogger } from "@sourcebot/shared";

const logger = createLogger('health-check');

// eslint-disable-next-line authz/require-auth-wrapper -- public health check, no user data returned
export const GET = apiHandler(async () => {
logger.debug('health check');
return Response.json({ status: 'ok' });
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/mcp/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -133,6 +133,7 @@ export const DELETE = apiHandler(async (request: NextRequest) => {
// supported. Per the MCP Streamable HTTP spec, servers that do not offer a GET SSE
// stream MUST return 405 Method Not Allowed.
// @see: https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#listening-for-messages-from-the-server
// eslint-disable-next-line authz/require-auth-wrapper -- MCP spec mandates 405 for GET when SSE stream is unsupported; no user data
export const GET = apiHandler(async (_request: NextRequest) => {
return new Response(null, {
status: StatusCodes.METHOD_NOT_ALLOWED,
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/openapi.json/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ async function loadOpenApiDocument() {
return JSON.parse(await fs.readFile(openApiPath, 'utf8'));
}

// eslint-disable-next-line authz/require-auth-wrapper -- public OpenAPI spec, intentionally unauthenticated
export const GET = apiHandler(async () => {
const document = await loadOpenApiDocument();

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { serviceErrorResponse } from "@/lib/serviceError";
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getRepoInfo() which calls withOptionalAuth
export const GET = apiHandler(async (
_request: NextRequest,
{ params }: { params: Promise<{ repoId: string }> }
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/repos/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";
import { listRepos } from "./listReposApi";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to listRepos() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = Object.fromEntries(
Object.keys(listReposQueryParamsSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/search/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to search() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await searchRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/source/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getFileSource() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = Object.fromEntries(
Object.keys(fileSourceRequestSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/stream_search/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from '@/lib/se
import { isServiceError } from '@/lib/utils';
import { NextRequest } from 'next/server';

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to streamSearch() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await searchRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/tree/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getTree() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await getTreeRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/version/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ import { GetVersionResponse } from "@/lib/types";
// @see: https://nextjs.org/docs/14/app/building-your-application/routing/route-handlers#caching
export const dynamic = "force-dynamic";

// eslint-disable-next-line authz/require-auth-wrapper -- public Sourcebot version string, no user data
export const GET = apiHandler(async () => {
return Response.json({
version: SOURCEBOT_VERSION,
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/webhook/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,6 +130,7 @@ if (env.GITLAB_REVIEW_AGENT_TOKEN) {
}
}

// eslint-disable-next-line authz/require-auth-wrapper -- authenticated via GitHub App / GitLab webhook secrets, not user session
export const POST = async (request: NextRequest) => {
const body = await request.json();
const headers = Object.fromEntries(Array.from(request.headers.entries(), ([key, value]) => [key.toLowerCase(), value]));
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/minidenticon/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { apiHandler } from '@/lib/apiHandler';

// Generates a minidenticon avatar PNG from an email address.
// Used as a fallback avatar in emails where data URIs aren't supported.
// eslint-disable-next-line authz/require-auth-wrapper -- public identicon generator, no user data returned
export const GET = apiHandler(async (request: NextRequest) => {
const email = request.nextUrl.searchParams.get('email');
if (!email) {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/repos/[repoId]/image/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ import { apiHandler } from "@/lib/apiHandler";
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getRepoImage() action which calls withOptionalAuth
export const GET = apiHandler(async (
_request: NextRequest,
{ params }: { params: Promise<{ repoId: string }> }
Expand Down
3 changes: 3 additions & 0 deletions packages/web/src/app/invite/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ import { getAuditService } from "@/ee/features/audit/factory";

const auditService = getAuditService();

// eslint-disable-next-line authz/require-auth-wrapper -- runs pre-org-membership; uses getAuthenticatedUser() directly since withAuth requires a user-to-org link this call is establishing
export const joinOrganization = async (inviteLinkId?: string) => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand DownExpand Up@@ -71,6 +72,7 @@ export const joinOrganization = async (inviteLinkId?: string) => sew(async () =>
}
});

// eslint-disable-next-line authz/require-auth-wrapper -- runs pre-org-membership; uses getAuthenticatedUser() directly since withAuth requires a user-to-org link this call is establishing
export const redeemInvite = async (inviteId: string): Promise<{ success: boolean; } | ServiceError> => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand DownExpand Up@@ -161,6 +163,7 @@ export const redeemInvite = async (inviteId: string): Promise<{ success: boolean
});


// eslint-disable-next-line authz/require-auth-wrapper -- runs pre-org-membership; uses getAuthenticatedUser() directly since the invitee is not yet a member
export const getInviteInfo = async (inviteId: string) => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/ee/features/sso/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -100,6 +100,7 @@ export const unlinkLinkedAccountProvider = async (provider: string) => sew(() =>
)
);

// eslint-disable-next-line authz/require-auth-wrapper -- UI-only preference cookie, no DB access
export const skipOptionalProvidersLink = async () => sew(async () => {
const cookieStore = await cookies();
cookieStore.set(OPTIONAL_PROVIDERS_LINK_SKIPPED_COOKIE_NAME, 'true', {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/features/chat/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -550,6 +550,7 @@ export const submitFeedback = async ({
})
)

// eslint-disable-next-line authz/require-auth-wrapper -- returns identity provider metadata for the login wall, consulted before auth
export const getAskGhLoginWallData = async () => sew(async () => {
const isEnabled = env.EXPERIMENT_ASK_GH_ENABLED === 'true';
if (!isEnabled) {
Expand Down
12 changes: 12 additions & 0 deletions packages/web/tools/eslint-plugin-local/index.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import requireAuthWrapper from './rules/requireAuthWrapper.mjs';

const plugin = {
meta: {
name: 'eslint-plugin-authz-local',
},
rules: {
'require-auth-wrapper': requireAuthWrapper,
},
};

export default plugin;
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/web/eslint.config.mjs
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
import nextCoreWebVitals from 'eslint-config-next/core-web-vitals';
import tseslint from 'typescript-eslint';
import tanstackQuery from '@tanstack/eslint-plugin-query';
import authzLocal from './tools/eslint-plugin-local/index.mjs';

const config = [
...nextCoreWebVitals,
...tseslint.configs.recommended,
...tanstackQuery.configs['flat/recommended'],
{
plugins: {
authz: authzLocal,
},
rules: {
'authz/require-auth-wrapper': 'error',
},
},
{
rules: {
// New react-hooks v7 rules disabled as too strict for this codebase's existing patterns.
Expand Down
5 changes: 5 additions & 0 deletions packages/web/src/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -817,6 +817,7 @@ export const getOrgAccountRequests = async () => sew(() =>
}));
}));

// eslint-disable-next-line authz/require-auth-wrapper -- calls getAuthenticatedUser() directly; runs pre-org-membership so cannot use withAuth
export const createAccountRequest = async () => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand DownExpand Up@@ -920,6 +921,7 @@ export const createAccountRequest = async () => sew(async () => {
}
});

// eslint-disable-next-line authz/require-auth-wrapper -- public org-config bit consulted on login/signup screens before any session exists
export const getMemberApprovalRequired = async (): Promise<boolean | ServiceError> => sew(async () => {
const org = await __unsafePrisma.org.findUnique({
where: {
Expand DownExpand Up@@ -1181,6 +1183,7 @@ export const getRepoImage = async (repoId: number): Promise<ArrayBuffer | Servic
})
});

// eslint-disable-next-line authz/require-auth-wrapper -- public org-config bit consulted before authentication to decide whether to gate the UI
export const getAnonymousAccessStatus = async (): Promise<boolean | ServiceError> => sew(async () => {
const org = await __unsafePrisma.org.findUnique({
where: { id: SINGLE_TENANT_ORG_ID },
Expand DownExpand Up@@ -1244,6 +1247,7 @@ export const setAnonymousAccessStatus = async (enabled: boolean): Promise<Servic
});
});

// eslint-disable-next-line authz/require-auth-wrapper -- UI-only preference cookie, no DB access
export const setAgenticSearchTutorialDismissedCookie = async (dismissed: boolean) => sew(async () => {
const cookieStore = await cookies();
cookieStore.set(AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME, dismissed ? "true" : "false", {
Expand All@@ -1253,6 +1257,7 @@ export const setAgenticSearchTutorialDismissedCookie = async (dismissed: boolean
return true;
});

// eslint-disable-next-line authz/require-auth-wrapper -- UI-only preference cookie, no DB access
export const dismissMobileUnsupportedSplashScreen = async () => sew(async () => {
const cookieStore = await cookies();
cookieStore.set(MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, 'true');
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/[...slug]/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,4 +10,5 @@ const handler = () => {
});
}

// eslint-disable-next-line authz/require-auth-wrapper -- 404 catch-all for unknown API endpoints, returns no user data
export { handler as GET, handler as POST, handler as PUT, handler as PATCH, handler as DELETE }
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
import { handlers } from "@/auth";
// eslint-disable-next-line authz/require-auth-wrapper -- NextAuth's own auth-flow handlers, not user-data endpoints
export const { GET, POST } = handlers;
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/blame/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getFileBlame() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = Object.fromEntries(
Object.keys(fileBlameRequestSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/chat/blocking/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,7 @@ const blockingChatRequestSchema = z.object({
* The chat session is persisted to the database, allowing users to view the full
* conversation (including tool calls and reasoning) in the web UI.
*/
// eslint-disable-next-line authz/require-auth-wrapper -- delegates to askCodebase() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const requestBody = await request.json();
const parsed = await blockingChatRequestSchema.safeParseAsync(requestBody);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/commit/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getCommit() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(getCommitQueryParamsSchema.shape).map(key => [
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { serviceErrorResponse, queryParamsSchemaValidationError } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to listCommitAuthors() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(listCommitAuthorsQueryParamsSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/commits/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { serviceErrorResponse, queryParamsSchemaValidationError } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to listCommits() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(listCommitsQueryParamsSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/diff/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getDiff() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(getDiffRequestSchema.shape).map(key => [
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants

// RFC 8414: OAuth 2.0 Authorization Server Metadata
// @see: https://datatracker.ietf.org/doc/html/rfc8414
// eslint-disable-next-line authz/require-auth-wrapper -- RFC 8414 public metadata endpoint
export const GET = oauthApiHandler(async () => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,7 @@ const PROTECTED_RESOURCES = new Set([
'api/mcp'
]);

// eslint-disable-next-line authz/require-auth-wrapper -- RFC 9728 public metadata endpoint
export const GET = oauthApiHandler(async (_request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,7 @@ const queryParamsSchema = z.object({
jobId: z.string(),
});

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getAccountSyncStatus() which calls withAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = {
jobId: request.nextUrl.searchParams.get('jobId') ?? undefined,
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/ee/audit/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ const auditQueryParamsSchema = auditQueryParamsBaseSchema.refine(
{ message: "'since' must be before 'until'", path: ["since"] }
);

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to fetchAuditRecords() which calls withAuth + withMinimumOrgRole(OWNER)
export const GET = apiHandler(async (request: NextRequest) => {
const entitlements = getEntitlements();
if (!entitlements.includes('audit')) {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ const registerRequestSchema = z.object({
logo_uri: z.string().url().nullish(),
});

// eslint-disable-next-line authz/require-auth-wrapper -- RFC 7591 dynamic client registration, intentionally unauthenticated
export const POST = oauthApiHandler(async (request: NextRequest) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants
// RFC 7009: OAuth 2.0 Token Revocation
// Always returns 200 regardless of whether the token existed.
// @see: https://datatracker.ietf.org/doc/html/rfc7009
// eslint-disable-next-line authz/require-auth-wrapper -- RFC 7009 token revocation, no user session required
export const POST = oauthApiHandler(async (request: NextRequest) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/ee/oauth/token/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants
// OAuth 2.0 Token Endpoint
// Supports grant_type=authorization_code with PKCE (RFC 7636).
// @see: https://datatracker.ietf.org/doc/html/rfc6749#section-3.2
// eslint-disable-next-line authz/require-auth-wrapper -- OAuth token endpoint, authenticated via PKCE code / refresh token, not user session
export const POST = oauthApiHandler(async (request: NextRequest) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@ import { getPermissionSyncStatus } from "./api";
* Returns whether a user has a account that has it's permissions
* synced for the first time.
*/
// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getPermissionSyncStatus() which calls withAuth
export const GET = apiHandler(async () => {
const result = await getPermissionSyncStatus();

Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/files/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getFiles() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await getFilesRequestSchema.safeParseAsync(body);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to findSearchBasedSymbolDefinitions() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await findRelatedSymbolsRequestSchema.safeParseAsync(body);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to findSearchBasedSymbolReferences() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await findRelatedSymbolsRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/health/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { createLogger } from "@sourcebot/shared";

const logger = createLogger('health-check');

// eslint-disable-next-line authz/require-auth-wrapper -- public health check, no user data returned
export const GET = apiHandler(async () => {
logger.debug('health check');
return Response.json({ status: 'ok' });
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/mcp/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -133,6 +133,7 @@ export const DELETE = apiHandler(async (request: NextRequest) => {
// supported. Per the MCP Streamable HTTP spec, servers that do not offer a GET SSE
// stream MUST return 405 Method Not Allowed.
// @see: https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#listening-for-messages-from-the-server
// eslint-disable-next-line authz/require-auth-wrapper -- MCP spec mandates 405 for GET when SSE stream is unsupported; no user data
export const GET = apiHandler(async (_request: NextRequest) => {
return new Response(null, {
status: StatusCodes.METHOD_NOT_ALLOWED,
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/openapi.json/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ async function loadOpenApiDocument() {
return JSON.parse(await fs.readFile(openApiPath, 'utf8'));
}

// eslint-disable-next-line authz/require-auth-wrapper -- public OpenAPI spec, intentionally unauthenticated
export const GET = apiHandler(async () => {
const document = await loadOpenApiDocument();

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { serviceErrorResponse } from "@/lib/serviceError";
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getRepoInfo() which calls withOptionalAuth
export const GET = apiHandler(async (
_request: NextRequest,
{ params }: { params: Promise<{ repoId: string }> }
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/repos/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";
import { listRepos } from "./listReposApi";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to listRepos() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = Object.fromEntries(
Object.keys(listReposQueryParamsSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/search/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to search() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await searchRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/source/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getFileSource() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = Object.fromEntries(
Object.keys(fileSourceRequestSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/stream_search/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from '@/lib/se
import { isServiceError } from '@/lib/utils';
import { NextRequest } from 'next/server';

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to streamSearch() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await searchRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/tree/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getTree() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await getTreeRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/version/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ import { GetVersionResponse } from "@/lib/types";
// @see: https://nextjs.org/docs/14/app/building-your-application/routing/route-handlers#caching
export const dynamic = "force-dynamic";

// eslint-disable-next-line authz/require-auth-wrapper -- public Sourcebot version string, no user data
export const GET = apiHandler(async () => {
return Response.json({
version: SOURCEBOT_VERSION,
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/webhook/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,6 +130,7 @@ if (env.GITLAB_REVIEW_AGENT_TOKEN) {
}
}

// eslint-disable-next-line authz/require-auth-wrapper -- authenticated via GitHub App / GitLab webhook secrets, not user session
export const POST = async (request: NextRequest) => {
const body = await request.json();
const headers = Object.fromEntries(Array.from(request.headers.entries(), ([key, value]) => [key.toLowerCase(), value]));
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/minidenticon/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { apiHandler } from '@/lib/apiHandler';

// Generates a minidenticon avatar PNG from an email address.
// Used as a fallback avatar in emails where data URIs aren't supported.
// eslint-disable-next-line authz/require-auth-wrapper -- public identicon generator, no user data returned
export const GET = apiHandler(async (request: NextRequest) => {
const email = request.nextUrl.searchParams.get('email');
if (!email) {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/repos/[repoId]/image/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ import { apiHandler } from "@/lib/apiHandler";
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getRepoImage() action which calls withOptionalAuth
export const GET = apiHandler(async (
_request: NextRequest,
{ params }: { params: Promise<{ repoId: string }> }
Expand Down
3 changes: 3 additions & 0 deletions packages/web/src/app/invite/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ import { getAuditService } from "@/ee/features/audit/factory";

const auditService = getAuditService();

// eslint-disable-next-line authz/require-auth-wrapper -- runs pre-org-membership; uses getAuthenticatedUser() directly since withAuth requires a user-to-org link this call is establishing
export const joinOrganization = async (inviteLinkId?: string) => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand DownExpand Up@@ -71,6 +72,7 @@ export const joinOrganization = async (inviteLinkId?: string) => sew(async () =>
}
});

// eslint-disable-next-line authz/require-auth-wrapper -- runs pre-org-membership; uses getAuthenticatedUser() directly since withAuth requires a user-to-org link this call is establishing
export const redeemInvite = async (inviteId: string): Promise<{ success: boolean; } | ServiceError> => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand DownExpand Up@@ -161,6 +163,7 @@ export const redeemInvite = async (inviteId: string): Promise<{ success: boolean
});


// eslint-disable-next-line authz/require-auth-wrapper -- runs pre-org-membership; uses getAuthenticatedUser() directly since the invitee is not yet a member
export const getInviteInfo = async (inviteId: string) => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/ee/features/sso/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -100,6 +100,7 @@ export const unlinkLinkedAccountProvider = async (provider: string) => sew(() =>
)
);

// eslint-disable-next-line authz/require-auth-wrapper -- UI-only preference cookie, no DB access
export const skipOptionalProvidersLink = async () => sew(async () => {
const cookieStore = await cookies();
cookieStore.set(OPTIONAL_PROVIDERS_LINK_SKIPPED_COOKIE_NAME, 'true', {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/features/chat/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -550,6 +550,7 @@ export const submitFeedback = async ({
})
)

// eslint-disable-next-line authz/require-auth-wrapper -- returns identity provider metadata for the login wall, consulted before auth
export const getAskGhLoginWallData = async () => sew(async () => {
const isEnabled = env.EXPERIMENT_ASK_GH_ENABLED === 'true';
if (!isEnabled) {
Expand Down
12 changes: 12 additions & 0 deletions packages/web/tools/eslint-plugin-local/index.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import requireAuthWrapper from './rules/requireAuthWrapper.mjs';

const plugin = {
meta: {
name: 'eslint-plugin-authz-local',
},
rules: {
'require-auth-wrapper': requireAuthWrapper,
},
};

export default plugin;
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/web/eslint.config.mjs
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
import nextCoreWebVitals from 'eslint-config-next/core-web-vitals';
import tseslint from 'typescript-eslint';
import tanstackQuery from '@tanstack/eslint-plugin-query';
import authzLocal from './tools/eslint-plugin-local/index.mjs';

const config = [
...nextCoreWebVitals,
...tseslint.configs.recommended,
...tanstackQuery.configs['flat/recommended'],
{
plugins: {
authz: authzLocal,
},
rules: {
'authz/require-auth-wrapper': 'error',
},
},
{
rules: {
// New react-hooks v7 rules disabled as too strict for this codebase's existing patterns.
Expand Down
5 changes: 5 additions & 0 deletions packages/web/src/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -817,6 +817,7 @@ export const getOrgAccountRequests = async () => sew(() =>
}));
}));

// eslint-disable-next-line authz/require-auth-wrapper -- calls getAuthenticatedUser() directly; runs pre-org-membership so cannot use withAuth
export const createAccountRequest = async () => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand DownExpand Up@@ -920,6 +921,7 @@ export const createAccountRequest = async () => sew(async () => {
}
});

// eslint-disable-next-line authz/require-auth-wrapper -- public org-config bit consulted on login/signup screens before any session exists
export const getMemberApprovalRequired = async (): Promise<boolean | ServiceError> => sew(async () => {
const org = await __unsafePrisma.org.findUnique({
where: {
Expand DownExpand Up@@ -1181,6 +1183,7 @@ export const getRepoImage = async (repoId: number): Promise<ArrayBuffer | Servic
})
});

// eslint-disable-next-line authz/require-auth-wrapper -- public org-config bit consulted before authentication to decide whether to gate the UI
export const getAnonymousAccessStatus = async (): Promise<boolean | ServiceError> => sew(async () => {
const org = await __unsafePrisma.org.findUnique({
where: { id: SINGLE_TENANT_ORG_ID },
Expand DownExpand Up@@ -1244,6 +1247,7 @@ export const setAnonymousAccessStatus = async (enabled: boolean): Promise<Servic
});
});

// eslint-disable-next-line authz/require-auth-wrapper -- UI-only preference cookie, no DB access
export const setAgenticSearchTutorialDismissedCookie = async (dismissed: boolean) => sew(async () => {
const cookieStore = await cookies();
cookieStore.set(AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME, dismissed ? "true" : "false", {
Expand All@@ -1253,6 +1257,7 @@ export const setAgenticSearchTutorialDismissedCookie = async (dismissed: boolean
return true;
});

// eslint-disable-next-line authz/require-auth-wrapper -- UI-only preference cookie, no DB access
export const dismissMobileUnsupportedSplashScreen = async () => sew(async () => {
const cookieStore = await cookies();
cookieStore.set(MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, 'true');
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/[...slug]/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,4 +10,5 @@ const handler = () => {
});
}

// eslint-disable-next-line authz/require-auth-wrapper -- 404 catch-all for unknown API endpoints, returns no user data
export { handler as GET, handler as POST, handler as PUT, handler as PATCH, handler as DELETE }
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
import { handlers } from "@/auth";
// eslint-disable-next-line authz/require-auth-wrapper -- NextAuth's own auth-flow handlers, not user-data endpoints
export const { GET, POST } = handlers;
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/blame/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getFileBlame() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = Object.fromEntries(
Object.keys(fileBlameRequestSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/chat/blocking/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,7 @@ const blockingChatRequestSchema = z.object({
* The chat session is persisted to the database, allowing users to view the full
* conversation (including tool calls and reasoning) in the web UI.
*/
// eslint-disable-next-line authz/require-auth-wrapper -- delegates to askCodebase() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const requestBody = await request.json();
const parsed = await blockingChatRequestSchema.safeParseAsync(requestBody);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/commit/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getCommit() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(getCommitQueryParamsSchema.shape).map(key => [
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { serviceErrorResponse, queryParamsSchemaValidationError } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to listCommitAuthors() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(listCommitAuthorsQueryParamsSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/commits/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { serviceErrorResponse, queryParamsSchemaValidationError } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to listCommits() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(listCommitsQueryParamsSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/diff/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getDiff() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(getDiffRequestSchema.shape).map(key => [
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants

// RFC 8414: OAuth 2.0 Authorization Server Metadata
// @see: https://datatracker.ietf.org/doc/html/rfc8414
// eslint-disable-next-line authz/require-auth-wrapper -- RFC 8414 public metadata endpoint
export const GET = oauthApiHandler(async () => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,7 @@ const PROTECTED_RESOURCES = new Set([
'api/mcp'
]);

// eslint-disable-next-line authz/require-auth-wrapper -- RFC 9728 public metadata endpoint
export const GET = oauthApiHandler(async (_request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,7 @@ const queryParamsSchema = z.object({
jobId: z.string(),
});

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getAccountSyncStatus() which calls withAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = {
jobId: request.nextUrl.searchParams.get('jobId') ?? undefined,
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/ee/audit/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ const auditQueryParamsSchema = auditQueryParamsBaseSchema.refine(
{ message: "'since' must be before 'until'", path: ["since"] }
);

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to fetchAuditRecords() which calls withAuth + withMinimumOrgRole(OWNER)
export const GET = apiHandler(async (request: NextRequest) => {
const entitlements = getEntitlements();
if (!entitlements.includes('audit')) {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ const registerRequestSchema = z.object({
logo_uri: z.string().url().nullish(),
});

// eslint-disable-next-line authz/require-auth-wrapper -- RFC 7591 dynamic client registration, intentionally unauthenticated
export const POST = oauthApiHandler(async (request: NextRequest) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants
// RFC 7009: OAuth 2.0 Token Revocation
// Always returns 200 regardless of whether the token existed.
// @see: https://datatracker.ietf.org/doc/html/rfc7009
// eslint-disable-next-line authz/require-auth-wrapper -- RFC 7009 token revocation, no user session required
export const POST = oauthApiHandler(async (request: NextRequest) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/ee/oauth/token/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants
// OAuth 2.0 Token Endpoint
// Supports grant_type=authorization_code with PKCE (RFC 7636).
// @see: https://datatracker.ietf.org/doc/html/rfc6749#section-3.2
// eslint-disable-next-line authz/require-auth-wrapper -- OAuth token endpoint, authenticated via PKCE code / refresh token, not user session
export const POST = oauthApiHandler(async (request: NextRequest) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@ import { getPermissionSyncStatus } from "./api";
* Returns whether a user has a account that has it's permissions
* synced for the first time.
*/
// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getPermissionSyncStatus() which calls withAuth
export const GET = apiHandler(async () => {
const result = await getPermissionSyncStatus();

Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/files/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getFiles() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await getFilesRequestSchema.safeParseAsync(body);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to findSearchBasedSymbolDefinitions() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await findRelatedSymbolsRequestSchema.safeParseAsync(body);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to findSearchBasedSymbolReferences() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await findRelatedSymbolsRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/health/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { createLogger } from "@sourcebot/shared";

const logger = createLogger('health-check');

// eslint-disable-next-line authz/require-auth-wrapper -- public health check, no user data returned
export const GET = apiHandler(async () => {
logger.debug('health check');
return Response.json({ status: 'ok' });
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/mcp/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -133,6 +133,7 @@ export const DELETE = apiHandler(async (request: NextRequest) => {
// supported. Per the MCP Streamable HTTP spec, servers that do not offer a GET SSE
// stream MUST return 405 Method Not Allowed.
// @see: https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#listening-for-messages-from-the-server
// eslint-disable-next-line authz/require-auth-wrapper -- MCP spec mandates 405 for GET when SSE stream is unsupported; no user data
export const GET = apiHandler(async (_request: NextRequest) => {
return new Response(null, {
status: StatusCodes.METHOD_NOT_ALLOWED,
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/openapi.json/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ async function loadOpenApiDocument() {
return JSON.parse(await fs.readFile(openApiPath, 'utf8'));
}

// eslint-disable-next-line authz/require-auth-wrapper -- public OpenAPI spec, intentionally unauthenticated
export const GET = apiHandler(async () => {
const document = await loadOpenApiDocument();

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { serviceErrorResponse } from "@/lib/serviceError";
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getRepoInfo() which calls withOptionalAuth
export const GET = apiHandler(async (
_request: NextRequest,
{ params }: { params: Promise<{ repoId: string }> }
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/repos/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";
import { listRepos } from "./listReposApi";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to listRepos() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = Object.fromEntries(
Object.keys(listReposQueryParamsSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/search/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to search() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await searchRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/source/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getFileSource() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = Object.fromEntries(
Object.keys(fileSourceRequestSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/stream_search/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from '@/lib/se
import { isServiceError } from '@/lib/utils';
import { NextRequest } from 'next/server';

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to streamSearch() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await searchRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/tree/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getTree() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await getTreeRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/version/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ import { GetVersionResponse } from "@/lib/types";
// @see: https://nextjs.org/docs/14/app/building-your-application/routing/route-handlers#caching
export const dynamic = "force-dynamic";

// eslint-disable-next-line authz/require-auth-wrapper -- public Sourcebot version string, no user data
export const GET = apiHandler(async () => {
return Response.json({
version: SOURCEBOT_VERSION,
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/webhook/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,6 +130,7 @@ if (env.GITLAB_REVIEW_AGENT_TOKEN) {
}
}

// eslint-disable-next-line authz/require-auth-wrapper -- authenticated via GitHub App / GitLab webhook secrets, not user session
export const POST = async (request: NextRequest) => {
const body = await request.json();
const headers = Object.fromEntries(Array.from(request.headers.entries(), ([key, value]) => [key.toLowerCase(), value]));
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/minidenticon/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { apiHandler } from '@/lib/apiHandler';

// Generates a minidenticon avatar PNG from an email address.
// Used as a fallback avatar in emails where data URIs aren't supported.
// eslint-disable-next-line authz/require-auth-wrapper -- public identicon generator, no user data returned
export const GET = apiHandler(async (request: NextRequest) => {
const email = request.nextUrl.searchParams.get('email');
if (!email) {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/repos/[repoId]/image/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ import { apiHandler } from "@/lib/apiHandler";
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getRepoImage() action which calls withOptionalAuth
export const GET = apiHandler(async (
_request: NextRequest,
{ params }: { params: Promise<{ repoId: string }> }
Expand Down
3 changes: 3 additions & 0 deletions packages/web/src/app/invite/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ import { getAuditService } from "@/ee/features/audit/factory";

const auditService = getAuditService();

// eslint-disable-next-line authz/require-auth-wrapper -- runs pre-org-membership; uses getAuthenticatedUser() directly since withAuth requires a user-to-org link this call is establishing
export const joinOrganization = async (inviteLinkId?: string) => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand DownExpand Up@@ -71,6 +72,7 @@ export const joinOrganization = async (inviteLinkId?: string) => sew(async () =>
}
});

// eslint-disable-next-line authz/require-auth-wrapper -- runs pre-org-membership; uses getAuthenticatedUser() directly since withAuth requires a user-to-org link this call is establishing
export const redeemInvite = async (inviteId: string): Promise<{ success: boolean; } | ServiceError> => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand DownExpand Up@@ -161,6 +163,7 @@ export const redeemInvite = async (inviteId: string): Promise<{ success: boolean
});


// eslint-disable-next-line authz/require-auth-wrapper -- runs pre-org-membership; uses getAuthenticatedUser() directly since the invitee is not yet a member
export const getInviteInfo = async (inviteId: string) => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/ee/features/sso/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -100,6 +100,7 @@ export const unlinkLinkedAccountProvider = async (provider: string) => sew(() =>
)
);

// eslint-disable-next-line authz/require-auth-wrapper -- UI-only preference cookie, no DB access
export const skipOptionalProvidersLink = async () => sew(async () => {
const cookieStore = await cookies();
cookieStore.set(OPTIONAL_PROVIDERS_LINK_SKIPPED_COOKIE_NAME, 'true', {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/features/chat/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -550,6 +550,7 @@ export const submitFeedback = async ({
})
)

// eslint-disable-next-line authz/require-auth-wrapper -- returns identity provider metadata for the login wall, consulted before auth
export const getAskGhLoginWallData = async () => sew(async () => {
const isEnabled = env.EXPERIMENT_ASK_GH_ENABLED === 'true';
if (!isEnabled) {
Expand Down
12 changes: 12 additions & 0 deletions packages/web/tools/eslint-plugin-local/index.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import requireAuthWrapper from './rules/requireAuthWrapper.mjs';

const plugin = {
meta: {
name: 'eslint-plugin-authz-local',
},
rules: {
'require-auth-wrapper': requireAuthWrapper,
},
};

export default plugin;
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/web/eslint.config.mjs
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
import nextCoreWebVitals from 'eslint-config-next/core-web-vitals';
import tseslint from 'typescript-eslint';
import tanstackQuery from '@tanstack/eslint-plugin-query';
import authzLocal from './tools/eslint-plugin-local/index.mjs';

const config = [
...nextCoreWebVitals,
...tseslint.configs.recommended,
...tanstackQuery.configs['flat/recommended'],
{
plugins: {
authz: authzLocal,
},
rules: {
'authz/require-auth-wrapper': 'error',
},
},
{
rules: {
// New react-hooks v7 rules disabled as too strict for this codebase's existing patterns.
Expand Down
5 changes: 5 additions & 0 deletions packages/web/src/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -817,6 +817,7 @@ export const getOrgAccountRequests = async () => sew(() =>
}));
}));

// eslint-disable-next-line authz/require-auth-wrapper -- calls getAuthenticatedUser() directly; runs pre-org-membership so cannot use withAuth
export const createAccountRequest = async () => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand DownExpand Up@@ -920,6 +921,7 @@ export const createAccountRequest = async () => sew(async () => {
}
});

// eslint-disable-next-line authz/require-auth-wrapper -- public org-config bit consulted on login/signup screens before any session exists
export const getMemberApprovalRequired = async (): Promise<boolean | ServiceError> => sew(async () => {
const org = await __unsafePrisma.org.findUnique({
where: {
Expand DownExpand Up@@ -1181,6 +1183,7 @@ export const getRepoImage = async (repoId: number): Promise<ArrayBuffer | Servic
})
});

// eslint-disable-next-line authz/require-auth-wrapper -- public org-config bit consulted before authentication to decide whether to gate the UI
export const getAnonymousAccessStatus = async (): Promise<boolean | ServiceError> => sew(async () => {
const org = await __unsafePrisma.org.findUnique({
where: { id: SINGLE_TENANT_ORG_ID },
Expand DownExpand Up@@ -1244,6 +1247,7 @@ export const setAnonymousAccessStatus = async (enabled: boolean): Promise<Servic
});
});

// eslint-disable-next-line authz/require-auth-wrapper -- UI-only preference cookie, no DB access
export const setAgenticSearchTutorialDismissedCookie = async (dismissed: boolean) => sew(async () => {
const cookieStore = await cookies();
cookieStore.set(AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME, dismissed ? "true" : "false", {
Expand All@@ -1253,6 +1257,7 @@ export const setAgenticSearchTutorialDismissedCookie = async (dismissed: boolean
return true;
});

// eslint-disable-next-line authz/require-auth-wrapper -- UI-only preference cookie, no DB access
export const dismissMobileUnsupportedSplashScreen = async () => sew(async () => {
const cookieStore = await cookies();
cookieStore.set(MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, 'true');
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/[...slug]/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,4 +10,5 @@ const handler = () => {
});
}

// eslint-disable-next-line authz/require-auth-wrapper -- 404 catch-all for unknown API endpoints, returns no user data
export { handler as GET, handler as POST, handler as PUT, handler as PATCH, handler as DELETE }
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
import { handlers } from "@/auth";
// eslint-disable-next-line authz/require-auth-wrapper -- NextAuth's own auth-flow handlers, not user-data endpoints
export const { GET, POST } = handlers;
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/blame/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getFileBlame() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = Object.fromEntries(
Object.keys(fileBlameRequestSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/chat/blocking/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,7 @@ const blockingChatRequestSchema = z.object({
* The chat session is persisted to the database, allowing users to view the full
* conversation (including tool calls and reasoning) in the web UI.
*/
// eslint-disable-next-line authz/require-auth-wrapper -- delegates to askCodebase() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const requestBody = await request.json();
const parsed = await blockingChatRequestSchema.safeParseAsync(requestBody);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/commit/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getCommit() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(getCommitQueryParamsSchema.shape).map(key => [
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { serviceErrorResponse, queryParamsSchemaValidationError } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to listCommitAuthors() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(listCommitAuthorsQueryParamsSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/commits/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { serviceErrorResponse, queryParamsSchemaValidationError } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to listCommits() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(listCommitsQueryParamsSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/diff/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getDiff() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(getDiffRequestSchema.shape).map(key => [
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants

// RFC 8414: OAuth 2.0 Authorization Server Metadata
// @see: https://datatracker.ietf.org/doc/html/rfc8414
// eslint-disable-next-line authz/require-auth-wrapper -- RFC 8414 public metadata endpoint
export const GET = oauthApiHandler(async () => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,7 @@ const PROTECTED_RESOURCES = new Set([
'api/mcp'
]);

// eslint-disable-next-line authz/require-auth-wrapper -- RFC 9728 public metadata endpoint
export const GET = oauthApiHandler(async (_request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,7 @@ const queryParamsSchema = z.object({
jobId: z.string(),
});

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getAccountSyncStatus() which calls withAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = {
jobId: request.nextUrl.searchParams.get('jobId') ?? undefined,
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/ee/audit/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ const auditQueryParamsSchema = auditQueryParamsBaseSchema.refine(
{ message: "'since' must be before 'until'", path: ["since"] }
);

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to fetchAuditRecords() which calls withAuth + withMinimumOrgRole(OWNER)
export const GET = apiHandler(async (request: NextRequest) => {
const entitlements = getEntitlements();
if (!entitlements.includes('audit')) {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ const registerRequestSchema = z.object({
logo_uri: z.string().url().nullish(),
});

// eslint-disable-next-line authz/require-auth-wrapper -- RFC 7591 dynamic client registration, intentionally unauthenticated
export const POST = oauthApiHandler(async (request: NextRequest) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants
// RFC 7009: OAuth 2.0 Token Revocation
// Always returns 200 regardless of whether the token existed.
// @see: https://datatracker.ietf.org/doc/html/rfc7009
// eslint-disable-next-line authz/require-auth-wrapper -- RFC 7009 token revocation, no user session required
export const POST = oauthApiHandler(async (request: NextRequest) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/ee/oauth/token/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants
// OAuth 2.0 Token Endpoint
// Supports grant_type=authorization_code with PKCE (RFC 7636).
// @see: https://datatracker.ietf.org/doc/html/rfc6749#section-3.2
// eslint-disable-next-line authz/require-auth-wrapper -- OAuth token endpoint, authenticated via PKCE code / refresh token, not user session
export const POST = oauthApiHandler(async (request: NextRequest) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@ import { getPermissionSyncStatus } from "./api";
* Returns whether a user has a account that has it's permissions
* synced for the first time.
*/
// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getPermissionSyncStatus() which calls withAuth
export const GET = apiHandler(async () => {
const result = await getPermissionSyncStatus();

Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/files/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getFiles() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await getFilesRequestSchema.safeParseAsync(body);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to findSearchBasedSymbolDefinitions() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await findRelatedSymbolsRequestSchema.safeParseAsync(body);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to findSearchBasedSymbolReferences() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await findRelatedSymbolsRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/health/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { createLogger } from "@sourcebot/shared";

const logger = createLogger('health-check');

// eslint-disable-next-line authz/require-auth-wrapper -- public health check, no user data returned
export const GET = apiHandler(async () => {
logger.debug('health check');
return Response.json({ status: 'ok' });
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/mcp/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -133,6 +133,7 @@ export const DELETE = apiHandler(async (request: NextRequest) => {
// supported. Per the MCP Streamable HTTP spec, servers that do not offer a GET SSE
// stream MUST return 405 Method Not Allowed.
// @see: https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#listening-for-messages-from-the-server
// eslint-disable-next-line authz/require-auth-wrapper -- MCP spec mandates 405 for GET when SSE stream is unsupported; no user data
export const GET = apiHandler(async (_request: NextRequest) => {
return new Response(null, {
status: StatusCodes.METHOD_NOT_ALLOWED,
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/openapi.json/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ async function loadOpenApiDocument() {
return JSON.parse(await fs.readFile(openApiPath, 'utf8'));
}

// eslint-disable-next-line authz/require-auth-wrapper -- public OpenAPI spec, intentionally unauthenticated
export const GET = apiHandler(async () => {
const document = await loadOpenApiDocument();

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { serviceErrorResponse } from "@/lib/serviceError";
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getRepoInfo() which calls withOptionalAuth
export const GET = apiHandler(async (
_request: NextRequest,
{ params }: { params: Promise<{ repoId: string }> }
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/repos/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";
import { listRepos } from "./listReposApi";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to listRepos() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = Object.fromEntries(
Object.keys(listReposQueryParamsSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/search/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to search() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await searchRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/source/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getFileSource() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = Object.fromEntries(
Object.keys(fileSourceRequestSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/stream_search/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from '@/lib/se
import { isServiceError } from '@/lib/utils';
import { NextRequest } from 'next/server';

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to streamSearch() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await searchRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/tree/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getTree() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await getTreeRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/version/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ import { GetVersionResponse } from "@/lib/types";
// @see: https://nextjs.org/docs/14/app/building-your-application/routing/route-handlers#caching
export const dynamic = "force-dynamic";

// eslint-disable-next-line authz/require-auth-wrapper -- public Sourcebot version string, no user data
export const GET = apiHandler(async () => {
return Response.json({
version: SOURCEBOT_VERSION,
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/webhook/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,6 +130,7 @@ if (env.GITLAB_REVIEW_AGENT_TOKEN) {
}
}

// eslint-disable-next-line authz/require-auth-wrapper -- authenticated via GitHub App / GitLab webhook secrets, not user session
export const POST = async (request: NextRequest) => {
const body = await request.json();
const headers = Object.fromEntries(Array.from(request.headers.entries(), ([key, value]) => [key.toLowerCase(), value]));
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/minidenticon/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { apiHandler } from '@/lib/apiHandler';

// Generates a minidenticon avatar PNG from an email address.
// Used as a fallback avatar in emails where data URIs aren't supported.
// eslint-disable-next-line authz/require-auth-wrapper -- public identicon generator, no user data returned
export const GET = apiHandler(async (request: NextRequest) => {
const email = request.nextUrl.searchParams.get('email');
if (!email) {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/repos/[repoId]/image/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ import { apiHandler } from "@/lib/apiHandler";
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getRepoImage() action which calls withOptionalAuth
export const GET = apiHandler(async (
_request: NextRequest,
{ params }: { params: Promise<{ repoId: string }> }
Expand Down
3 changes: 3 additions & 0 deletions packages/web/src/app/invite/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ import { getAuditService } from "@/ee/features/audit/factory";

const auditService = getAuditService();

// eslint-disable-next-line authz/require-auth-wrapper -- runs pre-org-membership; uses getAuthenticatedUser() directly since withAuth requires a user-to-org link this call is establishing
export const joinOrganization = async (inviteLinkId?: string) => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand DownExpand Up@@ -71,6 +72,7 @@ export const joinOrganization = async (inviteLinkId?: string) => sew(async () =>
}
});

// eslint-disable-next-line authz/require-auth-wrapper -- runs pre-org-membership; uses getAuthenticatedUser() directly since withAuth requires a user-to-org link this call is establishing
export const redeemInvite = async (inviteId: string): Promise<{ success: boolean; } | ServiceError> => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand DownExpand Up@@ -161,6 +163,7 @@ export const redeemInvite = async (inviteId: string): Promise<{ success: boolean
});


// eslint-disable-next-line authz/require-auth-wrapper -- runs pre-org-membership; uses getAuthenticatedUser() directly since the invitee is not yet a member
export const getInviteInfo = async (inviteId: string) => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/ee/features/sso/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -100,6 +100,7 @@ export const unlinkLinkedAccountProvider = async (provider: string) => sew(() =>
)
);

// eslint-disable-next-line authz/require-auth-wrapper -- UI-only preference cookie, no DB access
export const skipOptionalProvidersLink = async () => sew(async () => {
const cookieStore = await cookies();
cookieStore.set(OPTIONAL_PROVIDERS_LINK_SKIPPED_COOKIE_NAME, 'true', {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/features/chat/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -550,6 +550,7 @@ export const submitFeedback = async ({
})
)

// eslint-disable-next-line authz/require-auth-wrapper -- returns identity provider metadata for the login wall, consulted before auth
export const getAskGhLoginWallData = async () => sew(async () => {
const isEnabled = env.EXPERIMENT_ASK_GH_ENABLED === 'true';
if (!isEnabled) {
Expand Down
12 changes: 12 additions & 0 deletions packages/web/tools/eslint-plugin-local/index.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import requireAuthWrapper from './rules/requireAuthWrapper.mjs';

const plugin = {
meta: {
name: 'eslint-plugin-authz-local',
},
rules: {
'require-auth-wrapper': requireAuthWrapper,
},
};

export default plugin;
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/web/eslint.config.mjs
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
import nextCoreWebVitals from 'eslint-config-next/core-web-vitals';
import tseslint from 'typescript-eslint';
import tanstackQuery from '@tanstack/eslint-plugin-query';
import authzLocal from './tools/eslint-plugin-local/index.mjs';

const config = [
...nextCoreWebVitals,
...tseslint.configs.recommended,
...tanstackQuery.configs['flat/recommended'],
{
plugins: {
authz: authzLocal,
},
rules: {
'authz/require-auth-wrapper': 'error',
},
},
{
rules: {
// New react-hooks v7 rules disabled as too strict for this codebase's existing patterns.
Expand Down
5 changes: 5 additions & 0 deletions packages/web/src/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -817,6 +817,7 @@ export const getOrgAccountRequests = async () => sew(() =>
}));
}));

// eslint-disable-next-line authz/require-auth-wrapper -- calls getAuthenticatedUser() directly; runs pre-org-membership so cannot use withAuth
export const createAccountRequest = async () => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand DownExpand Up@@ -920,6 +921,7 @@ export const createAccountRequest = async () => sew(async () => {
}
});

// eslint-disable-next-line authz/require-auth-wrapper -- public org-config bit consulted on login/signup screens before any session exists
export const getMemberApprovalRequired = async (): Promise<boolean | ServiceError> => sew(async () => {
const org = await __unsafePrisma.org.findUnique({
where: {
Expand DownExpand Up@@ -1181,6 +1183,7 @@ export const getRepoImage = async (repoId: number): Promise<ArrayBuffer | Servic
})
});

// eslint-disable-next-line authz/require-auth-wrapper -- public org-config bit consulted before authentication to decide whether to gate the UI
export const getAnonymousAccessStatus = async (): Promise<boolean | ServiceError> => sew(async () => {
const org = await __unsafePrisma.org.findUnique({
where: { id: SINGLE_TENANT_ORG_ID },
Expand DownExpand Up@@ -1244,6 +1247,7 @@ export const setAnonymousAccessStatus = async (enabled: boolean): Promise<Servic
});
});

// eslint-disable-next-line authz/require-auth-wrapper -- UI-only preference cookie, no DB access
export const setAgenticSearchTutorialDismissedCookie = async (dismissed: boolean) => sew(async () => {
const cookieStore = await cookies();
cookieStore.set(AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME, dismissed ? "true" : "false", {
Expand All@@ -1253,6 +1257,7 @@ export const setAgenticSearchTutorialDismissedCookie = async (dismissed: boolean
return true;
});

// eslint-disable-next-line authz/require-auth-wrapper -- UI-only preference cookie, no DB access
export const dismissMobileUnsupportedSplashScreen = async () => sew(async () => {
const cookieStore = await cookies();
cookieStore.set(MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, 'true');
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/[...slug]/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,4 +10,5 @@ const handler = () => {
});
}

// eslint-disable-next-line authz/require-auth-wrapper -- 404 catch-all for unknown API endpoints, returns no user data
export { handler as GET, handler as POST, handler as PUT, handler as PATCH, handler as DELETE }
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
import { handlers } from "@/auth";
// eslint-disable-next-line authz/require-auth-wrapper -- NextAuth's own auth-flow handlers, not user-data endpoints
export const { GET, POST } = handlers;
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/blame/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getFileBlame() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = Object.fromEntries(
Object.keys(fileBlameRequestSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/chat/blocking/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,7 @@ const blockingChatRequestSchema = z.object({
* The chat session is persisted to the database, allowing users to view the full
* conversation (including tool calls and reasoning) in the web UI.
*/
// eslint-disable-next-line authz/require-auth-wrapper -- delegates to askCodebase() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const requestBody = await request.json();
const parsed = await blockingChatRequestSchema.safeParseAsync(requestBody);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/commit/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getCommit() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(getCommitQueryParamsSchema.shape).map(key => [
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { serviceErrorResponse, queryParamsSchemaValidationError } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to listCommitAuthors() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(listCommitAuthorsQueryParamsSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/commits/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { serviceErrorResponse, queryParamsSchemaValidationError } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to listCommits() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(listCommitsQueryParamsSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/diff/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getDiff() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(getDiffRequestSchema.shape).map(key => [
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants

// RFC 8414: OAuth 2.0 Authorization Server Metadata
// @see: https://datatracker.ietf.org/doc/html/rfc8414
// eslint-disable-next-line authz/require-auth-wrapper -- RFC 8414 public metadata endpoint
export const GET = oauthApiHandler(async () => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,7 @@ const PROTECTED_RESOURCES = new Set([
'api/mcp'
]);

// eslint-disable-next-line authz/require-auth-wrapper -- RFC 9728 public metadata endpoint
export const GET = oauthApiHandler(async (_request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,7 @@ const queryParamsSchema = z.object({
jobId: z.string(),
});

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getAccountSyncStatus() which calls withAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = {
jobId: request.nextUrl.searchParams.get('jobId') ?? undefined,
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/ee/audit/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ const auditQueryParamsSchema = auditQueryParamsBaseSchema.refine(
{ message: "'since' must be before 'until'", path: ["since"] }
);

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to fetchAuditRecords() which calls withAuth + withMinimumOrgRole(OWNER)
export const GET = apiHandler(async (request: NextRequest) => {
const entitlements = getEntitlements();
if (!entitlements.includes('audit')) {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ const registerRequestSchema = z.object({
logo_uri: z.string().url().nullish(),
});

// eslint-disable-next-line authz/require-auth-wrapper -- RFC 7591 dynamic client registration, intentionally unauthenticated
export const POST = oauthApiHandler(async (request: NextRequest) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants
// RFC 7009: OAuth 2.0 Token Revocation
// Always returns 200 regardless of whether the token existed.
// @see: https://datatracker.ietf.org/doc/html/rfc7009
// eslint-disable-next-line authz/require-auth-wrapper -- RFC 7009 token revocation, no user session required
export const POST = oauthApiHandler(async (request: NextRequest) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/ee/oauth/token/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants
// OAuth 2.0 Token Endpoint
// Supports grant_type=authorization_code with PKCE (RFC 7636).
// @see: https://datatracker.ietf.org/doc/html/rfc6749#section-3.2
// eslint-disable-next-line authz/require-auth-wrapper -- OAuth token endpoint, authenticated via PKCE code / refresh token, not user session
export const POST = oauthApiHandler(async (request: NextRequest) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@ import { getPermissionSyncStatus } from "./api";
* Returns whether a user has a account that has it's permissions
* synced for the first time.
*/
// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getPermissionSyncStatus() which calls withAuth
export const GET = apiHandler(async () => {
const result = await getPermissionSyncStatus();

Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/files/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getFiles() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await getFilesRequestSchema.safeParseAsync(body);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to findSearchBasedSymbolDefinitions() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await findRelatedSymbolsRequestSchema.safeParseAsync(body);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to findSearchBasedSymbolReferences() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await findRelatedSymbolsRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/health/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { createLogger } from "@sourcebot/shared";

const logger = createLogger('health-check');

// eslint-disable-next-line authz/require-auth-wrapper -- public health check, no user data returned
export const GET = apiHandler(async () => {
logger.debug('health check');
return Response.json({ status: 'ok' });
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/mcp/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -133,6 +133,7 @@ export const DELETE = apiHandler(async (request: NextRequest) => {
// supported. Per the MCP Streamable HTTP spec, servers that do not offer a GET SSE
// stream MUST return 405 Method Not Allowed.
// @see: https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#listening-for-messages-from-the-server
// eslint-disable-next-line authz/require-auth-wrapper -- MCP spec mandates 405 for GET when SSE stream is unsupported; no user data
export const GET = apiHandler(async (_request: NextRequest) => {
return new Response(null, {
status: StatusCodes.METHOD_NOT_ALLOWED,
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/openapi.json/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ async function loadOpenApiDocument() {
return JSON.parse(await fs.readFile(openApiPath, 'utf8'));
}

// eslint-disable-next-line authz/require-auth-wrapper -- public OpenAPI spec, intentionally unauthenticated
export const GET = apiHandler(async () => {
const document = await loadOpenApiDocument();

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { serviceErrorResponse } from "@/lib/serviceError";
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getRepoInfo() which calls withOptionalAuth
export const GET = apiHandler(async (
_request: NextRequest,
{ params }: { params: Promise<{ repoId: string }> }
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/repos/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";
import { listRepos } from "./listReposApi";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to listRepos() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = Object.fromEntries(
Object.keys(listReposQueryParamsSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/search/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to search() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await searchRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/source/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getFileSource() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = Object.fromEntries(
Object.keys(fileSourceRequestSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/stream_search/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from '@/lib/se
import { isServiceError } from '@/lib/utils';
import { NextRequest } from 'next/server';

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to streamSearch() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await searchRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/tree/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getTree() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await getTreeRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/version/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ import { GetVersionResponse } from "@/lib/types";
// @see: https://nextjs.org/docs/14/app/building-your-application/routing/route-handlers#caching
export const dynamic = "force-dynamic";

// eslint-disable-next-line authz/require-auth-wrapper -- public Sourcebot version string, no user data
export const GET = apiHandler(async () => {
return Response.json({
version: SOURCEBOT_VERSION,
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/webhook/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,6 +130,7 @@ if (env.GITLAB_REVIEW_AGENT_TOKEN) {
}
}

// eslint-disable-next-line authz/require-auth-wrapper -- authenticated via GitHub App / GitLab webhook secrets, not user session
export const POST = async (request: NextRequest) => {
const body = await request.json();
const headers = Object.fromEntries(Array.from(request.headers.entries(), ([key, value]) => [key.toLowerCase(), value]));
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/minidenticon/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { apiHandler } from '@/lib/apiHandler';

// Generates a minidenticon avatar PNG from an email address.
// Used as a fallback avatar in emails where data URIs aren't supported.
// eslint-disable-next-line authz/require-auth-wrapper -- public identicon generator, no user data returned
export const GET = apiHandler(async (request: NextRequest) => {
const email = request.nextUrl.searchParams.get('email');
if (!email) {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/repos/[repoId]/image/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ import { apiHandler } from "@/lib/apiHandler";
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getRepoImage() action which calls withOptionalAuth
export const GET = apiHandler(async (
_request: NextRequest,
{ params }: { params: Promise<{ repoId: string }> }
Expand Down
3 changes: 3 additions & 0 deletions packages/web/src/app/invite/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ import { getAuditService } from "@/ee/features/audit/factory";

const auditService = getAuditService();

// eslint-disable-next-line authz/require-auth-wrapper -- runs pre-org-membership; uses getAuthenticatedUser() directly since withAuth requires a user-to-org link this call is establishing
export const joinOrganization = async (inviteLinkId?: string) => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand DownExpand Up@@ -71,6 +72,7 @@ export const joinOrganization = async (inviteLinkId?: string) => sew(async () =>
}
});

// eslint-disable-next-line authz/require-auth-wrapper -- runs pre-org-membership; uses getAuthenticatedUser() directly since withAuth requires a user-to-org link this call is establishing
export const redeemInvite = async (inviteId: string): Promise<{ success: boolean; } | ServiceError> => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand DownExpand Up@@ -161,6 +163,7 @@ export const redeemInvite = async (inviteId: string): Promise<{ success: boolean
});


// eslint-disable-next-line authz/require-auth-wrapper -- runs pre-org-membership; uses getAuthenticatedUser() directly since the invitee is not yet a member
export const getInviteInfo = async (inviteId: string) => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/ee/features/sso/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -100,6 +100,7 @@ export const unlinkLinkedAccountProvider = async (provider: string) => sew(() =>
)
);

// eslint-disable-next-line authz/require-auth-wrapper -- UI-only preference cookie, no DB access
export const skipOptionalProvidersLink = async () => sew(async () => {
const cookieStore = await cookies();
cookieStore.set(OPTIONAL_PROVIDERS_LINK_SKIPPED_COOKIE_NAME, 'true', {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/features/chat/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -550,6 +550,7 @@ export const submitFeedback = async ({
})
)

// eslint-disable-next-line authz/require-auth-wrapper -- returns identity provider metadata for the login wall, consulted before auth
export const getAskGhLoginWallData = async () => sew(async () => {
const isEnabled = env.EXPERIMENT_ASK_GH_ENABLED === 'true';
if (!isEnabled) {
Expand Down
12 changes: 12 additions & 0 deletions packages/web/tools/eslint-plugin-local/index.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import requireAuthWrapper from './rules/requireAuthWrapper.mjs';

const plugin = {
meta: {
name: 'eslint-plugin-authz-local',
},
rules: {
'require-auth-wrapper': requireAuthWrapper,
},
};

export default plugin;
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/web/eslint.config.mjs
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
import nextCoreWebVitals from 'eslint-config-next/core-web-vitals';
import tseslint from 'typescript-eslint';
import tanstackQuery from '@tanstack/eslint-plugin-query';
import authzLocal from './tools/eslint-plugin-local/index.mjs';

const config = [
...nextCoreWebVitals,
...tseslint.configs.recommended,
...tanstackQuery.configs['flat/recommended'],
{
plugins: {
authz: authzLocal,
},
rules: {
'authz/require-auth-wrapper': 'error',
},
},
{
rules: {
// New react-hooks v7 rules disabled as too strict for this codebase's existing patterns.
Expand Down
5 changes: 5 additions & 0 deletions packages/web/src/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -817,6 +817,7 @@ export const getOrgAccountRequests = async () => sew(() =>
}));
}));

// eslint-disable-next-line authz/require-auth-wrapper -- calls getAuthenticatedUser() directly; runs pre-org-membership so cannot use withAuth
export const createAccountRequest = async () => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand DownExpand Up@@ -920,6 +921,7 @@ export const createAccountRequest = async () => sew(async () => {
}
});

// eslint-disable-next-line authz/require-auth-wrapper -- public org-config bit consulted on login/signup screens before any session exists
export const getMemberApprovalRequired = async (): Promise<boolean | ServiceError> => sew(async () => {
const org = await __unsafePrisma.org.findUnique({
where: {
Expand DownExpand Up@@ -1181,6 +1183,7 @@ export const getRepoImage = async (repoId: number): Promise<ArrayBuffer | Servic
})
});

// eslint-disable-next-line authz/require-auth-wrapper -- public org-config bit consulted before authentication to decide whether to gate the UI
export const getAnonymousAccessStatus = async (): Promise<boolean | ServiceError> => sew(async () => {
const org = await __unsafePrisma.org.findUnique({
where: { id: SINGLE_TENANT_ORG_ID },
Expand DownExpand Up@@ -1244,6 +1247,7 @@ export const setAnonymousAccessStatus = async (enabled: boolean): Promise<Servic
});
});

// eslint-disable-next-line authz/require-auth-wrapper -- UI-only preference cookie, no DB access
export const setAgenticSearchTutorialDismissedCookie = async (dismissed: boolean) => sew(async () => {
const cookieStore = await cookies();
cookieStore.set(AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME, dismissed ? "true" : "false", {
Expand All@@ -1253,6 +1257,7 @@ export const setAgenticSearchTutorialDismissedCookie = async (dismissed: boolean
return true;
});

// eslint-disable-next-line authz/require-auth-wrapper -- UI-only preference cookie, no DB access
export const dismissMobileUnsupportedSplashScreen = async () => sew(async () => {
const cookieStore = await cookies();
cookieStore.set(MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, 'true');
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/[...slug]/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,4 +10,5 @@ const handler = () => {
});
}

// eslint-disable-next-line authz/require-auth-wrapper -- 404 catch-all for unknown API endpoints, returns no user data
export { handler as GET, handler as POST, handler as PUT, handler as PATCH, handler as DELETE }
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
import { handlers } from "@/auth";
// eslint-disable-next-line authz/require-auth-wrapper -- NextAuth's own auth-flow handlers, not user-data endpoints
export const { GET, POST } = handlers;
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/blame/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getFileBlame() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = Object.fromEntries(
Object.keys(fileBlameRequestSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/chat/blocking/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,7 @@ const blockingChatRequestSchema = z.object({
* The chat session is persisted to the database, allowing users to view the full
* conversation (including tool calls and reasoning) in the web UI.
*/
// eslint-disable-next-line authz/require-auth-wrapper -- delegates to askCodebase() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const requestBody = await request.json();
const parsed = await blockingChatRequestSchema.safeParseAsync(requestBody);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/commit/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getCommit() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(getCommitQueryParamsSchema.shape).map(key => [
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { serviceErrorResponse, queryParamsSchemaValidationError } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to listCommitAuthors() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(listCommitAuthorsQueryParamsSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/commits/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { serviceErrorResponse, queryParamsSchemaValidationError } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to listCommits() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(listCommitsQueryParamsSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/diff/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getDiff() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(getDiffRequestSchema.shape).map(key => [
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants

// RFC 8414: OAuth 2.0 Authorization Server Metadata
// @see: https://datatracker.ietf.org/doc/html/rfc8414
// eslint-disable-next-line authz/require-auth-wrapper -- RFC 8414 public metadata endpoint
export const GET = oauthApiHandler(async () => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,7 @@ const PROTECTED_RESOURCES = new Set([
'api/mcp'
]);

// eslint-disable-next-line authz/require-auth-wrapper -- RFC 9728 public metadata endpoint
export const GET = oauthApiHandler(async (_request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,7 @@ const queryParamsSchema = z.object({
jobId: z.string(),
});

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getAccountSyncStatus() which calls withAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = {
jobId: request.nextUrl.searchParams.get('jobId') ?? undefined,
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/ee/audit/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ const auditQueryParamsSchema = auditQueryParamsBaseSchema.refine(
{ message: "'since' must be before 'until'", path: ["since"] }
);

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to fetchAuditRecords() which calls withAuth + withMinimumOrgRole(OWNER)
export const GET = apiHandler(async (request: NextRequest) => {
const entitlements = getEntitlements();
if (!entitlements.includes('audit')) {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ const registerRequestSchema = z.object({
logo_uri: z.string().url().nullish(),
});

// eslint-disable-next-line authz/require-auth-wrapper -- RFC 7591 dynamic client registration, intentionally unauthenticated
export const POST = oauthApiHandler(async (request: NextRequest) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants
// RFC 7009: OAuth 2.0 Token Revocation
// Always returns 200 regardless of whether the token existed.
// @see: https://datatracker.ietf.org/doc/html/rfc7009
// eslint-disable-next-line authz/require-auth-wrapper -- RFC 7009 token revocation, no user session required
export const POST = oauthApiHandler(async (request: NextRequest) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/ee/oauth/token/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants
// OAuth 2.0 Token Endpoint
// Supports grant_type=authorization_code with PKCE (RFC 7636).
// @see: https://datatracker.ietf.org/doc/html/rfc6749#section-3.2
// eslint-disable-next-line authz/require-auth-wrapper -- OAuth token endpoint, authenticated via PKCE code / refresh token, not user session
export const POST = oauthApiHandler(async (request: NextRequest) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@ import { getPermissionSyncStatus } from "./api";
* Returns whether a user has a account that has it's permissions
* synced for the first time.
*/
// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getPermissionSyncStatus() which calls withAuth
export const GET = apiHandler(async () => {
const result = await getPermissionSyncStatus();

Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/files/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getFiles() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await getFilesRequestSchema.safeParseAsync(body);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to findSearchBasedSymbolDefinitions() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await findRelatedSymbolsRequestSchema.safeParseAsync(body);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to findSearchBasedSymbolReferences() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await findRelatedSymbolsRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/health/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { createLogger } from "@sourcebot/shared";

const logger = createLogger('health-check');

// eslint-disable-next-line authz/require-auth-wrapper -- public health check, no user data returned
export const GET = apiHandler(async () => {
logger.debug('health check');
return Response.json({ status: 'ok' });
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/mcp/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -133,6 +133,7 @@ export const DELETE = apiHandler(async (request: NextRequest) => {
// supported. Per the MCP Streamable HTTP spec, servers that do not offer a GET SSE
// stream MUST return 405 Method Not Allowed.
// @see: https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#listening-for-messages-from-the-server
// eslint-disable-next-line authz/require-auth-wrapper -- MCP spec mandates 405 for GET when SSE stream is unsupported; no user data
export const GET = apiHandler(async (_request: NextRequest) => {
return new Response(null, {
status: StatusCodes.METHOD_NOT_ALLOWED,
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/openapi.json/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ async function loadOpenApiDocument() {
return JSON.parse(await fs.readFile(openApiPath, 'utf8'));
}

// eslint-disable-next-line authz/require-auth-wrapper -- public OpenAPI spec, intentionally unauthenticated
export const GET = apiHandler(async () => {
const document = await loadOpenApiDocument();

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { serviceErrorResponse } from "@/lib/serviceError";
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getRepoInfo() which calls withOptionalAuth
export const GET = apiHandler(async (
_request: NextRequest,
{ params }: { params: Promise<{ repoId: string }> }
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/repos/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";
import { listRepos } from "./listReposApi";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to listRepos() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = Object.fromEntries(
Object.keys(listReposQueryParamsSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/search/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to search() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await searchRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/source/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getFileSource() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = Object.fromEntries(
Object.keys(fileSourceRequestSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/stream_search/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from '@/lib/se
import { isServiceError } from '@/lib/utils';
import { NextRequest } from 'next/server';

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to streamSearch() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await searchRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/tree/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getTree() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await getTreeRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/version/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ import { GetVersionResponse } from "@/lib/types";
// @see: https://nextjs.org/docs/14/app/building-your-application/routing/route-handlers#caching
export const dynamic = "force-dynamic";

// eslint-disable-next-line authz/require-auth-wrapper -- public Sourcebot version string, no user data
export const GET = apiHandler(async () => {
return Response.json({
version: SOURCEBOT_VERSION,
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/webhook/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,6 +130,7 @@ if (env.GITLAB_REVIEW_AGENT_TOKEN) {
}
}

// eslint-disable-next-line authz/require-auth-wrapper -- authenticated via GitHub App / GitLab webhook secrets, not user session
export const POST = async (request: NextRequest) => {
const body = await request.json();
const headers = Object.fromEntries(Array.from(request.headers.entries(), ([key, value]) => [key.toLowerCase(), value]));
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/minidenticon/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { apiHandler } from '@/lib/apiHandler';

// Generates a minidenticon avatar PNG from an email address.
// Used as a fallback avatar in emails where data URIs aren't supported.
// eslint-disable-next-line authz/require-auth-wrapper -- public identicon generator, no user data returned
export const GET = apiHandler(async (request: NextRequest) => {
const email = request.nextUrl.searchParams.get('email');
if (!email) {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/repos/[repoId]/image/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ import { apiHandler } from "@/lib/apiHandler";
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getRepoImage() action which calls withOptionalAuth
export const GET = apiHandler(async (
_request: NextRequest,
{ params }: { params: Promise<{ repoId: string }> }
Expand Down
3 changes: 3 additions & 0 deletions packages/web/src/app/invite/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ import { getAuditService } from "@/ee/features/audit/factory";

const auditService = getAuditService();

// eslint-disable-next-line authz/require-auth-wrapper -- runs pre-org-membership; uses getAuthenticatedUser() directly since withAuth requires a user-to-org link this call is establishing
export const joinOrganization = async (inviteLinkId?: string) => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand DownExpand Up@@ -71,6 +72,7 @@ export const joinOrganization = async (inviteLinkId?: string) => sew(async () =>
}
});

// eslint-disable-next-line authz/require-auth-wrapper -- runs pre-org-membership; uses getAuthenticatedUser() directly since withAuth requires a user-to-org link this call is establishing
export const redeemInvite = async (inviteId: string): Promise<{ success: boolean; } | ServiceError> => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand DownExpand Up@@ -161,6 +163,7 @@ export const redeemInvite = async (inviteId: string): Promise<{ success: boolean
});


// eslint-disable-next-line authz/require-auth-wrapper -- runs pre-org-membership; uses getAuthenticatedUser() directly since the invitee is not yet a member
export const getInviteInfo = async (inviteId: string) => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/ee/features/sso/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -100,6 +100,7 @@ export const unlinkLinkedAccountProvider = async (provider: string) => sew(() =>
)
);

// eslint-disable-next-line authz/require-auth-wrapper -- UI-only preference cookie, no DB access
export const skipOptionalProvidersLink = async () => sew(async () => {
const cookieStore = await cookies();
cookieStore.set(OPTIONAL_PROVIDERS_LINK_SKIPPED_COOKIE_NAME, 'true', {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/features/chat/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -550,6 +550,7 @@ export const submitFeedback = async ({
})
)

// eslint-disable-next-line authz/require-auth-wrapper -- returns identity provider metadata for the login wall, consulted before auth
export const getAskGhLoginWallData = async () => sew(async () => {
const isEnabled = env.EXPERIMENT_ASK_GH_ENABLED === 'true';
if (!isEnabled) {
Expand Down
12 changes: 12 additions & 0 deletions packages/web/tools/eslint-plugin-local/index.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import requireAuthWrapper from './rules/requireAuthWrapper.mjs';

const plugin = {
meta: {
name: 'eslint-plugin-authz-local',
},
rules: {
'require-auth-wrapper': requireAuthWrapper,
},
};

export default plugin;
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/web/eslint.config.mjs
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
import nextCoreWebVitals from 'eslint-config-next/core-web-vitals';
import tseslint from 'typescript-eslint';
import tanstackQuery from '@tanstack/eslint-plugin-query';
import authzLocal from './tools/eslint-plugin-local/index.mjs';

const config = [
...nextCoreWebVitals,
...tseslint.configs.recommended,
...tanstackQuery.configs['flat/recommended'],
{
plugins: {
authz: authzLocal,
},
rules: {
'authz/require-auth-wrapper': 'error',
},
},
{
rules: {
// New react-hooks v7 rules disabled as too strict for this codebase's existing patterns.
Expand Down
5 changes: 5 additions & 0 deletions packages/web/src/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -817,6 +817,7 @@ export const getOrgAccountRequests = async () => sew(() =>
}));
}));

// eslint-disable-next-line authz/require-auth-wrapper -- calls getAuthenticatedUser() directly; runs pre-org-membership so cannot use withAuth
export const createAccountRequest = async () => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand DownExpand Up@@ -920,6 +921,7 @@ export const createAccountRequest = async () => sew(async () => {
}
});

// eslint-disable-next-line authz/require-auth-wrapper -- public org-config bit consulted on login/signup screens before any session exists
export const getMemberApprovalRequired = async (): Promise<boolean | ServiceError> => sew(async () => {
const org = await __unsafePrisma.org.findUnique({
where: {
Expand DownExpand Up@@ -1181,6 +1183,7 @@ export const getRepoImage = async (repoId: number): Promise<ArrayBuffer | Servic
})
});

// eslint-disable-next-line authz/require-auth-wrapper -- public org-config bit consulted before authentication to decide whether to gate the UI
export const getAnonymousAccessStatus = async (): Promise<boolean | ServiceError> => sew(async () => {
const org = await __unsafePrisma.org.findUnique({
where: { id: SINGLE_TENANT_ORG_ID },
Expand DownExpand Up@@ -1244,6 +1247,7 @@ export const setAnonymousAccessStatus = async (enabled: boolean): Promise<Servic
});
});

// eslint-disable-next-line authz/require-auth-wrapper -- UI-only preference cookie, no DB access
export const setAgenticSearchTutorialDismissedCookie = async (dismissed: boolean) => sew(async () => {
const cookieStore = await cookies();
cookieStore.set(AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME, dismissed ? "true" : "false", {
Expand All@@ -1253,6 +1257,7 @@ export const setAgenticSearchTutorialDismissedCookie = async (dismissed: boolean
return true;
});

// eslint-disable-next-line authz/require-auth-wrapper -- UI-only preference cookie, no DB access
export const dismissMobileUnsupportedSplashScreen = async () => sew(async () => {
const cookieStore = await cookies();
cookieStore.set(MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, 'true');
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/[...slug]/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,4 +10,5 @@ const handler = () => {
});
}

// eslint-disable-next-line authz/require-auth-wrapper -- 404 catch-all for unknown API endpoints, returns no user data
export { handler as GET, handler as POST, handler as PUT, handler as PATCH, handler as DELETE }
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
import { handlers } from "@/auth";
// eslint-disable-next-line authz/require-auth-wrapper -- NextAuth's own auth-flow handlers, not user-data endpoints
export const { GET, POST } = handlers;
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/blame/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getFileBlame() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = Object.fromEntries(
Object.keys(fileBlameRequestSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/chat/blocking/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,7 @@ const blockingChatRequestSchema = z.object({
* The chat session is persisted to the database, allowing users to view the full
* conversation (including tool calls and reasoning) in the web UI.
*/
// eslint-disable-next-line authz/require-auth-wrapper -- delegates to askCodebase() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const requestBody = await request.json();
const parsed = await blockingChatRequestSchema.safeParseAsync(requestBody);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/commit/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getCommit() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(getCommitQueryParamsSchema.shape).map(key => [
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { serviceErrorResponse, queryParamsSchemaValidationError } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to listCommitAuthors() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(listCommitAuthorsQueryParamsSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/commits/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { serviceErrorResponse, queryParamsSchemaValidationError } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to listCommits() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(listCommitsQueryParamsSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/diff/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getDiff() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
const rawParams = Object.fromEntries(
Object.keys(getDiffRequestSchema.shape).map(key => [
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants

// RFC 8414: OAuth 2.0 Authorization Server Metadata
// @see: https://datatracker.ietf.org/doc/html/rfc8414
// eslint-disable-next-line authz/require-auth-wrapper -- RFC 8414 public metadata endpoint
export const GET = oauthApiHandler(async () => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,7 @@ const PROTECTED_RESOURCES = new Set([
'api/mcp'
]);

// eslint-disable-next-line authz/require-auth-wrapper -- RFC 9728 public metadata endpoint
export const GET = oauthApiHandler(async (_request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,7 @@ const queryParamsSchema = z.object({
jobId: z.string(),
});

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getAccountSyncStatus() which calls withAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = {
jobId: request.nextUrl.searchParams.get('jobId') ?? undefined,
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/ee/audit/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ const auditQueryParamsSchema = auditQueryParamsBaseSchema.refine(
{ message: "'since' must be before 'until'", path: ["since"] }
);

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to fetchAuditRecords() which calls withAuth + withMinimumOrgRole(OWNER)
export const GET = apiHandler(async (request: NextRequest) => {
const entitlements = getEntitlements();
if (!entitlements.includes('audit')) {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ const registerRequestSchema = z.object({
logo_uri: z.string().url().nullish(),
});

// eslint-disable-next-line authz/require-auth-wrapper -- RFC 7591 dynamic client registration, intentionally unauthenticated
export const POST = oauthApiHandler(async (request: NextRequest) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants
// RFC 7009: OAuth 2.0 Token Revocation
// Always returns 200 regardless of whether the token existed.
// @see: https://datatracker.ietf.org/doc/html/rfc7009
// eslint-disable-next-line authz/require-auth-wrapper -- RFC 7009 token revocation, no user session required
export const POST = oauthApiHandler(async (request: NextRequest) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/ee/oauth/token/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants
// OAuth 2.0 Token Endpoint
// Supports grant_type=authorization_code with PKCE (RFC 7636).
// @see: https://datatracker.ietf.org/doc/html/rfc6749#section-3.2
// eslint-disable-next-line authz/require-auth-wrapper -- OAuth token endpoint, authenticated via PKCE code / refresh token, not user session
export const POST = oauthApiHandler(async (request: NextRequest) => {
if (!hasEntitlement('oauth')) {
return Response.json(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@ import { getPermissionSyncStatus } from "./api";
* Returns whether a user has a account that has it's permissions
* synced for the first time.
*/
// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getPermissionSyncStatus() which calls withAuth
export const GET = apiHandler(async () => {
const result = await getPermissionSyncStatus();

Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/files/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getFiles() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await getFilesRequestSchema.safeParseAsync(body);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to findSearchBasedSymbolDefinitions() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await findRelatedSymbolsRequestSchema.safeParseAsync(body);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to findSearchBasedSymbolReferences() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await findRelatedSymbolsRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/health/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { createLogger } from "@sourcebot/shared";

const logger = createLogger('health-check');

// eslint-disable-next-line authz/require-auth-wrapper -- public health check, no user data returned
export const GET = apiHandler(async () => {
logger.debug('health check');
return Response.json({ status: 'ok' });
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/mcp/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -133,6 +133,7 @@ export const DELETE = apiHandler(async (request: NextRequest) => {
// supported. Per the MCP Streamable HTTP spec, servers that do not offer a GET SSE
// stream MUST return 405 Method Not Allowed.
// @see: https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#listening-for-messages-from-the-server
// eslint-disable-next-line authz/require-auth-wrapper -- MCP spec mandates 405 for GET when SSE stream is unsupported; no user data
export const GET = apiHandler(async (_request: NextRequest) => {
return new Response(null, {
status: StatusCodes.METHOD_NOT_ALLOWED,
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/openapi.json/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ async function loadOpenApiDocument() {
return JSON.parse(await fs.readFile(openApiPath, 'utf8'));
}

// eslint-disable-next-line authz/require-auth-wrapper -- public OpenAPI spec, intentionally unauthenticated
export const GET = apiHandler(async () => {
const document = await loadOpenApiDocument();

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { serviceErrorResponse } from "@/lib/serviceError";
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getRepoInfo() which calls withOptionalAuth
export const GET = apiHandler(async (
_request: NextRequest,
{ params }: { params: Promise<{ repoId: string }> }
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/repos/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";
import { listRepos } from "./listReposApi";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to listRepos() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = Object.fromEntries(
Object.keys(listReposQueryParamsSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/search/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to search() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await searchRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/source/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getFileSource() which calls withOptionalAuth
export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = Object.fromEntries(
Object.keys(fileSourceRequestSchema.shape).map(key => [
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/stream_search/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from '@/lib/se
import { isServiceError } from '@/lib/utils';
import { NextRequest } from 'next/server';

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to streamSearch() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await searchRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/tree/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/se
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getTree() which calls withOptionalAuth
export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = await getTreeRequestSchema.safeParseAsync(body);
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/version/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ import { GetVersionResponse } from "@/lib/types";
// @see: https://nextjs.org/docs/14/app/building-your-application/routing/route-handlers#caching
export const dynamic = "force-dynamic";

// eslint-disable-next-line authz/require-auth-wrapper -- public Sourcebot version string, no user data
export const GET = apiHandler(async () => {
return Response.json({
version: SOURCEBOT_VERSION,
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/(server)/webhook/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,6 +130,7 @@ if (env.GITLAB_REVIEW_AGENT_TOKEN) {
}
}

// eslint-disable-next-line authz/require-auth-wrapper -- authenticated via GitHub App / GitLab webhook secrets, not user session
export const POST = async (request: NextRequest) => {
const body = await request.json();
const headers = Object.fromEntries(Array.from(request.headers.entries(), ([key, value]) => [key.toLowerCase(), value]));
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/minidenticon/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import { apiHandler } from '@/lib/apiHandler';

// Generates a minidenticon avatar PNG from an email address.
// Used as a fallback avatar in emails where data URIs aren't supported.
// eslint-disable-next-line authz/require-auth-wrapper -- public identicon generator, no user data returned
export const GET = apiHandler(async (request: NextRequest) => {
const email = request.nextUrl.searchParams.get('email');
if (!email) {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/repos/[repoId]/image/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ import { apiHandler } from "@/lib/apiHandler";
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";

// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getRepoImage() action which calls withOptionalAuth
export const GET = apiHandler(async (
_request: NextRequest,
{ params }: { params: Promise<{ repoId: string }> }
Expand Down
3 changes: 3 additions & 0 deletions packages/web/src/app/invite/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ import { getAuditService } from "@/ee/features/audit/factory";

const auditService = getAuditService();

// eslint-disable-next-line authz/require-auth-wrapper -- runs pre-org-membership; uses getAuthenticatedUser() directly since withAuth requires a user-to-org link this call is establishing
export const joinOrganization = async (inviteLinkId?: string) => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand DownExpand Up@@ -71,6 +72,7 @@ export const joinOrganization = async (inviteLinkId?: string) => sew(async () =>
}
});

// eslint-disable-next-line authz/require-auth-wrapper -- runs pre-org-membership; uses getAuthenticatedUser() directly since withAuth requires a user-to-org link this call is establishing
export const redeemInvite = async (inviteId: string): Promise<{ success: boolean; } | ServiceError> => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand DownExpand Up@@ -161,6 +163,7 @@ export const redeemInvite = async (inviteId: string): Promise<{ success: boolean
});


// eslint-disable-next-line authz/require-auth-wrapper -- runs pre-org-membership; uses getAuthenticatedUser() directly since the invitee is not yet a member
export const getInviteInfo = async (inviteId: string) => sew(async () => {
const authResult = await getAuthenticatedUser();
if (!authResult) {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/ee/features/sso/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -100,6 +100,7 @@ export const unlinkLinkedAccountProvider = async (provider: string) => sew(() =>
)
);

// eslint-disable-next-line authz/require-auth-wrapper -- UI-only preference cookie, no DB access
export const skipOptionalProvidersLink = async () => sew(async () => {
const cookieStore = await cookies();
cookieStore.set(OPTIONAL_PROVIDERS_LINK_SKIPPED_COOKIE_NAME, 'true', {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/features/chat/actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -550,6 +550,7 @@ export const submitFeedback = async ({
})
)

// eslint-disable-next-line authz/require-auth-wrapper -- returns identity provider metadata for the login wall, consulted before auth
export const getAskGhLoginWallData = async () => sew(async () => {
const isEnabled = env.EXPERIMENT_ASK_GH_ENABLED === 'true';
if (!isEnabled) {
Expand Down
12 changes: 12 additions & 0 deletions packages/web/tools/eslint-plugin-local/index.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import requireAuthWrapper from './rules/requireAuthWrapper.mjs';

const plugin = {
meta: {
name: 'eslint-plugin-authz-local',
},
rules: {
'require-auth-wrapper': requireAuthWrapper,
},
};

export default plugin;
Loading
Loading