(ctx: EngineContext)
| 92 | ) {} |
| 93 | |
| 94 | async start(ctx: EngineContext) { |
| 95 | // ==================== Auth bootstrap ==================== |
| 96 | // Generate the admin token on first run; subsequent boots no-op. |
| 97 | // We do this BEFORE any route mounts so the public-mode safety |
| 98 | // check below has a meaningful auth-file state to read. |
| 99 | const { bootstrapToken, getTokenInfo } = await import('@/services/auth/index.js') |
| 100 | await bootstrapToken({ |
| 101 | onFirstGeneration: (token) => { |
| 102 | console.log('') |
| 103 | console.log('═══════════════════════════════════════════════════════════════') |
| 104 | console.log(' First-run admin token (save this — won\'t be shown again):') |
| 105 | console.log('') |
| 106 | console.log(` ${token}`) |
| 107 | console.log('') |
| 108 | console.log(' To rotate: delete data/config/auth.json and restart.') |
| 109 | console.log('═══════════════════════════════════════════════════════════════') |
| 110 | console.log('') |
| 111 | }, |
| 112 | }) |
| 113 | |
| 114 | // ==================== Public-mode safety net ==================== |
| 115 | // Refuse to start if Alice is bound to a non-localhost interface |
| 116 | // without an admin token configured. Prevents the "I set |
| 117 | // OPENALICE_BIND_HOST=0.0.0.0 for testing and forgot auth" footgun. |
| 118 | const bindHost = (process.env['OPENALICE_BIND_HOST'] ?? '127.0.0.1').trim() |
| 119 | const bindIsPublic = bindHost !== '127.0.0.1' && bindHost !== '::1' && bindHost !== 'localhost' |
| 120 | if (bindIsPublic) { |
| 121 | const tokenInfo = await getTokenInfo() |
| 122 | if (!tokenInfo.exists && process.env['OPENALICE_DISABLE_AUTH'] !== '1') { |
| 123 | throw new Error( |
| 124 | `Refusing to start: OPENALICE_BIND_HOST="${bindHost}" exposes Alice ` + |
| 125 | `to non-localhost callers, but no admin token has been provisioned. ` + |
| 126 | `Start once with OPENALICE_BIND_HOST=127.0.0.1 to generate the token, ` + |
| 127 | `then re-set the bind. Set OPENALICE_DISABLE_AUTH=1 only when you ` + |
| 128 | `understand the implication (no protection at the Alice boundary).` |
| 129 | ) |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | // Load sub-channel definitions |
| 134 | const subChannels = await readWebSubchannels() |
| 135 | |
| 136 | // Initialize sessions for the default channel and all sub-channels |
| 137 | const sessions = new Map<string, SessionStore>() |
| 138 | |
| 139 | const defaultSession = new SessionStore('web/default') |
| 140 | await defaultSession.restore() |
| 141 | sessions.set('default', defaultSession) |
| 142 | |
| 143 | for (const ch of subChannels) { |
| 144 | const session = new SessionStore(`web/${ch.id}`) |
| 145 | await session.restore() |
| 146 | sessions.set(ch.id, session) |
| 147 | } |
| 148 | |
| 149 | // Initialize SSE map for known channels (entries are created lazily too) |
| 150 | this.sseByChannel.set('default', new Map()) |
| 151 | for (const ch of subChannels) { |
nothing calls this directly
no test coverage detected