chore: use the latest eslint and `@hono/eslint-config` (#904)

* chore: use the latest eslint and `@hono/eslint-config`

* update codes
pull/905/head
Yusuke Wada 2024-12-25 18:08:43 +09:00 committed by GitHub
parent 755d5cb84d
commit 7a401b0850
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
56 changed files with 675 additions and 428 deletions

View File

@ -1 +0,0 @@
dist

View File

@ -1,3 +0,0 @@
module.exports = {
extends: ['./packages/eslint-config/index.js'],
}

View File

@ -0,0 +1,8 @@
import baseConfig from './packages/eslint-config/index.js'
export default [
...baseConfig,
{
ignores: ['**/dist/*'],
},
]

View File

@ -61,9 +61,7 @@
"@types/node": "^20.10.4", "@types/node": "^20.10.4",
"@typescript-eslint/eslint-plugin": "^8.7.0", "@typescript-eslint/eslint-plugin": "^8.7.0",
"@typescript-eslint/parser": "^8.7.0", "@typescript-eslint/parser": "^8.7.0",
"eslint": "^8.57.0", "eslint": "^9.17.0",
"eslint-plugin-import-x": "^4.1.1",
"eslint-plugin-n": "^17.10.2",
"jest": "^29.5.0", "jest": "^29.5.0",
"jest-environment-miniflare": "^2.14.1", "jest-environment-miniflare": "^2.14.1",
"npm-run-all2": "^6.2.2", "npm-run-all2": "^6.2.2",

View File

@ -1,11 +1,12 @@
import type { Context, Env, MiddlewareHandler, ValidationTargets } from 'hono'; import { Ajv } from 'ajv'
import { validator } from 'hono/validator'; import type { JSONSchemaType, ErrorObject } from 'ajv'
import { Ajv, type JSONSchemaType, type ErrorObject } from 'ajv'; import type { Context, Env, MiddlewareHandler, ValidationTargets } from 'hono'
import { validator } from 'hono/validator'
type Hook<T, E extends Env, P extends string> = ( type Hook<T, E extends Env, P extends string> = (
result: { success: true; data: T } | { success: false; errors: ErrorObject[] }, result: { success: true; data: T } | { success: false; errors: ErrorObject[] },
c: Context<E, P> c: Context<E, P>
) => Response | Promise<Response> | void; ) => Response | Promise<Response> | void
/** /**
* Hono middleware that validates incoming data via an Ajv JSON schema. * Hono middleware that validates incoming data via an Ajv JSON schema.
@ -75,32 +76,32 @@ export function ajvValidator<
E, E,
P, P,
{ {
in: { [K in Target]: T }; in: { [K in Target]: T }
out: { [K in Target]: T }; out: { [K in Target]: T }
} }
> { > {
const ajv = new Ajv(); const ajv = new Ajv()
const validate = ajv.compile(schema); const validate = ajv.compile(schema)
return validator(target, (data, c) => { return validator(target, (data, c) => {
const valid = validate(data); const valid = validate(data)
if (valid) { if (valid) {
if (hook) { if (hook) {
const hookResult = hook({ success: true, data: data as T }, c); const hookResult = hook({ success: true, data: data as T }, c)
if (hookResult instanceof Response || hookResult instanceof Promise) { if (hookResult instanceof Response || hookResult instanceof Promise) {
return hookResult; return hookResult
} }
} }
return data as T; return data as T
} }
const errors = validate.errors || []; const errors = validate.errors || []
if (hook) { if (hook) {
const hookResult = hook({ success: false, errors }, c); const hookResult = hook({ success: false, errors }, c)
if (hookResult instanceof Response || hookResult instanceof Promise) { if (hookResult instanceof Response || hookResult instanceof Promise) {
return hookResult; return hookResult
} }
} }
return c.json({ success: false, errors }, 400); return c.json({ success: false, errors }, 400)
}); })
} }

View File

@ -1,12 +1,12 @@
import { Hono } from 'hono'; import type { JSONSchemaType, type ErrorObject } from 'ajv'
import type { Equal, Expect } from 'hono/utils/types'; import { Hono } from 'hono'
import { ajvValidator } from '../src'; import type { Equal, Expect } from 'hono/utils/types'
import { JSONSchemaType, type ErrorObject } from 'ajv'; import { ajvValidator } from '../src'
type ExtractSchema<T> = T extends Hono<infer _, infer S> ? S : never; type ExtractSchema<T> = T extends Hono<infer _, infer S> ? S : never
describe('Basic', () => { describe('Basic', () => {
const app = new Hono(); const app = new Hono()
const schema: JSONSchemaType<{ name: string; age: number }> = { const schema: JSONSchemaType<{ name: string; age: number }> = {
type: 'object', type: 'object',
@ -16,35 +16,35 @@ describe('Basic', () => {
}, },
required: ['name', 'age'], required: ['name', 'age'],
additionalProperties: false, additionalProperties: false,
}; }
const route = app.post('/author', ajvValidator('json', schema), (c) => { const route = app.post('/author', ajvValidator('json', schema), (c) => {
const data = c.req.valid('json'); const data = c.req.valid('json')
return c.json({ return c.json({
success: true, success: true,
message: `${data.name} is ${data.age}`, message: `${data.name} is ${data.age}`,
}); })
}); })
type Actual = ExtractSchema<typeof route>; type Actual = ExtractSchema<typeof route>
type Expected = { type Expected = {
'/author': { '/author': {
$post: { $post: {
input: { input: {
json: { json: {
name: string; name: string
age: number; age: number
}; }
}; }
output: { output: {
success: boolean; success: boolean
message: string; message: string
}; }
}; }
}; }
}; }
type verify = Expect<Equal<Expected, Actual>>; type verify = Expect<Equal<Expected, Actual>>
it('Should return 200 response', async () => { it('Should return 200 response', async () => {
const req = new Request('http://localhost/author', { const req = new Request('http://localhost/author', {
@ -56,15 +56,15 @@ describe('Basic', () => {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
}); })
const res = await app.request(req); const res = await app.request(req)
expect(res).not.toBeNull(); expect(res).not.toBeNull()
expect(res.status).toBe(200); expect(res.status).toBe(200)
expect(await res.json()).toEqual({ expect(await res.json()).toEqual({
success: true, success: true,
message: 'Superman is 20', message: 'Superman is 20',
}); })
}); })
it('Should return 400 response', async () => { it('Should return 400 response', async () => {
const req = new Request('http://localhost/author', { const req = new Request('http://localhost/author', {
@ -76,17 +76,17 @@ describe('Basic', () => {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
}); })
const res = await app.request(req); const res = await app.request(req)
expect(res).not.toBeNull(); expect(res).not.toBeNull()
expect(res.status).toBe(400); expect(res.status).toBe(400)
const data = (await res.json()) as { success: boolean }; const data = (await res.json()) as { success: boolean }
expect(data.success).toBe(false); expect(data.success).toBe(false)
}); })
}); })
describe('With Hook', () => { describe('With Hook', () => {
const app = new Hono(); const app = new Hono()
const schema: JSONSchemaType<{ id: number; title: string }> = { const schema: JSONSchemaType<{ id: number; title: string }> = {
type: 'object', type: 'object',
@ -96,39 +96,39 @@ describe('With Hook', () => {
}, },
required: ['id', 'title'], required: ['id', 'title'],
additionalProperties: false, additionalProperties: false,
}; }
app app
.post( .post(
'/post', '/post',
ajvValidator('json', schema, (result, c) => { ajvValidator('json', schema, (result, c) => {
if (!result.success) { if (!result.success) {
return c.text('Invalid!', 400); return c.text('Invalid!', 400)
} }
const data = result.data; const data = result.data
return c.text(`${data.id} is valid!`); return c.text(`${data.id} is valid!`)
}), }),
(c) => { (c) => {
const data = c.req.valid('json'); const data = c.req.valid('json')
return c.json({ return c.json({
success: true, success: true,
message: `${data.id} is ${data.title}`, message: `${data.id} is ${data.title}`,
}); })
} }
) )
.post( .post(
'/errorTest', '/errorTest',
ajvValidator('json', schema, (result, c) => { ajvValidator('json', schema, (result, c) => {
return c.json(result, 400); return c.json(result, 400)
}), }),
(c) => { (c) => {
const data = c.req.valid('json'); const data = c.req.valid('json')
return c.json({ return c.json({
success: true, success: true,
message: `${data.id} is ${data.title}`, message: `${data.id} is ${data.title}`,
}); })
} }
); )
it('Should return 200 response', async () => { it('Should return 200 response', async () => {
const req = new Request('http://localhost/post', { const req = new Request('http://localhost/post', {
@ -140,12 +140,12 @@ describe('With Hook', () => {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
}); })
const res = await app.request(req); const res = await app.request(req)
expect(res).not.toBeNull(); expect(res).not.toBeNull()
expect(res.status).toBe(200); expect(res.status).toBe(200)
expect(await res.text()).toBe('123 is valid!'); expect(await res.text()).toBe('123 is valid!')
}); })
it('Should return 400 response', async () => { it('Should return 400 response', async () => {
const req = new Request('http://localhost/post', { const req = new Request('http://localhost/post', {
@ -157,11 +157,11 @@ describe('With Hook', () => {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
}); })
const res = await app.request(req); const res = await app.request(req)
expect(res).not.toBeNull(); expect(res).not.toBeNull()
expect(res.status).toBe(400); expect(res.status).toBe(400)
}); })
it('Should return 400 response and error array', async () => { it('Should return 400 response and error array', async () => {
const req = new Request('http://localhost/errorTest', { const req = new Request('http://localhost/errorTest', {
@ -172,17 +172,17 @@ describe('With Hook', () => {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
}); })
const res = await app.request(req); const res = await app.request(req)
expect(res).not.toBeNull(); expect(res).not.toBeNull()
expect(res.status).toBe(400); expect(res.status).toBe(400)
const { errors, success } = (await res.json()) as { const { errors, success } = (await res.json()) as {
success: boolean; success: boolean
errors: ErrorObject[]; errors: ErrorObject[]
}; }
expect(success).toBe(false); expect(success).toBe(false)
expect(Array.isArray(errors)).toBe(true); expect(Array.isArray(errors)).toBe(true)
expect( expect(
errors.map((e: ErrorObject) => ({ errors.map((e: ErrorObject) => ({
keyword: e.keyword, keyword: e.keyword,
@ -195,6 +195,6 @@ describe('With Hook', () => {
instancePath: '', instancePath: '',
message: "must have required property 'title'", message: "must have required property 'title'",
}, },
]); ])
}); })
}); })

View File

@ -1,4 +1,5 @@
import { type, type Type, type ArkErrors } from 'arktype' import { type } from 'arktype'
import type { Type, ArkErrors } from 'arktype'
import type { Context, MiddlewareHandler, Env, ValidationTargets, TypedResponse } from 'hono' import type { Context, MiddlewareHandler, Env, ValidationTargets, TypedResponse } from 'hono'
import { validator } from 'hono/validator' import { validator } from 'hono/validator'

View File

@ -163,8 +163,7 @@ export function now() {
export function parseUrl(url?: string) { export function parseUrl(url?: string) {
const defaultUrl = 'http://localhost:3000/api/auth' const defaultUrl = 'http://localhost:3000/api/auth'
const parsedUrl = new URL(url?.startsWith('http') ? url : `https://${url}` || defaultUrl) const parsedUrl = new URL(url ? (url.startsWith('http') ? url : `https://${url}`) : defaultUrl)
const path = parsedUrl.pathname === '/' ? '/api/auth' : parsedUrl.pathname.replace(/\/$/, '') const path = parsedUrl.pathname === '/' ? '/api/auth' : parsedUrl.pathname.replace(/\/$/, '')
const base = `${parsedUrl.origin}${path}` const base = `${parsedUrl.origin}${path}`

View File

@ -1,12 +1,11 @@
import type { AuthConfig as AuthConfigCore } from '@auth/core' import type { AuthConfig as AuthConfigCore } from '@auth/core'
import { Auth } from '@auth/core' import { Auth, setEnvDefaults as coreSetEnvDefaults } from '@auth/core'
import type { AdapterUser } from '@auth/core/adapters' import type { AdapterUser } from '@auth/core/adapters'
import type { JWT } from '@auth/core/jwt' import type { JWT } from '@auth/core/jwt'
import type { Session } from '@auth/core/types' import type { Session } from '@auth/core/types'
import type { Context, MiddlewareHandler } from 'hono' import type { Context, MiddlewareHandler } from 'hono'
import { env } from 'hono/adapter' import { env } from 'hono/adapter'
import { HTTPException } from 'hono/http-exception' import { HTTPException } from 'hono/http-exception'
import { setEnvDefaults as coreSetEnvDefaults } from '@auth/core'
declare module 'hono' { declare module 'hono' {
interface ContextVariableMap { interface ContextVariableMap {
@ -43,7 +42,9 @@ export function reqWithEnvUrl(req: Request, authUrl?: string) {
const authUrlObj = new URL(authUrl) const authUrlObj = new URL(authUrl)
const props = ['hostname', 'protocol', 'port', 'password', 'username'] as const const props = ['hostname', 'protocol', 'port', 'password', 'username'] as const
for (const prop of props) { for (const prop of props) {
if (authUrlObj[prop]) reqUrlObj[prop] = authUrlObj[prop] if (authUrlObj[prop]) {
reqUrlObj[prop] = authUrlObj[prop]
}
} }
return new Request(reqUrlObj.href, req) return new Request(reqUrlObj.href, req)
} }
@ -51,12 +52,17 @@ export function reqWithEnvUrl(req: Request, authUrl?: string) {
const newReq = new Request(url.href, req) const newReq = new Request(url.href, req)
const proto = newReq.headers.get('x-forwarded-proto') const proto = newReq.headers.get('x-forwarded-proto')
const host = newReq.headers.get('x-forwarded-host') ?? newReq.headers.get('host') const host = newReq.headers.get('x-forwarded-host') ?? newReq.headers.get('host')
if (proto != null) url.protocol = proto.endsWith(':') ? proto : `${proto}:` if (proto != null) {
url.protocol = proto.endsWith(':') ? proto : `${proto}:`
}
if (host != null) { if (host != null) {
url.host = host url.host = host
const portMatch = host.match(/:(\d+)$/) const portMatch = host.match(/:(\d+)$/)
if (portMatch) url.port = portMatch[1] if (portMatch) {
else url.port = '' url.port = portMatch[1]
} else {
url.port = ''
}
newReq.headers.delete('x-forwarded-host') newReq.headers.delete('x-forwarded-host')
newReq.headers.delete('Host') newReq.headers.delete('Host')
newReq.headers.set('Host', host) newReq.headers.set('Host', host)

View File

@ -1,28 +1,24 @@
import type { BuiltInProviderType, RedirectableProviderType } from '@auth/core/providers'
import type { LoggerInstance, Session } from '@auth/core/types'
import * as React from 'react' import * as React from 'react'
import { import { useCallback, useContext, useEffect, useMemo, useState } from 'react'
type AuthClientConfig, import { ClientSessionError, fetchData, now, parseUrl, useOnline } from './client'
ClientSessionError, import type {
fetchData,
now,
parseUrl,
useOnline,
type SessionContextValue,
type SessionProviderProps,
type GetSessionParams,
type UseSessionOptions,
type LiteralUnion,
type SignInOptions,
type SignInAuthorizationParams,
type SignInResponse,
type ClientSafeProvider,
type SignOutParams,
type SignOutResponse,
WindowProps, WindowProps,
AuthState, AuthState,
AuthClientConfig,
SessionContextValue,
SessionProviderProps,
GetSessionParams,
UseSessionOptions,
LiteralUnion,
SignInOptions,
SignInAuthorizationParams,
SignInResponse,
ClientSafeProvider,
SignOutParams,
SignOutResponse,
} from './client' } from './client'
import type { LoggerInstance, Session } from '@auth/core/types'
import { useCallback, useContext, useEffect, useMemo, useState } from 'react'
import type { BuiltInProviderType, RedirectableProviderType } from '@auth/core/providers'
const logger: LoggerInstance = { const logger: LoggerInstance = {
debug: console.debug, debug: console.debug,
@ -431,7 +427,9 @@ export const useOauthPopupLogin = (
useEffect(() => { useEffect(() => {
const handleMessage = (event: MessageEvent<AuthState>) => { const handleMessage = (event: MessageEvent<AuthState>) => {
if (event.origin !== window.location.origin) return if (event.origin !== window.location.origin) {
return
}
if (event.data.status) { if (event.data.status) {
setState(event.data) setState(event.data)
if (event.data.status === 'success') { if (event.data.status === 'success') {

View File

@ -1,9 +1,9 @@
import { webcrypto } from 'node:crypto'
import { skipCSRFCheck } from '@auth/core' import { skipCSRFCheck } from '@auth/core'
import type { Adapter } from '@auth/core/adapters' import type { Adapter } from '@auth/core/adapters'
import Credentials from '@auth/core/providers/credentials' import Credentials from '@auth/core/providers/credentials'
import { Hono } from 'hono' import { Hono } from 'hono'
import { describe, expect, it, vi } from 'vitest' import { describe, expect, it, vi } from 'vitest'
import { webcrypto } from 'node:crypto'
import type { AuthConfig } from '../src' import type { AuthConfig } from '../src'
import { authHandler, verifyAuth, initAuthConfig, reqWithEnvUrl } from '../src' import { authHandler, verifyAuth, initAuthConfig, reqWithEnvUrl } from '../src'

View File

@ -1,6 +1,6 @@
import { decode } from 'hono/jwt'
import type { Enforcer } from 'casbin' import type { Enforcer } from 'casbin'
import type { Context } from 'hono' import type { Context } from 'hono'
import { decode } from 'hono/jwt'
import type { JWTPayload } from 'hono/utils/jwt/types' import type { JWTPayload } from 'hono/utils/jwt/types'
export const jwtAuthorizer = async ( export const jwtAuthorizer = async (
@ -14,10 +14,14 @@ export const jwtAuthorizer = async (
if (!payload) { if (!payload) {
const credentials = c.req.header('Authorization') const credentials = c.req.header('Authorization')
if (!credentials) return false if (!credentials) {
return false
}
const parts = credentials.split(/\s+/) const parts = credentials.split(/\s+/)
if (parts.length !== 2 || parts[0] !== 'Bearer') return false if (parts.length !== 2 || parts[0] !== 'Bearer') {
return false
}
const token = parts[1] const token = parts[1]

View File

@ -1,5 +1,5 @@
import { Enforcer } from 'casbin' import { Enforcer } from 'casbin'
import { type Context, MiddlewareHandler } from 'hono' import type { MiddlewareHandler, type Context } from 'hono'
interface CasbinOptions { interface CasbinOptions {
newEnforcer: Promise<Enforcer> newEnforcer: Promise<Enforcer>

View File

@ -1,10 +1,10 @@
import { describe, it, expect } from 'vitest'
import { Hono } from 'hono'
import { newEnforcer } from 'casbin' import { newEnforcer } from 'casbin'
import { Hono } from 'hono'
import { basicAuth } from 'hono/basic-auth'
import { jwt, sign } from 'hono/jwt'
import { describe, it, expect } from 'vitest'
import { casbin } from '../src' import { casbin } from '../src'
import { basicAuthorizer, jwtAuthorizer } from '../src/helper' import { basicAuthorizer, jwtAuthorizer } from '../src/helper'
import { jwt, sign } from 'hono/jwt'
import { basicAuth } from 'hono/basic-auth'
describe('Casbin Middleware Tests', () => { describe('Casbin Middleware Tests', () => {
describe('BasicAuthorizer', () => { describe('BasicAuthorizer', () => {

View File

@ -1,8 +1,10 @@
import 'reflect-metadata' import 'reflect-metadata'
import type { ClassConstructor, ClassTransformOptions } from 'class-transformer'
import { plainToClass } from 'class-transformer'
import type { ValidationError } from 'class-validator'
import { validate } from 'class-validator'
import type { Context, Env, Input, MiddlewareHandler, TypedResponse, ValidationTargets } from 'hono'
import { validator } from 'hono/validator' import { validator } from 'hono/validator'
import { ClassConstructor, ClassTransformOptions, plainToClass } from 'class-transformer'
import { Context, Env, Input, MiddlewareHandler, TypedResponse, ValidationTargets } from 'hono'
import { ValidationError, validate } from 'class-validator'
/** /**
* Hono middleware that validates incoming data using class-validator(https://github.com/typestack/class-validator). * Hono middleware that validates incoming data using class-validator(https://github.com/typestack/class-validator).

View File

@ -1,9 +1,10 @@
import { Hono } from 'hono'
import { classValidator } from '../src'
import type { Equal, Expect } from 'hono/utils/types'
import { IsInt, IsString, ValidateNested, ValidationError } from 'class-validator'
import { ExtractSchema } from 'hono/types'
import { Type } from 'class-transformer' import { Type } from 'class-transformer'
import type { ValidationError } from 'class-validator'
import { IsInt, IsString, ValidateNested } from 'class-validator'
import { Hono } from 'hono'
import type { ExtractSchema } from 'hono/types'
import type { Equal, Expect } from 'hono/utils/types'
import { classValidator } from '../src'
describe('Basic', () => { describe('Basic', () => {
const app = new Hono() const app = new Hono()

View File

@ -1,4 +1,5 @@
import { type ClerkClient, type ClerkOptions, createClerkClient } from '@clerk/backend' import { createClerkClient } from '@clerk/backend'
import type { ClerkClient, ClerkOptions } from '@clerk/backend'
import type { Context, MiddlewareHandler } from 'hono' import type { Context, MiddlewareHandler } from 'hono'
import { env } from 'hono/adapter' import { env } from 'hono/adapter'

View File

@ -1,24 +1,24 @@
import { Hono } from 'hono' import { Hono } from 'hono'
import { cloudflareAccess } from '../src'
import { describe, expect, it, vi } from 'vitest' import { describe, expect, it, vi } from 'vitest'
import crypto from 'crypto'; import crypto from 'crypto'
import { promisify } from 'util'; import { promisify } from 'util'
import { cloudflareAccess } from '../src'
const generateKeyPair = promisify(crypto.generateKeyPair); const generateKeyPair = promisify(crypto.generateKeyPair)
interface KeyPairResult { interface KeyPairResult {
publicKey: string; publicKey: string
privateKey: string; privateKey: string
} }
interface JWK { interface JWK {
kid: string; kid: string
kty: string; kty: string
alg: string; alg: string
use: string; use: string
e: string; e: string
n: string; n: string
} }
async function generateJWTKeyPair(): Promise<KeyPairResult> { async function generateJWTKeyPair(): Promise<KeyPairResult> {
@ -27,38 +27,38 @@ async function generateJWTKeyPair(): Promise<KeyPairResult> {
modulusLength: 2048, modulusLength: 2048,
publicKeyEncoding: { publicKeyEncoding: {
type: 'spki', type: 'spki',
format: 'pem' format: 'pem',
}, },
privateKeyEncoding: { privateKeyEncoding: {
type: 'pkcs8', type: 'pkcs8',
format: 'pem' format: 'pem',
} },
}); })
return { return {
publicKey, publicKey,
privateKey privateKey,
}; }
} catch (error) { } catch (error) {
throw new Error(`Failed to generate key pair: ${(error as Error).message}`); throw new Error(`Failed to generate key pair: ${(error as Error).message}`)
} }
} }
function generateKeyThumbprint(modulusBase64: string): string { function generateKeyThumbprint(modulusBase64: string): string {
const hash = crypto.createHash('sha256'); const hash = crypto.createHash('sha256')
hash.update(Buffer.from(modulusBase64, 'base64')); hash.update(Buffer.from(modulusBase64, 'base64'))
return hash.digest('hex'); return hash.digest('hex')
} }
function publicKeyToJWK(publicKey: string): JWK { function publicKeyToJWK(publicKey: string): JWK {
// Convert PEM to key object // Convert PEM to key object
const keyObject = crypto.createPublicKey(publicKey); const keyObject = crypto.createPublicKey(publicKey)
// Export the key in JWK format // Export the key in JWK format
const jwk = keyObject.export({ format: 'jwk' }); const jwk = keyObject.export({ format: 'jwk' })
// Generate key ID using the modulus // Generate key ID using the modulus
const kid = generateKeyThumbprint(jwk.n as string); const kid = generateKeyThumbprint(jwk.n as string)
return { return {
kid, kid,
@ -67,66 +67,65 @@ function publicKeyToJWK(publicKey: string): JWK {
use: 'sig', use: 'sig',
e: jwk.e as string, e: jwk.e as string,
n: jwk.n as string, n: jwk.n as string,
};
} }
}
function base64URLEncode(str: string): string { function base64URLEncode(str: string): string {
return Buffer.from(str) return Buffer.from(str)
.toString('base64') .toString('base64')
.replace(/\+/g, '-') .replace(/\+/g, '-')
.replace(/\//g, '_') .replace(/\//g, '_')
.replace(/=/g, ''); .replace(/=/g, '')
} }
function generateJWT(privateKey: string, payload: Record<string, any>, expiresIn: number = 3600): string { function generateJWT(
privateKey: string,
payload: Record<string, any>,
expiresIn: number = 3600
): string {
// Create header // Create header
const header = { const header = {
alg: 'RS256', alg: 'RS256',
typ: 'JWT' typ: 'JWT',
}; }
// Add expiration to payload // Add expiration to payload
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000)
const fullPayload = { const fullPayload = {
...payload, ...payload,
iat: now, iat: now,
exp: now + expiresIn exp: now + expiresIn,
};
// Encode header and payload
const encodedHeader = base64URLEncode(JSON.stringify(header));
const encodedPayload = base64URLEncode(JSON.stringify(fullPayload));
// Create signature
const signatureInput = `${encodedHeader}.${encodedPayload}`;
const signer = crypto.createSign('RSA-SHA256');
signer.update(signatureInput);
const signature = signer.sign(privateKey);
// @ts-ignore
const encodedSignature = base64URLEncode(signature);
// Combine all parts
return `${encodedHeader}.${encodedPayload}.${encodedSignature}`;
} }
// Encode header and payload
const encodedHeader = base64URLEncode(JSON.stringify(header))
const encodedPayload = base64URLEncode(JSON.stringify(fullPayload))
// Create signature
const signatureInput = `${encodedHeader}.${encodedPayload}`
const signer = crypto.createSign('RSA-SHA256')
signer.update(signatureInput)
const signature = signer.sign(privateKey)
// @ts-expect-error signature is not typed correctly
const encodedSignature = base64URLEncode(signature)
// Combine all parts
return `${encodedHeader}.${encodedPayload}.${encodedSignature}`
}
describe('Cloudflare Access middleware', async () => { describe('Cloudflare Access middleware', async () => {
const keyPair1 = await generateJWTKeyPair(); const keyPair1 = await generateJWTKeyPair()
const keyPair2 = await generateJWTKeyPair(); const keyPair2 = await generateJWTKeyPair()
const keyPair3 = await generateJWTKeyPair(); const keyPair3 = await generateJWTKeyPair()
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks()
vi.stubGlobal('fetch', async () => { vi.stubGlobal('fetch', async () => {
return Response.json({ return Response.json({
keys: [ keys: [publicKeyToJWK(keyPair1.publicKey), publicKeyToJWK(keyPair2.publicKey)],
publicKeyToJWK(keyPair1.publicKey), })
publicKeyToJWK(keyPair2.publicKey),
],
}) })
}) })
});
const app = new Hono() const app = new Hono()
@ -135,9 +134,12 @@ describe('Cloudflare Access middleware', async () => {
app.get('/access-payload', (c) => c.json(c.get('accessPayload'))) app.get('/access-payload', (c) => c.json(c.get('accessPayload')))
app.onError((err, c) => { app.onError((err, c) => {
return c.json({ return c.json(
{
err: err.toString(), err: err.toString(),
}, 500) },
500
)
}) })
it('Should be throw Missing bearer token when nothing is sent', async () => { it('Should be throw Missing bearer token when nothing is sent', async () => {
@ -150,8 +152,8 @@ describe('Cloudflare Access middleware', async () => {
it('Should be throw Unable to decode Bearer token when sending garbage', async () => { it('Should be throw Unable to decode Bearer token when sending garbage', async () => {
const res = await app.request('http://localhost/hello-behind-access', { const res = await app.request('http://localhost/hello-behind-access', {
headers: { headers: {
'cf-access-jwt-assertion': 'asdasdasda' 'cf-access-jwt-assertion': 'asdasdasda',
} },
}) })
expect(res).not.toBeNull() expect(res).not.toBeNull()
expect(res.status).toBe(401) expect(res.status).toBe(401)
@ -159,14 +161,18 @@ describe('Cloudflare Access middleware', async () => {
}) })
it('Should be throw Token is expired when sending expired token', async () => { it('Should be throw Token is expired when sending expired token', async () => {
const token = generateJWT(keyPair1.privateKey, { const token = generateJWT(
keyPair1.privateKey,
{
sub: '1234567890', sub: '1234567890',
}, -3600); },
-3600
)
const res = await app.request('http://localhost/hello-behind-access', { const res = await app.request('http://localhost/hello-behind-access', {
headers: { headers: {
'cf-access-jwt-assertion': token 'cf-access-jwt-assertion': token,
} },
}) })
expect(res).not.toBeNull() expect(res).not.toBeNull()
expect(res.status).toBe(401) expect(res.status).toBe(401)
@ -177,28 +183,30 @@ describe('Cloudflare Access middleware', async () => {
const token = generateJWT(keyPair1.privateKey, { const token = generateJWT(keyPair1.privateKey, {
sub: '1234567890', sub: '1234567890',
iss: 'https://different-team.cloudflareaccess.com', iss: 'https://different-team.cloudflareaccess.com',
}); })
const res = await app.request('http://localhost/hello-behind-access', { const res = await app.request('http://localhost/hello-behind-access', {
headers: { headers: {
'cf-access-jwt-assertion': token 'cf-access-jwt-assertion': token,
} },
}) })
expect(res).not.toBeNull() expect(res).not.toBeNull()
expect(res.status).toBe(401) expect(res.status).toBe(401)
expect(await res.text()).toBe('Authentication error: Expected team name https://my-cool-team-name.cloudflareaccess.com, but received https://different-team.cloudflareaccess.com') expect(await res.text()).toBe(
'Authentication error: Expected team name https://my-cool-team-name.cloudflareaccess.com, but received https://different-team.cloudflareaccess.com'
)
}) })
it('Should be throw Invalid token when sending token signed with private key not in the allowed list', async () => { it('Should be throw Invalid token when sending token signed with private key not in the allowed list', async () => {
const token = generateJWT(keyPair3.privateKey, { const token = generateJWT(keyPair3.privateKey, {
sub: '1234567890', sub: '1234567890',
iss: 'https://my-cool-team-name.cloudflareaccess.com', iss: 'https://my-cool-team-name.cloudflareaccess.com',
}); })
const res = await app.request('http://localhost/hello-behind-access', { const res = await app.request('http://localhost/hello-behind-access', {
headers: { headers: {
'cf-access-jwt-assertion': token 'cf-access-jwt-assertion': token,
} },
}) })
expect(res).not.toBeNull() expect(res).not.toBeNull()
expect(res.status).toBe(401) expect(res.status).toBe(401)
@ -209,12 +217,12 @@ describe('Cloudflare Access middleware', async () => {
const token = generateJWT(keyPair1.privateKey, { const token = generateJWT(keyPair1.privateKey, {
sub: '1234567890', sub: '1234567890',
iss: 'https://my-cool-team-name.cloudflareaccess.com', iss: 'https://my-cool-team-name.cloudflareaccess.com',
}); })
const res = await app.request('http://localhost/hello-behind-access', { const res = await app.request('http://localhost/hello-behind-access', {
headers: { headers: {
'cf-access-jwt-assertion': token 'cf-access-jwt-assertion': token,
} },
}) })
expect(res).not.toBeNull() expect(res).not.toBeNull()
expect(res.status).toBe(200) expect(res.status).toBe(200)
@ -225,12 +233,12 @@ describe('Cloudflare Access middleware', async () => {
const token = generateJWT(keyPair2.privateKey, { const token = generateJWT(keyPair2.privateKey, {
sub: '1234567890', sub: '1234567890',
iss: 'https://my-cool-team-name.cloudflareaccess.com', iss: 'https://my-cool-team-name.cloudflareaccess.com',
}); })
const res = await app.request('http://localhost/hello-behind-access', { const res = await app.request('http://localhost/hello-behind-access', {
headers: { headers: {
'cf-access-jwt-assertion': token 'cf-access-jwt-assertion': token,
} },
}) })
expect(res).not.toBeNull() expect(res).not.toBeNull()
expect(res.status).toBe(200) expect(res.status).toBe(200)
@ -241,20 +249,20 @@ describe('Cloudflare Access middleware', async () => {
const token = generateJWT(keyPair1.privateKey, { const token = generateJWT(keyPair1.privateKey, {
sub: '1234567890', sub: '1234567890',
iss: 'https://my-cool-team-name.cloudflareaccess.com', iss: 'https://my-cool-team-name.cloudflareaccess.com',
}); })
const res = await app.request('http://localhost/access-payload', { const res = await app.request('http://localhost/access-payload', {
headers: { headers: {
'cf-access-jwt-assertion': token 'cf-access-jwt-assertion': token,
} },
}) })
expect(res).not.toBeNull() expect(res).not.toBeNull()
expect(res.status).toBe(200) expect(res.status).toBe(200)
expect(await res.json()).toEqual({ expect(await res.json()).toEqual({
"sub":"1234567890", sub: '1234567890',
"iss":"https://my-cool-team-name.cloudflareaccess.com", iss: 'https://my-cool-team-name.cloudflareaccess.com',
"iat":expect.any(Number), iat: expect.any(Number),
"exp":expect.any(Number) exp: expect.any(Number),
}) })
}) })
@ -265,12 +273,14 @@ describe('Cloudflare Access middleware', async () => {
const res = await app.request('http://localhost/hello-behind-access', { const res = await app.request('http://localhost/hello-behind-access', {
headers: { headers: {
'cf-access-jwt-assertion': 'asdads' 'cf-access-jwt-assertion': 'asdads',
} },
}) })
expect(res).not.toBeNull() expect(res).not.toBeNull()
expect(res.status).toBe(500) expect(res.status).toBe(500)
expect(await res.json()).toEqual({"err":"Error: Authentication error: The Access Organization 'my-cool-team-name' does not exist"}) expect(await res.json()).toEqual({
err: "Error: Authentication error: The Access Organization 'my-cool-team-name' does not exist",
})
}) })
it('Should throw an error, if the access certs url is unavailable', async () => { it('Should throw an error, if the access certs url is unavailable', async () => {
@ -280,11 +290,13 @@ describe('Cloudflare Access middleware', async () => {
const res = await app.request('http://localhost/hello-behind-access', { const res = await app.request('http://localhost/hello-behind-access', {
headers: { headers: {
'cf-access-jwt-assertion': 'asdads' 'cf-access-jwt-assertion': 'asdads',
} },
}) })
expect(res).not.toBeNull() expect(res).not.toBeNull()
expect(res.status).toBe(500) expect(res.status).toBe(500)
expect(await res.json()).toEqual({"err":"Error: Authentication error: Received unexpected HTTP code 500 from Cloudflare Access"}) expect(await res.json()).toEqual({
err: 'Error: Authentication error: Received unexpected HTTP code 500 from Cloudflare Access',
})
}) })
}) })

View File

@ -1,18 +1,18 @@
import type { Context } from 'hono'
import { createMiddleware } from 'hono/factory' import { createMiddleware } from 'hono/factory'
import { Context } from 'hono'
import { HTTPException } from 'hono/http-exception' import { HTTPException } from 'hono/http-exception'
export type CloudflareAccessPayload = { export type CloudflareAccessPayload = {
aud: string[], aud: string[]
email: string, email: string
exp: number, exp: number
iat: number, iat: number
nbf: number, nbf: number
iss: string, iss: string
type: string, type: string
identity_nonce: string, identity_nonce: string
sub: string, sub: string
country: string, country: string
} }
export type CloudflareAccessVariables = { export type CloudflareAccessVariables = {
@ -39,7 +39,9 @@ export const cloudflareAccess = (accessTeamName: string) => {
return createMiddleware(async (c, next) => { return createMiddleware(async (c, next) => {
const encodedToken = getJwt(c) const encodedToken = getJwt(c)
if (encodedToken === null) return c.text('Authentication error: Missing bearer token', 401) if (encodedToken === null) {
return c.text('Authentication error: Missing bearer token', 401)
}
// Load jwt keys if they are not in memory or already expired // Load jwt keys if they are not in memory or already expired
if (Object.keys(cacheKeys).length === 0 || Math.floor(Date.now() / 1000) < cacheExpiration) { if (Object.keys(cacheKeys).length === 0 || Math.floor(Date.now() / 1000) < cacheExpiration) {
@ -59,19 +61,23 @@ export const cloudflareAccess = (accessTeamName: string) => {
// Is the token expired? // Is the token expired?
const expiryDate = new Date(token.payload.exp * 1000) const expiryDate = new Date(token.payload.exp * 1000)
const currentDate = new Date(Date.now()) const currentDate = new Date(Date.now())
if (expiryDate <= currentDate) return c.text('Authentication error: Token is expired', 401) if (expiryDate <= currentDate) {
return c.text('Authentication error: Token is expired', 401)
}
// Check is token is valid against at least one public key? // Check is token is valid against at least one public key?
if (!(await isValidJwtSignature(token, cacheKeys))) if (!(await isValidJwtSignature(token, cacheKeys))) {
return c.text('Authentication error: Invalid Token', 401) return c.text('Authentication error: Invalid Token', 401)
}
// Is signed from the correct team? // Is signed from the correct team?
const expectedIss = `https://${accessTeamName}.cloudflareaccess.com` const expectedIss = `https://${accessTeamName}.cloudflareaccess.com`
if (token.payload?.iss !== expectedIss) if (token.payload?.iss !== expectedIss) {
return c.text( return c.text(
`Authentication error: Expected team name ${expectedIss}, but received ${token.payload?.iss}`, `Authentication error: Expected team name ${expectedIss}, but received ${token.payload?.iss}`,
401 401
) )
}
c.set('accessPayload', token.payload) c.set('accessPayload', token.payload)
await next() await next()
@ -83,7 +89,6 @@ async function getPublicKeys(accessTeamName: string) {
const result = await fetch(jwtUrl, { const result = await fetch(jwtUrl, {
method: 'GET', method: 'GET',
// @ts-ignore
cf: { cf: {
// Dont cache error responses // Dont cache error responses
cacheTtlByStatus: { '200-299': 30, '300-599': 0 }, cacheTtlByStatus: { '200-299': 30, '300-599': 0 },
@ -92,16 +97,20 @@ async function getPublicKeys(accessTeamName: string) {
if (!result.ok) { if (!result.ok) {
if (result.status === 404) { if (result.status === 404) {
throw new HTTPException(500, { message: `Authentication error: The Access Organization '${accessTeamName}' does not exist` }) throw new HTTPException(500, {
message: `Authentication error: The Access Organization '${accessTeamName}' does not exist`,
})
} }
throw new HTTPException(500, { message: `Authentication error: Received unexpected HTTP code ${result.status} from Cloudflare Access` }) throw new HTTPException(500, {
message: `Authentication error: Received unexpected HTTP code ${result.status} from Cloudflare Access`,
})
} }
const data: any = await result.json() const data: any = await result.json()
// Because we keep CryptoKey's in memory between requests, we need to make sure they are refreshed once in a while // Because we keep CryptoKey's in memory between requests, we need to make sure they are refreshed once in a while
let cacheExpiration = Math.floor(Date.now() / 1000) + 3600 // 1h const cacheExpiration = Math.floor(Date.now() / 1000) + 3600 // 1h
const importedKeys: Record<string, CryptoKey> = {} const importedKeys: Record<string, CryptoKey> = {}
for (const key of data.keys) { for (const key of data.keys) {
@ -158,7 +167,9 @@ async function isValidJwtSignature(token: DecodedToken, keys: Record<string, Cry
for (const key of Object.values(keys)) { for (const key of Object.values(keys)) {
const isValid = await validateSingleKey(key, signature, data) const isValid = await validateSingleKey(key, signature, data)
if (isValid) return true if (isValid) {
return true
}
} }
return false return false

View File

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import type { Context, Env, Input as HonoInput, MiddlewareHandler, ValidationTargets } from 'hono'
import type { Submission } from '@conform-to/dom' import type { Submission } from '@conform-to/dom'
import type { Context, Env, Input as HonoInput, MiddlewareHandler, ValidationTargets } from 'hono'
import { getFormDataFromContext } from './utils' import { getFormDataFromContext } from './utils'
type FormTargetValue = ValidationTargets['form']['string'] type FormTargetValue = ValidationTargets['form']['string']

View File

@ -1,6 +1,6 @@
import { parseWithZod } from '@conform-to/zod'
import { Hono } from 'hono' import { Hono } from 'hono'
import { z } from 'zod' import { z } from 'zod'
import { parseWithZod } from '@conform-to/zod'
import { conformValidator } from '../src' import { conformValidator } from '../src'
describe('Validate common processing', () => { describe('Validate common processing', () => {

View File

@ -1,9 +1,9 @@
import * as z from 'zod' import { parseWithZod } from '@conform-to/zod'
import { Hono } from 'hono' import { Hono } from 'hono'
import { hc } from 'hono/client' import { hc } from 'hono/client'
import { parseWithZod } from '@conform-to/zod'
import { conformValidator } from '../src'
import { vi } from 'vitest' import { vi } from 'vitest'
import * as z from 'zod'
import { conformValidator } from '../src'
describe('Validate the hook option processing', () => { describe('Validate the hook option processing', () => {
const app = new Hono() const app = new Hono()

View File

@ -1,10 +1,10 @@
import type { ExtractSchema, ParsedFormValue } from 'hono/types' import { parseWithValibot } from 'conform-to-valibot'
import type { Equal, Expect } from 'hono/utils/types'
import type { StatusCode } from 'hono/utils/http-status'
import * as v from 'valibot'
import { Hono } from 'hono' import { Hono } from 'hono'
import { hc } from 'hono/client' import { hc } from 'hono/client'
import { parseWithValibot } from 'conform-to-valibot' import type { ExtractSchema, ParsedFormValue } from 'hono/types'
import type { StatusCode } from 'hono/utils/http-status'
import type { Equal, Expect } from 'hono/utils/types'
import * as v from 'valibot'
import { conformValidator } from '../src' import { conformValidator } from '../src'
describe('Validate requests using a Valibot schema', () => { describe('Validate requests using a Valibot schema', () => {

View File

@ -1,10 +1,10 @@
import type { ExtractSchema, ParsedFormValue } from 'hono/types' import { parseWithYup } from '@conform-to/yup'
import type { Equal, Expect } from 'hono/utils/types'
import type { StatusCode } from 'hono/utils/http-status'
import * as y from 'yup'
import { Hono } from 'hono' import { Hono } from 'hono'
import { hc } from 'hono/client' import { hc } from 'hono/client'
import { parseWithYup } from '@conform-to/yup' import type { ExtractSchema, ParsedFormValue } from 'hono/types'
import type { StatusCode } from 'hono/utils/http-status'
import type { Equal, Expect } from 'hono/utils/types'
import * as y from 'yup'
import { conformValidator } from '../src' import { conformValidator } from '../src'
describe('Validate requests using a Yup schema', () => { describe('Validate requests using a Yup schema', () => {

View File

@ -1,10 +1,10 @@
import type { ExtractSchema, ParsedFormValue } from 'hono/types' import { parseWithZod } from '@conform-to/zod'
import type { Equal, Expect } from 'hono/utils/types'
import type { StatusCode } from 'hono/utils/http-status'
import * as z from 'zod'
import { Hono } from 'hono' import { Hono } from 'hono'
import { hc } from 'hono/client' import { hc } from 'hono/client'
import { parseWithZod } from '@conform-to/zod' import type { ExtractSchema, ParsedFormValue } from 'hono/types'
import type { StatusCode } from 'hono/utils/http-status'
import type { Equal, Expect } from 'hono/utils/types'
import * as z from 'zod'
import { conformValidator } from '../src' import { conformValidator } from '../src'
describe('Validate requests using a Zod schema', () => { describe('Validate requests using a Zod schema', () => {

View File

@ -1,19 +1,18 @@
interface CloseEventInit extends EventInit { interface CloseEventInit extends EventInit {
code?: number; code?: number
reason?: string; reason?: string
wasClean?: boolean; wasClean?: boolean
} }
/** /**
* @link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent * @link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
*/ */
export const CloseEvent = globalThis.CloseEvent ?? class extends Event { export const CloseEvent =
globalThis.CloseEvent ??
class extends Event {
#eventInitDict #eventInitDict
constructor( constructor(type: string, eventInitDict: CloseEventInit = {}) {
type: string,
eventInitDict: CloseEventInit = {}
) {
super(type, eventInitDict) super(type, eventInitDict)
this.#eventInitDict = eventInitDict this.#eventInitDict = eventInitDict
} }

View File

@ -1,9 +1,9 @@
import { serve } from '@hono/node-server' import { serve } from '@hono/node-server'
import type { ServerType } from '@hono/node-server/dist/types' import type { ServerType } from '@hono/node-server/dist/types'
import { Hono } from 'hono' import { Hono } from 'hono'
import type { WSMessageReceive } from 'hono/ws'
import { WebSocket } from 'ws' import { WebSocket } from 'ws'
import { createNodeWebSocket } from '.' import { createNodeWebSocket } from '.'
import type { WSMessageReceive } from 'hono/ws'
describe('WebSocket helper', () => { describe('WebSocket helper', () => {
let app: Hono let app: Hono
@ -13,7 +13,6 @@ describe('WebSocket helper', () => {
beforeEach(async () => { beforeEach(async () => {
app = new Hono() app = new Hono()
;({ injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app })) ;({ injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app }))
server = await new Promise<ServerType>((resolve) => { server = await new Promise<ServerType>((resolve) => {

View File

@ -1,10 +1,10 @@
import type { Server } from 'node:http'
import type { Http2SecureServer, Http2Server } from 'node:http2'
import type { Hono } from 'hono' import type { Hono } from 'hono'
import type { UpgradeWebSocket, WSContext } from 'hono/ws' import type { UpgradeWebSocket, WSContext } from 'hono/ws'
import type { WebSocket } from 'ws' import type { WebSocket } from 'ws'
import { WebSocketServer } from 'ws' import { WebSocketServer } from 'ws'
import type { IncomingMessage } from 'http' import type { IncomingMessage } from 'http'
import type { Server } from 'node:http'
import type { Http2SecureServer, Http2Server } from 'node:http2'
import { CloseEvent } from './events' import { CloseEvent } from './events'
export interface NodeWebSocket { export interface NodeWebSocket {

View File

@ -1,6 +1,6 @@
import type { MiddlewareHandler } from 'hono' import type { MiddlewareHandler } from 'hono'
import { getCookie, setCookie } from 'hono/cookie'
import { env } from 'hono/adapter' import { env } from 'hono/adapter'
import { getCookie, setCookie } from 'hono/cookie'
import { HTTPException } from 'hono/http-exception' import { HTTPException } from 'hono/http-exception'
import { getRandomState } from '../../utils/getRandomState' import { getRandomState } from '../../utils/getRandomState'

View File

@ -1,6 +1,6 @@
import type { MiddlewareHandler } from 'hono' import type { MiddlewareHandler } from 'hono'
import { getCookie, setCookie } from 'hono/cookie'
import { env } from 'hono/adapter' import { env } from 'hono/adapter'
import { getCookie, setCookie } from 'hono/cookie'
import { HTTPException } from 'hono/http-exception' import { HTTPException } from 'hono/http-exception'
import { getRandomState } from '../../utils/getRandomState' import { getRandomState } from '../../utils/getRandomState'

View File

@ -1,6 +1,6 @@
import type { MiddlewareHandler } from 'hono' import type { MiddlewareHandler } from 'hono'
import { getCookie, setCookie } from 'hono/cookie'
import { env } from 'hono/adapter' import { env } from 'hono/adapter'
import { getCookie, setCookie } from 'hono/cookie'
import { HTTPException } from 'hono/http-exception' import { HTTPException } from 'hono/http-exception'
import { getRandomState } from '../../utils/getRandomState' import { getRandomState } from '../../utils/getRandomState'

View File

@ -1,6 +1,6 @@
import type { MiddlewareHandler } from 'hono' import type { MiddlewareHandler } from 'hono'
import { getCookie, setCookie } from 'hono/cookie'
import { env } from 'hono/adapter' import { env } from 'hono/adapter'
import { getCookie, setCookie } from 'hono/cookie'
import { HTTPException } from 'hono/http-exception' import { HTTPException } from 'hono/http-exception'
import { getRandomState } from '../../utils/getRandomState' import { getRandomState } from '../../utils/getRandomState'

View File

@ -1,6 +1,6 @@
import type { MiddlewareHandler } from 'hono' import type { MiddlewareHandler } from 'hono'
import { getCookie, setCookie } from 'hono/cookie'
import { env } from 'hono/adapter' import { env } from 'hono/adapter'
import { getCookie, setCookie } from 'hono/cookie'
import { HTTPException } from 'hono/http-exception' import { HTTPException } from 'hono/http-exception'
import { getCodeChallenge } from '../../utils/getCodeChallenge' import { getCodeChallenge } from '../../utils/getCodeChallenge'

View File

@ -36,6 +36,7 @@
"hono": ">=3.*" "hono": ">=3.*"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "^29.7.0",
"@types/jest": "^29.5.11", "@types/jest": "^29.5.11",
"@types/jsonwebtoken": "^9.0.5", "@types/jsonwebtoken": "^9.0.5",
"hono": "^4.0.1", "hono": "^4.0.1",

View File

@ -1,8 +1,8 @@
import crypto from 'node:crypto'
import { jest } from '@jest/globals' import { jest } from '@jest/globals'
import { Hono } from 'hono' import { Hono } from 'hono'
import jwt from 'jsonwebtoken' import jwt from 'jsonwebtoken'
import * as oauth2 from 'oauth4webapi' import * as oauth2 from 'oauth4webapi'
import crypto from 'node:crypto'
const MOCK_ISSUER = 'https://accounts.google.com' const MOCK_ISSUER = 'https://accounts.google.com'
const MOCK_CLIENT_ID = 'CLIENT_ID_001' const MOCK_CLIENT_ID = 'CLIENT_ID_001'
@ -387,7 +387,9 @@ describe('processOAuthCallback()', () => {
expect(res).not.toBeNull() expect(res).not.toBeNull()
expect(res.status).toBe(302) expect(res.status).toBe(302)
expect(res.headers.get('set-cookie')).toMatch( expect(res.headers.get('set-cookie')).toMatch(
new RegExp(`${MOCK_COOKIE_NAME}=[^;]+; Path=${process.env.OIDC_COOKIE_PATH}; HttpOnly; Secure`) new RegExp(
`${MOCK_COOKIE_NAME}=[^;]+; Path=${process.env.OIDC_COOKIE_PATH}; HttpOnly; Secure`
)
) )
}) })
test('Should return an error if the state parameter does not match', async () => { test('Should return an error if the state parameter does not match', async () => {

View File

@ -2,11 +2,8 @@ import type { Context } from 'hono'
import { createMiddleware } from 'hono/factory' import { createMiddleware } from 'hono/factory'
import type { DefaultMetricsCollectorConfiguration, RegistryContentType } from 'prom-client' import type { DefaultMetricsCollectorConfiguration, RegistryContentType } from 'prom-client'
import { Registry, collectDefaultMetrics as promCollectDefaultMetrics } from 'prom-client' import { Registry, collectDefaultMetrics as promCollectDefaultMetrics } from 'prom-client'
import { import { createStandardMetrics } from './standardMetrics'
type MetricOptions, import type { MetricOptions, CustomMetricsOptions } from './standardMetrics'
type CustomMetricsOptions,
createStandardMetrics,
} from './standardMetrics'
interface PrometheusOptions { interface PrometheusOptions {
registry?: Registry registry?: Registry

View File

@ -1,6 +1,6 @@
import type { Context } from 'hono' import type { Context } from 'hono'
import type { CounterConfiguration, HistogramConfiguration, Metric } from 'prom-client' import { Counter, Histogram } from 'prom-client'
import { Counter, Histogram, type Registry } from 'prom-client' import type { CounterConfiguration, HistogramConfiguration, Metric, Registry } from 'prom-client'
export type MetricOptions = { export type MetricOptions = {
disabled?: boolean disabled?: boolean

View File

@ -13,7 +13,6 @@ import {
import type { MiddlewareHandler } from 'hono' import type { MiddlewareHandler } from 'hono'
export const qwikMiddleware = (opts: ServerRenderOptions): MiddlewareHandler => { export const qwikMiddleware = (opts: ServerRenderOptions): MiddlewareHandler => {
// eslint-disable-next-line @typescript-eslint/no-extra-semi
;(globalThis as any).TextEncoderStream = TextEncoderStream ;(globalThis as any).TextEncoderStream = TextEncoderStream
const qwikSerializer = { const qwikSerializer = {
_deserializeData, _deserializeData,

View File

@ -1,7 +1,8 @@
import type { Context } from 'hono' import type { Context } from 'hono'
import type { Env, MiddlewareHandler } from 'hono/types' import type { Env, MiddlewareHandler } from 'hono/types'
import React from 'react' import React from 'react'
import { renderToString, type RenderToReadableStreamOptions } from 'react-dom/server' import { renderToString } from 'react-dom/server'
import type { RenderToReadableStreamOptions } from 'react-dom/server'
import type { Props } from '.' import type { Props } from '.'
type RendererOptions = { type RendererOptions = {

View File

@ -69,7 +69,7 @@ const SwaggerUI = (options: SwaggerUIOptions) => {
const middleware = const middleware =
<E extends Env>(options: SwaggerUIOptions): MiddlewareHandler<E> => <E extends Env>(options: SwaggerUIOptions): MiddlewareHandler<E> =>
async (c) => { async (c) => {
const title = options?.title ?? "SwaggerUI" const title = options?.title ?? 'SwaggerUI'
return c.html(/* html */ ` return c.html(/* html */ `
<html lang="en"> <html lang="en">
<head> <head>

View File

@ -1,6 +1,6 @@
import 'reflect-metadata' import 'reflect-metadata'
import { injectable, inject } from 'tsyringe'
import { Hono } from 'hono' import { Hono } from 'hono'
import { injectable, inject } from 'tsyringe'
import { tsyringe } from '../src' import { tsyringe } from '../src'
class Config { class Config {

View File

@ -1,6 +1,7 @@
import { container, DependencyContainer, InjectionToken } from 'tsyringe'
import type { Context, MiddlewareHandler } from 'hono' import type { Context, MiddlewareHandler } from 'hono'
import { createMiddleware } from 'hono/factory' import { createMiddleware } from 'hono/factory'
import type { DependencyContainer, InjectionToken } from 'tsyringe'
import { container } from 'tsyringe'
declare module 'hono' { declare module 'hono' {
interface ContextVariableMap { interface ContextVariableMap {

View File

@ -1,5 +1,7 @@
import { TSchema, Static, TypeGuard, ValueGuard } from '@sinclair/typebox' import type { TSchema, Static } from '@sinclair/typebox'
import { Value, type ValueError } from '@sinclair/typebox/value' import { TypeGuard, ValueGuard } from '@sinclair/typebox'
import { Value } from '@sinclair/typebox/value'
import type { ValueError } from '@sinclair/typebox/value'
import type { Context, Env, MiddlewareHandler, ValidationTargets } from 'hono' import type { Context, Env, MiddlewareHandler, ValidationTargets } from 'hono'
import { validator } from 'hono/validator' import { validator } from 'hono/validator'
import IsObject = ValueGuard.IsObject import IsObject = ValueGuard.IsObject
@ -7,7 +9,7 @@ import IsArray = ValueGuard.IsArray
export type Hook<T, E extends Env, P extends string> = ( export type Hook<T, E extends Env, P extends string> = (
result: { success: true; data: T } | { success: false; errors: ValueError[] }, result: { success: true; data: T } | { success: false; errors: ValueError[] },
c: Context<E, P>, c: Context<E, P>
) => Response | Promise<Response> | void ) => Response | Promise<Response> | void
/** /**
@ -61,11 +63,18 @@ export function tbValidator<
E extends Env, E extends Env,
P extends string, P extends string,
V extends { in: { [K in Target]: Static<T> }; out: { [K in Target]: Static<T> } } V extends { in: { [K in Target]: Static<T> }; out: { [K in Target]: Static<T> } }
>(target: Target, schema: T, hook?: Hook<Static<T>, E, P>, stripNonSchemaItems?: boolean): MiddlewareHandler<E, P, V> { >(
target: Target,
schema: T,
hook?: Hook<Static<T>, E, P>,
stripNonSchemaItems?: boolean
): MiddlewareHandler<E, P, V> {
// Compile the provided schema once rather than per validation. This could be optimized further using a shared schema // Compile the provided schema once rather than per validation. This could be optimized further using a shared schema
// compilation pool similar to the Fastify implementation. // compilation pool similar to the Fastify implementation.
return validator(target, (unprocessedData, c) => { return validator(target, (unprocessedData, c) => {
const data = stripNonSchemaItems ? removeNonSchemaItems(schema, unprocessedData) : unprocessedData const data = stripNonSchemaItems
? removeNonSchemaItems(schema, unprocessedData)
: unprocessedData
if (Value.Check(schema, data)) { if (Value.Check(schema, data)) {
if (hook) { if (hook) {
@ -90,7 +99,9 @@ export function tbValidator<
} }
function removeNonSchemaItems<T extends TSchema>(schema: T, obj: any): Static<T> { function removeNonSchemaItems<T extends TSchema>(schema: T, obj: any): Static<T> {
if (typeof obj !== 'object' || obj === null) return obj if (typeof obj !== 'object' || obj === null) {
return obj
}
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
return obj.map((item) => removeNonSchemaItems(schema.items, item)) return obj.map((item) => removeNonSchemaItems(schema.items, item))
@ -98,12 +109,9 @@ function removeNonSchemaItems<T extends TSchema>(schema: T, obj: any): Static<T>
const result: any = {} const result: any = {}
for (const key in schema.properties) { for (const key in schema.properties) {
if (obj.hasOwnProperty(key)) { if (Object.prototype.hasOwnProperty.call(obj, key)) {
const propertySchema = schema.properties[key] const propertySchema = schema.properties[key]
if ( if (IsObject(propertySchema) && !IsArray(propertySchema)) {
IsObject(propertySchema) &&
!IsArray(propertySchema)
) {
result[key] = removeNonSchemaItems(propertySchema as unknown as TSchema, obj[key]) result[key] = removeNonSchemaItems(propertySchema as unknown as TSchema, obj[key])
} else { } else {
result[key] = obj[key] result[key] = obj[key]

View File

@ -1,8 +1,8 @@
import { Type as T } from '@sinclair/typebox' import { Type as T } from '@sinclair/typebox'
import type { ValueError } from '@sinclair/typebox/value'
import { Hono } from 'hono' import { Hono } from 'hono'
import type { Equal, Expect } from 'hono/utils/types' import type { Equal, Expect } from 'hono/utils/types'
import { tbValidator } from '../src' import { tbValidator } from '../src'
import { ValueError } from '@sinclair/typebox/value'
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
type ExtractSchema<T> = T extends Hono<infer _, infer S> ? S : never type ExtractSchema<T> = T extends Hono<infer _, infer S> ? S : never
@ -91,7 +91,8 @@ describe('With Hook', () => {
title: T.String(), title: T.String(),
}) })
app.post( app
.post(
'/post', '/post',
tbValidator('json', schema, (result, c) => { tbValidator('json', schema, (result, c) => {
if (!result.success) { if (!result.success) {
@ -106,8 +107,9 @@ describe('With Hook', () => {
success: true, success: true,
message: `${data.id} is ${data.title}`, message: `${data.id} is ${data.title}`,
}) })
}, }
).post( )
.post(
'/errorTest', '/errorTest',
tbValidator('json', schema, (result, c) => { tbValidator('json', schema, (result, c) => {
return c.json(result, 400) return c.json(result, 400)
@ -118,7 +120,7 @@ describe('With Hook', () => {
success: true, success: true,
message: `${data.id} is ${data.title}`, message: `${data.id} is ${data.title}`,
}) })
}, }
) )
it('Should return 200 response', async () => { it('Should return 200 response', async () => {
@ -171,20 +173,22 @@ describe('With Hook', () => {
const { errors, success } = (await res.json()) as { success: boolean; errors: any[] } const { errors, success } = (await res.json()) as { success: boolean; errors: any[] }
expect(success).toBe(false) expect(success).toBe(false)
expect(Array.isArray(errors)).toBe(true) expect(Array.isArray(errors)).toBe(true)
expect(errors.map((e: ValueError) => ({ expect(
'type': e?.schema?.type, errors.map((e: ValueError) => ({
type: e?.schema?.type,
path: e?.path, path: e?.path,
message: e?.message, message: e?.message,
}))).toEqual([ }))
).toEqual([
{ {
'type': 'string', type: 'string',
'path': '/title', path: '/title',
'message': 'Required property', message: 'Required property',
}, },
{ {
'type': 'string', type: 'string',
'path': '/title', path: '/title',
'message': 'Expected string', message: 'Expected string',
}, },
]) ])
}) })
@ -207,25 +211,19 @@ describe('Remove non schema items', () => {
}), }),
}) })
app.post( app
'/stripValuesNested', .post('/stripValuesNested', tbValidator('json', nestedSchema, undefined, true), (c) => {
tbValidator('json', nestedSchema, undefined, true),
(c) => {
return c.json({ return c.json({
success: true, success: true,
message: c.req.valid('json'), message: c.req.valid('json'),
}) })
}, })
).post( .post('/stripValuesArray', tbValidator('json', T.Array(schema), undefined, true), (c) => {
'/stripValuesArray',
tbValidator('json', T.Array(schema), undefined, true),
(c) => {
return c.json({ return c.json({
success: true, success: true,
message: c.req.valid('json'), message: c.req.valid('json'),
}) })
}, })
)
it('Should remove all the values in the nested object and return a 200 response', async () => { it('Should remove all the values in the nested object and return a 200 response', async () => {
const req = new Request('http://localhost/stripValuesNested', { const req = new Request('http://localhost/stripValuesNested', {
@ -274,20 +272,21 @@ describe('Remove non schema items', () => {
const { message, success } = (await res.json()) as { success: boolean; message: any } const { message, success } = (await res.json()) as { success: boolean; message: any }
expect(success).toBe(true) expect(success).toBe(true)
expect(message).toEqual( expect(message).toEqual({
id: 123,
itemArray: [
{ id: 123, title: 'Hello' },
{ {
'id': 123, id: 123,
'itemArray': [{ 'id': 123, 'title': 'Hello' }, { title: 'Hello',
'id': 123,
'title': 'Hello',
}],
'item': { 'id': 123, 'title': 'Hello' },
'itemObject': {
'item1': { 'id': 123, 'title': 'Hello' },
'item2': { 'id': 123, 'title': 'Hello' },
}, },
],
item: { id: 123, title: 'Hello' },
itemObject: {
item1: { id: 123, title: 'Hello' },
item2: { id: 123, title: 'Hello' },
}, },
) })
}) })
it('Should remove all the values in the array and return a 200 response', async () => { it('Should remove all the values in the array and return a 200 response', async () => {
@ -303,7 +302,8 @@ describe('Remove non schema items', () => {
title: 'Hello 2', title: 'Hello 2',
nonExistentKey: 'error', nonExistentKey: 'error',
}, },
]), method: 'POST', ]),
method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -313,11 +313,12 @@ describe('Remove non schema items', () => {
const { message, success } = (await res.json()) as { success: boolean; message: Array<any> } const { message, success } = (await res.json()) as { success: boolean; message: Array<any> }
expect(res.status).toBe(200) expect(res.status).toBe(200)
expect(success).toBe(true) expect(success).toBe(true)
expect(message).toEqual([{ 'id': 123, 'title': 'Hello' }, { expect(message).toEqual([
'id': 123, { id: 123, title: 'Hello' },
'title': 'Hello 2', {
}], id: 123,
) title: 'Hello 2',
},
])
}) })
}) })

View File

@ -129,7 +129,7 @@ export const typiaValidator: TypiaValidator = (
validate: (input: any) => IValidation<any>, validate: (input: any) => IValidation<any>,
hook?: Hook<any, any, any> hook?: Hook<any, any, any>
): MiddlewareHandler => { ): MiddlewareHandler => {
if (target === 'query' || target === 'header') if (target === 'query' || target === 'header') {
return async (c, next) => { return async (c, next) => {
let value: any let value: any
if (target === 'query') { if (target === 'query') {
@ -140,15 +140,20 @@ export const typiaValidator: TypiaValidator = (
} satisfies IReadableURLSearchParams } satisfies IReadableURLSearchParams
} else { } else {
value = Object.create(null) value = Object.create(null)
for (const [key, headerValue] of c.req.raw.headers) value[key.toLowerCase()] = headerValue for (const [key, headerValue] of c.req.raw.headers) {
if (c.req.raw.headers.has('Set-Cookie')) value[key.toLowerCase()] = headerValue
}
if (c.req.raw.headers.has('Set-Cookie')) {
value['Set-Cookie'] = c.req.raw.headers.getSetCookie() value['Set-Cookie'] = c.req.raw.headers.getSetCookie()
} }
}
const result = validate(value) const result = validate(value)
if (hook) { if (hook) {
const res = await hook(result as never, c) const res = await hook(result as never, c)
if (res instanceof Response) return res if (res instanceof Response) {
return res
}
} }
if (!result.success) { if (!result.success) {
return c.json({ success: false, error: result.errors }, 400) return c.json({ success: false, error: result.errors }, 400)
@ -157,6 +162,7 @@ export const typiaValidator: TypiaValidator = (
await next() await next()
} }
}
return validator(target, async (value, c) => { return validator(target, async (value, c) => {
const result = validate(value) const result = validate(value)

View File

@ -1,6 +1,7 @@
import { Hono } from 'hono' import { Hono } from 'hono'
import type { Equal, Expect } from 'hono/utils/types' import type { Equal, Expect } from 'hono/utils/types'
import typia, { tags } from 'typia' import type { tags } from 'typia'
import typia from 'typia'
import { typiaValidator } from '../src/http' import { typiaValidator } from '../src/http'
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars

View File

@ -1,6 +1,12 @@
import type { Context, Env, Input as HonoInput, MiddlewareHandler, ValidationTargets } from 'hono' import type { Context, Env, Input as HonoInput, MiddlewareHandler, ValidationTargets } from 'hono'
import { validator } from 'hono/validator' import { validator } from 'hono/validator'
import type { GenericSchema, GenericSchemaAsync, InferInput, InferOutput, SafeParseResult } from 'valibot' import type {
GenericSchema,
GenericSchemaAsync,
InferInput,
InferOutput,
SafeParseResult,
} from 'valibot'
import { safeParseAsync } from 'valibot' import { safeParseAsync } from 'valibot'
export type Hook<T extends GenericSchema | GenericSchemaAsync, E extends Env, P extends string> = ( export type Hook<T extends GenericSchema | GenericSchemaAsync, E extends Env, P extends string> = (

View File

@ -1,8 +1,8 @@
import { Hono } from 'hono' import { Hono } from 'hono'
import type { StatusCode } from 'hono/utils/http-status'
import type { Equal, Expect } from 'hono/utils/types' import type { Equal, Expect } from 'hono/utils/types'
import { number, object, objectAsync, optional, optionalAsync, string } from 'valibot' import { number, object, objectAsync, optional, optionalAsync, string } from 'valibot'
import { vValidator } from '../src' import { vValidator } from '../src'
import { StatusCode } from 'hono/utils/http-status'
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
type ExtractSchema<T> = T extends Hono<infer _, infer S> ? S : never type ExtractSchema<T> = T extends Hono<infer _, infer S> ? S : never

View File

@ -1,9 +1,9 @@
import { defineConfig } from "tsup"; import { defineConfig } from 'tsup'
export default defineConfig({ export default defineConfig({
entryPoints: ["src/index.ts"], entryPoints: ['src/index.ts'],
format: ["cjs", "esm"], format: ['cjs', 'esm'],
dts: true, dts: true,
outDir: "dist", outDir: 'dist',
clean: true, clean: true,
}); })

View File

@ -26,7 +26,6 @@ import type {
ValidationTargets, ValidationTargets,
} from 'hono' } from 'hono'
import type { MergePath, MergeSchemaPath } from 'hono/types' import type { MergePath, MergeSchemaPath } from 'hono/types'
import type { JSONParsed, JSONValue, RemoveBlankRecord, SimplifyDeepArray } from 'hono/utils/types'
import type { import type {
ClientErrorStatusCode, ClientErrorStatusCode,
InfoStatusCode, InfoStatusCode,
@ -35,6 +34,7 @@ import type {
StatusCode, StatusCode,
SuccessStatusCode, SuccessStatusCode,
} from 'hono/utils/http-status' } from 'hono/utils/http-status'
import type { JSONParsed, JSONValue, RemoveBlankRecord, SimplifyDeepArray } from 'hono/utils/types'
import { mergePath } from 'hono/utils/url' import { mergePath } from 'hono/utils/url'
import type { ZodError, ZodSchema } from 'zod' import type { ZodError, ZodSchema } from 'zod'
import { ZodType, z } from 'zod' import { ZodType, z } from 'zod'
@ -657,7 +657,6 @@ export class OpenAPIHono<
} }
}) })
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return this as any return this as any
} }

View File

@ -1,8 +1,9 @@
import { assertType, describe, it } from 'vitest'
import { MiddlewareToHandlerType, OfHandlerType, OpenAPIHono, createRoute, z } from '../src/index'
import { createMiddleware } from 'hono/factory' import { createMiddleware } from 'hono/factory'
import type { ExtractSchema } from 'hono/types' import type { ExtractSchema } from 'hono/types'
import type { Equal, Expect } from 'hono/utils/types' import type { Equal, Expect } from 'hono/utils/types'
import { assertType, describe, it } from 'vitest'
import { OpenAPIHono, createRoute, z } from '../src/index'
import type { MiddlewareToHandlerType, OfHandlerType } from '../src/index'
describe('Types', () => { describe('Types', () => {
const RequestSchema = z.object({ const RequestSchema = z.object({
@ -276,7 +277,7 @@ describe('Middleware', () => {
}) })
it('Should infer Env from router middleware', async () => { it('Should infer Env from router middleware', async () => {
const app = new OpenAPIHono<{ Variables: { too: Symbol } }>() const app = new OpenAPIHono<{ Variables: { too: symbol } }>()
app.openapi( app.openapi(
createRoute({ createRoute({
method: 'get', method: 'get',
@ -308,7 +309,7 @@ describe('Middleware', () => {
type verifyFoo = Expect<Equal<typeof c.var.foo, string>> type verifyFoo = Expect<Equal<typeof c.var.foo, string>>
type verifyBar = Expect<Equal<typeof c.var.bar, number>> type verifyBar = Expect<Equal<typeof c.var.bar, number>>
type verifyToo = Expect<Equal<typeof c.var.too, Symbol>> type verifyToo = Expect<Equal<typeof c.var.too, symbol>>
return c.json({}) return c.json({})
} }
@ -316,7 +317,7 @@ describe('Middleware', () => {
}) })
it('Should infer Env root when no middleware provided', async () => { it('Should infer Env root when no middleware provided', async () => {
const app = new OpenAPIHono<{ Variables: { too: Symbol } }>() const app = new OpenAPIHono<{ Variables: { too: symbol } }>()
app.openapi( app.openapi(
createRoute({ createRoute({
method: 'get', method: 'get',
@ -331,7 +332,7 @@ describe('Middleware', () => {
(c) => { (c) => {
c.var.too c.var.too
type verify = Expect<Equal<typeof c.var.too, Symbol>> type verify = Expect<Equal<typeof c.var.too, symbol>>
return c.json({}) return c.json({})
} }

View File

@ -1,14 +1,14 @@
import type { RouteConfig } from '@asteasolutions/zod-to-openapi' import type { RouteConfig } from '@asteasolutions/zod-to-openapi'
import type { Context, TypedResponse } from 'hono' import type { Context, TypedResponse } from 'hono'
import { accepts } from 'hono/accepts'
import { bearerAuth } from 'hono/bearer-auth' import { bearerAuth } from 'hono/bearer-auth'
import { hc } from 'hono/client' import { hc } from 'hono/client'
import type { ServerErrorStatusCode } from 'hono/utils/http-status'
import type { Equal, Expect } from 'hono/utils/types'
import { describe, expect, expectTypeOf, it, vi } from 'vitest' import { describe, expect, expectTypeOf, it, vi } from 'vitest'
import { stringify } from 'yaml'
import type { RouteConfigToTypedResponse } from '../src/index' import type { RouteConfigToTypedResponse } from '../src/index'
import { OpenAPIHono, createRoute, z } from '../src/index' import { OpenAPIHono, createRoute, z } from '../src/index'
import type { Equal, Expect } from 'hono/utils/types'
import type { ServerErrorStatusCode } from 'hono/utils/http-status'
import { stringify } from 'yaml'
import { accepts } from 'hono/accepts'
describe('Constructor', () => { describe('Constructor', () => {
it('Should not require init object', () => { it('Should not require init object', () => {

View File

@ -1,6 +1,7 @@
import type { Context, Env, Input, MiddlewareHandler, TypedResponse, ValidationTargets } from 'hono' import type { Context, Env, Input, MiddlewareHandler, TypedResponse, ValidationTargets } from 'hono'
import { validator } from 'hono/validator' import { validator } from 'hono/validator'
import { ZodObject, type ZodError, type ZodSchema, type z } from 'zod' import { ZodObject } from 'zod'
import type { ZodError, ZodSchema, z } from 'zod'
export type Hook< export type Hook<
T, T,

View File

@ -1,8 +1,8 @@
import { Hono } from 'hono' import { Hono } from 'hono'
import type { Equal, Expect } from 'hono/utils/types' import type { Equal, Expect } from 'hono/utils/types'
import { vi } from 'vitest'
import { z } from 'zod' import { z } from 'zod'
import { zValidator } from '../src' import { zValidator } from '../src'
import { vi } from 'vitest'
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
type ExtractSchema<T> = T extends Hono<infer _, infer S> ? S : never type ExtractSchema<T> = T extends Hono<infer _, infer S> ? S : never

197
yarn.lock
View File

@ -2221,6 +2221,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@eslint-community/regexpp@npm:^4.12.1":
version: 4.12.1
resolution: "@eslint-community/regexpp@npm:4.12.1"
checksum: a03d98c246bcb9109aec2c08e4d10c8d010256538dcb3f56610191607214523d4fb1b00aa81df830b6dffb74c5fa0be03642513a289c567949d3e550ca11cdf6
languageName: node
linkType: hard
"@eslint-community/regexpp@npm:^4.4.0": "@eslint-community/regexpp@npm:^4.4.0":
version: 4.11.1 version: 4.11.1
resolution: "@eslint-community/regexpp@npm:4.11.1" resolution: "@eslint-community/regexpp@npm:4.11.1"
@ -2246,6 +2253,26 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@eslint/config-array@npm:^0.19.0":
version: 0.19.1
resolution: "@eslint/config-array@npm:0.19.1"
dependencies:
"@eslint/object-schema": "npm:^2.1.5"
debug: "npm:^4.3.1"
minimatch: "npm:^3.1.2"
checksum: 43b01f596ddad404473beae5cf95c013d29301c72778d0f5bf8a6699939c8a9a5663dbd723b53c5f476b88b0c694f76ea145d1aa9652230d140fe1161e4a4b49
languageName: node
linkType: hard
"@eslint/core@npm:^0.9.0":
version: 0.9.1
resolution: "@eslint/core@npm:0.9.1"
dependencies:
"@types/json-schema": "npm:^7.0.15"
checksum: 638104b1b5833a9bbf2329f0c0ddf322e4d6c0410b149477e02cd2b78c04722be90c14b91b8ccdef0d63a2404dff72a17b6b412ce489ea429ae6a8fcb8abff28
languageName: node
linkType: hard
"@eslint/eslintrc@npm:^2.1.4": "@eslint/eslintrc@npm:^2.1.4":
version: 2.1.4 version: 2.1.4
resolution: "@eslint/eslintrc@npm:2.1.4" resolution: "@eslint/eslintrc@npm:2.1.4"
@ -2280,6 +2307,23 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@eslint/eslintrc@npm:^3.2.0":
version: 3.2.0
resolution: "@eslint/eslintrc@npm:3.2.0"
dependencies:
ajv: "npm:^6.12.4"
debug: "npm:^4.3.2"
espree: "npm:^10.0.1"
globals: "npm:^14.0.0"
ignore: "npm:^5.2.0"
import-fresh: "npm:^3.2.1"
js-yaml: "npm:^4.1.0"
minimatch: "npm:^3.1.2"
strip-json-comments: "npm:^3.1.1"
checksum: 43867a07ff9884d895d9855edba41acf325ef7664a8df41d957135a81a477ff4df4196f5f74dc3382627e5cc8b7ad6b815c2cea1b58f04a75aced7c43414ab8b
languageName: node
linkType: hard
"@eslint/js@npm:8.57.0": "@eslint/js@npm:8.57.0":
version: 8.57.0 version: 8.57.0
resolution: "@eslint/js@npm:8.57.0" resolution: "@eslint/js@npm:8.57.0"
@ -2294,6 +2338,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@eslint/js@npm:9.17.0":
version: 9.17.0
resolution: "@eslint/js@npm:9.17.0"
checksum: a0fda8657a01c60aa540f95397754267ba640ffb126e011b97fd65c322a94969d161beeaef57c1441c495da2f31167c34bd38209f7c146c7225072378c3a933d
languageName: node
linkType: hard
"@eslint/object-schema@npm:^2.1.4": "@eslint/object-schema@npm:^2.1.4":
version: 2.1.4 version: 2.1.4
resolution: "@eslint/object-schema@npm:2.1.4" resolution: "@eslint/object-schema@npm:2.1.4"
@ -2301,6 +2352,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@eslint/object-schema@npm:^2.1.5":
version: 2.1.5
resolution: "@eslint/object-schema@npm:2.1.5"
checksum: 5320691ed41ecd09a55aff40ce8e56596b4eb81f3d4d6fe530c50fdd6552d88102d1c1a29d970ae798ce30849752a708772de38ded07a6f25b3da32ebea081d8
languageName: node
linkType: hard
"@eslint/plugin-kit@npm:^0.1.0": "@eslint/plugin-kit@npm:^0.1.0":
version: 0.1.0 version: 0.1.0
resolution: "@eslint/plugin-kit@npm:0.1.0" resolution: "@eslint/plugin-kit@npm:0.1.0"
@ -2310,6 +2368,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@eslint/plugin-kit@npm:^0.2.3":
version: 0.2.4
resolution: "@eslint/plugin-kit@npm:0.2.4"
dependencies:
levn: "npm:^0.4.1"
checksum: 1bcfc0a30b1df891047c1d8b3707833bded12a057ba01757a2a8591fdc8d8fe0dbb8d51d4b0b61b2af4ca1d363057abd7d2fb4799f1706b105734f4d3fa0dbf1
languageName: node
linkType: hard
"@fastify/busboy@npm:^2.0.0": "@fastify/busboy@npm:^2.0.0":
version: 2.1.0 version: 2.1.0
resolution: "@fastify/busboy@npm:2.1.0" resolution: "@fastify/busboy@npm:2.1.0"
@ -2719,6 +2786,7 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@hono/oidc-auth@workspace:packages/oidc-auth" resolution: "@hono/oidc-auth@workspace:packages/oidc-auth"
dependencies: dependencies:
"@jest/globals": "npm:^29.7.0"
"@types/jest": "npm:^29.5.11" "@types/jest": "npm:^29.5.11"
"@types/jsonwebtoken": "npm:^9.0.5" "@types/jsonwebtoken": "npm:^9.0.5"
hono: "npm:^4.0.1" hono: "npm:^4.0.1"
@ -2958,6 +3026,23 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft linkType: soft
"@humanfs/core@npm:^0.19.1":
version: 0.19.1
resolution: "@humanfs/core@npm:0.19.1"
checksum: aa4e0152171c07879b458d0e8a704b8c3a89a8c0541726c6b65b81e84fd8b7564b5d6c633feadc6598307d34564bd53294b533491424e8e313d7ab6c7bc5dc67
languageName: node
linkType: hard
"@humanfs/node@npm:^0.16.6":
version: 0.16.6
resolution: "@humanfs/node@npm:0.16.6"
dependencies:
"@humanfs/core": "npm:^0.19.1"
"@humanwhocodes/retry": "npm:^0.3.0"
checksum: 8356359c9f60108ec204cbd249ecd0356667359b2524886b357617c4a7c3b6aace0fd5a369f63747b926a762a88f8a25bc066fa1778508d110195ce7686243e1
languageName: node
linkType: hard
"@humanwhocodes/config-array@npm:^0.11.14": "@humanwhocodes/config-array@npm:^0.11.14":
version: 0.11.14 version: 0.11.14
resolution: "@humanwhocodes/config-array@npm:0.11.14" resolution: "@humanwhocodes/config-array@npm:0.11.14"
@ -2990,6 +3075,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@humanwhocodes/retry@npm:^0.4.1":
version: 0.4.1
resolution: "@humanwhocodes/retry@npm:0.4.1"
checksum: be7bb6841c4c01d0b767d9bb1ec1c9359ee61421ce8ba66c249d035c5acdfd080f32d55a5c9e859cdd7868788b8935774f65b2caf24ec0b7bd7bf333791f063b
languageName: node
linkType: hard
"@iarna/toml@npm:^2.2.5": "@iarna/toml@npm:^2.2.5":
version: 2.2.5 version: 2.2.5
resolution: "@iarna/toml@npm:2.2.5" resolution: "@iarna/toml@npm:2.2.5"
@ -4908,7 +5000,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/estree@npm:1.0.6": "@types/estree@npm:1.0.6, @types/estree@npm:^1.0.6":
version: 1.0.6 version: 1.0.6
resolution: "@types/estree@npm:1.0.6" resolution: "@types/estree@npm:1.0.6"
checksum: cdfd751f6f9065442cd40957c07fd80361c962869aa853c1c2fd03e101af8b9389d8ff4955a43a6fcfa223dd387a089937f95be0f3eec21ca527039fd2d9859a checksum: cdfd751f6f9065442cd40957c07fd80361c962869aa853c1c2fd03e101af8b9389d8ff4955a43a6fcfa223dd387a089937f95be0f3eec21ca527039fd2d9859a
@ -5002,7 +5094,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.6, @types/json-schema@npm:^7.0.9": "@types/json-schema@npm:*, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.6, @types/json-schema@npm:^7.0.9":
version: 7.0.15 version: 7.0.15
resolution: "@types/json-schema@npm:7.0.15" resolution: "@types/json-schema@npm:7.0.15"
checksum: a996a745e6c5d60292f36731dd41341339d4eeed8180bb09226e5c8d23759067692b1d88e5d91d72ee83dfc00d3aca8e7bd43ea120516c17922cbcb7c3e252db checksum: a996a745e6c5d60292f36731dd41341339d4eeed8180bb09226e5c8d23759067692b1d88e5d91d72ee83dfc00d3aca8e7bd43ea120516c17922cbcb7c3e252db
@ -6105,6 +6197,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"acorn@npm:^8.14.0":
version: 8.14.0
resolution: "acorn@npm:8.14.0"
bin:
acorn: bin/acorn
checksum: 6d4ee461a7734b2f48836ee0fbb752903606e576cc100eb49340295129ca0b452f3ba91ddd4424a1d4406a98adfb2ebb6bd0ff4c49d7a0930c10e462719bbfd7
languageName: node
linkType: hard
"agent-base@npm:6": "agent-base@npm:6":
version: 6.0.2 version: 6.0.2
resolution: "agent-base@npm:6.0.2" resolution: "agent-base@npm:6.0.2"
@ -8011,6 +8112,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"cross-spawn@npm:^7.0.6":
version: 7.0.6
resolution: "cross-spawn@npm:7.0.6"
dependencies:
path-key: "npm:^3.1.0"
shebang-command: "npm:^2.0.0"
which: "npm:^2.0.1"
checksum: 053ea8b2135caff68a9e81470e845613e374e7309a47731e81639de3eaeb90c3d01af0e0b44d2ab9d50b43467223b88567dfeb3262db942dc063b9976718ffc1
languageName: node
linkType: hard
"crypto-random-string@npm:^2.0.0": "crypto-random-string@npm:^2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "crypto-random-string@npm:2.0.0" resolution: "crypto-random-string@npm:2.0.0"
@ -9695,6 +9807,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"eslint-scope@npm:^8.2.0":
version: 8.2.0
resolution: "eslint-scope@npm:8.2.0"
dependencies:
esrecurse: "npm:^4.3.0"
estraverse: "npm:^5.2.0"
checksum: 8d2d58e2136d548ac7e0099b1a90d9fab56f990d86eb518de1247a7066d38c908be2f3df477a79cf60d70b30ba18735d6c6e70e9914dca2ee515a729975d70d6
languageName: node
linkType: hard
"eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.1, eslint-visitor-keys@npm:^3.4.3": "eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.1, eslint-visitor-keys@npm:^3.4.3":
version: 3.4.3 version: 3.4.3
resolution: "eslint-visitor-keys@npm:3.4.3" resolution: "eslint-visitor-keys@npm:3.4.3"
@ -9709,6 +9831,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"eslint-visitor-keys@npm:^4.2.0":
version: 4.2.0
resolution: "eslint-visitor-keys@npm:4.2.0"
checksum: 2ed81c663b147ca6f578312919483eb040295bbab759e5a371953456c636c5b49a559883e2677112453728d66293c0a4c90ab11cab3428cf02a0236d2e738269
languageName: node
linkType: hard
"eslint@npm:^8.57.0": "eslint@npm:^8.57.0":
version: 8.57.0 version: 8.57.0
resolution: "eslint@npm:8.57.0" resolution: "eslint@npm:8.57.0"
@ -9806,6 +9935,55 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"eslint@npm:^9.17.0":
version: 9.17.0
resolution: "eslint@npm:9.17.0"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.2.0"
"@eslint-community/regexpp": "npm:^4.12.1"
"@eslint/config-array": "npm:^0.19.0"
"@eslint/core": "npm:^0.9.0"
"@eslint/eslintrc": "npm:^3.2.0"
"@eslint/js": "npm:9.17.0"
"@eslint/plugin-kit": "npm:^0.2.3"
"@humanfs/node": "npm:^0.16.6"
"@humanwhocodes/module-importer": "npm:^1.0.1"
"@humanwhocodes/retry": "npm:^0.4.1"
"@types/estree": "npm:^1.0.6"
"@types/json-schema": "npm:^7.0.15"
ajv: "npm:^6.12.4"
chalk: "npm:^4.0.0"
cross-spawn: "npm:^7.0.6"
debug: "npm:^4.3.2"
escape-string-regexp: "npm:^4.0.0"
eslint-scope: "npm:^8.2.0"
eslint-visitor-keys: "npm:^4.2.0"
espree: "npm:^10.3.0"
esquery: "npm:^1.5.0"
esutils: "npm:^2.0.2"
fast-deep-equal: "npm:^3.1.3"
file-entry-cache: "npm:^8.0.0"
find-up: "npm:^5.0.0"
glob-parent: "npm:^6.0.2"
ignore: "npm:^5.2.0"
imurmurhash: "npm:^0.1.4"
is-glob: "npm:^4.0.0"
json-stable-stringify-without-jsonify: "npm:^1.0.1"
lodash.merge: "npm:^4.6.2"
minimatch: "npm:^3.1.2"
natural-compare: "npm:^1.4.0"
optionator: "npm:^0.9.3"
peerDependencies:
jiti: "*"
peerDependenciesMeta:
jiti:
optional: true
bin:
eslint: bin/eslint.js
checksum: 9edd8dd782b4ae2eb00a158ed4708194835d4494d75545fa63a51f020ed17f865c49b4ae1914a2ecbc7fdb262bd8059e811aeef9f0bae63dced9d3293be1bbdd
languageName: node
linkType: hard
"espree@npm:^10.0.1, espree@npm:^10.1.0": "espree@npm:^10.0.1, espree@npm:^10.1.0":
version: 10.1.0 version: 10.1.0
resolution: "espree@npm:10.1.0" resolution: "espree@npm:10.1.0"
@ -9817,6 +9995,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"espree@npm:^10.3.0":
version: 10.3.0
resolution: "espree@npm:10.3.0"
dependencies:
acorn: "npm:^8.14.0"
acorn-jsx: "npm:^5.3.2"
eslint-visitor-keys: "npm:^4.2.0"
checksum: 272beeaca70d0a1a047d61baff64db04664a33d7cfb5d144f84bc8a5c6194c6c8ebe9cc594093ca53add88baa23e59b01e69e8a0160ab32eac570482e165c462
languageName: node
linkType: hard
"espree@npm:^9.0.0, espree@npm:^9.6.0, espree@npm:^9.6.1": "espree@npm:^9.0.0, espree@npm:^9.6.0, espree@npm:^9.6.1":
version: 9.6.1 version: 9.6.1
resolution: "espree@npm:9.6.1" resolution: "espree@npm:9.6.1"
@ -11398,9 +11587,7 @@ __metadata:
"@types/node": "npm:^20.10.4" "@types/node": "npm:^20.10.4"
"@typescript-eslint/eslint-plugin": "npm:^8.7.0" "@typescript-eslint/eslint-plugin": "npm:^8.7.0"
"@typescript-eslint/parser": "npm:^8.7.0" "@typescript-eslint/parser": "npm:^8.7.0"
eslint: "npm:^8.57.0" eslint: "npm:^9.17.0"
eslint-plugin-import-x: "npm:^4.1.1"
eslint-plugin-n: "npm:^17.10.2"
jest: "npm:^29.5.0" jest: "npm:^29.5.0"
jest-environment-miniflare: "npm:^2.14.1" jest-environment-miniflare: "npm:^2.14.1"
npm-run-all2: "npm:^6.2.2" npm-run-all2: "npm:^6.2.2"