@super-line/server 0.8.0 → 0.10.0

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). */
@@ -228,16 +236,26 @@ interface ServerStoreHandle {
228
236
  create(id: string, data: unknown, accessRules: AccessRules): Promise<void>;
229
237
  /** Read a Resource (data + accessRules), or undefined if absent. */
230
238
  read(id: string): Promise<Resource | undefined>;
231
- /** Server co-write: replace the Resource's value (LWW), fanned out to subscribers with a `server` origin. */
232
- write(id: string, data: unknown): Promise<void>;
239
+ /**
240
+ * Server co-write: replace the Resource's value (LWW), fanned out to subscribers stamped with `origin`
241
+ * (default `"server"`) for echo-break + inspector attribution — mirrors {@link ServerStore.open}'s origin.
242
+ */
243
+ write(id: string, data: unknown, opts?: {
244
+ origin?: string;
245
+ }): Promise<void>;
233
246
  /** Grant a principal read/write on a Resource. */
234
247
  grant(id: string, principal: string, perms: Perms): Promise<void>;
235
248
  /** Revoke a principal's access to a Resource entirely. */
236
249
  revoke(id: string, principal: string): Promise<void>;
237
250
  /** Delete a Resource. */
238
251
  delete(id: string): Promise<void>;
239
- /** All Resource ids in this store. */
240
- 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[]>;
241
259
  /**
242
260
  * Open a reactive in-process co-writer over a Resource's canonical state — the server half's mirror of
243
261
  * `client.store(name).open(id)`. Reactive reads, merging `update`, and surgical `delete(path)` (the only
@@ -248,8 +266,156 @@ interface ServerStoreHandle {
248
266
  origin?: string;
249
267
  }): ServerReplica;
250
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
+ /** Union of the `clientToServer` keys handled across a plugin tuple `P` (subtracted from `implement`). */
277
+ type HandledKeys<P extends readonly SuperLinePlugin<any>[]> = P[number] extends SuperLinePlugin<infer S> ? keyof CtsOf<S> & string : never;
278
+ /** Remove the plugin-handled keys `HK` from every block (each role + `shared`) of a {@link Handlers} map. */
279
+ type SubtractHandlers<H, HK extends string> = {
280
+ [B in keyof H]: Omit<H[B], HK>;
281
+ };
282
+ /**
283
+ * A named, declarative bundle of runtime contributions registered on `plugins: [...]`. All fields
284
+ * are optional; a plugin ships as a pair (this server half + an optional client half). See ADR-0005.
285
+ * The optional type param `S` is the plugin's paired surface — its `handlers` compile against `S`, and
286
+ * `S`'s `clientToServer` keys are subtracted from the host's `implement()` obligation at compile time.
287
+ */
288
+ interface SuperLinePlugin<S extends Directional = {}> {
289
+ /** Unique among the server's plugins; a duplicate name throws at construction. */
290
+ name: string;
291
+ /**
292
+ * Node-local tap fired synchronously at each emit site with LIVE payload references (an observer
293
+ * must not mutate them). Reuses the {@link TapEvent} taxonomy; zero cost when no plugin taps. A
294
+ * throwing tap is isolated and routed to `onError` — it never fails the underlying operation.
295
+ */
296
+ onEvent?: (event: TapEvent) => void;
297
+ /** Middleware run before request/subscribe handlers, after the host chain, in plugin array order. */
298
+ use?: PluginMiddleware[];
299
+ /** Called once per accepted connection (multiplexed after the host's `onConnection`). */
300
+ onConnection?: (conn: Conn, ctx: unknown) => void;
301
+ /** Called when a connection closes (multiplexed after the host's `onDisconnect`). */
302
+ onDisconnect?: (conn: Conn, ctx: unknown, code: number) => void;
303
+ /** Receives any error thrown in middleware/handlers/hooks (multiplexed with the host's `onError`). */
304
+ onError?: (error: unknown, info: MiddlewareInfo) => void;
305
+ /**
306
+ * Request handlers for the plugin's paired surface `S`, built lazily with the {@link PluginContext}.
307
+ * Merged into dispatch under their method names; the host merges `S` into a role and these keys are
308
+ * subtracted from its `implement()` obligation. A key already handled by the host (or another plugin)
309
+ * throws at construction.
310
+ */
311
+ handlers?: (ctx: PluginContext) => HandlersFor<S>;
312
+ /**
313
+ * Server halves of Store pairs the plugin contributes, merged into the host's `stores` and reachable
314
+ * via `srv.store(name)`. A name colliding with a host store or another plugin's store throws at construction.
315
+ */
316
+ stores?: Record<string, ServerStore>;
317
+ /**
318
+ * A plugin-owned (reserved) connection class — its own role, handshake negotiation, and parallel contract,
319
+ * served over observer-invisible connections. See {@link PluginConnection}. (Phase 2.)
320
+ */
321
+ connection?: PluginConnection;
322
+ /**
323
+ * Imperative escape hatch, run once at construction with the plugin's {@link PluginContext}. Return
324
+ * an optional dispose function, called on `server.close()`. Use for wiring cluster-wide views from
325
+ * local taps + a plugin channel (the inspector's pattern), timers, or background subscriptions.
326
+ */
327
+ setup?: (ctx: PluginContext) => void | (() => void);
328
+ }
329
+ /**
330
+ * A plugin-owned connection class (ADR-0005 phase 2): a reserved role the transport negotiates (never one
331
+ * of the user contract's roles), dispatched against the plugin's own fixed `contract` — never merged into
332
+ * the user's. Matching conns are observer-invisible (excluded from conns/presence/heartbeat/user hooks).
333
+ * The inspector's Control-Center channel is one such class.
334
+ */
335
+ interface PluginConnection {
336
+ /** The reserved role; must be unique across the server (user roles + other reserved classes). */
337
+ role: string;
338
+ /** WebSocket subprotocol to advertise + match (browsers set this where they can't set headers). */
339
+ subprotocol?: string;
340
+ /** Predicate for transports without a subprotocol (SSE/libp2p): match on the normalized handshake. */
341
+ match?: (handshake: Handshake) => boolean;
342
+ /** The parallel contract these connections speak (its `clientToServer` = requests, `subscribe` topics = feeds). */
343
+ contract: Contract;
344
+ /**
345
+ * Request handlers for `contract`'s `clientToServer`, built with the {@link PluginContext}. A subscribe to
346
+ * one of `contract`'s topics bridges the conn to the plugin's {@link PluginChannel} of the same name.
347
+ */
348
+ handlers?: (ctx: PluginContext) => Record<string, (input: unknown, conn: Conn) => Awaitable<unknown>>;
349
+ }
350
+ /** A plugin-private adapter channel (reserved `x:<plugin>:` prefix), fanned out cluster-wide. */
351
+ interface PluginChannel {
352
+ /** Publish to this channel; delivered to every node's subscribers (local echo included). */
353
+ publish(data: unknown): void;
354
+ /** Subscribe to this channel. `meta.from` is the publishing node. Returns an unsubscribe fn. */
355
+ subscribe(handler: (data: unknown, meta: BusMeta) => void): () => void;
356
+ }
357
+ /**
358
+ * The capabilities handed to a plugin's `setup`/`handlers`: the server's public surface minus the
359
+ * footguns (`implement`/`close`), plus a privileged block — a plugin-private adapter {@link PluginChannel},
360
+ * node identity, the serializer, a read-only conns view, and the raw contract for reflection. Sized to
361
+ * the inspector's audited needs; grows case-by-case.
362
+ */
363
+ interface PluginContext {
364
+ /** This node's stable id (equals `srv.nodeId`). */
365
+ readonly nodeId: string;
366
+ /** This node's friendly name (equals `srv.nodeName`). */
367
+ readonly nodeName: string;
368
+ /** Alias of {@link PluginContext.nodeId} — the per-process instance id used to tag cluster fan-out. */
369
+ readonly instanceId: string;
370
+ /** The wire serializer configured on the server. */
371
+ readonly serializer: Serializer;
372
+ /** The raw contract, for reflection (e.g. `classifyContract`). */
373
+ readonly contract: Contract;
374
+ /** Connections accepted on THIS node (read-only snapshot; excludes reserved conns). */
375
+ readonly conns: readonly Conn[];
376
+ /** Node-local introspection (connections, rooms, topics on this process). */
377
+ readonly local: LocalView;
378
+ /** Cluster-wide presence introspection (rejects without a presence-capable adapter). */
379
+ readonly cluster: ClusterView;
380
+ /** Whether a user (by `identify` key) has at least one live connection anywhere. */
381
+ isOnline(userId: string): Promise<boolean>;
382
+ /** Publish a shared topic (server-only publish). */
383
+ publish(topic: string, data: unknown): void;
384
+ /** Subscribe server-side to a shared topic, cluster-wide (local echo). Returns an unsubscribe fn. */
385
+ subscribe(topic: string, handler: (data: unknown, meta: BusMeta) => void): () => void;
386
+ /** Target a single connection by id, on whatever node holds it. */
387
+ toConn(id: string): {
388
+ emit(event: string, data: unknown): void;
389
+ close(): void;
390
+ };
391
+ /** Target all of a user's connections across nodes. */
392
+ toUser(userId: string): {
393
+ emit(event: string, data: unknown): void;
394
+ disconnect(): void;
395
+ };
396
+ /** Server-controlled room membership + broadcast (loosely typed, mirroring toConn/toUser). */
397
+ room(name: string): {
398
+ add(conn: Conn): void;
399
+ remove(conn: Conn): void;
400
+ broadcast(event: string, data: unknown): void;
401
+ readonly size: number;
402
+ readonly connections: readonly Conn[];
403
+ };
404
+ /** Server-authoritative handle for a configured store (incl. plugin-contributed stores). */
405
+ store(name: string): ServerStoreHandle;
406
+ /** Configured stores (host + plugin) and their models. */
407
+ storeInfos(): StoreInfo[];
408
+ /** Full cluster descriptor for a local connection (identity + rooms + `describeConn` extras). */
409
+ describe(conn: Conn): ConnDescriptor;
410
+ /** A connection's descriptor anywhere in the cluster (rejects without presence support); undefined if absent. */
411
+ connectionById(id: string): Promise<ConnDescriptor | undefined>;
412
+ /** A plugin-private, cluster-wide adapter channel under the reserved `x:<plugin>:` prefix. */
413
+ channel(name: string): PluginChannel;
414
+ }
251
415
  /** Options for {@link createSuperLineServer}. */
252
- interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>> {
416
+ interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>, P extends readonly SuperLinePlugin<any>[] = readonly SuperLinePlugin<any>[]> {
417
+ /** Named runtime bundles (taps, middleware, lifecycle, handlers, stores). See {@link SuperLinePlugin}. */
418
+ plugins?: P;
253
419
  /** Client↔server transports to accept connections on (e.g. `webSocketServerTransport({ server })`). */
254
420
  transports: ServerTransport[];
255
421
  /** Wire serializer; MUST match the client. Defaults to `jsonSerializer`. */
@@ -281,14 +447,6 @@ interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>> {
281
447
  interval?: number;
282
448
  maxMissed?: number;
283
449
  } | false;
284
- /**
285
- * Enable the read-only Control Center inspector: emit `msg.*` telemetry and accept inspector
286
- * clients. The WS transport must also be created with `inspector: true` to negotiate the
287
- * `superline.inspector.v1` subprotocol. **Default off; dev / trusted-network only.**
288
- */
289
- inspector?: boolean | {
290
- redact?: string[];
291
- };
292
450
  /**
293
451
  * Pluggable persisted-state Stores, keyed by name (`{ scene: crdtStoreServer(), config: memoryStoreServer() }`).
294
452
  * Each is the server half of a Store pair; the client passes the matching client halves. Surfaced as
@@ -303,7 +461,7 @@ interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>> {
303
461
  onError?: (error: unknown, info: MiddlewareInfo) => void;
304
462
  }
305
463
  /** A running super-line server, returned by {@link createSuperLineServer}. */
306
- interface SuperLineServer<C extends Contract, A extends AuthResult<C>> {
464
+ interface SuperLineServer<C extends Contract, A extends AuthResult<C>, HK extends string = never> {
307
465
  /** This node's stable id (unique per server process). */
308
466
  readonly nodeId: string;
309
467
  /** This node's friendly name (from `nodeName`/`SUPER_LINE_NODE_NAME`, else a short `nodeId` slice). */
@@ -318,8 +476,8 @@ interface SuperLineServer<C extends Contract, A extends AuthResult<C>> {
318
476
  toConn(id: string): ConnTarget<C>;
319
477
  /** Target all of a user's connections (by `identify` key) across nodes (emit/disconnect). */
320
478
  toUser(userId: string): UserTarget<C>;
321
- /** Register handlers for shared + per-role requests (chainable). */
322
- implement(handlers: Handlers<C, A>): SuperLineServer<C, A>;
479
+ /** Register handlers for shared + per-role requests (chainable). Keys handled by a plugin (`HK`) are subtracted. */
480
+ implement(handlers: SubtractHandlers<Handlers<C, A>, HK>): SuperLineServer<C, A, HK>;
323
481
  /** Mixed-role connection group; broadcast() sends a shared contract event to members. */
324
482
  room(name: string): Room<C>;
325
483
  /** Publish a SHARED topic to all subscribers (server-only publish). */
@@ -359,6 +517,6 @@ interface SuperLineServer<C extends Contract, A extends AuthResult<C>> {
359
517
  * })
360
518
  * ```
361
519
  */
362
- declare function createSuperLineServer<C extends Contract, A extends AuthResult<C>>(contract: C, opts: SuperLineServerOptions<C, A>): SuperLineServer<C, A>;
520
+ 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>>;
363
521
 
364
- 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 };
522
+ 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). */
@@ -228,16 +236,26 @@ interface ServerStoreHandle {
228
236
  create(id: string, data: unknown, accessRules: AccessRules): Promise<void>;
229
237
  /** Read a Resource (data + accessRules), or undefined if absent. */
230
238
  read(id: string): Promise<Resource | undefined>;
231
- /** Server co-write: replace the Resource's value (LWW), fanned out to subscribers with a `server` origin. */
232
- write(id: string, data: unknown): Promise<void>;
239
+ /**
240
+ * Server co-write: replace the Resource's value (LWW), fanned out to subscribers stamped with `origin`
241
+ * (default `"server"`) for echo-break + inspector attribution — mirrors {@link ServerStore.open}'s origin.
242
+ */
243
+ write(id: string, data: unknown, opts?: {
244
+ origin?: string;
245
+ }): Promise<void>;
233
246
  /** Grant a principal read/write on a Resource. */
234
247
  grant(id: string, principal: string, perms: Perms): Promise<void>;
235
248
  /** Revoke a principal's access to a Resource entirely. */
236
249
  revoke(id: string, principal: string): Promise<void>;
237
250
  /** Delete a Resource. */
238
251
  delete(id: string): Promise<void>;
239
- /** All Resource ids in this store. */
240
- 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[]>;
241
259
  /**
242
260
  * Open a reactive in-process co-writer over a Resource's canonical state — the server half's mirror of
243
261
  * `client.store(name).open(id)`. Reactive reads, merging `update`, and surgical `delete(path)` (the only
@@ -248,8 +266,156 @@ interface ServerStoreHandle {
248
266
  origin?: string;
249
267
  }): ServerReplica;
250
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
+ /** Union of the `clientToServer` keys handled across a plugin tuple `P` (subtracted from `implement`). */
277
+ type HandledKeys<P extends readonly SuperLinePlugin<any>[]> = P[number] extends SuperLinePlugin<infer S> ? keyof CtsOf<S> & string : never;
278
+ /** Remove the plugin-handled keys `HK` from every block (each role + `shared`) of a {@link Handlers} map. */
279
+ type SubtractHandlers<H, HK extends string> = {
280
+ [B in keyof H]: Omit<H[B], HK>;
281
+ };
282
+ /**
283
+ * A named, declarative bundle of runtime contributions registered on `plugins: [...]`. All fields
284
+ * are optional; a plugin ships as a pair (this server half + an optional client half). See ADR-0005.
285
+ * The optional type param `S` is the plugin's paired surface — its `handlers` compile against `S`, and
286
+ * `S`'s `clientToServer` keys are subtracted from the host's `implement()` obligation at compile time.
287
+ */
288
+ interface SuperLinePlugin<S extends Directional = {}> {
289
+ /** Unique among the server's plugins; a duplicate name throws at construction. */
290
+ name: string;
291
+ /**
292
+ * Node-local tap fired synchronously at each emit site with LIVE payload references (an observer
293
+ * must not mutate them). Reuses the {@link TapEvent} taxonomy; zero cost when no plugin taps. A
294
+ * throwing tap is isolated and routed to `onError` — it never fails the underlying operation.
295
+ */
296
+ onEvent?: (event: TapEvent) => void;
297
+ /** Middleware run before request/subscribe handlers, after the host chain, in plugin array order. */
298
+ use?: PluginMiddleware[];
299
+ /** Called once per accepted connection (multiplexed after the host's `onConnection`). */
300
+ onConnection?: (conn: Conn, ctx: unknown) => void;
301
+ /** Called when a connection closes (multiplexed after the host's `onDisconnect`). */
302
+ onDisconnect?: (conn: Conn, ctx: unknown, code: number) => void;
303
+ /** Receives any error thrown in middleware/handlers/hooks (multiplexed with the host's `onError`). */
304
+ onError?: (error: unknown, info: MiddlewareInfo) => void;
305
+ /**
306
+ * Request handlers for the plugin's paired surface `S`, built lazily with the {@link PluginContext}.
307
+ * Merged into dispatch under their method names; the host merges `S` into a role and these keys are
308
+ * subtracted from its `implement()` obligation. A key already handled by the host (or another plugin)
309
+ * throws at construction.
310
+ */
311
+ handlers?: (ctx: PluginContext) => HandlersFor<S>;
312
+ /**
313
+ * Server halves of Store pairs the plugin contributes, merged into the host's `stores` and reachable
314
+ * via `srv.store(name)`. A name colliding with a host store or another plugin's store throws at construction.
315
+ */
316
+ stores?: Record<string, ServerStore>;
317
+ /**
318
+ * A plugin-owned (reserved) connection class — its own role, handshake negotiation, and parallel contract,
319
+ * served over observer-invisible connections. See {@link PluginConnection}. (Phase 2.)
320
+ */
321
+ connection?: PluginConnection;
322
+ /**
323
+ * Imperative escape hatch, run once at construction with the plugin's {@link PluginContext}. Return
324
+ * an optional dispose function, called on `server.close()`. Use for wiring cluster-wide views from
325
+ * local taps + a plugin channel (the inspector's pattern), timers, or background subscriptions.
326
+ */
327
+ setup?: (ctx: PluginContext) => void | (() => void);
328
+ }
329
+ /**
330
+ * A plugin-owned connection class (ADR-0005 phase 2): a reserved role the transport negotiates (never one
331
+ * of the user contract's roles), dispatched against the plugin's own fixed `contract` — never merged into
332
+ * the user's. Matching conns are observer-invisible (excluded from conns/presence/heartbeat/user hooks).
333
+ * The inspector's Control-Center channel is one such class.
334
+ */
335
+ interface PluginConnection {
336
+ /** The reserved role; must be unique across the server (user roles + other reserved classes). */
337
+ role: string;
338
+ /** WebSocket subprotocol to advertise + match (browsers set this where they can't set headers). */
339
+ subprotocol?: string;
340
+ /** Predicate for transports without a subprotocol (SSE/libp2p): match on the normalized handshake. */
341
+ match?: (handshake: Handshake) => boolean;
342
+ /** The parallel contract these connections speak (its `clientToServer` = requests, `subscribe` topics = feeds). */
343
+ contract: Contract;
344
+ /**
345
+ * Request handlers for `contract`'s `clientToServer`, built with the {@link PluginContext}. A subscribe to
346
+ * one of `contract`'s topics bridges the conn to the plugin's {@link PluginChannel} of the same name.
347
+ */
348
+ handlers?: (ctx: PluginContext) => Record<string, (input: unknown, conn: Conn) => Awaitable<unknown>>;
349
+ }
350
+ /** A plugin-private adapter channel (reserved `x:<plugin>:` prefix), fanned out cluster-wide. */
351
+ interface PluginChannel {
352
+ /** Publish to this channel; delivered to every node's subscribers (local echo included). */
353
+ publish(data: unknown): void;
354
+ /** Subscribe to this channel. `meta.from` is the publishing node. Returns an unsubscribe fn. */
355
+ subscribe(handler: (data: unknown, meta: BusMeta) => void): () => void;
356
+ }
357
+ /**
358
+ * The capabilities handed to a plugin's `setup`/`handlers`: the server's public surface minus the
359
+ * footguns (`implement`/`close`), plus a privileged block — a plugin-private adapter {@link PluginChannel},
360
+ * node identity, the serializer, a read-only conns view, and the raw contract for reflection. Sized to
361
+ * the inspector's audited needs; grows case-by-case.
362
+ */
363
+ interface PluginContext {
364
+ /** This node's stable id (equals `srv.nodeId`). */
365
+ readonly nodeId: string;
366
+ /** This node's friendly name (equals `srv.nodeName`). */
367
+ readonly nodeName: string;
368
+ /** Alias of {@link PluginContext.nodeId} — the per-process instance id used to tag cluster fan-out. */
369
+ readonly instanceId: string;
370
+ /** The wire serializer configured on the server. */
371
+ readonly serializer: Serializer;
372
+ /** The raw contract, for reflection (e.g. `classifyContract`). */
373
+ readonly contract: Contract;
374
+ /** Connections accepted on THIS node (read-only snapshot; excludes reserved conns). */
375
+ readonly conns: readonly Conn[];
376
+ /** Node-local introspection (connections, rooms, topics on this process). */
377
+ readonly local: LocalView;
378
+ /** Cluster-wide presence introspection (rejects without a presence-capable adapter). */
379
+ readonly cluster: ClusterView;
380
+ /** Whether a user (by `identify` key) has at least one live connection anywhere. */
381
+ isOnline(userId: string): Promise<boolean>;
382
+ /** Publish a shared topic (server-only publish). */
383
+ publish(topic: string, data: unknown): void;
384
+ /** Subscribe server-side to a shared topic, cluster-wide (local echo). Returns an unsubscribe fn. */
385
+ subscribe(topic: string, handler: (data: unknown, meta: BusMeta) => void): () => void;
386
+ /** Target a single connection by id, on whatever node holds it. */
387
+ toConn(id: string): {
388
+ emit(event: string, data: unknown): void;
389
+ close(): void;
390
+ };
391
+ /** Target all of a user's connections across nodes. */
392
+ toUser(userId: string): {
393
+ emit(event: string, data: unknown): void;
394
+ disconnect(): void;
395
+ };
396
+ /** Server-controlled room membership + broadcast (loosely typed, mirroring toConn/toUser). */
397
+ room(name: string): {
398
+ add(conn: Conn): void;
399
+ remove(conn: Conn): void;
400
+ broadcast(event: string, data: unknown): void;
401
+ readonly size: number;
402
+ readonly connections: readonly Conn[];
403
+ };
404
+ /** Server-authoritative handle for a configured store (incl. plugin-contributed stores). */
405
+ store(name: string): ServerStoreHandle;
406
+ /** Configured stores (host + plugin) and their models. */
407
+ storeInfos(): StoreInfo[];
408
+ /** Full cluster descriptor for a local connection (identity + rooms + `describeConn` extras). */
409
+ describe(conn: Conn): ConnDescriptor;
410
+ /** A connection's descriptor anywhere in the cluster (rejects without presence support); undefined if absent. */
411
+ connectionById(id: string): Promise<ConnDescriptor | undefined>;
412
+ /** A plugin-private, cluster-wide adapter channel under the reserved `x:<plugin>:` prefix. */
413
+ channel(name: string): PluginChannel;
414
+ }
251
415
  /** Options for {@link createSuperLineServer}. */
252
- interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>> {
416
+ interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>, P extends readonly SuperLinePlugin<any>[] = readonly SuperLinePlugin<any>[]> {
417
+ /** Named runtime bundles (taps, middleware, lifecycle, handlers, stores). See {@link SuperLinePlugin}. */
418
+ plugins?: P;
253
419
  /** Client↔server transports to accept connections on (e.g. `webSocketServerTransport({ server })`). */
254
420
  transports: ServerTransport[];
255
421
  /** Wire serializer; MUST match the client. Defaults to `jsonSerializer`. */
@@ -281,14 +447,6 @@ interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>> {
281
447
  interval?: number;
282
448
  maxMissed?: number;
283
449
  } | false;
284
- /**
285
- * Enable the read-only Control Center inspector: emit `msg.*` telemetry and accept inspector
286
- * clients. The WS transport must also be created with `inspector: true` to negotiate the
287
- * `superline.inspector.v1` subprotocol. **Default off; dev / trusted-network only.**
288
- */
289
- inspector?: boolean | {
290
- redact?: string[];
291
- };
292
450
  /**
293
451
  * Pluggable persisted-state Stores, keyed by name (`{ scene: crdtStoreServer(), config: memoryStoreServer() }`).
294
452
  * Each is the server half of a Store pair; the client passes the matching client halves. Surfaced as
@@ -303,7 +461,7 @@ interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>> {
303
461
  onError?: (error: unknown, info: MiddlewareInfo) => void;
304
462
  }
305
463
  /** A running super-line server, returned by {@link createSuperLineServer}. */
306
- interface SuperLineServer<C extends Contract, A extends AuthResult<C>> {
464
+ interface SuperLineServer<C extends Contract, A extends AuthResult<C>, HK extends string = never> {
307
465
  /** This node's stable id (unique per server process). */
308
466
  readonly nodeId: string;
309
467
  /** This node's friendly name (from `nodeName`/`SUPER_LINE_NODE_NAME`, else a short `nodeId` slice). */
@@ -318,8 +476,8 @@ interface SuperLineServer<C extends Contract, A extends AuthResult<C>> {
318
476
  toConn(id: string): ConnTarget<C>;
319
477
  /** Target all of a user's connections (by `identify` key) across nodes (emit/disconnect). */
320
478
  toUser(userId: string): UserTarget<C>;
321
- /** Register handlers for shared + per-role requests (chainable). */
322
- implement(handlers: Handlers<C, A>): SuperLineServer<C, A>;
479
+ /** Register handlers for shared + per-role requests (chainable). Keys handled by a plugin (`HK`) are subtracted. */
480
+ implement(handlers: SubtractHandlers<Handlers<C, A>, HK>): SuperLineServer<C, A, HK>;
323
481
  /** Mixed-role connection group; broadcast() sends a shared contract event to members. */
324
482
  room(name: string): Room<C>;
325
483
  /** Publish a SHARED topic to all subscribers (server-only publish). */
@@ -359,6 +517,6 @@ interface SuperLineServer<C extends Contract, A extends AuthResult<C>> {
359
517
  * })
360
518
  * ```
361
519
  */
362
- declare function createSuperLineServer<C extends Contract, A extends AuthResult<C>>(contract: C, opts: SuperLineServerOptions<C, A>): SuperLineServer<C, A>;
520
+ 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>>;
363
521
 
364
- 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 };
522
+ 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 };