@super-line/server 0.10.1 → 0.11.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, 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';
1
+ import { ServerMessageDef, RawConn, Serializer, ServerFrame, EmitData, Expr, CollectionQuery, CrdtServerReplica, DocListOpts, DocSummary, ConnDescriptor, Adapter, PresenceStore, Contract, RoleOf, NodeStat, SharedEvents, SharedServerRequests, ClientInput, Output, Directional, TapEvent, CtsOf, ServerInput, Handshake, SharedRequests, AnyData, RoleRequests, Events, DataOf, RoleTopics, SharedTopics, EventData, CollectionName, CrdtCollectionName, DocOf, RowOf, ServerTransport, CollectionStore, CrdtCollectionStore } from '@super-line/core';
2
2
 
3
3
  /**
4
4
  * A single client connection, passed to handlers as the third argument.
@@ -65,6 +65,76 @@ declare class Conn<Ev = Record<string, ServerMessageDef>, Ctx = unknown, Role ex
65
65
  */
66
66
  declare function resolvePrincipal(conn: Conn, identify?: (conn: Conn) => string | undefined): string;
67
67
 
68
+ type Awaitable$1<T> = T | Promise<T>;
69
+ /** The three row mutations a write policy guards. */
70
+ type WriteOp = 'insert' | 'update' | 'delete';
71
+ /**
72
+ * Row-security policy for one collection (see ADR-0006, decision 7). Server-side only — callbacks, never
73
+ * serialized to clients. **Deny-by-default**: omit `read` and clients cannot read the collection at all;
74
+ * omit `write` and clients cannot write it. Server co-writes via `srv.collection(n)` bypass both.
75
+ */
76
+ interface CollectionPolicy<Ctx = unknown, Row = unknown> {
77
+ /**
78
+ * A caller's visibility filter, ANDed into every snapshot, subscription, and change-route for that caller.
79
+ * Return `undefined` for "no filter" (the whole collection is visible). Return an {@link Expr} to restrict.
80
+ * Caveat: it is evaluated at subscribe time; principal-side state captured here (e.g. the caller's channel
81
+ * list) goes stale until the client resubscribes — row-side predicates re-evaluate on every change naturally.
82
+ */
83
+ read?: (principal: string, ctx: Ctx) => Awaitable$1<Expr | undefined>;
84
+ /**
85
+ * Per-row write guard. `next` is the incoming row (absent on delete), `prev` the current row (absent on
86
+ * insert). Return `false` to reject the op — which aborts the whole atomic batch it belongs to.
87
+ */
88
+ write?: (principal: string, op: WriteOp, next: Row | undefined, prev: Row | undefined, ctx: Ctx) => Awaitable$1<boolean>;
89
+ }
90
+ /**
91
+ * Server-authoritative handle for a collection (`srv.collection('messages')`). Writes bypass row policy
92
+ * (server is authoritative) but are still schema-validated, fan out, and — under relay — replicate across
93
+ * nodes exactly like a client batch. The door for business-logic mutations that a contract request handler owns.
94
+ */
95
+ interface ServerCollectionHandle<Row = unknown> {
96
+ /** Insert a row (its `key` field becomes the id). Throws `CONFLICT` if the id already exists. */
97
+ insert(row: Row): Promise<void>;
98
+ /** Replace a row by its `key` (LWW). Throws `NOT_FOUND` if absent. */
99
+ update(row: Row): Promise<void>;
100
+ /** Delete a row by id (idempotent). */
101
+ delete(id: string): Promise<void>;
102
+ /** Read one row by id, or undefined. */
103
+ read(id: string): Promise<Row | undefined>;
104
+ /** Materialize a snapshot (filter/sort/limit); omit the query for the whole collection. Server-side, policy-free. */
105
+ snapshot(query?: CollectionQuery): Promise<Row[]>;
106
+ }
107
+ /**
108
+ * Access policy for a CRDT document collection (ADR-0007, Q7). Guard-shaped — not the RLS filter shape of
109
+ * {@link CollectionPolicy}, because CRDT collections are opened by id (no subset to filter). **Deny-by-default**.
110
+ * Server co-writes via `srv.collection(n)` bypass both.
111
+ */
112
+ interface CrdtCollectionPolicy<Ctx = unknown, Doc = unknown> {
113
+ /** May this principal open `id`? Gets the post-merge plaintext `snapshot` (content-based auth like RLS reading a row). */
114
+ read?: (principal: string, id: string, snapshot: Doc | undefined, ctx: Ctx) => Awaitable$1<boolean>;
115
+ /** May this principal write to `id`? (Creation is server-authoritative — Q10 — so there is no `create` op here.) */
116
+ write?: (principal: string, id: string, ctx: Ctx) => Awaitable$1<boolean>;
117
+ }
118
+ /**
119
+ * Server-authoritative handle for a CRDT document collection (`srv.collection('scenes')`). Creation is
120
+ * server-only (Q10): `create` validates the initial doc and materializes it; `open` returns a reactive
121
+ * co-writer whose mutations fan out like a relayed client delta. Policy-free (server is authoritative).
122
+ */
123
+ interface ServerCrdtCollectionHandle<Doc = unknown> {
124
+ /** Create a document with pre-validated initial data. Throws `CONFLICT` if the id exists. */
125
+ create(id: string, data: Doc): Promise<void>;
126
+ /** Open a reactive co-writer over an existing document. `origin` (default `"server"`) attributes its writes. */
127
+ open(id: string, opts?: {
128
+ origin?: string;
129
+ }): CrdtServerReplica;
130
+ /** Read the current plaintext snapshot of a document, or undefined if absent. */
131
+ read(id: string): Promise<Doc | undefined>;
132
+ /** Delete a document (idempotent), fanning the deletion cluster-wide. */
133
+ delete(id: string): Promise<void>;
134
+ /** Enumerate document ids + summaries (no content query). */
135
+ list(opts?: DocListOpts): Promise<DocSummary[]>;
136
+ }
137
+
68
138
  /**
69
139
  * In-process pub/sub bus. Share one bus across multiple servers to simulate
70
140
  * multiple nodes in a test (each server gets its own adapter bound to the bus).
@@ -226,46 +296,6 @@ interface RoleLens<C extends Contract, R extends RoleOf<C>> {
226
296
  /** Publish to a topic in role `R`'s surface (reaches that role's subscribers). */
