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",
"@typescript-eslint/eslint-plugin": "^8.7.0",
"@typescript-eslint/parser": "^8.7.0",
"eslint": "^8.57.0",
"eslint-plugin-import-x": "^4.1.1",
"eslint-plugin-n": "^17.10.2",
"eslint": "^9.17.0",
"jest": "^29.5.0",
"jest-environment-miniflare": "^2.14.1",
"npm-run-all2": "^6.2.2",

View File

@ -1,11 +1,12 @@
import type { Context, Env, MiddlewareHandler, ValidationTargets } from 'hono';
import { validator } from 'hono/validator';
import { Ajv, type JSONSchemaType, type ErrorObject } from 'ajv';
import { Ajv } from 'ajv'
import type { JSONSchemaType, 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> = (
result: { success: true; data: T } | { success: false; errors: ErrorObject[] },
c: Context<E, P>
) => Response | Promise<Response> | void;
) => Response | Promise<Response> | void
/**
* Hono middleware that validates incoming data via an Ajv JSON schema.
@ -75,32 +76,32 @@ export function ajvValidator<
E,
P,
{
in: { [K in Target]: T };
out: { [K in Target]: T };
in: { [K in Target]: T }
out: { [K in Target]: T }
}
> {
const ajv = new Ajv();
const validate = ajv.compile(schema);
const ajv = new Ajv()
const validate = ajv.compile(schema)
return validator(target, (data, c) => {
const valid = validate(data);
const valid = validate(data)
if (valid) {
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) {
return hookResult;
return hookResult
}
}
return data as T;
return data as T
}
const errors = validate.errors || [];
const errors = validate.errors || []
if (hook) {
const hookResult = hook({ success: false, errors }, c);
const hookResult = hook({ success: false, errors }, c)
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 { Equal, Expect } from 'hono/utils/types';
import { ajvValidator } from '../src';
import { JSONSchemaType, type ErrorObject } from 'ajv';
import type { JSONSchemaType, type ErrorObject } from 'ajv'
import { Hono } from 'hono'
import type { Equal, Expect } from 'hono/utils/types'
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', () => {
const app = new Hono();
const app = new Hono()
const schema: JSONSchemaType<{ name: string; age: number }> = {
type: 'object',
@ -16,35 +16,35 @@ describe('Basic', () => {
},
required: ['name', 'age'],
additionalProperties: false,
};
}
const route = app.post('/author', ajvValidator('json', schema), (c) => {
const data = c.req.valid('json');
const data = c.req.valid('json')
return c.json({
success: true,
message: `${data.name} is ${data.age}`,
});
});
})
})
type Actual = ExtractSchema<typeof route>;
type Actual = ExtractSchema<typeof route>
type Expected = {
'/author': {
$post: {
input: {
json: {
name: string;
age: number;
};
};
name: string
age: number
}
}
output: {
success: boolean;
message: string;
};
};
};
};
success: boolean
message: string
}
}
}
}
type verify = Expect<Equal<Expected, Actual>>;
type verify = Expect<Equal<Expected, Actual>>
it('Should return 200 response', async () => {
const req = new Request('http://localhost/author', {
@ -56,15 +56,15 @@ describe('Basic', () => {
headers: {
'Content-Type': 'application/json',
},
});
const res = await app.request(req);
expect(res).not.toBeNull();
expect(res.status).toBe(200);
})
const res = await app.request(req)
expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(await res.json()).toEqual({
success: true,
message: 'Superman is 20',
});
});
})
})
it('Should return 400 response', async () => {
const req = new Request('http://localhost/author', {
@ -76,17 +76,17 @@ describe('Basic', () => {
headers: {
'Content-Type': 'application/json',
},
});
const res = await app.request(req);
expect(res).not.toBeNull();
expect(res.status).toBe(400);
const data = (await res.json()) as { success: boolean };
expect(data.success).toBe(false);
});
});
})
const res = await app.request(req)
expect(res).not.toBeNull()
expect(res.status).toBe(400)
const data = (await res.json()) as { success: boolean }
expect(data.success).toBe(false)
})
})
describe('With Hook', () => {
const app = new Hono();
const app = new Hono()
const schema: JSONSchemaType<{ id: number; title: string }> = {
type: 'object',
@ -96,39 +96,39 @@ describe('With Hook', () => {
},
required: ['id', 'title'],
additionalProperties: false,
};
}
app
.post(
'/post',
ajvValidator('json', schema, (result, c) => {
if (!result.success) {
return c.text('Invalid!', 400);
return c.text('Invalid!', 400)
}
const data = result.data;
return c.text(`${data.id} is valid!`);
const data = result.data
return c.text(`${data.id} is valid!`)
}),
(c) => {
const data = c.req.valid('json');
const data = c.req.valid('json')
return c.json({
success: true,
message: `${data.id} is ${data.title}`,
});
})
}
)
.post(
'/errorTest',
ajvValidator('json', schema, (result, c) => {
return c.json(result, 400);
return c.json(result, 400)
}),
(c) => {
const data = c.req.valid('json');
const data = c.req.valid('json')
return c.json({
success: true,
message: `${data.id} is ${data.title}`,
});
})
}
);
)
it('Should return 200 response', async () => {
const req = new Request('http://localhost/post', {
@ -140,12 +140,12 @@ describe('With Hook', () => {
headers: {
'Content-Type': 'application/json',
},
});
const res = await app.request(req);
expect(res).not.toBeNull();
expect(res.status).toBe(200);
expect(await res.text()).toBe('123 is valid!');
});
})
const res = await app.request(req)
expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(await res.text()).toBe('123 is valid!')
})
it('Should return 400 response', async () => {
const req = new Request('http://localhost/post', {
@ -157,11 +157,11 @@ describe('With Hook', () => {
headers: {
'Content-Type': 'application/json',
},
});
const res = await app.request(req);
expect(res).not.toBeNull();
expect(res.status).toBe(400);
});
})
const res = await app.request(req)
expect(res).not.toBeNull()
expect(res.status).toBe(400)
})
it('Should return 400 response and error array', async () => {
const req = new Request('http://localhost/errorTest', {
@ -172,17 +172,17 @@ describe('With Hook', () => {
headers: {
'Content-Type': 'application/json',
},
});
const res = await app.request(req);
expect(res).not.toBeNull();
expect(res.status).toBe(400);
})
const res = await app.request(req)
expect(res).not.toBeNull()
expect(res.status).toBe(400)
const { errors, success } = (await res.json()) as {
success: boolean;
errors: ErrorObject[];
};
expect(success).toBe(false);
expect(Array.isArray(errors)).toBe(true);
success: boolean
errors: ErrorObject[]
}
expect(success).toBe(false)
expect(Array.isArray(errors)).toBe(true)
expect(
errors.map((e: ErrorObject) => ({
keyword: e.keyword,
@ -195,6 +195,6 @@ describe('With Hook', () => {
instancePath: '',
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 { validator } from 'hono/validator'

View File

@ -93,7 +93,7 @@ export type WindowProps = {
}
export type AuthState = {
status: 'loading' | 'success' | 'errored'
status: 'loading' | 'success' | 'errored'
error?: string
}
@ -163,8 +163,7 @@ export function now() {
export function parseUrl(url?: string) {
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 base = `${parsedUrl.origin}${path}`

View File

@ -1,12 +1,11 @@
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 { JWT } from '@auth/core/jwt'
import type { Session } from '@auth/core/types'
import type { Context, MiddlewareHandler } from 'hono'
import { env } from 'hono/adapter'
import { HTTPException } from 'hono/http-exception'
import { setEnvDefaults as coreSetEnvDefaults } from '@auth/core'
declare module 'hono' {
interface ContextVariableMap {
@ -43,7 +42,9 @@ export function reqWithEnvUrl(req: Request, authUrl?: string) {
const authUrlObj = new URL(authUrl)
const props = ['hostname', 'protocol', 'port', 'password', 'username'] as const
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)
}
@ -51,12 +52,17 @@ export function reqWithEnvUrl(req: Request, authUrl?: string) {
const newReq = new Request(url.href, req)
const proto = newReq.headers.get('x-forwarded-proto')
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) {
url.host = host
const portMatch = host.match(/:(\d+)$/)
if (portMatch) url.port = portMatch[1]
else url.port = ''
if (portMatch) {
url.port = portMatch[1]
} else {
url.port = ''
}
newReq.headers.delete('x-forwarded-host')
newReq.headers.delete('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 {
type AuthClientConfig,
ClientSessionError,
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,
import { useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { ClientSessionError, fetchData, now, parseUrl, useOnline } from './client'
import type {
WindowProps,
AuthState,
AuthClientConfig,
SessionContextValue,
SessionProviderProps,
GetSessionParams,
UseSessionOptions,
LiteralUnion,
SignInOptions,
SignInAuthorizationParams,
SignInResponse,
ClientSafeProvider,
SignOutParams,
SignOutResponse,
} 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 = {
debug: console.debug,
@ -431,7 +427,9 @@ export const useOauthPopupLogin = (
useEffect(() => {
const handleMessage = (event: MessageEvent<AuthState>) => {
if (event.origin !== window.location.origin) return
if (event.origin !== window.location.origin) {
return
}
if (event.data.status) {
setState(event.data)
if (event.data.status === 'success') {

View File

@ -1,9 +1,9 @@
import { webcrypto } from 'node:crypto'
import { skipCSRFCheck } from '@auth/core'
import type { Adapter } from '@auth/core/adapters'
import Credentials from '@auth/core/providers/credentials'
import { Hono } from 'hono'
import { describe, expect, it, vi } from 'vitest'
import { webcrypto } from 'node:crypto'
import type { AuthConfig } 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 { Context } from 'hono'
import { decode } from 'hono/jwt'
import type { JWTPayload } from 'hono/utils/jwt/types'
export const jwtAuthorizer = async (
@ -14,10 +14,14 @@ export const jwtAuthorizer = async (
if (!payload) {
const credentials = c.req.header('Authorization')
if (!credentials) return false
if (!credentials) {
return false
}
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]

View File

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

View File

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

View File

@ -1,8 +1,10 @@
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 { 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).

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 { 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', () => {
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 { env } from 'hono/adapter'

View File

@ -1,24 +1,24 @@
import { Hono } from 'hono'
import { cloudflareAccess } from '../src'
import { describe, expect, it, vi } from 'vitest'
import crypto from 'crypto';
import { promisify } from 'util';
import crypto from 'crypto'
import { promisify } from 'util'
import { cloudflareAccess } from '../src'
const generateKeyPair = promisify(crypto.generateKeyPair);
const generateKeyPair = promisify(crypto.generateKeyPair)
interface KeyPairResult {
publicKey: string;
privateKey: string;
publicKey: string
privateKey: string
}
interface JWK {
kid: string;
kty: string;
alg: string;
use: string;
e: string;
n: string;
kid: string
kty: string
alg: string
use: string
e: string
n: string
}
async function generateJWTKeyPair(): Promise<KeyPairResult> {
@ -27,38 +27,38 @@ async function generateJWTKeyPair(): Promise<KeyPairResult> {
modulusLength: 2048,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
format: 'pem',
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem'
}
});
format: 'pem',
},
})
return {
publicKey,
privateKey
};
privateKey,
}
} 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 {
const hash = crypto.createHash('sha256');
hash.update(Buffer.from(modulusBase64, 'base64'));
return hash.digest('hex');
const hash = crypto.createHash('sha256')
hash.update(Buffer.from(modulusBase64, 'base64'))
return hash.digest('hex')
}
function publicKeyToJWK(publicKey: string): JWK {
// Convert PEM to key object
const keyObject = crypto.createPublicKey(publicKey);
const keyObject = crypto.createPublicKey(publicKey)
// Export the key in JWK format
const jwk = keyObject.export({ format: 'jwk' });
const jwk = keyObject.export({ format: 'jwk' })
// Generate key ID using the modulus
const kid = generateKeyThumbprint(jwk.n as string);
const kid = generateKeyThumbprint(jwk.n as string)
return {
kid,
@ -67,66 +67,65 @@ function publicKeyToJWK(publicKey: string): JWK {
use: 'sig',
e: jwk.e as string,
n: jwk.n as string,
};
}
}
function base64URLEncode(str: string): string {
return Buffer.from(str)
.toString('base64')
.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
const header = {
alg: 'RS256',
typ: 'JWT'
};
typ: 'JWT',
}
// Add expiration to payload
const now = Math.floor(Date.now() / 1000);
const now = Math.floor(Date.now() / 1000)
const fullPayload = {
...payload,
iat: now,
exp: now + expiresIn
};
exp: now + expiresIn,
}
// Encode header and payload
const encodedHeader = base64URLEncode(JSON.stringify(header));
const encodedPayload = base64URLEncode(JSON.stringify(fullPayload));
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);
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}`;
return `${encodedHeader}.${encodedPayload}.${encodedSignature}`
}
describe('Cloudflare Access middleware', async () => {
const keyPair1 = await generateJWTKeyPair();
const keyPair2 = await generateJWTKeyPair();
const keyPair3 = await generateJWTKeyPair();
const keyPair1 = await generateJWTKeyPair()
const keyPair2 = await generateJWTKeyPair()
const keyPair3 = await generateJWTKeyPair()
beforeEach(() => {
vi.clearAllMocks();
vi.clearAllMocks()
vi.stubGlobal('fetch', async () => {
return Response.json({
keys: [
publicKeyToJWK(keyPair1.publicKey),
publicKeyToJWK(keyPair2.publicKey),
],
keys: [publicKeyToJWK(keyPair1.publicKey), publicKeyToJWK(keyPair2.publicKey)],
})
})
});
})
const app = new Hono()
@ -135,9 +134,12 @@ describe('Cloudflare Access middleware', async () => {
app.get('/access-payload', (c) => c.json(c.get('accessPayload')))
app.onError((err, c) => {
return c.json({
err: err.toString(),
}, 500)
return c.json(
{
err: err.toString(),
},
500
)
})
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 () => {
const res = await app.request('http://localhost/hello-behind-access', {
headers: {
'cf-access-jwt-assertion': 'asdasdasda'
}
'cf-access-jwt-assertion': 'asdasdasda',
},
})
expect(res).not.toBeNull()
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 () => {
const token = generateJWT(keyPair1.privateKey, {
sub: '1234567890',
}, -3600);
const token = generateJWT(
keyPair1.privateKey,
{
sub: '1234567890',
},
-3600
)
const res = await app.request('http://localhost/hello-behind-access', {
headers: {
'cf-access-jwt-assertion': token
}
'cf-access-jwt-assertion': token,
},
})
expect(res).not.toBeNull()
expect(res.status).toBe(401)
@ -177,28 +183,30 @@ describe('Cloudflare Access middleware', async () => {
const token = generateJWT(keyPair1.privateKey, {
sub: '1234567890',
iss: 'https://different-team.cloudflareaccess.com',
});
})
const res = await app.request('http://localhost/hello-behind-access', {
headers: {
'cf-access-jwt-assertion': token
}
'cf-access-jwt-assertion': token,
},
})
expect(res).not.toBeNull()
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 () => {
const token = generateJWT(keyPair3.privateKey, {
sub: '1234567890',
iss: 'https://my-cool-team-name.cloudflareaccess.com',
});
})
const res = await app.request('http://localhost/hello-behind-access', {
headers: {
'cf-access-jwt-assertion': token
}
'cf-access-jwt-assertion': token,
},
})
expect(res).not.toBeNull()
expect(res.status).toBe(401)
@ -209,12 +217,12 @@ describe('Cloudflare Access middleware', async () => {
const token = generateJWT(keyPair1.privateKey, {
sub: '1234567890',
iss: 'https://my-cool-team-name.cloudflareaccess.com',
});
})
const res = await app.request('http://localhost/hello-behind-access', {
headers: {
'cf-access-jwt-assertion': token
}
'cf-access-jwt-assertion': token,
},
})
expect(res).not.toBeNull()
expect(res.status).toBe(200)
@ -225,12 +233,12 @@ describe('Cloudflare Access middleware', async () => {
const token = generateJWT(keyPair2.privateKey, {
sub: '1234567890',
iss: 'https://my-cool-team-name.cloudflareaccess.com',
});
})
const res = await app.request('http://localhost/hello-behind-access', {
headers: {
'cf-access-jwt-assertion': token
}
'cf-access-jwt-assertion': token,
},
})
expect(res).not.toBeNull()
expect(res.status).toBe(200)
@ -241,50 +249,54 @@ describe('Cloudflare Access middleware', async () => {
const token = generateJWT(keyPair1.privateKey, {
sub: '1234567890',
iss: 'https://my-cool-team-name.cloudflareaccess.com',
});
})
const res = await app.request('http://localhost/access-payload', {
headers: {
'cf-access-jwt-assertion': token
}
'cf-access-jwt-assertion': token,
},
})
expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(await res.json()).toEqual({
"sub":"1234567890",
"iss":"https://my-cool-team-name.cloudflareaccess.com",
"iat":expect.any(Number),
"exp":expect.any(Number)
sub: '1234567890',
iss: 'https://my-cool-team-name.cloudflareaccess.com',
iat: expect.any(Number),
exp: expect.any(Number),
})
})
it('Should throw an error, if the access organization does not exist', async () => {
vi.stubGlobal('fetch', async () => {
return Response.json({success: false}, {status: 404})
return Response.json({ success: false }, { status: 404 })
})
const res = await app.request('http://localhost/hello-behind-access', {
headers: {
'cf-access-jwt-assertion': 'asdads'
}
'cf-access-jwt-assertion': 'asdads',
},
})
expect(res).not.toBeNull()
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 () => {
vi.stubGlobal('fetch', async () => {
return Response.json({success: false}, {status: 500})
return Response.json({ success: false }, { status: 500 })
})
const res = await app.request('http://localhost/hello-behind-access', {
headers: {
'cf-access-jwt-assertion': 'asdads'
}
'cf-access-jwt-assertion': 'asdads',
},
})
expect(res).not.toBeNull()
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 { Context } from 'hono'
import { HTTPException } from 'hono/http-exception'
export type CloudflareAccessPayload = {
aud: string[],
email: string,
exp: number,
iat: number,
nbf: number,
iss: string,
type: string,
identity_nonce: string,
sub: string,
country: string,
aud: string[]
email: string
exp: number
iat: number
nbf: number
iss: string
type: string
identity_nonce: string
sub: string
country: string
}
export type CloudflareAccessVariables = {
@ -39,7 +39,9 @@ export const cloudflareAccess = (accessTeamName: string) => {
return createMiddleware(async (c, next) => {
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
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?
const expiryDate = new Date(token.payload.exp * 1000)
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?
if (!(await isValidJwtSignature(token, cacheKeys)))
if (!(await isValidJwtSignature(token, cacheKeys))) {
return c.text('Authentication error: Invalid Token', 401)
}
// Is signed from the correct team?
const expectedIss = `https://${accessTeamName}.cloudflareaccess.com`
if (token.payload?.iss !== expectedIss)
if (token.payload?.iss !== expectedIss) {
return c.text(
`Authentication error: Expected team name ${expectedIss}, but received ${token.payload?.iss}`,
401
)
}
c.set('accessPayload', token.payload)
await next()
@ -83,7 +89,6 @@ async function getPublicKeys(accessTeamName: string) {
const result = await fetch(jwtUrl, {
method: 'GET',
// @ts-ignore
cf: {
// Dont cache error responses
cacheTtlByStatus: { '200-299': 30, '300-599': 0 },
@ -92,16 +97,20 @@ async function getPublicKeys(accessTeamName: string) {
if (!result.ok) {
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()
// 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> = {}
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)) {
const isValid = await validateSingleKey(key, signature, data)
if (isValid) return true
if (isValid) {
return true
}
}
return false

View File

@ -1,7 +1,7 @@
/* 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 { Context, Env, Input as HonoInput, MiddlewareHandler, ValidationTargets } from 'hono'
import { getFormDataFromContext } from './utils'
type FormTargetValue = ValidationTargets['form']['string']

View File

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

View File

@ -1,10 +1,10 @@
import type { ExtractSchema, ParsedFormValue } from 'hono/types'
import type { Equal, Expect } from 'hono/utils/types'
import type { StatusCode } from 'hono/utils/http-status'
import * as v from 'valibot'
import { parseWithValibot } from 'conform-to-valibot'
import { Hono } from 'hono'
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'
describe('Validate requests using a Valibot schema', () => {

View File

@ -1,10 +1,10 @@
import type { ExtractSchema, ParsedFormValue } from 'hono/types'
import type { Equal, Expect } from 'hono/utils/types'
import type { StatusCode } from 'hono/utils/http-status'
import * as y from 'yup'
import { parseWithYup } from '@conform-to/yup'
import { Hono } from 'hono'
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'
describe('Validate requests using a Yup schema', () => {

View File

@ -1,10 +1,10 @@
import type { ExtractSchema, ParsedFormValue } from 'hono/types'
import type { Equal, Expect } from 'hono/utils/types'
import type { StatusCode } from 'hono/utils/http-status'
import * as z from 'zod'
import { parseWithZod } from '@conform-to/zod'
import { Hono } from 'hono'
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'
describe('Validate requests using a Zod schema', () => {

View File

@ -1,8 +1,8 @@
import { Hono } from 'hono'
import type {Context} from 'hono'
import { Hono } from 'hono'
import type { Context } from 'hono'
import { describe, expect, it, vi } from 'vitest'
import { createEmitter, defineHandler, defineHandlers, emitter } from './index'
import type {Emitter} from './index' // Adjust the import path as needed
import { createEmitter, defineHandler, defineHandlers, emitter } from './index'
import type { Emitter } from './index' // Adjust the import path as needed
describe('Event Emitter Middleware', () => {
describe('createEmitter', () => {

View File

@ -1,32 +1,31 @@
interface CloseEventInit extends EventInit {
code?: number;
reason?: string;
wasClean?: boolean;
code?: number
reason?: string
wasClean?: boolean
}
/**
* @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
constructor(
type: string,
eventInitDict: CloseEventInit = {}
) {
super(type, eventInitDict)
this.#eventInitDict = eventInitDict
constructor(type: string, eventInitDict: CloseEventInit = {}) {
super(type, eventInitDict)
this.#eventInitDict = eventInitDict
}
get wasClean(): boolean {
return this.#eventInitDict.wasClean ?? false
return this.#eventInitDict.wasClean ?? false
}
get code(): number {
return this.#eventInitDict.code ?? 0
return this.#eventInitDict.code ?? 0
}
get reason(): string {
return this.#eventInitDict.reason ?? ''
return this.#eventInitDict.reason ?? ''
}
}
}

View File

@ -1,9 +1,9 @@
import { serve } from '@hono/node-server'
import type { ServerType } from '@hono/node-server/dist/types'
import { Hono } from 'hono'
import type { WSMessageReceive } from 'hono/ws'
import { WebSocket } from 'ws'
import { createNodeWebSocket } from '.'
import type { WSMessageReceive } from 'hono/ws'
describe('WebSocket helper', () => {
let app: Hono
@ -13,7 +13,6 @@ describe('WebSocket helper', () => {
beforeEach(async () => {
app = new Hono()
;({ injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app }))
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 { UpgradeWebSocket, WSContext } from 'hono/ws'
import type { WebSocket } from 'ws'
import { WebSocketServer } from 'ws'
import type { IncomingMessage } from 'http'
import type { Server } from 'node:http'
import type { Http2SecureServer, Http2Server } from 'node:http2'
import { CloseEvent } from './events'
export interface NodeWebSocket {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
import crypto from 'node:crypto'
import { jest } from '@jest/globals'
import { Hono } from 'hono'
import jwt from 'jsonwebtoken'
import * as oauth2 from 'oauth4webapi'
import crypto from 'node:crypto'
const MOCK_ISSUER = 'https://accounts.google.com'
const MOCK_CLIENT_ID = 'CLIENT_ID_001'
@ -387,7 +387,9 @@ describe('processOAuthCallback()', () => {
expect(res).not.toBeNull()
expect(res.status).toBe(302)
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 () => {

View File

@ -2,11 +2,8 @@ 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 { createStandardMetrics } from './standardMetrics'
import type { MetricOptions, CustomMetricsOptions } from './standardMetrics'
interface PrometheusOptions {
registry?: Registry

View File

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

View File

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

View File

@ -1,7 +1,8 @@
import type { Context } from 'hono'
import type { Env, MiddlewareHandler } from 'hono/types'
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 '.'
type RendererOptions = {

View File

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

View File

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

View File

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

View File

@ -1,5 +1,7 @@
import { TSchema, Static, TypeGuard, ValueGuard } from '@sinclair/typebox'
import { Value, type ValueError } from '@sinclair/typebox/value'
import type { TSchema, Static } from '@sinclair/typebox'
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 { validator } from 'hono/validator'
import IsObject = ValueGuard.IsObject
@ -7,7 +9,7 @@ import IsArray = ValueGuard.IsArray
export type Hook<T, E extends Env, P extends string> = (
result: { success: true; data: T } | { success: false; errors: ValueError[] },
c: Context<E, P>,
c: Context<E, P>
) => Response | Promise<Response> | void
/**
@ -61,11 +63,18 @@ export function tbValidator<
E extends Env,
P extends string,
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
// compilation pool similar to the Fastify implementation.
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 (hook) {
@ -90,7 +99,9 @@ export function tbValidator<
}
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)) {
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 = {}
for (const key in schema.properties) {
if (obj.hasOwnProperty(key)) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const propertySchema = schema.properties[key]
if (
IsObject(propertySchema) &&
!IsArray(propertySchema)
) {
if (IsObject(propertySchema) && !IsArray(propertySchema)) {
result[key] = removeNonSchemaItems(propertySchema as unknown as TSchema, obj[key])
} else {
result[key] = obj[key]

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import { Hono } from 'hono'
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'
// 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 { 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'
export type Hook<T extends GenericSchema | GenericSchemaAsync, E extends Env, P extends string> = (

View File

@ -1,8 +1,8 @@
import { Hono } from 'hono'
import type { StatusCode } from 'hono/utils/http-status'
import type { Equal, Expect } from 'hono/utils/types'
import { number, object, objectAsync, optional, optionalAsync, string } from 'valibot'
import { vValidator } from '../src'
import { StatusCode } from 'hono/utils/http-status'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
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({
entryPoints: ["src/index.ts"],
format: ["cjs", "esm"],
entryPoints: ['src/index.ts'],
format: ['cjs', 'esm'],
dts: true,
outDir: "dist",
outDir: 'dist',
clean: true,
});
})

View File

@ -26,7 +26,6 @@ import type {
ValidationTargets,
} from 'hono'
import type { MergePath, MergeSchemaPath } from 'hono/types'
import type { JSONParsed, JSONValue, RemoveBlankRecord, SimplifyDeepArray } from 'hono/utils/types'
import type {
ClientErrorStatusCode,
InfoStatusCode,
@ -35,6 +34,7 @@ import type {
StatusCode,
SuccessStatusCode,
} from 'hono/utils/http-status'
import type { JSONParsed, JSONValue, RemoveBlankRecord, SimplifyDeepArray } from 'hono/utils/types'
import { mergePath } from 'hono/utils/url'
import type { ZodError, ZodSchema } 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
}

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 type { ExtractSchema } from 'hono/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', () => {
const RequestSchema = z.object({
@ -276,7 +277,7 @@ describe('Middleware', () => {
})
it('Should infer Env from router middleware', async () => {
const app = new OpenAPIHono<{ Variables: { too: Symbol } }>()
const app = new OpenAPIHono<{ Variables: { too: symbol } }>()
app.openapi(
createRoute({
method: 'get',
@ -308,7 +309,7 @@ describe('Middleware', () => {
type verifyFoo = Expect<Equal<typeof c.var.foo, string>>
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({})
}
@ -316,7 +317,7 @@ describe('Middleware', () => {
})
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(
createRoute({
method: 'get',
@ -331,7 +332,7 @@ describe('Middleware', () => {
(c) => {
c.var.too
type verify = Expect<Equal<typeof c.var.too, Symbol>>
type verify = Expect<Equal<typeof c.var.too, symbol>>
return c.json({})
}

View File

@ -1,14 +1,14 @@
import type { RouteConfig } from '@asteasolutions/zod-to-openapi'
import type { Context, TypedResponse } from 'hono'
import { accepts } from 'hono/accepts'
import { bearerAuth } from 'hono/bearer-auth'
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 { stringify } from 'yaml'
import type { RouteConfigToTypedResponse } 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', () => {
it('Should not require init object', () => {

View File

@ -1,6 +1,7 @@
import type { Context, Env, Input, MiddlewareHandler, TypedResponse, ValidationTargets } from 'hono'
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<
T,

View File

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

197
yarn.lock
View File

@ -2221,6 +2221,13 @@ __metadata:
languageName: node
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":
version: 4.11.1
resolution: "@eslint-community/regexpp@npm:4.11.1"
@ -2246,6 +2253,26 @@ __metadata:
languageName: node
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":
version: 2.1.4
resolution: "@eslint/eslintrc@npm:2.1.4"
@ -2280,6 +2307,23 @@ __metadata:
languageName: node
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":
version: 8.57.0
resolution: "@eslint/js@npm:8.57.0"
@ -2294,6 +2338,13 @@ __metadata:
languageName: node
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":
version: 2.1.4
resolution: "@eslint/object-schema@npm:2.1.4"
@ -2301,6 +2352,13 @@ __metadata:
languageName: node
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":
version: 0.1.0
resolution: "@eslint/plugin-kit@npm:0.1.0"
@ -2310,6 +2368,15 @@ __metadata:
languageName: node
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":
version: 2.1.0
resolution: "@fastify/busboy@npm:2.1.0"
@ -2719,6 +2786,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@hono/oidc-auth@workspace:packages/oidc-auth"
dependencies:
"@jest/globals": "npm:^29.7.0"
"@types/jest": "npm:^29.5.11"
"@types/jsonwebtoken": "npm:^9.0.5"
hono: "npm:^4.0.1"
@ -2958,6 +3026,23 @@ __metadata:
languageName: unknown
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":
version: 0.11.14
resolution: "@humanwhocodes/config-array@npm:0.11.14"
@ -2990,6 +3075,13 @@ __metadata:
languageName: node
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":
version: 2.2.5
resolution: "@iarna/toml@npm:2.2.5"
@ -4908,7 +5000,7 @@ __metadata:
languageName: node
linkType: hard
"@types/estree@npm:1.0.6":
"@types/estree@npm:1.0.6, @types/estree@npm:^1.0.6":
version: 1.0.6
resolution: "@types/estree@npm:1.0.6"
checksum: cdfd751f6f9065442cd40957c07fd80361c962869aa853c1c2fd03e101af8b9389d8ff4955a43a6fcfa223dd387a089937f95be0f3eec21ca527039fd2d9859a
@ -5002,7 +5094,7 @@ __metadata:
languageName: node
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
resolution: "@types/json-schema@npm:7.0.15"
checksum: a996a745e6c5d60292f36731dd41341339d4eeed8180bb09226e5c8d23759067692b1d88e5d91d72ee83dfc00d3aca8e7bd43ea120516c17922cbcb7c3e252db
@ -6105,6 +6197,15 @@ __metadata:
languageName: node
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":
version: 6.0.2
resolution: "agent-base@npm:6.0.2"
@ -8011,6 +8112,17 @@ __metadata:
languageName: node
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":
version: 2.0.0
resolution: "crypto-random-string@npm:2.0.0"
@ -9695,6 +9807,16 @@ __metadata:
languageName: node
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":
version: 3.4.3
resolution: "eslint-visitor-keys@npm:3.4.3"
@ -9709,6 +9831,13 @@ __metadata:
languageName: node
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":
version: 8.57.0
resolution: "eslint@npm:8.57.0"
@ -9806,6 +9935,55 @@ __metadata:
languageName: node
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":
version: 10.1.0
resolution: "espree@npm:10.1.0"
@ -9817,6 +9995,17 @@ __metadata:
languageName: node
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":
version: 9.6.1
resolution: "espree@npm:9.6.1"
@ -11398,9 +11587,7 @@ __metadata:
"@types/node": "npm:^20.10.4"
"@typescript-eslint/eslint-plugin": "npm:^8.7.0"
"@typescript-eslint/parser": "npm:^8.7.0"
eslint: "npm:^8.57.0"
eslint-plugin-import-x: "npm:^4.1.1"
eslint-plugin-n: "npm:^17.10.2"
eslint: "npm:^9.17.0"
jest: "npm:^29.5.0"
jest-environment-miniflare: "npm:^2.14.1"
npm-run-all2: "npm:^6.2.2"