@rpcbase/server 0.476.0 → 0.477.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.
Files changed (33) hide show
  1. package/dist/{handler-DEEir2xV.js → handler-BOTZftAB.js} +29 -29
  2. package/dist/{handler-BITFtEr_.js → handler-B_mMDLBO.js} +80 -39
  3. package/dist/{handler-BYVnU9H-.js → handler-Cl-0-832.js} +1 -1
  4. package/dist/{handler-CHuOXAtH.js → handler-Dd20DHyz.js} +15 -11
  5. package/dist/index.d.ts +0 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +102 -87
  8. package/dist/notifications/api/notifications/handler.d.ts.map +1 -1
  9. package/dist/notifications.js +1 -1
  10. package/dist/rts/api/changes/handler.d.ts.map +1 -1
  11. package/dist/rts/index.d.ts +3 -1
  12. package/dist/rts/index.d.ts.map +1 -1
  13. package/dist/{index-Ckx0UHs6.js → rts/index.js} +99 -32
  14. package/dist/{schemas-DI7ewltq.js → schemas-D5T9tDtI.js} +609 -12
  15. package/dist/{shared-Chfrv8o6.js → shared-UGuDRAKK.js} +16 -30
  16. package/dist/uploads/api/file-uploads/handlers/completeUpload.d.ts.map +1 -1
  17. package/dist/uploads/api/file-uploads/handlers/getStatus.d.ts.map +1 -1
  18. package/dist/uploads/api/file-uploads/handlers/uploadChunk.d.ts.map +1 -1
  19. package/dist/uploads/api/file-uploads/shared.d.ts +3 -0
  20. package/dist/uploads/api/file-uploads/shared.d.ts.map +1 -1
  21. package/dist/uploads.js +1 -1
  22. package/package.json +4 -4
  23. package/dist/passwordHashStorage.test.d.ts +0 -2
  24. package/dist/passwordHashStorage.test.d.ts.map +0 -1
  25. package/dist/rts/api/changes/handler.test.d.ts +0 -2
  26. package/dist/rts/api/changes/handler.test.d.ts.map +0 -1
  27. package/dist/rts/index.ws.test.d.ts +0 -2
  28. package/dist/rts/index.ws.test.d.ts.map +0 -1
  29. package/dist/rts.d.ts +0 -3
  30. package/dist/rts.d.ts.map +0 -1
  31. package/dist/rts.js +0 -13
  32. package/dist/uploads/api/files/handlers/getFile.test.d.ts +0 -2
  33. package/dist/uploads/api/files/handlers/getFile.test.d.ts.map +0 -1
@@ -1,8 +1,8 @@
1
1
  import { loadModel, getTenantFilesystemDb } from "@rpcbase/db";
2
2
  import { GridFSBucket, ObjectId } from "mongodb";
3
- import { g as getTenantId, a as getModelCtx, b as getOwnershipSelector, e as ensureUploadIndexes, c as getBucketName, d as getUserId, f as getChunkSizeBytes, h as getSessionTtlMs, i as computeSha256Hex, t as toBufferPayload, n as normalizeSha256Hex, j as getMaxClientUploadBytesPerSecond, k as getRawBodyLimitBytes } from "./shared-Chfrv8o6.js";
3
+ import { g as getTenantId, a as getModelCtx, b as buildUploadsAbility, c as getUploadSessionAccessQuery, e as ensureUploadIndexes, d as getBucketName, f as getUserId, h as getChunkSizeBytes, i as getSessionTtlMs, j as computeSha256Hex, t as toBufferPayload, n as normalizeSha256Hex, k as getMaxClientUploadBytesPerSecond, l as getRawBodyLimitBytes } from "./shared-UGuDRAKK.js";
4
4
  import { randomBytes } from "node:crypto";
