@strapi-community/plugin-io 5.0.6 → 5.2.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.
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  const require$$0$4 = require("socket.io");
3
3
  const node_async_hooks = require("node:async_hooks");
4
- const dates$1 = require("date-fns");
5
4
  const require$$1 = require("crypto");
5
+ const dates$1 = require("date-fns");
6
6
  const require$$0$5 = require("child_process");
7
7
  const require$$0$6 = require("os");
8
8
  const require$$0$8 = require("path");
@@ -35,8 +35,8 @@ function _interopNamespace(e) {
35
35
  return Object.freeze(n);
36
36
  }
37
37
  const require$$0__default = /* @__PURE__ */ _interopDefault(require$$0$4);
38
- const dates__namespace = /* @__PURE__ */ _interopNamespace(dates$1);
39
38
  const require$$1__default = /* @__PURE__ */ _interopDefault(require$$1);
39
+ const dates__namespace = /* @__PURE__ */ _interopNamespace(dates$1);
40
40
  const require$$0__default$1 = /* @__PURE__ */ _interopDefault(require$$0$5);
41
41
  const require$$0__default$2 = /* @__PURE__ */ _interopDefault(require$$0$6);
42
42
  const require$$0__default$4 = /* @__PURE__ */ _interopDefault(require$$0$8);
@@ -83,10 +83,10 @@ const require$$0$3 = {
83
83
  strapi: strapi$1
84
84
  };
85
85
  const pluginPkg = require$$0$3;