227
297
  publish<T extends keyof RoleTopics<C, R>>(topic: T, data: EmitData<RoleTopics<C, R>[T]>): void;
228
298
  }
229
- /**
230
- * Server-side handle for one configured Store, reached via `srv.store.<name>`. The server is
231
- * authoritative: it creates Resources, grants/revokes access, and may co-write. `data` is untyped
232
- * (stores are off-contract — see ADR-0003); callers assert the shape.
233
- */
234
- interface ServerStoreHandle {
235
- /** Create a Resource with initial data + access rules (deny-by-default for everyone unlisted). */
236
- create(id: string, data: unknown, accessRules: AccessRules): Promise<void>;
237
- /** Read a Resource (data + accessRules), or undefined if absent. */
238
- read(id: string): Promise<Resource | undefined>;
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>;
246
- /** Grant a principal read/write on a Resource. */
247
- grant(id: string, principal: string, perms: Perms): Promise<void>;
248
- /** Revoke a principal's access to a Resource entirely. */
249
- revoke(id: string, principal: string): Promise<void>;
250
- /** Delete a Resource. */
251
- delete(id: string): Promise<void>;
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[]>;
259
- /**
260
- * Open a reactive in-process co-writer over a Resource's canonical state — the server half's mirror of
261
- * `client.store(name).open(id)`. Reactive reads, merging `update`, and surgical `delete(path)` (the only
262
- * way to remove a key server-side), all server-authoritative and fanned out to subscribers. `origin`
263
- * (default `"server"`) tags its Changes for inspector attribution. Throws if the store has no `open`.
264
- */
265
- open(id: string, opts?: {
266
- origin?: string;
267
- }): ServerReplica;
268
- }
269
299
  /**
270
300
  * Handlers a plugin provides for its paired surface `S` (ADR-0004): one per `clientToServer` key,
271
301
  * typed from `S`. `ctx`/`conn` are loose — a plugin is written independently of the host's per-role ctx.
@@ -277,9 +307,15 @@ type HandlersFor<S extends Directional> = {
277
307
  type PluginHandledKeys<U> = U extends SuperLinePlugin<infer S> ? keyof CtsOf<S> & string : never;
278
308
  /** Union of the `clientToServer` keys handled across a plugin tuple `P` (subtracted from `implement`). */