5
- import { o as object, n as number, s as string, b as boolean, a as array, _ as _enum } from "./schemas-DI7ewltq.js";
5
+ import { o as object, n as number, s as string, b as boolean, a as array, _ as _enum } from "./schemas-D5T9tDtI.js";
6
6
  const waitForStreamFinished = async (stream) => new Promise((resolve, reject) => {
7
7
  stream.once("finish", resolve);
8
8
  stream.once("error", reject);
@@ -37,7 +37,8 @@ const abortUploadStream = async (stream) => {
37
37
  }
38
38
  }
39
39
  try {
40
- stream.destroy();
40
+ ;
41
+ stream.destroy?.();
41
42
  } catch {
42
43
  }
43
44
  };
@@ -57,21 +58,21 @@ const completeUpload = async (_payload, ctx) => {
57
58
  loadModel("RBUploadSession", modelCtx),
58
59
  loadModel("RBUploadChunk", modelCtx)
59
60
  ]);
60
- const existing = await UploadSession.findOne({ _id: uploadId }).lean();
61
+ const ability = buildUploadsAbility(ctx, tenantId);
62
+ if (!ability.can("update", "RBUploadSession")) {
63
+ ctx.res.status(401);
64
+ return { ok: false, error: "unauthorized" };
65
+ }
66
+ const existing = await UploadSession.findOne({ $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, "read")] }).lean();
61
67
  if (!existing) {
62
68
  ctx.res.status(404);
63
69
  return { ok: false, error: "not_found" };
64
70
  }
65
- const ownershipSelector = getOwnershipSelector(ctx, existing);
66
- if (!ownershipSelector) {
67
- ctx.res.status(401);
68
- return { ok: false, error: "unauthorized" };
69
- }
70
71
  if (existing.status === "done" && existing.fileId) {
71
72
  return { ok: true, fileId: existing.fileId };
72
73
  }
73
74
  const locked = await UploadSession.findOneAndUpdate(
74
- { _id: uploadId, ...ownershipSelector, status: "uploading" },
75
+ { $and: [{ _id: uploadId }, { status: "uploading" }, getUploadSessionAccessQuery(ability, "update")] },
75
76
  { $set: { status: "assembling" }, $unset: { error: "" } },
76
77
  { new: true }
77
78
  ).lean();
@@ -84,7 +85,7 @@ const completeUpload = async (_payload, ctx) => {
84
85
  const nativeDb = fsDb.db;
85
86
  if (!nativeDb) {
86
87
  await UploadSession.updateOne(
87
- { _id: uploadId, ...ownershipSelector },
88
+ { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, "update")] },
88
89
  { $set: { status: "error", error: "filesystem_db_unavailable" } }
89
90
  );
90
91
  ctx.res.status(500);
@@ -106,8 +107,7 @@ const completeUpload = async (_payload, ctx) => {
106
107
  const cursor = UploadChunk.find({ uploadId }).sort({ index: 1 }).cursor();
107
108
  let expectedIndex = 0;
108
109
  try {
109
- for await (const doc of cursor) {
110
- const chunkDoc = doc;
110
+ for await (const chunkDoc of cursor) {
111
111
  if (chunkDoc.index !== expectedIndex) {
112
112
  throw new Error("missing_chunks");
113
113
  }
@@ -131,7 +131,7 @@ const completeUpload = async (_payload, ctx) => {
131
131
  throw new Error("missing_file_id");
132
132
  }
133
133
  await UploadSession.updateOne(
134
- { _id: uploadId, ...ownershipSelector },
134
+ { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, "update")] },
135
135
  { $set: { status: "done", fileId }, $unset: { error: "" } }
136
136
  );
137
137
  try {
@@ -144,14 +144,14 @@ const completeUpload = async (_payload, ctx) => {
144
144
  await abortUploadStream(uploadStream);
145
145
  if (message === "missing_chunks") {
146
146
  await UploadSession.updateOne(
147
- { _id: uploadId, ...ownershipSelector },
147
+ { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, "update")] },
148
148
  { $set: { status: "uploading" } }
149
149
  );
150
150
  ctx.res.status(409);
151
151
  return { ok: false, error: "missing_chunks" };
152
152
  }
153
153
  await UploadSession.updateOne(
154
- { _id: uploadId, ...ownershipSelector },
154
+ { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, "update")] },
155
155
  { $set: { status: "error", error: message } }
156
156
  );
