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
11 changes: 11 additions & 0 deletions .changeset/real-trainers-knock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@clerk/astro": patch
"@clerk/backend": patch
"@clerk/express": patch
"@clerk/fastify": patch
"@clerk/nextjs": patch
"@clerk/nuxt": patch
"@clerk/tanstack-react-start": patch
---

Added internal helper type for `auth` and `getAuth()` functions that don't require a request or context parameter
2 changes: 2 additions & 0 deletions .typedoc/__tests__/__snapshots__/file-structure.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
"nextjs/create-sync-get-auth.mdx",
"nextjs/current-user.mdx",
"nextjs/get-auth.mdx",
"nextjs/session-auth-with-redirect.mdx",
"clerk-react/api-keys.mdx",
"clerk-react/checkout-button-props.mdx",
"clerk-react/checkout-button.mdx",
Expand Down Expand Up @@ -247,6 +248,7 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
"backend/email-address.mdx",
"backend/external-account.mdx",
"backend/feature.mdx",
"backend/get-auth-fn-no-request.mdx",
"backend/get-auth-fn.mdx",
"backend/identification-link.mdx",
"backend/infer-auth-object-from-token-array.mdx",
Expand Down
11 changes: 3 additions & 8 deletions packages/astro/src/server/clerk-middleware.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import type { AuthObject, ClerkClient } from '@clerk/backend';
import type {
AuthenticateRequestOptions,
AuthOptions,
ClerkRequest,
RedirectFun,
RequestState,
SignedInAuthObject,
SignedOutAuthObject,
} from '@clerk/backend/internal';
import {
AuthStatus,
Expand Down Expand Up @@ -36,18 +35,14 @@ import type {
AstroMiddlewareNextParam,
AstroMiddlewareReturn,
AuthFn,
AuthOptions,
SessionAuthObjectWithRedirect,
} from './types';
import { isRedirect, setHeader } from './utils';

const CONTROL_FLOW_ERROR = {
REDIRECT_TO_SIGN_IN: 'CLERK_PROTECT_REDIRECT_TO_SIGN_IN',
};

type ClerkMiddlewareAuthObject = (SignedInAuthObject | SignedOutAuthObject) & {
redirectToSignIn: (opts?: { returnBackUrl?: URL | string | null }) => Response;
};

type ClerkAstroMiddlewareHandler = (
auth: AuthFn,
context: AstroMiddlewareContextParam,
Expand Down Expand Up @@ -396,7 +391,7 @@ const redirectAdapter = (url: string | URL) => {

const createMiddlewareRedirectToSignIn = (
clerkRequest: ClerkRequest,
): ClerkMiddlewareAuthObject['redirectToSignIn'] => {
): SessionAuthObjectWithRedirect['redirectToSignIn'] => {
return (opts = {}) => {
const err = new Error(CONTROL_FLOW_ERROR.REDIRECT_TO_SIGN_IN) as any;
err.returnBackUrl = opts.returnBackUrl === null ? '' : opts.returnBackUrl || clerkRequest.clerkUrl.toString();
Expand Down
52 changes: 4 additions & 48 deletions packages/astro/src/server/types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
import type { AuthObject, InvalidTokenAuthObject, MachineAuthObject, SessionAuthObject } from '@clerk/backend';
import type {
AuthenticateRequestOptions,
InferAuthObjectFromToken,
InferAuthObjectFromTokenArray,
RedirectFun,
SessionTokenType,
TokenType,
} from '@clerk/backend/internal';
import type { PendingSessionOptions } from '@clerk/types';
import type { SessionAuthObject } from '@clerk/backend';
import type { GetAuthFnNoRequest, RedirectFun } from '@clerk/backend/internal';
import type { APIContext } from 'astro';

/**
Expand All @@ -26,44 +18,8 @@ export type AstroMiddlewareContextParam = APIContext;
export type AstroMiddlewareNextParam = MiddlewareNext;
export type AstroMiddlewareReturn = Response | Promise<Response>;

export type AuthOptions = PendingSessionOptions & Pick<AuthenticateRequestOptions, 'acceptsToken'>;

type SessionAuthObjectWithRedirect = SessionAuthObject & {
export type SessionAuthObjectWithRedirect = SessionAuthObject & {
redirectToSignIn: RedirectFun<Response>;
};

export interface AuthFn {
/**
* @example
* const auth = context.locals.auth({ acceptsToken: ['session_token', 'api_key'] })
*/
<T extends TokenType[]>(
options: AuthOptions & { acceptsToken: T },
):
| InferAuthObjectFromTokenArray<
T,
SessionAuthObjectWithRedirect,
MachineAuthObject<Exclude<T[number], SessionTokenType>>
>
| InvalidTokenAuthObject;

/**
* @example
* const auth = context.locals.auth({ acceptsToken: 'session_token' })
*/
<T extends TokenType>(
options: AuthOptions & { acceptsToken: T },
): InferAuthObjectFromToken<T, SessionAuthObjectWithRedirect, MachineAuthObject<Exclude<T, SessionTokenType>>>;

/**
* @example
* const auth = context.locals.auth({ acceptsToken: 'any' })
*/
(options: AuthOptions & { acceptsToken: 'any' }): AuthObject;

/**
* @example
* const auth = context.locals.auth()
*/
(options?: PendingSessionOptions): SessionAuthObjectWithRedirect;
}
export type AuthFn = GetAuthFnNoRequest<SessionAuthObjectWithRedirect>;
2 changes: 2 additions & 0 deletions packages/backend/src/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export type {
InferAuthObjectFromToken,
InferAuthObjectFromTokenArray,
GetAuthFn,
AuthOptions,
GetAuthFnNoRequest,
} from './tokens/types';

export { TokenType } from './tokens/tokenTypes';
Expand Down
132 changes: 91 additions & 41 deletions packages/backend/src/tokens/__tests__/getAuth.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,99 @@
import { expectTypeOf, test } from 'vitest';

import type { RedirectFun } from '../../createRedirect';
import type { AuthObject, InvalidTokenAuthObject } from '../authObjects';
import type { GetAuthFn, MachineAuthObject, SessionAuthObject } from '../types';

// Across our SDKs, we have a getAuth() function
const getAuth: GetAuthFn<Request> = (_request: any, _options: any) => {
return {} as any;
};

test('infers the correct AuthObject type for each accepted token type', () => {
const request = new Request('https://example.com');

// Session token by default
expectTypeOf(getAuth(request)).toMatchTypeOf<SessionAuthObject>();

// Individual token types
expectTypeOf(getAuth(request, { acceptsToken: 'session_token' })).toMatchTypeOf<SessionAuthObject>();
expectTypeOf(getAuth(request, { acceptsToken: 'api_key' })).toMatchTypeOf<MachineAuthObject<'api_key'>>();
expectTypeOf(getAuth(request, { acceptsToken: 'm2m_token' })).toMatchTypeOf<MachineAuthObject<'m2m_token'>>();
expectTypeOf(getAuth(request, { acceptsToken: 'oauth_token' })).toMatchTypeOf<MachineAuthObject<'oauth_token'>>();

// Array of token types
expectTypeOf(getAuth(request, { acceptsToken: ['session_token', 'm2m_token'] })).toMatchTypeOf<
SessionAuthObject | MachineAuthObject<'m2m_token'> | InvalidTokenAuthObject
>();
expectTypeOf(getAuth(request, { acceptsToken: ['m2m_token', 'oauth_token'] })).toMatchTypeOf<
MachineAuthObject<'m2m_token' | 'oauth_token'> | InvalidTokenAuthObject
>();

// Any token type
expectTypeOf(getAuth(request, { acceptsToken: 'any' })).toMatchTypeOf<AuthObject>();
import type { GetAuthFn, GetAuthFnNoRequest, MachineAuthObject, SessionAuthObject } from '../types';

describe('getAuth() or auth() with request parameter', () => {
const getAuth: GetAuthFn<Request> = (_request: any, _options: any) => {
return {} as any;
};

test('infers the correct AuthObject type for each accepted token type', () => {
const request = new Request('https://example.com');

// Session token by default
expectTypeOf(getAuth(request)).toExtend<SessionAuthObject>();

// Individual token types
expectTypeOf(getAuth(request, { acceptsToken: 'session_token' })).toExtend<SessionAuthObject>();
expectTypeOf(getAuth(request, { acceptsToken: 'api_key' })).toExtend<MachineAuthObject<'api_key'>>();
expectTypeOf(getAuth(request, { acceptsToken: 'm2m_token' })).toExtend<MachineAuthObject<'m2m_token'>>();
expectTypeOf(getAuth(request, { acceptsToken: 'oauth_token' })).toExtend<MachineAuthObject<'oauth_token'>>();

// Array of token types
expectTypeOf(getAuth(request, { acceptsToken: ['session_token', 'm2m_token'] })).toExtend<
SessionAuthObject | MachineAuthObject<'m2m_token'> | InvalidTokenAuthObject
>();
expectTypeOf(getAuth(request, { acceptsToken: ['m2m_token', 'oauth_token'] })).toExtend<
MachineAuthObject<'m2m_token' | 'oauth_token'> | InvalidTokenAuthObject
>();

// Any token type
expectTypeOf(getAuth(request, { acceptsToken: 'any' })).toExtend<AuthObject>();
});

test('verifies discriminated union works correctly with acceptsToken: any', () => {
const request = new Request('https://example.com');

const auth = getAuth(request, { acceptsToken: 'any' });

if (auth.tokenType === 'session_token') {
expectTypeOf(auth).toExtend<SessionAuthObject>();
} else if (auth.tokenType === 'api_key') {
expectTypeOf(auth).toExtend<MachineAuthObject<'api_key'>>();
} else if (auth.tokenType === 'm2m_token') {
expectTypeOf(auth).toExtend<MachineAuthObject<'m2m_token'>>();
} else if (auth.tokenType === 'oauth_token') {
expectTypeOf(auth).toExtend<MachineAuthObject<'oauth_token'>>();
}
});
});

test('verifies discriminated union works correctly with acceptsToken: any', () => {
const request = new Request('https://example.com');
describe('getAuth() or auth() without request parameter', () => {
type SessionAuthWithRedirect = SessionAuthObject & {
redirectToSignIn: RedirectFun<Response>;
redirectToSignUp: RedirectFun<Response>;
};

// Mimic Next.js auth() helper
const auth: GetAuthFnNoRequest<SessionAuthWithRedirect, true> = (_options: any) => {
return {} as any;
};

test('infers the correct AuthObject type for each accepted token type', async () => {
// Session token by default
expectTypeOf(await auth()).toExtend<SessionAuthWithRedirect>();

// Individual token types
expectTypeOf(await auth({ acceptsToken: 'session_token' })).toExtend<SessionAuthWithRedirect>();
expectTypeOf(await auth({ acceptsToken: 'api_key' })).toExtend<MachineAuthObject<'api_key'>>();
expectTypeOf(await auth({ acceptsToken: 'm2m_token' })).toExtend<MachineAuthObject<'m2m_token'>>();
expectTypeOf(await auth({ acceptsToken: 'oauth_token' })).toExtend<MachineAuthObject<'oauth_token'>>();

// Array of token types
expectTypeOf(await auth({ acceptsToken: ['session_token', 'm2m_token'] })).toExtend<
SessionAuthWithRedirect | MachineAuthObject<'m2m_token'> | InvalidTokenAuthObject
>();
expectTypeOf(await auth({ acceptsToken: ['m2m_token', 'oauth_token'] })).toExtend<
MachineAuthObject<'m2m_token' | 'oauth_token'> | InvalidTokenAuthObject
>();

// Any token type
expectTypeOf(await auth({ acceptsToken: 'any' })).toExtend<AuthObject>();
});

const auth = getAuth(request, { acceptsToken: 'any' });
test('verifies discriminated union works correctly with acceptsToken: any', async () => {
const authObject = await auth({ acceptsToken: 'any' });

if (auth.tokenType === 'session_token') {
expectTypeOf(auth).toMatchTypeOf<SessionAuthObject>();
} else if (auth.tokenType === 'api_key') {
expectTypeOf(auth).toMatchTypeOf<MachineAuthObject<'api_key'>>();
} else if (auth.tokenType === 'm2m_token') {
expectTypeOf(auth).toMatchTypeOf<MachineAuthObject<'m2m_token'>>();
} else if (auth.tokenType === 'oauth_token') {
expectTypeOf(auth).toMatchTypeOf<MachineAuthObject<'oauth_token'>>();
}
if (authObject.tokenType === 'session_token') {
expectTypeOf(authObject).toExtend<SessionAuthWithRedirect>();
} else if (authObject.tokenType === 'api_key') {
expectTypeOf(authObject).toExtend<MachineAuthObject<'api_key'>>();
} else if (authObject.tokenType === 'm2m_token') {
expectTypeOf(authObject).toExtend<MachineAuthObject<'m2m_token'>>();
} else if (authObject.tokenType === 'oauth_token') {
expectTypeOf(authObject).toExtend<MachineAuthObject<'oauth_token'>>();
}
});
});
51 changes: 49 additions & 2 deletions packages/backend/src/tokens/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,14 +187,13 @@ export type MachineAuthObject<T extends Exclude<TokenType, SessionTokenType>> =
? AuthenticatedMachineObject<T> | UnauthenticatedMachineObject<T>
: never;

type AuthOptions = PendingSessionOptions & { acceptsToken?: AuthenticateRequestOptions['acceptsToken'] };
export type AuthOptions = PendingSessionOptions & { acceptsToken?: AuthenticateRequestOptions['acceptsToken'] };

type MaybePromise<T, IsPromise extends boolean> = IsPromise extends true ? Promise<T> : T;

/**
* Shared generic overload type for getAuth() helpers across SDKs.
*
* - Parameterized by the request type (RequestType).
* - Handles different accepted token types and their corresponding return types.
*/
export interface GetAuthFn<RequestType, ReturnsPromise extends boolean = false> {
Expand Down Expand Up @@ -235,3 +234,51 @@ export interface GetAuthFn<RequestType, ReturnsPromise extends boolean = false>
*/
(req: RequestType, options?: PendingSessionOptions): MaybePromise<SessionAuthObject, ReturnsPromise>;
}

/**
* Shared generic overload type for auth() or getAuth() helpers that don't require a request parameter.
*
* - Handles different accepted token types and their corresponding return types.
* - The SessionAuthType parameter allows frameworks to extend the base SessionAuthObject with additional properties like redirect methods.
*/
export interface GetAuthFnNoRequest<
SessionAuthType extends SessionAuthObject = SessionAuthObject,
ReturnsPromise extends boolean = false,
> {
/**
* @example
* const authObject = await auth({ acceptsToken: ['session_token', 'api_key'] })
*/
<T extends TokenType[]>(
options: AuthOptions & { acceptsToken: T },
): MaybePromise<
| InferAuthObjectFromTokenArray<T, SessionAuthType, MachineAuthObject<Exclude<T[number], SessionTokenType>>>
| InvalidTokenAuthObject,
ReturnsPromise
>;

/**
* @example
* const authObject = await auth({ acceptsToken: 'session_token' })
*/
<T extends TokenType>(
options: AuthOptions & { acceptsToken: T },
): MaybePromise<
InferAuthObjectFromToken<T, SessionAuthType, MachineAuthObject<Exclude<T, SessionTokenType>>>,
ReturnsPromise
>;

/**
* @example
* const authObject = await auth({ acceptsToken: 'any' })
*/
(
options: AuthOptions & { acceptsToken: 'any' },
): MaybePromise<Exclude<AuthObject, SessionAuthObject> | SessionAuthType, ReturnsPromise>;

/**
* @example
* const authObject = await auth()
*/
(options?: PendingSessionOptions): MaybePromise<SessionAuthType, ReturnsPromise>;
}
7 changes: 2 additions & 5 deletions packages/express/src/getAuth.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import type { AuthenticateRequestOptions, GetAuthFn } from '@clerk/backend/internal';
import type { AuthOptions, GetAuthFn } from '@clerk/backend/internal';
import { getAuthObjectForAcceptedToken } from '@clerk/backend/internal';
import type { PendingSessionOptions } from '@clerk/types';
import type { Request as ExpressRequest } from 'express';

import { middlewareRequired } from './errors';
import { requestHasAuthObject } from './utils';

type GetAuthOptions = PendingSessionOptions & { acceptsToken?: AuthenticateRequestOptions['acceptsToken'] };

/**
* Retrieves the Clerk AuthObject using the current request object.
*
* @param {GetAuthOptions} options - Optional configuration for retriving auth object.
* @returns {AuthObject} Object with information about the request state and claims.
* @throws {Error} `clerkMiddleware` or `requireAuth` is required to be set in the middleware chain before this util is used.
*/
export const getAuth: GetAuthFn<ExpressRequest> = ((req: ExpressRequest, options?: GetAuthOptions) => {
export const getAuth: GetAuthFn<ExpressRequest> = ((req: ExpressRequest, options?: AuthOptions) => {
if (!requestHasAuthObject(req)) {
throw new Error(middlewareRequired('getAuth'));
}
Expand Down
11 changes: 2 additions & 9 deletions packages/fastify/src/getAuth.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import type {
AuthenticateRequestOptions,
GetAuthFn,
SignedInAuthObject,
SignedOutAuthObject,
} from '@clerk/backend/internal';
import type { AuthOptions, GetAuthFn, SignedInAuthObject, SignedOutAuthObject } from '@clerk/backend/internal';
import { getAuthObjectForAcceptedToken } from '@clerk/backend/internal';
import type { FastifyRequest } from 'fastify';

import { pluginRegistrationRequired } from './errors';

type GetAuthOptions = { acceptsToken?: AuthenticateRequestOptions['acceptsToken'] };

export const getAuth: GetAuthFn<FastifyRequest> = ((req: FastifyRequest, options?: GetAuthOptions) => {
export const getAuth: GetAuthFn<FastifyRequest> = ((req: FastifyRequest, options?: AuthOptions) => {
const authReq = req as FastifyRequest & { auth: SignedInAuthObject | SignedOutAuthObject };

if (!authReq.auth) {
Expand Down
Loading
Loading