279
309
  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. */
310
+ /**
311
+ * Remove the plugin-handled keys `HK` from every block (each role + `shared`) of a {@link Handlers} map. A block
312
+ * a plugin fully owns collapses to `{}` and becomes OPTIONAL — so a host needn't pass an empty `shared: {}` /
313
+ * `guest: {}` for a role or shared surface a plugin handles entirely.
314
+ */
281
315
  type SubtractHandlers<H, HK extends string> = {
282
- [B in keyof H]: Omit<H[B], HK>;
316
+ [B in keyof H as [keyof Omit<H[B], HK>] extends [never] ? never : B]: Omit<H[B], HK>;
317
+ } & {
318
+ [B in keyof H as [keyof Omit<H[B], HK>] extends [never] ? B : never]?: Omit<H[B], HK>;
283
319
  };
284
320
  /**
285
321
  * A named, declarative bundle of runtime contributions registered on `plugins: [...]`. All fields
@@ -312,10 +348,13 @@ interface SuperLinePlugin<S extends Directional = {}> {
312
348
  */
313
349
  handlers?: (ctx: PluginContext) => HandlersFor<S>;
314
350
  /**
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.
351
+ * Row-security policies for the collections the plugin's contract fragment declares (see {@link CollectionPolicy}).
352
+ * Merged into the host's `policies`; a collection already policied by the host or another plugin throws at
353
+ * construction, and a policy for a collection no fragment declared throws too. Deny-by-default still holds —
354
+ * a collection nobody policies is server-only. Lets a plugin ship its collections locked down (e.g. deny-all
355
+ * on secret tables) without the host hand-spreading them.
317
356
  */
318
- stores?: Record<string, ServerStore>;
357
+ policies?: Record<string, CollectionPolicy<unknown, unknown>>;
319
358
  /**
320
359
  * A plugin-owned (reserved) connection class — its own role, handshake negotiation, and parallel contract,
321
360
  * served over observer-invisible connections. See {@link PluginConnection}. (Phase 2.)
@@ -403,10 +442,14 @@ interface PluginContext {
403
442
  readonly size: number;
404
443
  readonly connections: readonly Conn[];
405
444
  };
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[];
445
+ /** Server-authoritative handle for a contract collection (loosely typed here as the LWW row handle — the surface plugins/inspector use); throws if none is configured. */
446
+ collection(name: string): ServerCollectionHandle;
447
+ /** Declared collections (name + key + advisory references) for the schema graph. */
448
+ collectionInfos(): {
449
+ name: string;
450
+ key: string;
451
+ references: Record<string, string>;
452
+ }[];
410
453
  /** Full cluster descriptor for a local connection (identity + rooms + `describeConn` extras). */
411
454
  describe(conn: Conn): ConnDescriptor;
412
455
  /** A connection's descriptor anywhere in the cluster (rejects without presence support); undefined if absent. */
@@ -450,11 +493,32 @@ interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>, P
450
493
  maxMissed?: number;
451
494
  } | false;
