fastify-jwt icon indicating copy to clipboard operation
fastify-jwt copied to clipboard

Type augmentation for namespace methods?

Open 58bits opened this issue 1 year ago • 2 comments

Prerequisites

  • [X] I have written a descriptive issue title
  • [X] I have searched existing issues to ensure the issue has not already been raised

Issue

EDIT: Simplifying this question a little.

We've created the following namespaced jwt plugin based on the docs here... https://github.com/fastify/fastify-jwt?tab=readme-ov-file#namespace

import fp from 'fastify-plugin'
import { FastifyPluginAsync, FastifyInstance } from 'fastify'
import fastifyJwt, { FastifyJWTOptions, JWT } from '@fastify/jwt'

// https://fastify.dev/docs/latest/Reference/TypeScript/#creating-a-typescript-fastify-plugin
declare module 'fastify' {
  interface FastifyInstance {
    jwt: {
      accessToken: JWT
      refreshToken: JWT
    }
  }

  interface FastifyReply {
    accessTokenSign: JWT['sign']
    refreshTokenSign: JWT['sign']
  }

  interface FastifyRequest {
    accessTokenVerify: JWT['verify']
    accessTokenDecode: JWT['decode']
    refreshTokenVerify: JWT['verify']
    refreshTokenDecode: JWT['decode']
  }
}

const jwtPlugin: FastifyPluginAsync<FastifyJWTOptions> = async (app: FastifyInstance, options) => {
  app.register<FastifyJWTOptions>(fastifyJwt, {
    // secret: Buffer.from(app.config.jwt.secret as string, 'base64'),
    secret: {
      private: app.config.jwt.access.secret.private,
      public: app.config.jwt.access.secret.public,
    },
    sign: {
      algorithm: 'RS256',
      iss: app.config.jwt.access.issuer,
      aud: app.config.jwt.access.audience,
      expiresIn: app.config.jwt.access.expiresIn,
    },
    verify: {
      algorithms: ['RS256'],
      allowedIss: app.config.jwt.access.issuer,
    },
    namespace: 'accessToken',
    jwtDecode: 'accessTokenDecode',
    jwtSign: 'accessTokenSign',
    jwtVerify: 'accessTokenVerify',
  })

  app.register<FastifyJWTOptions>(fastifyJwt, {
    // secret: Buffer.from(app.config.jwt.secret as string, 'base64'),
    secret: {
      private: app.config.jwt.refresh.secret.private,
      public: app.config.jwt.refresh.secret.public,
    },
    sign: {
      algorithm: 'RS256',
      iss: app.config.jwt.refresh.issuer,
      aud: app.config.jwt.refresh.audience,
      expiresIn: app.config.jwt.refresh.expiresIn,
    },
    verify: {
      algorithms: ['RS256'],
      allowedIss: app.config.jwt.refresh.issuer,
    },
    namespace: 'refreshToken',
    jwtDecode: 'refreshTokenDecode',
    jwtSign: 'refreshTokenSign',
    jwtVerify: 'refreshTokenVerify',
  })
}

export default fp(jwtPlugin)

With the above, when calling either of the namespaced sign methods, our augmented type definition above, and the signature on the actual method don't exactly match - which means we suspect, that

accessTokenSign: JWT['sign'] is not the correct way to assign a type to the accessTokenSign and other methods. - since the reply and request augmented methods are asynchronous - i.e. require either a callback, or return a promise.

accessTokenVerify: JWT['verify'] is also not the correct type annotation, as the request.accessTokenVerify method does not need a token as a parameter (it looks up the token from the request header).

And so our question really is - what's the correct way to augment the FastifyReply and FastifyRequest types with correct sign, verify and decode methods when using namespaces?

58bits avatar Sep 24 '24 01:09 58bits

You are requesting to expose the below types. https://github.com/fastify/fastify-jwt/blob/db719f020e79db9f262c639e34705afefcf1416b/types/jwt.d.ts#L20-L38

climba03003 avatar Sep 24 '24 05:09 climba03003

@climba03003 - yes I think you're right. I've also updated the above to include our augmented FastifyInstance .

Would the exported function types look like this? (courtesy ChatGPT). If so I'd be glad to submit a PR

EDIT: Okay we're actually working on this, and the types are more likely to be something like:

export interface JwtSignFunction {
  (
    payload: SignPayloadType,
    options?: FastifyJwtSignOptions | Partial<SignOptions>
  ): Promise<string>
  (payload: SignPayloadType, callback: SignerCallback): void
  (
    payload: SignPayloadType,
    options: FastifyJwtSignOptions | Partial<SignOptions>,
    callback: SignerCallback
  ): void
}

export interface JwtVerifyFunction {
  <Decoded extends VerifyPayloadType>(options?: FastifyJwtVerifyOptions): Promise<Decoded>
  <Decoded extends VerifyPayloadType>(callback: VerifierCallback): void
  <Decoded extends VerifyPayloadType>(
    options: FastifyJwtVerifyOptions,
    callback: VerifierCallback
  ): void
  <Decoded extends VerifyPayloadType>(options?: Partial<VerifyOptions>): Promise<Decoded>
  <Decoded extends VerifyPayloadType>(
    options: Partial<VerifyOptions>,
    callback: VerifierCallback
  ): void
}

export interface JwtDecodeFunction {
  <Decoded extends DecodePayloadType>(options?: FastifyJwtDecodeOptions): Promise<Decoded>
  <Decoded extends DecodePayloadType>(callback: DecodeCallback<Decoded>): void
  <Decoded extends DecodePayloadType>(
    options: FastifyJwtDecodeOptions,
    callback: DecodeCallback<Decoded>
  ): void
}

And then implemented in our augmented types as :

declare module 'fastify' {
  interface FastifyInstance {
    jwt: {
      accessToken: JWT
      refreshToken: JWT
    }
  }

  interface FastifyReply {
    accessTokenSign: JwtSignFunction
    refreshTokenSign: JwtSignFunction
  }

  interface FastifyRequest {
    accessTokenVerify: JwtVerifyFunction
    accessTokenDecode: JwtDecodeFunction
    refreshTokenVerify: JwtVerifyFunction
    refreshTokenDecode: JwtDecodeFunction
  }
}

Again - if this is close, and I can help by submitting a PR then I'd be glad to.

58bits avatar Sep 24 '24 09:09 58bits