@rpcbase/server 0.466.0 → 0.468.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.
@@ -5,6 +5,10 @@ const TENANT_ID_QUERY_PARAM = "rb-tenant-id";
5
5
  const USER_ID_HEADER = "rb-user-id";
6
6
  const QUERY_KEY_MAX_LEN = 4096;
7
7
  const QUERY_MAX_LIMIT = 4096;
8
+ const INTERNAL_MODEL_NAMES = /* @__PURE__ */ new Set(["RtsChange", "RtsCounter"]);
9
+ const DEFAULT_MAX_PAYLOAD_BYTES = 1024 * 1024;
10
+ const DEFAULT_MAX_SUBSCRIPTIONS_PER_SOCKET = 256;
11
+ const DEFAULT_DISPATCH_DEBOUNCE_MS = 25;
8
12
  const initializedServers = /* @__PURE__ */ new WeakSet();
9
13
  const customHandlers = [];
10
14
  const sockets = /* @__PURE__ */ new Map();
@@ -14,6 +18,12 @@ const socketCleanup = /* @__PURE__ */ new Map();
14
18
  const socketSubscriptions = /* @__PURE__ */ new Map();
15
19
  const subscriptions = /* @__PURE__ */ new Map();
16
20
  const changeStreams = /* @__PURE__ */ new Map();
