diff --git a/apps/stage-tamagotchi/scripts/update-test/start-server.ts b/apps/stage-tamagotchi/scripts/update-test/start-server.ts index 62e989816..e11d010dc 100644 --- a/apps/stage-tamagotchi/scripts/update-test/start-server.ts +++ b/apps/stage-tamagotchi/scripts/update-test/start-server.ts @@ -71,8 +71,12 @@ async function main() { exit(0) } - process.on('SIGINT', () => { void close() }) - process.on('SIGTERM', () => { void close() }) + process.on('SIGINT', () => { + void close() + }) + process.on('SIGTERM', () => { + void close() + }) } if (import.meta.main) { diff --git a/apps/stage-tamagotchi/src/main/services/airi/channel-server/config.test.ts b/apps/stage-tamagotchi/src/main/services/airi/channel-server/config.test.ts new file mode 100644 index 000000000..7c5bb8c73 --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/airi/channel-server/config.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it, vi } from 'vitest' + +import { ensureServerChannelConfigDefaults } from './config' + +describe('ensureServerChannelConfigDefaults', () => { + it('keeps an existing auth token', () => { + const generateToken = vi.fn(() => 'generated-token') + + const result = ensureServerChannelConfigDefaults({ + authToken: 'existing-token', + hostname: '0.0.0.0', + tlsConfig: null, + }, generateToken) + + expect(result.changed).toBe(false) + expect(result.config).toEqual({ + authToken: 'existing-token', + hostname: '0.0.0.0', + tlsConfig: null, + }) + expect(generateToken).not.toHaveBeenCalled() + }) + + it('generates a token when the config is missing one', () => { + const generateToken = vi.fn(() => 'generated-token') + + const result = ensureServerChannelConfigDefaults({ + authToken: '', + hostname: '', + tlsConfig: null, + }, generateToken) + + expect(result.changed).toBe(true) + expect(result.config).toEqual({ + authToken: 'generated-token', + hostname: '127.0.0.1', + tlsConfig: null, + }) + expect(generateToken).toHaveBeenCalledOnce() + }) +}) diff --git a/apps/stage-tamagotchi/src/main/services/airi/channel-server/config.ts b/apps/stage-tamagotchi/src/main/services/airi/channel-server/config.ts new file mode 100644 index 000000000..474fcd359 --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/airi/channel-server/config.ts @@ -0,0 +1,23 @@ +import type { ElectronServerChannelConfig } from '../../../../shared/eventa' + +export function ensureServerChannelConfigDefaults( + config: Partial, + generateToken: () => string, +) { + const nextConfig: ElectronServerChannelConfig = { + authToken: config.authToken?.trim() || generateToken(), + hostname: config.hostname?.trim() || '127.0.0.1', + tlsConfig: config.tlsConfig || null, + } + + const previousConfig: ElectronServerChannelConfig = { + authToken: config.authToken?.trim() || '', + hostname: config.hostname?.trim() || '127.0.0.1', + tlsConfig: config.tlsConfig || null, + } + + return { + changed: JSON.stringify(previousConfig) !== JSON.stringify(nextConfig), + config: nextConfig, + } +} diff --git a/apps/stage-tamagotchi/src/main/services/airi/channel-server/index.ts b/apps/stage-tamagotchi/src/main/services/airi/channel-server/index.ts index 9af9256af..d90f55013 100644 --- a/apps/stage-tamagotchi/src/main/services/airi/channel-server/index.ts +++ b/apps/stage-tamagotchi/src/main/services/airi/channel-server/index.ts @@ -1,7 +1,9 @@ import type { Server, ServerOptions } from '@proj-airi/server-runtime/server' import type { Lifecycle } from 'injeca' -import { X509Certificate } from 'node:crypto' +import type { ElectronServerChannelConfig } from '../../../../shared/eventa' + +import { randomUUID, X509Certificate } from 'node:crypto' import { existsSync, readFileSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { env, platform } from 'node:process' @@ -23,8 +25,11 @@ import { electronGetServerChannelConfig, } from '../../../../shared/eventa' import { createConfig } from '../../../libs/electron/persistence' +import { ensureServerChannelConfigDefaults } from './config' const channelServerConfigSchema = object({ + hostname: optional(string()), + authToken: optional(string()), tlsConfig: optional(nullable(object({ cert: optional(string()), key: optional(string()), @@ -33,11 +38,15 @@ const channelServerConfigSchema = object({ }) const channelServerInvokeConfigSchema = z.object({ + hostname: z.string().optional(), + authToken: z.string().optional(), tlsConfig: z.object({ }).nullable().optional(), }).strict() const channelServerConfigStore = createConfig('server-channel', 'config.json', channelServerConfigSchema, { default: { + hostname: '127.0.0.1', + authToken: '', tlsConfig: null, }, autoHeal: true, @@ -62,25 +71,41 @@ interface ServerChannelCertificateVerifyRequest { } } -async function getChannelServerConfig(): Promise { - return channelServerConfigStore.get() || { tlsConfig: null } +function getServerChannelPort() { + return env.SERVER_CHANNEL_PORT ? Number.parseInt(env.SERVER_CHANNEL_PORT) : 6121 +} + +async function getChannelServerConfig(): Promise { + const config = channelServerConfigStore.get() || { hostname: '127.0.0.1', authToken: '', tlsConfig: null } + + return { + hostname: config.hostname || '127.0.0.1', + authToken: config.authToken || '', + tlsConfig: config.tlsConfig || null, + } } function getServerRuntimeBaseOptions() { return { - port: env.PORT ? Number.parseInt(env.PORT) : 6121, - hostname: env.SERVER_RUNTIME_HOSTNAME || '0.0.0.0', + port: getServerChannelPort(), + hostname: '127.0.0.1', } } async function resolveServerRuntimeOptions(config: ServerOptions): Promise { return { ...getServerRuntimeBaseOptions(), + auth: { + token: 'authToken' in config && typeof config.authToken === 'string' ? config.authToken : '', + }, + hostname: 'hostname' in config && typeof config.hostname === 'string' + ? config.hostname || '127.0.0.1' + : '127.0.0.1', tlsConfig: config.tlsConfig ? await getOrCreateCertificate() : null, } } -async function normalizeChannelServerOptions(payload: unknown, fallback?: ServerOptions) { +async function normalizeChannelServerOptions(payload: unknown, fallback?: ElectronServerChannelConfig) { if (!fallback) { fallback = await getChannelServerConfig() } @@ -90,14 +115,18 @@ async function normalizeChannelServerOptions(payload: unknown, fallback?: Server return fallback } - return { + const normalizedConfig = { + hostname: parsed.data.hostname ?? fallback.hostname, + authToken: parsed.data.authToken ?? fallback.authToken, tlsConfig: typeof parsed.data.tlsConfig === 'undefined' ? null : parsed.data.tlsConfig, } + + return ensureServerChannelConfigDefaults(normalizedConfig, randomUUID).config } function getCertificateDomains(): string[] { const localIPs = getLocalIPs() - const hostname = env.SERVER_RUNTIME_HOSTNAME + const hostname = channelServerConfigStore.get()?.hostname || env.SERVER_RUNTIME_HOSTNAME return Array.from(new Set([ 'localhost', '127.0.0.1', @@ -283,11 +312,12 @@ export async function setupServerChannel(params: { lifecycle: Lifecycle }): Prom configureServerChannelCertificateTrust() const storedConfig = await getChannelServerConfig() + const { changed: storedConfigChanged, config: normalizedStoredConfig } = ensureServerChannelConfigDefaults(storedConfig, randomUUID) + if (storedConfigChanged) { + channelServerConfigStore.update(normalizedStoredConfig) + } - const serverChannel = createServer({ - ...storedConfig, - ...(await resolveServerRuntimeOptions(storedConfig)), - }) + const serverChannel = createServer(await resolveServerRuntimeOptions(normalizedStoredConfig)) const mutex = new Mutex() @@ -386,26 +416,28 @@ export async function createServerChannelService(params: { serverChannel: Server defineInvokeHandler(context, electronApplyServerChannelConfig, async (req) => { const current = await getChannelServerConfig() const next = await normalizeChannelServerOptions(req, current) - const changed = JSON.stringify(next.tlsConfig) !== JSON.stringify(current.tlsConfig) + const tlsChanged = JSON.stringify(next.tlsConfig) !== JSON.stringify(current.tlsConfig) + const hostnameChanged = next.hostname !== current.hostname + const authTokenChanged = next.authToken !== current.authToken + const runtimeChanged = tlsChanged || hostnameChanged || authTokenChanged try { - if (changed) { + if (runtimeChanged) { const nextRuntimeOptions = await resolveServerRuntimeOptions(next) await params.serverChannel.updateConfig(nextRuntimeOptions) await params.serverChannel.restart() - channelServerConfigStore.update(next) - - return next + } + else { + await params.serverChannel.start() } - await params.serverChannel.start() channelServerConfigStore.update(next) return next } catch (error) { useLogg('main/server-runtime').withError(error).error('Failed to apply server channel configuration') - if (changed) { + if (runtimeChanged) { const previousRuntimeOptions = await resolveServerRuntimeOptions(current) try { diff --git a/apps/stage-tamagotchi/src/renderer/App.vue b/apps/stage-tamagotchi/src/renderer/App.vue index 2993cb1d2..aec02a951 100644 --- a/apps/stage-tamagotchi/src/renderer/App.vue +++ b/apps/stage-tamagotchi/src/renderer/App.vue @@ -131,9 +131,14 @@ onMounted(async () => { await settingsAudioDeviceStore.initialize() const serverChannelConfig = await getServerChannelConfig() - serverChannelSettingsStore.websocketTlsConfig = serverChannelConfig.tlsConfig + serverChannelSettingsStore.tlsConfig = serverChannelConfig.tlsConfig ?? null + serverChannelSettingsStore.hostname = serverChannelConfig.hostname + serverChannelSettingsStore.authToken = serverChannelConfig.authToken - await serverChannelStore.initialize({ possibleEvents: ['ui:configure'] }).catch(err => console.error('Failed to initialize Mods Server Channel in App.vue:', err)) + await serverChannelStore.initialize({ + token: serverChannelConfig.authToken || undefined, + possibleEvents: ['ui:configure'], + }).catch(err => console.error('Failed to initialize Mods Server Channel in App.vue:', err)) if (!isChatWindowRoute()) { contextBridgeStore.initialize() characterOrchestratorStore.initialize() diff --git a/apps/stage-tamagotchi/src/renderer/pages/settings/connection/index.vue b/apps/stage-tamagotchi/src/renderer/pages/settings/connection/index.vue index 49e3e7a26..3ffddd6a8 100644 --- a/apps/stage-tamagotchi/src/renderer/pages/settings/connection/index.vue +++ b/apps/stage-tamagotchi/src/renderer/pages/settings/connection/index.vue @@ -1,23 +1,53 @@