fix(stage-ui): preserve and propagate discord context in bridge (#934)

---------

Co-authored-by: Neko Ayaka <neko@ayaka.moe>
This commit is contained in:
skyline624
2026-01-11 23:42:45 +08:00
committed by GitHub
co-authored by Neko Ayaka
parent 00666852fc
commit b500eb8f5b
17 changed files with 149 additions and 110 deletions
+2 -1
View File
@@ -39,6 +39,7 @@
"@proj-airi/server-shared": "workspace:^",
"crossws": "^0.4.1",
"h3": "^2.0.1-rc.6",
"listhen": "^1.9.0"
"listhen": "^1.9.0",
"nanoid": "catalog:"
}
}
+50 -49
View File
@@ -11,6 +11,9 @@ import type { AuthenticatedPeer, Peer } from './types'
import { availableLogLevelStrings, Format, LogLevelString, logLevelStringToLogLevelMap, useLogg } from '@guiiai/logg'
import { MessageHeartbeat, MessageHeartbeatKind, WebSocketEventSource } from '@proj-airi/server-shared/types'
import { defineWebSocketHandler, H3 } from 'h3'
import { nanoid } from 'nanoid'
import packageJSON from '../package.json'
import { optionOrEnv } from './config'
import {
@@ -20,10 +23,42 @@ import {
matchesDestinations,
} from './middlewares'
function createServerEventMetadata(serverInstanceId: string, parentId?: string) {
return {
event: {
id: nanoid(),
parentId,
},
source: {
plugin: WebSocketEventSource.Server,
instanceId: serverInstanceId,
version: packageJSON.version,
},
}
}
// pre-stringified responses
const RESPONSES = {
authenticated: JSON.stringify({ type: 'module:authenticated', data: { authenticated: true }, source: WebSocketEventSource.Server } satisfies WebSocketEvent),
notAuthenticated: JSON.stringify({ type: 'error', data: { message: 'not authenticated' }, source: WebSocketEventSource.Server } satisfies WebSocketEvent),
authenticated: (serverInstanceId: string, parentId?: string) => JSON.stringify({
type: 'module:authenticated',
data: { authenticated: true },
metadata: createServerEventMetadata(serverInstanceId, parentId),
} satisfies WebSocketEvent),
notAuthenticated: (serverInstanceId: string, parentId?: string) => JSON.stringify({
type: 'error',
data: { message: 'not authenticated' },
metadata: createServerEventMetadata(serverInstanceId, parentId),
} satisfies WebSocketEvent),
error: (message: string, serverInstanceId: string, parentId?: string) => JSON.stringify({
type: 'error',
data: { message },
metadata: createServerEventMetadata(serverInstanceId, parentId),
}),
heartbeat: (kind: MessageHeartbeatKind, message: MessageHeartbeat | string, serverInstanceId: string, parentId?: string) => JSON.stringify({
type: 'transport:connection:heartbeat',
data: { kind, message, at: Date.now() },
metadata: createServerEventMetadata(serverInstanceId, parentId),
} satisfies WebSocketEvent),
}
const DEFAULT_HEARTBEAT_TTL_MS = 60_000
@@ -34,6 +69,7 @@ function send(peer: Peer, event: WebSocketEvent<Record<string, unknown>> | strin
}
export function setupApp(options?: {
instanceId?: string
auth?: {
token: string
}
@@ -51,6 +87,7 @@ export function setupApp(options?: {
message?: MessageHeartbeat | string
}
}): H3 {
const instanceId = options?.instanceId || optionOrEnv(undefined, 'SERVER_INSTANCE_ID', nanoid())
const authToken = optionOrEnv(options?.auth?.token, 'AUTHENTICATION_TOKEN', '')
const appLogLevel = optionOrEnv(options?.logger?.app?.level, 'LOG_LEVEL', LogLevelString.Log, { validator: (value): value is LogLevelString => availableLogLevelStrings.includes(value as LogLevelString) })
@@ -144,7 +181,7 @@ export function setupApp(options?: {
}
catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err)
send(peer, { type: 'error', data: { message: `invalid JSON, error: ${errorMessage}` }, source: WebSocketEventSource.Server })
send(peer, RESPONSES.error(`invalid JSON, error: ${errorMessage}`, instanceId))
return
}
@@ -171,15 +208,7 @@ export function setupApp(options?: {
}
if (event.data.kind === MessageHeartbeatKind.Ping) {
send(peer, {
type: 'transport:connection:heartbeat',
data: {
kind: MessageHeartbeatKind.Pong,
message: heartbeatMessage,
at: Date.now(),
},
source: WebSocketEventSource.Server,
})
send(peer, RESPONSES.heartbeat(MessageHeartbeatKind.Pong, heartbeatMessage, instanceId, event.metadata?.event.id))
}
return
@@ -188,11 +217,7 @@ export function setupApp(options?: {
case 'module:authenticate': {
if (authToken && event.data.token !== authToken) {
logger.withFields({ peer: peer.id, peerRemote: peer.remoteAddress, peerRequest: peer.request.url }).log('authentication failed')
send(peer, {
type: 'error',
data: { message: 'invalid token' },
source: WebSocketEventSource.Server,
})
send(peer, RESPONSES.error('invalid token', instanceId, event.metadata?.event.id))
return
}
@@ -217,31 +242,19 @@ export function setupApp(options?: {
// verify
const { name, index, identity } = event.data as { name: string, index?: number, identity?: MetadataEventSource }
if (!name || typeof name !== 'string') {
send(peer, {
type: 'error',
data: { message: 'the field \'name\' must be a non-empty string for event \'module:announce\'' },
source: WebSocketEventSource.Server,
})
send(peer, RESPONSES.error('the field \'name\' must be a non-empty string for event \'module:announce\'', instanceId))
return
}
if (typeof index !== 'undefined') {
if (!Number.isInteger(index) || index < 0) {
send(peer, {
type: 'error',
data: { message: 'the field \'index\' must be a non-negative integer for event \'module:announce\'' },
source: WebSocketEventSource.Server,
})
send(peer, RESPONSES.error('the field \'index\' must be a non-negative integer for event \'module:announce\'', instanceId))
return
}
}
if (authToken && !p.authenticated) {
send(peer, {
type: 'error',
data: { message: 'must authenticate before announcing' },
source: WebSocketEventSource.Server,
})
send(peer, RESPONSES.error('must authenticate before announcing', instanceId))
return
}
@@ -261,21 +274,13 @@ export function setupApp(options?: {
const { moduleName, moduleIndex, config } = event.data
if (moduleName === '') {
send(peer, {
type: 'error',
data: { message: 'the field \'moduleName\' can\'t be empty for event \'ui:configure\'' },
source: WebSocketEventSource.Server,
})
send(peer, RESPONSES.error('the field \'moduleName\' can\'t be empty for event \'ui:configure\'', instanceId))
return
}
if (typeof moduleIndex !== 'undefined') {
if (!Number.isInteger(moduleIndex) || moduleIndex < 0) {
send(peer, {
type: 'error',
data: { message: 'the field \'moduleIndex\' must be a non-negative integer for event \'ui:configure\'' },
source: WebSocketEventSource.Server,
})
send(peer, RESPONSES.error('the field \'moduleIndex\' must be a non-negative integer for event \'ui:configure\'', instanceId))
return
}
@@ -286,16 +291,12 @@ export function setupApp(options?: {
send(target.peer, {
type: 'module:configure',
data: { config },
// NOTICE: here we will forward the source as-is
source: event.source,
// NOTICE: this will forward the original event metadata as-is
metadata: event.metadata,
})
}
else {
send(peer, {
type: 'error',
data: { message: 'module not found, it hasn\'t announced itself or the name is incorrect' },
source: WebSocketEventSource.Server,
})
send(peer, RESPONSES.error('module not found, it hasn\'t announced itself or the name is incorrect', instanceId))
}
return
@@ -1,4 +1,4 @@
import type { RouteTargetExpression, WebSocketBaseEvent, WebSocketEvents } from '@proj-airi/server-shared/types'
import type { RouteTargetExpression, WebSocketBaseEvent, WebSocketEventOf, WebSocketEvents } from '@proj-airi/server-shared/types'
import type { AuthenticatedPeer } from '../types'
@@ -24,20 +24,26 @@ function createPeer(options: {
}
}
function createSparkNotifyEvent(overrides?: Partial<WebSocketBaseEvent<'spark:notify', WebSocketEvents['spark:notify'], any>>): WebSocketBaseEvent<'spark:notify', WebSocketEvents['spark:notify'], any> {
function createSparkNotifyEvent(overrides: Partial<WebSocketEventOf<'spark:notify'>> = {}): WebSocketBaseEvent<'spark:notify', WebSocketEvents['spark:notify'], any> {
const data: WebSocketEvents['spark:notify'] = {
id: 'evt-1',
eventId: 'spark-1',
kind: 'ping',
urgency: 'soon',
headline: 'hello',
destinations: ['module:character'],
...overrides.data,
}
return {
type: 'spark:notify',
data: {
id: 'evt-1',
eventId: 'spark-1',
kind: 'ping',
urgency: 'soon',
headline: 'hello',
destinations: ['module:character'],
data,
metadata: overrides.metadata ?? {
source: { plugin: 'server-runtime', instanceId: 'test' },
event: { id: data.id },
},
source: 'proj-airi:server-runtime',
...overrides,
}
route: overrides.route,
} as WebSocketBaseEvent<'spark:notify', WebSocketEvents['spark:notify'], any>
}
describe('match-expression', () => {
@@ -69,7 +69,7 @@ export function createPolicyMiddleware(policy: RoutingPolicy): RouteMiddleware {
}
}
export function collectDestinations(event: WebSocketEvent) {
export function collectDestinations(event: WebSocketEvent | (Omit<WebSocketEvent, 'metadata'> & Partial<Pick<WebSocketEvent, 'metadata'>>)) {
if (event.route?.destinations?.length) {
return event.route.destinations
}