diff --git a/.changeset/calm-predicates-infer.md b/.changeset/calm-predicates-infer.md new file mode 100644 index 00000000000..5730343d3c1 --- /dev/null +++ b/.changeset/calm-predicates-infer.md @@ -0,0 +1,5 @@ +--- +'@clerk/nextjs': patch +--- + +Fix the return type of `auth.protect()` when a predicate is passed together with the `token` option. The result is now typed from the accepted token types, so `auth.protect(() => true, { token: 'api_key' })` resolves to the API key auth object instead of a session auth object. diff --git a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts index 4b3e6754dfd..3323bf74e9f 100644 --- a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts +++ b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts @@ -6,7 +6,7 @@ import { MalformedURLError } from '@clerk/shared/pathMatcher'; import assert from 'assert'; import type { NextFetchEvent } from 'next/server'; import { NextRequest, NextResponse } from 'next/server'; -import { afterAll, beforeAll, beforeEach, describe, expect, it, type Mock, vi } from 'vitest'; +import { afterAll, beforeAll, beforeEach, describe, expect, expectTypeOf, it, type Mock, vi } from 'vitest'; import { clerkClient } from '../clerkClient'; import type { ClerkMiddlewareAuth } from '../clerkMiddleware'; @@ -118,6 +118,51 @@ describe('ClerkMiddleware type tests', () => { }); }); + it('infers the protect return type from the token option', () => { + clerkMiddlewareMock(auth => { + const predicate = () => true; + type SessionToken = typeof TokenType.SessionToken; + type ApiKey = typeof TokenType.ApiKey; + type M2MToken = typeof TokenType.M2MToken; + + expectTypeOf(auth.protect()).resolves.toHaveProperty('tokenType').toEqualTypeOf(); + expectTypeOf(auth.protect({ permission: 'org:admin:example1' })) + .resolves.toHaveProperty('tokenType') + .toEqualTypeOf(); + expectTypeOf(auth.protect({ unauthorizedUrl: '/denied' })) + .resolves.toHaveProperty('tokenType') + .toEqualTypeOf(); + expectTypeOf(auth.protect({ token: TokenType.SessionToken })) + .resolves.toHaveProperty('tokenType') + .toEqualTypeOf(); + expectTypeOf(auth.protect({ token: TokenType.ApiKey })) + .resolves.toHaveProperty('tokenType') + .toEqualTypeOf(); + expectTypeOf(auth.protect({ token: [TokenType.SessionToken, TokenType.M2MToken] })) + .resolves.toHaveProperty('tokenType') + .toEqualTypeOf(); + expectTypeOf(auth.protect({ token: 'any' })) + .resolves.toHaveProperty('tokenType') + .toEqualTypeOf(); + expectTypeOf(auth.protect(predicate)).resolves.toHaveProperty('tokenType').toEqualTypeOf(); + expectTypeOf(auth.protect(predicate, { unauthorizedUrl: '/denied' })) + .resolves.toHaveProperty('tokenType') + .toEqualTypeOf(); + expectTypeOf(auth.protect(predicate, { token: TokenType.SessionToken })) + .resolves.toHaveProperty('tokenType') + .toEqualTypeOf(); + expectTypeOf(auth.protect(predicate, { token: TokenType.ApiKey })) + .resolves.toHaveProperty('tokenType') + .toEqualTypeOf(); + expectTypeOf(auth.protect(predicate, { token: [TokenType.SessionToken, TokenType.M2MToken] })) + .resolves.toHaveProperty('tokenType') + .toEqualTypeOf(); + expectTypeOf(auth.protect(predicate, { token: 'any' })) + .resolves.toHaveProperty('tokenType') + .toEqualTypeOf(); + }); + }); + it('can be used with just an optional options object', () => { clerkMiddlewareMock({ secretKey: '', publishableKey: '' }); clerkMiddlewareMock(); diff --git a/packages/nextjs/src/server/protect.ts b/packages/nextjs/src/server/protect.ts index 73b4cf74f9d..8feb7806be6 100644 --- a/packages/nextjs/src/server/protect.ts +++ b/packages/nextjs/src/server/protect.ts @@ -5,6 +5,7 @@ import type { InferAuthObjectFromToken, InferAuthObjectFromTokenArray, RedirectFun, + SessionTokenType, SignedInAuthObject, } from '@clerk/backend/internal'; import { constants, isTokenTypeAccepted, TokenType } from '@clerk/backend/internal'; @@ -34,6 +35,14 @@ type AuthProtectOptions = { unauthenticatedUrl?: string; }; +type InferProtectedAuthObject = T extends TokenType[] + ? InferAuthObjectFromTokenArray + : T extends TokenType + ? InferAuthObjectFromToken + : T extends 'any' + ? SignedInAuthObject | AuthenticatedMachineObject + : SignedInAuthObject; + /** * Throws a Nextjs notFound error if user is not authenticated or authorized. */ @@ -52,38 +61,21 @@ export interface AuthProtect { * @example * auth.protect(has => has({ permission: 'org:admin:example1' })); */ - ( + ( params?: (has: CheckAuthorizationFromSessionClaims) => boolean, - options?: AuthProtectOptions, - ): Promise; + options?: AuthProtectOptions & { token?: T }, + ): Promise>; /** * @example + * auth.protect(); * auth.protect({ token: 'session_token' }); - */ - ( - options?: AuthProtectOptions & { token: T }, - ): Promise>; - - /** - * @example * auth.protect({ token: ['session_token', 'm2m_token'] }); - */ - ( - options?: AuthProtectOptions & { token: T }, - ): Promise>; - - /** - * @example * auth.protect({ token: 'any' }); */ - (options?: AuthProtectOptions & { token: 'any' }): Promise; - - /** - * @example - * auth.protect(); - */ - (options?: AuthProtectOptions): Promise; + ( + options?: AuthProtectOptions & { token?: T }, + ): Promise>; } export function createProtect(opts: {