452
495
  /**
453
- * Pluggable persisted-state Stores, keyed by name (`{ scene: crdtStoreServer(), config: memoryStoreServer() }`).
454
- * Each is the server half of a Store pair; the client passes the matching client halves. Surfaced as
455
- * `srv.store.<name>` and `client.store.<name>`. Stores are off-contract and untyped (ADR-0003).
496
+ * The single {@link CollectionStore} backend serving every collection this contract declares (typed rows;
497
+ * ADR-0006), e.g. `collections: memoryCollections()`. One backend one transaction domain, so a
498
+ * cross-collection batch commits atomically.
499
+ */
500
+ collections?: CollectionStore;
501
+ /**
502
+ * The {@link CrdtCollectionStore} backend serving this contract's CRDT document collections (ADR-0007),
503
+ * e.g. `crdtCollections: crdtMemoryCollections()`. A backend per family: CRDT docs never join a
504
+ * cross-collection atomic batch, so they get their own backend, separate from `collections`.
505
+ */
506
+ crdtCollections?: CrdtCollectionStore;
507
+ /**
508
+ * Access policies per collection. Deny-by-default: a collection with no policy is server-only. LWW row
509
+ * collections take an RLS-style {@link CollectionPolicy} (read → filter); CRDT document collections take a
510
+ * guard-shaped {@link CrdtCollectionPolicy} (read → bool). Server co-writes via `srv.collection(n)` bypass them.
456
511
  */
457
- stores?: Record<string, ServerStore>;
512
+ policies?: {
513
+ [N in CollectionName<C>]?: N extends CrdtCollectionName<C> ? CrdtCollectionPolicy<CtxUnion<A>, DocOf<C, N>> : CollectionPolicy<CtxUnion<A>, RowOf<C, N>>;
514
+ };
515
+ /**
516
+ * Opt-in advisory foreign-key checks (see ADR-0006, decision 8). When true, an insert/update whose
517
+ * `references` field points at a non-existent row is rejected at the accepting node. Best-effort under
518
+ * relay clustering (no global serialization point) and does NOT resolve intra-batch parent-then-child
519
+ * references; there are no cascades. Off by default.
520
+ */
521
+ checkReferences?: boolean;
458
522
  /** Called once per accepted connection. */
459
523
  onConnection?: (conn: Conn, ctx: CtxUnion<A>) => void;
460
524
  /** Called when a connection closes, with the WebSocket close `code`. */
@@ -493,8 +557,13 @@ interface SuperLineServer<C extends Contract, A extends AuthResult<C>, HK extend
493
557
  subscribe<T extends keyof SharedTopics<C>>(topic: T, handler: (data: EventData<SharedTopics<C>[T]>, meta: BusMeta) => void): () => void;
494
558
  /** Lens for role-scoped sends, e.g. forRole('user').publish('feed', data). */
495
559
  forRole<R extends RoleOf<C>>(role: R): RoleLens<C, R>;
496
- /** Server-authoritative handle for a configured Store (`srv.store('scene').create(...)`). Throws `NOT_FOUND` if the name isn't configured. */
497
- store(name: string): ServerStoreHandle;
560
+ /**
561
+ * Server-authoritative handle for a contract collection, typed by the contract: an LWW row collection gives
562
+ * `ServerCollectionHandle` (`insert`/`update`/`batch`), a CRDT document collection gives
563
+ * `ServerCrdtCollectionHandle` (`create`/`open`). Throws `NOT_FOUND` if no matching backend is configured
564
+ * or the name isn't declared.
565
+ */
566
+ collection<N extends CollectionName<C>>(name: N): N extends CrdtCollectionName<C> ? ServerCrdtCollectionHandle<DocOf<C, N>> : ServerCollectionHandle<RowOf<C, N>>;
498
567
  close(): Promise<void>;
499
568
  }
500
569
  /**
@@ -521,4 +590,4 @@ interface SuperLineServer<C extends Contract, A extends AuthResult<C>, HK extend
521
590
  */
522
591
  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>>;
