Compare commits

...

93 Commits

Author SHA1 Message Date
Tim Barley 2f0016819d
Merge 05e09f6a4b into 0dc8b719b4 2025-04-28 19:22:49 +03:00
github-actions[bot] 0dc8b719b4
Version Packages (#1145)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-28 15:21:28 +09:00
Andrei 01cd896e9b
fix(arktype-validator): Don't return restricted fields in error responses (#1137)
* fix(arktype-validator): add failing test for cookie header

* fix(arktype-validator): add restricted fields that are not returned in the "data" field of the error

* chore: add changeset
2025-04-28 15:17:34 +09:00
github-actions[bot] 928f8cd5b8
Version Packages (#1144)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-27 20:38:32 +09:00
Shotaro Nakamura 1765a9a3aa
fix(node-ws): make adapter uncrashable (#1141)
* fix(node-ws): make adapter uncrashable

* add changeset

* unnessesary diff

* update yarn.lock

* make changeset patch
2025-04-27 20:34:43 +09:00
github-actions[bot] 247f7705b3
Version Packages (#1143)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-27 20:30:24 +09:00
Yusuke Wada 8ed99d9d79
feat(zod-validator): add `validationFunction` option (#1140)
Co-authored-by: migawka <migawka@amadeustech.dev>
2025-04-27 20:12:13 +09:00
Yusuke Wada b9fa57530a
chore: format codes (#1142) 2025-04-27 19:28:24 +09:00
github-actions[bot] a756d2235b
Version Packages (#1139)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-26 13:12:27 +09:00
Leia 237bff1b82
fix(node-ws):missing code and reason on CloseEvent (#1138)
fixes #1012
2025-04-26 13:09:26 +09:00
Tim Barley 05e09f6a4b
Merge branch 'main' into main 2025-04-09 10:31:04 -04:00
Jonathan Haines 595fa28485 ci(release): yarn config set npmAuthToken (#1117) 2025-04-09 09:28:14 -04:00
chimame 3e13eefc67 chore(conform-validator): Change conform valibot adapter to official library (#1114) 2025-04-09 09:28:14 -04:00
Jonathan Haines 95b87e5b5a ci(release): restore build during release (#1116) 2025-04-09 09:28:14 -04:00
github-actions[bot] b4ceb3a82c Version Packages (#1115)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-09 09:28:14 -04:00
Jonathan Haines e4c5c3d07f fix(zod-openapi): republish without workspace reference (#1111)
fixes #1109
2025-04-09 09:28:14 -04:00
github-actions[bot] 95f746f964 Version Packages (#1108)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-09 09:28:14 -04:00
Jonathan Haines 17df8a47c8 feat(eslint-config): enable linting with type information (#1098) 2025-04-09 09:28:14 -04:00
github-actions[bot] dc917efec0 Version Packages (#1107)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-09 09:28:14 -04:00
Yusuke Wada 59559961bb fix(zod-openapi): infer Env correctly if the middleware is `[]` (#1106)
* fix(zod-openapi): infer Env correctly if the middleware is `[]`

* add changeset
2025-04-09 09:28:14 -04:00
github-actions[bot] b09d7bbc2c Version Packages (#1101)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-09 09:28:14 -04:00
liquidleif 509ead8934 fix(oauth-providers): Update twitter authorization url (#1099)
Closes #1100

* Update twitter authorization url

The twitter authorization URL is outdated.

* add a changeset
2025-04-09 09:28:14 -04:00
Jonathan Haines 741e9d49ff build: typescript project references (#1077)
* build: typescript project references

* chore: remove duplicate keys
2025-04-09 09:28:14 -04:00
github-actions[bot] 7e2652d297 Version Packages (#1095)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-09 09:28:14 -04:00
Shotaro Nakamura d61e8a66ea fix(node-ws): adapter shouldn't send buffer as a event (#1094)
* fix(node-ws): adapter shouldn't send buffer as a event

* chore: changeset
2025-04-09 09:28:14 -04:00
Jonathan Haines 3b5eb36b4f chore(dev-deps): upgrade to hono v4 (#1092)
* chore(dev-deps): upgrade to hono v4

* chore(zod-openapi): build workspace dependencies

* chore(trpc-server): ignore null body type
2025-04-09 09:28:14 -04:00
Jonathan Haines 6edd7bc1ce ci: run eslint in each workflow (#1083) 2025-04-09 09:28:14 -04:00
Yann Normand 226f4e7b9b docs:(zod-openapi): add note about app.route (#1088) 2025-04-09 09:28:14 -04:00
github-actions[bot] 77fce3c040 Version Packages (#1087)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-09 09:28:14 -04:00
Yusuke Wada 8810eccff2 chore(eslint-config): add missing changeset (#1085) 2025-04-09 09:28:14 -04:00
Yusuke Wada db5207c509 fix(eslint-config): add spread to `tseslint.configs.recommended` (#1084) 2025-04-09 09:28:14 -04:00
github-actions[bot] c9dfd2b5a3 Version Packages (#1082)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-09 09:28:14 -04:00
Yusuke Wada e2e4f6aa52 fix(sentry): fix the type error (#1081) 2025-04-09 09:28:14 -04:00
Yusuke Wada 5f3a3fa29c fix(typebox-validator): export modules correctly (#1080) 2025-04-09 09:28:14 -04:00
Yusuke Wada 76f00959c6 refactor(typebox-validator): fix the type error (#1079) 2025-04-09 09:28:14 -04:00
Yusuke Wada 70834f9f70 refactor(zod-openapi): fix type errors (#1078)
* refactor(zod-openapi): fix type errors

* fix types
2025-04-09 09:28:14 -04:00
Aditya Mathur 7cba581041 feat: updated @hono/eslint-config package (#1031)
* chore(eslint-config): update dependencies and improve configuration

* chore(eslint-config): replace @typescript-eslint packages with typescript-eslint

* chore: completed changes suggested by @BarryThePenguin

* chore: updated the repo eslint config

* chore: updated the lockfile

* feat: added ci and minor changes

* chore: updated the eslint version in package.json

* chore: updated the lockfile

* add changeset

* `@ryoppippi/unplugin-typia` as devDependencies

---------

Co-authored-by: Yusuke Wada <yusuke@kamawada.com>
2025-04-09 09:28:14 -04:00
Jonathan Haines 21afe41dec ci: use node v20 (#1076) 2025-04-09 09:28:14 -04:00
Jonathan Haines 5b1e8ae2e5 test: move tests to src directory (#1075)
* test(react-renderer): move tests to src directory

* test: move tests to src directory

* test: ensure vitest-pool-workers is installed at the root
2025-04-09 09:28:14 -04:00
Jonathan Haines e31cd008aa build(react-renderer): lint published package (#1058)
Co-authored-by: Yusuke Wada <yusuke@kamawada.com>
2025-04-09 09:26:27 -04:00
Jonathan Haines 7498fbcff1 build(sentry): lint published package (#1059)
Co-authored-by: Yusuke Wada <yusuke@kamawada.com>
2025-04-09 09:26:27 -04:00
Jonathan Haines 4db6cce55d build(react-compat): lint published package (#1060) 2025-04-09 09:26:27 -04:00
Jonathan Haines 1924c630a8 build(standard-validator): lint published package (#1061)
Co-authored-by: Yusuke Wada <yusuke@kamawada.com>
2025-04-09 09:26:27 -04:00
Yusuke Wada 2e6c051b11 chore: update the lockfile (#1074) 2025-04-09 09:26:27 -04:00
Jonathan Haines 369680c298 build(swagger-ui): lint published package (#1063) 2025-04-09 09:26:27 -04:00
Jonathan Haines ddba7cf343 build(valibot-validator): lint published package (#1068) 2025-04-09 09:26:27 -04:00
Jonathan Haines 8557f3bc0b build(typia-validator): lint published package (#1067) 2025-04-09 09:26:27 -04:00
Yusuke Wada 008ce5eb83 chore: update the lockfile (#1073) 2025-04-09 09:26:27 -04:00
Jonathan Haines b1d32f5783 build(zod-validator): lint published package (#1070) 2025-04-09 09:26:27 -04:00
Jonathan Haines 5a65ef92b0 build(zod-openapi): lint published package (#1069) 2025-04-09 09:26:27 -04:00
Jonathan Haines a2c278515f build(typebox-validator): lint published package (#1066) 2025-04-09 09:26:27 -04:00
Jonathan Haines 72b20df1c8 build(tsyringe): lint published package (#1065) 2025-04-09 09:26:27 -04:00
Jonathan Haines 34c71fead3 build(trpc-server): lint published package (#1064) 2025-04-09 09:26:27 -04:00
Jonathan Haines b73045462e build(swagger-editor): lint published package (#1062) 2025-04-09 09:26:27 -04:00
Jonathan Haines cb6c76fef7 build(oidc-auth): lint published package (#1054)
* build(oidc-auth): lint published package

* build(oidc-auth): include require condition in subpath exports
2025-04-09 09:26:26 -04:00
Jonathan Haines 3529d32b17 build(qwik-city): lint published package (#1057)
* build(qwik-city): lint published package

* ci(qwik-city): add workflow to run build and publint
2025-04-09 09:26:26 -04:00
Jonathan Haines 5f4572f5b8 build(prometheus): lint published package (#1056) 2025-04-09 09:26:26 -04:00
Jonathan Haines 978da3e14b build(otel): lint published package (#1055) 2025-04-09 09:26:26 -04:00
Yusuke Wada 2d91b60742 chore: update the lockfile (#1072) 2025-04-09 09:26:26 -04:00
Jonathan Haines fcce865c87 build(oauth-providers): lint published package (#1053) 2025-04-09 09:26:26 -04:00
Jonathan Haines 9e77615d91 build(node-ws): lint published package (#1052) 2025-04-09 09:26:26 -04:00
Jonathan Haines abd52ce669 build(medley-router): lint published package (#1051) 2025-04-09 09:26:26 -04:00
Jonathan Haines 6a7e42d4ee build(hello): lint published package (#1050) 2025-04-09 09:26:26 -04:00
Jonathan Haines c1adb69a5c build(graphql-server): lint published package (#1049) 2025-04-09 09:26:26 -04:00
Jonathan Haines a5b900e503 build(firebase-auth): lint published package (#1048) 2025-04-09 09:26:26 -04:00
Jonathan Haines 66c265f7ea build(esbuild-transpiler): lint published package (#1046)
* build(esbuild-transpiler): lint published package

* chore: fix repository directory reference
2025-04-09 09:26:26 -04:00
Yusuke Wada 7a8b23d8c8 chore: update the lockfile (#1071) 2025-04-09 09:26:26 -04:00
Jonathan Haines 3b9448b870 build(event-emitter): lint published package (#1047) 2025-04-09 09:26:26 -04:00
Jonathan Haines 23b4cdd414 build(effect-validator): lint published package (#1045) 2025-04-09 09:26:26 -04:00
Jonathan Haines 4f2744b181 build(conform-validator): lint published package (#1044) 2025-04-09 09:26:26 -04:00
Jonathan Haines 8f97fdf83e build(cloudflare-access): lint published package (#1043) 2025-04-09 09:26:26 -04:00
Jonathan Haines 15971bde72 build(clerk-auth): lint published package (#1042) 2025-04-09 09:26:26 -04:00
Jonathan Haines 9e23349777 build(class-validator): lint published package (#1041) 2025-04-09 09:26:26 -04:00
Jonathan Haines d65cc146f9 build(auth-js): lint published package (#1034)
* build(auth-js): lint published package

* ci(auth-js): run publint

* build(auth-js): remove no splitting flag
2025-04-09 09:26:26 -04:00
github-actions[bot] 4b66525ad3 Version Packages (#1037)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-09 09:26:26 -04:00
wayofthepie 650cd0409b feat(oidc-auth): allow setting audience for oidc-auth (#1010) 2025-04-09 09:26:26 -04:00
Jonathan Haines 43794beaf5 ci(bun-transpiler): run publint (#1035) 2025-04-09 09:26:26 -04:00
Jonathan Haines 1b29fd1c35 build(casbin): lint published package (#1036) 2025-04-09 09:26:26 -04:00
Jonathan Haines 9da50dcc8c build(arktype-validator): lint published package (#1033)
* build(arktype-validator): lint published package

* ci(arktype-validator): run publint
2025-04-09 09:26:26 -04:00
Jonathan Haines 59a9a2747e chore: add tsup to monorepo root (#1032) 2025-04-09 09:26:26 -04:00
Jonathan Haines 95dd8e74ad build(ajv-validator): lint published package (#1030) 2025-04-09 09:26:26 -04:00
github-actions[bot] 64154467f0 Version Packages (#1028)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-09 09:26:26 -04:00
Musa Asukhanov 35031cb9f1 fix: Move "default" entrypoint down in "typia-validator" (#1027)
* Move "default" entrypoint down in "typia-validator"

* Add changeset

* Fix changeset
2025-04-09 09:26:26 -04:00
github-actions[bot] fc20f9c6f4 Version Packages (#1025)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-09 09:26:26 -04:00
Sungyu Kang 23fa14c596 chore(typia): bump 8.0.3 (#1024)
* chore(typia): bump 8.0.3

* chore: changeset

* fix: review

* fix: patch

* Update packages/typia-validator/package.json

Co-authored-by: Jonathan Haines <jonno.haines@gmail.com>

---------

Co-authored-by: Jonathan Haines <jonno.haines@gmail.com>
2025-04-09 09:26:26 -04:00
Jonathan Haines 4a1038ee66 chore: add coverage badges (#1023)
* chore: add coverage badges

* ci(casbin): fix spelling
2025-04-09 09:26:26 -04:00
Jonathan Haines 9d7a29d178 ci: initial coverage (#1022) 2025-04-09 09:26:26 -04:00
Jonathan Haines 86cb7db506 ci(coverage): upload initial coverage to codecov (#1021)
* ci(coverage): upload initial coverage to codecov

* ci(coverage): add flags

* ci(bun-transpiler): add coverage
2025-04-09 09:26:26 -04:00
Yusuke Wada e2726fd622 chore: update the lock file (#1019) 2025-04-09 09:26:26 -04:00
Jonathan Haines 990c8a5047 ci: run workspace scripts (#1015)
* ci: run workspace scripts

* ci: remove run option

* ci: remvoe default working directory

* test(firebase-auth): start emulator in vitest
2025-04-09 09:26:26 -04:00
github-actions[bot] 5ffb59fb53 Version Packages (#1018)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-09 09:26:26 -04:00
Milan Raj 2c16357028 feat(oidc-auth) Add initOidcAuthMiddleware and avoid mutating environment variables (#980)
* Add setOidcAuthEnv

* Avoid test relying on mutated global

* Test and docs

* Changeset

* style

* Update type import

* Switch to setOidcAuthEnvMiddleware

* Update changeset description

* nit remove unneeded optional param on getOidcAuthEnv

* Rename to initOidcAuthMiddleware
2025-04-09 09:26:26 -04:00
Tim Barley 76665d716e feat(oauth-providers): Add MSEntra OAuth Provider 2025-03-14 15:45:18 -04:00
41 changed files with 1024 additions and 166 deletions

View File

@ -0,0 +1,5 @@
---
'@hono/oauth-providers': minor
---
The PR adds Microsoft Entra (AzureAD) to the list of supported 3rd-party OAuth providers.

View File

@ -67,7 +67,7 @@ export function ajvValidator<
T,
Target extends keyof ValidationTargets,
E extends Env = Env,
P extends string = string
P extends string = string,
>(
target: Target,
schema: JSONSchemaType<T>,

View File

@ -1,5 +1,11 @@
# @hono/arktype-validator
## 2.0.1
### Patch Changes
- [#1137](https://github.com/honojs/middleware/pull/1137) [`01cd896e9b3c6a00c3c16ed59e0c3d20f5983918`](https://github.com/honojs/middleware/commit/01cd896e9b3c6a00c3c16ed59e0c3d20f5983918) Thanks [@MonsterDeveloper](https://github.com/MonsterDeveloper)! - Don't return restricted data fields on error responses
## 2.0.0
### Major Changes

View File

@ -1,6 +1,6 @@
{
"name": "@hono/arktype-validator",
"version": "2.0.0",
"version": "2.0.1",
"description": "ArkType validator middleware",
"type": "module",
"main": "dist/index.js",

View File

@ -35,6 +35,17 @@ describe('Basic', () => {
}
)
app.get(
'/headers',
arktypeValidator(
'header',
type({
'User-Agent': 'string',
})
),
(c) => c.json({ success: true, userAgent: c.header('User-Agent') })
)
type Actual = ExtractSchema<typeof route>
type Expected = {
'/author': {
@ -98,6 +109,22 @@ describe('Basic', () => {
const data = (await res.json()) as { success: boolean }
expect(data['success']).toBe(false)
})
it("doesn't return cookies after headers validation", async () => {
const req = new Request('http://localhost/headers', {
headers: {
'User-Agent': 'invalid',
Cookie: 'SECRET=123',
},
})
const res = await app.request(req)
expect(res).not.toBeNull()
expect(res.status).toBe(400)
const data = (await res.json()) as { succcess: false; errors: type.errors }
expect(data.errors).toHaveLength(1)
expect(data.errors[0].data).not.toHaveProperty('cookie')
})
})
describe('With Hook', () => {

View File

@ -10,6 +10,10 @@ export type Hook<T, E extends Env, P extends string, O = {}> = (
type HasUndefined<T> = undefined extends T ? true : false
const RESTRICTED_DATA_FIELDS = {
header: ['cookie'],
}
export const arktypeValidator = <
T extends Type,
Target extends keyof ValidationTargets,
@ -23,7 +27,7 @@ export const arktypeValidator = <
} = {
in: HasUndefined<I> extends true ? { [K in Target]?: I } : { [K in Target]: I }
out: { [K in Target]: O }
}
},
>(
target: Target,
schema: T,
@ -54,7 +58,31 @@ export const arktypeValidator = <
return c.json(
{
success: false,
errors: out,
errors:
target in RESTRICTED_DATA_FIELDS
? out.map((error) => {
const restrictedFields =
RESTRICTED_DATA_FIELDS[target as keyof typeof RESTRICTED_DATA_FIELDS] || []
if (
error &&
typeof error === 'object' &&
'data' in error &&
typeof error.data === 'object' &&
error.data !== null &&
!Array.isArray(error.data)
) {
const dataCopy = { ...(error.data as Record<string, unknown>) }
for (const field of restrictedFields) {
delete dataCopy[field]
}
error.data = dataCopy
}
return error
})
: out,
},
400
)

View File

@ -225,7 +225,7 @@ export function SessionProvider(props: SessionProviderProps) {
}
return updatedSession
},
} as SessionContextValue),
}) as SessionContextValue,
[session, loading, setSession]
)

View File

@ -64,7 +64,7 @@ type Hook<
E extends Env,
P extends string,
Target extends keyof ValidationTargets = keyof ValidationTargets,
O = object
O = object,
> = (
result: ({ success: true } | { success: false; errors: ValidationError[] }) & {
data: T
@ -119,19 +119,19 @@ export const classValidator = <
[K in Target]?: K extends 'json'
? In
: HasUndefined<keyof ValidationTargets[K]> extends true
? { [K2 in keyof In]?: ValidationTargets[K][K2] }
: { [K2 in keyof In]: ValidationTargets[K][K2] }
? { [K2 in keyof In]?: ValidationTargets[K][K2] }
: { [K2 in keyof In]: ValidationTargets[K][K2] }
}
: {
[K in Target]: K extends 'json'
? In
: HasUndefined<keyof ValidationTargets[K]> extends true
? { [K2 in keyof In]?: ValidationTargets[K][K2] }
: { [K2 in keyof In]: ValidationTargets[K][K2] }
? { [K2 in keyof In]?: ValidationTargets[K][K2] }
: { [K2 in keyof In]: ValidationTargets[K][K2] }
}
out: { [K in Target]: Output }
},
V extends I = I
V extends I = I,
>(
target: Target,
dataType: T,

View File

@ -32,7 +32,7 @@ export const conformValidator = <
form: { [K in keyof In]: FormTargetValue }
}
out: { form: GetSuccessSubmission<Out> }
}
},
>(
parse: F,
hook?: Hook<F, E, P>

View File

@ -19,18 +19,18 @@ export const effectValidator = <
[K in Target]?: K extends 'json'
? In
: HasUndefined<keyof ValidationTargets[K]> extends true
? { [K2 in keyof In]?: ValidationTargets[K][K2] }
: { [K2 in keyof In]: ValidationTargets[K][K2] }
? { [K2 in keyof In]?: ValidationTargets[K][K2] }
: { [K2 in keyof In]: ValidationTargets[K][K2] }
}
: {
[K in Target]: K extends 'json'
? In
: HasUndefined<keyof ValidationTargets[K]> extends true
? { [K2 in keyof In]?: ValidationTargets[K][K2] }
: { [K2 in keyof In]: ValidationTargets[K][K2] }
? { [K2 in keyof In]?: ValidationTargets[K][K2] }
: { [K2 in keyof In]: ValidationTargets[K][K2] }
}
out: { [K in Target]: Out }
}
},
>(
target: Target,
schema: S.Schema<Type, Encoded, never>

View File

@ -35,7 +35,7 @@ export interface Emitter<EPMap extends EventPayloadMap> {
export const defineHandler = <
EPMap extends EventPayloadMap,
Key extends keyof EPMap,
E extends Env = Env
E extends Env = Env,
>(
handler: EventHandler<EPMap[Key], E>
): EventHandler<EPMap[Key], E> => {

View File

@ -1,5 +1,17 @@
# @hono/node-ws
## 1.1.3
### Patch Changes
- [#1141](https://github.com/honojs/middleware/pull/1141) [`1765a9a3aa1ab6aa59b0efd2d161b7bfaa565ef7`](https://github.com/honojs/middleware/commit/1765a9a3aa1ab6aa59b0efd2d161b7bfaa565ef7) Thanks [@nakasyou](https://github.com/nakasyou)! - Make it uncrashable
## 1.1.2
### Patch Changes
- [#1138](https://github.com/honojs/middleware/pull/1138) [`237bff1b82f2c0adfcd015dc97538b36cfb5d418`](https://github.com/honojs/middleware/commit/237bff1b82f2c0adfcd015dc97538b36cfb5d418) Thanks [@leia-uwu](https://github.com/leia-uwu)! - Fix missing code and reason on `CloseEvent`
## 1.1.1
### Patch Changes

View File

@ -1,6 +1,6 @@
{
"name": "@hono/node-ws",
"version": "1.1.1",
"version": "1.1.3",
"description": "WebSocket helper for Node.js",
"type": "module",
"main": "dist/index.js",

View File

@ -170,18 +170,21 @@ describe('WebSocket helper', () => {
})
it('CloseEvent should be executed without crash', async () => {
const testCode = 3001
const testReason = 'Test!'
app.get(
'/',
upgradeWebSocket(() => ({
onClose() {
// doing some stuff here
onClose(event) {
expect(event.code).toBe(testCode)
expect(event.reason).toBe(testReason)
},
}))
)
const ws = new WebSocket('ws://localhost:3030/')
await new Promise<void>((resolve) => ws.on('open', resolve))
ws.close()
ws.close(testCode, testReason)
})
it('Should be able to send and receive binary content with good length', async () => {
@ -211,6 +214,26 @@ describe('WebSocket helper', () => {
})
})
it('Should onError works well', async () => {
const mainPromise = new Promise<unknown>((resolve) =>
app.get(
'/',
upgradeWebSocket(
() => {
throw 0
},
{
onError(err) {
resolve(err)
},
}
)
)
)
const ws = new WebSocket('ws://localhost:3030/')
expect(await mainPromise).toBe(0)
})
describe('Types', () => {
it('Should not throw a type error with an app with Variables generics', () => {
const app = new Hono<{

View File

@ -1,5 +1,5 @@
import type { Hono } from 'hono'
import type { UpgradeWebSocket, WSContext } from 'hono/ws'
import type { UpgradeWebSocket, WSContext, WSEvents } from 'hono/ws'
import type { WebSocket } from 'ws'
import { WebSocketServer } from 'ws'
import type { IncomingMessage } from 'http'
@ -9,7 +9,12 @@ import type { Duplex } from 'node:stream'
import { CloseEvent } from './events'
export interface NodeWebSocket {
upgradeWebSocket: UpgradeWebSocket<WebSocket>
upgradeWebSocket: UpgradeWebSocket<
WebSocket,
{
onError: (err: unknown) => void
}
>
injectWebSocket(server: Server | Http2Server | Http2SecureServer): void
}
export interface NodeWebSocketInit {
@ -80,7 +85,7 @@ export const createNodeWebSocket = (init: NodeWebSocketInit): NodeWebSocket => {
})
})
},
upgradeWebSocket: (createEvents) =>
upgradeWebSocket: (createEvents, options) =>
async function upgradeWebSocket(c, next) {
if (c.req.header('upgrade')?.toLowerCase() !== 'websocket') {
// Not websocket
@ -91,7 +96,14 @@ export const createNodeWebSocket = (init: NodeWebSocketInit): NodeWebSocket => {
const response = new Response()
;(async () => {
const ws = await nodeUpgradeWebSocket(c.env.incoming, response)
const events = await createEvents(c)
let events: WSEvents<WebSocket>
try {
events = await createEvents(c)
} catch (e) {
;(options?.onError ?? console.error)(e)
ws.close()
return
}
const ctx: WSContext<WebSocket> = {
binaryType: 'arraybuffer',
@ -110,33 +122,49 @@ export const createNodeWebSocket = (init: NodeWebSocketInit): NodeWebSocket => {
},
url: new URL(c.req.url),
}
events.onOpen?.(new Event('open'), ctx)
try {
events?.onOpen?.(new Event('open'), ctx)
} catch (e) {
;(options?.onError ?? console.error)(e)
}
ws.on('message', (data, isBinary) => {
const datas = Array.isArray(data) ? data : [data]
for (const data of datas) {
events.onMessage?.(
new MessageEvent('message', {
data: isBinary
? data instanceof ArrayBuffer
? data
: data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)
: data.toString('utf-8'),
try {
events?.onMessage?.(
new MessageEvent('message', {
data: isBinary
? data instanceof ArrayBuffer
? data
: data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)
: data.toString('utf-8'),
}),
ctx
)
} catch (e) {
;(options?.onError ?? console.error)(e)
}
}
})
ws.on('close', (code, reason) => {
try {
events?.onClose?.(new CloseEvent('close', { code, reason: reason.toString() }), ctx)
} catch (e) {
;(options?.onError ?? console.error)(e)
}
})
ws.on('error', (error) => {
try {
events?.onError?.(
new ErrorEvent('error', {
error: error,
}),
ctx
)
} catch (e) {
;(options?.onError ?? console.error)(e)
}
})
ws.on('close', () => {
events.onClose?.(new CloseEvent('close'), ctx)
})
ws.on('error', (error) => {
events.onError?.(
new ErrorEvent('error', {
error: error,
}),
ctx
)
})
})()
return response

View File

@ -1087,6 +1087,128 @@ The validation endpoint helps your application detect when tokens become invalid
> For security and compliance, make sure to implement regular token validation in your application. If a token becomes invalid, promptly sign out the user and terminate their OAuth session.
### MSEntra
```ts
import { Hono } from 'hono'
import { msentraAuth } from '@hono/oauth-providers/msentra'
const app = new Hono()
app.use(
'/msentra',
msentraAuth({
client_id: process.env.MSENTRA_ID,
client_secret: process.env.MSENTRA_SECRET,
tenant_id: process.env.MSENTRA_TENANT_ID
scope: [
'openid',
'profile',
'email',
'https;//graph.microsoft.com/.default',
]
})
)
export default app
```
### Parameters
- `client_id`:
- Type: `string`.
- `Required`.
- Your app client Id. You can find this in your Azure Portal.
- `client_secret`:
- Type: `string`.
- `Required`.
- Your app client secret. You can find this in your Azure Portal.
> ⚠️ Do **not** share your **client secret** to ensure the security of your app.
- `tenant_id`:
- Type: `string`
- `Required`.
- Your Microsoft Tenant's Id. You can find this in your Azure Portal.
- `scope`:
- Type: `string[]`.
- `Required`.
- Set of **permissions** to request the user's authorization to access your app for retrieving
user information and performing actions on their behalf.
#### Authentication Flow
After the completion of the MSEntra OAuth flow, essential data has been prepared for use in the
subsequent steps that your app needs to take.
`msentraAuth` method provides 4 set key data:
- `token`:
- Access token to make requests to the MSEntra API for retrieving user information and
performing actions on their behalf.
- Type:
```
{
token: string
expires_in: number
refresh_token: string
}
```
- `granted-scopes`:
- Scopes for which the user has granted permissions.
- Type: `string[]`.
- `user-msentra`:
- User basic info retrieved from MSEntra
- Type:
```
{
businessPhones: string[],
displayName: string
givenName: string
jobTitle: string
mail: string
mobilePhone: string
officeLocation: string
surname: string
userPrincipalName: string
id: string
}
```
> [!NOTE]
> To access this data, utilize the `c.get` method within the callback of the upcoming HTTP request
> handler.
```ts
app.get('/msentra', (c) => {
const token = c.get('token')
const grantedScopes = c.get('granted-scopes')
const user = c.get('user-msentra')
return c.json({
token,
grantedScopes,
user,
})
})
```
#### Refresh Token
Once the user token expires you can refresh their token without the need to prompt the user again
for access. In such scenario, you can utilize the `refreshToken` method, which accepts the
`client_id`, `client_secret`, `tenant_id`, and `refresh_token` as parameters.
> [!NOTE]
> The `refresh_token` can be used once. Once the token is refreshed MSEntra gives you a new
> `refresh_token` along with the new token.
```ts
import { msentraAuth, refreshToken } from '@hono/oauth-providers/msentra'
app.get('/msentra/refresh', (c, next) => {
const newTokens = await refreshToken({ client_id, client_secret, tenant_id, refresh_token })
})
```
## Advance Usage
### Customize `redirect_uri`

View File

@ -10,6 +10,11 @@ import type {
import type { GitHubErrorResponse, GitHubTokenResponse } from './src/providers/github'
import type { GoogleErrorResponse, GoogleTokenResponse, GoogleUser } from './src/providers/google'
import type { LinkedInErrorResponse, LinkedInTokenResponse } from './src/providers/linkedin'
import type {
MSEntraErrorResponse,
MSEntraTokenResponse,
MSEntraUser,
} from './src/providers/msentra'
import type {
TwitchErrorResponse,
TwitchTokenResponse,
@ -206,6 +211,31 @@ export const handlers = [
return HttpResponse.json(twitchValidateError, { status: 401 })
}
),
// MSEntra
http.post(
'https://login.microsoft.com/fake-tenant-id/oauth2/v2.0/token',
async ({
request,
}): Promise<StrictResponse<Partial<MSEntraTokenResponse> | MSEntraErrorResponse>> => {
const body = new URLSearchParams(await request.text())
if (body.get('code') === dummyCode || body.get('refresh_token') === msentraRefreshToken) {
return HttpResponse.json(msentraToken)
}
return HttpResponse.json(msentraCodeError)
}
),
http.get(
'https://graph.microsoft.com/v1.0/me',
async ({ request }): Promise<StrictResponse<Partial<MSEntraUser> | MSEntraErrorResponse>> => {
const authorization = request.headers.get('authorization')
if (authorization === `Bearer ${msentraToken.access_token}`) {
return HttpResponse.json(msentraUser)
}
return HttpResponse.json(msentraCodeError)
}
),
]
export const dummyCode = '4/0AfJohXl9tS46EmTA6u9x3pJQiyCNyahx4DLJaeJelzJ0E5KkT4qJmCtjq9n3FxBvO40ofg'
@ -558,3 +588,28 @@ export const twitchValidateError = {
status: 401,
message: 'invalid access token',
}
export const msentraRefreshToken = 'paofniueawnbfisdjkaierlufjkdnsj'
export const msentraToken = {
...dummyToken,
refresh_token: msentraRefreshToken,
}
export const msentraUser = {
'@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#users/$entity',
businessPhones: ['111-111-1111'],
displayName: 'Test User',
givenName: 'Test',
jobTitle: 'Developer',
mail: 'example@email.com',
mobilePhone: '111-111-1111',
officeLocation: 'es-419',
preferredLanguage: null,
surname: 'User',
userPrincipalName: 'example@email.com',
id: '11111111-1111-1111-1111-111111111111',
}
export const msentraCodeError = {
error: 'invalid_grant',
error_description: 'AADSTS1234567: Invalid request.',
error_codes: [1234567],
}

View File

@ -27,6 +27,30 @@
"default": "./dist/index.cjs"
}
},
"./*": {
"import": {
"types": "./dist/providers/*/index.d.ts",
"default": "./dist/providers/*/index.js"
},
"require": {
"types": "./dist/providers/*/index.d.cts",
"default": "./dist/providers/*/index.cjs"
}
},
"./msentra": {
"import": {
"types": "./dist/providers/msentra/index.d.mts",
"default": "./dist/providers/msentra/index.mjs"
},
"require": {
"types": "./dist/providers/msentra/index.d.ts",
"default": "./dist/providers/msentra/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./*": {
"import": {
"types": "./dist/providers/*/index.d.ts",
@ -60,6 +84,9 @@
],
"twitch": [
"./dist/providers/twitch/index.d.ts"
],
"msentra": [
"./dist/providers/msentra/index.d.ts"
]
}
},

View File

@ -19,6 +19,10 @@ import {
linkedInCodeError,
linkedInToken,
linkedInUser,
msentraCodeError,
msentraRefreshToken,
msentraToken,
msentraUser,
xCodeError,
xRefreshToken,
xRefreshTokenError,
@ -48,6 +52,8 @@ import { googleAuth } from './providers/google'
import type { GoogleUser } from './providers/google'
import { linkedinAuth } from './providers/linkedin'
import type { LinkedInUser } from './providers/linkedin'
import type { MSEntraUser } from './providers/msentra'
import { msentraAuth, refreshToken as msentraRefresh } from './providers/msentra'
import type { TwitchUser } from './providers/twitch'
import {
twitchAuth,
@ -421,6 +427,55 @@ describe('OAuth Middleware', () => {
return c.json(response)
})
// MSEntra
app.use(
'/msentra',
msentraAuth({
client_id,
client_secret,
tenant_id: 'fake-tenant-id',
scope: ['openid', 'email', 'profile'],
})
)
app.use('/msentra-custom-redirect', (c, next) => {
return msentraAuth({
client_id,
client_secret,
tenant_id: 'fake-tenant-id',
scope: ['openid', 'email', 'profile'],
redirect_uri: 'http://localhost:3000/msentra',
})(c, next)
})
app.get('/msentra', (c) => {
const user = c.get('user-msentra')
const token = c.get('token')
const grantedScopes = c.get('granted-scopes')
return c.json({
user,
token,
grantedScopes,
})
})
app.get('/msentra/refresh', async (c) => {
const response = await msentraRefresh({
client_id,
client_secret,
tenant_id: 'fake-tenant-id',
refresh_token: msentraRefreshToken,
})
return c.json(response)
})
app.get('/msentra/refresh/error', async (c) => {
const response = await msentraRefresh({
client_id,
client_secret,
tenant_id: 'fake-tenant-id',
refresh_token: 'wrong-refresh-token',
})
return c.json(response)
})
beforeAll(() => {
server.listen()
})
@ -973,4 +1028,77 @@ describe('OAuth Middleware', () => {
})
})
})
describe('msentraAuth middleware', () => {
describe('middleware', () => {
it('Should redirect', async () => {
const res = await app.request('/msentra')
expect(res).not.toBeNull()
expect(res.status).toBe(302)
expect(res.headers)
})
it('Should redirect to custom redirect_uri', async () => {
const res = await app.request('/msentra-custom-redirect')
expect(res).not.toBeNull()
expect(res.status).toBe(302)
const redirectLocation = res.headers.get('location')!
const redirectUrl = new URL(redirectLocation)
expect(redirectUrl.searchParams.get('redirect_uri')).toBe('http://localhost:3000/msentra')
})
it('Prevent CSRF attack', async () => {
const res = await app.request(`/msentra?code=${dummyCode}&state=malware-state`)
expect(res).not.toBeNull()
expect(res.status).toBe(401)
})
it('Should throw error for invalid code', async () => {
const res = await app.request('/msentra?code=9348ffdsd-sdsdbad-code')
const text = await res.text()
expect(res).not.toBeNull()
expect(res.status).toBe(400)
expect(text).toBe(msentraCodeError.error)
})
it('Should work with received code', async () => {
const res = await app.request(`/msentra?code=${dummyCode}`)
const response = (await res.json()) as {
token: Token
user: MSEntraUser
grantedScopes: string[]
}
expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(response.user).toEqual(msentraUser)
expect(response.grantedScopes).toEqual(msentraToken.scope.split(' '))
expect(response.token).toEqual({
token: msentraToken.access_token,
expires_in: msentraToken.expires_in,
refresh_token: msentraToken.refresh_token,
})
})
})
describe('Refresh Token', () => {
it('Should refresh token', async () => {
const res = await app.request('/msentra/refresh')
expect(res).not.toBeNull()
expect(await res.json()).toEqual(msentraToken)
})
it('Should return error for refresh', async () => {
const res = await app.request('/msentra/refresh/error')
expect(res).not.toBeNull()
expect(res.status).toBe(400)
expect(await res.text()).toBe(msentraCodeError.error)
})
})
})
})

View File

@ -0,0 +1,124 @@
import { HTTPException } from 'hono/http-exception'
import { toQueryParams } from '../../utils/objectToQuery'
import type { MSEntraErrorResponse, MSEntraToken, MSEntraTokenResponse, MSEntraUser } from './types'
type MSEntraAuthFlow = {
client_id: string
client_secret: string
tenant_id: string
redirect_uri: string
code: string | undefined
token: MSEntraToken | undefined
scope: string[]
state?: string
}
export class AuthFlow {
client_id: string
client_secret: string
tenant_id: string
redirect_uri: string
code: string | undefined
token: MSEntraToken | undefined
scope: string[]
state: string | undefined
user: Partial<MSEntraUser> | undefined
granted_scopes: string[] | undefined
constructor({
client_id,
client_secret,
tenant_id,
redirect_uri,
code,
token,
scope,
state,
}: MSEntraAuthFlow) {
this.client_id = client_id
this.client_secret = client_secret
this.tenant_id = tenant_id
this.redirect_uri = redirect_uri
this.code = code
this.token = token
this.scope = scope
this.state = state
this.user = undefined
if (
this.client_id === undefined ||
this.client_secret === undefined ||
this.tenant_id === undefined ||
this.scope.length <= 0
) {
throw new HTTPException(400, {
message: 'Required parameters were not found. Please provide them to proceed.',
})
}
}
redirect() {
const parsedOptions = toQueryParams({
response_type: 'code',
redirect_uri: this.redirect_uri,
client_id: this.client_id,
include_granted_scopes: true,
scope: this.scope.join(' '),
state: this.state,
})
return `https://login.microsoft.com/${this.tenant_id}/oauth2/v2.0/authorize?${parsedOptions}`
}
async getTokenFromCode() {
const parsedOptions = toQueryParams({
client_id: this.client_id,
client_secret: this.client_secret,
redirect_uri: this.redirect_uri,
code: this.code,
grant_type: 'authorization_code',
})
const response = (await fetch(
`https://login.microsoft.com/${this.tenant_id}/oauth2/v2.0/token`,
{
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
body: parsedOptions,
}
).then((res) => res.json())) as MSEntraTokenResponse | MSEntraErrorResponse
if ('error' in response) {
throw new HTTPException(400, { message: response.error })
}
if ('access_token' in response) {
this.token = {
token: response.access_token,
expires_in: response.expires_in,
refresh_token: response.refresh_token,
}
this.granted_scopes = response.scope.split(' ')
}
}
async getUserData() {
await this.getTokenFromCode()
//TODO: add support for extra fields
const response = (await fetch('https://graph.microsoft.com/v1.0/me', {
headers: {
authorization: `Bearer ${this.token?.token}`,
},
}).then(async (res) => res.json())) as MSEntraUser | MSEntraErrorResponse
if ('error' in response) {
throw new HTTPException(400, { message: response.error })
}
if ('id' in response) {
this.user = response
}
}
}

View File

@ -0,0 +1,11 @@
export { msentraAuth } from './msentraAuth'
export { refreshToken } from './refreshToken'
export * from './types'
import type { OAuthVariables } from '../../types'
import type { MSEntraUser } from './types'
declare module 'hono' {
interface ContextVariableMap extends OAuthVariables {
'user-msentra': Partial<MSEntraUser> | undefined
}
}

View File

@ -0,0 +1,63 @@
import type { MiddlewareHandler } from 'hono'
import { env } from 'hono/adapter'
import { getCookie, setCookie } from 'hono/cookie'
import { HTTPException } from 'hono/http-exception'
import { getRandomState } from '../../utils/getRandomState'
import { AuthFlow } from './authFlow'
export function msentraAuth(options: {
client_id?: string
client_secret?: string
tenant_id?: string
redirect_uri?: string
code?: string | undefined
scope: string[]
state?: string
}): MiddlewareHandler {
return async (c, next) => {
// Generate encoded "keys" if not provided
const newState = options.state || getRandomState()
// Create new Auth instance
const auth = new AuthFlow({
client_id: options.client_id || (env(c).MSENTRA_ID as string),
client_secret: options.client_secret || (env(c).MSENTRA_SECRET as string),
tenant_id: options.tenant_id || (env(c).MSENTRA_TENANT_ID as string),
redirect_uri: options.redirect_uri || c.req.url.split('?')[0],
code: c.req.query('code'),
token: {
token: c.req.query('access_token') as string,
expires_in: Number(c.req.query('expires_in')) as number,
},
scope: options.scope,
})
// Redirect to login dialog
if (!auth.code) {
setCookie(c, 'state', newState, {
maxAge: 60 * 10,
httpOnly: true,
path: '/',
})
return c.redirect(auth.redirect())
}
// Avoid CSRF attack by checking state
if (c.req.url.includes('?')) {
const storedState = getCookie(c, 'state')
if (c.req.query('state') !== storedState) {
throw new HTTPException(401)
}
}
// Retrieve user data from Microsoft Entra
await auth.getUserData()
// Set return info
c.set('token', auth.token)
c.set('user-msentra', auth.user)
c.set('granted-scopes', auth.granted_scopes)
await next()
}
}

View File

@ -0,0 +1,40 @@
import { HTTPException } from 'hono/http-exception'
import { toQueryParams } from '../../utils/objectToQuery'
import type { MSEntraErrorResponse, MSEntraTokenResponse } from './types'
export async function refreshToken({
client_id,
client_secret,
tenant_id,
refresh_token,
}: {
client_id: string
client_secret: string
tenant_id: string
refresh_token: string
}) {
if (!refresh_token) {
throw new HTTPException(400, { message: 'missing refresh token' })
}
const params = toQueryParams({
client_id,
client_secret,
refresh_token,
grant_type: 'refresh_token',
})
const response = (await fetch(`https://login.microsoft.com/${tenant_id}/oauth2/v2.0/token`, {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
body: params,
}).then((res) => res.json())) as MSEntraTokenResponse | MSEntraErrorResponse
if ('error' in response) {
throw new HTTPException(400, { message: response.error })
}
return response
}

View File

@ -0,0 +1,32 @@
import type { Token } from '../../types'
export type MSEntraErrorResponse = {
error: string
error_description: string
error_codes: number[]
}
export type MSEntraTokenResponse = {
access_token: string
expires_in: number
scope: string
token_type: string
id_token: string
refresh_token: string
}
export type MSEntraUser = {
id: string
upn: string
verified_email: boolean
name: string
given_name: string
family_name: string
picture: string
local: string
employeeId: string
}
export type MSEntraToken = Token & {
refresh_token?: string
}

View File

@ -162,7 +162,7 @@ export interface TwitchUserResponse {
view_count: number
email: string
created_at: string
}
},
]
}

View File

@ -44,7 +44,7 @@ const getMetricConstructor = (type: MetricOptions['type']) =>
({
counter: Counter,
histogram: Histogram,
}[type])
})[type]
export const createStandardMetrics = ({
registry,

View File

@ -9,9 +9,8 @@ import * as zodSchemas from './__schemas__/zod'
import { sValidator } from '.'
type ExtractSchema<T> = T extends Hono<infer _, infer S> ? S : never
type MergeDiscriminatedUnion<U> = UnionToIntersection<U> extends infer O
? { [K in keyof O]: O[K] }
: never
type MergeDiscriminatedUnion<U> =
UnionToIntersection<U> extends infer O ? { [K in keyof O]: O[K] } : never
const libs = ['valibot', 'zod', 'arktype'] as const
const schemasByLibrary = {

View File

@ -10,7 +10,7 @@ type Hook<
E extends Env,
P extends string,
Target extends keyof ValidationTargets = keyof ValidationTargets,
O = {}
O = {},
> = (
result: (
| { success: true; data: T }
@ -42,7 +42,7 @@ const sValidator = <
}
out: { [K in Target]: Out }
},
V extends I = I
V extends I = I,
>(
target: Target,
schema: Schema,

View File

@ -148,7 +148,7 @@ export const renderSwaggerUIOptions = (options: DistSwaggerUIOptions) => {
return ''
}
})
.filter(item => item !== '')
.filter((item) => item !== '')
.join(',')
return optionsStrings

View File

@ -68,7 +68,7 @@ export function tbValidator<
V extends {
in: { [K in Target]: Static<T> }
out: { [K in Target]: ExcludeResponseType<Static<T>> }
}
},
>(
target: Target,
schema: T,

View File

@ -11,54 +11,57 @@ interface IFailure<T> {
type BaseType<T> = T extends string
? string
: T extends number
? number
: T extends boolean
? boolean
: T extends symbol
? symbol
: T extends bigint
? bigint
: T
type Parsed<T> = T extends Record<string | number, any>
? {
[K in keyof T]-?: T[K] extends (infer U)[]
? (BaseType<U> | null | undefined)[] | undefined
: BaseType<T[K]> | null | undefined
}
: BaseType<T>
? number
: T extends boolean
? boolean
: T extends symbol
? symbol
: T extends bigint
? bigint
: T
type Parsed<T> =
T extends Record<string | number, any>
? {
[K in keyof T]-?: T[K] extends (infer U)[]
? (BaseType<U> | null | undefined)[] | undefined
: BaseType<T[K]> | null | undefined
}
: BaseType<T>
export type QueryValidation<O extends Record<string | number, any> = any> = (
input: string | URLSearchParams
) => IValidation<O>
export type QueryOutputType<T> = T extends QueryValidation<infer O> ? O : never
type QueryStringify<T> = T extends Record<string | number, any>
? {
// Suppress to split union types
[K in keyof T]: [T[K]] extends [bigint | number | boolean]
? `${T[K]}`
: T[K] extends (infer U)[]
? [U] extends [bigint | number | boolean]
? `${U}`[]
: T[K]
: T[K]
}
: T
type QueryStringify<T> =
T extends Record<string | number, any>
? {
// Suppress to split union types
[K in keyof T]: [T[K]] extends [bigint | number | boolean]
? `${T[K]}`
: T[K] extends (infer U)[]
? [U] extends [bigint | number | boolean]
? `${U}`[]
: T[K]
: T[K]
}
: T
export type HeaderValidation<O extends Record<string | number, any> = any> = (
input: Record<string, string | string[] | undefined>
) => IValidation<O>
export type HeaderOutputType<T> = T extends HeaderValidation<infer O> ? O : never
type HeaderStringify<T> = T extends Record<string | number, any>
? {
// Suppress to split union types
[K in keyof T]: [T[K]] extends [bigint | number | boolean]
? `${T[K]}`
: T[K] extends (infer U)[]
? [U] extends [bigint | number | boolean]
? `${U}`
: U
: T[K]
}
: T
type HeaderStringify<T> =
T extends Record<string | number, any>
? {
// Suppress to split union types
[K in keyof T]: [T[K]] extends [bigint | number | boolean]
? `${T[K]}`
: T[K] extends (infer U)[]
? [U] extends [bigint | number | boolean]
? `${U}`
: U
: T[K]
}
: T
export type HttpHook<T, E extends Env, P extends string, O = {}> = (
result: IValidation.ISuccess<T> | IFailure<Parsed<T>>,
@ -82,7 +85,7 @@ interface TypiaValidator {
V extends { in: { query: QueryStringify<O> }; out: { query: O } } = {
in: { query: QueryStringify<O> }
out: { query: O }
}
},
>(
target: 'query',
validate: T,
@ -97,7 +100,7 @@ interface TypiaValidator {
V extends { in: { header: HeaderStringify<O> }; out: { header: O } } = {
in: { header: HeaderStringify<O> }
out: { header: O }
}
},
>(
target: 'header',
validate: T,
@ -116,7 +119,7 @@ interface TypiaValidator {
} = {
in: { [K in Target]: O }
out: { [K in Target]: O }
}
},
>(
target: Target,
validate: T,

View File

@ -23,7 +23,7 @@ export const typiaValidator = <
} = {
in: { [K in Target]: O }
out: { [K in Target]: O }
}
},
>(
target: Target,
validate: T,

View File

@ -14,7 +14,7 @@ export type Hook<
E extends Env,
P extends string,
Target extends keyof ValidationTargets = keyof ValidationTargets,
O = {}
O = {},
> = (
result: SafeParseResult<T> & {
target: Target
@ -45,7 +45,7 @@ export const vValidator = <
}
out: { [K in Target]: Out }
},
V extends I = I
V extends I = I,
>(
target: Target,
schema: T,

View File

@ -1,5 +1,12 @@
# @hono/zod-openapi
## 0.19.6
### Patch Changes
- Updated dependencies [[`8ed99d9d791ed6bd8b897c705289b0464947e632`](https://github.com/honojs/middleware/commit/8ed99d9d791ed6bd8b897c705289b0464947e632)]:
- @hono/zod-validator@0.5.0
## 0.19.5
### Patch Changes

View File

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

View File

@ -73,7 +73,7 @@ type IsForm<T> = T extends string
type ReturnJsonOrTextOrResponse<
ContentType,
Content,
Status extends keyof StatusCodeRangeDefinitions | StatusCode
Status extends keyof StatusCodeRangeDefinitions | StatusCode,
> = ContentType extends string
? ContentType extends `application/${infer Start}json${infer _End}`
? Start extends '' | `${string}+` | `vnd.${string}+`
@ -88,8 +88,8 @@ type ReturnJsonOrTextOrResponse<
>
: never
: ContentType extends `text/plain${infer _Rest}`
? TypedResponse<Content, ExtractStatusCode<Status>, 'text'>
: Response
? TypedResponse<Content, ExtractStatusCode<Status>, 'text'>
: Response
: never
type RequestPart<R extends RouteConfig, Part extends string> = Part extends keyof R['request']
@ -101,7 +101,7 @@ type HasUndefined<T> = undefined extends T ? true : false
type InputTypeBase<
R extends RouteConfig,
Part extends string,
Type extends keyof ValidationTargets
Type extends keyof ValidationTargets,
> = R['request'] extends RequestTypes
? RequestPart<R, Part> extends ZodType
? {
@ -125,22 +125,22 @@ type InputTypeJson<R extends RouteConfig> = R['request'] extends RequestTypes
? IsJson<keyof R['request']['body']['content']> extends never
? {}
: R['request']['body']['content'][keyof R['request']['body']['content']] extends Record<
'schema',
ZodSchema<any>
>
? {
in: {
json: z.input<
R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
>
'schema',
ZodSchema<any>
>
? {
in: {
json: z.input<
R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
>
}
out: {
json: z.output<
R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
>
}
}
out: {
json: z.output<
R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
>
}
}
: {}
: {}
: {}
: {}
: {}
@ -151,22 +151,22 @@ type InputTypeForm<R extends RouteConfig> = R['request'] extends RequestTypes
? IsForm<keyof R['request']['body']['content']> extends never
? {}
: R['request']['body']['content'][keyof R['request']['body']['content']] extends Record<
'schema',
ZodSchema<any>
>
? {
in: {
form: z.input<
R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
>
'schema',
ZodSchema<any>
>
? {
in: {
form: z.input<
R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
>
}
out: {
form: z.output<
R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
>
}
}
out: {
form: z.output<
R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
>
}
}
: {}
: {}
: {}
: {}
: {}
@ -249,8 +249,8 @@ type HonoInit<E extends Env> = ConstructorParameters<typeof Hono>[0] & OpenAPIHo
type AsArray<T> = T extends undefined // TODO move to utils?
? []
: T extends any[]
? T
: [T]
? T
: [T]
/**
* Like simplify but recursive
@ -265,17 +265,14 @@ export type DeepSimplify<T> = {
/**
* Helper to infer generics from {@link MiddlewareHandler}
*/
export type OfHandlerType<T extends MiddlewareHandler> = T extends MiddlewareHandler<
infer E,
infer P,
infer I
>
? {
env: E
path: P
input: I
}
: never
export type OfHandlerType<T extends MiddlewareHandler> =
T extends MiddlewareHandler<infer E, infer P, infer I>
? {
env: E
path: P
input: I
}
: never
/**
* Reduce a tuple of middleware handlers into a single
@ -285,7 +282,7 @@ export type OfHandlerType<T extends MiddlewareHandler> = T extends MiddlewareHan
export type MiddlewareToHandlerType<M extends MiddlewareHandler<any, any, any>[]> = M extends [
infer First,
infer Second,
...infer Rest
...infer Rest,
]
? First extends MiddlewareHandler<any, any, any>
? Second extends MiddlewareHandler<any, any, any>
@ -297,23 +294,22 @@ export type MiddlewareToHandlerType<M extends MiddlewareHandler<any, any, any>[]
OfHandlerType<First>['path'], // Keep path from First
OfHandlerType<First>['input'] // Keep input from First
>,
...Rest
...Rest,
]
>
: never
: never
: never
: M extends [infer Last]
? Last // Return the last remaining handler in the array
: MiddlewareHandler<Env>
? Last // Return the last remaining handler in the array
: MiddlewareHandler<Env>
type RouteMiddlewareParams<R extends RouteConfig> = OfHandlerType<
MiddlewareToHandlerType<AsArray<R['middleware']>>
>
export type RouteConfigToEnv<R extends RouteConfig> = RouteMiddlewareParams<R> extends never
? Env
: RouteMiddlewareParams<R>['env']
export type RouteConfigToEnv<R extends RouteConfig> =
RouteMiddlewareParams<R> extends never ? Env : RouteMiddlewareParams<R>['env']
export type RouteHandler<
R extends RouteConfig,
@ -324,7 +320,7 @@ export type RouteHandler<
InputTypeCookie<R> &
InputTypeForm<R> &
InputTypeJson<R>,
P extends string = ConvertPathType<R['path']>
P extends string = ConvertPathType<R['path']>,
> = Handler<
E,
P,
@ -352,7 +348,7 @@ export type RouteHook<
InputTypeCookie<R> &
InputTypeForm<R> &
InputTypeJson<R>,
P extends string = ConvertPathType<R['path']>
P extends string = ConvertPathType<R['path']>,
> = Hook<
I,
E,
@ -371,7 +367,7 @@ export type OpenAPIObjectConfigure<E extends Env, P extends string> =
export class OpenAPIHono<
E extends Env = Env,
S extends Schema = {},
BasePath extends string = '/'
BasePath extends string = '/',
> extends Hono<E, S, BasePath> {
openAPIRegistry: OpenAPIRegistry
defaultHook?: OpenAPIHonoOptions<E>['defaultHook']
@ -421,7 +417,7 @@ export class OpenAPIHono<
InputTypeCookie<R> &
InputTypeForm<R> &
InputTypeJson<R>,
P extends string = ConvertPathType<R['path']>
P extends string = ConvertPathType<R['path']>,
>(
{ middleware: routeMiddleware, hide, ...route }: R,
handler: Handler<
@ -609,7 +605,7 @@ export class OpenAPIHono<
SubPath extends string,
SubEnv extends Env,
SubSchema extends Schema,
SubBasePath extends string
SubBasePath extends string,
>(
path: SubPath,
app: Hono<SubEnv, SubSchema, SubBasePath>
@ -619,7 +615,7 @@ export class OpenAPIHono<
SubPath extends string,
SubEnv extends Env,
SubSchema extends Schema,
SubBasePath extends string
SubBasePath extends string,
>(
path: SubPath,
app?: Hono<SubEnv, SubSchema, SubBasePath>

View File

@ -1,5 +1,11 @@
# @hono/zod-validator
## 0.5.0
### Minor Changes
- [#1140](https://github.com/honojs/middleware/pull/1140) [`8ed99d9d791ed6bd8b897c705289b0464947e632`](https://github.com/honojs/middleware/commit/8ed99d9d791ed6bd8b897c705289b0464947e632) Thanks [@yusukebe](https://github.com/yusukebe)! - feat: add `validationFunction` option
## 0.4.3
### Patch Changes

View File

@ -68,6 +68,30 @@ app.post(
)
```
### Custom validation function
By default, this Validator validates values using `.safeParseAsync`.
```ts
await schema.safeParseAsync(value)
```
But, if you want to use the [`.passthrough`](https://zod.dev/?id=passthrough), you can specify your own function in `validationFunction`.
```ts
app.post(
'/',
zValidator('json', schema, undefined, {
validationFunction: async (schema, value) => {
return await schema.passthrough().safeParseAsync(value)
},
}),
(c) => {
// ...
}
)
```
## Author
Yusuke Wada <https://github.com/yusukebe>

View File

@ -1,6 +1,6 @@
{
"name": "@hono/zod-validator",
"version": "0.4.3",
"version": "0.5.0",
"description": "Validator middleware using Zod",
"type": "module",
"main": "dist/index.js",

View File

@ -378,3 +378,85 @@ describe('Case-Insensitive Headers', () => {
type verify = Expect<Equal<Expected, Actual>>
})
})
describe('With options + validationFunction', () => {
const app = new Hono()
const jsonSchema = z.object({
name: z.string(),
age: z.number(),
})
const route = app
.post('/', zValidator('json', jsonSchema), (c) => {
const data = c.req.valid('json')
return c.json({
success: true,
data,
})
})
.post(
'/extended',
zValidator('json', jsonSchema, undefined, {
validationFunction: async (schema, value) => {
return await schema.passthrough().safeParseAsync(value)
},
}),
(c) => {
const data = c.req.valid('json')
return c.json({
success: true,
data,
})
}
)
it('Should be ok due to passthrough schema', async () => {
const req = new Request('http://localhost/extended', {
body: JSON.stringify({
name: 'Superman',
age: 20,
length: 170,
weight: 55,
}),
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
const res = await app.request(req)
expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(await res.json()).toEqual({
success: true,
data: {
name: 'Superman',
age: 20,
length: 170,
weight: 55,
},
})
})
it('Should be ok due to required schema', async () => {
const req = new Request('http://localhost', {
body: JSON.stringify({
name: 'Superman',
age: 20,
}),
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
const res = await app.request(req)
expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(await res.json()).toEqual({
success: true,
data: {
name: 'Superman',
age: 20,
},
})
})
})

View File

@ -1,14 +1,14 @@
import type { Context, Env, Input, MiddlewareHandler, TypedResponse, ValidationTargets } from 'hono'
import { validator } from 'hono/validator'
import { ZodObject } from 'zod'
import type { ZodError, ZodSchema, z } from 'zod'
import type { SafeParseReturnType, ZodError, ZodSchema, z } from 'zod'
export type Hook<
T,
E extends Env,
P extends string,
Target extends keyof ValidationTargets = keyof ValidationTargets,
O = {}
O = {},
> = (
result: ({ success: true; data: T } | { success: false; error: ZodError; data: T }) & {
target: Target
@ -39,11 +39,18 @@ export const zValidator = <
}
out: { [K in Target]: Out }
},
V extends I = I
V extends I = I,
>(
target: Target,
schema: T,
hook?: Hook<z.infer<T>, E, P, Target>
hook?: Hook<z.infer<T>, E, P, Target>,
options?: {
validationFunction: (
schema: T,
value: ValidationTargets[Target]
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) => SafeParseReturnType<any, any> | Promise<SafeParseReturnType<any, any>>
}
): MiddlewareHandler<E, P, V> =>
// @ts-expect-error not typed well
validator(target, async (value, c) => {
@ -63,7 +70,10 @@ export const zValidator = <
)
}
const result = await schema.safeParseAsync(validatorValue)
const result =
options && options.validationFunction
? await options.validationFunction(schema, validatorValue)
: await schema.safeParseAsync(validatorValue)
if (hook) {
const hookResult = await hook({ data: validatorValue, ...result, target }, c)