|
1 | 1 | import type { H3Event } from 'h3' |
| 2 | +import type { IncomingMessage } from 'node:http' |
2 | 3 | import type { AuthRouteRules } from '../../types' |
3 | 4 | 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' |
14 | 6 | import { getRouteRules, useRuntimeConfig } from 'nitropack/runtime' |
15 | 7 |
|
16 | 8 | export { |
17 | 9 | defineEventHandler, |
18 | | - getQuery, |
19 | | - getRequestHost, |
20 | | - getRequestProtocol, |
21 | | - getRequestURL, |
22 | | - readBody, |
23 | | - toWebRequest, |
24 | 10 | useRuntimeConfig, |
25 | 11 | } |
26 | 12 |
|
27 | 13 | 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 | +} |
28 | 118 |
|
29 | 119 | export function splitCookiesString(cookiesString: string | string[]): string[] { |
30 | 120 | return splitSetCookieString(cookiesString) |
31 | 121 | } |
32 | 122 |
|
33 | 123 | 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 | + } |
35 | 144 | } |
36 | 145 |
|
37 | 146 | export function createAuthError(status: number, statusText: string): Error { |
38 | | - return createError({ statusCode: status, statusMessage: statusText }) |
| 147 | + return createNitro2AuthError(status, statusText) |
39 | 148 | } |
0 commit comments