import * as Effect from "effect/Effect"; import type { Agent, Connection, ConnectionContext } from "agents"; import { loadCustomerConfig, loadCustomerSecrets, } from "../configuration/customer.ts"; import type { Env } from "./personal-agent"; import { verifyOwnerSession } from "./session"; export interface SessionConnection { expiresAt: number; expirySchedule: string; } export interface SessionExpiry { connectionId: string; expiresAt: number; } type SessionAgent = Pick, "cancelSchedule" | "getConnection"> & { schedule( when: Date, callback: "expireSession", payload: SessionExpiry, ): Promise<{ id: string }>; }; // Worker ingress authorizes before any native protocol output. These hooks bound // the accepted socket's lifetime using attachments and schedules across wakes. export function connectSession( agent: SessionAgent, env: Env, connection: Connection, context: ConnectionContext, ) { return Effect.gen(function* () { const { effectiveInstallation } = loadCustomerConfig(env); const { sessionSecret } = loadCustomerSecrets(env); const session = yield* verifyOwnerSession( context.request, sessionSecret, effectiveInstallation, ); if (!session) { connection.close(4001, "Authentication required"); return; } const expiry = yield* Effect.promise(() => agent.schedule(new Date(session.expiresAt * 1000), "expireSession", { connectionId: connection.id, expiresAt: session.expiresAt, }), ); connection.setState({ expiresAt: session.expiresAt, expirySchedule: expiry.id, }); }); } export function closeSession( agent: SessionAgent, connection: Connection, ) { return Effect.gen(function* () { const expiry = connection.state?.expirySchedule; if (expiry) yield* Effect.promise(() => agent.cancelSchedule(expiry)); }); } export function expireSession(agent: SessionAgent, payload: SessionExpiry) { const connection = agent.getConnection( payload.connectionId, ); if (connection?.state?.expiresAt === payload.expiresAt) connection.close(4001, "Session expired"); }