npm.io
0.2.0 • Published yesterdayCLI

@clerk/eslint-plugin

Licence
MIT
Version
0.2.0
Deps
0
Size
288 kB
Vulns
0
Weekly
0
Stars
1.7K


@clerk/eslint-plugin

Overview

This lint rule is experimental, but should already be working well.

We encourage trying it out and getting in touch with us about your experience.

If you try it out, pin your version as we might do breaking changes in minors before v1.

ESLint rules to help with Clerk patterns across JavaScript frameworks.

Currently includes a Next.js App Router rule to help enforce protecting resources where they are used. Instead of relying on a proxy matcher, you declare which folders are protected and the require-auth-protection rule flags any page, layout, template, default, route, or Server Function under those folders that doesn't guard itself.

Client components are not checked by the rule. These can only get privileged access through other protected resources, or via external API calls which are assumed to be separately protected.

The rule only detects protected or not, which corresponds to signed in/signed out. You are still responsible for making sure the checks are correct and that the user has the correct permissions to access the resource.

The config declares intent for tooling — it does not guard anything at runtime. Protection only happens when each resource calls await auth.protect() (or an equivalent check). This rule verifies that it does.

Installation

npm install --save-dev @clerk/eslint-plugin

Requires ESLint >=9 (flat config), and also works with Oxlint when configured as a jsPlugin.

Usage

Register the plugin and declare your protected/public folder globs in eslint.config.mjs, for example:

import clerkNext from '@clerk/eslint-plugin/next';

export default [
  {
    plugins: { '@clerk/next': clerkNext },
    rules: {
      '@clerk/next/require-auth-protection': [
        'error',
        {
          protected: ['**'],
          public: ['src/app/sign-in/**', 'src/app/sign-up/**'],
        },
      ],
    },
  },
];

You need to adapt the exact paths to your application structure.

This rule also works with Oxlint, you can configure the rules just like above after adding the plugin as a jsPlugin in .oxlintrc.json:

"jsPlugins": [
  {
    "name": "@clerk/next",
    "specifier": "@clerk/eslint-plugin/next"
  }
]

Note that the bulk auto-fixer described further down does require eslint to be installed.

Options

Option Type Default Description
protected string[] (required) Folder globs relative to the project root whose resources must be guarded.
public string[] [] Folder globs relative to the project root that are exempt.
resources object all true Resource groups to check. Supports routeHandlers, serverFunctions, and serverComponentEntrypoints, each as an optional boolean.
mixedScopeLayouts 'auto' | string[] 'auto' Layouts/templates that intentionally wrap both protected and public descendants. 'auto' allows them silently; a list requires each such folder to be acknowledged explicitly.
rootDir string (auto) Project root used to resolve project-relative folder globs. Defaults to the nearest ancestor eslint.config.*, then ESLint/Oxlint cwd. Relative paths are resolved against cwd.

Globs use a minimal dialect — only * (single segment) and ** (any depth). When a folder matches both protected and public, the most specific pattern wins, and protected wins ties.

Path matching

protected and public are project-relative folder globs. Use app/** for a root app directory, src/app/** for a src/app directory, and src/**, shared/**, etc. for other project folders.

The project root is normally determined by the nearest eslint.config.* ancestor of the file being checked, and falls back to cwd. You can configure it manually using rootDir; relative rootDir values are resolved against ESLint/Oxlint cwd.

We recommend starting with protected: ['**']. This protects the following resources by default:

  • Server Functions are checked wherever they live in the project when their folder is protected
  • Regardless of configuration, App Router entrypoints like page.jsx or route.js are only checked if they live under app/ or src/app/ relative to the project root

Use public for explicit exemptions:

{
  protected: ['**'],
  public: [
    'src/app/sign-in/**',
    'src/app/sign-up/**',
    'src/actions/public/**',
  ],
}
Public by default

While we recommend protecting all resources by default and opting out for public ones, it is also possible to make the default public and opting in to what should be protected.

If you do, we recommend public: ['src/app/**'] over public: ['**'], so that Server Functions outside the app/ folder are still considered protected by default:

{
  // Routes are considered public by default
  public: ['src/app/**'],
  protected: [
    // Protect everything outside of src/app/
    '**',
    // Opt into protection for parts of src/app/
    'src/app/(dashboard)/**'
  ],
}

With public: ['**'] it's easy to accidentally add a Server Function to a shared folder and forget adding protection since you'll get no lint error.

Opting out certain resources

Use resources to disable whole resource groups when a project only wants this rule to enforce protection for some App Router resources:

{
  protected: ['**'],
  resources: {
    routeHandlers: true,
    serverFunctions: true,
    serverComponentEntrypoints: false,
  },
}

We recommend leaving all as true, but switching some off can be useful during incremental migrations. This configuration also scopes suggestions and bulk-fix tooling: disabled resource groups are not reported by the rule, so they will not receive editor quick-fixes or bulk-applied fixes.

Monorepo setups

If you keep a separate eslint.config.* in each application, this rule will work with your monorepo setup out of the box.

If you have a single top level eslint.config.*, you need to define the rule once for each application, and you need to configure rootDir to point to each application root (for example apps/web). This makes sense when you consider that each protected and public pattern is application specific. For example:

{
  files: ['apps/web/src/**/*.{ts,tsx}'],
  rules: {
    '@clerk/next/require-auth-protection': [
      'error',
      {
        rootDir: 'apps/web',
        protected: ['**'],
        public: ['src/app/sign-in/**'],
      }
    ]
  }
}