523
592
 
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 };
593
+ export { type AuthResult, type BusMeta, type ClusterView, type CollectionPolicy, Conn, type ConnTarget, type CrdtCollectionPolicy, 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 ServerCollectionHandle, type ServerCrdtCollectionHandle, type SubtractHandlers, type SuperLinePlugin, type SuperLineServer, type SuperLineServerOptions, type UserTarget, type WriteOp, 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, 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';
1
+ import { ServerMessageDef, RawConn, Serializer, ServerFrame, EmitData, Expr, CollectionQuery, CrdtServerReplica, DocListOpts, DocSummary, ConnDescriptor, Adapter, PresenceStore, Contract, RoleOf, NodeStat, SharedEvents, SharedServerRequests, ClientInput, Output, Directional, TapEvent, CtsOf, ServerInput, Handshake, SharedRequests, AnyData, RoleRequests, Events, DataOf, RoleTopics, SharedTopics, EventData, CollectionName, CrdtCollectionName, DocOf, RowOf, ServerTransport, CollectionStore, CrdtCollectionStore } from '@super-line/core';
2
2
 
3
3
  /**
4
4
  * A single client connection, passed to handlers as the third argument.
@@ -65,6 +65,76 @@ declare class Conn<Ev = Record<string, ServerMessageDef>, Ctx = unknown, Role ex
65
65
  */
66
66
  declare function resolvePrincipal(conn: Conn, identify?: (conn: Conn) => string | undefined): string;
67
67
 
68
+ type Awaitable$1<T> = T | Promise<T>;
69
+ /** The three row mutations a write policy guards. */
70
+ type WriteOp = 'insert' | 'update' | 'delete';
71
+ /**
72
+ * Row-security policy for one collection (see ADR-0006, decision 7). Server-side only — callbacks, never
73
+ * serialized to clients. **Deny-by-default**: omit `read` and clients cannot read the collection at all;
74
+ * omit `write` and clients cannot write it. Server co-writes via `srv.collection(n)` bypass both.
75
+ */
76
+ interface CollectionPolicy<Ctx = unknown, Row = unknown> {
77
+ /**
78
+ * A caller's visibility filter, ANDed into every snapshot, subscription, and change-route for that caller.
79
+ * Return `undefined` for "no filter" (the whole collection is visible). Return an {@link Expr} to restrict.
80
+ * Caveat: it is evaluated at subscribe time; principal-side state captured here (e.g. the caller's channel
81
+ * list) goes stale until the client resubscribes — row-side predicates re-evaluate on every change naturally.
82
+ */
83
+ read?: (principal: string, ctx: Ctx) => Awaitable$1<Expr | undefined>;
84
+ /**
85
+ * Per-row write guard. `next` is the incoming row (absent on delete), `prev` the current row (absent on
86
+ * insert). Return `false` to reject the op — which aborts the whole atomic batch it belongs to.
87
+ */
88
+ write?: (principal: string, op: WriteOp, next: Row | undefined, prev: Row | undefined, ctx: Ctx) => Awaitable$1<boolean>;
89
+ }
90
+ /**
91
+ * Server-authoritative handle for a collection (`srv.collection('messages')`). Writes bypass row policy
92
+ * (server is authoritative) but are still schema-validated, fan out, and — under relay — replicate across
93
+ * nodes exactly like a client batch. The door for business-logic mutations that a contract request handler owns.
94
+ */
95
+ interface ServerCollectionHandle<Row = unknown> {
96
+ /** Insert a row (its `key` field becomes the id). Throws `CONFLICT` if the id already exists. */
97
+ insert(row: Row): Promise<void>;
98
+ /** Replace a row by its `key` (LWW). Throws `NOT_FOUND` if absent. */
99
+ update(row: Row): Promise<void>;
100
+ /** Delete a row by id (idempotent). */
101
+ delete(id: string): Promise<void>;
102
+ /** Read one row by id, or undefined. */
103
+ read(id: string): Promise<Row | undefined>;
104
+ /** Materialize a snapshot (filter/sort/limit); omit the query for the whole collection. Server-side, policy-free. */
105
+ snapshot(query?: CollectionQuery): Promise<Row[]>;
106
+ }
107
+ /**
108
+ * Access policy for a CRDT document collection (ADR-0007, Q7). Guard-shaped — not the RLS filter shape of
109
+ * {@link CollectionPolicy}, because CRDT collections are opened by id (no subset to filter). **Deny-by-default**.
110
+ * Server co-writes via `srv.collection(n)` bypass both.
111
+ */
112
+ interface CrdtCollectionPolicy<Ctx = unknown, Doc = unknown> {
113
+ /** May this principal open `id`? Gets the post-merge plaintext `snapshot` (content-based auth like RLS reading a row). */
114
+ read?: (principal: string, id: string, snapshot: Doc | undefined, ctx: Ctx) => Awaitable$1<boolean>;
115
+ /** May this principal write to `id`? (Creation is server-authoritative — Q10 — so there is no `create` op here.) */
116
+ write?: (principal: string, id: string, ctx: Ctx) => Awaitable$1<boolean>;
117
+ }
118
+ /**
119
+ * Server-authoritative handle for a CRDT document collection (`srv.collection('scenes')`). Creation is
120
+ * server-only (Q10): `create` validates the initial doc and materializes it; `open` returns a reactive
121
+ * co-writer whose mutations fan out like a relayed client delta. Policy-free (server is authoritative).
122
+ */
123
+ interface ServerCrdtCollectionHandle<Doc = unknown> {
124
+ /** Create a document with pre-validated initial data. Throws `CONFLICT` if the id exists. */
125
+ create(id: string, data: Doc): Promise<void>;
126
+ /** Open a reactive co-writer over an existing document. `origin` (default `"server"`) attributes its writes. */
127
+ open(id: string, opts?: {
128
+ origin?: string;
129
+ }): CrdtServerReplica;
130
+ /** Read the current plaintext snapshot of a document, or undefined if absent. */
131
+ read(id: string): Promise<Doc | undefined>;
132
+ /** Delete a document (idempotent), fanning the deletion cluster-wide. */
133
+ delete(id: string): Promise<void>;
134
+ /** Enumerate document ids + summaries (no content query). */
135
+ list(opts?: DocListOpts): Promise<DocSummary[]>;
136
+ }
137
+
68
138
  /**
69
139
  * In-process pub/sub bus. Share one bus across multiple servers to simulate
70
140
  * multiple nodes in a test (each server gets its own adapter bound to the bus).
@@ -226,46 +296,6 @@ interface RoleLens<C extends Contract, R extends RoleOf<C>> {
226
296
  /** Publish to a topic in role `R`'s surface (reaches that role's subscribers). */
227
297
  publish<T extends keyof RoleTopics<C, R>>(topic: T, data: EmitData<RoleTopics<C, R>[T]>): void;
228
298
  }
