@rpcbase/server 0.629.0 → 0.631.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.js CHANGED
@@ -2,11 +2,13 @@ import { n as __toESM, t as __commonJSMin } from "./rolldown-runtime-BPOCksWG.js
2
2
  import { c as runRtsQuery, d as getDerivedKey, i as normalizeRtsQueryOptions, n as buildRtsAbilityFromRequest, o as resolveRtsRequestTenantId, r as isRtsRequestAuthorized, s as runRtsCount, t as RTS_QUERY_WINDOW_MAX_COUNT, u as syncAuthenticatedSessionMiddleware } from "./queryWindow-NCe5vJHt.js";
3
3
  import { i as sendEmail, n as configureEmailTransport, r as createResendEmailTransport, t as EmailDeliveryError } from "./email-6gJ8ibKp.js";
4
4
  import { DISPOSABLE_EMAIL_ERROR_CODE, INVALID_EMAIL_DOMAIN_ERROR_CODE, clearEmailDomainValidationCache, getEmailAddressDomain, getEmailAddressValidationErrorCode, isEmailAddressValid, validateEmailDomain } from "./email-validation/index.js";
5
+ import { l as getFileReferencesCollectionName, s as getBucketName, y as resolveStoredUploadAccessScope } from "./shared-B1VLaHsk.js";
6
+ import { r as deleteUploadFileReference } from "./fileReferences-Dcv9PTqD.js";
5
7
  import session from "express-session";
6
8
  import { RedisStore } from "connect-redis";
7
9
  import MongoStore from "connect-mongo";
8
10
  import { createClient } from "redis";
9
- import { MongoClient } from "mongodb";
11
+ import { GridFSBucket, MongoClient, ObjectId } from "mongodb";
10
12
  import env from "@rpcbase/env";
11
13
  import { RtsSsrRuntimeProvider, SSR_ERROR_STATE_GLOBAL_KEY, STATIC_RPCBASE_RTS_HYDRATION_DATA_KEY, SsrErrorFallback, initApiClient, serializeSsrErrorState } from "@rpcbase/client";
12
14
  import { getMongoDirectConnection, getMongoUrl } from "@rpcbase/db/mongo";
@@ -22,6 +24,7 @@ import crypto, { scrypt } from "crypto";
22
24
  import httpProxy from "http-proxy-3";
23
25
  import inspector from "node:inspector";
24
26
  import { fileURLToPath } from "node:url";
27
+ import { getTenantFilesystemDb, models } from "@rpcbase/db";
25
28
  import { Transform, Writable } from "node:stream";
26
29
  import { StrictMode, createElement } from "react";
27
30
  import { renderToPipeableStream, renderToStaticMarkup } from "react-dom/server";
@@ -6412,6 +6415,17 @@ const showNotificationFromMessage = async (message) => {
6412
6415
  return true
6413
6416
  }
6414
6417
 
6418
+ const fetchAssetWithNetworkRetry = async (request) => {
6419
+ try {
6420
+ const response = await fetch(request)
6421
+ if (response.status < 500) return response
6422
+ } catch {
6423
+ return fetch(request)
6424
+ }
6425
+
6426
+ return fetch(request)
6427
+ }
6428
+
6415
6429
  const getTargetUrl = (link, linkMode, client) => {
6416
6430
  try {
6417
6431
  if (linkMode === "merge_current_search" && client) {
@@ -6451,6 +6465,14 @@ self.addEventListener("message", (event) => {
6451
6465
  })())
6452
6466
  })
6453
6467
 