What counts as protected

The rule is satisfied when the relevant function guards itself at the top, either by calling auth.protect():

import { auth } from '@clerk/nextjs/server';

export default async function Page() {
  await auth.protect();
  // A protect call with a more narrow role or permissions check also works:
  await auth.protect({ role: 'org:admin' });

  // ...
}

…or by an early-exit check derived from auth() that returns, throws, or redirects signed-out users (via Next.js navigation helpers or Clerk's redirectToSignIn() / redirectToSignUp() from auth()):

import { auth } from '@clerk/nextjs/server';

export default async function Page() {
  const { isAuthenticated, redirectToSignIn } = await auth();
  if (!isAuthenticated) redirectToSignIn();
  // ...
}

General protection must happen at the top of the function, but additional narrowing auth checks can happen further down.

Suggestions

For each unprotected resource it flags, the rule offers an editor quick-fix suggestion that inserts await auth.protect() at the top of the function (making it async and adding the import { auth } from '@clerk/nextjs/server' import if needed). Suggestions are opt-in: they appear in your editor's quick-fix menu and are not applied by eslint --fix, since adding a protection check changes runtime behavior.

Bulk auto-fixing

Applying these fixes changes the runtime behavior of your application — await auth.protect() enforces authentication where there potentially was none, or might override custom auth checks that were already in place. Always review the changes and test your application afterwards.

Because the protection insertion is a suggestion rather than an autofix, eslint --fix deliberately won't apply it. To apply it across many files at once, use the bundled command, which lints with your existing ESLint config (so your protected/public globs are honored) and applies the suggestion to every resource it can safely fix:

# Fix everything under the current directory
npx clerk-next-fix-auth-protection

# Preview without writing
npx clerk-next-fix-auth-protection --dry-run

# Scope fixes to a specific pattern, this will still
# use protected/public from your ESLint config, but
# can be useful to only fix a subset of your application
npx clerk-next-fix-auth-protection "src/**"

Resources the rule can't safely fix on its own (imported/wrapped exports, unacknowledged mixed-scope layouts) are listed as needing manual attention, and the command exits non-zero when any remain (or when --dry-run would make changes).

The same logic is available programmatically:

import { fixAuthProtection } from '@clerk/eslint-plugin/next/fix-auth-protection';

const { fixed, unresolved } = await fixAuthProtection({
  patterns: ['src/**'],
  dryRun: false,
});

Implementation details

This section describes the exact details of how the lint rule works. You normally do not need to read or understand this if you only want to use the rule.

Within folders that are configured as protected (and that eslint covers), this rule checks these resources when their resource group is enabled:

  • No files with 'use client' at the top - Early bailout for these
  • serverComponentEntrypoints: the default export of page, layout, template, and default files
  • routeHandlers: all http verb exports of route files (GET, POST etc - API endpoints)
  • serverFunctions: all exports of files with 'use server' at the top, and all inline functions that have 'use server' at the top of the function

Notably, it does not:

  • Check loading or error files
    • These normally don't use privileged resources, but if yours do, make sure you protect them
  • Check arbitrary Server Components
    • Only the different page entrypoints listed above are checked
    • If you are importing a Server Component doing privileged access into a non-protected page, like an admin panel on an otherwise public page, it should be guarded but the lint rule does not detect this

At the top of the relevant async function, after any directives or TypeScript-only declarations, to count as protected the rule accepts these patterns:

// -- Using the default .protect() behavior --
await auth.protect()
await (await auth()).protect()
// Any kind of variable declaration is okay
const { userId } = await auth.protect();
// More narrow checks are also fine
await auth.protect({ role: 'org:admin' })

// -- Custom handling --
const { isAuthenticated, userId, sessionId, redirectToSignIn, redirectToSignUp } = await auth();
// Any of these checks are okay
// Note: For useAuth() on the client !userId can also mean
// "loading", but here it's fine
if (
  !userId || userId == null || userId === null ||
  !sessionId || sessionId == null || sessionId === null ||
  !isAuthenticated
) {
  // It is fine to have arbitrary code here:
  console.log('Unauthenticated');
  // To count as protected, the function needs to have an
  // unconditional "exit" at the top level, these count:
  return;
  throw;
  // Matched by callee identifier name (imports are not traced):
  redirect();
  permanentRedirect();
  notFound();
  unauthorized();
  forbidden();
  redirectToSignIn();
  redirectToSignUp();
}

Support

For help, visit our support page.

Contributing

We're open to all community contributions! Please read our contribution guidelines and code of conduct.

Security

@clerk/eslint-plugin is a static analysis aid, not a runtime guard. It's provided to help you catch missing protections and it does error on the side of caution, but there are no guarantees it will catch everything, there might be edge cases it does not catch.

We aim to fix bugs leading to false negatives promptly, but they are not considered vulnerabilities and will not lead to us posting advisories. You are free to file lint rule bugs via normal GitHub issues.

For more information and to report what you think is a security issue, please refer to our security documentation.

License

This project is licensed under the MIT license.

See LICENSE for more information.

Keywords