@super-line/server 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { ServerMessageDef, RawConn, Serializer, ServerFrame, EmitData, ConnDescriptor, Adapter, PresenceStore, Contract, RoleOf, NodeStat, SharedEvents, SharedServerRequests, ClientInput, Output, SharedRequests, ServerInput, AnyData, RoleRequests, Events, DataOf, RoleTopics, AccessRules, Resource, Perms, ServerReplica, SharedTopics, EventData, ServerTransport, Handshake, ServerStore } from '@super-line/core';
1
+ import { ServerMessageDef, RawConn, Serializer, ServerFrame, EmitData, ConnDescriptor, Adapter, PresenceStore, Contract, RoleOf, NodeStat, SharedEvents, SharedServerRequests, ClientInput, Output, Directional, TapEvent, AccessRules, Resource, Perms, ListOpts, ResourceSummary, SearchOpts, ServerReplica, StoreInfo, CtsOf, ServerInput, ServerStore, Handshake, SharedRequests, AnyData, RoleRequests, Events, DataOf, RoleTopics, SharedTopics, EventData, ServerTransport } from '@super-line/core';
2
2
 
3
3
  /**
4
4
  * A single client connection, passed to handlers as the third argument.
@@ -132,13 +132,21 @@ type Handlers<C extends Contract, A> = ([keyof SharedRequests<C>] extends [never
132
132
  };
133
133
  /** Context passed to middleware and lifecycle hooks about the current operation. */
134
134
  interface MiddlewareInfo {
135
- /** Whether this is a request, a topic subscribe, or a bus event delivery. */
136
- kind: 'request' | 'subscribe' | 'event';
137
- /** The request/topic/event name. */
135
+ /**
136
+ * The operation kind. Middleware only ever sees `'request'`/`'subscribe'`; `'event'` marks a bus
137
+ * event delivery, and `'connect'`/`'disconnect'` mark a lifecycle-hook throw routed to `onError`.
138
+ */
139
+ kind: 'request' | 'subscribe' | 'event' | 'connect' | 'disconnect';
140
+ /** The request/topic/event name (the hook name for lifecycle errors). */
138
141
  name: string;
139
142
  /** The connection the operation is on, if any (`conn.role` available). Absent for bus events. */
140
143
  conn?: Conn;
141
144
  }
145
+ /**
146
+ * A plugin's flat middleware — like {@link Middleware} but `ctx` is `unknown` (a plugin is written
147
+ * independently of the host's per-role ctx). Concatenated after the host chain, in plugin array order.
148
+ */
149
+ type PluginMiddleware = (ctx: unknown, info: MiddlewareInfo, next: () => Promise<void>) => Awaitable<void>;
142
150
  /** Metadata passed to a {@link SuperLineServer.subscribe} callback alongside the event payload. */
