Compare commits

..

8 Commits

Author SHA1 Message Date
github-actions[bot] 0dc6a1f5e6
Version Packages (#1325)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-21 15:04:24 +09:00
James Talmage d89fed7eec
feat(auth-js): Support async ConfigHandler (#1324)
* feat(auth-js): Support async ConfigHandler

* add changeset

* format
2025-07-21 14:55:46 +09:00
github-actions[bot] b24925168a
Version Packages (#1323)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-20 08:36:17 +09:00
Andrei 2a46bfbba0
fix(standard-validator): arktype leaking headers (#1282)
* fix(standard-validator): arktype strip headers from error output

* chore(standard-validator): fix formatting

* feat(changeset): add changeset

* fix: change header schema to lowercase and cleanup tests

* fix: strip out sensitive fields for valibot

* chore: refactor, cleanup and add jsdoc
2025-07-20 08:30:09 +09:00
github-actions[bot] 28601b311b
Version Packages (#1321)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-19 18:53:02 +09:00
Yusuke Wada 5a3cbc4cc6
chore: fix the changeset (#1322) 2025-07-19 18:47:26 +09:00
Yusuke Wada e835cf6183
chore: add changeset (#1320) 2025-07-19 18:26:04 +09:00
Yusuke Wada 2339eefa1c
fix(zod-openapi): correct importing `zod` (#1319) 2025-07-19 18:03:34 +09:00
18 changed files with 265 additions and 11 deletions

View File

@ -1,5 +1,11 @@
# @hono/auth-js # @hono/auth-js
## 1.1.0
### Minor Changes
- [#1324](https://github.com/honojs/middleware/pull/1324) [`d89fed7eecfa151c18b0a8cf95dae1dfe83dfec2`](https://github.com/honojs/middleware/commit/d89fed7eecfa151c18b0a8cf95dae1dfe83dfec2) Thanks [@jamestalmage](https://github.com/jamestalmage)! - Allow async authjs Config
## 1.0.17 ## 1.0.17
### Patch Changes ### Patch Changes

View File

@ -1,6 +1,6 @@
{ {
"name": "@hono/auth-js", "name": "@hono/auth-js",
"version": "1.0.17", "version": "1.1.0",
"license": "MIT", "license": "MIT",
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",

View File

@ -1,6 +1,6 @@
{ {
"name": "@hono/auth-js", "name": "@hono/auth-js",
"version": "1.0.17", "version": "1.1.0",
"description": "A third-party Auth js middleware for Hono", "description": "A third-party Auth js middleware for Hono",
"main": "dist/index.js", "main": "dist/index.js",
"type": "module", "type": "module",

View File

@ -47,6 +47,27 @@ describe('Config', () => {
expect(res.status).toBe(200) expect(res.status).toBe(200)
}) })
it('Should allow async ConfigHandler', async () => {
globalThis.process.env = { AUTH_SECRET: 'secret' }
const app = new Hono()
app.use(
'/*',
initAuthConfig(async () => {
await new Promise((resolve) => setTimeout(resolve, 1))
return {
basePath: '/api/auth',
providers: [],
}
})
)
app.use('/api/auth/*', authHandler())
const req = new Request('http://localhost/api/auth/signin')
const res = await app.request(req)
expect(res.status).toBe(200)
})
it('Should return 401 is if auth cookie is invalid or missing', async () => { it('Should return 401 is if auth cookie is invalid or missing', async () => {
const app = new Hono() const app = new Hono()

View File

@ -29,7 +29,7 @@ export type AuthUser = {
export interface AuthConfig extends Omit<AuthConfigCore, 'raw'> {} export interface AuthConfig extends Omit<AuthConfigCore, 'raw'> {}
export type ConfigHandler = (c: Context) => AuthConfig export type ConfigHandler = (c: Context) => AuthConfig | Promise<AuthConfig>
export function setEnvDefaults(env: AuthEnv, config: AuthConfig): void { export function setEnvDefaults(env: AuthEnv, config: AuthConfig): void {
config.secret ??= env.AUTH_SECRET config.secret ??= env.AUTH_SECRET
@ -118,7 +118,7 @@ export function verifyAuth(): MiddlewareHandler {
export function initAuthConfig(cb: ConfigHandler): MiddlewareHandler { export function initAuthConfig(cb: ConfigHandler): MiddlewareHandler {
return async (c, next) => { return async (c, next) => {
const config = cb(c) const config = await cb(c)
c.set('authConfig', config) c.set('authConfig', config)
await next() await next()
} }

View File

@ -1,5 +1,11 @@
# @hono/standard-validator # @hono/standard-validator
## 0.1.4
### Patch Changes
- [#1282](https://github.com/honojs/middleware/pull/1282) [`2a46bfbba0695ae713b58211fd5e83a08b829ad6`](https://github.com/honojs/middleware/commit/2a46bfbba0695ae713b58211fd5e83a08b829ad6) Thanks [@MonsterDeveloper](https://github.com/MonsterDeveloper)! - Fix cookies output for arktype in standard schema validator
## 0.1.3 ## 0.1.3
### Patch Changes ### Patch Changes

View File

@ -26,7 +26,12 @@ const querySortSchema = type({
order: "'asc'|'desc'", order: "'asc'|'desc'",
}) })
const headerSchema = type({
'user-agent': 'string',
})
export { export {
headerSchema,
idJSONSchema, idJSONSchema,
personJSONSchema, personJSONSchema,
postJSONSchema, postJSONSchema,

View File

@ -28,7 +28,12 @@ const querySortSchema = object({
order: picklist(['asc', 'desc']), order: picklist(['asc', 'desc']),
}) })
const headerSchema = object({
'user-agent': string(),
})
export { export {
headerSchema,
idJSONSchema, idJSONSchema,
personJSONSchema, personJSONSchema,
postJSONSchema, postJSONSchema,

View File

@ -28,7 +28,12 @@ const querySortSchema = z.object({
order: z.enum(['asc', 'desc']), order: z.enum(['asc', 'desc']),
}) })
const headerSchema = z.object({
'user-agent': z.string(),
})
export { export {
headerSchema,
idJSONSchema, idJSONSchema,
personJSONSchema, personJSONSchema,
postJSONSchema, postJSONSchema,

View File

@ -1,6 +1,6 @@
{ {
"name": "@hono/standard-validator", "name": "@hono/standard-validator",
"version": "0.1.3", "version": "0.1.4",
"license": "MIT", "license": "MIT",
"exports": { "exports": {
".": "./src/index.ts" ".": "./src/index.ts"

View File

@ -1,6 +1,6 @@
{ {
"name": "@hono/standard-validator", "name": "@hono/standard-validator",
"version": "0.1.3", "version": "0.1.4",
"description": "Validator middleware using Standard Schema", "description": "Validator middleware using Standard Schema",
"type": "module", "type": "module",
"main": "dist/index.cjs", "main": "dist/index.cjs",

View File

@ -352,6 +352,47 @@ describe('Standard Schema Validation', () => {
> >
}) })
}) })
describe('Sensitive Data Removal', () => {
it("doesn't return cookies after headers validation", async () => {
const app = new Hono()
const schema = schemas.headerSchema
app.get('/headers', sValidator('header', schema), (c) =>
c.json({ success: true, userAgent: c.req.header('User-Agent') })
)
const req = new Request('http://localhost/headers', {
headers: {
// Not passing the User-Agent header to trigger the validation error
Cookie: 'SECRET=123',
},
})
const res = await app.request(req)
expect(res.status).toBe(400)
const data = (await res.json()) as { success: false; error: unknown[] }
expect(data.success).toBe(false)
expect(data.error).toBeDefined()
if (lib === 'arktype') {
expect(
(data.error as { data: Record<string, unknown> }[]).some(
(error) => error.data && error.data.cookie
)
).toBe(false)
}
if (lib === 'valibot') {
expect(
(data.error as { path: { input: Record<string, unknown> }[] }[]).some((error) =>
error.path.some((path) => path.input.cookie)
)
).toBe(false)
}
})
})
}) })
}) })
}) })

View File

@ -1,6 +1,7 @@
import type { StandardSchemaV1 } from '@standard-schema/spec' import type { StandardSchemaV1 } from '@standard-schema/spec'
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 { sanitizeIssues } from './sanitize-issues'
type HasUndefined<T> = undefined extends T ? true : false type HasUndefined<T> = undefined extends T ? true : false
type TOrPromiseOfT<T> = T | Promise<T> type TOrPromiseOfT<T> = T | Promise<T>
@ -21,6 +22,67 @@ type Hook<
c: Context<E, P> c: Context<E, P>
) => TOrPromiseOfT<Response | void | TypedResponse<O>> ) => TOrPromiseOfT<Response | void | TypedResponse<O>>
/**
* Validation middleware for libraries that support [Standard Schema](https://standardschema.dev/) specification.
*
* This middleware validates incoming request data against a provided schema
* that conforms to the Standard Schema specification. It supports validation
* of JSON bodies, headers, queries, forms, and other request targets.
*
* @param target - The request target to validate ('json', 'header', 'query', 'form', etc.)
* @param schema - A schema object conforming to Standard Schema specification
* @param hook - Optional hook function called with validation results for custom error handling
* @returns A Hono middleware handler that validates requests and makes validated data available via `c.req.valid()`
*
* @example Basic JSON validation
* ```ts
* import { z } from 'zod'
* import { sValidator } from '@hono/standard-validator'
*
* const schema = z.object({
* name: z.string(),
* age: z.number(),
* })
*
* app.post('/author', sValidator('json', schema), (c) => {
* const data = c.req.valid('json')
* return c.json({
* success: true,
* message: `${data.name} is ${data.age}`,
* })
* })
* ```
*
* @example With custom error handling hook
* ```ts
* app.post(
* '/post',
* sValidator('json', schema, (result, c) => {
* if (!result.success) {
* return c.text('Invalid!', 400)
* }
* }),
* (c) => {
* // Handler code
* }
* )
* ```
*
* @example Header validation
* ```ts
* import { object, string } from 'valibot'
*
* const schema = object({
* 'content-type': string(),
* 'user-agent': string(),
* })
*
* app.post('/author', sValidator('header', schema), (c) => {
* const headers = c.req.valid('header')
* // do something with headers
* })
* ```
*/
const sValidator = < const sValidator = <
Schema extends StandardSchemaV1, Schema extends StandardSchemaV1,
Target extends keyof ValidationTargets, Target extends keyof ValidationTargets,
@ -71,7 +133,9 @@ const sValidator = <
} }
if (result.issues) { if (result.issues) {
return c.json({ data: value, error: result.issues, success: false }, 400) const processedIssues = sanitizeIssues(result.issues, schema['~standard'].vendor, target)
return c.json({ data: value, error: processedIssues, success: false }, 400)
} }
return result.value as StandardSchemaV1.InferOutput<Schema> return result.value as StandardSchemaV1.InferOutput<Schema>

View File

@ -0,0 +1,95 @@
import type { StandardSchemaV1 } from '@standard-schema/spec'
import type { ValidationTargets } from 'hono'
const RESTRICTED_DATA_FIELDS = {
header: ['cookie'],
}
/**
* Sanitizes validation issues by removing sensitive data fields from error messages.
*
* This function removes potentially sensitive information (like cookies) from validation
* error messages before they are returned to the client. It handles different validation
* library formats based on the vendor string.
*
* @param issues - Array of validation issues from Standard Schema validation
* @param vendor - The validation library vendor identifier (e.g., 'arktype', 'valibot')
* @param target - The validation target being processed ('header', 'json', etc.)
* @returns Sanitized array of validation issues with sensitive data removed
*
* @example
* ```ts
* const issues = [{ message: 'Invalid header', data: { cookie: 'secret' } }]
* const sanitized = sanitizeIssues(issues, 'arktype', 'header')
* // Returns issues with cookie field removed from data
* ```
*/
export function sanitizeIssues(
issues: readonly StandardSchemaV1.Issue[],
vendor: string,
target: keyof ValidationTargets
): readonly StandardSchemaV1.Issue[] {
if (!(target in RESTRICTED_DATA_FIELDS)) {
return issues
}
const restrictedFields =
RESTRICTED_DATA_FIELDS[target as keyof typeof RESTRICTED_DATA_FIELDS] || []
if (vendor === 'arktype') {
return sanitizeArktypeIssues(issues, restrictedFields)
}
if (vendor === 'valibot') {
return sanitizeValibotIssues(issues, restrictedFields)
}
return issues
}
function sanitizeArktypeIssues(
issues: readonly StandardSchemaV1.Issue[],
restrictedFields: string[]
): readonly StandardSchemaV1.Issue[] {
return issues.map((issue) => {
if (
issue &&
typeof issue === 'object' &&
'data' in issue &&
typeof issue.data === 'object' &&
issue.data !== null &&
!Array.isArray(issue.data)
) {
const dataCopy = { ...(issue.data as Record<string, unknown>) }
for (const field of restrictedFields) {
delete dataCopy[field]
}
return { ...issue, data: dataCopy }
}
return issue
}) as readonly StandardSchemaV1.Issue[]
}
function sanitizeValibotIssues(
issues: readonly StandardSchemaV1.Issue[],
restrictedFields: string[]
): readonly StandardSchemaV1.Issue[] {
return issues.map((issue) => {
if (issue && typeof issue === 'object' && 'path' in issue && Array.isArray(issue.path)) {
for (const path of issue.path) {
if (
typeof path === 'object' &&
'input' in path &&
typeof path.input === 'object' &&
path.input !== null &&
!Array.isArray(path.input)
) {
for (const field of restrictedFields) {
delete path.input[field]
}
}
}
}
return issue
}) as readonly StandardSchemaV1.Issue[]
}

View File

@ -1,5 +1,11 @@
# @hono/zod-openapi # @hono/zod-openapi
## 1.0.2
### Patch Changes
- [#1320](https://github.com/honojs/middleware/pull/1320) [`e835cf6183fb4717cfdbe156d9b3d423625a0e15`](https://github.com/honojs/middleware/commit/e835cf6183fb4717cfdbe156d9b3d423625a0e15) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: correct importing `zod`
## 1.0.1 ## 1.0.1
### Patch Changes ### Patch Changes

View File

@ -1,6 +1,6 @@
{ {
"name": "@hono/zod-openapi", "name": "@hono/zod-openapi",
"version": "1.0.1", "version": "1.0.2",
"license": "MIT", "license": "MIT",
"exports": { "exports": {
".": "./src/index.ts" ".": "./src/index.ts"

View File

@ -1,6 +1,6 @@
{ {
"name": "@hono/zod-openapi", "name": "@hono/zod-openapi",
"version": "1.0.1", "version": "1.0.2",
"description": "A wrapper class of Hono which supports OpenAPI.", "description": "A wrapper class of Hono which supports OpenAPI.",
"type": "module", "type": "module",
"module": "dist/index.js", "module": "dist/index.js",

View File

@ -39,8 +39,8 @@ import type { JSONParsed, JSONValue, RemoveBlankRecord, SimplifyDeepArray } from
import { mergePath } from 'hono/utils/url' import { mergePath } from 'hono/utils/url'
import type { OpenAPIObject } from 'openapi3-ts/oas30' import type { OpenAPIObject } from 'openapi3-ts/oas30'
import type { OpenAPIObject as OpenAPIV31bject } from 'openapi3-ts/oas31' import type { OpenAPIObject as OpenAPIV31bject } from 'openapi3-ts/oas31'
import { ZodType, z } from 'zod/v4' import { ZodType, z } from 'zod'
import type { ZodError } from 'zod/v4' import type { ZodError } from 'zod'
type MaybePromise<T> = Promise<T> | T type MaybePromise<T> = Promise<T> | T