import { type SessionPreviewDescriptor, type SessionPreviewTarget } from '../core/session-preview.js'; /** Internal daemon rows carry the literal loopback target so the central * dashboard can proxy it. Browser rows never do: they receive only a * same-origin path plus a timestamp. * * Riff 矩阵:远端 sandbox 会话在 daemon 的 loopback 上不可能有 Web 服务,注册路由 * 已经拒绝它们;这里再拦一道,让任何来源(历史行、切后端前留下的行)都不会给 * 浏览器一个点进去必然失败的预览入口。 */ export declare function projectSessionPreviewForBrowser(session: unknown): unknown; export declare function projectSessionPreviewsForBrowser(sessions: unknown[]): unknown[]; /** Projection for daemon `GET /api/sessions/:id` envelopes. Keep this beside * the list/SSE projectors so every browser delivery path shares one rule. */ export declare function projectSessionDetailForBrowser(body: unknown): unknown; /** Apply the identical projection to the central SSE stream. A future clear * patch maps to `preview: null`, ensuring browser stores discard stale state. */ export declare function projectSessionPreviewEventForBrowser(type: string, body: unknown): unknown; export type SessionPreviewResolution = { ok: true; target: SessionPreviewTarget; } | { ok: false; status: 403 | 404 | 409 | 501; error: 'unknown_session' | 'session_owner_mismatch' | 'session_not_active' | 'preview_not_registered' | 'remote_host_forbidden' | 'invalid_preview_target' /** Riff 等远端 backend:daemon loopback 天然不可达,明确不支持而非「不可达」。 */ | 'preview_unsupported' /** P1-12:端口已经不由注册时那个进程持有,路由失效。 */ | 'preview_target_stale'; }; /** Resolve an aggregator row under positive session + daemon ownership proof. * The requested URL never supplies a host or port; both come solely from this * exact owned row and are revalidated as literal loopback data. */ export declare function resolveSessionPreviewFromRow(input: { row: unknown; sessionId: string; ownerLarkAppId: string | undefined; }): SessionPreviewResolution; /** * 中央 Dashboard 每一跳预览请求的完整判定。顺序即安全次序,任何一步失败都不代理: * * 会话归属 → 会话仍活着 → backend 支持预览 → 目标本身合法 → 拥有它的 daemon 在线 * → **端口此刻仍由注册时那个进程持有**(P1-12)。 * * 最后一条是每次落地前都要重跑的:注册时的一次 TCP connect 只证明当时有人在监听, * 而 dev server 退出后端口号会被内核回收,下一个抢到它的本机进程就继承了这条已登录 * 用户可达的同源代理路由。判定失效时同步回 `preview_target_stale`,并通过 * `onStaleTarget` 触发收口(断掉既有预览长连接、收回交互租约、让 daemon 清 target 并 * 广播 `preview: null`)。 */ export declare function resolveSessionPreviewForProxy(input: { row: unknown; sessionId: string; ownerLarkAppId: string | undefined; daemonOnline: boolean; isTargetOwned: (target: SessionPreviewTarget) => boolean; /** P1-3:把判定失效的**那一个** target 一起交出去,收口方据此只作废这一次注册 * (见 `takeSessionPreviewTarget` 的 `expectedRegisteredAt`)。 */ onStaleTarget?: (sessionId: string, staleTarget: SessionPreviewTarget) => void; }): SessionPreviewResolution | { ok: false; status: 503; error: 'daemon_offline'; }; export declare function previewDescriptorFromRow(row: unknown): SessionPreviewDescriptor | undefined; /** * P1-1:daemon 生命周期事件 → 「这个会话的预览长连接要不要收口」。 * * 原来只认两件事:`session.exited`,以及带 `previewTarget` 的 `session.update`。 * 漏掉的是 `session.spawned`——daemon 的 `/api/events` **每次**被连上都会把全部会话 * 重放成 spawned(SSE attach 的确定性重放),而 daemon 重启期间广播的 * `preview: null` 是丢的(事件总线没有 replay buffer),重连之后只以 spawned 形式 * 补齐。于是「目标已经没了,浏览器那条流还在流」这一支完全不在收口范围内。 * * 但 spawned 不能无条件收口:aggregator 在通知监听器**之前**就把整行覆盖掉了, * 每次 SSE 重连都会重放全部会话,无条件 teardown 等于每次重连误杀全部预览长连接。 * 所以 dashboard 侧自记一份「上次见过的目标指纹」,只有指纹**确实变了**才收口; * 第一次见到某个会话不算变(那是重放/新建,不是换靶)。 * * `lastSeenFingerprints` 由调用方持有并在此原地更新——收口判据和记忆必须是同一个 * 动作,分开写迟早会漏更新一条分支。 */ export declare function previewTeardownForDaemonEvent(event: { type: string; body: unknown; }, lastSeenFingerprints: Map): string | undefined; //# sourceMappingURL=preview-contract.d.ts.map