import { log, error as logError } from '../../logger' import { Hono } from 'hono' import { createSession, getSession, updateSessionTitle, archiveSession, resolveExistingSessionId, } from '../../services/session' import { createWorkItem } from '../../services/work-dispatch' import { apiKeyAuth, acceptCliHeaders } from '../../auth/middleware' import { publishSessionEvent } from '../../services/transport' import { storeBindSession, storeGetEnvironmentOwnership } from '../../store' const app = new Hono() /** POST /v1/sessions — Create session. * Used by replBridge/ACP workers to auto-create their initial session. * Session ownership: user owner = env.owner_user_id, team owner = env.team_id * (auto-inherited). If env has no owner_user_id yet (claim flow pending), * returns 409 so the worker can retry after claim completes. */ app.post('/', acceptCliHeaders, apiKeyAuth, async c => { const body = await c.req.json() const username = c.get('userId') // Resolve env ownership to bind session_owners. let envOwnerUserId: string | null = null let envTeamId: string | null = null if (body.environment_id) { const ownership = storeGetEnvironmentOwnership(body.environment_id) if (ownership) { envOwnerUserId = ownership.ownerUserId envTeamId = ownership.teamId // Claim flow pending: env exists but has no user owner yet. Worker // should retry after claim completes (Q4 = option A). if (!ownership.ownerUserId) { return c.json( { error: { type: 'environment_unowned', message: 'Environment has no owner yet; claim it via the printed URL before creating sessions', }, }, 409, ) } } } const session = createSession({ ...body, username, visibility: envTeamId ? 'team' : 'private', }) // Write session_owners: team owner inherited from env, user owner = env owner. // This is the key fix for the "not your sessions" bug — previously /v1/sessions // only set sessions.username, never wrote session_owners, so the Web UI list // (which filters by session_owners) never showed these sessions. if (envTeamId) { storeBindSession(session.id, envTeamId, 'team') } if (envOwnerUserId) { storeBindSession(session.id, envOwnerUserId, 'user') } // Create work item if environment is specified if (body.environment_id) { try { await createWorkItem(body.environment_id, session.id) } catch (err) { logError(`[RCS] Failed to create work item: ${(err as Error).message}`) } } // Publish initial events if provided if (body.events && Array.isArray(body.events)) { for (const evt of body.events) { publishSessionEvent(session.id, evt.type || 'init', evt, 'outbound') } } return c.json(session, 200) }) /** GET /v1/sessions/:id — Get session */ app.get('/:id', acceptCliHeaders, apiKeyAuth, async c => { const sessionId = resolveExistingSessionId(c.req.param('id')!) ?? c.req.param('id')! const session = getSession(sessionId) if (!session) { return c.json( { error: { type: 'not_found', message: 'Session not found' } }, 404, ) } return c.json(session, 200) }) /** PATCH /v1/sessions/:id — Update session title */ app.patch('/:id', acceptCliHeaders, apiKeyAuth, async c => { const sessionId = resolveExistingSessionId(c.req.param('id')!) ?? c.req.param('id')! const existing = getSession(sessionId) if (!existing) { return c.json( { error: { type: 'not_found', message: 'Session not found' } }, 404, ) } const body = await c.req.json() if (body.title) { updateSessionTitle(sessionId, body.title) } const session = getSession(sessionId) return c.json(session, 200) }) /** POST /v1/sessions/:id/archive — Archive session */ app.post('/:id/archive', acceptCliHeaders, apiKeyAuth, async c => { const sessionId = resolveExistingSessionId(c.req.param('id')!) ?? c.req.param('id')! const session = getSession(sessionId) if (!session) { return c.json( { error: { type: 'not_found', message: 'Session not found' } }, 404, ) } try { archiveSession(sessionId) } catch { return c.json({ status: 'ok' }, 409) } return c.json({ status: 'ok' }, 200) }) /** POST /v1/sessions/:id/events — Send event to session */ app.post('/:id/events', acceptCliHeaders, apiKeyAuth, async c => { const sessionId = resolveExistingSessionId(c.req.param('id')!) ?? c.req.param('id')! const session = getSession(sessionId) if (!session) { return c.json( { error: { type: 'not_found', message: 'Session not found' } }, 404, ) } const body = await c.req.json() const events = body.events ? Array.isArray(body.events) ? body.events : [body.events] : Array.isArray(body) ? body : [body] const published = [] for (const evt of events) { const result = publishSessionEvent( sessionId, evt.type || 'message', evt, 'inbound', ) published.push(result) } return c.json({ status: 'ok', events: published.length }, 200) }) export default app