86
- const pluginId$7 = pluginPkg.strapi.name;
87
- var pluginId_1 = { pluginId: pluginId$7 };
88
- const { pluginId: pluginId$6 } = pluginId_1;
89
- function getService$3({ name, plugin = pluginId$6, type: type2 = "plugin" }) {
86
+ const pluginId$9 = pluginPkg.strapi.name;
87
+ var pluginId_1 = { pluginId: pluginId$9 };
88
+ const { pluginId: pluginId$8 } = pluginId_1;
89
+ function getService$3({ name, plugin = pluginId$8, type: type2 = "plugin" }) {
90
90
  let serviceUID = `${type2}::${plugin}`;
91
91
  if (name && name.length) {
92
92
  serviceUID += `.${name}`;
@@ -108,11 +108,24 @@ async function handshake$2(socket, next) {
108
108
  try {
109
109
  let room;
110
110
  if (strategy2 && strategy2.length) {
111
- const strategyType = strategy2 === "jwt" ? "role" : "token";
111
+ let strategyType;
112
+ if (strategy2 === "jwt") {
113
+ strategyType = "role";
114
+ } else if (strategy2 === "admin-jwt") {
115
+ strategyType = "admin";
116
+ } else {
117
+ strategyType = "token";
118
+ }
112
119
  const ctx = await strategyService[strategyType].authenticate(auth);
113
120
  room = strategyService[strategyType].getRoomName(ctx);
121
+ if (strategyType === "admin") {
122
+ socket.adminUser = ctx;
123
+ }
114
124
  } else if (strapi.plugin("users-permissions")) {
115
- const role = await strapi.query("plugin::users-permissions.role").findOne({ where: { type: "public" }, select: ["id", "name"] });
125
+ const role = await strapi.documents("plugin::users-permissions.role").findFirst({
126
+ filters: { type: "public" },
127
+ fields: ["id", "name"]
128
+ });
116
129
  room = strategyService["role"].getRoomName(role);
117
130
  }
118
131
  if (room) {
@@ -143,7 +156,7 @@ var constants$7 = {
143
156
  const { Server } = require$$0__default.default;
144
157
  const { handshake } = middleware;
145
158
  const { getService: getService$1 } = getService_1;
146
- const { pluginId: pluginId$5 } = pluginId_1;
159
+ const { pluginId: pluginId$7 } = pluginId_1;
147
160
  const { API_TOKEN_TYPE: API_TOKEN_TYPE$1 } = constants$7;
148
161
  let SocketIO$2 = class SocketIO {
149
162
  constructor(options) {
@@ -263,11 +276,11 @@ function requireSanitizeSensitiveFields() {
263
276
  return sanitizeSensitiveFields;
264
277
  }
265
278
  const { SocketIO: SocketIO2 } = structures;
266
- const { pluginId: pluginId$4 } = pluginId_1;
279
+ const { pluginId: pluginId$6 } = pluginId_1;
267
280
  async function bootstrapIO$1({ strapi: strapi2 }) {
268
- const settingsService = strapi2.plugin(pluginId$4).service("settings");
281
+ const settingsService = strapi2.plugin(pluginId$6).service("settings");
269
282
  const settings2 = await settingsService.getSettings();
270
- const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
283
+ const monitoringService = strapi2.plugin(pluginId$6).service("monitoring");
271
284
  const serverOptions = {
272
285
  cors: {
273
286
  origin: settings2.cors?.origins || ["http://localhost:3000"],
@@ -337,31 +350,56 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
337
350
  return next(new Error("Max connections reached"));
338
351
  }
339
352
  const token = socket.handshake.auth?.token || socket.handshake.query?.token;
353
+ const strategy2 = socket.handshake.auth?.strategy;
354
+ const isAdmin = socket.handshake.auth?.isAdmin === true;
340
355
  if (token) {
341
- try {
342
- const decoded = await strapi2.plugin("users-permissions").service("jwt").verify(token);
343
- strapi2.log.info(`socket.io: JWT decoded - user id: ${decoded.id}`);
344
- if (decoded.id) {
345
- const users = await strapi2.documents("plugin::users-permissions.user").findMany({
346
- filters: { id: decoded.id },
347
- populate: { role: true },
348
- limit: 1
349
- });
350
- const user = users.length > 0 ? users[0] : null;
351
- if (user) {
356
+ if (isAdmin || strategy2 === "admin-jwt") {
357
+ try {
358
+ const presenceController = strapi2.plugin(pluginId$6).controller("presence");
359
+ const session = presenceController.consumeSessionToken(token);
360
+ if (session) {
352
361
  socket.user = {
353
- id: user.id,
354
- username: user.username,
355
- email: user.email,
356
- role: user.role?.name || "authenticated"
362
+ id: session.userId,
363
+ username: `${session.user.firstname || ""} ${session.user.lastname || ""}`.trim() || `Admin ${session.userId}`,
364
+ email: session.user.email || `admin-${session.userId}`,
365
+ role: "strapi-super-admin",
366
+ isAdmin: true
357
367
  };
358
- strapi2.log.info(`socket.io: User authenticated - ${user.username} (${user.email})`);
368
+ socket.adminUser = session.user;
369
+ presenceController.registerSocket(socket.id, token);
370
+ strapi2.log.info(`socket.io: Admin authenticated - ${socket.user.username} (ID: ${session.userId})`);
359
371
  } else {
360
- strapi2.log.warn(`socket.io: User not found for id: ${decoded.id}`);
372
+ strapi2.log.warn(`socket.io: Admin session token invalid or expired`);
361
373
  }
374
+ } catch (err) {
375
+ strapi2.log.warn(`socket.io: Admin session verification failed: ${err.message}`);
376
+ }
377
+ } else {
378
+ try {
379
+ const decoded = await strapi2.plugin("users-permissions").service("jwt").verify(token);
380
+ strapi2.log.info(`socket.io: JWT decoded - user id: ${decoded.id}`);
381
+ if (decoded.id) {
382
+ const users = await strapi2.documents("plugin::users-permissions.user").findMany({
383
+ filters: { id: decoded.id },
384
+ populate: { role: true },
385
+ limit: 1
386
+ });
387
+ const user = users.length > 0 ? users[0] : null;
388
+ if (user) {
389
+ socket.user = {
390
+ id: user.id,
391
+ username: user.username,
392
+ email: user.email,
393
+ role: user.role?.name || "authenticated"
394
+ };
395
+ strapi2.log.info(`socket.io: User authenticated - ${user.username} (${user.email})`);
396
+ } else {
397
+ strapi2.log.warn(`socket.io: User not found for id: ${decoded.id}`);
398
+ }
399
+ }
400
+ } catch (err) {
401
+ strapi2.log.warn(`socket.io: JWT verification failed: ${err.message}`);
362
402
  }
363
- } catch (err) {
364
- strapi2.log.warn(`socket.io: JWT verification failed: ${err.message}`);
365
403
  }
366
404
  } else {
367
405
  strapi2.log.debug(`socket.io: No token provided, connecting as public`);
@@ -400,6 +438,11 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
400
438
  }
401
439
  });
402
440
  }
441
+ const presenceService = strapi2.plugin(pluginId$6).service("presence");
442
+ const previewService = strapi2.plugin(pluginId$6).service("preview");
443
+ if (settings2.presence?.enabled !== false) {
444
+ presenceService.startCleanupInterval();
445
+ }
403
446
  io2.server.on("connection", (socket) => {
404
447
  const clientIp = socket.handshake.address || "unknown";
405
448
  const username = socket.user?.username || "anonymous";
@@ -411,6 +454,10 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
411
454
  user: socket.user || null
412
455
  });
413
456
  }
457
+ if (settings2.presence?.enabled !== false) {
458
+ const user = socket.user || socket.adminUser;
459
+ presenceService.registerConnection(socket.id, user);
460
+ }
414
461
  if (settings2.rooms?.autoJoinByRole) {
415
462
  const userRole = socket.user?.role || "public";
416
463
  const rooms = settings2.rooms.autoJoinByRole[userRole] || [];
@@ -455,6 +502,70 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
455
502
  const rooms = Array.from(socket.rooms).filter((r) => r !== socket.id);
456
503
  if (callback) callback({ success: true, rooms });
457
504
  });
505
+ socket.on("presence:join", async ({ uid, documentId }, callback) => {
506
+ if (settings2.presence?.enabled === false) {
507
+ if (callback) callback({ success: false, error: "Presence is disabled" });
508
+ return;
509
+ }
510
+ if (!uid || !documentId) {
511
+ if (callback) callback({ success: false, error: "uid and documentId are required" });
512
+ return;
513
+ }
514
+ const result = await presenceService.joinEntity(socket.id, uid, documentId);
515
+ if (callback) callback(result);
516
+ });
517
+ socket.on("presence:leave", async ({ uid, documentId }, callback) => {
518
+ if (settings2.presence?.enabled === false) {
519
+ if (callback) callback({ success: false, error: "Presence is disabled" });
520
+ return;
521
+ }
522
+ if (!uid || !documentId) {
523
+ if (callback) callback({ success: false, error: "uid and documentId are required" });
524
+ return;
525
+ }
526
+ const result = await presenceService.leaveEntity(socket.id, uid, documentId);
527
+ if (callback) callback(result);
528
+ });
529
+ socket.on("presence:heartbeat", (callback) => {
530
+ const result = presenceService.heartbeat(socket.id);
531
+ if (callback) callback(result);
532
+ });
533
+ socket.on("presence:typing", ({ uid, documentId, fieldName }) => {
534
+ if (settings2.presence?.enabled === false) return;
535
+ presenceService.broadcastTyping(socket.id, uid, documentId, fieldName);
536
+ });
537
+ socket.on("presence:check", async ({ uid, documentId }, callback) => {
538
+ if (settings2.presence?.enabled === false) {
539
+ if (callback) callback({ success: false, error: "Presence is disabled" });
540
+ return;
541
+ }
542
+ const editors = await presenceService.getEntityEditors(uid, documentId);
543
+ if (callback) callback({ success: true, editors, isBeingEdited: editors.length > 0 });
544
+ });
545
+ socket.on("preview:subscribe", async ({ uid, documentId }, callback) => {
546
+ if (settings2.livePreview?.enabled === false) {
547
+ if (callback) callback({ success: false, error: "Live preview is disabled" });
548
+ return;
549
+ }
550
+ if (!uid || !documentId) {
551
+ if (callback) callback({ success: false, error: "uid and documentId are required" });
552
+ return;
553
+ }
554
+ const result = await previewService.subscribe(socket.id, uid, documentId);
555
+ if (callback) callback(result);
556
+ });
557
+ socket.on("preview:unsubscribe", ({ uid, documentId }, callback) => {
558
+ if (!uid || !documentId) {
559
+ if (callback) callback({ success: false, error: "uid and documentId are required" });
560
+ return;
561
+ }
562
+ const result = previewService.unsubscribe(socket.id, uid, documentId);
563
+ if (callback) callback(result);
564
+ });
565
+ socket.on("preview:field-change", ({ uid, documentId, fieldName, value }) => {
566
+ if (settings2.livePreview?.enabled === false) return;
567
+ previewService.emitFieldChange(socket.id, uid, documentId, fieldName, value);
568
+ });
458
569
  socket.on("subscribe-entity", async ({ uid, id }, callback) => {
459
570
  if (settings2.entitySubscriptions?.enabled === false) {
460
571
  if (callback) callback({ success: false, error: "Entity subscriptions are disabled" });
@@ -589,7 +700,7 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
589
700
  strapi2.log.debug(`socket.io: Private message from ${socket.id} to ${to}`);
590
701
  if (callback) callback({ success: true });
591
702
  });
592
- socket.on("disconnect", (reason) => {
703
+ socket.on("disconnect", async (reason) => {
593
704
  if (settings2.monitoring?.enableConnectionLogging) {
594
705
  strapi2.log.info(`socket.io: Client disconnected (id: ${socket.id}, user: ${username}, reason: ${reason})`);
595
706
  monitoringService.logEvent("disconnect", {
@@ -598,6 +709,19 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
598
709
  user: socket.user || null
599
710
  });
600
711
  }
712
+ if (settings2.presence?.enabled !== false) {
713
+ await presenceService.unregisterConnection(socket.id);
714
+ }
715
+ if (settings2.livePreview?.enabled !== false) {
716
+ previewService.cleanupSocket(socket.id);
717
+ }
718
+ try {
719
+ const presenceController = strapi2.plugin(pluginId$6).controller("presence");
720
+ if (presenceController?.unregisterSocket) {
721
+ presenceController.unregisterSocket(socket.id);
722
+ }
723
+ } catch (e) {
724
+ }
601
725
  });
602
726
  socket.on("error", (error2) => {
603
727
  strapi2.log.error(`socket.io: Socket error (id: ${socket.id}): ${error2.message}`);
@@ -741,17 +865,52 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
741
865
  }
742
866
  });
743
867
  const enabledContentTypes = allContentTypes.size;
868
+ strapi2.$io.presence = {
869
+ /**
870
+ * Get editors for an entity
871
+ */
872
+ getEditors: (uid, documentId) => presenceService.getEntityEditors(uid, documentId),
873
+ /**
874
+ * Check if entity is being edited
875
+ */
876
+ isBeingEdited: (uid, documentId) => presenceService.isEntityBeingEdited(uid, documentId),
877
+ /**
878
+ * Get presence statistics
879
+ */
880
+ getStats: () => presenceService.getStats()
881
+ };
882
+ strapi2.$io.preview = {
883
+ /**
884
+ * Emit draft change to preview subscribers
885
+ */
886
+ emitDraftChange: (uid, documentId, data, diff2) => previewService.emitDraftChange(uid, documentId, data, diff2),
887
+ /**
888
+ * Emit publish event
889
+ */
890
+ emitPublish: (uid, documentId, data) => previewService.emitPublish(uid, documentId, data),
891
+ /**
892
+ * Emit unpublish event
893
+ */
894
+ emitUnpublish: (uid, documentId) => previewService.emitUnpublish(uid, documentId),
895
+ /**
896
+ * Get preview statistics
897
+ */
898
+ getStats: () => previewService.getStats()
899
+ };
744
900
  const origins = settings2.cors?.origins?.join(", ") || "http://localhost:3000";
745
901
  const features = [];
746
902
  if (settings2.redis?.enabled) features.push("Redis");
747
903
  if (settings2.namespaces?.enabled) features.push(`Namespaces(${Object.keys(settings2.namespaces.list || {}).length})`);
748
904
  if (settings2.security?.rateLimiting?.enabled) features.push("RateLimit");
905
+ if (settings2.presence?.enabled !== false) features.push("Presence");
906
+ if (settings2.livePreview?.enabled !== false) features.push("LivePreview");
907
+ if (settings2.fieldLevelChanges?.enabled !== false) features.push("FieldDiff");
749
908
  strapi2.log.info(`socket.io: Plugin initialized`);
750
- strapi2.log.info(` Origins: ${origins}`);
751
- strapi2.log.info(` Content Types: ${enabledContentTypes}`);
752
- strapi2.log.info(` Max Connections: ${settings2.connection?.maxConnections || 1e3}`);
909
+ strapi2.log.info(` - Origins: ${origins}`);
910
+ strapi2.log.info(` - Content Types: ${enabledContentTypes}`);
911
+ strapi2.log.info(` - Max Connections: ${settings2.connection?.maxConnections || 1e3}`);
753
912
  if (features.length > 0) {
754
- strapi2.log.info(` Features: ${features.join(", ")}`);
913
+ strapi2.log.info(` - Features: ${features.join(", ")}`);
755
914
  }
756
915
  }
757
916
  var io = { bootstrapIO: bootstrapIO$1 };
@@ -825,7 +984,7 @@ function getTransactionCtx() {
825
984
  }
826
985
  return transactionCtx;
827
986
  }
828
- const { pluginId: pluginId$3 } = pluginId_1;
987
+ const { pluginId: pluginId$5 } = pluginId_1;
829
988
  function scheduleAfterTransaction(callback, delay = 0) {
830
989
  const runner = () => setTimeout(callback, delay);
831
990
  const ctx = getTransactionCtx();
@@ -912,17 +1071,47 @@ async function bootstrapLifecycles$1({ strapi: strapi2 }) {
912
1071
  }, 50);
913
1072
  }
914
1073
  };
1074
+ subscriber.beforeUpdate = async (event) => {
1075
+ if (!isActionEnabled(strapi2, uid, "update")) return;
1076
+ const fieldLevelEnabled = strapi2.$ioSettings?.fieldLevelChanges?.enabled !== false;
1077
+ if (!fieldLevelEnabled) return;
1078
+ try {
1079
+ const documentId = event.params.where?.documentId || event.params.documentId;
1080
+ if (!documentId) return;
1081
+ const existing = await strapi2.documents(uid).findOne({ documentId });
1082
+ if (existing) {
1083
+ if (!event.state.io) event.state.io = {};
1084
+ event.state.io.previousData = JSON.parse(JSON.stringify(existing));
1085
+ }
1086
+ } catch (error2) {
1087
+ strapi2.log.debug(`socket.io: Could not fetch previous data for diff: ${error2.message}`);
1088
+ }
1089
+ };
915
1090
  subscriber.afterUpdate = async (event) => {
916
1091
  if (!isActionEnabled(strapi2, uid, "update")) return;
917
- const eventData = {
918
- event: "update",
919
- schema: event.model,
920
- data: JSON.parse(JSON.stringify(event.result))
921
- // Deep clone
922
- };
1092
+ const newData = JSON.parse(JSON.stringify(event.result));
1093
+ const previousData = event.state.io?.previousData || null;
1094
+ const modelInfo = { singularName: event.model.singularName, uid: event.model.uid };
923
1095
  scheduleAfterTransaction(() => {
924
1096
  try {
925
- strapi2.$io.emit(eventData);
1097
+ const diffService = strapi2.plugin(pluginId$5).service("diff");
1098
+ const previewService = strapi2.plugin(pluginId$5).service("preview");
1099
+ const fieldLevelEnabled = strapi2.$ioSettings?.fieldLevelChanges?.enabled !== false;
1100
+ let eventPayload;
1101
+ if (fieldLevelEnabled && previousData && diffService) {
1102
+ eventPayload = diffService.createEventPayload("update", modelInfo, previousData, newData);
1103
+ } else {
1104
+ eventPayload = {
1105
+ event: "update",
1106
+ schema: modelInfo,
1107
+ data: newData
1108
+ };
1109
+ }
1110
+ strapi2.$io.emit(eventPayload);
1111
+ if (previewService && newData.documentId) {
1112
+ const diff2 = fieldLevelEnabled ? eventPayload.diff : null;
1113
+ previewService.emitDraftChange(uid, newData.documentId, newData, diff2);
1114
+ }
926
1115
  } catch (error2) {
927
1116
  strapi2.log.debug(`socket.io: Could not emit update event for ${uid}:`, error2.message);
928
1117
  }
@@ -1023,14 +1212,14 @@ var config$1 = {
1023
1212
  validator(config2) {
1024
1213
  }
1025
1214
  };
1026
- const { pluginId: pluginId$2 } = pluginId_1;
1215
+ const { pluginId: pluginId$4 } = pluginId_1;
1027
1216
  var settings$3 = ({ strapi: strapi2 }) => ({
1028
1217
  /**
1029
1218
  * GET /io/settings
1030
1219
  * Retrieve current plugin settings
1031
1220
  */
1032
1221
  async getSettings(ctx) {
1033
- const settingsService = strapi2.plugin(pluginId$2).service("settings");
1222
+ const settingsService = strapi2.plugin(pluginId$4).service("settings");
1034
1223
  const settings2 = await settingsService.getSettings();
1035
1224
  ctx.body = { data: settings2 };
1036
1225
  },
@@ -1039,7 +1228,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1039
1228
  * Update plugin settings and hot-reload Socket.IO
1040
1229
  */
1041
1230
  async updateSettings(ctx) {
1042
- const settingsService = strapi2.plugin(pluginId$2).service("settings");
1231
+ const settingsService = strapi2.plugin(pluginId$4).service("settings");
1043
1232
  const { body } = ctx.request;
1044
1233
  await settingsService.getSettings();
1045
1234
  const updatedSettings = await settingsService.setSettings(body);
@@ -1072,7 +1261,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1072
1261
  * Get connection and event statistics
1073
1262
  */
1074
1263
  async getStats(ctx) {
1075
- const monitoringService = strapi2.plugin(pluginId$2).service("monitoring");
1264
+ const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
1076
1265
  const connectionStats = monitoringService.getConnectionStats();
1077
1266
  const eventStats = monitoringService.getEventStats();
1078
1267
  ctx.body = {
@@ -1087,7 +1276,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1087
1276
  * Get recent event log
1088
1277
  */
1089
1278
  async getEventLog(ctx) {
1090
- const monitoringService = strapi2.plugin(pluginId$2).service("monitoring");
1279
+ const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
1091
1280
  const limit = parseInt(ctx.query.limit) || 50;
1092
1281
  const log = monitoringService.getEventLog(limit);
1093
1282
  ctx.body = { data: log };
@@ -1097,7 +1286,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1097
1286
  * Send a test event
1098
1287
  */
1099
1288
  async sendTestEvent(ctx) {
1100
- const monitoringService = strapi2.plugin(pluginId$2).service("monitoring");
1289
+ const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
1101
1290
  const { eventName, data } = ctx.request.body;
1102
1291
  try {
1103
1292
  const result = monitoringService.sendTestEvent(eventName || "test", data || {});
@@ -1111,7 +1300,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1111
1300
  * Reset monitoring statistics
1112
1301
  */
1113
1302
  async resetStats(ctx) {
1114
- const monitoringService = strapi2.plugin(pluginId$2).service("monitoring");
1303
+ const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
1115
1304
  monitoringService.resetStats();
1116
1305
  ctx.body = { data: { success: true } };
1117
1306
  },
@@ -1135,7 +1324,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1135
1324
  * Get lightweight stats for dashboard widget
1136
1325
  */
1137
1326
  async getMonitoringStats(ctx) {
1138
- const monitoringService = strapi2.plugin(pluginId$2).service("monitoring");
1327
+ const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
1139
1328
  const connectionStats = monitoringService.getConnectionStats();
1140
1329
  const eventStats = monitoringService.getEventStats();
1141
1330
  ctx.body = {
@@ -1154,13 +1343,245 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1154
1343
  };
1155
1344
  }
1156
1345
  });
1346
+ const { randomUUID, createHash } = require$$1__default.default;
1347
+ const sessionTokens = /* @__PURE__ */ new Map();
1348
+ const activeSockets = /* @__PURE__ */ new Map();
1349
+ const refreshThrottle = /* @__PURE__ */ new Map();
1350
+ const SESSION_TTL = 10 * 60 * 1e3;
1351
+ const REFRESH_COOLDOWN = 30 * 1e3;
1352
+ const CLEANUP_INTERVAL = 2 * 60 * 1e3;
1353
+ const hashToken = (token) => {
1354
+ return createHash("sha256").update(token).digest("hex");
1355
+ };
1356
+ setInterval(() => {
1357
+ const now = Date.now();
1358
+ let cleaned = 0;
1359
+ for (const [tokenHash, session] of sessionTokens.entries()) {
1360
+ if (session.expiresAt < now) {
1361
+ sessionTokens.delete(tokenHash);
1362
+ cleaned++;
1363
+ }
1364
+ }
1365
+ for (const [userId, lastRefresh] of refreshThrottle.entries()) {
1366
+ if (now - lastRefresh > 60 * 60 * 1e3) {
1367
+ refreshThrottle.delete(userId);
1368
+ }
1369
+ }
1370
+ if (cleaned > 0) {
1371
+ console.log(`[plugin-io] [CLEANUP] Removed ${cleaned} expired session tokens`);
1372
+ }
1373
+ }, CLEANUP_INTERVAL);
1374
+ var presence$3 = ({ strapi: strapi2 }) => ({
1375
+ /**
1376
+ * Creates a session token for admin users to connect to Socket.IO
1377
+ * Implements rate limiting and secure token storage
1378
+ * @param {object} ctx - Koa context
1379
+ */
1380
+ async createSession(ctx) {
1381
+ const adminUser = ctx.state.user;
1382
+ if (!adminUser) {
1383
+ strapi2.log.warn("[plugin-io] Presence session requested without admin user");
1384
+ return ctx.unauthorized("Admin authentication required");
1385
+ }
1386
+ const lastRefresh = refreshThrottle.get(adminUser.id);
1387
+ const now = Date.now();
1388
+ if (lastRefresh && now - lastRefresh < REFRESH_COOLDOWN) {
1389
+ const waitTime = Math.ceil((REFRESH_COOLDOWN - (now - lastRefresh)) / 1e3);
1390
+ strapi2.log.warn(`[plugin-io] Rate limit: User ${adminUser.id} must wait ${waitTime}s`);
1391
+ return ctx.tooManyRequests(`Please wait ${waitTime} seconds before requesting a new session`);
1392
+ }
1393
+ try {
1394
+ const token = randomUUID();
1395
+ const tokenHash = hashToken(token);
1396
+ const expiresAt = now + SESSION_TTL;
1397
+ sessionTokens.set(tokenHash, {
1398
+ tokenHash,
1399
+ userId: adminUser.id,
1400
+ user: {
1401
+ id: adminUser.id,
1402
+ // Only store minimal user data needed for display
1403
+ firstname: adminUser.firstname,
1404
+ lastname: adminUser.lastname
1405
+ },
1406
+ createdAt: now,
1407
+ expiresAt,
1408
+ usageCount: 0,
1409
+ maxUsage: 10
1410
+ // Max reconnects with same token
1411
+ });
1412
+ refreshThrottle.set(adminUser.id, now);
1413
+ strapi2.log.info(`[plugin-io] Presence session created for admin user: ${adminUser.id}`);
1414
+ ctx.body = {
1415
+ token,
1416
+ // Send plaintext token to client (only time it's exposed)
1417
+ expiresAt,
1418
+ refreshAfter: now + SESSION_TTL * 0.7,
1419
+ // Suggest refresh at 70% of TTL
1420
+ wsPath: "/socket.io",
1421
+ wsUrl: `${ctx.protocol}://${ctx.host}`
1422
+ };
1423
+ } catch (error2) {
1424
+ strapi2.log.error("[plugin-io] Failed to create presence session:", error2);
1425
+ return ctx.internalServerError("Failed to create session");
1426
+ }
1427
+ },
1428
+ /**
1429
+ * Validates a session token and tracks usage
1430
+ * Implements usage limits to prevent token abuse
1431
+ * @param {string} token - Session token to validate
1432
+ * @returns {object|null} Session data or null if invalid/expired
1433
+ */
1434
+ consumeSessionToken(token) {
1435
+ if (!token || typeof token !== "string") {
1436
+ return null;
1437
+ }
1438
+ const tokenHash = hashToken(token);
1439
+ const session = sessionTokens.get(tokenHash);
1440
+ if (!session) {
1441
+ strapi2.log.debug("[plugin-io] Token not found in session store");
1442
+ return null;
1443
+ }
1444
+ const now = Date.now();
1445
+ if (session.expiresAt < now) {
1446
+ sessionTokens.delete(tokenHash);
1447
+ strapi2.log.debug("[plugin-io] Token expired, removed from store");
1448
+ return null;
1449
+ }
1450
+ if (session.usageCount >= session.maxUsage) {
1451
+ strapi2.log.warn(`[plugin-io] Token usage limit exceeded for user ${session.userId}`);
1452
+ sessionTokens.delete(tokenHash);
1453
+ return null;
1454
+ }
1455
+ session.usageCount++;
1456
+ session.lastUsed = now;
1457
+ return session;
1458
+ },
1459
+ /**
1460
+ * Registers a socket as using a specific token
1461
+ * @param {string} socketId - Socket ID
1462
+ * @param {string} token - The token being used
1463
+ */
1464
+ registerSocket(socketId, token) {
1465
+ if (!socketId || !token) return;
1466
+ const tokenHash = hashToken(token);
1467
+ activeSockets.set(socketId, tokenHash);
1468
+ },
1469
+ /**
1470
+ * Unregisters a socket when it disconnects
1471
+ * @param {string} socketId - Socket ID
1472
+ */
1473
+ unregisterSocket(socketId) {
1474
+ activeSockets.delete(socketId);
1475
+ },
1476
+ /**
1477
+ * Invalidates all sessions for a specific user (e.g., on logout)
1478
+ * @param {number} userId - User ID to invalidate
1479
+ * @returns {number} Number of sessions invalidated
1480
+ */
1481
+ invalidateUserSessions(userId) {
1482
+ let invalidated = 0;
1483
+ for (const [tokenHash, session] of sessionTokens.entries()) {
1484
+ if (session.userId === userId) {
1485
+ sessionTokens.delete(tokenHash);
1486
+ invalidated++;
1487
+ }
1488
+ }
1489
+ refreshThrottle.delete(userId);
1490
+ strapi2.log.info(`[plugin-io] Invalidated ${invalidated} sessions for user ${userId}`);
1491
+ return invalidated;
1492
+ },
1493
+ /**
1494
+ * Gets session statistics (for monitoring) - internal method
1495
+ * @returns {object} Session statistics
1496
+ */
1497
+ getSessionStatsInternal() {
1498
+ const now = Date.now();
1499
+ let active = 0;
1500
+ let expiringSoon = 0;
1501
+ for (const session of sessionTokens.values()) {
1502
+ if (session.expiresAt > now) {
1503
+ active++;
1504
+ if (session.expiresAt - now < 2 * 60 * 1e3) {
1505
+ expiringSoon++;
1506
+ }
1507
+ }
1508
+ }
1509
+ return {
1510
+ activeSessions: active,
1511
+ expiringSoon,
1512
+ activeSocketConnections: activeSockets.size,
1513
+ sessionTTL: SESSION_TTL,
1514
+ refreshCooldown: REFRESH_COOLDOWN
1515
+ };
1516
+ },
1517
+ /**
1518
+ * HTTP Handler: Gets session statistics for admin monitoring
1519
+ * @param {object} ctx - Koa context
1520
+ */
1521
+ async getSessionStats(ctx) {
1522
+ const adminUser = ctx.state.user;
1523
+ if (!adminUser) {
1524
+ return ctx.unauthorized("Admin authentication required");
1525
+ }
1526
+ try {
1527
+ const stats = this.getSessionStatsInternal();
1528
+ ctx.body = { data: stats };
1529
+ } catch (error2) {
1530
+ strapi2.log.error("[plugin-io] Failed to get session stats:", error2);
1531
+ return ctx.internalServerError("Failed to get session statistics");
1532
+ }
1533
+ },
1534
+ /**
1535
+ * HTTP Handler: Invalidates all sessions for a specific user
1536
+ * @param {object} ctx - Koa context
1537
+ */
1538
+ async invalidateUserSessionsHandler(ctx) {
1539
+ const adminUser = ctx.state.user;
1540
+ if (!adminUser) {
1541
+ return ctx.unauthorized("Admin authentication required");
1542
+ }
1543
+ const { userId } = ctx.params;
1544
+ if (!userId) {
1545
+ return ctx.badRequest("User ID is required");
1546
+ }
1547
+ try {
1548
+ const userIdNum = parseInt(userId, 10);
1549
+ if (isNaN(userIdNum)) {
1550
+ return ctx.badRequest("Invalid user ID");
1551
+ }
1552
+ const invalidated = this.invalidateUserSessions(userIdNum);
1553
+ strapi2.log.info(`[plugin-io] Admin ${adminUser.id} invalidated ${invalidated} sessions for user ${userIdNum}`);
1554
+ ctx.body = {
1555
+ data: {
1556
+ userId: userIdNum,
1557
+ invalidatedSessions: invalidated,
1558
+ message: `Successfully invalidated ${invalidated} session(s)`
1559
+ }
1560
+ };
1561
+ } catch (error2) {
1562
+ strapi2.log.error("[plugin-io] Failed to invalidate user sessions:", error2);
1563
+ return ctx.internalServerError("Failed to invalidate sessions");
1564
+ }
1565
+ }
1566
+ });
1157
1567
  const settings$2 = settings$3;
1568
+ const presence$2 = presence$3;
1158
1569
  var controllers$1 = {
1159
- settings: settings$2
1570
+ settings: settings$2,
1571
+ presence: presence$2
1160
1572
  };
1161
1573
  var admin$1 = {
1162
1574
  type: "admin",
1163
1575
  routes: [
1576
+ // Presence Session - issues JWT token for Socket.IO connection
1577
+ {
1578
+ method: "POST",
1579
+ path: "/presence/session",
1580
+ handler: "presence.createSession",
1581
+ config: {
1582
+ policies: ["admin::isAuthenticatedAdmin"]
1583
+ }
1584
+ },
1164
1585
  {
1165
1586
  method: "GET",
1166
1587
  path: "/settings",
@@ -1232,6 +1653,24 @@ var admin$1 = {
1232
1653
  config: {
1233
1654
  policies: ["admin::isAuthenticatedAdmin"]
1234
1655
  }
1656
+ },
1657
+ // Security: Session statistics
1658
+ {
1659
+ method: "GET",
1660
+ path: "/security/sessions",
1661
+ handler: "presence.getSessionStats",
1662
+ config: {
1663
+ policies: ["admin::isAuthenticatedAdmin"]
1664
+ }
1665
+ },
1666
+ // Security: Invalidate user sessions (force logout)
1667
+ {
1668
+ method: "POST",
1669
+ path: "/security/invalidate/:userId",
1670
+ handler: "presence.invalidateUserSessionsHandler",
1671
+ config: {
1672
+ policies: ["admin::isAuthenticatedAdmin"]
1673
+ }
1235
1674
  }
1236
1675
  ]
1237
1676
  };
@@ -21500,9 +21939,9 @@ function padZeros(value, tok, options) {
21500
21939
  if (!tok.isPadded) {
21501
21940
  return value;
21502
21941
  }
21503
- let diff = Math.abs(tok.maxLen - String(value).length);
21942
+ let diff2 = Math.abs(tok.maxLen - String(value).length);
21504
21943
  let relax = options.relaxZeros !== false;
21505
- switch (diff) {
21944
+ switch (diff2) {
21506
21945
  case 0:
21507
21946
  return "";
21508
21947
  case 1:
@@ -21510,7 +21949,7 @@ function padZeros(value, tok, options) {
21510
21949
  case 2:
21511
21950
  return relax ? "0{0,2}" : "00";
21512
21951
  default: {
21513
- return relax ? `0{0,${diff}}` : `0{${diff}}`;
21952
+ return relax ? `0{0,${diff2}}` : `0{${diff2}}`;
21514
21953
  }
21515
21954
  }
21516
21955
  }
@@ -29433,6 +29872,66 @@ var strategies = ({ strapi: strapi2 }) => {
29433
29872
  const apiTokenService = getService({ type: "admin", plugin: "api-token" });
29434
29873
  const jwtService = getService({ name: "jwt", plugin: "users-permissions" });
29435
29874
  const userService = getService({ name: "user", plugin: "users-permissions" });
29875
+ const admin2 = {
29876
+ name: "io-admin",
29877
+ credentials: function(user) {
29878
+ return `${this.name}-${user.id}`;
29879
+ },
29880
+ /**
29881
+ * Authenticates admin user via session token
29882
+ * @param {object} auth - Auth object containing token
29883
+ * @param {object} socket - Socket instance for registration
29884
+ * @returns {object} User data if authenticated
29885
+ * @throws {UnauthorizedError} If authentication fails
29886
+ */
29887
+ authenticate: async function(auth, socket) {
29888
+ const token2 = auth.token;
29889
+ if (!token2 || typeof token2 !== "string") {
29890
+ strapi2.log.warn("[plugin-io] Admin auth failed: No token provided");
29891
+ throw new UnauthorizedError2("Invalid admin credentials");
29892
+ }
29893
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
29894
+ if (!uuidRegex.test(token2)) {
29895
+ strapi2.log.warn("[plugin-io] Admin auth failed: Invalid token format");
29896
+ throw new UnauthorizedError2("Invalid token format");
29897
+ }
29898
+ try {
29899
+ const presenceController = strapi2.plugin("io").controller("presence");
29900
+ const session = presenceController.consumeSessionToken(token2);
29901
+ if (!session) {
29902
+ strapi2.log.warn("[plugin-io] Admin auth failed: Token not valid or expired");
29903
+ throw new UnauthorizedError2("Invalid or expired session token");
29904
+ }
29905
+ if (socket?.id) {
29906
+ presenceController.registerSocket(socket.id, token2);
29907
+ }
29908
+ strapi2.log.info(`[plugin-io] Admin authenticated: User ID ${session.userId}`);
29909
+ return {
29910
+ id: session.userId,
29911
+ ...session.user
29912
+ };
29913
+ } catch (error2) {
29914
+ if (error2 instanceof UnauthorizedError2) {
29915
+ throw error2;
29916
+ }
29917
+ strapi2.log.error("[plugin-io] Admin session verification error:", error2.message);
29918
+ throw new UnauthorizedError2("Authentication failed");
29919
+ }
29920
+ },
29921
+ /**
29922
+ * Cleanup when socket disconnects
29923
+ * @param {object} socket - Socket instance
29924
+ */
29925
+ onDisconnect: function(socket) {
29926
+ if (socket?.id) {
29927
+ const presenceController = strapi2.plugin("io").controller("presence");
29928
+ presenceController.unregisterSocket(socket.id);
29929
+ }
29930
+ },
29931
+ getRoomName: function(user) {
29932
+ return `${this.name}-user-${user.id}`;
29933
+ }
29934
+ };
29436
29935
  const role = {
29437
29936
  name: "io-role",
29438
29937
  credentials: function(role2) {
@@ -29575,6 +30074,7 @@ var strategies = ({ strapi: strapi2 }) => {
29575
30074
  }
29576
30075
  };
29577
30076
  return {
30077
+ admin: admin2,
29578
30078
  role,
29579
30079
  token
29580
30080
  };
@@ -29668,12 +30168,12 @@ function transformEntry(entry, type2) {
29668
30168
  // meta: {},
29669
30169
  };
29670
30170
  }
29671
- const { pluginId: pluginId$1 } = pluginId_1;
30171
+ const { pluginId: pluginId$3 } = pluginId_1;
29672
30172
  var settings$1 = ({ strapi: strapi2 }) => {
29673
30173
  const getPluginStore = () => {
29674
30174
  return strapi2.store({
29675
30175
  type: "plugin",
29676
- name: pluginId$1
30176
+ name: pluginId$3
29677
30177
  });
29678
30178
  };
29679
30179
  const getDefaultSettings = () => ({
@@ -29776,6 +30276,41 @@ var settings$1 = ({ strapi: strapi2 }) => {
29776
30276
  enableConnectionLogging: true,
29777
30277
  enableEventLogging: false,
29778
30278
  maxEventLogSize: 100
30279
+ },
30280
+ // Presence System (Collaboration Awareness)
30281
+ presence: {
30282
+ enabled: true,
30283
+ // Enable presence tracking
30284
+ heartbeatInterval: 3e4,
30285
+ // Heartbeat interval in ms
30286
+ staleTimeout: 6e4,
30287
+ // Time before connection considered stale
30288
+ showAvatars: true,
30289
+ // Show user avatars in UI
30290
+ showTypingIndicator: true
30291
+ // Show typing indicators
30292
+ },
30293
+ // Live Preview (Real-time Draft Updates)
30294
+ livePreview: {
30295
+ enabled: true,
30296
+ // Enable live preview
30297
+ draftEvents: true,
30298
+ // Emit events for draft changes
30299
+ debounceMs: 300,
30300
+ // Debounce field changes
30301
+ maxSubscriptionsPerSocket: 50
30302
+ // Max preview subscriptions per socket
30303
+ },
30304
+ // Field-level Changes (Diff-based Updates)
30305
+ fieldLevelChanges: {
30306
+ enabled: true,
30307
+ // Enable field-level diff
30308
+ includeFullData: false,
30309
+ // Include full data alongside diff
30310
+ excludeFields: ["updatedAt", "updatedBy", "createdAt", "createdBy"],
30311
+ // Fields to exclude from diff
30312
+ maxDiffDepth: 3
30313
+ // Maximum nesting depth for diff
29779
30314
  }
29780
30315
  });
29781
30316
  return {
@@ -29816,7 +30351,7 @@ var settings$1 = ({ strapi: strapi2 }) => {
29816
30351
  getDefaultSettings
29817
30352
  };
29818
30353
  };
29819
- const { pluginId } = pluginId_1;
30354
+ const { pluginId: pluginId$2 } = pluginId_1;
29820
30355
  var monitoring$1 = ({ strapi: strapi2 }) => {
29821
30356
  let eventLog = [];
29822
30357
  let eventStats = {
@@ -29960,17 +30495,795 @@ var monitoring$1 = ({ strapi: strapi2 }) => {
29960
30495
  }
29961
30496
  };
29962
30497
  };
30498
+ const { pluginId: pluginId$1 } = pluginId_1;
30499
+ var presence$1 = ({ strapi: strapi2 }) => {
30500
+ const activeConnections = /* @__PURE__ */ new Map();
30501
+ const entityEditors = /* @__PURE__ */ new Map();
30502
+ let cleanupInterval = null;
30503
+ const getEntityKey = (uid, documentId) => `${uid}:${documentId}`;
30504
+ const getPresenceSettings = () => {
30505
+ const settings2 = strapi2.$ioSettings || {};
30506
+ return {
30507
+ enabled: settings2.presence?.enabled ?? true,
30508
+ heartbeatInterval: settings2.presence?.heartbeatInterval ?? 3e4,
30509
+ staleTimeout: settings2.presence?.staleTimeout ?? 6e4,
30510
+ showAvatars: settings2.presence?.showAvatars ?? true
30511
+ };
30512
+ };
30513
+ const broadcastPresenceUpdate = async (uid, documentId) => {
30514
+ const io2 = strapi2.$io?.server;
30515
+ if (!io2) return;
30516
+ const entityKey = getEntityKey(uid, documentId);
30517
+ const editorSocketIds = entityEditors.get(entityKey) || /* @__PURE__ */ new Set();
30518
+ const editors = [];
30519
+ for (const socketId of editorSocketIds) {
30520
+ const connection = activeConnections.get(socketId);
30521
+ if (connection?.user) {
30522
+ editors.push({
30523
+ socketId,
30524
+ user: {
30525
+ id: connection.user.id,
30526
+ username: connection.user.username,
30527
+ email: connection.user.email,
30528
+ firstname: connection.user.firstname,
30529
+ lastname: connection.user.lastname
30530
+ },
30531
+ joinedAt: connection.entities?.get(entityKey) || Date.now()
30532
+ });
30533
+ }
30534
+ }
30535
+ const roomName = `presence:${entityKey}`;
30536
+ io2.to(roomName).emit("presence:update", {
30537
+ uid,
30538
+ documentId,
30539
+ editors,
30540
+ count: editors.length,
30541
+ timestamp: Date.now()
30542
+ });
30543
+ strapi2.log.debug(`socket.io: Presence update for ${entityKey} - ${editors.length} editor(s)`);
30544
+ };
30545
+ return {
30546
+ /**
30547
+ * Registers a new socket connection for presence tracking
30548
+ * @param {string} socketId - Socket ID
30549
+ * @param {object} user - User object (can be null for anonymous)
30550
+ */
30551
+ registerConnection(socketId, user = null) {
30552
+ const settings2 = getPresenceSettings();
30553
+ if (!settings2.enabled) return;
30554
+ activeConnections.set(socketId, {
30555
+ user,
30556
+ entities: /* @__PURE__ */ new Map(),
30557
+ // entityKey -> joinedAt timestamp
30558
+ lastSeen: Date.now(),
30559
+ connectedAt: Date.now()
30560
+ });
30561
+ strapi2.log.debug(`socket.io: Presence registered for socket ${socketId}`);
30562
+ },
30563
+ /**
30564
+ * Unregisters a socket connection and cleans up all entity presence
30565
+ * @param {string} socketId - Socket ID
30566
+ */
30567
+ async unregisterConnection(socketId) {
30568
+ const connection = activeConnections.get(socketId);
30569
+ if (!connection) return;
30570
+ if (connection.entities) {
30571
+ for (const entityKey of connection.entities.keys()) {
30572
+ const [uid, documentId] = entityKey.split(":");
30573
+ await this.leaveEntity(socketId, uid, documentId, false);
30574
+ }
30575
+ }
30576
+ activeConnections.delete(socketId);
30577
+ strapi2.log.debug(`socket.io: Presence unregistered for socket ${socketId}`);
30578
+ },
30579
+ /**
30580
+ * User joins an entity for editing
30581
+ * @param {string} socketId - Socket ID
30582
+ * @param {string} uid - Content type UID
30583
+ * @param {string} documentId - Document ID
30584
+ * @returns {object} Join result with current editors
30585
+ */
30586
+ async joinEntity(socketId, uid, documentId) {
30587
+ const settings2 = getPresenceSettings();
30588
+ if (!settings2.enabled) {
30589
+ return { success: false, error: "Presence is disabled" };
30590
+ }
30591
+ const connection = activeConnections.get(socketId);
30592
+ if (!connection) {
30593
+ return { success: false, error: "Socket not registered for presence" };
30594
+ }
30595
+ const entityKey = getEntityKey(uid, documentId);
30596
+ if (!entityEditors.has(entityKey)) {
30597
+ entityEditors.set(entityKey, /* @__PURE__ */ new Set());
30598
+ }
30599
+ entityEditors.get(entityKey).add(socketId);
30600
+ connection.entities.set(entityKey, Date.now());
30601
+ connection.lastSeen = Date.now();
30602
+ const io2 = strapi2.$io?.server;
30603
+ const socket = io2?.sockets.sockets.get(socketId);
30604
+ if (socket) {
30605
+ socket.join(`presence:${entityKey}`);
30606
+ }
30607
+ await broadcastPresenceUpdate(uid, documentId);
30608
+ strapi2.log.info(`socket.io: User ${connection.user?.username || "anonymous"} joined entity ${entityKey}`);
30609
+ return {
30610
+ success: true,
30611
+ entityKey,
30612
+ editors: await this.getEntityEditors(uid, documentId)
30613
+ };
30614
+ },
30615
+ /**
30616
+ * User leaves an entity
30617
+ * @param {string} socketId - Socket ID
30618
+ * @param {string} uid - Content type UID
30619
+ * @param {string} documentId - Document ID
30620
+ * @param {boolean} broadcast - Whether to broadcast update (default: true)
30621
+ * @returns {object} Leave result
30622
+ */
30623
+ async leaveEntity(socketId, uid, documentId, broadcast = true) {
30624
+ const settings2 = getPresenceSettings();
30625
+ if (!settings2.enabled) {
30626
+ return { success: false, error: "Presence is disabled" };
30627
+ }
30628
+ const entityKey = getEntityKey(uid, documentId);
30629
+ const connection = activeConnections.get(socketId);
30630
+ const editors = entityEditors.get(entityKey);
30631
+ if (editors) {
30632
+ editors.delete(socketId);
30633
+ if (editors.size === 0) {
30634
+ entityEditors.delete(entityKey);
30635
+ }
30636
+ }
30637
+ if (connection?.entities) {
30638
+ connection.entities.delete(entityKey);
30639
+ }
30640
+ const io2 = strapi2.$io?.server;
30641
+ const socket = io2?.sockets.sockets.get(socketId);
30642
+ if (socket) {
30643
+ socket.leave(`presence:${entityKey}`);
30644
+ }
30645
+ if (broadcast) {
30646
+ await broadcastPresenceUpdate(uid, documentId);
30647
+ }
30648
+ strapi2.log.debug(`socket.io: Socket ${socketId} left entity ${entityKey}`);
30649
+ return { success: true, entityKey };
30650
+ },
30651
+ /**
30652
+ * Gets all editors currently editing an entity
30653
+ * @param {string} uid - Content type UID
30654
+ * @param {string} documentId - Document ID
30655
+ * @returns {Array} List of editors with user info
30656
+ */
30657
+ async getEntityEditors(uid, documentId) {
30658
+ const entityKey = getEntityKey(uid, documentId);
30659
+ const editorSocketIds = entityEditors.get(entityKey) || /* @__PURE__ */ new Set();
30660
+ const editors = [];
30661
+ for (const socketId of editorSocketIds) {
30662
+ const connection = activeConnections.get(socketId);
30663
+ if (connection?.user) {
30664
+ editors.push({
30665
+ socketId,
30666
+ user: {
30667
+ id: connection.user.id,
30668
+ username: connection.user.username,
30669
+ email: connection.user.email,
30670
+ firstname: connection.user.firstname,
30671
+ lastname: connection.user.lastname
30672
+ },
30673
+ joinedAt: connection.entities?.get(entityKey) || Date.now()
30674
+ });
30675
+ }
30676
+ }
30677
+ return editors;
30678
+ },
30679
+ /**
30680
+ * Updates heartbeat for a socket to keep presence alive
30681
+ * @param {string} socketId - Socket ID
30682
+ * @returns {object} Heartbeat result
30683
+ */
30684
+ heartbeat(socketId) {
30685
+ const connection = activeConnections.get(socketId);
30686
+ if (!connection) {
30687
+ return { success: false, error: "Socket not registered" };
30688
+ }
30689
+ connection.lastSeen = Date.now();
30690
+ return { success: true, lastSeen: connection.lastSeen };
30691
+ },
30692
+ /**
30693
+ * Cleans up stale connections that haven't sent heartbeat
30694
+ * @returns {number} Number of connections cleaned up
30695
+ */
30696
+ async cleanup() {
30697
+ const settings2 = getPresenceSettings();
30698
+ const staleTimeout = settings2.staleTimeout;
30699
+ const now = Date.now();
30700
+ let cleanedUp = 0;
30701
+ for (const [socketId, connection] of activeConnections) {
30702
+ if (now - connection.lastSeen > staleTimeout) {
30703
+ await this.unregisterConnection(socketId);
30704
+ cleanedUp++;
30705
+ }
30706
+ }
30707
+ if (cleanedUp > 0) {
30708
+ strapi2.log.info(`socket.io: Presence cleanup removed ${cleanedUp} stale connection(s)`);
30709
+ }
30710
+ return cleanedUp;
30711
+ },
30712
+ /**
30713
+ * Starts the cleanup interval
30714
+ */
30715
+ startCleanupInterval() {
30716
+ const settings2 = getPresenceSettings();
30717
+ if (!settings2.enabled) return;
30718
+ cleanupInterval = setInterval(() => {
30719
+ this.cleanup();
30720
+ }, 6e4);
30721
+ strapi2.log.debug("socket.io: Presence cleanup interval started");
30722
+ },
30723
+ /**
30724
+ * Stops the cleanup interval
30725
+ */
30726
+ stopCleanupInterval() {
30727
+ if (cleanupInterval) {
30728
+ clearInterval(cleanupInterval);
30729
+ cleanupInterval = null;
30730
+ }
30731
+ },
30732
+ /**
30733
+ * Gets presence statistics
30734
+ * @returns {object} Presence stats
30735
+ */
30736
+ getStats() {
30737
+ const totalConnections = activeConnections.size;
30738
+ const totalEntitiesBeingEdited = entityEditors.size;
30739
+ let authenticated = 0;
30740
+ let anonymous = 0;
30741
+ for (const connection of activeConnections.values()) {
30742
+ if (connection.user) {
30743
+ authenticated++;
30744
+ } else {
30745
+ anonymous++;
30746
+ }
30747
+ }
30748
+ return {
30749
+ totalConnections,
30750
+ authenticated,
30751
+ anonymous,
30752
+ totalEntitiesBeingEdited,
30753
+ entities: Array.from(entityEditors.entries()).map(([key, editors]) => ({
30754
+ entityKey: key,
30755
+ editorCount: editors.size
30756
+ }))
30757
+ };
30758
+ },
30759
+ /**
30760
+ * Gets all entities a user is currently editing
30761
+ * @param {string} socketId - Socket ID
30762
+ * @returns {Array} List of entity keys
30763
+ */
30764
+ getUserEntities(socketId) {
30765
+ const connection = activeConnections.get(socketId);
30766
+ if (!connection) return [];
30767
+ return Array.from(connection.entities.keys());
30768
+ },
30769
+ /**
30770
+ * Checks if an entity is being edited by anyone
30771
+ * @param {string} uid - Content type UID
30772
+ * @param {string} documentId - Document ID
30773
+ * @returns {boolean} True if entity has editors
30774
+ */
30775
+ isEntityBeingEdited(uid, documentId) {
30776
+ const entityKey = getEntityKey(uid, documentId);
30777
+ const editors = entityEditors.get(entityKey);
30778
+ return editors ? editors.size > 0 : false;
30779
+ },
30780
+ /**
30781
+ * Broadcasts a typing indicator for an entity
30782
+ * @param {string} socketId - Socket ID of typing user
30783
+ * @param {string} uid - Content type UID
30784
+ * @param {string} documentId - Document ID
30785
+ * @param {string} fieldName - Name of field being edited
30786
+ */
30787
+ broadcastTyping(socketId, uid, documentId, fieldName) {
30788
+ const io2 = strapi2.$io?.server;
30789
+ if (!io2) return;
30790
+ const connection = activeConnections.get(socketId);
30791
+ if (!connection?.user) return;
30792
+ const entityKey = getEntityKey(uid, documentId);
30793
+ const roomName = `presence:${entityKey}`;
30794
+ const socket = io2.sockets.sockets.get(socketId);
30795
+ if (socket) {
30796
+ socket.to(roomName).emit("presence:typing", {
30797
+ uid,
30798
+ documentId,
30799
+ user: {
30800
+ id: connection.user.id,
30801
+ username: connection.user.username
30802
+ },
30803
+ fieldName,
30804
+ timestamp: Date.now()
30805
+ });
30806
+ }
30807
+ }
30808
+ };
30809
+ };
30810
+ const { pluginId } = pluginId_1;
30811
+ var preview$1 = ({ strapi: strapi2 }) => {
30812
+ const previewSubscribers = /* @__PURE__ */ new Map();
30813
+ const socketState = /* @__PURE__ */ new Map();
30814
+ const getEntityKey = (uid, documentId) => `${uid}:${documentId}`;
30815
+ const getPreviewSettings = () => {
30816
+ const settings2 = strapi2.$ioSettings || {};
30817
+ return {
30818
+ enabled: settings2.livePreview?.enabled ?? true,
30819
+ draftEvents: settings2.livePreview?.draftEvents ?? true,
30820
+ debounceMs: settings2.livePreview?.debounceMs ?? 300,
30821
+ maxSubscriptionsPerSocket: settings2.livePreview?.maxSubscriptionsPerSocket ?? 50
30822
+ };
30823
+ };
30824
+ const emitToSubscribers = (uid, documentId, eventType, data) => {
30825
+ const io2 = strapi2.$io?.server;
30826
+ if (!io2) return;
30827
+ const entityKey = getEntityKey(uid, documentId);
30828
+ const subscribers = previewSubscribers.get(entityKey);
30829
+ if (!subscribers || subscribers.size === 0) return;
30830
+ const roomName = `preview:${entityKey}`;
30831
+ io2.to(roomName).emit(eventType, {
30832
+ uid,
30833
+ documentId,
30834
+ ...data,
30835
+ timestamp: Date.now()
30836
+ });
30837
+ strapi2.log.debug(`socket.io: Preview event '${eventType}' sent to ${subscribers.size} subscriber(s) for ${entityKey}`);
30838
+ };
30839
+ return {
30840
+ /**
30841
+ * Subscribes a socket to preview updates for an entity
30842
+ * @param {string} socketId - Socket ID
30843
+ * @param {string} uid - Content type UID
30844
+ * @param {string} documentId - Document ID
30845
+ * @returns {object} Subscription result
30846
+ */
30847
+ async subscribe(socketId, uid, documentId) {
30848
+ const settings2 = getPreviewSettings();
30849
+ if (!settings2.enabled) {
30850
+ return { success: false, error: "Live preview is disabled" };
30851
+ }
30852
+ const entityKey = getEntityKey(uid, documentId);
30853
+ const io2 = strapi2.$io?.server;
30854
+ const socket = io2?.sockets.sockets.get(socketId);
30855
+ if (!socket) {
30856
+ return { success: false, error: "Socket not found" };
30857
+ }
30858
+ const currentSubs = Array.from(socket.rooms).filter((r) => r.startsWith("preview:")).length;
30859
+ if (currentSubs >= settings2.maxSubscriptionsPerSocket) {
30860
+ return { success: false, error: `Maximum preview subscriptions (${settings2.maxSubscriptionsPerSocket}) reached` };
30861
+ }
30862
+ if (!previewSubscribers.has(entityKey)) {
30863
+ previewSubscribers.set(entityKey, /* @__PURE__ */ new Set());
30864
+ }
30865
+ previewSubscribers.get(entityKey).add(socketId);
30866
+ socket.join(`preview:${entityKey}`);
30867
+ if (!socketState.has(socketId)) {
30868
+ socketState.set(socketId, { debounceTimers: /* @__PURE__ */ new Map() });
30869
+ }
30870
+ strapi2.log.debug(`socket.io: Socket ${socketId} subscribed to preview for ${entityKey}`);
30871
+ try {
30872
+ const entity = await strapi2.documents(uid).findOne({ documentId });
30873
+ if (entity) {
30874
+ socket.emit("preview:initial", {
30875
+ uid,
30876
+ documentId,
30877
+ data: entity,
30878
+ timestamp: Date.now()
30879
+ });
30880
+ }
30881
+ } catch (err) {
30882
+ strapi2.log.warn(`socket.io: Could not fetch initial preview data for ${entityKey}: ${err.message}`);
30883
+ }
30884
+ return {
30885
+ success: true,
30886
+ entityKey,
30887
+ subscriberCount: previewSubscribers.get(entityKey).size
30888
+ };
30889
+ },
30890
+ /**
30891
+ * Unsubscribes a socket from preview updates
30892
+ * @param {string} socketId - Socket ID
30893
+ * @param {string} uid - Content type UID
30894
+ * @param {string} documentId - Document ID
30895
+ * @returns {object} Unsubscription result
30896
+ */
30897
+ unsubscribe(socketId, uid, documentId) {
30898
+ const entityKey = getEntityKey(uid, documentId);
30899
+ const subscribers = previewSubscribers.get(entityKey);
30900
+ if (subscribers) {
30901
+ subscribers.delete(socketId);
30902
+ if (subscribers.size === 0) {
30903
+ previewSubscribers.delete(entityKey);
30904
+ }
30905
+ }
30906
+ const io2 = strapi2.$io?.server;
30907
+ const socket = io2?.sockets.sockets.get(socketId);
30908
+ if (socket) {
30909
+ socket.leave(`preview:${entityKey}`);
30910
+ }
30911
+ const state = socketState.get(socketId);
30912
+ if (state?.debounceTimers.has(entityKey)) {
30913
+ clearTimeout(state.debounceTimers.get(entityKey));
30914
+ state.debounceTimers.delete(entityKey);
30915
+ }
30916
+ strapi2.log.debug(`socket.io: Socket ${socketId} unsubscribed from preview for ${entityKey}`);
30917
+ return { success: true, entityKey };
30918
+ },
30919
+ /**
30920
+ * Cleans up all subscriptions for a socket
30921
+ * @param {string} socketId - Socket ID
30922
+ */
30923
+ cleanupSocket(socketId) {
30924
+ for (const [entityKey, subscribers] of previewSubscribers) {
30925
+ if (subscribers.has(socketId)) {
30926
+ subscribers.delete(socketId);
30927
+ if (subscribers.size === 0) {
30928
+ previewSubscribers.delete(entityKey);
30929
+ }
30930
+ }
30931
+ }
30932
+ const state = socketState.get(socketId);
30933
+ if (state) {
30934
+ for (const timerId of state.debounceTimers.values()) {
30935
+ clearTimeout(timerId);
30936
+ }
30937
+ socketState.delete(socketId);
30938
+ }
30939
+ },
30940
+ /**
30941
+ * Emits a draft change event to preview subscribers
30942
+ * @param {string} uid - Content type UID
30943
+ * @param {string} documentId - Document ID
30944
+ * @param {object} data - Changed data
30945
+ * @param {object} diff - Field-level diff (optional)
30946
+ */
30947
+ emitDraftChange(uid, documentId, data, diff2 = null) {
30948
+ const settings2 = getPreviewSettings();
30949
+ if (!settings2.enabled || !settings2.draftEvents) return;
30950
+ emitToSubscribers(uid, documentId, "preview:change", {
30951
+ data,
30952
+ diff: diff2,
30953
+ isDraft: true
30954
+ });
30955
+ },
30956
+ /**
30957
+ * Emits a debounced field change event
30958
+ * @param {string} socketId - Socket ID of the editor
30959
+ * @param {string} uid - Content type UID
30960
+ * @param {string} documentId - Document ID
30961
+ * @param {string} fieldName - Name of changed field
30962
+ * @param {*} value - New field value
30963
+ */
30964
+ emitFieldChange(socketId, uid, documentId, fieldName, value) {
30965
+ const settings2 = getPreviewSettings();
30966
+ if (!settings2.enabled) return;
30967
+ const entityKey = getEntityKey(uid, documentId);
30968
+ const state = socketState.get(socketId);
30969
+ if (state?.debounceTimers.has(entityKey)) {
30970
+ clearTimeout(state.debounceTimers.get(entityKey));
30971
+ }
30972
+ const timerId = setTimeout(() => {
30973
+ emitToSubscribers(uid, documentId, "preview:field", {
30974
+ fieldName,
30975
+ value,
30976
+ editorSocketId: socketId
30977
+ });
30978
+ state?.debounceTimers.delete(entityKey);
30979
+ }, settings2.debounceMs);
30980
+ if (state) {
30981
+ state.debounceTimers.set(entityKey, timerId);
30982
+ }
30983
+ },
30984
+ /**
30985
+ * Emits publish event to preview subscribers
30986
+ * @param {string} uid - Content type UID
30987
+ * @param {string} documentId - Document ID
30988
+ * @param {object} data - Published data
30989
+ */
30990
+ emitPublish(uid, documentId, data) {
30991
+ emitToSubscribers(uid, documentId, "preview:publish", {
30992
+ data,
30993
+ isDraft: false
30994
+ });
30995
+ },
30996
+ /**
30997
+ * Emits unpublish event to preview subscribers
30998
+ * @param {string} uid - Content type UID
30999
+ * @param {string} documentId - Document ID
31000
+ */
31001
+ emitUnpublish(uid, documentId) {
31002
+ emitToSubscribers(uid, documentId, "preview:unpublish", {
31003
+ isDraft: true
31004
+ });
31005
+ },
31006
+ /**
31007
+ * Gets the number of preview subscribers for an entity
31008
+ * @param {string} uid - Content type UID
31009
+ * @param {string} documentId - Document ID
31010
+ * @returns {number} Subscriber count
31011
+ */
31012
+ getSubscriberCount(uid, documentId) {
31013
+ const entityKey = getEntityKey(uid, documentId);
31014
+ return previewSubscribers.get(entityKey)?.size || 0;
31015
+ },
31016
+ /**
31017
+ * Gets all entities with active preview subscribers
31018
+ * @returns {Array} List of entity keys with subscriber counts
31019
+ */
31020
+ getActivePreviewEntities() {
31021
+ const entities = [];
31022
+ for (const [entityKey, subscribers] of previewSubscribers) {
31023
+ const [uid, documentId] = entityKey.split(":");
31024
+ entities.push({
31025
+ uid,
31026
+ documentId,
31027
+ entityKey,
31028
+ subscriberCount: subscribers.size
31029
+ });
31030
+ }
31031
+ return entities;
31032
+ },
31033
+ /**
31034
+ * Checks if live preview is enabled
31035
+ * @returns {boolean} True if enabled
31036
+ */
31037
+ isEnabled() {
31038
+ return getPreviewSettings().enabled;
31039
+ },
31040
+ /**
31041
+ * Gets preview statistics
31042
+ * @returns {object} Preview stats
31043
+ */
31044
+ getStats() {
31045
+ let totalSubscriptions = 0;
31046
+ for (const subscribers of previewSubscribers.values()) {
31047
+ totalSubscriptions += subscribers.size;
31048
+ }
31049
+ return {
31050
+ totalEntitiesWithSubscribers: previewSubscribers.size,
31051
+ totalSubscriptions,
31052
+ entities: this.getActivePreviewEntities()
31053
+ };
31054
+ }
31055
+ };
31056
+ };
31057
+ var diff$1 = ({ strapi: strapi2 }) => {
31058
+ const getDiffSettings = () => {
31059
+ const settings2 = strapi2.$ioSettings || {};
31060
+ return {
31061
+ enabled: settings2.fieldLevelChanges?.enabled ?? true,
31062
+ includeFullData: settings2.fieldLevelChanges?.includeFullData ?? false,
31063
+ excludeFields: settings2.fieldLevelChanges?.excludeFields ?? ["updatedAt", "updatedBy", "createdAt", "createdBy"],
31064
+ maxDiffDepth: settings2.fieldLevelChanges?.maxDiffDepth ?? 3
31065
+ };
31066
+ };
31067
+ const isPlainObject2 = (value) => {
31068
+ return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
31069
+ };
31070
+ const isEqual2 = (a, b) => {
31071
+ if (a === b) return true;
31072
+ if (a === null || b === null) return a === b;
31073
+ if (typeof a !== typeof b) return false;
31074
+ if (a instanceof Date && b instanceof Date) {
31075
+ return a.getTime() === b.getTime();
31076
+ }
31077
+ if (Array.isArray(a) && Array.isArray(b)) {
31078
+ if (a.length !== b.length) return false;
31079
+ return a.every((item, index2) => isEqual2(item, b[index2]));
31080
+ }
31081
+ if (isPlainObject2(a) && isPlainObject2(b)) {
31082
+ const keysA = Object.keys(a);
31083
+ const keysB = Object.keys(b);
31084
+ if (keysA.length !== keysB.length) return false;
31085
+ return keysA.every((key) => isEqual2(a[key], b[key]));
31086
+ }
31087
+ return false;
31088
+ };
31089
+ const safeClone = (value) => {
31090
+ if (value === null || value === void 0) return value;
31091
+ if (value instanceof Date) return value.toISOString();
31092
+ if (Array.isArray(value)) return value.map(safeClone);
31093
+ if (isPlainObject2(value)) {
31094
+ const cloned = {};
31095
+ for (const [key, val] of Object.entries(value)) {
31096
+ cloned[key] = safeClone(val);
31097
+ }
31098
+ return cloned;
31099
+ }
31100
+ return value;
31101
+ };
31102
+ const calculateDiffInternal = (oldData, newData, options = {}, depth2 = 0) => {
31103
+ const { excludeFields = [], maxDiffDepth = 3 } = options;
31104
+ const diff2 = {};
31105
+ if (!oldData || !newData) {
31106
+ return { _replaced: true, old: safeClone(oldData), new: safeClone(newData) };
31107
+ }
31108
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(oldData || {}), ...Object.keys(newData || {})]);
31109
+ for (const key of allKeys) {
31110
+ if (excludeFields.includes(key)) continue;
31111
+ const oldValue = oldData?.[key];
31112
+ const newValue = newData?.[key];
31113
+ if (isEqual2(oldValue, newValue)) continue;
31114
+ if (isPlainObject2(oldValue) && isPlainObject2(newValue) && depth2 < maxDiffDepth) {
31115
+ const nestedDiff = calculateDiffInternal(oldValue, newValue, options, depth2 + 1);
31116
+ if (Object.keys(nestedDiff).length > 0) {
31117
+ diff2[key] = nestedDiff;
31118
+ }
31119
+ } else {
31120
+ diff2[key] = {
31121
+ old: safeClone(oldValue),
31122
+ new: safeClone(newValue)
31123
+ };
31124
+ }
31125
+ }
31126
+ return diff2;
31127
+ };
31128
+ return {
31129
+ /**
31130
+ * Calculates field-level diff between old and new data
31131
+ * @param {object} oldData - Previous data state
31132
+ * @param {object} newData - New data state
31133
+ * @returns {object} Diff result with changed fields and metadata
31134
+ */
31135
+ calculateDiff(oldData, newData) {
31136
+ const settings2 = getDiffSettings();
31137
+ if (!settings2.enabled) {
31138
+ return {
31139
+ enabled: false,
31140
+ hasChanges: !isEqual2(oldData, newData),
31141
+ diff: null,
31142
+ fullData: newData
31143
+ };
31144
+ }
31145
+ const diff2 = calculateDiffInternal(oldData, newData, {
31146
+ excludeFields: settings2.excludeFields,
31147
+ maxDiffDepth: settings2.maxDiffDepth
31148
+ });
31149
+ const changedFields = Object.keys(diff2);
31150
+ const hasChanges = changedFields.length > 0;
31151
+ const result = {
31152
+ enabled: true,
31153
+ hasChanges,
31154
+ changedFields,
31155
+ changedFieldCount: changedFields.length,
31156
+ diff: hasChanges ? diff2 : null,
31157
+ timestamp: Date.now()
31158
+ };
31159
+ if (settings2.includeFullData) {
31160
+ result.fullData = newData;
31161
+ }
31162
+ return result;
31163
+ },
31164
+ /**
31165
+ * Applies a diff to a target object
31166
+ * @param {object} target - Target object to apply diff to
31167
+ * @param {object} diff - Diff to apply
31168
+ * @returns {object} Updated target object
31169
+ */
31170
+ applyDiff(target, diff2) {
31171
+ if (!diff2 || typeof diff2 !== "object") return target;
31172
+ const result = { ...target };
31173
+ for (const [key, change] of Object.entries(diff2)) {
31174
+ if (change._replaced) {
31175
+ result[key] = change.new;
31176
+ } else if (change.old !== void 0 && change.new !== void 0) {
31177
+ result[key] = change.new;
31178
+ } else if (isPlainObject2(change)) {
31179
+ result[key] = this.applyDiff(result[key] || {}, change);
31180
+ }
31181
+ }
31182
+ return result;
31183
+ },
31184
+ /**
31185
+ * Validates if a diff is applicable to a content type
31186
+ * @param {string} uid - Content type UID
31187
+ * @param {object} diff - Diff to validate
31188
+ * @returns {object} Validation result
31189
+ */
31190
+ validateDiff(uid, diff2) {
31191
+ if (!diff2) {
31192
+ return { valid: true, errors: [] };
31193
+ }
31194
+ const contentType = strapi2.contentTypes[uid];
31195
+ if (!contentType) {
31196
+ return { valid: false, errors: [`Content type ${uid} not found`] };
31197
+ }
31198
+ const errors2 = [];
31199
+ const attributes = contentType.attributes || {};
31200
+ for (const field of Object.keys(diff2)) {
31201
+ if (!attributes[field] && field !== "id" && field !== "documentId") {
31202
+ errors2.push(`Field '${field}' does not exist in ${uid}`);
31203
+ }
31204
+ }
31205
+ return {
31206
+ valid: errors2.length === 0,
31207
+ errors: errors2
31208
+ };
31209
+ },
31210
+ /**
31211
+ * Creates an event payload with diff information
31212
+ * @param {string} eventType - Event type (create, update, delete)
31213
+ * @param {object} schema - Content type schema info
31214
+ * @param {object} oldData - Previous data (null for create)
31215
+ * @param {object} newData - New data (null for delete)
31216
+ * @returns {object} Event payload with diff
31217
+ */
31218
+ createEventPayload(eventType, schema2, oldData, newData) {
31219
+ const settings2 = getDiffSettings();
31220
+ if (eventType === "create") {
31221
+ return {
31222
+ event: eventType,
31223
+ schema: { singularName: schema2.singularName, uid: schema2.uid },
31224
+ data: newData,
31225
+ diff: null,
31226
+ timestamp: Date.now()
31227
+ };
31228
+ }
31229
+ if (eventType === "delete") {
31230
+ return {
31231
+ event: eventType,
31232
+ schema: { singularName: schema2.singularName, uid: schema2.uid },
31233
+ data: { id: oldData?.id, documentId: oldData?.documentId },
31234
+ deletedData: settings2.includeFullData ? oldData : null,
31235
+ diff: null,
31236
+ timestamp: Date.now()
31237
+ };
31238
+ }
31239
+ const diffResult = this.calculateDiff(oldData, newData);
31240
+ const payload = {
31241
+ event: eventType,
31242
+ schema: { singularName: schema2.singularName, uid: schema2.uid },
31243
+ documentId: newData?.documentId || newData?.id,
31244
+ diff: diffResult.diff,
31245
+ changedFields: diffResult.changedFields,
31246
+ hasChanges: diffResult.hasChanges,
31247
+ timestamp: Date.now()
31248
+ };
31249
+ if (settings2.includeFullData || !settings2.enabled) {
31250
+ payload.data = newData;
31251
+ }
31252
+ return payload;
31253
+ },
31254
+ /**
31255
+ * Checks if diff feature is enabled
31256
+ * @returns {boolean} True if enabled
31257
+ */
31258
+ isEnabled() {
31259
+ return getDiffSettings().enabled;
31260
+ },
31261
+ /**
31262
+ * Gets current diff settings
31263
+ * @returns {object} Current settings
31264
+ */
31265
+ getSettings() {
31266
+ return getDiffSettings();
31267
+ }
31268
+ };
31269
+ };
29963
31270
  const strategy = strategies;
29964
31271
  const sanitize = sanitize_1;
29965
31272
  const transform = transform$1;
29966
31273
  const settings = settings$1;
29967
31274
  const monitoring = monitoring$1;
31275
+ const presence = presence$1;
31276
+ const preview = preview$1;
31277
+ const diff = diff$1;
29968
31278
  var services$1 = {
29969
31279
  sanitize,
29970
31280
  strategy,
29971
31281
  transform,
29972
31282
  settings,
29973
- monitoring
31283
+ monitoring,
31284
+ presence,
31285
+ preview,
31286
+ diff
29974
31287
  };
29975
31288
  const bootstrap = bootstrap_1;
29976
31289
  const config = config$1;