6468
+ self.addEventListener("fetch", (event) => {
6469
+ const request = event.request
6470
+ if (request.method !== "GET") return
6471
+ if (request.destination !== "script" && request.destination !== "style") return
6472
+ if (new URL(request.url).origin !== self.location.origin) return
6473
+ event.respondWith(fetchAssetWithNetworkRetry(request))
6474
+ })
6475
+
6454
6476
  self.addEventListener("push", (event) => {
6455
6477
  let message = null
6456
6478
  try {
@@ -7744,6 +7766,270 @@ var ssrMiddleware = ({ viteInstance, dataRoutes, errorExtraComponent, getHtmlOut
7744
7766
  }
7745
7767
  };
7746
7768
  //#endregion
7747
- export { DISPOSABLE_EMAIL_ERROR_CODE, EmailDeliveryError, INVALID_EMAIL_DOMAIN_ERROR_CODE, RPCBASE_SERVICE_WORKER_MESSAGE_SOURCE, RPCBASE_SERVICE_WORKER_PATH, bindShutdownSignals, clearEmailDomainValidationCache, configureEmailTransport, createResendEmailTransport, getDerivedKey, getEmailAddressDomain, getEmailAddressValidationErrorCode, hashPassword, hashPasswordForStorage, initServer, isEmailAddressValid, onShutdown, registerRpcbaseServiceWorkerRoute, rpcbaseServiceWorkerRequestHandler, rpcbaseServiceWorkerSource, sendEmail, ssrMiddleware, validateEmailDomain, verifyPasswordFromStorage };
7769
+ //#region src/accountDeletionUploads.ts
7770
+ var toTrimmedString$2 = (value) => typeof value === "string" ? value.trim() : "";
7771
+ var getNativeDb$1 = (connection) => {
7772
+ if (!connection.db) throw new Error(`Mongo database '${connection.name}' is unavailable.`);
7773
+ return connection.db;
7774
+ };
7775
+ var deleteGridFsFile = async (db, bucket, bucketName, fileId) => {
7776
+ const variants = await db.collection(`${bucketName}.files`).find({ "metadata.variantOf": fileId.toHexString() }, { projection: { _id: 1 } }).toArray();
7777
+ await bucket.delete(fileId);
7778
+ for (const variant of variants) if (variant._id instanceof ObjectId) await bucket.delete(variant._id);
7779
+ };
7780
+ var removeUserFromSharedFile = async (files, fileId) => {
7781
+ await files.updateMany({ $or: [{ _id: fileId }, { "metadata.variantOf": fileId.toHexString() }] }, { $unset: { "metadata.userId": "" } });
7782
+ };
7783
+ var deleteRpcbaseUserUploads = async (tenantId, userId) => {
7784
+ const modelCtx = { tenantId };
7785
+ const [UploadSession, UploadChunk] = await Promise.all([models.getUnsafe("RBUploadSession", modelCtx), models.getUnsafe("RBUploadChunk", modelCtx)]);
7786
+ const uploadIds = (await UploadSession.find({ userId }, { _id: 1 }).lean()).map((session) => session._id);
7787
+ const filesystemDb = getNativeDb$1(await getTenantFilesystemDb(tenantId));
7788
+ const bucketName = getBucketName();
7789
+ const references = filesystemDb.collection(getFileReferencesCollectionName(bucketName));
7790
+ const userReferences = await references.find({ userId }).toArray();
7791
+ for (const reference of userReferences) {
7792
+ if (resolveStoredUploadAccessScope(reference) === "owner") {
7793
+ await deleteUploadFileReference({
7794
+ tenantId,
7795
+ bucketName,
7796
+ referenceId: reference._id
7797
+ });
7798
+ continue;
7799
+ }
7800
+ await references.updateOne({ _id: reference._id }, { $unset: { userId: "" } });
7801
+ }
7802
+ const files = filesystemDb.collection(`${bucketName}.files`);
7803
+ const userFiles = await files.find({ "metadata.userId": userId }).toArray();
7804
+ const bucket = userFiles.length > 0 ? new GridFSBucket(filesystemDb, { bucketName }) : null;
7805
+ for (const file of userFiles) {
7806
+ if (!(file._id instanceof ObjectId) || !bucket) continue;
7807
+ const metadata = file.metadata ?? {};
7808
+ const deduplication = metadata.deduplication;
7809
+ const isDeduplicated = Boolean(deduplication && typeof deduplication === "object" && toTrimmedString$2(deduplication.key));
7810
+ if (resolveStoredUploadAccessScope(metadata) === "owner" && !isDeduplicated) {
7811
+ await deleteGridFsFile(filesystemDb, bucket, bucketName, file._id);
7812
+ continue;
7813
+ }
7814
+ await removeUserFromSharedFile(files, file._id);
7815
+ }
7816
+ await Promise.all([uploadIds.length > 0 ? UploadChunk.deleteMany({ uploadId: { $in: uploadIds } }) : Promise.resolve(), UploadSession.deleteMany({ userId })]);
7817
+ };
7818
+ //#endregion
7819
+ //#region src/accountDeletion.ts
7820
+ var RpcbaseUserAccountNotFoundError = class extends Error {
7821
+ constructor(userId) {
7822
+ super(`RPCBase user account '${userId}' was not found.`);
7823
+ this.name = "RpcbaseUserAccountNotFoundError";
7824
+ }
7825
+ };
7826
+ var toTrimmedString$1 = (value) => typeof value === "string" ? value.trim() : "";
7827
+ var normalizeRequiredId = (value, label) => {
7828
+ const normalized = value.trim();
7829
+ if (!normalized) throw new Error(`${label} is required.`);
7830
+ return normalized;
7831
+ };
7832
+ var toStringArray = (value) => {
7833
+ if (!Array.isArray(value)) return [];
7834
+ return value.map(toTrimmedString$1).filter(Boolean);
7835
+ };
7836
+ var getTenantRoleIds = (value) => {
7837
+ if (value instanceof Map) return Array.from(value.keys()).map(toTrimmedString$1).filter(Boolean);
7838
+ if (!value || typeof value !== "object" || Array.isArray(value)) return [];
7839
+ return Object.keys(value).map(toTrimmedString$1).filter(Boolean);
7840
+ };
7841
+ var getAccountTenantIds = (user) => Array.from(/* @__PURE__ */ new Set([...toStringArray(user.tenants), ...getTenantRoleIds(user.tenantRoles)]));
7842
+ var getNativeDb = (connection) => {
7843
+ if (!connection.db) throw new Error(`Mongo database '${connection.name}' is unavailable.`);
7844
+ return connection.db;
7845
+ };
7846
+ var listCollectionNames = async (connection) => {
7847
+ return (await getNativeDb(connection).listCollections({}, { nameOnly: true }).toArray()).map((collection) => toTrimmedString$1(collection.name)).filter((collectionName) => collectionName && !collectionName.startsWith("system."));
7848
+ };
7849
+ var buildTenantMembershipFilter = (tenantId) => {
7850
+ const clauses = [{ tenants: tenantId }];
7851
+ if (!tenantId.includes(".") && !tenantId.startsWith("$")) clauses.push({ [`tenantRoles.${tenantId}`]: { $exists: true } });
7852
+ return { $or: clauses };
7853
+ };
7854
+ var deleteRpcbaseTenantUserData = async (tenantId, userId) => {
7855
+ const modelCtx = { tenantId };
7856
+ const [Notification, NotificationDelivery, NotificationSettings] = await Promise.all([
7857
+ models.getUnsafe("RBNotification", modelCtx),
7858
+ models.getUnsafe("RBNotificationDelivery", modelCtx),
7859
+ models.getUnsafe("RBNotificationSettings", modelCtx)
7860
+ ]);
7861
+ await Promise.all([
7862
+ Notification.deleteMany({ userId }),
7863
+ NotificationDelivery.deleteMany({ userId }),
7864
+ NotificationSettings.deleteMany({ userId }),
7865
+ deleteRpcbaseUserUploads(tenantId, userId)
7866
+ ]);
7867
+ };
7868
+ var deleteTenantGlobalData = async (connection, collectionNames, userCollectionName, tenantId, session) => {
7869
+ for (const collectionName of collectionNames) {
7870
+ if (collectionName === userCollectionName) continue;
7871
+ const collection = connection.collection(collectionName);
7872
+ if (await collection.findOne({ tenantId }, {
7873
+ projection: { _id: 1 },
7874
+ session
7875
+ })) await collection.deleteMany({ tenantId }, { session });
7876
+ }
7877
+ };
7878
+ var deleteGlobalUserAccount = async ({ connection, users, userDocumentId }) => {
7879
+ const collectionNames = await listCollectionNames(connection);
7880
+ const session = await connection.startSession();
7881
+ let result = {
7882
+ tenantIds: [],
7883
+ deletedTenantIds: []
7884
+ };
7885
+ try {
7886
+ await session.withTransaction(async () => {
7887
+ const user = await users.findOne({ _id: userDocumentId }, {
7888
+ projection: {
7889
+ _id: 1,
7890
+ tenants: 1,
7891
+ tenantRoles: 1
7892
+ },
7893
+ session
7894
+ });
7895
+ if (!user) throw new RpcbaseUserAccountNotFoundError(String(userDocumentId));
7896
+ const tenantIds = getAccountTenantIds(user);
7897
+ const deletedTenantIds = [];
7898
+ for (const tenantId of tenantIds) if (!await users.findOne({
7899
+ _id: { $ne: userDocumentId },
7900
+ ...buildTenantMembershipFilter(tenantId)
7901
+ }, {
7902
+ projection: { _id: 1 },
7903
+ session
7904
+ })) deletedTenantIds.push(tenantId);
7905
+ for (const tenantId of deletedTenantIds) await deleteTenantGlobalData(connection, collectionNames, users.collectionName, tenantId, session);
7906
+ if ((await users.deleteOne({ _id: userDocumentId }, { session })).deletedCount !== 1) throw new Error("RPCBase user account was not deleted.");
7907
+ result = {
7908
+ tenantIds,
7909
+ deletedTenantIds
7910
+ };
7911
+ });
7912
+ return result;
7913
+ } finally {
7914
+ await session.endSession();
7915
+ }
7916
+ };
7917
+ var dropTenantDatabases = async (tenantId) => {
7918
+ const tenantModel = await models.getUnsafe("RBNotification", { tenantId });
7919
+ const filesystemConnection = await getTenantFilesystemDb(tenantId);
7920
+ return (await Promise.allSettled([tenantModel.db.dropDatabase(), filesystemConnection.dropDatabase()])).every((result) => result.status === "fulfilled");
7921
+ };
7922
+ var deleteRpcbaseUserAccount = async ({ userId: rawUserId, deleteTenantUserData }) => {
7923
+ const userId = normalizeRequiredId(rawUserId, "User ID");
7924
+ const User = await models.getGlobal("RBUser", { req: { session: null } });
7925
+ const account = await User.findById(userId, {
7926
+ _id: 1,
7927
+ tenants: 1,
7928
+ tenantRoles: 1
7929
+ }).lean();
7930
+ if (!account?._id) throw new RpcbaseUserAccountNotFoundError(userId);
7931
+ const initialTenantIds = getAccountTenantIds(account);
7932
+ for (const tenantId of initialTenantIds) {
7933
+ await deleteTenantUserData?.({
7934
+ tenantId,
7935
+ userId
7936
+ });
7937
+ await deleteRpcbaseTenantUserData(tenantId, userId);
7938
+ }
7939
+ const users = User.collection;
7940
+ const globalDeletion = await deleteGlobalUserAccount({
7941
+ connection: User.db,
7942
+ users,
7943
+ userDocumentId: account._id
7944
+ });
7945
+ const failedTenantDatabaseIds = [];
7946
+ for (const tenantId of globalDeletion.deletedTenantIds) if (!await dropTenantDatabases(tenantId)) failedTenantDatabaseIds.push(tenantId);
7947
+ return {
7948
+ ...globalDeletion,
7949
+ failedTenantDatabaseIds
7950
+ };
7951
+ };
7952
+ //#endregion
7953
+ //#region src/userSessionDeletion.ts
7954
+ var toTrimmedString = (value) => typeof value === "string" ? value.trim() : "";
7955
+ var getSessionStore = (value) => {
7956
+ if (!value || typeof value !== "object") return null;
7957
+ const candidate = value;
7958
+ return typeof candidate.destroy === "function" ? candidate : null;
7959
+ };
7960
+ var getCurrentSession = (value) => {
7961
+ if (!value || typeof value !== "object") return null;
7962
+ const candidate = value;
7963
+ return typeof candidate.destroy === "function" ? candidate : null;
7964
+ };
7965
+ var destroyStoredSession = async (store, sessionId) => {
7966
+ await new Promise((resolve, reject) => {
7967
+ store.destroy(sessionId, (error) => {
7968
+ if (error) {
7969
+ reject(error);
7970
+ return;
7971
+ }
7972
+ resolve();
7973
+ });
7974
+ });
7975
+ };
7976
+ var loadStoredSessions = async (store) => {
7977
+ if (!store.all) return null;
7978
+ return await new Promise((resolve, reject) => {
7979
+ store.all?.((error, sessions) => {
7980
+ if (error) {
7981
+ reject(error);
7982
+ return;
7983
+ }
7984
+ resolve(sessions ?? null);
7985
+ });
7986
+ });
7987
+ };
7988
+ var destroyOtherUserSessions = async (request, userId) => {
7989
+ const store = getSessionStore(request.sessionStore);
7990
+ if (!store?.all) return 0;
7991
+ const storedSessions = await loadStoredSessions(store);
7992
+ if (!storedSessions) return 0;
7993
+ const currentSessionId = toTrimmedString(request.sessionID);
7994
+ const sessionIds = (Array.isArray(storedSessions) ? storedSessions.map((session) => [toTrimmedString(session.id), session]) : Object.entries(storedSessions)).filter(([, session]) => toTrimmedString(session.user?.id) === userId).map(([sessionId]) => sessionId).filter((sessionId) => sessionId && sessionId !== currentSessionId);
7995
+ await Promise.all(sessionIds.map((sessionId) => destroyStoredSession(store, sessionId)));
7996
+ return sessionIds.length;
7997
+ };
7998
+ var destroyCurrentSession = async (request) => {
7999
+ const session = getCurrentSession(request.session);
8000
+ if (!session) return 0;
8001
+ session.user = void 0;
8002
+ await new Promise((resolve, reject) => {
8003
+ session.destroy((error) => {
8004
+ if (error) {
8005
+ reject(error);
8006
+ return;
8007
+ }
8008
+ resolve();
8009
+ });
8010
+ });
8011
+ return 1;
8012
+ };
8013
+ var destroyRpcbaseUserSessions = async ({ request, userId: rawUserId }) => {
8014
+ const userId = rawUserId.trim();
8015
+ if (!userId) throw new Error("User ID is required.");
8016
+ const errors = [];
8017
+ let destroyedSessionCount = 0;
8018
+ try {
8019
+ destroyedSessionCount += await destroyOtherUserSessions(request, userId);
8020
+ } catch (error) {
8021
+ errors.push(error);
8022
+ }
8023
+ try {
8024
+ destroyedSessionCount += await destroyCurrentSession(request);
8025
+ } catch (error) {
8026
+ errors.push(error);
8027
+ }
8028
+ if (errors.length === 1) throw errors[0];
8029
+ if (errors.length > 1) throw new AggregateError(errors, "Could not destroy every RPCBase user session.");
8030
+ return { destroyedSessionCount };
8031
+ };
8032
+ //#endregion
8033
+ export { DISPOSABLE_EMAIL_ERROR_CODE, EmailDeliveryError, INVALID_EMAIL_DOMAIN_ERROR_CODE, RPCBASE_SERVICE_WORKER_MESSAGE_SOURCE, RPCBASE_SERVICE_WORKER_PATH, RpcbaseUserAccountNotFoundError, bindShutdownSignals, clearEmailDomainValidationCache, configureEmailTransport, createResendEmailTransport, deleteRpcbaseUserAccount, destroyRpcbaseUserSessions, getDerivedKey, getEmailAddressDomain, getEmailAddressValidationErrorCode, hashPassword, hashPasswordForStorage, initServer, isEmailAddressValid, onShutdown, registerRpcbaseServiceWorkerRoute, rpcbaseServiceWorkerRequestHandler, rpcbaseServiceWorkerSource, sendEmail, ssrMiddleware, validateEmailDomain, verifyPasswordFromStorage };
7748
8034
 
7749
8035
  //# sourceMappingURL=index.js.map