229
- /**
230
- * Server-side handle for one configured Store, reached via `srv.store.<name>`. The server is
231
- * authoritative: it creates Resources, grants/revokes access, and may co-write. `data` is untyped
232
- * (stores are off-contract — see ADR-0003); callers assert the shape.
233
- */
234
- interface ServerStoreHandle {
235
- /** Create a Resource with initial data + access rules (deny-by-default for everyone unlisted). */
236
- create(id: string, data: unknown, accessRules: AccessRules): Promise<void>;
237
- /** Read a Resource (data + accessRules), or undefined if absent. */
238
- read(id: string): Promise<Resource | undefined>;
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>;
246
- /** Grant a principal read/write on a Resource. */
247
- grant(id: string, principal: string, perms: Perms): Promise<void>;
248
- /** Revoke a principal's access to a Resource entirely. */
249
- revoke(id: string, principal: string): Promise<void>;
250
- /** Delete a Resource. */
251
- delete(id: string): Promise<void>;
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[]>;
259
- /**
260
- * Open a reactive in-process co-writer over a Resource's canonical state — the server half's mirror of
261
- * `client.store(name).open(id)`. Reactive reads, merging `update`, and surgical `delete(path)` (the only
262
- * way to remove a key server-side), all server-authoritative and fanned out to subscribers. `origin`
263
- * (default `"server"`) tags its Changes for inspector attribution. Throws if the store has no `open`.
264
- */
265
- open(id: string, opts?: {
266
- origin?: string;
267
- }): ServerReplica;
268
- }
269
299
  /**
270
300
  * Handlers a plugin provides for its paired surface `S` (ADR-0004): one per `clientToServer` key,
271
301
  * typed from `S`. `ctx`/`conn` are loose — a plugin is written independently of the host's per-role ctx.
@@ -277,9 +307,15 @@ type HandlersFor<S extends Directional> = {
277
307
  type PluginHandledKeys<U> = U extends SuperLinePlugin<infer S> ? keyof CtsOf<S> & string : never;
278
308
  /** Union of the `clientToServer` keys handled across a plugin tuple `P` (subtracted from `implement`). */
