feat(stage-tamagotchi,stage-ui,server-sdk,server-runtime): zero-trust websocket authentication (#1582)

This commit is contained in:
Richard Pinedo
2026-04-07 04:05:35 +08:00
committed by GitHub
parent 5452c72351
commit 99be852fe4
21 changed files with 630 additions and 122 deletions
@@ -15,7 +15,12 @@ function createPeer(options: {
labels?: Record<string, string>
}): AuthenticatedPeer {
return {
peer: { id: options.id, send: () => 0 },
peer: {
id: options.id,
send: () => 0,
request: { url: 'http://localhost', headers: new Headers() },
remoteAddress: '127.0.0.1',
},
authenticated: true,
name: options.name,
identity: options.plugin && options.instanceId
@@ -92,6 +97,21 @@ describe('route middleware', () => {
expect(collectDestinations(event)).toEqual(['label:env=prod'])
})
it('respects explicit empty destinations as an override', () => {
const event = createSparkNotifyEvent({
data: {
id: 'evt-3',
eventId: 'spark-3',
kind: 'ping',
urgency: 'soon',
headline: 'hello',
destinations: ['module:character'],
},
route: { destinations: [] },
})
expect(collectDestinations(event)).toEqual([])
})
it('treats an explicit empty route destination list as the override', () => {
const event = createSparkNotifyEvent({
@@ -42,7 +42,7 @@ function getPeerLabels(peer: AuthenticatedPeer) {
}
}
export function matchesRouteExpression(expression: RouteTargetExpression, peer: AuthenticatedPeer) {
export function matchesRouteExpression(expression: RouteTargetExpression, peer: AuthenticatedPeer): boolean {
switch (expression.type) {
case 'and':
return expression.all.every(expr => matchesRouteExpression(expr, peer))
@@ -0,0 +1,87 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const serveMocks = vi.hoisted(() => {
let resolveServe: (() => void) | null = null
let rejectServe: ((error: Error) => void) | null = null
const serveCall = vi.fn(() => new Promise<void>((resolve, reject) => {
resolveServe = resolve
rejectServe = reject
}))
const closeCall = vi.fn(async () => {})
const setupAppCall = vi.fn(() => ({
app: {
fetch: vi.fn(async () => ({ crossws: {} })),
},
closeAllPeers: vi.fn(),
}))
return {
closeCall,
rejectServe: (error: Error) => rejectServe?.(error),
resolveServe: () => resolveServe?.(),
serveCall,
setupAppCall,
}
})
vi.mock('h3', () => ({
H3: class {
get = vi.fn()
},
defineWebSocketHandler: vi.fn(handler => handler),
serve: vi.fn(() => ({
serve: serveMocks.serveCall,
close: serveMocks.closeCall,
})),
}))
vi.mock('crossws/server', () => ({
plugin: vi.fn(() => ({})),
}))
vi.mock('..', () => ({
normalizeLoggerConfig: () => ({
appLogFormat: 'pretty',
appLogLevel: 'log',
}),
setupApp: serveMocks.setupAppCall,
}))
describe('createServer', async () => {
const { createServer } = await import('./server')
beforeEach(() => {
vi.clearAllMocks()
})
it('deduplicates concurrent start calls while a start is already in progress', async () => {
const server = createServer({ hostname: '127.0.0.1', port: 6121 })
const firstStart = server.start()
const secondStart = server.start()
expect(serveMocks.serveCall).toHaveBeenCalledTimes(1)
serveMocks.resolveServe()
await Promise.all([firstStart, secondStart])
expect(serveMocks.serveCall).toHaveBeenCalledTimes(1)
})
it('clears the single-flight state when start fails', async () => {
const server = createServer({ hostname: '127.0.0.1', port: 6121 })
const firstStart = server.start()
serveMocks.rejectServe(new Error('bind failed'))
await expect(firstStart).rejects.toThrow('bind failed')
const retryStart = server.start()
expect(serveMocks.serveCall).toHaveBeenCalledTimes(2)
serveMocks.resolveServe()
await retryStart
})
})
+58 -49
View File
@@ -71,11 +71,12 @@ export function getLocalIPs(): string[] {
}
export function createServer(opts?: ServerOptions): Server {
let options = merge<ServerOptions>({ port: 6121, hostname: '0.0.0.0' }, opts)
let options = merge<ServerOptions>({ port: 6121, hostname: '127.0.0.1' }, opts)
const { appLogFormat, appLogLevel } = normalizeLoggerConfig(options)
const log = useLogg('@proj-airi/server-runtime/server').withLogLevelString(appLogLevel).withFormat(appLogFormat)
let serverInstance: ServerInstance | null = null
let startTask: Promise<void> | null = null
log.withFields({ hasTlsConfig: !!options?.tlsConfig }).log('creating server channel')
@@ -110,60 +111,68 @@ export function createServer(opts?: ServerOptions): Server {
if (serverInstance) {
return
}
if (startTask) {
return startTask
}
const secureEnabled = options?.tlsConfig != null
const h3App = setupApp()
startTask = (async () => {
const secureEnabled = options?.tlsConfig != null
const h3App = setupApp(options)
const port = options.port
const hostname = options.hostname
const port = options.port
const hostname = options.hostname
const instance = serve(h3App.app, {
// @ts-expect-error - the .crossws property wasn't extended in types
plugins: [ws({ resolve: async req => (await h3App.app.fetch(req)).crossws })],
port,
hostname,
tls: options?.tlsConfig || undefined,
reusePort: true,
silent: true,
manual: true,
gracefulShutdown: {
forceTimeout: 0.5,
gracefulTimeout: 0.5,
},
const instance = serve(h3App.app, {
// @ts-expect-error - the .crossws property wasn't extended in types
plugins: [ws({ resolve: async req => (await h3App.app.fetch(req)).crossws })],
port,
hostname,
tls: options?.tlsConfig || undefined,
reusePort: true,
silent: true,
manual: true,
gracefulShutdown: {
forceTimeout: 0.5,
gracefulTimeout: 0.5,
},
})
try {
serverInstance = {
close: async (closeActiveConnections = false) => {
log.log('closing all peers')
h3App.closeAllPeers()
log.log('closing server instance')
await instance.close(closeActiveConnections)
log.log('server instance closed')
},
}
await instance.serve()
const protocol = secureEnabled ? 'wss' : 'ws'
if (hostname === '0.0.0.0') {
const ips = getLocalIPs().filter(ip => ip !== '127.0.0.1' && ip !== '::1')
const targets = ips.length > 0 ? ips.join(', ') : 'localhost'
log.log(`@proj-airi/server-runtime started on ${protocol}://0.0.0.0:${port} (reachable via: ${targets})`)
}
else {
log.log(`@proj-airi/server-runtime started on ${protocol}://${hostname}:${port}`)
}
}
catch (error) {
serverInstance = null
h3App.closeAllPeers()
await instance.close(true).catch(() => {})
log.withError(error).error('failed to start WebSocket server')
throw error
}
})().finally(() => {
startTask = null
})
try {
await instance.serve()
serverInstance = {
close: async (closeActiveConnections = false) => {
log.log('closing all peers')
h3App.closeAllPeers()
log.log('closing server instance')
await instance.close(closeActiveConnections)
log.log('server instance closed')
},
}
const protocol = secureEnabled ? 'wss' : 'ws'
if (hostname === '0.0.0.0') {
const ips = getLocalIPs().filter(ip => ip !== '127.0.0.1' && ip !== '::1')
const targets = ips.length > 0 ? ips.join(', ') : 'localhost'
log.log(`@proj-airi/server-runtime started on ${protocol}://0.0.0.0:${port} (reachable via: ${targets})`)
}
else {
log.log(`@proj-airi/server-runtime started on ${protocol}://${hostname}:${port}`)
}
}
catch (error) {
serverInstance = null
h3App.closeAllPeers()
await instance.close(true).catch(() => {})
log.withError(error).error('failed to start WebSocket server')
throw error
}
return startTask
}
async function stop() {
await closeServer(true)
}
@@ -13,6 +13,11 @@ export interface Peer {
* WebSocket lifecycle state (mirrors WebSocket.readyState)
*/
readyState?: number
request?: {
url?: string
headers?: Headers
}
remoteAddress?: string
}
export interface NamedPeer {