Skip to content

Commit 5c9ad3c

Browse files
fix(deps): update dependency h3 to v2 (#557)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: vitehub-bot[bot] <320448255+vitehub-bot[bot]@users.noreply.github.com>
1 parent 445d664 commit 5c9ad3c

9 files changed

Lines changed: 23567 additions & 650 deletions

File tree

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@
102102
"cookie-es": "^3.1.1",
103103
"defu": "^6.1.7",
104104
"get-port-please": "^3.2.0",
105-
"h3": "^1.15.11",
105+
"h3": "^2.0.0",
106106
"jiti": "^2.7.0",
107107
"nostics": "^1.2.0",
108108
"pathe": "^2.0.3",

‎pnpm-lock.yaml‎

Lines changed: 23382 additions & 607 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pnpm-workspace.yaml‎

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,19 @@
11
minimumReleaseAgeExcludePrune: true
22
minimumReleaseAgeExclude:
3-
- '@nuxt/cli@4.0.0'
4-
# eslint-disable-next-line yaml/plain-scalar
5-
- '@nuxt/kit@4.6.0'
63
- '@nuxthub/core@0.10.9'
7-
- '@nuxthub/core@0.10.10'
8-
- happy-dom@21.0.4
94
- better-call@1.4.4
10-
- '@better-auth/core@1.7.11'
115
- '@better-auth/core@1.7.12'
12-
- '@better-auth/drizzle-adapter@1.7.11'
136
- '@better-auth/drizzle-adapter@1.7.12'
14-
- '@better-auth/kysely-adapter@1.7.11'
157
- '@better-auth/kysely-adapter@1.7.12'
16-
- '@better-auth/memory-adapter@1.7.11'
178
- '@better-auth/memory-adapter@1.7.12'
18-
- '@better-auth/mongo-adapter@1.7.11'
199
- '@better-auth/mongo-adapter@1.7.12'
20-
- '@better-auth/passkey@1.7.11'
2110
- '@better-auth/passkey@1.7.12'
22-
- '@better-auth/prisma-adapter@1.7.11'
2311
- '@better-auth/prisma-adapter@1.7.12'
24-
- '@better-auth/telemetry@1.7.11'
2512
- '@better-auth/telemetry@1.7.12'
26-
- auth@1.7.11
2713
- auth@1.7.12
28-
- better-auth@1.7.11
2914
- better-auth@1.7.12
3015
- better-call@1.4.4
31-
- satori@0.63.2
16+
- satori@0.63.1
3217
- miniflare@5.20261019.1-beta
3318
- wrangler@4.151.3
3419

‎src/module/hooks.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Nuxt, NuxtPage } from '@nuxt/schema'
22
import type { AuthRouteRules } from '../runtime/types'
33
import type { ServerImportsTypesFile } from './compatibility'
44
import { existsSync, statSync } from 'node:fs'
5-
import { addComponentsDir, addImports, addNitroPlugin, addPlugin, addRouteMiddleware, addServerHandler, addServerImports, extendPages, updateTemplates } from '@nuxt/kit'
5+
import { addComponentsDir, addImports, addNitroPlugin, addPlugin, addRouteMiddleware, addServerHandler, addServerImports, addTypeTemplate, extendPages, updateTemplates } from '@nuxt/kit'
66
import { defu } from 'defu'
77
import { isAbsolute, join } from 'pathe'
88
import { createRouter, toRouteMatcher } from 'radix3'
@@ -93,6 +93,15 @@ export function registerAuthMiddleware(resolve: (path: string) => string): void
9393
export function registerPrepareTypesHook(input: RegisterPrepareTypesHookInput): void {
9494
const { nuxt, serverDir, hasHubDb, serverImportsTypesFile } = input
9595

96+
addTypeTemplate({
97+
filename: 'types/auth-route-rules.d.ts',
98+
getContents: () => `
99+
declare module '#auth/route-rules' {
100+
export const authRouteRules: Record<string, { auth?: unknown }>
101+
}
102+
`,
103+
}, { nitro: true, node: true })
104+
96105
nuxt.hook('prepare:types', ({ nodeTsConfig, nodeReferences, sharedReferences }) => {
97106
nodeTsConfig.compilerOptions ||= {}
98107
nodeTsConfig.compilerOptions.paths ||= {}
Lines changed: 127 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,148 @@
11
import type { H3Event } from 'h3'
2+
import type { IncomingMessage } from 'node:http'
23
import type { AuthRouteRules } from '../../types'
34
import { splitSetCookieString } from 'cookie-es'
4-
import {
5-
createError,
6-
defineEventHandler,
7-
getQuery,
8-
getRequestHost,
9-
getRequestProtocol,
10-
getRequestURL,
11-
readBody,
12-
toWebRequest,
13-
} from 'h3'
5+
import { defineEventHandler } from 'h3'
146
import { getRouteRules, useRuntimeConfig } from 'nitropack/runtime'
157

168
export {
179
defineEventHandler,
18-
getQuery,
19-
getRequestHost,
20-
getRequestProtocol,
21-
getRequestURL,
22-
readBody,
23-
toWebRequest,
2410
useRuntimeConfig,
2511
}
2612

2713
export type ServerEvent = H3Event
14+
type Nitro2Event = ServerEvent & { path?: string, node?: { req?: IncomingMessage }, web?: { request?: Request } }
15+
16+
// Nitro 2's dev server serializes errors with h3 v1 while the module runs on h3 v2.
17+
function createNitro2AuthError(status: number, statusText: string): Error {
18+
// Use a plain Error with both h3 generations' identity fields. Nitro 2's
19+
// bundled h3 v1 recognizes the constructor marker, while h3 v2 recognizes
20+
// the HTTPError name and status. Keeping the object independent of either
21+
// package's Error subclass lets it cross the dev-server boundary intact.
22+
const error = new Error(statusText)
23+
const markerConstructor = function Nitro2ErrorConstructor() {}
24+
Object.defineProperty(markerConstructor, '__h3_error__', { value: true })
25+
Object.defineProperties(error, {
26+
name: { value: 'HTTPError', configurable: true, enumerable: false, writable: true },
27+
constructor: { value: markerConstructor, configurable: true },
28+
__h3_error__: { value: true, configurable: true, enumerable: true, writable: true },
29+
status: { value: status, configurable: true, enumerable: true, writable: true },
30+
statusText: { value: statusText, configurable: true, enumerable: true, writable: true },
31+
statusCode: { value: status, configurable: true, enumerable: true, writable: true },
32+
statusMessage: { value: statusText, configurable: true, enumerable: true, writable: true },
33+
toJSON: {
34+
value: () => ({
35+
status,
36+
statusText,
37+
statusCode: status,
38+
statusMessage: statusText,
39+
message: statusText,
40+
unhandled: false,
41+
fatal: false,
42+
}),
43+
configurable: true,
44+
},
45+
unhandled: { value: false, configurable: true, enumerable: true, writable: true },
46+
fatal: { value: false, configurable: true, enumerable: true, writable: true },
47+
})
48+
return error
49+
}
50+
51+
// Nitro 2 events expose Node's IncomingMessage while h3 v2 helpers expect a
52+
// Web Request based event. Keep this shim independent of h3's event shape so
53+
// Nitro 2 hosts continue to work when the project installs h3 v2.
54+
function requestHeaders(event: ServerEvent): IncomingMessage['headers'] {
55+
return (event as Nitro2Event).node?.req?.headers ?? {}
56+
}
57+
58+
export function getRequestHost(event: ServerEvent, options: { xForwardedHost?: boolean } = {}): string {
59+
const headers = requestHeaders(event)
60+
if (options.xForwardedHost) {
61+
const forwarded = headers['x-forwarded-host']
62+
const host = Array.isArray(forwarded) ? forwarded[0] : forwarded?.split(',')[0]?.trim()
63+
if (host)
64+
return host
65+
}
66+
const host = headers.host
67+
return (Array.isArray(host) ? host[0] : host) || 'localhost'
68+
}
69+
70+
export function getRequestProtocol(event: ServerEvent, options: { xForwardedProto?: boolean } = {}): string {
71+
const headers = requestHeaders(event)
72+
if (options.xForwardedProto !== false && headers['x-forwarded-proto'] === 'https')
73+
return 'https'
74+
return (event as Nitro2Event).node?.req?.socket && (event as Nitro2Event).node.req.socket['encrypted' as never] ? 'https' : 'http'
75+
}
76+
77+
export function getRequestURL(event: ServerEvent): URL {
78+
const req = (event as Nitro2Event).node?.req ?? {}
79+
const path = req.originalUrl || req.url || (event as Nitro2Event).path || '/'
80+
return new URL(path, `${getRequestProtocol(event)}://${getRequestHost(event)}`)
81+
}
82+
83+
export function getQuery(event: ServerEvent): Record<string, string | string[]> {
84+
const query: Record<string, string | string[]> = {}
85+
for (const [key, value] of getRequestURL(event).searchParams) {
86+
const previous = query[key]
87+
query[key] = previous === undefined ? value : Array.isArray(previous) ? [...previous, value] : [previous, value]
88+
}
89+
return query
90+
}
91+
92+
export function toWebRequest(event: ServerEvent): Request {
93+
const req = (event as Nitro2Event).node?.req
94+
const webRequest = (event as Nitro2Event).web?.request
95+
if (webRequest)
96+
return webRequest
97+
const init: RequestInit & { duplex?: 'half' } = {
98+
method: req?.method,
99+
headers: req?.headers
100+
? Object.fromEntries(Object.entries(req.headers).flatMap(([key, value]) => value === undefined ? [] : [[key, Array.isArray(value) ? value.join(', ') : value]]))
101+
: undefined,
102+
duplex: 'half',
103+
}
104+
if (req?.method !== 'GET' && req?.method !== 'HEAD')
105+
init.body = req as unknown as BodyInit
106+
return new Request(getRequestURL(event), init)
107+
}
108+
109+
export async function readBody(event: ServerEvent): Promise<unknown> {
110+
const request = toWebRequest(event)
111+
const contentType = request.headers.get('content-type') || ''
112+
if (contentType.includes('application/json'))
113+
return request.json()
114+
if (contentType.includes('application/x-www-form-urlencoded'))
115+
return Object.fromEntries((await request.text()).split('&').filter(Boolean).map(pair => pair.split('=').map(decodeURIComponent)))
116+
return request.text()
117+
}
28118

29119
export function splitCookiesString(cookiesString: string | string[]): string[] {
30120
return splitSetCookieString(cookiesString)
31121
}
32122

33123
export function getAuthRouteRules(event: ServerEvent): AuthRouteRules {
34-
return getRouteRules(event) as AuthRouteRules
124+
// h3 v2 events created by Nitro 2 compatibility handlers may omit the
125+
// legacy context and path properties. Nitro's route-rules helper stores its
126+
// cache there and reads the path directly from the event.
127+
const eventWithContext = event as unknown as { context?: Record<string, unknown>, path?: string }
128+
if (!eventWithContext.context)
129+
eventWithContext.context = {}
130+
if (!eventWithContext.path)
131+
eventWithContext.path = getRequestURL(event).pathname
132+
// Dev handlers may fall through before Nitro has initialized route rules on
133+
// the event. Treat that as an unruled route so auth middleware can continue
134+
// and apply the configured protection instead of throwing on `rules.auth`.
135+
// A dev server handler can invoke middleware before Nitro has fully
136+
// initialized its route-rule state. In that case the Nitro helper may
137+
// throw; the caller can still resolve the generated auth rules below.
138+
try {
139+
return (getRouteRules(event) as AuthRouteRules | undefined) || {}
140+
}
141+
catch {
142+
return {}
143+
}
35144
}
36145

37146
export function createAuthError(status: number, statusText: string): Error {
38-
return createError({ statusCode: status, statusMessage: statusText })
147+
return createNitro2AuthError(status, statusText)
39148
}

‎src/runtime/server/internal/portable.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ export function splitCookiesString(cookiesString: string | string[]): string[] {
3131
}
3232

3333
export function getAuthRouteRules(event: ServerEvent): AuthRouteRules {
34-
const rules = getRouteRules(event) as { auth?: unknown }
35-
return { auth: normalizeAuthRouteRule(rules.auth) }
34+
const rules = getRouteRules(event) as { auth?: unknown } | undefined
35+
return { auth: normalizeAuthRouteRule(rules?.auth) }
3636
}
3737

3838
export function createAuthError(status: number, statusText: string): Error {

‎src/runtime/server/middleware/route-access.ts‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import type { AuthMeta, AuthMode, AuthRouteRules } from '../../types'
2+
import { defu } from 'defu'
3+
import { createRouter, toRouteMatcher } from 'radix3'
24
import { normalizeAuthRoutePath, shouldSkipAuthRouteRules } from '../../internal/auth-route-rules'
35
import { matchesUser } from '../../utils/match-user'
46
import { prepareAuthSecret } from '../internal/auth-secret'
57
import { createAuthError, defineEventHandler, getAuthRouteRules, getRequestURL, useRuntimeConfig } from '../internal/nitro-compat'
68
import { getUserSession, requireUserSession } from '../utils/session'
79

10+
let authRouteRulesMatcherPromise: Promise<ReturnType<typeof toRouteMatcher> | null> | null = null
11+
812
export default defineEventHandler(async (event) => {
913
// Runs before app handlers, so their synchronous serverAuth() calls see the derived secret.
1014
await prepareAuthSecret()
@@ -20,7 +24,7 @@ export default defineEventHandler(async (event) => {
2024
if (shouldSkipAuthRouteRules(path))
2125
return
2226

23-
const rules = getAuthRouteRules(event) as AuthRouteRules
27+
const rules = await resolveAuthRouteRules(event, path)
2428
if (!rules.auth)
2529
return
2630

@@ -43,3 +47,24 @@ export default defineEventHandler(async (event) => {
4347
}
4448
}
4549
})
50+
51+
async function resolveAuthRouteRules(event: Parameters<typeof getAuthRouteRules>[0], path: string): Promise<AuthRouteRules> {
52+
const rules = getAuthRouteRules(event) as AuthRouteRules | undefined
53+
if (rules?.auth !== undefined)
54+
return rules
55+
56+
if (!authRouteRulesMatcherPromise) {
57+
authRouteRulesMatcherPromise = import('#auth/route-rules').then((mod) => {
58+
const routeRules = (mod as { authRouteRules?: Record<string, AuthRouteRules> }).authRouteRules || {}
59+
return Object.keys(routeRules).length
60+
? toRouteMatcher(createRouter({ routes: routeRules }))
61+
: null
62+
}).catch(() => null)
63+
}
64+
65+
const matcher = await authRouteRulesMatcherPromise
66+
const matches = matcher?.matchAll(path) as Partial<AuthRouteRules>[] | undefined
67+
if (!matches?.length)
68+
return rules || {}
69+
return defu({}, ...matches.reverse()) as AuthRouteRules
70+
}

‎src/runtime/server/utils/auth.ts‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,22 @@ function resolveEventOrigin(event?: ServerEvent): string | undefined {
123123
if (!event)
124124
return undefined
125125

126-
const host = getRequestHost(event, { xForwardedHost: true })
127-
const protocol = getRequestProtocol(event, { xForwardedProto: true })
126+
let host: string
127+
let protocol: string
128+
try {
129+
host = getRequestHost(event, { xForwardedHost: true })
130+
protocol = getRequestProtocol(event, { xForwardedProto: true })
131+
}
132+
catch {
133+
const request = (event as ServerEvent & { node?: { req?: { headers?: Record<string, string | string[] | undefined>, socket?: { encrypted?: boolean } } } }).node?.req
134+
const headers = request?.headers ?? {}
135+
const forwardedHost = headers['x-forwarded-host']
136+
const forwarded = Array.isArray(forwardedHost) ? forwardedHost[0] : forwardedHost?.split(',')[0]?.trim()
137+
const rawHost = forwarded || headers.host
138+
host = (Array.isArray(rawHost) ? rawHost[0] : rawHost) || ''
139+
const forwardedProto = headers['x-forwarded-proto']
140+
protocol = (Array.isArray(forwardedProto) ? forwardedProto[0] : forwardedProto) || (request?.socket?.encrypted ? 'https' : 'http')
141+
}
128142
if (!host || !protocol)
129143
return undefined
130144

‎test/cases/fonts-route-rules/modules/dev-assets.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { addDevServerHandler, defineNuxtModule } from '@nuxt/kit'
2-
import { eventHandler, setResponseHeader } from 'h3'
2+
import { eventHandler } from 'h3'
33
import { joinURL } from 'ufo'
44

55
export default defineNuxtModule({
@@ -35,7 +35,7 @@ export default defineNuxtModule({
3535
addDevServerHandler({
3636
route: joinURL(nuxt.options.app.baseURL, '/api/auth-required'),
3737
handler: eventHandler((event) => {
38-
setResponseHeader(event, 'x-test-dev-handler', 'fallthrough')
38+
event.node?.res?.setHeader('x-test-dev-handler', 'fallthrough')
3939
}),
4040
})
4141
},

0 commit comments

Comments
 (0)