157
157
  ctx.res.status(500);
@@ -174,21 +174,21 @@ const getStatus = async (_payload, ctx) => {
174
174
  loadModel("RBUploadSession", modelCtx),
175
175
  loadModel("RBUploadChunk", modelCtx)
176
176
  ]);
177
- const session = await UploadSession.findOne({ _id: uploadId }).lean();
177
+ const ability = buildUploadsAbility(ctx, tenantId);
178
+ if (!ability.can("read", "RBUploadSession")) {
179
+ ctx.res.status(401);
180
+ return { ok: false, error: "unauthorized" };
181
+ }
182
+ const session = await UploadSession.findOne({ $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, "read")] }).lean();
178
183
  if (!session) {
179
184
  ctx.res.status(404);
180
185
  return { ok: false, error: "not_found" };
181
186
  }
182
- const ownershipSelector = getOwnershipSelector(ctx, session);
183
- if (!ownershipSelector) {
184
- ctx.res.status(401);
185
- return { ok: false, error: "unauthorized" };
186
- }
187
187
  const receivedDocs = await UploadChunk.find(
188
188
  { uploadId },
189
189
  { index: 1, _id: 0 }
190
190
  ).sort({ index: 1 }).lean();
191
- const received = receivedDocs.map((d) => Number(d?.index ?? -1)).filter((n) => Number.isInteger(n) && n >= 0);
191
+ const received = receivedDocs.map((doc) => typeof doc.index === "number" ? doc.index : -1).filter((n) => Number.isInteger(n) && n >= 0);
192
192
  return {
193
193
  ok: true,
194
194
  status: session.status,
@@ -294,16 +294,16 @@ const uploadChunk = async (payload, ctx) => {
294
294
  loadModel("RBUploadSession", modelCtx),
295
295
  loadModel("RBUploadChunk", modelCtx)
296
296
  ]);
297
- const session = await UploadSession.findOne({ _id: uploadId }).lean();
297
+ const ability = buildUploadsAbility(ctx, tenantId);
298
+ if (!ability.can("update", "RBUploadSession")) {
299
+ ctx.res.status(401);
300
+ return { ok: false, error: "unauthorized" };
301
+ }
302
+ const session = await UploadSession.findOne({ $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, "update")] }).lean();
298
303
  if (!session) {
299
304
  ctx.res.status(404);
300
305
  return { ok: false, error: "not_found" };
301
306
  }
