chore: add lint rule and format (#367)
* chore: add lint rule and format * fix ci * add typescript * yarn install * add `@cloudflare/workers-types`pull/373/head
parent
fce4d55b9a
commit
e8b494b207
|
@ -21,8 +21,5 @@ jobs:
|
|||
with:
|
||||
node-version: 18.x
|
||||
- run: yarn install --frozen-lockfile
|
||||
- name: Build zod-validator in root directory
|
||||
run: yarn build:zod-validator
|
||||
working-directory: .
|
||||
- run: yarn build
|
||||
- run: yarn test
|
||||
|
|
|
@ -13,7 +13,7 @@ export class ClientSessionError extends AuthError {}
|
|||
export interface AuthClientConfig {
|
||||
baseUrl: string
|
||||
basePath: string
|
||||
credentials?: RequestCredentials,
|
||||
credentials?: RequestCredentials
|
||||
/** Stores last session response */
|
||||
_session?: Session | null | undefined
|
||||
/** Used for timestamp since last sycned (in seconds) */
|
||||
|
@ -33,9 +33,7 @@ export interface UseSessionOptions<R extends boolean> {
|
|||
|
||||
// Util type that matches some strings literally, but allows any other string as well.
|
||||
// @source https://github.com/microsoft/TypeScript/issues/29729#issuecomment-832522611
|
||||
export type LiteralUnion<T extends U, U = string> =
|
||||
| T
|
||||
| (U & Record<never, never>)
|
||||
export type LiteralUnion<T extends U, U = string> = T | (U & Record<never, never>)
|
||||
|
||||
export interface ClientSafeProvider {
|
||||
id: LiteralUnion<BuiltInProviderType>
|
||||
|
@ -137,7 +135,9 @@ export async function fetchData<T = any>(
|
|||
|
||||
const res = await fetch(url, options)
|
||||
const data = await res.json()
|
||||
if (!res.ok) throw data
|
||||
if (!res.ok) {
|
||||
throw data
|
||||
}
|
||||
return data as T
|
||||
} catch (error) {
|
||||
logger.error(new ClientFetchError((error as Error).message, error as any))
|
||||
|
|
|
@ -68,8 +68,7 @@ export async function getAuthUser(c: Context): Promise<AuthUser | null> {
|
|||
...config.callbacks,
|
||||
async session(...args) {
|
||||
authUser = args[0]
|
||||
const session =
|
||||
(await config.callbacks?.session?.(...args)) ?? args[0].session
|
||||
const session = (await config.callbacks?.session?.(...args)) ?? args[0].session
|
||||
const user = args[0].user ?? args[0].token
|
||||
return { user, ...session } satisfies Session
|
||||
},
|
||||
|
@ -90,7 +89,9 @@ export function verifyAuth(): MiddlewareHandler {
|
|||
status: 401,
|
||||
})
|
||||
throw new HTTPException(401, { res })
|
||||
} else c.set('authUser', authUser)
|
||||
} else {
|
||||
c.set('authUser', authUser)
|
||||
}
|
||||
|
||||
await next()
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ class AuthConfigManager {
|
|||
_config: AuthClientConfig = {
|
||||
baseUrl: parseUrl(window.location.origin).origin,
|
||||
basePath: parseUrl(window.location.origin).path,
|
||||
credentials:'same-origin',
|
||||
credentials: 'same-origin',
|
||||
_lastSync: 0,
|
||||
_session: undefined,
|
||||
_getSession: () => {},
|
||||
|
@ -112,8 +112,11 @@ export function useSession<R extends boolean>(
|
|||
error: 'SessionRequired',
|
||||
callbackUrl: window.location.href,
|
||||
})}`
|
||||
if (onUnauthenticated) onUnauthenticated()
|
||||
else window.location.href = url
|
||||
if (onUnauthenticated) {
|
||||
onUnauthenticated()
|
||||
} else {
|
||||
window.location.href = url
|
||||
}
|
||||
}
|
||||
}, [requiredAndNotLoading, onUnauthenticated])
|
||||
|
||||
|
@ -153,7 +156,11 @@ export async function getSession(params?: GetSessionParams) {
|
|||
* @internal
|
||||
*/
|
||||
export async function getCsrfToken() {
|
||||
const response = await fetchData<{ csrfToken: string }>('csrf', authConfigManager.getConfig(), logger)
|
||||
const response = await fetchData<{ csrfToken: string }>(
|
||||
'csrf',
|
||||
authConfigManager.getConfig(),
|
||||
logger
|
||||
)
|
||||
return response?.csrfToken ?? ''
|
||||
}
|
||||
|
||||
|
@ -215,7 +222,9 @@ export async function signIn<P extends RedirectableProviderType | undefined = un
|
|||
const url = (data as any).url ?? callbackUrl
|
||||
window.location.href = url
|
||||
// If url contains a hash, the browser does not reload the page. We reload manually
|
||||
if (url.includes('#')) window.location.reload()
|
||||
if (url.includes('#')) {
|
||||
window.location.reload()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -261,7 +270,9 @@ export async function signOut<R extends boolean = true>(
|
|||
const url = (data as any).url ?? callbackUrl
|
||||
window.location.href = url
|
||||
// If url contains a hash, the browser does not reload the page. We reload manually
|
||||
if (url.includes('#')) window.location.reload()
|
||||
if (url.includes('#')) {
|
||||
window.location.reload()
|
||||
}
|
||||
// @ts-expect-error TODO: Fix this
|
||||
return
|
||||
}
|
||||
|
@ -285,11 +296,12 @@ export function SessionProvider(props: SessionProviderProps) {
|
|||
__AUTHJS._lastSync = hasInitialSession ? now() : 0
|
||||
|
||||
const [session, setSession] = React.useState(() => {
|
||||
if (hasInitialSession) __AUTHJS._session = props.session
|
||||
if (hasInitialSession) {
|
||||
__AUTHJS._session = props.session
|
||||
}
|
||||
return props.session
|
||||
})
|
||||
|
||||
|
||||
const [loading, setLoading] = React.useState(!hasInitialSession)
|
||||
|
||||
React.useEffect(() => {
|
||||
|
@ -363,8 +375,9 @@ export function SessionProvider(props: SessionProviderProps) {
|
|||
// and makes our tab visible again, re-fetch the session, but only if
|
||||
// this feature is not disabled.
|
||||
const visibilityHandler = () => {
|
||||
if (refetchOnWindowFocus && document.visibilityState === 'visible')
|
||||
if (refetchOnWindowFocus && document.visibilityState === 'visible') {
|
||||
__AUTHJS._getSession({ event: 'visibilitychange' })
|
||||
}
|
||||
}
|
||||
document.addEventListener('visibilitychange', visibilityHandler, false)
|
||||
return () => document.removeEventListener('visibilitychange', visibilityHandler, false)
|
||||
|
@ -390,7 +403,9 @@ export function SessionProvider(props: SessionProviderProps) {
|
|||
data: session,
|
||||
status: loading ? 'loading' : session ? 'authenticated' : 'unauthenticated',
|
||||
async update(data: any) {
|
||||
if (loading || !session) return
|
||||
if (loading || !session) {
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
const newSession = await fetchData<Session>(
|
||||
'session',
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import {webcrypto} from 'node:crypto'
|
||||
import { webcrypto } from 'node:crypto'
|
||||
import { Hono } from 'hono'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { authHandler, verifyAuth,initAuthConfig} from '../src'
|
||||
|
||||
import { authHandler, verifyAuth, initAuthConfig } from '../src'
|
||||
|
||||
// @ts-expect-error - global crypto
|
||||
//needed for node 18 and below but should work in node 20 and above
|
||||
global.crypto = webcrypto
|
||||
global.crypto = webcrypto
|
||||
|
||||
describe('Auth.js Adapter Middleware', () => {
|
||||
it('Should return 500 if AUTH_SECRET is missing', async () => {
|
||||
|
@ -37,7 +36,7 @@ describe('Auth.js Adapter Middleware', () => {
|
|||
const app = new Hono()
|
||||
|
||||
app.use('/*', (c, next) => {
|
||||
c.env = {'AUTH_SECRET':'secret'}
|
||||
c.env = { AUTH_SECRET: 'secret' }
|
||||
return next()
|
||||
})
|
||||
|
||||
|
@ -60,7 +59,7 @@ describe('Auth.js Adapter Middleware', () => {
|
|||
const app = new Hono()
|
||||
|
||||
app.use('/*', (c, next) => {
|
||||
c.env = {'AUTH_SECRET':'secret'}
|
||||
c.env = { AUTH_SECRET: 'secret' }
|
||||
return next()
|
||||
})
|
||||
|
||||
|
@ -77,7 +76,7 @@ describe('Auth.js Adapter Middleware', () => {
|
|||
|
||||
app.use('/api/auth/*', authHandler())
|
||||
|
||||
app.get('/api/protected', (c)=> c.text('protected'))
|
||||
app.get('/api/protected', (c) => c.text('protected'))
|
||||
const req = new Request('http://localhost/api/protected')
|
||||
const res = await app.request(req)
|
||||
expect(res.status).toBe(401)
|
||||
|
|
|
@ -39,5 +39,8 @@
|
|||
"hono": "^3.11.7",
|
||||
"tsup": "^8.0.1",
|
||||
"vitest": "^1.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.14.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,9 @@ export const bunTranspiler = (options?: BunTranspilerOptions) => {
|
|||
const extensions = options?.extensions ?? defaultOptions.extensions
|
||||
const headers = options?.headers ?? defaultOptions.headers
|
||||
|
||||
if (extensions?.every((ext) => !url.pathname.endsWith(ext))) return
|
||||
if (extensions?.every((ext) => !url.pathname.endsWith(ext))) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const loader = url.pathname.split('.').pop() as Bun.TranspilerOptions['loader']
|
||||
|
|
|
@ -25,7 +25,9 @@ export const esbuildTranspiler = (options?: EsbuildTranspilerOptions) => {
|
|||
const url = new URL(c.req.url)
|
||||
const extensions = options?.extensions ?? ['.ts', '.tsx']
|
||||
|
||||
if (extensions.every((ext) => !url.pathname.endsWith(ext))) return
|
||||
if (extensions.every((ext) => !url.pathname.endsWith(ext))) {
|
||||
return
|
||||
}
|
||||
|
||||
const headers = { 'content-type': options?.contentType ?? 'text/javascript' }
|
||||
const script = await c.res.text()
|
||||
|
|
|
@ -85,6 +85,8 @@ const setFirebaseToken = (c: Context, idToken: FirebaseIdToken) => c.set(idToken
|
|||
|
||||
export const getFirebaseToken = (c: Context): FirebaseIdToken | null => {
|
||||
const idToken = c.get(idTokenContextKey)
|
||||
if (!idToken) return null
|
||||
if (!idToken) {
|
||||
return null
|
||||
}
|
||||
return idToken
|
||||
}
|
||||
|
|
|
@ -96,7 +96,9 @@ const TestSchema = new GraphQLSchema({
|
|||
|
||||
const urlString = (query?: Record<string, string>): string => {
|
||||
const base = 'http://localhost/graphql'
|
||||
if (!query) return base
|
||||
if (!query) {
|
||||
return base
|
||||
}
|
||||
const queryString = new URLSearchParams(query).toString()
|
||||
return `${base}?${queryString}`
|
||||
}
|
||||
|
|
|
@ -159,7 +159,9 @@ const TestSchema = new GraphQLSchema({
|
|||
|
||||
const urlString = (query?: Record<string, string>): string => {
|
||||
const base = 'http://localhost/graphql'
|
||||
if (!query) return base
|
||||
if (!query) {
|
||||
return base
|
||||
}
|
||||
const queryString = new URLSearchParams(query).toString()
|
||||
return `${base}?${queryString}`
|
||||
}
|
||||
|
|
|
@ -84,10 +84,15 @@ export class AuthFlow {
|
|||
body: parsedOptions,
|
||||
}).then((res) => res.json())) as DiscordTokenResponse | DiscordErrorResponse
|
||||
|
||||
if ('error_description' in response)
|
||||
if ('error_description' in response) {
|
||||
throw new HTTPException(400, { message: response.error_description })
|
||||
if ('error' in response) throw new HTTPException(400, { message: response.error })
|
||||
if ('message' in response) throw new HTTPException(400, { message: response.message })
|
||||
}
|
||||
if ('error' in response) {
|
||||
throw new HTTPException(400, { message: response.error })
|
||||
}
|
||||
if ('message' in response) {
|
||||
throw new HTTPException(400, { message: response.message })
|
||||
}
|
||||
|
||||
if ('access_token' in response) {
|
||||
this.token = {
|
||||
|
@ -103,7 +108,9 @@ export class AuthFlow {
|
|||
}
|
||||
}
|
||||
|
||||
if ('scope' in response) this.granted_scopes = response.scope.split(' ')
|
||||
if ('scope' in response) {
|
||||
this.granted_scopes = response.scope.split(' ')
|
||||
}
|
||||
}
|
||||
|
||||
async getUserData() {
|
||||
|
@ -114,11 +121,18 @@ export class AuthFlow {
|
|||
},
|
||||
}).then((res) => res.json())) as DiscordMeResponse | DiscordErrorResponse
|
||||
|
||||
if ('error_description' in response)
|
||||
if ('error_description' in response) {
|
||||
throw new HTTPException(400, { message: response.error_description })
|
||||
if ('error' in response) throw new HTTPException(400, { message: response.error })
|
||||
if ('message' in response) throw new HTTPException(400, { message: response.message })
|
||||
}
|
||||
if ('error' in response) {
|
||||
throw new HTTPException(400, { message: response.error })
|
||||
}
|
||||
if ('message' in response) {
|
||||
throw new HTTPException(400, { message: response.message })
|
||||
}
|
||||
|
||||
if ('user' in response) this.user = response.user
|
||||
if ('user' in response) {
|
||||
this.user = response.user
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,9 @@ export async function refreshToken(
|
|||
body: params,
|
||||
}).then((res) => res.json())) as DiscordTokenResponse | { error: string }
|
||||
|
||||
if ('error' in response) throw new HTTPException(400, { message: response.error })
|
||||
if ('error' in response) {
|
||||
throw new HTTPException(400, { message: response.error })
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
|
|
@ -21,7 +21,9 @@ export async function revokeToken(
|
|||
body: params,
|
||||
})
|
||||
|
||||
if (response.status !== 200) throw new HTTPException(400, { message: 'Something went wrong' })
|
||||
if (response.status !== 200) {
|
||||
throw new HTTPException(400, { message: 'Something went wrong' })
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -78,7 +78,9 @@ export class AuthFlow {
|
|||
| FacebookTokenResponse
|
||||
| FacebookErrorResponse
|
||||
|
||||
if ('error' in response) throw new HTTPException(400, { message: response.error?.message })
|
||||
if ('error' in response) {
|
||||
throw new HTTPException(400, { message: response.error?.message })
|
||||
}
|
||||
|
||||
if ('access_token' in response) {
|
||||
this.token = {
|
||||
|
@ -93,9 +95,13 @@ export class AuthFlow {
|
|||
`https://graph.facebook.com/v18.0/me?access_token=${this.token?.token}`
|
||||
).then((res) => res.json())) as FacebookMeResponse | FacebookErrorResponse
|
||||
|
||||
if ('error' in response) throw new HTTPException(400, { message: response.error?.message })
|
||||
if ('error' in response) {
|
||||
throw new HTTPException(400, { message: response.error?.message })
|
||||
}
|
||||
|
||||
if ('id' in response) this.user = response
|
||||
if ('id' in response) {
|
||||
this.user = response
|
||||
}
|
||||
}
|
||||
|
||||
async getUserData() {
|
||||
|
@ -107,8 +113,12 @@ export class AuthFlow {
|
|||
`https://graph.facebook.com/${this.user?.id}?fields=${parsedFields}&access_token=${this.token?.token}`
|
||||
).then((res) => res.json())) as FacebookUser | FacebookErrorResponse
|
||||
|
||||
if ('error' in response) throw new HTTPException(400, { message: response.error?.message })
|
||||
if ('error' in response) {
|
||||
throw new HTTPException(400, { message: response.error?.message })
|
||||
}
|
||||
|
||||
if ('id' in response) this.user = response
|
||||
if ('id' in response) {
|
||||
this.user = response
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,8 +65,9 @@ export class AuthFlow {
|
|||
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
||||
}).then((res) => res.json())) as GitHubTokenResponse | GitHubErrorResponse
|
||||
|
||||
if ('error_description' in response)
|
||||
if ('error_description' in response) {
|
||||
throw new HTTPException(400, { message: response.error_description })
|
||||
}
|
||||
|
||||
if ('access_token' in response) {
|
||||
this.token = {
|
||||
|
@ -85,7 +86,9 @@ export class AuthFlow {
|
|||
}
|
||||
|
||||
async getUserData() {
|
||||
if (!this.token?.token) await this.getTokenFromCode()
|
||||
if (!this.token?.token) {
|
||||
await this.getTokenFromCode()
|
||||
}
|
||||
|
||||
const response = (await fetch('https://api.github.com/user', {
|
||||
headers: {
|
||||
|
@ -96,7 +99,9 @@ export class AuthFlow {
|
|||
},
|
||||
}).then((res) => res.json())) as GitHubUser | GitHubErrorResponse
|
||||
|
||||
if ('message' in response) throw new HTTPException(400, { message: response.message })
|
||||
if ('message' in response) {
|
||||
throw new HTTPException(400, { message: response.message })
|
||||
}
|
||||
|
||||
if ('id' in response) {
|
||||
this.user = response
|
||||
|
|
|
@ -91,7 +91,9 @@ export class AuthFlow {
|
|||
}),
|
||||
}).then((res) => res.json())) as GoogleTokenResponse | GoogleErrorResponse
|
||||
|
||||
if ('error' in response) throw new HTTPException(400, { message: response.error_description })
|
||||
if ('error' in response) {
|
||||
throw new HTTPException(400, { message: response.error_description })
|
||||
}
|
||||
|
||||
if ('access_token' in response) {
|
||||
this.token = {
|
||||
|
@ -112,8 +114,12 @@ export class AuthFlow {
|
|||
},
|
||||
}).then((res) => res.json())) as GoogleUser | GoogleErrorResponse
|
||||
|
||||
if ('error' in response) throw new HTTPException(400, { message: response.error?.message })
|
||||
if ('error' in response) {
|
||||
throw new HTTPException(400, { message: response.error?.message })
|
||||
}
|
||||
|
||||
if ('id' in response) this.user = response
|
||||
if ('id' in response) {
|
||||
this.user = response
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,7 +79,9 @@ export class AuthFlow {
|
|||
},
|
||||
}).then((res) => res.json())) as LinkedInTokenResponse | LinkedInErrorResponse
|
||||
|
||||
if ('error' in response) throw new HTTPException(400, { message: response.error_description })
|
||||
if ('error' in response) {
|
||||
throw new HTTPException(400, { message: response.error_description })
|
||||
}
|
||||
|
||||
if ('access_token' in response) {
|
||||
this.token = {
|
||||
|
@ -106,9 +108,13 @@ export class AuthFlow {
|
|||
},
|
||||
}).then((res) => res.json())) as LinkedInUser | LinkedInErrorResponse
|
||||
|
||||
if ('message' in response) throw new HTTPException(400, { message: response.message })
|
||||
if ('message' in response) {
|
||||
throw new HTTPException(400, { message: response.message })
|
||||
}
|
||||
|
||||
if ('sub' in response) this.user = response
|
||||
if ('sub' in response) {
|
||||
this.user = response
|
||||
}
|
||||
}
|
||||
|
||||
async getAppToken() {
|
||||
|
@ -122,7 +128,9 @@ export class AuthFlow {
|
|||
(res) => res.json()
|
||||
)) as LinkedInTokenResponse | LinkedInErrorResponse
|
||||
|
||||
if ('error' in response) throw new HTTPException(400, { message: response.error_description })
|
||||
if ('error' in response) {
|
||||
throw new HTTPException(400, { message: response.error_description })
|
||||
}
|
||||
|
||||
if ('access_token' in response) {
|
||||
this.token = {
|
||||
|
|
|
@ -93,7 +93,9 @@ export class AuthFlow {
|
|||
},
|
||||
}).then((res) => res.json())) as XTokenResponse | XErrorResponse
|
||||
|
||||
if ('error' in response) throw new HTTPException(400, { message: response.error_description })
|
||||
if ('error' in response) {
|
||||
throw new HTTPException(400, { message: response.error_description })
|
||||
}
|
||||
|
||||
if ('access_token' in response) {
|
||||
this.token = {
|
||||
|
@ -122,9 +124,12 @@ export class AuthFlow {
|
|||
},
|
||||
}).then((res) => res.json())) as XMeResponse | XErrorResponse
|
||||
|
||||
if ('error_description' in response)
|
||||
if ('error_description' in response) {
|
||||
throw new HTTPException(400, { message: response.error_description })
|
||||
}
|
||||
|
||||
if ('data' in response) this.user = response.data
|
||||
if ('data' in response) {
|
||||
this.user = response.data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,8 +21,9 @@ export async function refreshToken(
|
|||
},
|
||||
}).then((res) => res.json())) as XTokenResponse | XErrorResponse
|
||||
|
||||
if ('error_description' in response)
|
||||
if ('error_description' in response) {
|
||||
throw new HTTPException(400, { message: response.error_description })
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
|
|
@ -21,8 +21,9 @@ export async function revokeToken(
|
|||
},
|
||||
}).then((res) => res.json())) as XRevokeResponse | XErrorResponse
|
||||
|
||||
if ('error_description' in response)
|
||||
if ('error_description' in response) {
|
||||
throw new HTTPException(400, { message: response.error_description })
|
||||
}
|
||||
|
||||
return response.revoked
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type { DefaultBodyType, StrictResponse } from 'msw'
|
||||
import { HttpResponse, http } from 'msw'
|
||||
|
||||
import type { DiscordErrorResponse, DiscordTokenResponse } from '../src/providers/discord'
|
||||
import type {
|
||||
FacebookErrorResponse,
|
||||
FacebookTokenResponse,
|
||||
|
@ -14,7 +15,6 @@ import type {
|
|||
} from '../src/providers/google/types'
|
||||
import type { LinkedInErrorResponse, LinkedInTokenResponse } from '../src/providers/linkedin'
|
||||
import type { XErrorResponse, XRevokeResponse, XTokenResponse } from '../src/providers/x'
|
||||
import { DiscordErrorResponse, DiscordTokenResponse } from '../src/providers/discord'
|
||||
|
||||
export const handlers = [
|
||||
// Google
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import { Hono } from 'hono'
|
||||
import { setupServer } from 'msw/node'
|
||||
import type { DiscordUser } from '../src/providers/discord'
|
||||
import {
|
||||
refreshToken as discordRefresh,
|
||||
revokeToken as discordRevoke,
|
||||
discordAuth,
|
||||
} from '../src/providers/discord'
|
||||
import { facebookAuth } from '../src/providers/facebook'
|
||||
import type { FacebookUser } from '../src/providers/facebook'
|
||||
import { githubAuth } from '../src/providers/github'
|
||||
|
@ -38,13 +44,6 @@ import {
|
|||
discordRefreshTokenError,
|
||||
} from './handlers'
|
||||
|
||||
import {
|
||||
refreshToken as discordRefresh,
|
||||
revokeToken as discordRevoke,
|
||||
discordAuth,
|
||||
DiscordUser,
|
||||
} from '../src/providers/discord'
|
||||
|
||||
const server = setupServer(...handlers)
|
||||
server.listen()
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Hono } from 'hono'
|
||||
import type { Histogram} from 'prom-client'
|
||||
import type { Histogram } from 'prom-client'
|
||||
import { Registry } from 'prom-client'
|
||||
import { prometheus } from './index'
|
||||
|
||||
|
@ -7,9 +7,12 @@ describe('Prometheus middleware', () => {
|
|||
const app = new Hono()
|
||||
const registry = new Registry()
|
||||
|
||||
app.use('*', prometheus({
|
||||
registry,
|
||||
}).registerMetrics)
|
||||
app.use(
|
||||
'*',
|
||||
prometheus({
|
||||
registry,
|
||||
}).registerMetrics
|
||||
)
|
||||
|
||||
app.get('/', (c) => c.text('hello'))
|
||||
app.get('/user/:id', (c) => c.text(c.req.param('id')))
|
||||
|
@ -21,10 +24,13 @@ describe('Prometheus middleware', () => {
|
|||
const app = new Hono()
|
||||
const registry = new Registry()
|
||||
|
||||
app.use('*', prometheus({
|
||||
registry,
|
||||
prefix: 'myprefix_',
|
||||
}).registerMetrics)
|
||||
app.use(
|
||||
'*',
|
||||
prometheus({
|
||||
registry,
|
||||
prefix: 'myprefix_',
|
||||
}).registerMetrics
|
||||
)
|
||||
|
||||
expect(await registry.metrics()).toMatchInlineSnapshot(`
|
||||
"# HELP myprefix_http_request_duration_seconds Duration of HTTP requests in seconds
|
||||
|
@ -40,17 +46,20 @@ describe('Prometheus middleware', () => {
|
|||
const app = new Hono()
|
||||
const registry = new Registry()
|
||||
|
||||
app.use('*', prometheus({
|
||||
registry,
|
||||
metricOptions: {
|
||||
requestsTotal: {
|
||||
customLabels: {
|
||||
id: (c) => c.req.query('id') ?? 'unknown',
|
||||
contentType: (c) => c.res.headers.get('content-type') ?? 'unknown',
|
||||
}
|
||||
app.use(
|
||||
'*',
|
||||
prometheus({
|
||||
registry,
|
||||
metricOptions: {
|
||||
requestsTotal: {
|
||||
customLabels: {
|
||||
id: (c) => c.req.query('id') ?? 'unknown',
|
||||
contentType: (c) => c.res.headers.get('content-type') ?? 'unknown',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}).registerMetrics)
|
||||
}).registerMetrics
|
||||
)
|
||||
|
||||
app.get('/', (c) => c.text('hello'))
|
||||
|
||||
|
@ -80,7 +89,7 @@ describe('Prometheus middleware', () => {
|
|||
ok: 'true',
|
||||
},
|
||||
value: 1,
|
||||
}
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
|
@ -98,7 +107,7 @@ describe('Prometheus middleware', () => {
|
|||
ok: 'false',
|
||||
},
|
||||
value: 1,
|
||||
}
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
@ -107,10 +116,13 @@ describe('Prometheus middleware', () => {
|
|||
it('updates the http_requests_duration metric with the correct labels on successful responses', async () => {
|
||||
await app.request('http://localhost/')
|
||||
|
||||
const { values } = await (registry.getSingleMetric('http_request_duration_seconds') as Histogram)!.get()!
|
||||
const { values } = await (registry.getSingleMetric(
|
||||
'http_request_duration_seconds'
|
||||
) as Histogram)!.get()!
|
||||
|
||||
const countMetric = values.find(
|
||||
(v) => v.metricName === 'http_request_duration_seconds_count' &&
|
||||
(v) =>
|
||||
v.metricName === 'http_request_duration_seconds_count' &&
|
||||
v.labels.method === 'GET' &&
|
||||
v.labels.route === '/' &&
|
||||
v.labels.status === '200'
|
||||
|
@ -122,10 +134,13 @@ describe('Prometheus middleware', () => {
|
|||
it('updates the http_requests_duration metric with the correct labels on errors', async () => {
|
||||
await app.request('http://localhost/notfound')
|
||||
|
||||
const { values } = await (registry.getSingleMetric('http_request_duration_seconds') as Histogram)!.get()!
|
||||
const { values } = await (registry.getSingleMetric(
|
||||
'http_request_duration_seconds'
|
||||
) as Histogram)!.get()!
|
||||
|
||||
const countMetric = values.find(
|
||||
(v) => v.metricName === 'http_request_duration_seconds_count' &&
|
||||
(v) =>
|
||||
v.metricName === 'http_request_duration_seconds_count' &&
|
||||
v.labels.method === 'GET' &&
|
||||
v.labels.route === '/*' &&
|
||||
v.labels.status === '404'
|
||||
|
@ -146,8 +161,8 @@ describe('Prometheus middleware', () => {
|
|||
metricOptions: {
|
||||
requestDuration: {
|
||||
disabled: true, // Disable duration metrics to make the test result more predictable
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
app.use('*', registerMetrics)
|
||||
|
|
|
@ -2,19 +2,20 @@ import type { Context } from 'hono'
|
|||
import { createMiddleware } from 'hono/factory'
|
||||
import type { DefaultMetricsCollectorConfiguration, RegistryContentType } from 'prom-client'
|
||||
import { Registry, collectDefaultMetrics as promCollectDefaultMetrics } from 'prom-client'
|
||||
import { type MetricOptions, type CustomMetricsOptions, createStandardMetrics } from './standardMetrics'
|
||||
import {
|
||||
type MetricOptions,
|
||||
type CustomMetricsOptions,
|
||||
createStandardMetrics,
|
||||
} from './standardMetrics'
|
||||
|
||||
interface PrometheusOptions {
|
||||
registry?: Registry;
|
||||
collectDefaultMetrics?: boolean | DefaultMetricsCollectorConfiguration<RegistryContentType>;
|
||||
prefix?: string;
|
||||
metricOptions?: Omit<CustomMetricsOptions, 'prefix' | 'register'>;
|
||||
registry?: Registry
|
||||
collectDefaultMetrics?: boolean | DefaultMetricsCollectorConfiguration<RegistryContentType>
|
||||
prefix?: string
|
||||
metricOptions?: Omit<CustomMetricsOptions, 'prefix' | 'register'>
|
||||
}
|
||||
|
||||
const evaluateCustomLabels = (
|
||||
customLabels: MetricOptions['customLabels'],
|
||||
context: Context,
|
||||
) => {
|
||||
const evaluateCustomLabels = (customLabels: MetricOptions['customLabels'], context: Context) => {
|
||||
const labels: Record<string, string> = {}
|
||||
|
||||
for (const [key, fn] of Object.entries(customLabels ?? {})) {
|
||||
|
@ -36,7 +37,7 @@ export const prometheus = (options?: PrometheusOptions) => {
|
|||
promCollectDefaultMetrics({
|
||||
prefix,
|
||||
register: registry,
|
||||
...(typeof collectDefaultMetrics === 'object' && collectDefaultMetrics)
|
||||
...(typeof collectDefaultMetrics === 'object' && collectDefaultMetrics),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -53,7 +54,6 @@ export const prometheus = (options?: PrometheusOptions) => {
|
|||
|
||||
try {
|
||||
await next()
|
||||
|
||||
} finally {
|
||||
const commonLabels = {
|
||||
method: c.req.method,
|
||||
|
@ -64,14 +64,14 @@ export const prometheus = (options?: PrometheusOptions) => {
|
|||
|
||||
timer?.({
|
||||
...commonLabels,
|
||||
...evaluateCustomLabels(metricOptions?.requestDuration?.customLabels, c)
|
||||
...evaluateCustomLabels(metricOptions?.requestDuration?.customLabels, c),
|
||||
})
|
||||
|
||||
metrics.requestsTotal?.inc({
|
||||
...commonLabels,
|
||||
...evaluateCustomLabels(metricOptions?.requestsTotal?.customLabels, c)
|
||||
...evaluateCustomLabels(metricOptions?.requestsTotal?.customLabels, c),
|
||||
})
|
||||
}
|
||||
})
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,11 +3,11 @@ import type { CounterConfiguration, HistogramConfiguration, Metric } from 'prom-
|
|||
import { Counter, Histogram, type Registry } from 'prom-client'
|
||||
|
||||
export type MetricOptions = {
|
||||
disabled?: boolean;
|
||||
customLabels?: Record<string, (c: Context) => string>;
|
||||
disabled?: boolean
|
||||
customLabels?: Record<string, (c: Context) => string>
|
||||
} & (
|
||||
({ type: 'counter' } & CounterConfiguration<string>) |
|
||||
({ type: 'histogram' } & HistogramConfiguration<string>)
|
||||
| ({ type: 'counter' } & CounterConfiguration<string>)
|
||||
| ({ type: 'histogram' } & HistogramConfiguration<string>)
|
||||
)
|
||||
|
||||
const standardMetrics = {
|
||||
|
@ -18,7 +18,7 @@ const standardMetrics = {
|
|||
labelNames: ['method', 'status', 'ok', 'route'],
|
||||
// OpenTelemetry recommendation for histogram buckets of http request duration:
|
||||
// https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserverrequestduration
|
||||
buckets: [ 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10 ],
|
||||
buckets: [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10],
|
||||
},
|
||||
requestsTotal: {
|
||||
type: 'counter',
|
||||
|
@ -35,22 +35,25 @@ export type CustomMetricsOptions = {
|
|||
}
|
||||
|
||||
type CreatedMetrics = {
|
||||
[Name in MetricName]: (typeof standardMetrics)[Name]['type'] extends 'counter' ? Counter<string> : Histogram<string>
|
||||
[Name in MetricName]: (typeof standardMetrics)[Name]['type'] extends 'counter'
|
||||
? Counter<string>
|
||||
: Histogram<string>
|
||||
}
|
||||
|
||||
const getMetricConstructor = (type: MetricOptions['type']) => ({
|
||||
counter: Counter,
|
||||
histogram: Histogram,
|
||||
})[type]
|
||||
const getMetricConstructor = (type: MetricOptions['type']) =>
|
||||
({
|
||||
counter: Counter,
|
||||
histogram: Histogram,
|
||||
}[type])
|
||||
|
||||
export const createStandardMetrics = ({
|
||||
registry,
|
||||
prefix = '',
|
||||
customOptions,
|
||||
} : {
|
||||
registry: Registry;
|
||||
prefix?: string;
|
||||
customOptions?: CustomMetricsOptions;
|
||||
}: {
|
||||
registry: Registry
|
||||
prefix?: string
|
||||
customOptions?: CustomMetricsOptions
|
||||
}) => {
|
||||
const createdMetrics: Record<string, Metric> = {}
|
||||
|
||||
|
@ -70,11 +73,12 @@ export const createStandardMetrics = ({
|
|||
...(opts as object),
|
||||
name: `${prefix}${opts.name}`,
|
||||
help: opts.help,
|
||||
registers: [...opts.registers ?? [], registry],
|
||||
labelNames: [...opts.labelNames ?? [], ...Object.keys(opts.customLabels ?? {})],
|
||||
...(opts.type === 'histogram' && opts.buckets && {
|
||||
buckets: opts.buckets,
|
||||
}),
|
||||
registers: [...(opts.registers ?? []), registry],
|
||||
labelNames: [...(opts.labelNames ?? []), ...Object.keys(opts.customLabels ?? {})],
|
||||
...(opts.type === 'histogram' &&
|
||||
opts.buckets && {
|
||||
buckets: opts.buckets,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -42,7 +42,9 @@ export const sentry = (
|
|||
...options,
|
||||
})
|
||||
c.set('sentry', sentry)
|
||||
if (callback) callback(sentry)
|
||||
if (callback) {
|
||||
callback(sentry)
|
||||
}
|
||||
|
||||
await next()
|
||||
if (c.error) {
|
||||
|
|
|
@ -58,7 +58,9 @@ export const renderSwaggerUIOptions = (options: DistSwaggerUIOptions) => {
|
|||
return `${key}: '${v}'`
|
||||
}
|
||||
if (RENDER_TYPE_MAP[key] === RENDER_TYPE.STRING_ARRAY) {
|
||||
if (!Array.isArray(v)) return ''
|
||||
if (!Array.isArray(v)) {
|
||||
return ''
|
||||
}
|
||||
return `${key}: [${v.map((ve) => `${ve}`).join(',')}]`
|
||||
}
|
||||
if (RENDER_TYPE_MAP[key] === RENDER_TYPE.JSON_STRING) {
|
||||
|
|
|
@ -1,39 +1,37 @@
|
|||
/*eslint quotes: ["off", "single"]*/
|
||||
|
||||
import { renderSwaggerUIOptions } from '../src/swagger/renderer'
|
||||
|
||||
describe('SwaggerUIOption Rendering', () => {
|
||||
it('renders correctly with configUrl', () => {
|
||||
it('renders correctly with configUrl', () =>
|
||||
expect(
|
||||
renderSwaggerUIOptions({
|
||||
configUrl: 'https://petstore3.swagger.io/api/v3/openapi.json',
|
||||
})
|
||||
).toEqual('configUrl: \'https://petstore3.swagger.io/api/v3/openapi.json\'')
|
||||
})
|
||||
).toEqual("configUrl: 'https://petstore3.swagger.io/api/v3/openapi.json'"))
|
||||
|
||||
it('renders correctly with presets', () => {
|
||||
it('renders correctly with presets', () =>
|
||||
expect(
|
||||
renderSwaggerUIOptions({
|
||||
presets: ['SwaggerUIBundle.presets.apis', 'SwaggerUIStandalonePreset'],
|
||||
})
|
||||
).toEqual('presets: [SwaggerUIBundle.presets.apis,SwaggerUIStandalonePreset]')
|
||||
})
|
||||
).toEqual('presets: [SwaggerUIBundle.presets.apis,SwaggerUIStandalonePreset]'))
|
||||
|
||||
it('renders correctly with plugins', () => {
|
||||
it('renders correctly with plugins', () =>
|
||||
expect(
|
||||
renderSwaggerUIOptions({
|
||||
plugins: ['SwaggerUIBundle.plugins.DownloadUrl'],
|
||||
})
|
||||
).toEqual('plugins: [SwaggerUIBundle.plugins.DownloadUrl]')
|
||||
})
|
||||
).toEqual('plugins: [SwaggerUIBundle.plugins.DownloadUrl]'))
|
||||
|
||||
it('renders correctly with deepLinking', () => {
|
||||
it('renders correctly with deepLinking', () =>
|
||||
expect(
|
||||
renderSwaggerUIOptions({
|
||||
deepLinking: true,
|
||||
})
|
||||
).toEqual('deepLinking: true')
|
||||
})
|
||||
).toEqual('deepLinking: true'))
|
||||
|
||||
it('renders correctly with spec', () => {
|
||||
it('renders correctly with spec', () =>
|
||||
expect(
|
||||
renderSwaggerUIOptions({
|
||||
spec: {
|
||||
|
@ -51,15 +49,14 @@ describe('SwaggerUIOption Rendering', () => {
|
|||
})
|
||||
).toEqual(
|
||||
'spec: {"openapi":"3.0.0","info":{"title":"Swagger Petstore","version":"1.0.0"},"servers":[{"url":"https://petstore3.swagger.io/api/v3"}]}'
|
||||
)
|
||||
})
|
||||
))
|
||||
|
||||
it('renders correctly with url', () => {
|
||||
expect(
|
||||
renderSwaggerUIOptions({
|
||||
url: 'https://petstore3.swagger.io/api/v3/openapi.json',
|
||||
})
|
||||
).toEqual('url: \'https://petstore3.swagger.io/api/v3/openapi.json\'')
|
||||
).toEqual("url: 'https://petstore3.swagger.io/api/v3/openapi.json'")
|
||||
})
|
||||
|
||||
it('renders correctly with urls', () => {
|
||||
|
@ -77,59 +74,52 @@ describe('SwaggerUIOption Rendering', () => {
|
|||
)
|
||||
})
|
||||
|
||||
it('renders correctly with layout', () => {
|
||||
it('renders correctly with layout', () =>
|
||||
expect(
|
||||
renderSwaggerUIOptions({
|
||||
layout: 'StandaloneLayout',
|
||||
})
|
||||
).toEqual('layout: \'StandaloneLayout\'')
|
||||
})
|
||||
).toEqual("layout: 'StandaloneLayout'"))
|
||||
|
||||
it('renders correctly with docExpansion', () => {
|
||||
it('renders correctly with docExpansion', () =>
|
||||
expect(
|
||||
renderSwaggerUIOptions({
|
||||
docExpansion: 'list',
|
||||
})
|
||||
).toEqual('docExpansion: \'list\'')
|
||||
})
|
||||
).toEqual("docExpansion: 'list'"))
|
||||
|
||||
it('renders correctly with maxDisplayedTags', () => {
|
||||
it('renders correctly with maxDisplayedTags', () =>
|
||||
expect(
|
||||
renderSwaggerUIOptions({
|
||||
maxDisplayedTags: 5,
|
||||
})
|
||||
).toEqual('maxDisplayedTags: 5')
|
||||
})
|
||||
).toEqual('maxDisplayedTags: 5'))
|
||||
|
||||
it('renders correctly with operationsSorter', () => {
|
||||
it('renders correctly with operationsSorter', () =>
|
||||
expect(
|
||||
renderSwaggerUIOptions({
|
||||
operationsSorter: '(a, b) => a.path.localeCompare(b.path)',
|
||||
})
|
||||
).toEqual('operationsSorter: (a, b) => a.path.localeCompare(b.path)')
|
||||
})
|
||||
).toEqual('operationsSorter: (a, b) => a.path.localeCompare(b.path)'))
|
||||
|
||||
it('renders correctly with requestInterceptor', () => {
|
||||
it('renders correctly with requestInterceptor', () =>
|
||||
expect(
|
||||
renderSwaggerUIOptions({
|
||||
requestInterceptor: '(req) => req',
|
||||
})
|
||||
).toEqual('requestInterceptor: (req) => req')
|
||||
})
|
||||
).toEqual('requestInterceptor: (req) => req'))
|
||||
|
||||
it('renders correctly with responseInterceptor', () => {
|
||||
it('renders correctly with responseInterceptor', () =>
|
||||
expect(
|
||||
renderSwaggerUIOptions({
|
||||
responseInterceptor: '(res) => res',
|
||||
})
|
||||
).toEqual('responseInterceptor: (res) => res')
|
||||
})
|
||||
).toEqual('responseInterceptor: (res) => res'))
|
||||
|
||||
it('renders correctly with persistAuthorization', () => {
|
||||
it('renders correctly with persistAuthorization', () =>
|
||||
expect(
|
||||
renderSwaggerUIOptions({
|
||||
persistAuthorization: true,
|
||||
})
|
||||
).toEqual('persistAuthorization: true')
|
||||
})
|
||||
).toEqual('persistAuthorization: true'))
|
||||
})
|
||||
|
|
Binary file not shown.
|
@ -41,16 +41,18 @@
|
|||
"zod": "3.*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240117.0",
|
||||
"hono": "^3.11.7",
|
||||
"jest": "^29.7.0",
|
||||
"openapi3-ts": "^4.1.2",
|
||||
"tsup": "^8.0.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vitest": "^1.0.1",
|
||||
"zod": "^3.22.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@asteasolutions/zod-to-openapi": "^5.5.0",
|
||||
"@hono/zod-validator": "^0.1.11"
|
||||
"@hono/zod-validator": "0.1.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
|
|
File diff suppressed because it is too large
Load Diff
13
yarn.lock
13
yarn.lock
|
@ -876,6 +876,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@cloudflare/workers-types@npm:^4.20240117.0":
|
||||
version: 4.20240117.0
|
||||
resolution: "@cloudflare/workers-types@npm:4.20240117.0"
|
||||
checksum: 900b796af2ae97257e1f6171b9c37d718c5f8ae064cea8f8d1c48e52ccde01492c5cfa61716fc703d05a6bd92e50a55f7d9e525d2c91919db76e42458c3e8e76
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@colors/colors@npm:1.5.0":
|
||||
version: 1.5.0
|
||||
resolution: "@colors/colors@npm:1.5.0"
|
||||
|
@ -1688,11 +1695,13 @@ __metadata:
|
|||
resolution: "@hono/zod-openapi@workspace:packages/zod-openapi"
|
||||
dependencies:
|
||||
"@asteasolutions/zod-to-openapi": "npm:^5.5.0"
|
||||
"@hono/zod-validator": "npm:^0.1.11"
|
||||
"@cloudflare/workers-types": "npm:^4.20240117.0"
|
||||
"@hono/zod-validator": "npm:0.1.11"
|
||||
hono: "npm:^3.11.7"
|
||||
jest: "npm:^29.7.0"
|
||||
openapi3-ts: "npm:^4.1.2"
|
||||
tsup: "npm:^8.0.1"
|
||||
typescript: "npm:^5.3.3"
|
||||
vitest: "npm:^1.0.1"
|
||||
zod: "npm:^3.22.1"
|
||||
peerDependencies:
|
||||
|
@ -1701,7 +1710,7 @@ __metadata:
|
|||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@hono/zod-validator@npm:^0.1.11, @hono/zod-validator@workspace:packages/zod-validator":
|
||||
"@hono/zod-validator@npm:0.1.11, @hono/zod-validator@workspace:packages/zod-validator":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@hono/zod-validator@workspace:packages/zod-validator"
|
||||
dependencies:
|
||||
|
|
Loading…
Reference in New Issue