279
309
  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. */
310
+ /**
311
+ * Remove the plugin-handled keys `HK` from every block (each role + `shared`) of a {@link Handlers} map. A block
312
+ * a plugin fully owns collapses to `{}` and becomes OPTIONAL — so a host needn't pass an empty `shared: {}` /
313
+ * `guest: {}` for a role or shared surface a plugin handles entirely.
314
+ */
281
315
  type SubtractHandlers<H, HK extends string> = {
282
- [B in keyof H]: Omit<H[B], HK>;
316
+ [B in keyof H as [keyof Omit<H[B], HK>] extends [never] ? never : B]: Omit<H[B], HK>;
317
+ } & {
318
+ [B in keyof H as [keyof Omit<H[B], HK>] extends [never] ? B : never]?: Omit<H[B], HK>;
283
319
  };
284
320
  /**
285
321
  * A named, declarative bundle of runtime contributions registered on `plugins: [...]`. All fields
@@ -312,10 +348,13 @@ interface SuperLinePlugin<S extends Directional = {}> {
312
348
  */
313
349
  handlers?: (ctx: PluginContext) => HandlersFor<S>;
314
350
  /**
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.
351
+ * Row-security policies for the collections the plugin's contract fragment declares (see {@link CollectionPolicy}).
352
+ * Merged into the host's `policies`; a collection already policied by the host or another plugin throws at
353
+ * construction, and a policy for a collection no fragment declared throws too. Deny-by-default still holds —
354
+ * a collection nobody policies is server-only. Lets a plugin ship its collections locked down (e.g. deny-all
355
+ * on secret tables) without the host hand-spreading them.
317
356
  */
318
- stores?: Record<string, ServerStore>;
357
+ policies?: Record<string, CollectionPolicy<unknown, unknown>>;
319
358
  /**
320
359
  * A plugin-owned (reserved) connection class — its own role, handshake negotiation, and parallel contract,
321
360
  * served over observer-invisible connections. See {@link PluginConnection}. (Phase 2.)
@@ -403,10 +442,14 @@ interface PluginContext {
403
442
  readonly size: number;
404
443
  readonly connections: readonly Conn[];
405
444
  };
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[];
445
+ /** Server-authoritative handle for a contract collection (loosely typed here as the LWW row handle — the surface plugins/inspector use); throws if none is configured. */
446
+ collection(name: string): ServerCollectionHandle;
447
+ /** Declared collections (name + key + advisory references) for the schema graph. */
448
+ collectionInfos(): {
449
+ name: string;
450
+ key: string;
451
+ references: Record<string, string>;
452
+ }[];
410
453
  /** Full cluster descriptor for a local connection (identity + rooms + `describeConn` extras). */
411
454
  describe(conn: Conn): ConnDescriptor;
412
455
  /** A connection's descriptor anywhere in the cluster (rejects without presence support); undefined if absent. */
@@ -450,11 +493,32 @@ interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>, P
450
493
  maxMissed?: number;
451
494
  } | false;