143
151
  interface BusMeta {
144
152
  /** The node that published the event. Equals `srv.nodeId` for a same-node publish (local echo). */
@@ -241,8 +249,13 @@ interface ServerStoreHandle {
241
249
  revoke(id: string, principal: string): Promise<void>;
242
250
  /** Delete a Resource. */
243
251
  delete(id: string): Promise<void>;
244
- /** All Resource ids in this store. */
245
- list(): Promise<string[]>;
252
+ /**
253
+ * Summaries of this store's Resources, server-side filtered / sorted / paginated per {@link ListOpts}
254
+ * (omit `opts` for every Resource, id-ascending). Forwards to {@link ServerStore.list}.
255
+ */
256
+ list(opts?: ListOpts): Promise<ResourceSummary[]>;
257
+ /** Distinct principals granted anywhere in this store (store-global). Forwards to {@link ServerStore.searchPrincipals}. */
258
+ searchPrincipals(opts: SearchOpts): Promise<string[]>;
246
259
  /**
247
260
  * Open a reactive in-process co-writer over a Resource's canonical state — the server half's mirror of
248
261
  * `client.store(name).open(id)`. Reactive reads, merging `update`, and surgical `delete(path)` (the only
@@ -253,8 +266,158 @@ interface ServerStoreHandle {
253
266
  origin?: string;
254
267
  }): ServerReplica;
255
268
  }
269
+ /**
270
+ * Handlers a plugin provides for its paired surface `S` (ADR-0004): one per `clientToServer` key,
271
+ * typed from `S`. `ctx`/`conn` are loose — a plugin is written independently of the host's per-role ctx.
272
+ */
273
+ type HandlersFor<S extends Directional> = {
274
+ [K in keyof CtsOf<S>]: (input: ServerInput<CtsOf<S>[K]>, ctx: unknown, conn: Conn) => Awaitable<Output<CtsOf<S>[K]>>;
275
+ };
276
+ /** Keys one plugin handles; a naked param so a multi-plugin `P[number]` union distributes per-plugin. */
277
+ type PluginHandledKeys<U> = U extends SuperLinePlugin<infer S> ? keyof CtsOf<S> & string : never;
278
+ /** Union of the `clientToServer` keys handled across a plugin tuple `P` (subtracted from `implement`). */
279
+ type HandledKeys<P extends readonly SuperLinePlugin<any>[]> = PluginHandledKeys<P[number]>;
280
+ /** Remove the plugin-handled keys `HK` from every block (each role + `shared`) of a {@link Handlers} map. */
281
+ type SubtractHandlers<H, HK extends string> = {
282
+ [B in keyof H]: Omit<H[B], HK>;
283
+ };
284
+ /**
285
+ * A named, declarative bundle of runtime contributions registered on `plugins: [...]`. All fields
286
+ * are optional; a plugin ships as a pair (this server half + an optional client half). See ADR-0005.
287
+ * The optional type param `S` is the plugin's paired surface — its `handlers` compile against `S`, and
288
+ * `S`'s `clientToServer` keys are subtracted from the host's `implement()` obligation at compile time.
289
+ */
290
+ interface SuperLinePlugin<S extends Directional = {}> {
291
+ /** Unique among the server's plugins; a duplicate name throws at construction. */
292
+ name: string;
293
+ /**
294
+ * Node-local tap fired synchronously at each emit site with LIVE payload references (an observer
295
+ * must not mutate them). Reuses the {@link TapEvent} taxonomy; zero cost when no plugin taps. A
296
+ * throwing tap is isolated and routed to `onError` — it never fails the underlying operation.
297
+ */
298
+ onEvent?: (event: TapEvent) => void;
299
+ /** Middleware run before request/subscribe handlers, after the host chain, in plugin array order. */
300
+ use?: PluginMiddleware[];
301
+ /** Called once per accepted connection (multiplexed after the host's `onConnection`). */
302
+ onConnection?: (conn: Conn, ctx: unknown) => void;
303
+ /** Called when a connection closes (multiplexed after the host's `onDisconnect`). */
304
+ onDisconnect?: (conn: Conn, ctx: unknown, code: number) => void;
305
+ /** Receives any error thrown in middleware/handlers/hooks (multiplexed with the host's `onError`). */
306
+ onError?: (error: unknown, info: MiddlewareInfo) => void;
307
+ /**
308
+ * Request handlers for the plugin's paired surface `S`, built lazily with the {@link PluginContext}.
309
+ * Merged into dispatch under their method names; the host merges `S` into a role and these keys are
310
+ * subtracted from its `implement()` obligation. A key already handled by the host (or another plugin)
311
+ * throws at construction.
312
+ */
313
+ handlers?: (ctx: PluginContext) => HandlersFor<S>;
314
+ /**
315
+ * Server halves of Store pairs the plugin contributes, merged into the host's `stores` and reachable
316
+ * via `srv.store(name)`. A name colliding with a host store or another plugin's store throws at construction.
317
+ */
318
+ stores?: Record<string, ServerStore>;
319
+ /**
320
+ * A plugin-owned (reserved) connection class — its own role, handshake negotiation, and parallel contract,
321
+ * served over observer-invisible connections. See {@link PluginConnection}. (Phase 2.)
322
+ */
323
+ connection?: PluginConnection;
324
+ /**
325
+ * Imperative escape hatch, run once at construction with the plugin's {@link PluginContext}. Return
326
+ * an optional dispose function, called on `server.close()`. Use for wiring cluster-wide views from
327
+ * local taps + a plugin channel (the inspector's pattern), timers, or background subscriptions.
328
+ */
329
+ setup?: (ctx: PluginContext) => void | (() => void);
330
+ }
331
+ /**
332
+ * A plugin-owned connection class (ADR-0005 phase 2): a reserved role the transport negotiates (never one
333
+ * of the user contract's roles), dispatched against the plugin's own fixed `contract` — never merged into
334
+ * the user's. Matching conns are observer-invisible (excluded from conns/presence/heartbeat/user hooks).
335
+ * The inspector's Control-Center channel is one such class.
336
+ */
337
+ interface PluginConnection {
338
+ /** The reserved role; must be unique across the server (user roles + other reserved classes). */
339
+ role: string;
340
+ /** WebSocket subprotocol to advertise + match (browsers set this where they can't set headers). */
341
+ subprotocol?: string;
342
+ /** Predicate for transports without a subprotocol (SSE/libp2p): match on the normalized handshake. */
343
+ match?: (handshake: Handshake) => boolean;
344
+ /** The parallel contract these connections speak (its `clientToServer` = requests, `subscribe` topics = feeds). */
345
+ contract: Contract;
346
+ /**
347
+ * Request handlers for `contract`'s `clientToServer`, built with the {@link PluginContext}. A subscribe to
348
+ * one of `contract`'s topics bridges the conn to the plugin's {@link PluginChannel} of the same name.
349
+ */
350
+ handlers?: (ctx: PluginContext) => Record<string, (input: unknown, conn: Conn) => Awaitable<unknown>>;
351
+ }
352
+ /** A plugin-private adapter channel (reserved `x:<plugin>:` prefix), fanned out cluster-wide. */
353
+ interface PluginChannel {
354
+ /** Publish to this channel; delivered to every node's subscribers (local echo included). */
355
+ publish(data: unknown): void;
356
+ /** Subscribe to this channel. `meta.from` is the publishing node. Returns an unsubscribe fn. */
357
+ subscribe(handler: (data: unknown, meta: BusMeta) => void): () => void;
358
+ }
359
+ /**
360
+ * The capabilities handed to a plugin's `setup`/`handlers`: the server's public surface minus the
361
+ * footguns (`implement`/`close`), plus a privileged block — a plugin-private adapter {@link PluginChannel},
362
+ * node identity, the serializer, a read-only conns view, and the raw contract for reflection. Sized to
363
+ * the inspector's audited needs; grows case-by-case.
364
+ */
365
+ interface PluginContext {
366
+ /** This node's stable id (equals `srv.nodeId`). */
367
+ readonly nodeId: string;
368
+ /** This node's friendly name (equals `srv.nodeName`). */
369
+ readonly nodeName: string;
370
+ /** Alias of {@link PluginContext.nodeId} — the per-process instance id used to tag cluster fan-out. */
371
+ readonly instanceId: string;
372
+ /** The wire serializer configured on the server. */
373
+ readonly serializer: Serializer;
374
+ /** The raw contract, for reflection (e.g. `classifyContract`). */
375
+ readonly contract: Contract;
376
+ /** Connections accepted on THIS node (read-only snapshot; excludes reserved conns). */
377
+ readonly conns: readonly Conn[];
378
+ /** Node-local introspection (connections, rooms, topics on this process). */
379
+ readonly local: LocalView;
380
+ /** Cluster-wide presence introspection (rejects without a presence-capable adapter). */
381
+ readonly cluster: ClusterView;
382
+ /** Whether a user (by `identify` key) has at least one live connection anywhere. */
383
+ isOnline(userId: string): Promise<boolean>;
384
+ /** Publish a shared topic (server-only publish). */
385
+ publish(topic: string, data: unknown): void;
386
+ /** Subscribe server-side to a shared topic, cluster-wide (local echo). Returns an unsubscribe fn. */
387
+ subscribe(topic: string, handler: (data: unknown, meta: BusMeta) => void): () => void;
388
+ /** Target a single connection by id, on whatever node holds it. */
389
+ toConn(id: string): {
390
+ emit(event: string, data: unknown): void;
391
+ close(): void;
392
+ };
393
+ /** Target all of a user's connections across nodes. */
394
+ toUser(userId: string): {
395
+ emit(event: string, data: unknown): void;
396
+ disconnect(): void;
397
+ };
398
+ /** Server-controlled room membership + broadcast (loosely typed, mirroring toConn/toUser). */
399
+ room(name: string): {
400
+ add(conn: Conn): void;
401
+ remove(conn: Conn): void;
402
+ broadcast(event: string, data: unknown): void;
403
+ readonly size: number;
404
+ readonly connections: readonly Conn[];
405
+ };
406
+ /** Server-authoritative handle for a configured store (incl. plugin-contributed stores). */
407
+ store(name: string): ServerStoreHandle;
408
+ /** Configured stores (host + plugin) and their models. */
409
+ storeInfos(): StoreInfo[];
410
+ /** Full cluster descriptor for a local connection (identity + rooms + `describeConn` extras). */
411
+ describe(conn: Conn): ConnDescriptor;
412
+ /** A connection's descriptor anywhere in the cluster (rejects without presence support); undefined if absent. */
413
+ connectionById(id: string): Promise<ConnDescriptor | undefined>;
414
+ /** A plugin-private, cluster-wide adapter channel under the reserved `x:<plugin>:` prefix. */
415
+ channel(name: string): PluginChannel;
416
+ }
256
417
  /** Options for {@link createSuperLineServer}. */
257
- interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>> {
418
+ interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>, P extends readonly SuperLinePlugin<any>[] = readonly SuperLinePlugin<any>[]> {
419
+ /** Named runtime bundles (taps, middleware, lifecycle, handlers, stores). See {@link SuperLinePlugin}. */
420
+ plugins?: P;
258
421
  /** Client↔server transports to accept connections on (e.g. `webSocketServerTransport({ server })`). */
259
422
  transports: ServerTransport[];
260
423
  /** Wire serializer; MUST match the client. Defaults to `jsonSerializer`. */
@@ -286,14 +449,6 @@ interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>> {
286
449
  interval?: number;
287
450
  maxMissed?: number;
288
451
  } | false;
289
- /**
290
- * Enable the read-only Control Center inspector: emit `msg.*` telemetry and accept inspector
291
- * clients. The WS transport must also be created with `inspector: true` to negotiate the
292
- * `superline.inspector.v1` subprotocol. **Default off; dev / trusted-network only.**
293
- */
294
- inspector?: boolean | {
295
- redact?: string[];
296
- };
297
452
  /**
298
453
  * Pluggable persisted-state Stores, keyed by name (`{ scene: crdtStoreServer(), config: memoryStoreServer() }`).
299
454
  * Each is the server half of a Store pair; the client passes the matching client halves. Surfaced as
@@ -308,7 +463,7 @@ interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>> {
308
463
  onError?: (error: unknown, info: MiddlewareInfo) => void;
309
464
  }
310
465
  /** A running super-line server, returned by {@link createSuperLineServer}. */
311
- interface SuperLineServer<C extends Contract, A extends AuthResult<C>> {
466
+ interface SuperLineServer<C extends Contract, A extends AuthResult<C>, HK extends string = never> {
312
467
  /** This node's stable id (unique per server process). */
313
468
  readonly nodeId: string;
314
469
  /** This node's friendly name (from `nodeName`/`SUPER_LINE_NODE_NAME`, else a short `nodeId` slice). */
@@ -323,8 +478,8 @@ interface SuperLineServer<C extends Contract, A extends AuthResult<C>> {
323
478
  toConn(id: string): ConnTarget<C>;
324
479
  /** Target all of a user's connections (by `identify` key) across nodes (emit/disconnect). */
325
480
  toUser(userId: string): UserTarget<C>;
326
- /** Register handlers for shared + per-role requests (chainable). */
327
- implement(handlers: Handlers<C, A>): SuperLineServer<C, A>;
481
+ /** Register handlers for shared + per-role requests (chainable). Keys handled by a plugin (`HK`) are subtracted. */
482
+ implement(handlers: SubtractHandlers<Handlers<C, A>, HK>): SuperLineServer<C, A, HK>;
328
483
  /** Mixed-role connection group; broadcast() sends a shared contract event to members. */
329
484
  room(name: string): Room<C>;
330
485
  /** Publish a SHARED topic to all subscribers (server-only publish). */
@@ -364,6 +519,6 @@ interface SuperLineServer<C extends Contract, A extends AuthResult<C>> {
364
519
  * })
365
520
  * ```
366
521
  */
367
- declare function createSuperLineServer<C extends Contract, A extends AuthResult<C>>(contract: C, opts: SuperLineServerOptions<C, A>): SuperLineServer<C, A>;
522
+ declare function createSuperLineServer<C extends Contract, A extends AuthResult<C>, const P extends readonly SuperLinePlugin<any>[] = []>(contract: C, opts: SuperLineServerOptions<C, A, P>): SuperLineServer<C, A, HandledKeys<P>>;
368
523
 
369
- export { type AuthResult, type BusMeta, type ClusterView, Conn, type ConnTarget, type Handlers, type LocalView, MemoryBus, type Middleware, type MiddlewareInfo, type RoleLens, type Room, type ServerStoreHandle, type SuperLineServer, type SuperLineServerOptions, type UserTarget, createInMemoryAdapter, createSuperLineServer, resolvePrincipal };
524
+ export { type AuthResult, type BusMeta, type ClusterView, Conn, type ConnTarget, type HandledKeys, type Handlers, type HandlersFor, type LocalView, MemoryBus, type Middleware, type MiddlewareInfo, type PluginChannel, type PluginConnection, type PluginContext, type PluginMiddleware, type RoleLens, type Room, type ServerStoreHandle, type SubtractHandlers, type SuperLinePlugin, type SuperLineServer, type SuperLineServerOptions, type UserTarget, createInMemoryAdapter, createSuperLineServer, resolvePrincipal };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ServerMessageDef, RawConn, Serializer, ServerFrame, EmitData, ConnDescriptor, Adapter, PresenceStore, Contract, RoleOf, NodeStat, SharedEvents, SharedServerRequests, ClientInput, Output, SharedRequests, ServerInput, AnyData, RoleRequests, Events, DataOf, RoleTopics, AccessRules, Resource, Perms, ServerReplica, SharedTopics, EventData, ServerTransport, Handshake, ServerStore } from '@super-line/core';
1
+ import { ServerMessageDef, RawConn, Serializer, ServerFrame, EmitData, ConnDescriptor, Adapter, PresenceStore, Contract, RoleOf, NodeStat, SharedEvents, SharedServerRequests, ClientInput, Output, Directional, TapEvent, AccessRules, Resource, Perms, ListOpts, ResourceSummary, SearchOpts, ServerReplica, StoreInfo, CtsOf, ServerInput, ServerStore, Handshake, SharedRequests, AnyData, RoleRequests, Events, DataOf, RoleTopics, SharedTopics, EventData, ServerTransport } from '@super-line/core';
2
2
 
3
3
  /**
4
4
  * A single client connection, passed to handlers as the third argument.
@@ -132,13 +132,21 @@ type Handlers<C extends Contract, A> = ([keyof SharedRequests<C>] extends [never
132
132
  };
133
133
  /** Context passed to middleware and lifecycle hooks about the current operation. */
134
134
  interface MiddlewareInfo {
135
- /** Whether this is a request, a topic subscribe, or a bus event delivery. */
136
- kind: 'request' | 'subscribe' | 'event';
137
- /** The request/topic/event name. */
135
+ /**
136
+ * The operation kind. Middleware only ever sees `'request'`/`'subscribe'`; `'event'` marks a bus
137
+ * event delivery, and `'connect'`/`'disconnect'` mark a lifecycle-hook throw routed to `onError`.
138
+ */
139
+ kind: 'request' | 'subscribe' | 'event' | 'connect' | 'disconnect';
140
+ /** The request/topic/event name (the hook name for lifecycle errors). */
138
141
  name: string;
139
142
  /** The connection the operation is on, if any (`conn.role` available). Absent for bus events. */
140
143
  conn?: Conn;
141
144
  }
145
+ /**
146
+ * A plugin's flat middleware — like {@link Middleware} but `ctx` is `unknown` (a plugin is written
147
+ * independently of the host's per-role ctx). Concatenated after the host chain, in plugin array order.
148
+ */
149
+ type PluginMiddleware = (ctx: unknown, info: MiddlewareInfo, next: () => Promise<void>) => Awaitable<void>;
142
150
  /** Metadata passed to a {@link SuperLineServer.subscribe} callback alongside the event payload. */
143
151
  interface BusMeta {
144
152
  /** The node that published the event. Equals `srv.nodeId` for a same-node publish (local echo). */
@@ -241,8 +249,13 @@ interface ServerStoreHandle {
241
249
  revoke(id: string, principal: string): Promise<void>;
242
250
  /** Delete a Resource. */
243
251
  delete(id: string): Promise<void>;
244
- /** All Resource ids in this store. */
245
- list(): Promise<string[]>;
252
+ /**
253
+ * Summaries of this store's Resources, server-side filtered / sorted / paginated per {@link ListOpts}
254
+ * (omit `opts` for every Resource, id-ascending). Forwards to {@link ServerStore.list}.
255
+ */
256
+ list(opts?: ListOpts): Promise<ResourceSummary[]>;
257
+ /** Distinct principals granted anywhere in this store (store-global). Forwards to {@link ServerStore.searchPrincipals}. */
258
+ searchPrincipals(opts: SearchOpts): Promise<string[]>;
246
259
  /**
247
260
  * Open a reactive in-process co-writer over a Resource's canonical state — the server half's mirror of
248
261
  * `client.store(name).open(id)`. Reactive reads, merging `update`, and surgical `delete(path)` (the only
@@ -253,8 +266,158 @@ interface ServerStoreHandle {
253
266
  origin?: string;
254
267
  }): ServerReplica;
255
268
  }
269
+ /**
270
+ * Handlers a plugin provides for its paired surface `S` (ADR-0004): one per `clientToServer` key,
271
+ * typed from `S`. `ctx`/`conn` are loose — a plugin is written independently of the host's per-role ctx.
272
+ */
273
+ type HandlersFor<S extends Directional> = {
274
+ [K in keyof CtsOf<S>]: (input: ServerInput<CtsOf<S>[K]>, ctx: unknown, conn: Conn) => Awaitable<Output<CtsOf<S>[K]>>;
275
+ };
276
+ /** Keys one plugin handles; a naked param so a multi-plugin `P[number]` union distributes per-plugin. */
277
+ type PluginHandledKeys<U> = U extends SuperLinePlugin<infer S> ? keyof CtsOf<S> & string : never;
278
+ /** Union of the `clientToServer` keys handled across a plugin tuple `P` (subtracted from `implement`). */
279
+ type HandledKeys<P extends readonly SuperLinePlugin<any>[]> = PluginHandledKeys<P[number]>;
280
+ /** Remove the plugin-handled keys `HK` from every block (each role + `shared`) of a {@link Handlers} map. */
281
+ type SubtractHandlers<H, HK extends string> = {
282
+ [B in keyof H]: Omit<H[B], HK>;
283
+ };
284
+ /**
285
+ * A named, declarative bundle of runtime contributions registered on `plugins: [...]`. All fields
286
+ * are optional; a plugin ships as a pair (this server half + an optional client half). See ADR-0005.
287
+ * The optional type param `S` is the plugin's paired surface — its `handlers` compile against `S`, and
288
+ * `S`'s `clientToServer` keys are subtracted from the host's `implement()` obligation at compile time.
289
+ */
290
+ interface SuperLinePlugin<S extends Directional = {}> {
291
+ /** Unique among the server's plugins; a duplicate name throws at construction. */
292
+ name: string;
293
+ /**
294
+ * Node-local tap fired synchronously at each emit site with LIVE payload references (an observer
295
+ * must not mutate them). Reuses the {@link TapEvent} taxonomy; zero cost when no plugin taps. A
296
+ * throwing tap is isolated and routed to `onError` — it never fails the underlying operation.
297
+ */
298
+ onEvent?: (event: TapEvent) => void;
299
+ /** Middleware run before request/subscribe handlers, after the host chain, in plugin array order. */
300
+ use?: PluginMiddleware[];
301
+ /** Called once per accepted connection (multiplexed after the host's `onConnection`). */
302
+ onConnection?: (conn: Conn, ctx: unknown) => void;
303
+ /** Called when a connection closes (multiplexed after the host's `onDisconnect`). */
304
+ onDisconnect?: (conn: Conn, ctx: unknown, code: number) => void;
305
+ /** Receives any error thrown in middleware/handlers/hooks (multiplexed with the host's `onError`). */
306
+ onError?: (error: unknown, info: MiddlewareInfo) => void;
307
+ /**
308
+ * Request handlers for the plugin's paired surface `S`, built lazily with the {@link PluginContext}.
309
+ * Merged into dispatch under their method names; the host merges `S` into a role and these keys are
310
+ * subtracted from its `implement()` obligation. A key already handled by the host (or another plugin)
311
+ * throws at construction.
312
+ */
313
+ handlers?: (ctx: PluginContext) => HandlersFor<S>;
314
+ /**
315
+ * Server halves of Store pairs the plugin contributes, merged into the host's `stores` and reachable
316
+ * via `srv.store(name)`. A name colliding with a host store or another plugin's store throws at construction.
317
+ */
318
+ stores?: Record<string, ServerStore>;
319
+ /**
320
+ * A plugin-owned (reserved) connection class — its own role, handshake negotiation, and parallel contract,
321
+ * served over observer-invisible connections. See {@link PluginConnection}. (Phase 2.)
322
+ */
323
+ connection?: PluginConnection;
324
+ /**
325
+ * Imperative escape hatch, run once at construction with the plugin's {@link PluginContext}. Return
326
+ * an optional dispose function, called on `server.close()`. Use for wiring cluster-wide views from
327
+ * local taps + a plugin channel (the inspector's pattern), timers, or background subscriptions.
328
+ */
329
+ setup?: (ctx: PluginContext) => void | (() => void);
330
+ }
331
+ /**
332
+ * A plugin-owned connection class (ADR-0005 phase 2): a reserved role the transport negotiates (never one
333
+ * of the user contract's roles), dispatched against the plugin's own fixed `contract` — never merged into
334
+ * the user's. Matching conns are observer-invisible (excluded from conns/presence/heartbeat/user hooks).
335
+ * The inspector's Control-Center channel is one such class.
336
+ */
337
+ interface PluginConnection {
338
+ /** The reserved role; must be unique across the server (user roles + other reserved classes). */
339
+ role: string;
340
+ /** WebSocket subprotocol to advertise + match (browsers set this where they can't set headers). */
341
+ subprotocol?: string;
342
+ /** Predicate for transports without a subprotocol (SSE/libp2p): match on the normalized handshake. */
343
+ match?: (handshake: Handshake) => boolean;
344
+ /** The parallel contract these connections speak (its `clientToServer` = requests, `subscribe` topics = feeds). */
345
+ contract: Contract;
346
+ /**
347
+ * Request handlers for `contract`'s `clientToServer`, built with the {@link PluginContext}. A subscribe to
348
+ * one of `contract`'s topics bridges the conn to the plugin's {@link PluginChannel} of the same name.
349
+ */
350
+ handlers?: (ctx: PluginContext) => Record<string, (input: unknown, conn: Conn) => Awaitable<unknown>>;
351
+ }
352
+ /** A plugin-private adapter channel (reserved `x:<plugin>:` prefix), fanned out cluster-wide. */
353
+ interface PluginChannel {
354
+ /** Publish to this channel; delivered to every node's subscribers (local echo included). */
355
+ publish(data: unknown): void;
356
+ /** Subscribe to this channel. `meta.from` is the publishing node. Returns an unsubscribe fn. */
357
+ subscribe(handler: (data: unknown, meta: BusMeta) => void): () => void;
358
+ }
359
+ /**
360
+ * The capabilities handed to a plugin's `setup`/`handlers`: the server's public surface minus the
361
+ * footguns (`implement`/`close`), plus a privileged block — a plugin-private adapter {@link PluginChannel},
362
+ * node identity, the serializer, a read-only conns view, and the raw contract for reflection. Sized to
363
+ * the inspector's audited needs; grows case-by-case.
364
+ */
365
+ interface PluginContext {
366
+ /** This node's stable id (equals `srv.nodeId`). */
367
+ readonly nodeId: string;
368
+ /** This node's friendly name (equals `srv.nodeName`). */
369
+ readonly nodeName: string;
370
+ /** Alias of {@link PluginContext.nodeId} — the per-process instance id used to tag cluster fan-out. */
371
+ readonly instanceId: string;
372
+ /** The wire serializer configured on the server. */
373
+ readonly serializer: Serializer;
374
+ /** The raw contract, for reflection (e.g. `classifyContract`). */
375
+ readonly contract: Contract;
376
+ /** Connections accepted on THIS node (read-only snapshot; excludes reserved conns). */
377
+ readonly conns: readonly Conn[];
378
+ /** Node-local introspection (connections, rooms, topics on this process). */
379
+ readonly local: LocalView;
380
+ /** Cluster-wide presence introspection (rejects without a presence-capable adapter). */
381
+ readonly cluster: ClusterView;
382
+ /** Whether a user (by `identify` key) has at least one live connection anywhere. */
383
+ isOnline(userId: string): Promise<boolean>;
384
+ /** Publish a shared topic (server-only publish). */
385
+ publish(topic: string, data: unknown): void;
386
+ /** Subscribe server-side to a shared topic, cluster-wide (local echo). Returns an unsubscribe fn. */
387
+ subscribe(topic: string, handler: (data: unknown, meta: BusMeta) => void): () => void;
388
+ /** Target a single connection by id, on whatever node holds it. */
389
+ toConn(id: string): {
390
+ emit(event: string, data: unknown): void;
391
+ close(): void;
392
+ };
393
+ /** Target all of a user's connections across nodes. */
394
+ toUser(userId: string): {
395
+ emit(event: string, data: unknown): void;
396
+ disconnect(): void;
397
+ };
398
+ /** Server-controlled room membership + broadcast (loosely typed, mirroring toConn/toUser). */
399
+ room(name: string): {
400
+ add(conn: Conn): void;
401
+ remove(conn: Conn): void;
402
+ broadcast(event: string, data: unknown): void;
403
+ readonly size: number;
404
+ readonly connections: readonly Conn[];
405
+ };
406
+ /** Server-authoritative handle for a configured store (incl. plugin-contributed stores). */
407
+ store(name: string): ServerStoreHandle;
408
+ /** Configured stores (host + plugin) and their models. */
409
+ storeInfos(): StoreInfo[];
410
+ /** Full cluster descriptor for a local connection (identity + rooms + `describeConn` extras). */
411
+ describe(conn: Conn): ConnDescriptor;
412
+ /** A connection's descriptor anywhere in the cluster (rejects without presence support); undefined if absent. */
413
+ connectionById(id: string): Promise<ConnDescriptor | undefined>;
414
+ /** A plugin-private, cluster-wide adapter channel under the reserved `x:<plugin>:` prefix. */
415
+ channel(name: string): PluginChannel;
416
+ }
256
417
  /** Options for {@link createSuperLineServer}. */
257
- interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>> {
418
+ interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>, P extends readonly SuperLinePlugin<any>[] = readonly SuperLinePlugin<any>[]> {
419
+ /** Named runtime bundles (taps, middleware, lifecycle, handlers, stores). See {@link SuperLinePlugin}. */
420
+ plugins?: P;
258
421
  /** Client↔server transports to accept connections on (e.g. `webSocketServerTransport({ server })`). */
259
422
  transports: ServerTransport[];
260
423
  /** Wire serializer; MUST match the client. Defaults to `jsonSerializer`. */
@@ -286,14 +449,6 @@ interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>> {
286
449
  interval?: number;
287
450
  maxMissed?: number;
288
451
  } | false;
289
- /**
290
- * Enable the read-only Control Center inspector: emit `msg.*` telemetry and accept inspector
291
- * clients. The WS transport must also be created with `inspector: true` to negotiate the
292
- * `superline.inspector.v1` subprotocol. **Default off; dev / trusted-network only.**
293
- */
294
- inspector?: boolean | {
295
- redact?: string[];
296
- };
297
452
  /**
298
453
  * Pluggable persisted-state Stores, keyed by name (`{ scene: crdtStoreServer(), config: memoryStoreServer() }`).
299
454
  * Each is the server half of a Store pair; the client passes the matching client halves. Surfaced as
@@ -308,7 +463,7 @@ interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>> {
308
463
  onError?: (error: unknown, info: MiddlewareInfo) => void;
309
464
  }
310
465
  /** A running super-line server, returned by {@link createSuperLineServer}. */
311
- interface SuperLineServer<C extends Contract, A extends AuthResult<C>> {
466
+ interface SuperLineServer<C extends Contract, A extends AuthResult<C>, HK extends string = never> {
312
467
  /** This node's stable id (unique per server process). */
313
468
  readonly nodeId: string;
314
469
  /** This node's friendly name (from `nodeName`/`SUPER_LINE_NODE_NAME`, else a short `nodeId` slice). */
@@ -323,8 +478,8 @@ interface SuperLineServer<C extends Contract, A extends AuthResult<C>> {
323
478
  toConn(id: string): ConnTarget<C>;
324
479
  /** Target all of a user's connections (by `identify` key) across nodes (emit/disconnect). */
325
480
  toUser(userId: string): UserTarget<C>;
326
- /** Register handlers for shared + per-role requests (chainable). */
327
- implement(handlers: Handlers<C, A>): SuperLineServer<C, A>;
481
+ /** Register handlers for shared + per-role requests (chainable). Keys handled by a plugin (`HK`) are subtracted. */
482
+ implement(handlers: SubtractHandlers<Handlers<C, A>, HK>): SuperLineServer<C, A, HK>;
328
483
  /** Mixed-role connection group; broadcast() sends a shared contract event to members. */
329
484
  room(name: string): Room<C>;
330
485
  /** Publish a SHARED topic to all subscribers (server-only publish). */
@@ -364,6 +519,6 @@ interface SuperLineServer<C extends Contract, A extends AuthResult<C>> {
364
519
  * })
365
520
  * ```
366
521
  */
367
- declare function createSuperLineServer<C extends Contract, A extends AuthResult<C>>(contract: C, opts: SuperLineServerOptions<C, A>): SuperLineServer<C, A>;
522
+ declare function createSuperLineServer<C extends Contract, A extends AuthResult<C>, const P extends readonly SuperLinePlugin<any>[] = []>(contract: C, opts: SuperLineServerOptions<C, A, P>): SuperLineServer<C, A, HandledKeys<P>>;
368
523
 
369
- export { type AuthResult, type BusMeta, type ClusterView, Conn, type ConnTarget, type Handlers, type LocalView, MemoryBus, type Middleware, type MiddlewareInfo, type RoleLens, type Room, type ServerStoreHandle, type SuperLineServer, type SuperLineServerOptions, type UserTarget, createInMemoryAdapter, createSuperLineServer, resolvePrincipal };
524
+ export { type AuthResult, type BusMeta, type ClusterView, Conn, type ConnTarget, type HandledKeys, type Handlers, type HandlersFor, type LocalView, MemoryBus, type Middleware, type MiddlewareInfo, type PluginChannel, type PluginConnection, type PluginContext, type PluginMiddleware, type RoleLens, type Room, type ServerStoreHandle, type SubtractHandlers, type SuperLinePlugin, type SuperLineServer, type SuperLineServerOptions, type UserTarget, createInMemoryAdapter, createSuperLineServer, resolvePrincipal };