diff --git a/packages/devframe/src/adapters/dev.ts b/packages/devframe/src/adapters/dev.ts index dd4513ba..92b497c5 100644 --- a/packages/devframe/src/adapters/dev.ts +++ b/packages/devframe/src/adapters/dev.ts @@ -1,4 +1,4 @@ -import type { Peer } from 'crossws' +import type { DevframeRpcConnection } from 'devframe/rpc/transports/ws-server' import type { DevframeAuthHandler } from '../node/auth/handler' import type { StartedServer } from '../node/instance-shell' import type { DevframeDefinition, DevframeWsOptions, McpRouteOptions } from '../types/devframe' @@ -82,16 +82,16 @@ export interface CreateDevServerOptions { */ mcp?: boolean | McpRouteOptions /** - * Called once per new WS connection, right after its session is created. - * Forwarded verbatim to the underlying WS transport binding. + * Called once per new RPC connection, right after its session is created. + * Forwarded verbatim to the underlying transport binding. */ - onPeerConnect?: (peer: Peer, session: DevframeNodeRpcSession) => void + onPeerConnect?: (connection: DevframeRpcConnection, session: DevframeNodeRpcSession) => void /** - * Called once per closed WS connection, right after its session's + * Called once per closed RPC connection, right after its session's * disconnect bookkeeping runs. Forwarded verbatim to the underlying - * the underlying WS transport binding. + * transport binding. */ - onPeerDisconnect?: (peer: Peer, meta: DevframeNodeRpcSessionMeta) => void + onPeerDisconnect?: (connection: DevframeRpcConnection, meta: DevframeNodeRpcSessionMeta) => void /** * Called once the WS server is bound. Devframe stays headless * otherwise — wire this if you want a startup banner. diff --git a/packages/devframe/src/adapters/initiate.ts b/packages/devframe/src/adapters/initiate.ts index cf104172..d39b132c 100644 --- a/packages/devframe/src/adapters/initiate.ts +++ b/packages/devframe/src/adapters/initiate.ts @@ -1,5 +1,4 @@ -import type { Peer } from 'crossws' -import type { WsOriginRegistry } from 'devframe/rpc/transports/ws-server' +import type { DevframeRpcConnection, WsOriginRegistry } from 'devframe/rpc/transports/ws-server' import type { ConnectionMeta, DevframeNodeContext, DevframeNodeRpcSession, DevframeNodeRpcSessionMeta, DevframeStorageScope } from 'devframe/types' import type { Buffer } from 'node:buffer' import type { IncomingMessage, Server as NodeHttpServer, ServerResponse } from 'node:http' @@ -129,15 +128,15 @@ export interface InitDevframeOptions { */ destroyUnmatchedUpgrades?: boolean /** - * Called once per new WS connection, right after its session is created. + * Called once per new RPC connection, right after its session is created. * Forwarded verbatim to the underlying transport. */ - onPeerConnect?: (peer: Peer, session: DevframeNodeRpcSession) => void + onPeerConnect?: (connection: DevframeRpcConnection, session: DevframeNodeRpcSession) => void /** - * Called once per closed WS connection, right after the transport's own + * Called once per closed RPC connection, right after the transport's own * disconnect bookkeeping runs. */ - onPeerDisconnect?: (peer: Peer, meta: DevframeNodeRpcSessionMeta) => void + onPeerDisconnect?: (connection: DevframeRpcConnection, meta: DevframeNodeRpcSessionMeta) => void } export interface DevframeInstance { diff --git a/packages/devframe/src/node/auth/handler.ts b/packages/devframe/src/node/auth/handler.ts index b2b65ae8..679ef3ba 100644 --- a/packages/devframe/src/node/auth/handler.ts +++ b/packages/devframe/src/node/auth/handler.ts @@ -1,5 +1,5 @@ -import type { Peer } from 'crossws' import type { RpcFunctionDefinitionAny } from 'devframe/rpc' +import type { DevframeRpcConnection } from 'devframe/rpc/transports/ws-server' import type { DevframeNodeRpcSession } from 'devframe/types' /** @@ -28,12 +28,12 @@ export interface DevframeAuthHandler { */ authorize: (methodName: string, session: DevframeNodeRpcSession) => boolean /** - * Connect-time trust: reads a bearer token off the peer's upgrade request - * (an `Authorization: Bearer ` header, or a static/pre-shared - * token from `clientAuthTokens`) and, when valid, marks the session + * Connect-time trust: reads a bearer token off the connection's initial + * request (a static/pre-shared token from `clientAuthTokens`, or a token + * minted by the code exchange) and, when valid, marks the session * trusted immediately — before the client's own handshake call. */ - onConnect: (peer: Peer, session: DevframeNodeRpcSession) => void + onConnect: (connection: DevframeRpcConnection, session: DevframeNodeRpcSession) => void /** * Print the current one-time code and its magic-link URL. Devframe stays * headless — call this yourself once the server is listening. Safe to diff --git a/packages/devframe/src/node/instance-shell.ts b/packages/devframe/src/node/instance-shell.ts index 6b125eb4..0e0cb98d 100644 --- a/packages/devframe/src/node/instance-shell.ts +++ b/packages/devframe/src/node/instance-shell.ts @@ -1,7 +1,6 @@ import type { BirpcGroup } from 'birpc' -import type { Peer } from 'crossws' import type { NodeAdapter } from 'crossws/adapters/node' -import type { WsOriginRegistry, WsRpcTransport } from 'devframe/rpc/transports/ws-server' +import type { DevframeRpcConnection, WsOriginRegistry, WsRpcTransport } from 'devframe/rpc/transports/ws-server' import type { H3 } from 'h3' import type { Buffer } from 'node:buffer' import type { IncomingMessage, Server as NodeHttpServer, ServerResponse } from 'node:http' @@ -56,8 +55,8 @@ interface BindHttpAndWsOptions { auth?: boolean | DevframeAuthHandler allowedOrigins?: readonly string[] | WsOriginRegistry | false destroyUnmatched?: boolean - onPeerConnect?: (peer: Peer, session: DevframeNodeRpcSession) => void - onPeerDisconnect?: (peer: Peer, meta: DevframeNodeRpcSessionMeta) => void + onPeerConnect?: (connection: DevframeRpcConnection, session: DevframeNodeRpcSession) => void + onPeerDisconnect?: (connection: DevframeRpcConnection, meta: DevframeNodeRpcSessionMeta) => void } /** @@ -212,8 +211,8 @@ export interface CreateInstanceShellOptions void - onPeerDisconnect?: (peer: Peer, meta: DevframeNodeRpcSessionMeta) => void + onPeerConnect?: (connection: DevframeRpcConnection, session: DevframeNodeRpcSession) => void + onPeerDisconnect?: (connection: DevframeRpcConnection, meta: DevframeNodeRpcSessionMeta) => void /** * Advertise the WS route as a base-absolute path (`__ws`) instead of * the base-relative default. A hub serves one meta document from several diff --git a/packages/devframe/src/node/rpc-core.ts b/packages/devframe/src/node/rpc-core.ts index 4b3ebb33..66002f72 100644 --- a/packages/devframe/src/node/rpc-core.ts +++ b/packages/devframe/src/node/rpc-core.ts @@ -1,5 +1,5 @@ import type { BirpcGroup, EventOptions } from 'birpc' -import type { Peer } from 'crossws' +import type { DevframeRpcConnection } from 'devframe/rpc/transports/ws-server' import type { DevframeNodeContext, DevframeNodeRpcSession, DevframeNodeRpcSessionMeta, DevframeRpcClientFunctions, DevframeRpcServerFunctions } from 'devframe/types' import type { DevframeAuthHandler } from './auth' import type { RpcFunctionsHostImpl } from './host-functions' @@ -16,10 +16,10 @@ export interface CreateContextRpcServerOptions { auth?: boolean | DevframeAuthHandler /** Lower-level per-call gate by method name and session, without a full handler. */ authorize?: (methodName: string, session: DevframeNodeRpcSession) => boolean - /** Called once per new WS connection, right after its session is created. */ - onPeerConnect?: (peer: Peer, session: DevframeNodeRpcSession) => void - /** Called once per closed WS connection, after the transport's disconnect bookkeeping. */ - onPeerDisconnect?: (peer: Peer, meta: DevframeNodeRpcSessionMeta) => void + /** Called once per new RPC connection, right after its session is created. */ + onPeerConnect?: (connection: DevframeRpcConnection, session: DevframeNodeRpcSession) => void + /** Called once per closed RPC connection, after the transport's disconnect bookkeeping. */ + onPeerDisconnect?: (connection: DevframeRpcConnection, meta: DevframeNodeRpcSessionMeta) => void /** Forwarded verbatim to birpc's `rpcOptions` so a host keeps seeing RPC failures. */ rpcOptions?: Pick< EventOptions, @@ -32,12 +32,12 @@ export interface ContextRpcServer { /** The resolved auth handler when `auth` was passed as one. */ authHandler?: DevframeAuthHandler /** - * Peer lifecycle handlers to wire into a WS transport + * Connection lifecycle handlers to wire into a transport binding * (`attachWsRpcTransport`'s `onConnected` / `onDisconnected`, or any other * crossws adapter's peer hooks via `createWsRpcPeerHooks`). */ - onConnected?: (peer: Peer, meta: DevframeNodeRpcSessionMeta) => void - onDisconnected: (peer: Peer, meta: DevframeNodeRpcSessionMeta) => void + onConnected?: (connection: DevframeRpcConnection, meta: DevframeNodeRpcSessionMeta) => void + onDisconnected: (connection: DevframeRpcConnection, meta: DevframeNodeRpcSessionMeta) => void } /** @@ -131,18 +131,18 @@ export function createContextRpcServer(options: CreateContextRpcServerOptions): } const onConnected = (authHandler || options.onPeerConnect) - ? (peer: Peer, meta: DevframeNodeRpcSessionMeta) => { + ? (connection: DevframeRpcConnection, meta: DevframeNodeRpcSessionMeta) => { const session: DevframeNodeRpcSession = { meta, rpc: rpcGroup.clients.find(client => (client as any).$meta === meta) as any, } - authHandler?.onConnect(peer, session) - options.onPeerConnect?.(peer, session) + authHandler?.onConnect(connection, session) + options.onPeerConnect?.(connection, session) } : undefined - const onDisconnected = (peer: Peer, meta: DevframeNodeRpcSessionMeta): void => { - options.onPeerDisconnect?.(peer, meta) + const onDisconnected = (connection: DevframeRpcConnection, meta: DevframeNodeRpcSessionMeta): void => { + options.onPeerDisconnect?.(connection, meta) rpcHost._emitSessionDisconnected(meta) } diff --git a/packages/devframe/src/recipes/interactive-auth.ts b/packages/devframe/src/recipes/interactive-auth.ts index c9bfc4f9..1d214185 100644 --- a/packages/devframe/src/recipes/interactive-auth.ts +++ b/packages/devframe/src/recipes/interactive-auth.ts @@ -150,18 +150,18 @@ export function createInteractiveAuth( } function onConnect( - peer: { request?: { url?: string, headers?: { get?: (name: string) => string | null } } }, + connection: { request?: { url?: string, headers?: { get?: (name: string) => string | null | undefined } } }, session: DevframeNodeRpcSession, ): void { let token: string | undefined let requestOrigin: string | undefined try { - const url = new URL(peer.request?.url ?? '', 'http://localhost') + const url = new URL(connection.request?.url ?? '', 'http://localhost') token = url.searchParams.get(DEVFRAME_AUTH_TOKEN_QUERY_PARAM) ?? undefined } catch {} try { - requestOrigin = peer.request?.headers?.get?.('origin') ?? undefined + requestOrigin = connection.request?.headers?.get?.('origin') ?? undefined } catch {} if (!token) diff --git a/packages/devframe/src/rpc/transports/session.ts b/packages/devframe/src/rpc/transports/session.ts new file mode 100644 index 00000000..59111807 --- /dev/null +++ b/packages/devframe/src/rpc/transports/session.ts @@ -0,0 +1,84 @@ +import type { Peer } from 'crossws' + +/** + * Which wire transport produced an RPC connection. Every transport speaks + * the same birpc channel protocol; the kind only matters to code that needs + * transport-specific behavior (e.g. reaching the WS escape hatch on + * {@link DevframeRpcConnection.peer}). + */ +export type DevframeRpcTransportKind = 'websocket' | 'sse' + +/** + * Structural view of the connect-time HTTP request behind an RPC connection + * — the WS upgrade request, or the request opening an SSE stream. Shaped to + * match both the web `Request` a crossws peer exposes and a plain + * `node:http` request wrapper, so auth hooks can read the bearer-token + * query param and the `Origin` header without caring which transport (or + * runtime) produced the connection. + */ +export interface DevframeRpcConnectionRequest { + /** Request URL (may be path-only, e.g. `/__ws?devframe_auth_token=…`). */ + url?: string + /** Header lookup, `Headers`-style. */ + headers?: { get: (name: string) => string | null | undefined } +} + +/** + * A live RPC connection, independent of the transport that carries it. One + * exists per connected client; transport bindings construct it alongside the + * session meta and hand both to the connect/disconnect hooks + * (`onPeerConnect` / `onPeerDisconnect`, {@link DevframeAuthHandler.onConnect}). + */ +export interface DevframeRpcConnection { + /** Session id — the same value as the session meta's `id`. */ + id: number + /** The transport carrying this connection. */ + transport: DevframeRpcTransportKind + /** The connect-time HTTP request (upgrade request / stream request). */ + request?: DevframeRpcConnectionRequest + /** Send a raw wire frame to this client. Prefer the birpc channel. */ + send?: (data: string) => void + /** Terminate the connection from the server side. */ + close?: (code?: number, reason?: string) => void + /** + * The crossws peer backing a `websocket` connection — the WS-specific + * escape hatch (pub/sub, raw socket access). Absent on other transports. + */ + peer?: Peer +} + +export interface DevframeNodeRpcSessionMeta { + id: number + /** The crossws peer backing this session's socket (WS transport only). */ + peer?: Peer + clientAuthToken?: string + isTrusted?: boolean + subscribedStates: Set + /** + * Streams this session has subscribed to via + * `rpc.streaming.subscribe(channel, id)`. Tracked here for O(1) cleanup + * on disconnect; the wire format is `${channel}\x1F${id}`. + */ + subscribedStreams?: Set + /** + * Inbound streams this session is currently uploading to (via + * `rpc.streaming.upload(channel, id)`). Tracked for cleanup on + * disconnect; same wire format as `subscribedStreams`. + */ + uploadingStreams?: Set +} + +let sessionId = 0 + +/** + * Mint the per-connection session meta every transport binding shares — + * one id space across transports, so session bookkeeping (streaming + * subscriptions, shared-state sync, auth trust) never collides between a + * WS peer and an SSE session on the same server. + */ +export function createRpcSessionMeta(): DevframeNodeRpcSessionMeta { + return { + id: sessionId++, + subscribedStates: new Set(), + } +} diff --git a/packages/devframe/src/rpc/transports/ws-server.ts b/packages/devframe/src/rpc/transports/ws-server.ts index b11a6fda..4dfe91fa 100644 --- a/packages/devframe/src/rpc/transports/ws-server.ts +++ b/packages/devframe/src/rpc/transports/ws-server.ts @@ -7,6 +7,7 @@ import type { Server as HttpsServer, ServerOptions as HttpsServerOptions } from import type { AddressInfo } from 'node:net' import type { Duplex } from 'node:stream' import type { RpcFunctionDefinitionAny } from '../types' +import type { DevframeNodeRpcSessionMeta, DevframeRpcConnection } from './session' import { createServer as createHttpServer } from 'node:http' import { createServer as createHttpsServer } from 'node:https' import crossws from 'crossws/adapters/node' @@ -14,27 +15,14 @@ import { DEVFRAME_VIEWER_ORIGIN_QUERY_PARAM, DEVFRAME_VIEWER_ORIGIN_TOKEN_QUERY_ import { randomToken, timingSafeEqual } from 'devframe/utils/crypto-token' import { structuredCloneParse, structuredCloneStringify } from 'devframe/utils/structured-clone' import { strictJsonStringify, STRUCTURED_CLONE_PREFIX } from '../serialization' +import { createRpcSessionMeta } from './session' -export interface DevframeNodeRpcSessionMeta { - id: number - /** The crossws peer backing this session's socket. */ - peer?: Peer - clientAuthToken?: string - isTrusted?: boolean - subscribedStates: Set - /** - * Streams this session has subscribed to via - * `rpc.streaming.subscribe(channel, id)`. Tracked here for O(1) cleanup - * on disconnect; the wire format is `${channel}\x1F${id}`. - */ - subscribedStreams?: Set - /** - * Inbound streams this session is currently uploading to (via - * `rpc.streaming.upload(channel, id)`). Tracked for cleanup on - * disconnect; same wire format as `subscribedStreams`. - */ - uploadingStreams?: Set -} +export type { + DevframeNodeRpcSessionMeta, + DevframeRpcConnection, + DevframeRpcConnectionRequest, + DevframeRpcTransportKind, +} from './session' export interface WsRpcTransportOptions { /** @@ -94,8 +82,8 @@ export interface WsRpcTransportOptions { * loses dev-time validation for `jsonSerializable: true` declarations. */ definitions?: ReadonlyMap> - onConnected?: (peer: Peer, meta: DevframeNodeRpcSessionMeta) => void - onDisconnected?: (peer: Peer, meta: DevframeNodeRpcSessionMeta) => void + onConnected?: (connection: DevframeRpcConnection, meta: DevframeNodeRpcSessionMeta) => void + onDisconnected?: (connection: DevframeRpcConnection, meta: DevframeNodeRpcSessionMeta) => void /** Override the default per-call serializer. Most callers should leave this unset. */ serialize?: ChannelOptions['serialize'] /** Override the default per-call deserializer. Most callers should leave this unset. */ @@ -208,8 +196,6 @@ export interface WsRpcTransport { close: () => Promise } -let sessionId = 0 - const EMPTY_DEFS: ReadonlyMap> = new Map() function NOOP() {} @@ -342,6 +328,7 @@ export function createWsRpcPeerHooks< interface PeerState { meta: DevframeNodeRpcSessionMeta + connection: DevframeRpcConnection channel: ChannelOptions /** birpc's inbound-message handler, registered via the channel's `on`. */ onMessage?: (data: string) => void @@ -350,10 +337,15 @@ export function createWsRpcPeerHooks< return { open: (peer) => { - const meta: DevframeNodeRpcSessionMeta = { - id: sessionId++, + const meta = createRpcSessionMeta() + meta.peer = peer + const connection: DevframeRpcConnection = { + id: meta.id, + transport: 'websocket', + request: peer.request, + send: data => peer.send(data), + close: (code, reason) => peer.close(code, reason), peer, - subscribedStates: new Set(), } // Per-connection state: maps an incoming request id to its method @@ -361,7 +353,7 @@ export function createWsRpcPeerHooks< // up in `definitions` and pick the right encoder. One map per // session — request-id spaces don't collide across sessions. const pendingRequestMethods = new Map() - const state: PeerState = { meta, channel: undefined as unknown as ChannelOptions } + const state: PeerState = { meta, connection, channel: undefined as unknown as ChannelOptions } const channel: ChannelOptions = { post: (data) => { peer.send(data) @@ -404,7 +396,7 @@ export function createWsRpcPeerHooks< rpcGroup.updateChannels((channels) => { channels.push(channel) }) - onConnected(peer, meta) + onConnected(connection, meta) }, message: (peer, message) => { states.get(peer)?.onMessage?.(message.text()) @@ -419,7 +411,7 @@ export function createWsRpcPeerHooks< if (index >= 0) channels.splice(index, 1) }) - onDisconnected(peer, state.meta) + onDisconnected(state.connection, state.meta) }, } } diff --git a/packages/devframe/src/types/context.ts b/packages/devframe/src/types/context.ts index d49b304f..6ce1af13 100644 --- a/packages/devframe/src/types/context.ts +++ b/packages/devframe/src/types/context.ts @@ -103,8 +103,33 @@ export interface ConnectionMetaWebsocket { host?: string } +/** + * Object form of {@link ConnectionMeta.sse} — the same proxy-safe shape and + * resolution rules as {@link ConnectionMetaWebsocket}, producing an + * `http(s)://` endpoint instead of a `ws(s)://` one. Set `port` (and/or + * `host`) only when the SSE endpoint genuinely lives on a different origin + * than the page, e.g. a side-car server on its own port. + */ +export interface ConnectionMetaSse { + /** + * Path to the SSE endpoint. Relative paths (the default, e.g. `__sse`) + * are resolved against `__connection.json`'s location; absolute paths + * (`/__sse`) resolve against the page origin. + */ + path?: string + /** Override the port. Combined with the page hostname unless `host` is set. */ + port?: number + /** Override the host (`hostname[:port]`). Use for a fully cross-origin endpoint. */ + host?: string +} + export interface ConnectionMeta { - backend: 'websocket' | 'static' + /** + * The server's primary live-RPC transport (`websocket` / `sse`), `static` + * for a pre-computed RPC dump with no live server, or `none` for a server + * exposing no RPC transport at all (e.g. an MCP-only deployment). + */ + backend: 'websocket' | 'sse' | 'static' | 'none' /** * WebSocket endpoint, resolved by the client into a `ws(s)://` URL: * @@ -115,6 +140,17 @@ export interface ConnectionMeta { * URL with its protocol swapped, or a path resolved same-origin. */ websocket?: number | string | ConnectionMetaWebsocket + /** + * SSE endpoint (`GET` opens the event stream, `POST` carries RPC calls), + * resolved by the client into an `http(s)://` URL with the same + * proxy-safe rules as {@link ConnectionMeta.websocket}: + * + * - {@link ConnectionMetaSse} — the proxy-flexible default; a + * same-origin path relative to `__connection.json`. + * - `string` — a full `http(s)://` URL used verbatim, or a path + * resolved same-origin. + */ + sse?: string | ConnectionMetaSse /** * Present when the dev server exposes a route-based MCP endpoint * (`cli.mcp`). Advertises the MCP Streamable-HTTP route so in-browser diff --git a/packages/devframe/src/types/rpc.ts b/packages/devframe/src/types/rpc.ts index 77146768..1a1e24e7 100644 --- a/packages/devframe/src/types/rpc.ts +++ b/packages/devframe/src/types/rpc.ts @@ -6,7 +6,12 @@ import type { StreamReader, StreamSink } from 'devframe/utils/streaming-channel' import type { DevframeNodeContext } from './context' import type { DevframeRpcClientFunctions, DevframeRpcServerFunctions } from './rpc-augments' -export type { DevframeNodeRpcSessionMeta } +export type { + DevframeNodeRpcSessionMeta, + DevframeRpcConnection, + DevframeRpcConnectionRequest, + DevframeRpcTransportKind, +} from 'devframe/rpc/transports/ws-server' export interface DevframeNodeRpcSession { meta: DevframeNodeRpcSessionMeta diff --git a/plugins/code-server/src/client/index.ts b/plugins/code-server/src/client/index.ts index b0b0974c..4bc2392e 100644 --- a/plugins/code-server/src/client/index.ts +++ b/plugins/code-server/src/client/index.ts @@ -1,8 +1,9 @@ import type { DevframeConnectionStatus, DevframeRpcClient, DevframeRpcClientOptions } from 'devframe/client' +import type { ConnectionMeta } from 'devframe/types' import { connectDevframe } from 'devframe/client' export { STATE_KEY } from '../constants' -export type { DevframeConnectionStatus, DevframeRpcClient } +export type { ConnectionMeta, DevframeConnectionStatus, DevframeRpcClient } export type { CodeServerBackend, CodeServerConnect, diff --git a/plugins/code-server/src/spa/composables/rpc.ts b/plugins/code-server/src/spa/composables/rpc.ts index 3a10674a..86c01896 100644 --- a/plugins/code-server/src/spa/composables/rpc.ts +++ b/plugins/code-server/src/spa/composables/rpc.ts @@ -1,4 +1,4 @@ -import type { DevframeConnectionStatus, DevframeRpcClient } from '@devframes/plugin-code-server/client' +import type { ConnectionMeta, DevframeConnectionStatus, DevframeRpcClient } from '@devframes/plugin-code-server/client' import { connectCodeServer } from '@devframes/plugin-code-server/client' import { reactive, shallowRef } from 'vue' @@ -6,7 +6,7 @@ export const connection = reactive<{ connected: boolean status: DevframeConnectionStatus error: string | null - backend: 'websocket' | 'static' | null + backend: ConnectionMeta['backend'] | null }>({ connected: false, status: 'connecting', diff --git a/tests/__snapshots__/tsnapi/@devframes/plugin-code-server/client.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/plugin-code-server/client.snapshot.d.ts index e1b5d1e9..d8c8c538 100644 --- a/tests/__snapshots__/tsnapi/@devframes/plugin-code-server/client.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/@devframes/plugin-code-server/client.snapshot.d.ts @@ -16,6 +16,7 @@ export { CodeServerSharedState } export { CodeServerStartResult } export { CodeServerStatus } export { CodeServerStatusResult } +export { ConnectionMeta } export { DevframeConnectionStatus } export { DevframeRpcClient } export { STATE_KEY } diff --git a/tests/__snapshots__/tsnapi/devframe/adapters/dev.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/adapters/dev.snapshot.d.ts index 88043ce8..7083242d 100644 --- a/tests/__snapshots__/tsnapi/devframe/adapters/dev.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/adapters/dev.snapshot.d.ts @@ -13,8 +13,8 @@ export interface CreateDevServerOptions { openBrowser?: boolean | string; auth?: boolean | DevframeAuthHandler; mcp?: boolean | McpRouteOptions; - onPeerConnect?: (_: Peer, _: DevframeNodeRpcSession) => void; - onPeerDisconnect?: (_: Peer, _: DevframeNodeRpcSessionMeta) => void; + onPeerConnect?: (_: DevframeRpcConnection, _: DevframeNodeRpcSession) => void; + onPeerDisconnect?: (_: DevframeRpcConnection, _: DevframeNodeRpcSessionMeta) => void; onReady?: (_: { origin: string; port: number; diff --git a/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts index 858397e8..99663fbc 100644 --- a/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts @@ -64,8 +64,9 @@ export interface AgentToolProviderHandle extends AgentHandle { notifyChanged: () => void; } export interface ConnectionMeta { - backend: 'websocket' | 'static'; + backend: 'websocket' | 'sse' | 'static' | 'none'; websocket?: number | string | ConnectionMetaWebsocket; + sse?: string | ConnectionMetaSse; mcp?: { path: string; port?: number; @@ -75,6 +76,11 @@ export interface ConnectionMeta { authToken?: string; viewerOriginToken?: string; } +export interface ConnectionMetaSse { + path?: string; + port?: number; + host?: string; +} export interface ConnectionMetaWebsocket { path?: string; port?: number; @@ -213,6 +219,20 @@ export interface DevframeRpcClientFunctions { 'devframe:rpc:client-state:updated': (_: string, _: any, _: string) => Promise; 'devframe:rpc:client-state:patch': (_: string, _: any[], _: string) => Promise; } +export interface DevframeRpcConnection { + id: number; + transport: DevframeRpcTransportKind; + request?: DevframeRpcConnectionRequest; + send?: (_: string) => void; + close?: (_?: number, _?: string) => void; + peer?: Peer; +} +export interface DevframeRpcConnectionRequest { + url?: string; + headers?: { + get: (_: string) => string | null | undefined; + }; +} export interface DevframeRpcServerFunctions { 'anonymous:devframe:auth': (_: { authToken: string; @@ -402,6 +422,7 @@ export type DevframeDeploymentKind = 'standalone' | 'hosted'; export type DevframeDiagnosticsDefinition = ReturnType>; export type DevframeDiagnosticsLogger = Record; export type DevframeDuplicationStrategy = 'warn' | 'silent' | 'throw' | 'duplicate'; +export type DevframeRpcTransportKind = 'websocket' | 'sse'; export type DevframeRuntime = 'cli' | 'build' | 'spa' | 'vite' | 'embedded'; export type DevframeServiceId = keyof DevframeServicesRegistry | (string & {}); export type DevframeServiceOf = ID extends keyof DevframeServicesRegistry ? DevframeServicesRegistry[ID] : unknown; diff --git a/tests/__snapshots__/tsnapi/devframe/initiate.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/initiate.snapshot.d.ts index f9bcfbda..2933e6b8 100644 --- a/tests/__snapshots__/tsnapi/devframe/initiate.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/initiate.snapshot.d.ts @@ -32,8 +32,8 @@ export interface InitDevframeOptions { app?: H3; getStorageDir?: (_: DevframeStorageScope) => string; destroyUnmatchedUpgrades?: boolean; - onPeerConnect?: (_: Peer, _: DevframeNodeRpcSession) => void; - onPeerDisconnect?: (_: Peer, _: DevframeNodeRpcSessionMeta) => void; + onPeerConnect?: (_: DevframeRpcConnection, _: DevframeNodeRpcSession) => void; + onPeerDisconnect?: (_: DevframeRpcConnection, _: DevframeNodeRpcSessionMeta) => void; } // #endregion diff --git a/tests/__snapshots__/tsnapi/devframe/rpc/transports/ws-server.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/rpc/transports/ws-server.snapshot.d.ts index db79e248..5423c5b1 100644 --- a/tests/__snapshots__/tsnapi/devframe/rpc/transports/ws-server.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/rpc/transports/ws-server.snapshot.d.ts @@ -7,6 +7,9 @@ export { createWsOriginRegistry } export { CreateWsOriginRegistryOptions } export { createWsRpcPeerHooks } export { DevframeNodeRpcSessionMeta } +export { DevframeRpcConnection } +export { DevframeRpcConnectionRequest } +export { DevframeRpcTransportKind } export { isAllowedOrigin } export { isLoopbackHostname } export { WsOriginRegistry } diff --git a/tests/__snapshots__/tsnapi/devframe/rpc/transports/ws-server.snapshot.js b/tests/__snapshots__/tsnapi/devframe/rpc/transports/ws-server.snapshot.js index 3519ab90..49c4c324 100644 --- a/tests/__snapshots__/tsnapi/devframe/rpc/transports/ws-server.snapshot.js +++ b/tests/__snapshots__/tsnapi/devframe/rpc/transports/ws-server.snapshot.js @@ -1,10 +1,10 @@ /** * Generated by tsnapi — public API snapshot of `devframe/rpc/transports/ws-server` */ -// #region Functions -export function attachWsRpcTransport(_, _) {} -export function createWsOriginRegistry(_) {} -export function createWsRpcPeerHooks(_, _) {} -export function isAllowedOrigin(_, _) {} -export function isLoopbackHostname(_) {} +// #region Other +export { attachWsRpcTransport } +export { createWsOriginRegistry } +export { createWsRpcPeerHooks } +export { isAllowedOrigin } +export { isLoopbackHostname } // #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts index 8e608c68..285d3175 100644 --- a/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts @@ -12,6 +12,7 @@ export { AgentToolInput } export { AgentToolProvider } export { AgentToolProviderHandle } export { ConnectionMeta } +export { ConnectionMetaSse } export { ConnectionMetaWebsocket } export { DevframeAgentHost } export { DevframeAgentHostEvents } @@ -31,8 +32,11 @@ export { DevframeNodeContext } export { DevframeNodeRpcSession } export { DevframeNodeRpcSessionMeta } export { DevframeRpcClientFunctions } +export { DevframeRpcConnection } +export { DevframeRpcConnectionRequest } export { DevframeRpcServerFunctions } export { DevframeRpcSharedStates } +export { DevframeRpcTransportKind } export { DevframeRuntime } export { DevframeScopedNodeContext } export { DevframeScopedNodeRpc } diff --git a/tests/helpers/serve-test-context.ts b/tests/helpers/serve-test-context.ts index ae2e5071..c9ab2919 100644 --- a/tests/helpers/serve-test-context.ts +++ b/tests/helpers/serve-test-context.ts @@ -1,7 +1,6 @@ -import type { Peer } from 'crossws' import type { StartedServer } from 'devframe/internal' import type { DevframeAuthHandler } from 'devframe/node/auth' -import type { ConnectionMeta, DevframeNodeContext, DevframeNodeRpcSession } from 'devframe/types' +import type { ConnectionMeta, DevframeNodeContext, DevframeNodeRpcSession, DevframeRpcConnection } from 'devframe/types' import type { H3 } from 'h3' import { createServer } from 'node:http' import { isIP } from 'node:net' @@ -27,8 +26,8 @@ export interface ServeTestContextOptions { app?: H3 /** Auth intent, same contract as the shell binding: `false` auto-trusts. */ auth?: boolean | DevframeAuthHandler - /** Called once per new WS connection, right after its session is created. */ - onPeerConnect?: (peer: Peer, session: DevframeNodeRpcSession) => void + /** Called once per new RPC connection, right after its session is created. */ + onPeerConnect?: (connection: DevframeRpcConnection, session: DevframeNodeRpcSession) => void } /**