452
495
  /**
453
- * Pluggable persisted-state Stores, keyed by name (`{ scene: crdtStoreServer(), config: memoryStoreServer() }`).
454
- * Each is the server half of a Store pair; the client passes the matching client halves. Surfaced as
455
- * `srv.store.<name>` and `client.store.<name>`. Stores are off-contract and untyped (ADR-0003).
496
+ * The single {@link CollectionStore} backend serving every collection this contract declares (typed rows;
497
+ * ADR-0006), e.g. `collections: memoryCollections()`. One backend one transaction domain, so a
498
+ * cross-collection batch commits atomically.
499
+ */
500
+ collections?: CollectionStore;
501
+ /**
502
+ * The {@link CrdtCollectionStore} backend serving this contract's CRDT document collections (ADR-0007),
503
+ * e.g. `crdtCollections: crdtMemoryCollections()`. A backend per family: CRDT docs never join a
504
+ * cross-collection atomic batch, so they get their own backend, separate from `collections`.
505
+ */
506
+ crdtCollections?: CrdtCollectionStore;
507
+ /**
508
+ * Access policies per collection. Deny-by-default: a collection with no policy is server-only. LWW row
509
+ * collections take an RLS-style {@link CollectionPolicy} (read → filter); CRDT document collections take a
510
+ * guard-shaped {@link CrdtCollectionPolicy} (read → bool). Server co-writes via `srv.collection(n)` bypass them.
456
511
  */
457
- stores?: Record<string, ServerStore>;
512
+ policies?: {
513
+ [N in CollectionName<C>]?: N extends CrdtCollectionName<C> ? CrdtCollectionPolicy<CtxUnion<A>, DocOf<C, N>> : CollectionPolicy<CtxUnion<A>, RowOf<C, N>>;
514
+ };
515
+ /**
516
+ * Opt-in advisory foreign-key checks (see ADR-0006, decision 8). When true, an insert/update whose
517
+ * `references` field points at a non-existent row is rejected at the accepting node. Best-effort under
518
+ * relay clustering (no global serialization point) and does NOT resolve intra-batch parent-then-child
519
+ * references; there are no cascades. Off by default.
520
+ */
521
+ checkReferences?: boolean;
458
522
  /** Called once per accepted connection. */
459
523
  onConnection?: (conn: Conn, ctx: CtxUnion<A>) => void;
460
524
  /** Called when a connection closes, with the WebSocket close `code`. */
@@ -493,8 +557,13 @@ interface SuperLineServer<C extends Contract, A extends AuthResult<C>, HK extend
493
557
  subscribe<T extends keyof SharedTopics<C>>(topic: T, handler: (data: EventData<SharedTopics<C>[T]>, meta: BusMeta) => void): () => void;
494
558
  /** Lens for role-scoped sends, e.g. forRole('user').publish('feed', data). */
495
559
  forRole<R extends RoleOf<C>>(role: R): RoleLens<C, R>;
496
- /** Server-authoritative handle for a configured Store (`srv.store('scene').create(...)`). Throws `NOT_FOUND` if the name isn't configured. */
497
- store(name: string): ServerStoreHandle;
560
+ /**
561
+ * Server-authoritative handle for a contract collection, typed by the contract: an LWW row collection gives
562
+ * `ServerCollectionHandle` (`insert`/`update`/`batch`), a CRDT document collection gives
563
+ * `ServerCrdtCollectionHandle` (`create`/`open`). Throws `NOT_FOUND` if no matching backend is configured
564
+ * or the name isn't declared.
565
+ */
566
+ collection<N extends CollectionName<C>>(name: N): N extends CrdtCollectionName<C> ? ServerCrdtCollectionHandle<DocOf<C, N>> : ServerCollectionHandle<RowOf<C, N>>;
498
567
  close(): Promise<void>;
499
568
  }
500
569
  /**
@@ -521,4 +590,4 @@ interface SuperLineServer<C extends Contract, A extends AuthResult<C>, HK extend
521
590
  */
522
591
  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>>;
523
592
 
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 };
593
+ export { type AuthResult, type BusMeta, type ClusterView, type CollectionPolicy, Conn, type ConnTarget, type CrdtCollectionPolicy, 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 ServerCollectionHandle, type ServerCrdtCollectionHandle, type SubtractHandlers, type SuperLinePlugin, type SuperLineServer, type SuperLineServerOptions, type UserTarget, type WriteOp, createInMemoryAdapter, createSuperLineServer, resolvePrincipal };