302
- const ownershipSelector = getOwnershipSelector(ctx, session);
303
- if (!ownershipSelector) {
304
- ctx.res.status(401);
305
- return { ok: false, error: "unauthorized" };
306
- }
307
307
  if (session.status !== "uploading") {
308
308
  ctx.res.status(409);
309
309
  return { ok: false, error: "not_uploading" };
@@ -1,6 +1,7 @@
1
1
  import { loadModel } from "@rpcbase/db";
2
+ import { buildAbilityFromSession, getAccessibleByQuery } from "@rpcbase/db/acl";
2
3
  import { createNotification, sendNotificationsDigestForUser } from "./notifications.js";
3
- import { o as object, b as boolean, n as number, a as array, s as string, r as record, u as unknown, _ as _enum } from "./schemas-DI7ewltq.js";
4
+ import { o as object, b as boolean, n as number, a as array, s as string, r as record, u as unknown, _ as _enum } from "./schemas-D5T9tDtI.js";
4
5
  const getSessionUser = (ctx) => {
5
6
  const rawSessionUser = ctx.req.session?.user;
6
7
  const userId = typeof rawSessionUser?.id === "string" ? rawSessionUser.id.trim() : "";
@@ -123,6 +124,11 @@ const listNotifications = async (payload, ctx) => {
123
124
  if (!session) {
124
125
  return { ok: false, error: "unauthorized" };
125
126
  }
127
+ const ability = buildAbilityFromSession({ tenantId: session.tenantId, session: ctx.req.session });
128
+ if (!ability.can("read", "RBNotification")) {
129
+ ctx.res.status(403);
130
+ return { ok: false, error: "forbidden" };
131
+ }
126
132
  const parsed = listRequestSchema.safeParse(payload);
127
133
  if (!parsed.success) {
128
134
  ctx.res.status(400);
@@ -137,33 +143,31 @@ const listNotifications = async (payload, ctx) => {
137
143
  const settings = await SettingsModel.findOne({ userId }).lean();
138
144
  const disabledTopics = buildDisabledTopics(settings, "inApp");
139
145
  const NotificationModel = await loadModel("RBNotification", ctx);
140
- const query = { userId };
141
- if (!includeArchived) {
142
- query.archivedAt = { $exists: false };
143
- }
144
- if (unreadOnly) {
145
- query.readAt = { $exists: false };
146
- }
147
- if (disabledTopics.length > 0) {
148
- query.topic = { $nin: disabledTopics };
149
- }
146
+ const queryFilters = [
147
+ { userId },
148
+ getAccessibleByQuery(ability, "read", "RBNotification")
149
+ ];
150
+ if (!includeArchived) queryFilters.push({ archivedAt: { $exists: false } });
151
+ if (unreadOnly) queryFilters.push({ readAt: { $exists: false } });
152
+ if (disabledTopics.length > 0) queryFilters.push({ topic: { $nin: disabledTopics } });
153
+ const query = { $and: queryFilters };
150
154
  const notifications = await NotificationModel.find(query).sort({ createdAt: -1 }).limit(limit).lean();
151
- const unseenQuery = {
152
- userId,
153
- archivedAt: { $exists: false },
154
- seenAt: { $exists: false }
155
- };
156
- if (disabledTopics.length > 0) {
157
- unseenQuery.topic = { $nin: disabledTopics };
158
- }
159
- const unreadQuery = {
160
- userId,
161
- archivedAt: { $exists: false },
162
- readAt: { $exists: false }
163
- };
164
- if (disabledTopics.length > 0) {
165
- unreadQuery.topic = { $nin: disabledTopics };
166
- }
155
+ const unseenQueryFilters = [
156
+ { userId },
157
+ { archivedAt: { $exists: false } },
158
+ { seenAt: { $exists: false } },
159
+ getAccessibleByQuery(ability, "read", "RBNotification")
160
+ ];
161
+ if (disabledTopics.length > 0) unseenQueryFilters.push({ topic: { $nin: disabledTopics } });
162
+ const unseenQuery = { $and: unseenQueryFilters };
163
+ const unreadQueryFilters = [
164
+ { userId },
165
+ { archivedAt: { $exists: false } },
166
+ { readAt: { $exists: false } },
167
+ getAccessibleByQuery(ability, "read", "RBNotification")
168
+ ];
169
+ if (disabledTopics.length > 0) unreadQueryFilters.push({ topic: { $nin: disabledTopics } });
170
+ const unreadQuery = { $and: unreadQueryFilters };
167
171
  const [unreadCount, unseenCount] = await Promise.all([
168
172
  NotificationModel.countDocuments(unreadQuery),
169
173
  NotificationModel.countDocuments(unseenQuery)
@@ -195,6 +199,11 @@ const createNotificationForCurrentUser = async (payload, ctx) => {
195
199
  if (!session) {
196
200
  return { ok: false, error: "unauthorized" };
197
201
  }
202
+ const ability = buildAbilityFromSession({ tenantId: session.tenantId, session: ctx.req.session });
203
+ if (!ability.can("create", "RBNotification")) {
204
+ ctx.res.status(403);
205
+ return { ok: false, error: "forbidden" };
206
+ }
198
207
  const parsed = createRequestSchema.safeParse(payload);
199
208
  if (!parsed.success) {
200
209
  ctx.res.status(400);
@@ -215,6 +224,11 @@ const markRead = async (_payload, ctx) => {
215
224
  if (!session) {
216
225
  return { ok: false, error: "unauthorized" };
217
226
  }
227
+ const ability = buildAbilityFromSession({ tenantId: session.tenantId, session: ctx.req.session });
228
+ if (!ability.can("update", "RBNotification")) {
229
+ ctx.res.status(403);
230
+ return { ok: false, error: "forbidden" };
231
+ }
218
232
  const notificationId = typeof ctx.req.params.notificationId === "string" ? ctx.req.params.notificationId.trim() : "";
219
233
  if (!notificationId) {
220
234
  ctx.res.status(400);
@@ -224,7 +238,7 @@ const markRead = async (_payload, ctx) => {
224
238
  const now = /* @__PURE__ */ new Date();
225
239
  try {
226
240
  await NotificationModel.updateOne(
227
- { _id: notificationId, userId: session.userId, archivedAt: { $exists: false } },
241
+ { $and: [{ _id: notificationId }, { archivedAt: { $exists: false } }, getAccessibleByQuery(ability, "update", "RBNotification")] },
228
242
  { $set: { readAt: now, seenAt: now } }
229
243
  );
230
244
  } catch {
@@ -238,18 +252,23 @@ const markAllRead = async (_payload, ctx) => {
238
252
  if (!session) {
239
253
  return { ok: false, error: "unauthorized" };
240
254
  }
255
+ const ability = buildAbilityFromSession({ tenantId: session.tenantId, session: ctx.req.session });
256
+ if (!ability.can("update", "RBNotification")) {
257
+ ctx.res.status(403);
258
+ return { ok: false, error: "forbidden" };
259
+ }
241
260
  const SettingsModel = await loadModel("RBNotificationSettings", ctx);
242
261
  const settings = await SettingsModel.findOne({ userId: session.userId }).lean();
243
262
  const disabledTopics = buildDisabledTopics(settings, "inApp");
244
263
  const NotificationModel = await loadModel("RBNotification", ctx);
245
- const query = {
246
- userId: session.userId,
247
- archivedAt: { $exists: false },
248
- readAt: { $exists: false }
249
- };
250
- if (disabledTopics.length > 0) {
251
- query.topic = { $nin: disabledTopics };
252
- }
264
+ const queryFilters = [
265
+ { userId: session.userId },
266
+ { archivedAt: { $exists: false } },
267
+ { readAt: { $exists: false } },
268
+ getAccessibleByQuery(ability, "update", "RBNotification")
269
+ ];
270
+ if (disabledTopics.length > 0) queryFilters.push({ topic: { $nin: disabledTopics } });
271
+ const query = { $and: queryFilters };
253
272
  const now = /* @__PURE__ */ new Date();
254
273
  await NotificationModel.updateMany(query, { $set: { readAt: now, seenAt: now } });
255
274
  return { ok: true };
@@ -259,6 +278,11 @@ const archiveNotification = async (_payload, ctx) => {
259
278
  if (!session) {
260
279
  return { ok: false, error: "unauthorized" };
261
280
  }
281
+ const ability = buildAbilityFromSession({ tenantId: session.tenantId, session: ctx.req.session });
282
+ if (!ability.can("update", "RBNotification")) {
283
+ ctx.res.status(403);
284
+ return { ok: false, error: "forbidden" };
285
+ }
262
286
  const notificationId = typeof ctx.req.params.notificationId === "string" ? ctx.req.params.notificationId.trim() : "";
263
287
  if (!notificationId) {
264
288
  ctx.res.status(400);
@@ -267,7 +291,7 @@ const archiveNotification = async (_payload, ctx) => {
267
291
  const NotificationModel = await loadModel("RBNotification", ctx);
268
292
  try {
269
293
  await NotificationModel.updateOne(
270
- { _id: notificationId, userId: session.userId, archivedAt: { $exists: false } },
294
+ { $and: [{ _id: notificationId }, { archivedAt: { $exists: false } }, getAccessibleByQuery(ability, "update", "RBNotification")] },
271
295
  { $set: { archivedAt: /* @__PURE__ */ new Date() } }
272
296
  );
273
297
  } catch {
@@ -281,8 +305,15 @@ const getSettings = async (_payload, ctx) => {
281
305
  if (!session) {
282
306
  return { ok: false, error: "unauthorized" };
283
307
  }
308
+ const ability = buildAbilityFromSession({ tenantId: session.tenantId, session: ctx.req.session });
309
+ if (!ability.can("read", "RBNotificationSettings")) {
310
+ ctx.res.status(403);
311
+ return { ok: false, error: "forbidden" };
312
+ }
284
313
  const SettingsModel = await loadModel("RBNotificationSettings", ctx);
285
- const settings = await SettingsModel.findOne({ userId: session.userId }).lean();
314
+ const settings = await SettingsModel.findOne(
315
+ { $and: [{ userId: session.userId }, getAccessibleByQuery(ability, "read", "RBNotificationSettings")] }
316
+ ).lean();
286
317
  const digestFrequencyRaw = typeof settings?.digestFrequency === "string" ? settings.digestFrequency : "weekly";
287
318
  const digestFrequency = digestFrequencyRaw === "off" || digestFrequencyRaw === "daily" || digestFrequencyRaw === "weekly" ? digestFrequencyRaw : "weekly";
288
319
  const topicPreferences = Array.isArray(settings?.topicPreferences) ? settings.topicPreferences.map((pref) => ({
@@ -305,6 +336,11 @@ const updateSettings = async (payload, ctx) => {
305
336
  if (!session) {
306
337
  return { ok: false, error: "unauthorized" };
307
338
  }
339
+ const ability = buildAbilityFromSession({ tenantId: session.tenantId, session: ctx.req.session });
340
+ if (!ability.can("update", "RBNotificationSettings")) {
341
+ ctx.res.status(403);
342
+ return { ok: false, error: "forbidden" };
343
+ }
308
344
  const parsed = updateSettingsRequestSchema.safeParse(payload);
309
345
  if (!parsed.success) {
310
346
  ctx.res.status(400);
@@ -336,7 +372,7 @@ const updateSettings = async (payload, ctx) => {
336
372
  ops.$set = nextValues;
337
373
  }
338
374
  const settings = await SettingsModel.findOneAndUpdate(
339
- { userId: session.userId },
375
+ { $and: [{ userId: session.userId }, getAccessibleByQuery(ability, "update", "RBNotificationSettings")] },
340
376
  ops,
341
377
  { upsert: true, new: true, setDefaultsOnInsert: true }
342
378
  ).lean();
@@ -362,6 +398,11 @@ const runDigest = async (payload, ctx) => {
362
398
  if (!session) {
363
399
  return { ok: false, error: "unauthorized" };
364
400
  }
401
+ const ability = buildAbilityFromSession({ tenantId: session.tenantId, session: ctx.req.session });
402
+ if (!ability.can("read", "RBNotification")) {
403
+ ctx.res.status(403);
404
+ return { ok: false, error: "forbidden" };
405
+ }
365
406
  const parsed = digestRunRequestSchema.safeParse(payload);
366
407
  if (!parsed.success) {
367
408
  ctx.res.status(400);
@@ -1,6 +1,6 @@
1
1
  import { getTenantFilesystemDb } from "@rpcbase/db";
2
2
  import { ObjectId, GridFSBucket } from "mongodb";
3
- import { g as getTenantId, c as getBucketName } from "./shared-Chfrv8o6.js";
3
+ import { g as getTenantId, d as getBucketName } from "./shared-UGuDRAKK.js";
4
4
  const deleteFile = async (_payload, ctx) => {
5
5
  const tenantId = getTenantId(ctx);
6
6
  if (!tenantId) {
@@ -1,5 +1,6 @@
1
1
  import { loadModel, ZRBRtsChangeOp } from "@rpcbase/db";
2
- import { o as object, a as array, s as string, n as number, b as boolean, _ as _enum } from "./schemas-DI7ewltq.js";
2
+ import { buildAbilityFromSession } from "@rpcbase/db/acl";
3
+ import { o as object, a as array, s as string, n as number, b as boolean, _ as _enum } from "./schemas-D5T9tDtI.js";
3
4
  const Route = "/api/rb/rts/changes";
4
5
  const requestSchema = object({
5
6
  sinceSeq: number().int().min(0).default(0),
@@ -68,6 +69,7 @@ const changesHandler = async (payload, ctx) => {
68
69
  ctx.res.status(401);
69
70
  return { ok: false, latestSeq: 0, changes: [] };
70
71
  }
72
+ const ability = buildAbilityFromSession({ tenantId, session: ctx.req.session });
71
73
  const modelCtx = getModelCtx(ctx, tenantId);
72
74
  const [RtsChange, RtsCounter] = await Promise.all([
73
75
  loadModel("RBRtsChange", modelCtx),
@@ -76,24 +78,26 @@ const changesHandler = async (payload, ctx) => {
76
78
  const counter = await RtsCounter.findOne({ _id: "rts" }, { seq: 1 }).lean();
77
79
  const latestSeq = Number(counter?.seq ?? 0) || 0;
78
80
  const { sinceSeq, limit, modelNames } = parsed.data;
79
- const earliestSelector = {};
80
- if (Array.isArray(modelNames) && modelNames.length) {
81
- earliestSelector.modelName = { $in: modelNames };
81
+ const requestedModelNames = Array.isArray(modelNames) && modelNames.length ? modelNames.map((m) => String(m)).filter(Boolean) : null;
82
+ const allowedModelNames = requestedModelNames ? requestedModelNames.filter((m) => ability.can("read", m)) : Array.from(
83
+ new Set(
84
+ (await RtsChange.distinct("modelName")).map((m) => String(m)).filter(Boolean).filter((m) => ability.can("read", m))
85
+ )
86
+ );
87
+ let earliestSeq;
88
+ if (allowedModelNames.length) {
89
+ const earliest = await RtsChange.findOne({ modelName: { $in: allowedModelNames } }, { seq: 1 }).sort({ seq: 1 }).lean();
90
+ earliestSeq = earliest?.seq ? Number(earliest.seq) : void 0;
82
91
  }
83
- const earliest = await RtsChange.findOne(earliestSelector, { seq: 1 }).sort({ seq: 1 }).lean();
84
- const earliestSeq = earliest?.seq ? Number(earliest.seq) : void 0;
85
92
  const needsFullResync = typeof earliestSeq === "number" && sinceSeq < earliestSeq - 1;
86
- const selector = { seq: { $gt: sinceSeq } };
87
- if (Array.isArray(modelNames) && modelNames.length) {
88
- selector.modelName = { $in: modelNames };
89
- }
93
+ const selector = { seq: { $gt: sinceSeq }, modelName: { $in: allowedModelNames } };
90
94
  const changes = await RtsChange.find(selector, { _id: 0, seq: 1, modelName: 1, op: 1, docId: 1 }).sort({ seq: 1 }).limit(limit).lean();
91
95
  return {
92
96
  ok: true,
93
97
  needsFullResync: needsFullResync || void 0,
94
98
  earliestSeq,
95
99
  latestSeq,
96
- changes: Array.isArray(changes) ? changes.filter(isRtsChangeRecord).map((c) => ({
100
+ changes: Array.isArray(changes) ? changes.filter(isRtsChangeRecord).filter((c) => ability.can("read", c.modelName)).map((c) => ({
97
101
  seq: Number(c.seq),
98
102
  modelName: String(c.modelName),
99
103
  op: c.op,
package/dist/index.d.ts CHANGED
@@ -4,5 +4,4 @@ export * from './hashPassword';
4
4
  export * from './passwordHashStorage';
5
5
  export * from './ssrMiddleware';
6
6
  export * from './email';
7
- export * from './rts/index';
8
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,uBAAuB,CAAA;AACrC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,SAAS,CAAA;AACvB,cAAc,aAAa,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,uBAAuB,CAAA;AACrC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,SAAS,CAAA"}