@super-line/server 0.3.0 → 0.5.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/README.md CHANGED
@@ -3,18 +3,19 @@
3
3
  The server for [**super-line**](https://mertdogar.github.io/super-line/) — end-to-end typesafe WebSockets for TypeScript. Implements a shared contract over [`ws`](https://www.npmjs.com/package/ws): role-keyed handlers, rooms, topics, middleware, lifecycle hooks, and node-to-node messaging.
4
4
 
5
5
  ```bash
6
- pnpm add @super-line/core @super-line/server zod
6
+ pnpm add @super-line/core @super-line/server @super-line/transport-websocket zod
7
7
  ```
8
8
 
9
9
  ```ts
10
10
  import http from 'node:http'
11
11
  import { createSuperLineServer } from '@super-line/server'
12
+ import { webSocketServerTransport } from '@super-line/transport-websocket'
12
13
  import { api } from './contract'
13
14
 
14
15
  const server = http.createServer()
15
16
  const srv = createSuperLineServer(api, {
16
- server,
17
- authenticate: (req) => ({ role: 'user' as const, ctx: { id: '1' } }), // throw -> 401
17
+ transports: [webSocketServerTransport({ server })],
18
+ authenticate: (h) => ({ role: 'user' as const, ctx: { id: '1' } }), // throw -> 401
18
19
  })
19
20
 
20
21
  srv.implement({
@@ -29,7 +30,7 @@ srv.implement({
29
30
  server.listen(3000)
30
31
  ```
31
32
 
32
- Authenticate returns `{ role, ctx }`; cross-role calls are rejected with `NOT_FOUND`. Scale across processes with [`@super-line/adapter-redis`](https://www.npmjs.com/package/@super-line/adapter-redis).
33
+ Authenticate receives the `Handshake` (`{ transport, headers, query, peer?, raw }`) and returns `{ role, ctx }`; cross-role calls are rejected with `NOT_FOUND`. The wire is carried by a pluggable transport — [`@super-line/transport-websocket`](https://www.npmjs.com/package/@super-line/transport-websocket) provides the WS transport shown above; other transports (HTTP/SSE, libp2p) are available — see the Transports guide. Scale across processes with [`@super-line/adapter-redis`](https://www.npmjs.com/package/@super-line/adapter-redis).
33
34
 
34
35
  - 📖 Docs: <https://mertdogar.github.io/super-line/>
35
36
  - 📚 Guides: [roles & auth](https://mertdogar.github.io/super-line/guide/roles-auth), [events & rooms](https://mertdogar.github.io/super-line/guide/events-rooms)
package/dist/index.cjs CHANGED
@@ -16272,36 +16272,38 @@ __export(index_exports, {
16272
16272
  Conn: () => Conn,
16273
16273
  MemoryBus: () => MemoryBus,
16274
16274
  createInMemoryAdapter: () => createInMemoryAdapter,
16275
- createSuperLineServer: () => createSuperLineServer
16275
+ createSuperLineServer: () => createSuperLineServer,
16276
+ resolvePrincipal: () => resolvePrincipal
16276
16277
  });
16277
16278
  module.exports = __toCommonJS(index_exports);
16278
16279
  var import_node_crypto = require("crypto");
16279
- var import_ws = require("ws");
16280
16280
  var import_core2 = require("@super-line/core");
16281
16281
 
16282
16282
  // src/conn.ts
16283
16283
  var Conn = class {
16284
- constructor(ws, id, role, ctx, serializer, backpressure, onEmit) {
16285
- this.ws = ws;
16284
+ constructor(raw, id, role, ctx, serializer, onEmit) {
16285
+ this.raw = raw;
16286
16286
  this.id = id;
16287
16287
  this.role = role;
16288
16288
  this.ctx = ctx;
16289
16289
  this.serializer = serializer;
16290
- this.backpressure = backpressure;
16291
16290
  this.onEmit = onEmit;
16292
16291
  }
16293
- ws;
16292
+ raw;
16294
16293
  id;
16295
16294
  role;
16296
16295
  ctx;
16297
16296
  serializer;
16298
- backpressure;
16299
16297
  onEmit;
16300
16298
  /** Namespaced channels (rooms + topics) this connection belongs to. */
16301
16299
  channels = /* @__PURE__ */ new Set();
16302
16300
  /** Mutable per-connection scratch state, typed per role by the contract's `data` schema. */
16303
16301
  data = {};
16304
- /** When this connection was accepted (`Date.now()` at the upgrade). */
16302
+ /** The client↔server transport (wire) this connection was accepted on (set by the server at accept). */
16303
+ transport;
16304
+ /** ACL identity for stores: `identify(conn) ?? conn.id`, set by the server at accept (always defined there). */
16305
+ principal;
16306
+ /** When this connection was accepted (`Date.now()`). */
16305
16307
  connectedAt = Date.now();
16306
16308
  /** When the server last sent a heartbeat ping to this connection (managed by the server). */
16307
16309
  lastPingAt;
@@ -16309,37 +16311,33 @@ var Conn = class {
16309
16311
  lastPongAt;
16310
16312
  /** Pings sent since the last pong; drives reaping (managed by the server). */
16311
16313
  missedPongs = 0;
16312
- // true => the frame was handled by the backpressure policy and must not be sent
16313
- overBackpressure() {
16314
- const bp = this.backpressure;
16315
- if (!bp || this.ws.bufferedAmount <= bp.maxBufferedBytes) return false;
16316
- if (bp.onExceed === "drop") {
16317
- console.warn(`[super-line] dropping frame: conn ${this.id} over backpressure limit`);
16318
- return true;
16319
- }
16320
- this.ws.close(1013);
16321
- return true;
16322
- }
16323
16314
  /** Encode and send a frame (unicast, e.g. req/res). */
16324
16315
  send(frame) {
16325
- if (this.ws.readyState !== this.ws.OPEN || this.overBackpressure()) return;
16326
- this.ws.send(this.serializer.encode(frame));
16316
+ if (!this.raw.writable) return;
16317
+ this.raw.send(this.serializer.encode(frame));
16327
16318
  }
16328
16319
  /** Forward an already-encoded frame (fan-out path; encoded once at the source). */
16329
16320
  sendRaw(payload) {
16330
- if (this.ws.readyState !== this.ws.OPEN || this.overBackpressure()) return;
16331
- this.ws.send(payload);
16321
+ if (!this.raw.writable) return;
16322
+ this.raw.send(payload);
16332
16323
  }
16333
16324
  /** Push an event to THIS connection (node-local). Scoped to the role's events. */
16334
16325
  emit(event, data) {
16335
16326
  this.onEmit?.(String(event), data);
16336
16327
  this.send({ t: "evt", e: String(event), d: data });
16337
16328
  }
16338
- /** Close the underlying socket. */
16329
+ /** Graceful close of the underlying transport connection. */
16339
16330
  close() {
16340
- this.ws.close();
16331
+ this.raw.close();
16332
+ }
16333
+ /** Hard close with no handshake — used by heartbeat reaping. */
16334
+ terminate() {
16335
+ this.raw.terminate();
16341
16336
  }
16342
16337
  };
16338
+ function resolvePrincipal(conn, identify) {
16339
+ return identify?.(conn) ?? conn.id;
16340
+ }
16343
16341
 
16344
16342
  // src/memory-adapter.ts
16345
16343
  var MemoryBus = class {
@@ -16456,6 +16454,8 @@ var CONN = "c:";
16456
16454
  var USER = "u:";
16457
16455
  var REPLY = "reply:";
16458
16456
  var INSPECT = "i:";
16457
+ var STORE = "s:";
16458
+ var SERVER_ORIGIN = "server";
16459
16459
  function createSuperLineServer(contract, opts) {
16460
16460
  const c = contract;
16461
16461
  const serializer = opts.serializer ?? import_core2.jsonSerializer;
@@ -16464,10 +16464,7 @@ function createSuperLineServer(contract, opts) {
16464
16464
  const inspectorRedact = new Set(
16465
16465
  opts.inspector && typeof opts.inspector === "object" ? opts.inspector.redact ?? [] : []
16466
16466
  );
16467
- const wss = new import_ws.WebSocketServer({
16468
- noServer: true,
16469
- handleProtocols: (protocols) => inspectorEnabled && protocols.has(import_core2.INSPECTOR_SUBPROTOCOL) ? import_core2.INSPECTOR_SUBPROTOCOL : false
16470
- });
16467
+ const storeMap = opts.stores ?? {};
16471
16468
  const conns = /* @__PURE__ */ new Set();
16472
16469
  const inspectorConns = /* @__PURE__ */ new Set();
16473
16470
  const members = /* @__PURE__ */ new Map();
@@ -16477,6 +16474,7 @@ function createSuperLineServer(contract, opts) {
16477
16474
  const replyChannel = REPLY + instanceId;
16478
16475
  let impl = {};
16479
16476
  let closing = false;
16477
+ let relaying = false;
16480
16478
  let nextSReq = 1;
16481
16479
  const originWaiters = /* @__PURE__ */ new Map();
16482
16480
  const ownerRouting = /* @__PURE__ */ new Map();
@@ -16492,6 +16490,7 @@ function createSuperLineServer(contract, opts) {
16492
16490
  connectedAt: conn.connectedAt,
16493
16491
  ...userId !== void 0 ? { userId } : {},
16494
16492
  rooms,
16493
+ ...conn.transport !== void 0 ? { transport: conn.transport } : {},
16495
16494
  ...opts.describeConn?.(conn)
16496
16495
  };
16497
16496
  }
@@ -16508,6 +16507,10 @@ function createSuperLineServer(contract, opts) {
16508
16507
  handlePersonal(channel, payload);
16509
16508
  return;
16510
16509
  }
16510
+ if (channel.startsWith(STORE)) {
16511
+ handleStoreRelay(channel, payload);
16512
+ return;
16513
+ }
16511
16514
  const set = members.get(channel);
16512
16515
  if (set) for (const conn of set) conn.sendRaw(payload);
16513
16516
  const busSet = busListeners.get(channel);
@@ -16587,12 +16590,12 @@ function createSuperLineServer(contract, opts) {
16587
16590
  void adapter.presence?.beat(instanceId);
16588
16591
  for (const conn of conns) {
16589
16592
  if (hb.maxMissed != null && conn.missedPongs >= hb.maxMissed) {
16590
- conn.ws.terminate();
16593
+ conn.terminate();
16591
16594
  continue;
16592
16595
  }
16593
16596
  conn.missedPongs++;
16594
16597
  conn.lastPingAt = now;
16595
- conn.ws.ping();
16598
+ conn.send({ t: "ping" });
16596
16599
  }
16597
16600
  }, hb.interval ?? 3e4);
16598
16601
  hbTimer.unref?.();
@@ -16760,94 +16763,66 @@ function createSuperLineServer(contract, opts) {
16760
16763
  return { descriptor: remote, ctxAvailable: false };
16761
16764
  }
16762
16765
  };
16763
- if (opts.server) {
16764
- opts.server.on("upgrade", (req, socket, head) => {
16765
- void handleUpgrade(req, socket, head);
16766
- });
16767
- }
16768
- function isInspectorRequest(req) {
16769
- if (!inspectorEnabled) return false;
16770
- const raw = req.headers["sec-websocket-protocol"];
16771
- if (!raw) return false;
16772
- const offered = (Array.isArray(raw) ? raw.join(",") : raw).split(",").map((p) => p.trim());
16773
- return offered.includes(import_core2.INSPECTOR_SUBPROTOCOL);
16774
- }
16775
- async function handleUpgrade(req, socket, head) {
16776
- if (opts.path) {
16777
- const { pathname } = new URL(req.url ?? "/", "http://localhost");
16778
- if (pathname !== opts.path) return;
16766
+ const authHook = async (handshake) => {
16767
+ const auth = await opts.authenticate(handshake);
16768
+ return { role: auth.role, ctx: auth.ctx, transport: handshake.transport };
16769
+ };
16770
+ function acceptConn(raw, auth) {
16771
+ const role = auth.role;
16772
+ const ctx = auth.ctx;
16773
+ const inspector = role === import_core2.INSPECTOR_ROLE;
16774
+ if (inspector && !inspectorEnabled) {
16775
+ raw.close();
16776
+ return;
16779
16777
  }
16780
- const inspector = isInspectorRequest(req);
16781
- let role;
16782
- let ctx;
16778
+ const connId = (0, import_node_crypto.randomUUID)();
16779
+ const conn = new Conn(
16780
+ raw,
16781
+ connId,
16782
+ role,
16783
+ ctx,
16784
+ serializer,
16785
+ inspectorEnabled ? (event, data) => emitInspectorEvent({ type: "msg.event", target: connId, name: event, data: safeSnapshot(data) }) : void 0
16786
+ );
16787
+ conn.transport = auth.transport;
16788
+ conn.principal = resolvePrincipal(conn, opts.identify);
16789
+ raw.onMessage((bytes) => {
16790
+ void onMessage(conn, bytes);
16791
+ });
16783
16792
  if (inspector) {
16784
- role = import_core2.INSPECTOR_ROLE;
16785
- ctx = {};
16786
- } else {
16787
- let auth;
16788
- try {
16789
- auth = await opts.authenticate(req);
16790
- } catch {
16791
- socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
16792
- socket.destroy();
16793
- return;
16794
- }
16795
- role = auth.role;
16796
- ctx = auth.ctx;
16797
- }
16798
- wss.handleUpgrade(req, socket, head, (ws) => {
16799
- const connId = (0, import_node_crypto.randomUUID)();
16800
- const conn = new Conn(
16801
- ws,
16802
- connId,
16803
- role,
16804
- ctx,
16805
- serializer,
16806
- opts.backpressure,
16807
- inspectorEnabled ? (event, data) => emitInspectorEvent({ type: "msg.event", target: connId, name: event, data: safeSnapshot(data) }) : void 0
16808
- );
16809
- ws.on("message", (data, isBinary) => {
16810
- void onMessage(conn, data, isBinary);
16811
- });
16812
- if (inspector) {
16813
- inspectorConns.add(conn);
16814
- ws.on("close", () => {
16815
- inspectorConns.delete(conn);
16816
- for (const channel of conn.channels) leaveChannel(conn, channel);
16817
- });
16818
- return;
16819
- }
16820
- conns.add(conn);
16821
- ws.on("pong", () => {
16822
- conn.lastPongAt = Date.now();
16823
- conn.missedPongs = 0;
16824
- });
16825
- ws.on("close", (code) => {
16826
- conns.delete(conn);
16793
+ inspectorConns.add(conn);
16794
+ raw.onClose(() => {
16795
+ inspectorConns.delete(conn);
16827
16796
  for (const channel of conn.channels) leaveChannel(conn, channel);
16828
- void adapter.presence?.del(conn.id);
16829
- const goneUserId = opts.identify?.(conn);
16830
- emitInspectorEvent({
16831
- type: "disconnect",
16832
- connId: conn.id,
16833
- nodeId: instanceId,
16834
- ...goneUserId !== void 0 ? { userId: goneUserId } : {}
16835
- });
16836
- opts.onDisconnect?.(conn, ctx, code);
16837
16797
  });
16838
- opts.onConnection?.(conn, ctx);
16839
- const descriptor = buildDescriptor(conn);
16840
- void adapter.presence?.set(descriptor);
16841
- emitInspectorEvent({ type: "connect", descriptor });
16842
- void joinChannel(conn, CONN + conn.id);
16843
- const uid = opts.identify?.(conn);
16844
- if (uid !== void 0) void joinChannel(conn, USER + uid);
16798
+ return;
16799
+ }
16800
+ conns.add(conn);
16801
+ raw.onClose((code) => {
16802
+ conns.delete(conn);
16803
+ for (const channel of conn.channels) leaveChannel(conn, channel);
16804
+ void adapter.presence?.del(conn.id);
16805
+ const goneUserId = opts.identify?.(conn);
16806
+ emitInspectorEvent({
16807
+ type: "disconnect",
16808
+ connId: conn.id,
16809
+ nodeId: instanceId,
16810
+ ...goneUserId !== void 0 ? { userId: goneUserId } : {}
16811
+ });
16812
+ opts.onDisconnect?.(conn, ctx, code);
16845
16813
  });
16846
- }
16847
- async function onMessage(conn, data, isBinary) {
16814
+ opts.onConnection?.(conn, ctx);
16815
+ const descriptor = buildDescriptor(conn);
16816
+ void adapter.presence?.set(descriptor);
16817
+ emitInspectorEvent({ type: "connect", descriptor });
16818
+ void joinChannel(conn, CONN + conn.id);
16819
+ const uid = opts.identify?.(conn);
16820
+ if (uid !== void 0) void joinChannel(conn, USER + uid);
16821
+ }
16822
+ async function onMessage(conn, bytes) {
16848
16823
  let frame;
16849
16824
  try {
16850
- frame = serializer.decode(toWire(data, isBinary));
16825
+ frame = serializer.decode(bytes);
16851
16826
  } catch {
16852
16827
  return;
16853
16828
  }
@@ -16860,12 +16835,22 @@ function createSuperLineServer(contract, opts) {
16860
16835
  else if (frame.t === "unsub") {
16861
16836
  const ns = topicNamespace(conn.role, frame.c);
16862
16837
  if (ns) leaveChannel(conn, TOPIC + ns + ":" + frame.c);
16863
- } else if (frame.t === "sres") {
16838
+ } else if (frame.t === "sopen") await handleStoreOpen(conn, frame);
16839
+ else if (frame.t === "srd") await handleStoreRead(conn, frame);
16840
+ else if (frame.t === "swr") await handleStoreWrite(conn, frame);
16841
+ else if (frame.t === "sclose") handleStoreClose(conn, frame);
16842
+ else if (frame.t === "sres") {
16864
16843
  await handleClientReply(conn, frame.i, { ok: true, d: frame.d });
16865
16844
  } else if (frame.t === "serr") {
16866
16845
  await handleClientReply(conn, frame.i, { ok: false, code: frame.code, m: frame.m, d: frame.d });
16846
+ } else if (frame.t === "pong") {
16847
+ conn.lastPongAt = Date.now();
16848
+ conn.missedPongs = 0;
16867
16849
  }
16868
16850
  }
16851
+ for (const transport of opts.transports) {
16852
+ void transport.start({ authenticate: authHook, onConnection: acceptConn });
16853
+ }
16869
16854
  function runMiddleware(info, terminal) {
16870
16855
  const chain = opts.use ?? [];
16871
16856
  let last = -1;
@@ -16943,6 +16928,100 @@ function createSuperLineServer(contract, opts) {
16943
16928
  conn.send({ t: "res", i: frame.i, d: null });
16944
16929
  });
16945
16930
  }
16931
+ function storeOrErr(conn, id, name) {
16932
+ const store = storeMap[name];
16933
+ if (!store) conn.send({ t: "err", i: id, code: "NOT_FOUND", m: `Unknown store: ${name}` });
16934
+ return store;
16935
+ }
16936
+ async function handleStoreOpen(conn, frame) {
16937
+ const store = storeOrErr(conn, frame.i, frame.n);
16938
+ if (!store) return;
16939
+ await dispatchOp(conn, frame.i, { kind: "subscribe", name: `store:${frame.n}/${frame.id}`, conn }, async () => {
16940
+ const resource = await store.read(frame.id);
16941
+ if (!resource) throw new import_core2.SuperLineError("NOT_FOUND", `No resource: ${frame.n}/${frame.id}`);
16942
+ const principal = conn.principal ?? conn.id;
16943
+ if (!resource.accessRules[principal]?.read)
16944
+ throw new import_core2.SuperLineError("FORBIDDEN", `Read denied: ${frame.n}/${frame.id}`);
16945
+ await joinChannel(conn, STORE + frame.n + ":" + frame.id);
16946
+ if (inspectorEnabled) emitInspectorEvent({ type: "store.subscribe", connId: conn.id, store: frame.n, id: frame.id });
16947
+ conn.send({ t: "res", i: frame.i, d: resource.data });
16948
+ });
16949
+ }
16950
+ async function handleStoreRead(conn, frame) {
16951
+ const store = storeOrErr(conn, frame.i, frame.n);
16952
+ if (!store) return;
16953
+ await dispatchOp(conn, frame.i, { kind: "request", name: `store:${frame.n}/${frame.id}`, conn }, async () => {
16954
+ const resource = await store.read(frame.id);
16955
+ if (!resource) throw new import_core2.SuperLineError("NOT_FOUND", `No resource: ${frame.n}/${frame.id}`);
16956
+ const principal = conn.principal ?? conn.id;
16957
+ if (!resource.accessRules[principal]?.read)
16958
+ throw new import_core2.SuperLineError("FORBIDDEN", `Read denied: ${frame.n}/${frame.id}`);
16959
+ conn.send({ t: "res", i: frame.i, d: resource.data });
16960
+ });
16961
+ }
16962
+ async function handleStoreWrite(conn, frame) {
16963
+ const store = storeOrErr(conn, frame.i, frame.n);
16964
+ if (!store) return;
16965
+ await dispatchOp(conn, frame.i, { kind: "request", name: `store:${frame.n}/${frame.id}`, conn }, async () => {
16966
+ const resource = await store.read(frame.id);
16967
+ if (!resource) throw new import_core2.SuperLineError("NOT_FOUND", `No resource: ${frame.n}/${frame.id}`);
16968
+ const principal = conn.principal ?? conn.id;
16969
+ if (!resource.accessRules[principal]?.write)
16970
+ throw new import_core2.SuperLineError("FORBIDDEN", `Write denied: ${frame.n}/${frame.id}`);
16971
+ await store.apply({ id: frame.id, update: frame.u, origin: frame.o });
16972
+ if (inspectorEnabled)
16973
+ emitInspectorEvent({
16974
+ type: "store.write",
16975
+ store: frame.n,
16976
+ id: frame.id,
16977
+ origin: frame.o,
16978
+ connId: conn.id,
16979
+ data: safeSnapshot(frame.u)
16980
+ });
16981
+ conn.send({ t: "res", i: frame.i, d: null });
16982
+ });
16983
+ }
16984
+ function handleStoreClose(conn, frame) {
16985
+ if (!storeMap[frame.n]) return;
16986
+ leaveChannel(conn, STORE + frame.n + ":" + frame.id);
16987
+ if (inspectorEnabled) emitInspectorEvent({ type: "store.unsubscribe", connId: conn.id, store: frame.n, id: frame.id });
16988
+ }
16989
+ for (const [name, store] of Object.entries(storeMap)) {
16990
+ store.onChange((change) => {
16991
+ if (relaying) return;
16992
+ void adapter.publish(
16993
+ STORE + name + ":" + change.id,
16994
+ serializer.encode({
16995
+ t: "sch",
16996
+ n: name,
16997
+ id: change.id,
16998
+ u: change.update,
16999
+ o: change.origin,
17000
+ nd: instanceId
17001
+ })
17002
+ );
17003
+ });
17004
+ }
17005
+ function handleStoreRelay(channel, payload) {
17006
+ const set = members.get(channel);
17007
+ if (set) for (const conn of set) conn.sendRaw(payload);
17008
+ let frame;
17009
+ try {
17010
+ frame = serializer.decode(payload);
17011
+ } catch {
17012
+ return;
17013
+ }
17014
+ if (frame.nd === instanceId) return;
17015
+ const store = storeMap[frame.n];
17016
+ if (!store || store.clustering !== "relay") return;
17017
+ relaying = true;
17018
+ try {
17019
+ void store.apply({ id: frame.id, update: frame.u, origin: frame.o });
17020
+ } catch {
17021
+ } finally {
17022
+ relaying = false;
17023
+ }
17024
+ }
16946
17025
  function room(name) {
16947
17026
  const channel = ROOM + name;
16948
17027
  return {
@@ -17047,6 +17126,45 @@ function createSuperLineServer(contract, opts) {
17047
17126
  void adapter.publish(CONN + id, serializer.encode(env));
17048
17127
  });
17049
17128
  }
17129
+ const storeApi = {};
17130
+ for (const [name, store] of Object.entries(storeMap)) {
17131
+ const readOrThrow = async (id) => {
17132
+ const r = await store.read(id);
17133
+ if (!r) throw new import_core2.SuperLineError("NOT_FOUND", `No resource: ${name}/${id}`);
17134
+ return r;
17135
+ };
17136
+ storeApi[name] = {
17137
+ async create(id, data, accessRules) {
17138
+ await store.create(id, data, accessRules);
17139
+ },
17140
+ read(id) {
17141
+ return Promise.resolve(store.read(id));
17142
+ },
17143
+ async write(id, data) {
17144
+ await store.apply({ id, update: data, origin: SERVER_ORIGIN });
17145
+ if (inspectorEnabled)
17146
+ emitInspectorEvent({ type: "store.write", store: name, id, origin: SERVER_ORIGIN, data: safeSnapshot(data) });
17147
+ },
17148
+ async grant(id, principal, perms) {
17149
+ const r = await readOrThrow(id);
17150
+ await store.setAccess(id, { ...r.accessRules, [principal]: perms });
17151
+ if (inspectorEnabled) emitInspectorEvent({ type: "store.grant", store: name, id, principal, perms });
17152
+ },
17153
+ async revoke(id, principal) {
17154
+ const r = await readOrThrow(id);
17155
+ const next = { ...r.accessRules };
17156
+ delete next[principal];
17157
+ await store.setAccess(id, next);
17158
+ if (inspectorEnabled) emitInspectorEvent({ type: "store.revoke", store: name, id, principal });
17159
+ },
17160
+ async delete(id) {
17161
+ await store.delete(id);
17162
+ },
17163
+ list() {
17164
+ return Promise.resolve(store.list());
17165
+ }
17166
+ };
17167
+ }
17050
17168
  const api = {
17051
17169
  nodeId: instanceId,
17052
17170
  nodeName,
@@ -17128,6 +17246,11 @@ function createSuperLineServer(contract, opts) {
17128
17246
  }
17129
17247
  };
17130
17248
  },
17249
+ store(name) {
17250
+ const handle = storeApi[name];
17251
+ if (!handle) throw new import_core2.SuperLineError("NOT_FOUND", `Store not configured: ${name}`);
17252
+ return handle;
17253
+ },
17131
17254
  async close() {
17132
17255
  if (closing) return;
17133
17256
  closing = true;
@@ -17136,22 +17259,16 @@ function createSuperLineServer(contract, opts) {
17136
17259
  for (const conn of inspectorConns) conn.close();
17137
17260
  await adapter.presence?.clearNode(instanceId);
17138
17261
  await adapter.close?.();
17139
- await new Promise((resolve) => {
17140
- wss.close(() => resolve());
17141
- });
17262
+ for (const transport of opts.transports) await transport.stop();
17142
17263
  }
17143
17264
  };
17144
17265
  return api;
17145
17266
  }
17146
- function toWire(data, _isBinary) {
17147
- if (Array.isArray(data)) return Buffer.concat(data);
17148
- if (data instanceof ArrayBuffer) return new Uint8Array(data);
17149
- return data;
17150
- }
17151
17267
  // Annotate the CommonJS export names for ESM import in node:
17152
17268
  0 && (module.exports = {
17153
17269
  Conn,
17154
17270
  MemoryBus,
17155
17271
  createInMemoryAdapter,
17156
- createSuperLineServer
17272
+ createSuperLineServer,
17273
+ resolvePrincipal
17157
17274
  });