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
pull/1095/head
Shotaro Nakamura 2025-04-01 21:35:45 +09:00 committed by GitHub
parent b18f24379b
commit 519404ad2c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 56 additions and 48 deletions

View File

@ -0,0 +1,5 @@
---
'@hono/node-ws': patch
---
Adapter won't send Buffer as a MessageEvent.

View File

@ -1,5 +1,5 @@
import type { Context } from 'hono'
import { getCookie } from 'hono/cookie';
import { getCookie } from 'hono/cookie'
import { createMiddleware } from 'hono/factory'
import { HTTPException } from 'hono/http-exception'

View File

@ -52,9 +52,7 @@ describe('WebSocket helper', () => {
})
it('Should be rejected if upgradeWebSocket is not used', async () => {
app.get(
'/', (c)=>c.body('')
)
app.get('/', (c) => c.body(''))
{
const ws = new WebSocket('ws://localhost:3030/')
@ -70,7 +68,8 @@ describe('WebSocket helper', () => {
expect(await mainPromise).toBe(true)
}
{ //also should rejected on fallback
{
//also should rejected on fallback
const ws = new WebSocket('ws://localhost:3030/notFound')
const mainPromise = new Promise<boolean>((resolve) => {
ws.onerror = () => {
@ -202,11 +201,11 @@ describe('WebSocket helper', () => {
ws.send(binaryData)
const receivedMessage = await mainPromise
expect(receivedMessage).toBeInstanceOf(Buffer)
expect((receivedMessage as Buffer).byteLength).toBe(binaryData.length)
expect(receivedMessage).toBeInstanceOf(ArrayBuffer)
expect((receivedMessage as ArrayBuffer).byteLength).toBe(binaryData.length)
binaryData.forEach((val, idx) => {
expect((receivedMessage as Buffer).at(idx)).toBe(val)
expect(new Uint8Array(receivedMessage as ArrayBuffer)[idx]).toBe(val)
})
})

View File

@ -25,7 +25,10 @@ export interface NodeWebSocketInit {
*/
export const createNodeWebSocket = (init: NodeWebSocketInit): NodeWebSocket => {
const wss = new WebSocketServer({ noServer: true })
const waiterMap = new Map<IncomingMessage, { resolve: (ws: WebSocket) => void, response: Response }>()
const waiterMap = new Map<
IncomingMessage,
{ resolve: (ws: WebSocket) => void; response: Response }
>()
wss.on('connection', (ws, request) => {
const waiter = waiterMap.get(request)
@ -64,9 +67,9 @@ export const createNodeWebSocket = (init: NodeWebSocketInit): NodeWebSocket => {
if (!waiter || waiter.response !== response) {
socket.end(
'HTTP/1.1 400 Bad Request\r\n' +
'Connection: close\r\n' +
'Content-Length: 0\r\n' +
'\r\n'
'Connection: close\r\n' +
'Content-Length: 0\r\n' +
'\r\n'
)
waiterMap.delete(request)
return
@ -113,7 +116,11 @@ export const createNodeWebSocket = (init: NodeWebSocketInit): NodeWebSocket => {
for (const data of datas) {
events.onMessage?.(
new MessageEvent('message', {
data: isBinary ? data : data.toString('utf-8'),
data: isBinary
? data instanceof ArrayBuffer
? data
: data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)
: data.toString('utf-8'),
}),
ctx
)

View File

@ -77,13 +77,13 @@ export class AuthFlow {
const url = 'https://id.twitch.tv/oauth2/token'
const response = (await fetch(url, {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: parsedOptions,
}).then((res) => res.json() as Promise<TwitchTokenResponse>))
}).then((res) => res.json() as Promise<TwitchTokenResponse>)
if ('error' in response) {
throw new HTTPException(400, { message: response.error })

View File

@ -14,18 +14,18 @@ export async function refreshToken(
client_secret,
})
const response = (await fetch('https://id.twitch.tv/oauth2/token', {
const response = await fetch('https://id.twitch.tv/oauth2/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: params,
}).then((res) => res.json() as Promise<TwitchRefreshResponse>))
}).then((res) => res.json() as Promise<TwitchRefreshResponse>)
if ('error' in response) {
throw new HTTPException(400, { message: response.error })
}
if ('message' in response) {
throw new HTTPException(400, { message: response.message as string })
}

View File

@ -2,10 +2,7 @@ import { HTTPException } from 'hono/http-exception'
import { toQueryParams } from '../../utils/objectToQuery'
import type { TwitchRevokingResponse } from './types'
export async function revokeToken(
client_id: string,
token: string
): Promise<boolean> {
export async function revokeToken(client_id: string, token: string): Promise<boolean> {
const params = toQueryParams({
client_id: client_id,
token,
@ -23,14 +20,16 @@ export async function revokeToken(
if (!res.ok) {
// Try to parse error response
try {
const errorResponse = await res.json() as TwitchRevokingResponse
const errorResponse = (await res.json()) as TwitchRevokingResponse
if (errorResponse && typeof errorResponse === 'object' && 'message' in errorResponse) {
throw new HTTPException(400, { message: errorResponse.message })
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
// If parsing fails, throw a generic error with the status
throw new HTTPException(400, { message: `Token revocation failed with status: ${res.status}` })
throw new HTTPException(400, {
message: `Token revocation failed with status: ${res.status}`,
})
}
}

View File

@ -2,10 +2,10 @@ export type Scopes =
// Analytics
| 'analytics:read:extensions'
| 'analytics:read:games'
// Bits
| 'bits:read'
// Channel
| 'channel:bot'
| 'channel:manage:ads'
@ -85,11 +85,11 @@ export type Scopes =
| 'moderator:read:vips'
| 'moderator:read:warnings'
| 'moderator:manage:warnings'
// IRC Chat Scopes
| 'chat:edit'
| 'chat:read'
// PubSub-specific Chat Scopes
| 'whispers:read'
@ -110,7 +110,6 @@ export type TwitchRefreshError = Required<Pick<TwitchErrorResponse, 'status' | '
export type TwitchTokenError = Required<Pick<TwitchErrorResponse, 'status' | 'message' | 'error'>>
// Success responses types from Twitch API
export interface TwitchValidateSuccess {
client_id: string
@ -150,19 +149,21 @@ export type TwitchTokenResponse = TwitchTokenSuccess | TwitchTokenError
export type TwitchValidateResponse = TwitchValidateSuccess | TwitchValidateError
export interface TwitchUserResponse {
data: [{
id: string
login: string
display_name: string
type: string
broadcaster_type: string
description: string
profile_image_url: string
offline_image_url: string
view_count: number
email: string
created_at: string
}]
data: [
{
id: string
login: string
display_name: string
type: string
broadcaster_type: string
description: string
profile_image_url: string
offline_image_url: string
view_count: number
email: string
created_at: string
}
]
}
export type TwitchUser = TwitchUserResponse['data'][0]

View File

@ -1,14 +1,11 @@
import { HTTPException } from 'hono/http-exception'
import type { TwitchValidateResponse } from './types'
export async function validateToken(
token: string
): Promise<TwitchValidateResponse> {
export async function validateToken(token: string): Promise<TwitchValidateResponse> {
const response = await fetch('https://id.twitch.tv/oauth2/validate', {
method: 'GET',
headers: {
authorization: `Bearer ${token}`,
authorization: `Bearer ${token}`,
},
}).then((res) => res.json() as Promise<TwitchValidateResponse>)