21
+ const dispatchTimers = /* @__PURE__ */ new Map();
22
+ const upgradeMeta = /* @__PURE__ */ new WeakMap();
23
+ let maxPayloadBytes = DEFAULT_MAX_PAYLOAD_BYTES;
24
+ let maxSubscriptionsPerSocket = DEFAULT_MAX_SUBSCRIPTIONS_PER_SOCKET;
25
+ let dispatchDebounceMs = DEFAULT_DISPATCH_DEBOUNCE_MS;
26
+ let allowInternalModels = false;
17
27
  class RtsSocket {
18
28
  id;
19
29
  tenantId;
@@ -70,6 +80,13 @@ const sendWs = (ws, message) => {
70
80
  if (ws.readyState !== 1) return;
71
81
  ws.send(JSON.stringify(message));
72
82
  };
83
+ const redactErrorMessage = (err) => {
84
+ const raw = err instanceof Error ? err.message : "Unknown error";
85
+ const trimmedModelList = raw.replace(/\.\s+Available models:[\s\S]*$/, "");
86
+ const maxLen = 256;
87
+ if (trimmedModelList.length <= maxLen) return trimmedModelList;
88
+ return trimmedModelList.slice(0, maxLen);
89
+ };
73
90
  const unauthorized = (socket, message = "Unauthorized") => {
74
91
  try {
75
92
  socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
@@ -92,10 +109,11 @@ const badRequest = (socket, message = "Bad Request") => {
92
109
  };
93
110
  const runSessionMiddleware = async (sessionMiddleware, req) => {
94
111
  await new Promise((resolve, reject) => {
95
- sessionMiddleware(req, {}, (err) => {
112
+ const next = (err) => {
96
113
  if (err) reject(err);
97
114
  else resolve();
98
- });
115
+ };
116
+ sessionMiddleware(req, {}, next);
99
117
  });
100
118
  };
101
119
  const parseUpgradeMeta = async ({
@@ -107,37 +125,39 @@ const parseUpgradeMeta = async ({
107
125
  if (!tenantId) {
108
126
  throw new Error("Missing rb-tenant-id query parameter");
109
127
  }
110
- const raw = req.headers[USER_ID_HEADER];
111
- const headerUserId = Array.isArray(raw) ? raw[0] : raw;
112
- if (headerUserId) return { tenantId, userId: headerUserId };
113
- if (!sessionMiddleware) {
114
- throw new Error("Missing rb-user-id header (reverse-proxy) and no session middleware configured");
115
- }
116
- const upgradeReq = req;
117
- try {
118
- await runSessionMiddleware(sessionMiddleware, upgradeReq);
119
- } catch {
120
- throw new Error("Failed to load session for RTS");
121
- }
122
- const sessionUser = upgradeReq.session?.user;
123
- const sessionUserId = sessionUser?.id;
124
- if (!sessionUserId) {
125
- throw new Error("Not signed in (missing session.user.id)");
126
- }
127
- const signedInTenants = sessionUser?.signed_in_tenants;
128
- const currentTenantId = sessionUser?.current_tenant_id;
129
- if (Array.isArray(signedInTenants) && signedInTenants.length > 0) {
130
- if (!signedInTenants.includes(tenantId)) {
131
- throw new Error("Tenant not authorized for this session");
128
+ if (sessionMiddleware) {
129
+ const upgradeReq = req;
130
+ try {
131
+ await runSessionMiddleware(sessionMiddleware, upgradeReq);
132
+ } catch {
133
+ throw new Error("Failed to load session for RTS");
134
+ }
135
+ const sessionUser = upgradeReq.session?.user;
136
+ const sessionUserId = sessionUser?.id;
137
+ if (!sessionUserId) {
138
+ throw new Error("Not signed in (missing session.user.id)");
132
139
  }
133
- } else if (currentTenantId) {
134
- if (currentTenantId !== tenantId) {
140
+ const signedInTenants = sessionUser?.signed_in_tenants;
141
+ const currentTenantId = sessionUser?.current_tenant_id;
142
+ if (Array.isArray(signedInTenants) && signedInTenants.length > 0) {
143
+ if (!signedInTenants.includes(tenantId)) {
144
+ throw new Error("Tenant not authorized for this session");
145
+ }
146
+ } else if (currentTenantId) {
147
+ if (currentTenantId !== tenantId) {
148
+ throw new Error("Tenant not authorized for this session");
149
+ }
150
+ } else {
135
151
  throw new Error("Tenant not authorized for this session");
136
152
  }
137
- } else {
138
- throw new Error("Tenant not authorized for this session");
153
+ return { tenantId, userId: sessionUserId };
139
154
  }
140
- return { tenantId, userId: sessionUserId };
155
+ const raw = req.headers[USER_ID_HEADER];
156
+ const headerUserId = Array.isArray(raw) ? raw[0] : raw;
157
+ if (!headerUserId) {
158
+ throw new Error("Missing rb-user-id header (reverse-proxy) and no session middleware configured");
159
+ }
160
+ return { tenantId, userId: headerUserId };
141
161
  };
142
162
  const getTenantModel = async (tenantId, modelName) => {
143
163
  const ctx = {
@@ -159,15 +179,32 @@ const normalizeLimit = (limit) => {
159
179
  const normalizeOptions = (options) => {
160
180
  if (!options || typeof options !== "object") return {};
161
181
  const normalized = {};
162
- if (options.projection && typeof options.projection === "object") {
182
+ if (options.projection && typeof options.projection === "object" && !Array.isArray(options.projection)) {
163
183
  normalized.projection = options.projection;
164
184
  }
165
- if (options.sort && typeof options.sort === "object") {
185
+ if (options.sort && typeof options.sort === "object" && !Array.isArray(options.sort)) {
166
186
  normalized.sort = options.sort;
167
187
  }
168
188
  normalized.limit = normalizeLimit(options.limit);
169
189
  return normalized;
170
190
  };
191
+ const makeDispatchKey = (tenantId, modelName) => `${tenantId}:${modelName}`;
192
+ const clearDispatchTimer = (tenantId, modelName) => {
193
+ const key = makeDispatchKey(tenantId, modelName);
194
+ const timer = dispatchTimers.get(key);
195
+ if (!timer) return;
196
+ clearTimeout(timer);
197
+ dispatchTimers.delete(key);
198
+ };
199
+ const scheduleDispatchSubscriptionsForModel = (tenantId, modelName) => {
200
+ const key = makeDispatchKey(tenantId, modelName);
201
+ if (dispatchTimers.has(key)) return;
202
+ const delay = Math.max(0, Math.min(1e3, Math.floor(dispatchDebounceMs)));
203
+ dispatchTimers.set(key, setTimeout(() => {
204
+ dispatchTimers.delete(key);
205
+ void dispatchSubscriptionsForModel(tenantId, modelName);
206
+ }, delay));
207
+ };
171
208
  const runAndSendQuery = async ({
172
209
  tenantId,
173
210
  targetSocketIds,
@@ -210,7 +247,7 @@ const dispatchSubscriptionsForModel = async (tenantId, modelName) => {
210
247
  options: sub.options
211
248
  });
212
249
  } catch (err) {
213
- const error = err instanceof Error ? err.message : "Unknown error";
250
+ const error = redactErrorMessage(err);
214
251
  const payload = { type: "query_payload", modelName, queryKey, error };
215
252
  for (const socketId of targetSocketIds) {
216
253
  const ws = sockets.get(socketId);
@@ -229,15 +266,17 @@ const ensureChangeStream = async (tenantId, modelName) => {
229
266
  fullDocument: "updateLookup"
230
267
  });
231
268
  stream.on("change", () => {
232
- void dispatchSubscriptionsForModel(tenantId, modelName);
269
+ scheduleDispatchSubscriptionsForModel(tenantId, modelName);
233
270
  });
234
271
  stream.on("close", () => {
272
+ clearDispatchTimer(tenantId, modelName);
235
273
  const map = changeStreams.get(tenantId);
236
274
  map?.delete(modelName);
237
275
  if (map && map.size === 0) changeStreams.delete(tenantId);
238
276
  });
239
277
  stream.on("error", () => {
240
278
  try {
279
+ clearDispatchTimer(tenantId, modelName);
241
280
  stream.close();
242
281
  } catch {
243
282
  }
@@ -304,6 +343,7 @@ const removeSocketSubscription = ({
304
343
  stream.close();
305
344
  } catch {
306
345
  }
346
+ clearDispatchTimer(tenantId, modelName);
307
347
  tenantStreams?.delete(modelName);
308
348
  if (tenantStreams && tenantStreams.size === 0) changeStreams.delete(tenantId);
309
349
  }
@@ -354,6 +394,10 @@ const handleClientMessage = async ({
354
394
  return;
355
395
  }
356
396
  if (!message.modelName || typeof message.modelName !== "string") return;
397
+ if (!allowInternalModels && INTERNAL_MODEL_NAMES.has(message.modelName)) {
398
+ sendWs(ws, { type: "query_payload", modelName: message.modelName, queryKey: message.queryKey ?? "", error: "Model not allowed" });
399
+ return;
400
+ }
357
401
  if (!message.queryKey || typeof message.queryKey !== "string") return;
358
402
  if (message.queryKey.length > QUERY_KEY_MAX_LEN) return;
359
403
  if (message.type === "remove_query") {
@@ -368,6 +412,18 @@ const handleClientMessage = async ({
368
412
  if (!message.query || typeof message.query !== "object") return;
369
413
  const options = normalizeOptions(message.options);
370
414
  if (message.type === "registerQuery") {
415
+ const existing = socketSubscriptions.get(socketId)?.get(message.modelName)?.has(message.queryKey) ?? false;
416
+ if (!existing) {
417
+ let count = 0;
418
+ const byModel = socketSubscriptions.get(socketId);
419
+ if (byModel) {
420
+ for (const set of byModel.values()) count += set.size;
421
+ }
422
+ if (count >= maxSubscriptionsPerSocket) {
423
+ sendWs(ws, { type: "query_payload", modelName: message.modelName, queryKey: message.queryKey, error: "Too many subscriptions" });
424
+ return;
425
+ }
426
+ }
371
427
  addSocketSubscription({
372
428
  socketId,
373
429
  tenantId: meta.tenantId,
@@ -379,7 +435,7 @@ const handleClientMessage = async ({
379
435
  try {
380
436
  await ensureChangeStream(meta.tenantId, message.modelName);
381
437
  } catch (err) {
382
- const error = err instanceof Error ? err.message : "Unable to initialize change stream";
438
+ const error = redactErrorMessage(err);
383
439
  sendWs(ws, { type: "query_payload", modelName: message.modelName, queryKey: message.queryKey, error });
384
440
  return;
385
441
  }
@@ -394,19 +450,34 @@ const handleClientMessage = async ({
394
450
  options
395
451
  });
396
452
  } catch (err) {
397
- const error = err instanceof Error ? err.message : "Unknown error";
453
+ const error = redactErrorMessage(err);
398
454
  sendWs(ws, { type: "query_payload", modelName: message.modelName, queryKey: message.queryKey, error });
399
455
  }
400
456
  };
401
457
  const initRts = ({
402
458
  server,
403
459
  path = "/rts",
404
- sessionMiddleware
460
+ sessionMiddleware,
461
+ maxPayloadBytes: maxPayloadBytesArg,
462
+ maxSubscriptionsPerSocket: maxSubscriptionsPerSocketArg,
463
+ dispatchDebounceMs: dispatchDebounceMsArg,
464
+ allowInternalModels: allowInternalModelsArg
405
465
  }) => {
406
466
  if (initializedServers.has(server)) return;
407
467
  initializedServers.add(server);
408
- const wss = new WebSocketServer({ noServer: true });
468
+ if (typeof maxPayloadBytesArg === "number" && Number.isFinite(maxPayloadBytesArg) && maxPayloadBytesArg > 0) {
469
+ maxPayloadBytes = Math.floor(maxPayloadBytesArg);
470
+ }
471
+ if (typeof maxSubscriptionsPerSocketArg === "number" && Number.isFinite(maxSubscriptionsPerSocketArg) && maxSubscriptionsPerSocketArg > 0) {
472
+ maxSubscriptionsPerSocket = Math.floor(maxSubscriptionsPerSocketArg);
473
+ }
474
+ if (typeof dispatchDebounceMsArg === "number" && Number.isFinite(dispatchDebounceMsArg) && dispatchDebounceMsArg >= 0) {
475
+ dispatchDebounceMs = Math.floor(dispatchDebounceMsArg);
476
+ }
477
+ allowInternalModels = Boolean(allowInternalModelsArg);
478
+ const wss = new WebSocketServer({ noServer: true, maxPayload: maxPayloadBytes });
409
479
  server.on("upgrade", (req, socket, head) => {
480
+ upgradeMeta.delete(req);
410
481
  let url;
411
482
  try {
412
483
  url = new URL(req.url ?? "", `http://${req.headers.host ?? "localhost"}`);
@@ -418,7 +489,7 @@ const initRts = ({
418
489
  void (async () => {
419
490
  try {
420
491
  const meta = await parseUpgradeMeta({ req, url, sessionMiddleware });
421
- req.__rb_rts_meta = meta;
492
+ upgradeMeta.set(req, meta);
422
493
  wss.handleUpgrade(req, socket, head, (ws) => {
423
494
  wss.emit("connection", ws, req);
424
495
  });
@@ -436,7 +507,8 @@ const initRts = ({
436
507
  });
437
508
  });
438
509
  wss.on("connection", (ws, req) => {
439
- const meta = req.__rb_rts_meta;
510
+ const meta = upgradeMeta.get(req);
511
+ upgradeMeta.delete(req);
440
512
  if (!meta) {
441
513
  try {
442
514
  ws.close();
@@ -481,7 +553,7 @@ const registerRtsHandler = (handler) => {
481
553
  customHandlers.push(handler);
482
554
  };
483
555
  const notifyRtsModelChanged = (tenantId, modelName) => {
484
- void dispatchSubscriptionsForModel(tenantId, modelName);
556
+ scheduleDispatchSubscriptionsForModel(tenantId, modelName);
485
557
  };
486
558
  export {
487
559
  initRts as i,