Skip to content

Commit 70b8c70

Browse files
chore(web): add ESLint rule require-auth-wrapper (#1199)
* chore(web): add ESLint rule require-auth-wrapper Adds an authz/require-auth-wrapper rule that flags exported route handlers and server actions whose body doesn't textually reference withAuth() or withOptionalAuth(). The check is boundary-only by design — false positives are allowlisted with a // eslint-disable-next-line comment plus a reason. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: update CHANGELOG for #1199 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Move changelog entry Removed mention of the `authz/require-auth-wrapper` ESLint rule from the changelog. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f1510c8 commit 70b8c70

40 files changed

Lines changed: 507 additions & 0 deletions

File tree

packages/web/eslint.config.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
import nextCoreWebVitals from 'eslint-config-next/core-web-vitals';
22
import tseslint from 'typescript-eslint';
33
import tanstackQuery from '@tanstack/eslint-plugin-query';
4+
import authzLocal from './tools/eslint-plugin-local/index.mjs';
45

56
const config = [
67
...nextCoreWebVitals,
78
...tseslint.configs.recommended,
89
...tanstackQuery.configs['flat/recommended'],
10+
{
11+
plugins: {
12+
authz: authzLocal,
13+
},
14+
rules: {
15+
'authz/require-auth-wrapper': 'error',
16+
},
17+
},
918
{
1019
rules: {
1120
// New react-hooks v7 rules disabled as too strict for this codebase's existing patterns.

packages/web/src/actions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,7 @@ export const getOrgAccountRequests = async () => sew(() =>
817817
}));
818818
}));
819819

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

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

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

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

1260+
// eslint-disable-next-line authz/require-auth-wrapper -- UI-only preference cookie, no DB access
12561261
export const dismissMobileUnsupportedSplashScreen = async () => sew(async () => {
12571262
const cookieStore = await cookies();
12581263
cookieStore.set(MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, 'true');

packages/web/src/app/api/(server)/[...slug]/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ const handler = () => {
1010
});
1111
}
1212

13+
// eslint-disable-next-line authz/require-auth-wrapper -- 404 catch-all for unknown API endpoints, returns no user data
1314
export { handler as GET, handler as POST, handler as PUT, handler as PATCH, handler as DELETE }
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
import { handlers } from "@/auth";
2+
// eslint-disable-next-line authz/require-auth-wrapper -- NextAuth's own auth-flow handlers, not user-data endpoints
23
export const { GET, POST } = handlers;

packages/web/src/app/api/(server)/blame/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
77
import { isServiceError } from "@/lib/utils";
88
import { NextRequest } from "next/server";
99

10+
// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getFileBlame() which calls withOptionalAuth
1011
export const GET = apiHandler(async (request: NextRequest) => {
1112
const rawParams = Object.fromEntries(
1213
Object.keys(fileBlameRequestSchema.shape).map(key => [

packages/web/src/app/api/(server)/chat/blocking/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const blockingChatRequestSchema = z.object({
3737
* The chat session is persisted to the database, allowing users to view the full
3838
* conversation (including tool calls and reasoning) in the web UI.
3939
*/
40+
// eslint-disable-next-line authz/require-auth-wrapper -- delegates to askCodebase() which calls withOptionalAuth
4041
export const POST = apiHandler(async (request: NextRequest) => {
4142
const requestBody = await request.json();
4243
const parsed = await blockingChatRequestSchema.safeParseAsync(requestBody);

packages/web/src/app/api/(server)/commit/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
55
import { isServiceError } from "@/lib/utils";
66
import { NextRequest } from "next/server";
77

8+
// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getCommit() which calls withOptionalAuth
89
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
910
const rawParams = Object.fromEntries(
1011
Object.keys(getCommitQueryParamsSchema.shape).map(key => [

packages/web/src/app/api/(server)/commits/authors/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { serviceErrorResponse, queryParamsSchemaValidationError } from "@/lib/se
66
import { isServiceError } from "@/lib/utils";
77
import { NextRequest } from "next/server";
88

9+
// eslint-disable-next-line authz/require-auth-wrapper -- delegates to listCommitAuthors() which calls withOptionalAuth
910
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
1011
const rawParams = Object.fromEntries(
1112
Object.keys(listCommitAuthorsQueryParamsSchema.shape).map(key => [

packages/web/src/app/api/(server)/commits/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { serviceErrorResponse, queryParamsSchemaValidationError } from "@/lib/se
66
import { isServiceError } from "@/lib/utils";
77
import { NextRequest } from "next/server";
88

9+
// eslint-disable-next-line authz/require-auth-wrapper -- delegates to listCommits() which calls withOptionalAuth
910
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
1011
const rawParams = Object.fromEntries(
1112
Object.keys(listCommitsQueryParamsSchema.shape).map(key => [

packages/web/src/app/api/(server)/diff/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
55
import { isServiceError } from "@/lib/utils";
66
import { NextRequest } from "next/server";
77

8+
// eslint-disable-next-line authz/require-auth-wrapper -- delegates to getDiff() which calls withOptionalAuth
89
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
910
const rawParams = Object.fromEntries(
1011
Object.keys(getDiffRequestSchema.shape).map(key => [

0 commit comments

Comments
 (0)