@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
  import require$$0$4 from "socket.io";
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
+ import require$$1 from "crypto";
3
4
  import * as dates$1 from "date-fns";
4
5
  import dates__default from "date-fns";
5
- import require$$1 from "crypto";
6
6
  import require$$0$5 from "child_process";
7
7
  import require$$0$6 from "os";
8
8
  import require$$0$8 from "path";
@@ -51,10 +51,10 @@ const require$$0$3 = {
51
51
  strapi: strapi$1
52
52
  };
53
53
  const pluginPkg = require$$0$3;
54
- const pluginId$7 = pluginPkg.strapi.name;
55
- var pluginId_1 = { pluginId: pluginId$7 };
56
- const { pluginId: pluginId$6 } = pluginId_1;
57
- function getService$3({ name, plugin = pluginId$6, type: type2 = "plugin" }) {
54
+ const pluginId$9 = pluginPkg.strapi.name;
55
+ var pluginId_1 = { pluginId: pluginId$9 };
56
+ const { pluginId: pluginId$8 } = pluginId_1;
57
+ function getService$3({ name, plugin = pluginId$8, type: type2 = "plugin" }) {
58
58
  let serviceUID = `${type2}::${plugin}`;
59
59
  if (name && name.length) {
60
60
  serviceUID += `.${name}`;
@@ -76,11 +76,24 @@ async function handshake$2(socket, next) {
76
76
  try {
77
77
  let room;
78
78
  if (strategy2 && strategy2.length) {
79
- const strategyType = strategy2 === "jwt" ? "role" : "token";
79
+ let strategyType;
80
+ if (strategy2 === "jwt") {
81
+ strategyType = "role";
82
+ } else if (strategy2 === "admin-jwt") {
83
+ strategyType = "admin";
84
+ } else {
85
+ strategyType = "token";
86
+ }
80
87
  const ctx = await strategyService[strategyType].authenticate(auth);
81
88
  room = strategyService[strategyType].getRoomName(ctx);
89
+ if (strategyType === "admin") {
90
+ socket.adminUser = ctx;
91
+ }
82
92
  } else if (strapi.plugin("users-permissions")) {
83
- const role = await strapi.query("plugin::users-permissions.role").findOne({ where: { type: "public" }, select: ["id", "name"] });
93
+ const role = await strapi.documents("plugin::users-permissions.role").findFirst({
94
+ filters: { type: "public" },
95
+ fields: ["id", "name"]
96
+ });
84
97
  room = strategyService["role"].getRoomName(role);
85
98
  }
86
99
  if (room) {
@@ -111,7 +124,7 @@ var constants$7 = {
111
124
  const { Server } = require$$0$4;
112
125
  const { handshake } = middleware;
113
126
  const { getService: getService$1 } = getService_1;
114
- const { pluginId: pluginId$5 } = pluginId_1;
127
+ const { pluginId: pluginId$7 } = pluginId_1;
115
128
  const { API_TOKEN_TYPE: API_TOKEN_TYPE$1 } = constants$7;
116
129
  let SocketIO$2 = class SocketIO {
117
130
  constructor(options) {
@@ -231,11 +244,11 @@ function requireSanitizeSensitiveFields() {
231
244
  return sanitizeSensitiveFields;
232
245
  }
233
246
  const { SocketIO: SocketIO2 } = structures;
234
- const { pluginId: pluginId$4 } = pluginId_1;
247
+ const { pluginId: pluginId$6 } = pluginId_1;
235
248
  async function bootstrapIO$1({ strapi: strapi2 }) {
236
- const settingsService = strapi2.plugin(pluginId$4).service("settings");
249
+ const settingsService = strapi2.plugin(pluginId$6).service("settings");
237
250
  const settings2 = await settingsService.getSettings();
238
- const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
251
+ const monitoringService = strapi2.plugin(pluginId$6).service("monitoring");
239
252
  const serverOptions = {
240
253
  cors: {
241
254
  origin: settings2.cors?.origins || ["http://localhost:3000"],
@@ -305,31 +318,56 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
305
318
  return next(new Error("Max connections reached"));
306
319
  }
307
320
  const token = socket.handshake.auth?.token || socket.handshake.query?.token;
321
+ const strategy2 = socket.handshake.auth?.strategy;
322
+ const isAdmin = socket.handshake.auth?.isAdmin === true;
308
323
  if (token) {
309
- try {
310
- const decoded = await strapi2.plugin("users-permissions").service("jwt").verify(token);
311
- strapi2.log.info(`socket.io: JWT decoded - user id: ${decoded.id}`);
312
- if (decoded.id) {
313
- const users = await strapi2.documents("plugin::users-permissions.user").findMany({
314
- filters: { id: decoded.id },
315
- populate: { role: true },
316
- limit: 1
317
- });
318
- const user = users.length > 0 ? users[0] : null;
319
- if (user) {
324
+ if (isAdmin || strategy2 === "admin-jwt") {
325
+ try {
326
+ const presenceController = strapi2.plugin(pluginId$6).controller("presence");
327
+ const session = presenceController.consumeSessionToken(token);
328
+ if (session) {
320
329
  socket.user = {
321
- id: user.id,
322
- username: user.username,
323
- email: user.email,
324
- role: user.role?.name || "authenticated"
330
+ id: session.userId,
331
+ username: `${session.user.firstname || ""} ${session.user.lastname || ""}`.trim() || `Admin ${session.userId}`,
332
+ email: session.user.email || `admin-${session.userId}`,
333
+ role: "strapi-super-admin",
334
+ isAdmin: true
325
335
  };
326
- strapi2.log.info(`socket.io: User authenticated - ${user.username} (${user.email})`);
336
+ socket.adminUser = session.user;
337
+ presenceController.registerSocket(socket.id, token);
338
+ strapi2.log.info(`socket.io: Admin authenticated - ${socket.user.username} (ID: ${session.userId})`);
327
339
  } else {
328
- strapi2.log.warn(`socket.io: User not found for id: ${decoded.id}`);
340
+ strapi2.log.warn(`socket.io: Admin session token invalid or expired`);
329
341
  }
342
+ } catch (err) {
343
+ strapi2.log.warn(`socket.io: Admin session verification failed: ${err.message}`);
344
+ }
345
+ } else {
346
+ try {
347
+ const decoded = await strapi2.plugin("users-permissions").service("jwt").verify(token);
348
+ strapi2.log.info(`socket.io: JWT decoded - user id: ${decoded.id}`);
349
+ if (decoded.id) {
350
+ const users = await strapi2.documents("plugin::users-permissions.user").findMany({
351
+ filters: { id: decoded.id },
352
+ populate: { role: true },
353
+ limit: 1
354
+ });
355
+ const user = users.length > 0 ? users[0] : null;
356
+ if (user) {
357
+ socket.user = {
358
+ id: user.id,
359
+ username: user.username,
360
+ email: user.email,
361
+ role: user.role?.name || "authenticated"
362
+ };
363
+ strapi2.log.info(`socket.io: User authenticated - ${user.username} (${user.email})`);
364
+ } else {
365
+ strapi2.log.warn(`socket.io: User not found for id: ${decoded.id}`);
366
+ }
367
+ }
368
+ } catch (err) {
369
+ strapi2.log.warn(`socket.io: JWT verification failed: ${err.message}`);
330
370
  }
331
- } catch (err) {
332
- strapi2.log.warn(`socket.io: JWT verification failed: ${err.message}`);
333
371
  }
334
372
  } else {
335
373
  strapi2.log.debug(`socket.io: No token provided, connecting as public`);
@@ -368,6 +406,11 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
368
406
  }
369
407
  });
370
408
  }
409
+ const presenceService = strapi2.plugin(pluginId$6).service("presence");
410
+ const previewService = strapi2.plugin(pluginId$6).service("preview");
411
+ if (settings2.presence?.enabled !== false) {
412
+ presenceService.startCleanupInterval();
413
+ }
371
414
  io2.server.on("connection", (socket) => {
372
415
  const clientIp = socket.handshake.address || "unknown";
373
416
  const username = socket.user?.username || "anonymous";
@@ -379,6 +422,10 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
379
422
  user: socket.user || null
380
423
  });
381
424
  }
425
+ if (settings2.presence?.enabled !== false) {
426
+ const user = socket.user || socket.adminUser;
427
+ presenceService.registerConnection(socket.id, user);
428
+ }
382
429
  if (settings2.rooms?.autoJoinByRole) {
383
430
  const userRole = socket.user?.role || "public";
384
431
  const rooms = settings2.rooms.autoJoinByRole[userRole] || [];
@@ -423,6 +470,70 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
423
470
  const rooms = Array.from(socket.rooms).filter((r) => r !== socket.id);
424
471
  if (callback) callback({ success: true, rooms });
425
472
  });
473
+ socket.on("presence:join", async ({ uid, documentId }, callback) => {
474
+ if (settings2.presence?.enabled === false) {
475
+ if (callback) callback({ success: false, error: "Presence is disabled" });
476
+ return;
477
+ }
478
+ if (!uid || !documentId) {
479
+ if (callback) callback({ success: false, error: "uid and documentId are required" });
480
+ return;
481
+ }
482
+ const result = await presenceService.joinEntity(socket.id, uid, documentId);
483
+ if (callback) callback(result);
484
+ });
485
+ socket.on("presence:leave", async ({ uid, documentId }, callback) => {
486
+ if (settings2.presence?.enabled === false) {
487
+ if (callback) callback({ success: false, error: "Presence is disabled" });
488
+ return;
489
+ }
490
+ if (!uid || !documentId) {
491
+ if (callback) callback({ success: false, error: "uid and documentId are required" });
492
+ return;
493
+ }
494
+ const result = await presenceService.leaveEntity(socket.id, uid, documentId);
495
+ if (callback) callback(result);
496
+ });
497
+ socket.on("presence:heartbeat", (callback) => {
498
+ const result = presenceService.heartbeat(socket.id);
499
+ if (callback) callback(result);
500
+ });
501
+ socket.on("presence:typing", ({ uid, documentId, fieldName }) => {
502
+ if (settings2.presence?.enabled === false) return;
503
+ presenceService.broadcastTyping(socket.id, uid, documentId, fieldName);
504
+ });
505
+ socket.on("presence:check", async ({ uid, documentId }, callback) => {
506
+ if (settings2.presence?.enabled === false) {
507
+ if (callback) callback({ success: false, error: "Presence is disabled" });
508
+ return;
509
+ }
510
+ const editors = await presenceService.getEntityEditors(uid, documentId);
511
+ if (callback) callback({ success: true, editors, isBeingEdited: editors.length > 0 });
512
+ });
513
+ socket.on("preview:subscribe", async ({ uid, documentId }, callback) => {
514
+ if (settings2.livePreview?.enabled === false) {
515
+ if (callback) callback({ success: false, error: "Live preview is disabled" });
516
+ return;
517
+ }
518
+ if (!uid || !documentId) {
519
+ if (callback) callback({ success: false, error: "uid and documentId are required" });
520
+ return;
521
+ }
522
+ const result = await previewService.subscribe(socket.id, uid, documentId);
523
+ if (callback) callback(result);
524
+ });
525
+ socket.on("preview:unsubscribe", ({ uid, documentId }, callback) => {
526
+ if (!uid || !documentId) {
527
+ if (callback) callback({ success: false, error: "uid and documentId are required" });
528
+ return;
529
+ }
530
+ const result = previewService.unsubscribe(socket.id, uid, documentId);
531
+ if (callback) callback(result);
532
+ });
533
+ socket.on("preview:field-change", ({ uid, documentId, fieldName, value }) => {
534
+ if (settings2.livePreview?.enabled === false) return;
535
+ previewService.emitFieldChange(socket.id, uid, documentId, fieldName, value);
536
+ });
426
537
  socket.on("subscribe-entity", async ({ uid, id }, callback) => {
427
538
  if (settings2.entitySubscriptions?.enabled === false) {
428
539
  if (callback) callback({ success: false, error: "Entity subscriptions are disabled" });
@@ -557,7 +668,7 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
557
668
  strapi2.log.debug(`socket.io: Private message from ${socket.id} to ${to}`);
558
669
  if (callback) callback({ success: true });
559
670
  });
560
- socket.on("disconnect", (reason) => {
671
+ socket.on("disconnect", async (reason) => {
561
672
  if (settings2.monitoring?.enableConnectionLogging) {
562
673
  strapi2.log.info(`socket.io: Client disconnected (id: ${socket.id}, user: ${username}, reason: ${reason})`);
563
674
  monitoringService.logEvent("disconnect", {
@@ -566,6 +677,19 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
566
677
  user: socket.user || null
567
678
  });
568
679
  }
680
+ if (settings2.presence?.enabled !== false) {
681
+ await presenceService.unregisterConnection(socket.id);
682
+ }
683
+ if (settings2.livePreview?.enabled !== false) {
684
+ previewService.cleanupSocket(socket.id);
685
+ }
686
+ try {
687
+ const presenceController = strapi2.plugin(pluginId$6).controller("presence");
688
+ if (presenceController?.unregisterSocket) {
689
+ presenceController.unregisterSocket(socket.id);
690
+ }
691
+ } catch (e) {
692
+ }
569
693
  });
570
694
  socket.on("error", (error2) => {
571
695
  strapi2.log.error(`socket.io: Socket error (id: ${socket.id}): ${error2.message}`);
@@ -709,17 +833,52 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
709
833
  }
710
834
  });
711
835
  const enabledContentTypes = allContentTypes.size;
836
+ strapi2.$io.presence = {
837
+ /**
838
+ * Get editors for an entity
839
+ */
840
+ getEditors: (uid, documentId) => presenceService.getEntityEditors(uid, documentId),
841
+ /**
842
+ * Check if entity is being edited
843
+ */
844
+ isBeingEdited: (uid, documentId) => presenceService.isEntityBeingEdited(uid, documentId),
845
+ /**
846
+ * Get presence statistics
847
+ */
848
+ getStats: () => presenceService.getStats()
849
+ };
850
+ strapi2.$io.preview = {
851
+ /**
852
+ * Emit draft change to preview subscribers
853
+ */
854
+ emitDraftChange: (uid, documentId, data, diff2) => previewService.emitDraftChange(uid, documentId, data, diff2),
855
+ /**
856
+ * Emit publish event
857
+ */
858
+ emitPublish: (uid, documentId, data) => previewService.emitPublish(uid, documentId, data),
859
+ /**
860
+ * Emit unpublish event
861
+ */
862
+ emitUnpublish: (uid, documentId) => previewService.emitUnpublish(uid, documentId),
863
+ /**
864
+ * Get preview statistics
865
+ */
866
+ getStats: () => previewService.getStats()
867
+ };
712
868
  const origins = settings2.cors?.origins?.join(", ") || "http://localhost:3000";
713
869
  const features = [];
714
870
  if (settings2.redis?.enabled) features.push("Redis");
715
871
  if (settings2.namespaces?.enabled) features.push(`Namespaces(${Object.keys(settings2.namespaces.list || {}).length})`);
716
872
  if (settings2.security?.rateLimiting?.enabled) features.push("RateLimit");
873
+ if (settings2.presence?.enabled !== false) features.push("Presence");
874
+ if (settings2.livePreview?.enabled !== false) features.push("LivePreview");
875
+ if (settings2.fieldLevelChanges?.enabled !== false) features.push("FieldDiff");
717
876
  strapi2.log.info(`socket.io: Plugin initialized`);
718
- strapi2.log.info(` Origins: ${origins}`);
719
- strapi2.log.info(` Content Types: ${enabledContentTypes}`);
720
- strapi2.log.info(` Max Connections: ${settings2.connection?.maxConnections || 1e3}`);
877
+ strapi2.log.info(` - Origins: ${origins}`);
878
+ strapi2.log.info(` - Content Types: ${enabledContentTypes}`);
879
+ strapi2.log.info(` - Max Connections: ${settings2.connection?.maxConnections || 1e3}`);
721
880
  if (features.length > 0) {
722
- strapi2.log.info(` Features: ${features.join(", ")}`);
881
+ strapi2.log.info(` - Features: ${features.join(", ")}`);
723
882
  }
724
883
  }
725
884
  var io = { bootstrapIO: bootstrapIO$1 };
@@ -793,7 +952,7 @@ function getTransactionCtx() {
793
952
  }
794
953
  return transactionCtx;
795
954
  }
796
- const { pluginId: pluginId$3 } = pluginId_1;
955
+ const { pluginId: pluginId$5 } = pluginId_1;
797
956
  function scheduleAfterTransaction(callback, delay = 0) {
798
957
  const runner = () => setTimeout(callback, delay);
799
958
  const ctx = getTransactionCtx();
@@ -880,17 +1039,47 @@ async function bootstrapLifecycles$1({ strapi: strapi2 }) {
880
1039
  }, 50);
881
1040
  }
882
1041
  };
1042
+ subscriber.beforeUpdate = async (event) => {
1043
+ if (!isActionEnabled(strapi2, uid, "update")) return;
1044
+ const fieldLevelEnabled = strapi2.$ioSettings?.fieldLevelChanges?.enabled !== false;
1045
+ if (!fieldLevelEnabled) return;
1046
+ try {
1047
+ const documentId = event.params.where?.documentId || event.params.documentId;
1048
+ if (!documentId) return;
1049
+ const existing = await strapi2.documents(uid).findOne({ documentId });
1050
+ if (existing) {
1051
+ if (!event.state.io) event.state.io = {};
1052
+ event.state.io.previousData = JSON.parse(JSON.stringify(existing));
1053
+ }
1054
+ } catch (error2) {
1055
+ strapi2.log.debug(`socket.io: Could not fetch previous data for diff: ${error2.message}`);
1056
+ }
1057
+ };
883
1058
  subscriber.afterUpdate = async (event) => {
884
1059
  if (!isActionEnabled(strapi2, uid, "update")) return;
885
- const eventData = {
886
- event: "update",
887
- schema: event.model,
888
- data: JSON.parse(JSON.stringify(event.result))
889
- // Deep clone
890
- };
1060
+ const newData = JSON.parse(JSON.stringify(event.result));
1061
+ const previousData = event.state.io?.previousData || null;
1062
+ const modelInfo = { singularName: event.model.singularName, uid: event.model.uid };
891
1063
  scheduleAfterTransaction(() => {
892
1064
  try {
893
- strapi2.$io.emit(eventData);
1065
+ const diffService = strapi2.plugin(pluginId$5).service("diff");
1066
+ const previewService = strapi2.plugin(pluginId$5).service("preview");
1067
+ const fieldLevelEnabled = strapi2.$ioSettings?.fieldLevelChanges?.enabled !== false;
1068
+ let eventPayload;
1069
+ if (fieldLevelEnabled && previousData && diffService) {
1070
+ eventPayload = diffService.createEventPayload("update", modelInfo, previousData, newData);
1071
+ } else {
1072
+ eventPayload = {
1073
+ event: "update",
1074
+ schema: modelInfo,
1075
+ data: newData
1076
+ };
1077
+ }
1078
+ strapi2.$io.emit(eventPayload);
1079
+ if (previewService && newData.documentId) {
1080
+ const diff2 = fieldLevelEnabled ? eventPayload.diff : null;
1081
+ previewService.emitDraftChange(uid, newData.documentId, newData, diff2);
1082
+ }
894
1083
  } catch (error2) {
895
1084
  strapi2.log.debug(`socket.io: Could not emit update event for ${uid}:`, error2.message);
896
1085
  }
@@ -991,14 +1180,14 @@ var config$1 = {
991
1180
  validator(config2) {
992
1181
  }
993
1182
  };
994
- const { pluginId: pluginId$2 } = pluginId_1;
1183
+ const { pluginId: pluginId$4 } = pluginId_1;
995
1184
  var settings$3 = ({ strapi: strapi2 }) => ({
996
1185
  /**
997
1186
  * GET /io/settings
998
1187
  * Retrieve current plugin settings
999
1188
  */
1000
1189
  async getSettings(ctx) {
1001
- const settingsService = strapi2.plugin(pluginId$2).service("settings");
1190
+ const settingsService = strapi2.plugin(pluginId$4).service("settings");
1002
1191
  const settings2 = await settingsService.getSettings();
1003
1192
  ctx.body = { data: settings2 };
1004
1193
  },
@@ -1007,7 +1196,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1007
1196
  * Update plugin settings and hot-reload Socket.IO
1008
1197
  */
1009
1198
  async updateSettings(ctx) {
1010
- const settingsService = strapi2.plugin(pluginId$2).service("settings");
1199
+ const settingsService = strapi2.plugin(pluginId$4).service("settings");
1011
1200
  const { body } = ctx.request;
1012
1201
  await settingsService.getSettings();
1013
1202
  const updatedSettings = await settingsService.setSettings(body);
@@ -1040,7 +1229,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1040
1229
  * Get connection and event statistics
1041
1230
  */
1042
1231
  async getStats(ctx) {
1043
- const monitoringService = strapi2.plugin(pluginId$2).service("monitoring");
1232
+ const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
1044
1233
  const connectionStats = monitoringService.getConnectionStats();
1045
1234
  const eventStats = monitoringService.getEventStats();
1046
1235
  ctx.body = {
@@ -1055,7 +1244,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1055
1244
  * Get recent event log
1056
1245
  */
1057
1246
  async getEventLog(ctx) {
1058
- const monitoringService = strapi2.plugin(pluginId$2).service("monitoring");
1247
+ const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
1059
1248
  const limit = parseInt(ctx.query.limit) || 50;
1060
1249
  const log = monitoringService.getEventLog(limit);
1061
1250
  ctx.body = { data: log };
@@ -1065,7 +1254,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1065
1254
  * Send a test event
1066
1255
  */
1067
1256
  async sendTestEvent(ctx) {
1068
- const monitoringService = strapi2.plugin(pluginId$2).service("monitoring");
1257
+ const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
1069
1258
  const { eventName, data } = ctx.request.body;
1070
1259
  try {
1071
1260
  const result = monitoringService.sendTestEvent(eventName || "test", data || {});
@@ -1079,7 +1268,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1079
1268
  * Reset monitoring statistics
1080
1269
  */
1081
1270
  async resetStats(ctx) {
1082
- const monitoringService = strapi2.plugin(pluginId$2).service("monitoring");
1271
+ const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
1083
1272
  monitoringService.resetStats();
1084
1273
  ctx.body = { data: { success: true } };
1085
1274
  },
@@ -1103,7 +1292,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1103
1292
  * Get lightweight stats for dashboard widget
1104
1293
  */
1105
1294
  async getMonitoringStats(ctx) {
1106
- const monitoringService = strapi2.plugin(pluginId$2).service("monitoring");
1295
+ const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
1107
1296
  const connectionStats = monitoringService.getConnectionStats();
1108
1297
  const eventStats = monitoringService.getEventStats();
1109
1298
  ctx.body = {
@@ -1122,13 +1311,245 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1122
1311
  };
1123
1312
  }
1124
1313
  });
1314
+ const { randomUUID, createHash } = require$$1;
1315
+ const sessionTokens = /* @__PURE__ */ new Map();
1316
+ const activeSockets = /* @__PURE__ */ new Map();
1317
+ const refreshThrottle = /* @__PURE__ */ new Map();
1318
+ const SESSION_TTL = 10 * 60 * 1e3;
1319
+ const REFRESH_COOLDOWN = 30 * 1e3;
1320
+ const CLEANUP_INTERVAL = 2 * 60 * 1e3;
1321
+ const hashToken = (token) => {
1322
+ return createHash("sha256").update(token).digest("hex");
1323
+ };
1324
+ setInterval(() => {
1325
+ const now = Date.now();
1326
+ let cleaned = 0;
1327
+ for (const [tokenHash, session] of sessionTokens.entries()) {
1328
+ if (session.expiresAt < now) {
1329
+ sessionTokens.delete(tokenHash);
1330
+ cleaned++;
1331
+ }
1332
+ }
1333
+ for (const [userId, lastRefresh] of refreshThrottle.entries()) {
1334
+ if (now - lastRefresh > 60 * 60 * 1e3) {
1335
+ refreshThrottle.delete(userId);
1336
+ }
1337
+ }
1338
+ if (cleaned > 0) {
1339
+ console.log(`[plugin-io] [CLEANUP] Removed ${cleaned} expired session tokens`);
1340
+ }
1341
+ }, CLEANUP_INTERVAL);
1342
+ var presence$3 = ({ strapi: strapi2 }) => ({
1343
+ /**
1344
+ * Creates a session token for admin users to connect to Socket.IO
1345
+ * Implements rate limiting and secure token storage
1346
+ * @param {object} ctx - Koa context
1347
+ */
1348
+ async createSession(ctx) {
1349
+ const adminUser = ctx.state.user;
1350
+ if (!adminUser) {
1351
+ strapi2.log.warn("[plugin-io] Presence session requested without admin user");
1352
+ return ctx.unauthorized("Admin authentication required");
1353
+ }
1354
+ const lastRefresh = refreshThrottle.get(adminUser.id);
1355
+ const now = Date.now();
1356
+ if (lastRefresh && now - lastRefresh < REFRESH_COOLDOWN) {
1357
+ const waitTime = Math.ceil((REFRESH_COOLDOWN - (now - lastRefresh)) / 1e3);
1358
+ strapi2.log.warn(`[plugin-io] Rate limit: User ${adminUser.id} must wait ${waitTime}s`);
1359
+ return ctx.tooManyRequests(`Please wait ${waitTime} seconds before requesting a new session`);
1360
+ }
1361
+ try {
1362
+ const token = randomUUID();
1363
+ const tokenHash = hashToken(token);
1364
+ const expiresAt = now + SESSION_TTL;
1365
+ sessionTokens.set(tokenHash, {
1366
+ tokenHash,
1367
+ userId: adminUser.id,
1368
+ user: {
1369
+ id: adminUser.id,
1370
+ // Only store minimal user data needed for display
1371
+ firstname: adminUser.firstname,
1372
+ lastname: adminUser.lastname
1373
+ },
1374
+ createdAt: now,
1375
+ expiresAt,
1376
+ usageCount: 0,
1377
+ maxUsage: 10
1378
+ // Max reconnects with same token
1379
+ });
1380
+ refreshThrottle.set(adminUser.id, now);
1381
+ strapi2.log.info(`[plugin-io] Presence session created for admin user: ${adminUser.id}`);
1382
+ ctx.body = {
1383
+ token,
1384
+ // Send plaintext token to client (only time it's exposed)
1385
+ expiresAt,
1386
+ refreshAfter: now + SESSION_TTL * 0.7,
1387
+ // Suggest refresh at 70% of TTL
1388
+ wsPath: "/socket.io",
1389
+ wsUrl: `${ctx.protocol}://${ctx.host}`
1390
+ };
1391
+ } catch (error2) {
1392
+ strapi2.log.error("[plugin-io] Failed to create presence session:", error2);
1393
+ return ctx.internalServerError("Failed to create session");
1394
+ }
1395
+ },
1396
+ /**
1397
+ * Validates a session token and tracks usage
1398
+ * Implements usage limits to prevent token abuse
1399
+ * @param {string} token - Session token to validate
1400
+ * @returns {object|null} Session data or null if invalid/expired
1401
+ */
1402
+ consumeSessionToken(token) {
1403
+ if (!token || typeof token !== "string") {
1404
+ return null;
1405
+ }
1406
+ const tokenHash = hashToken(token);
1407
+ const session = sessionTokens.get(tokenHash);
1408
+ if (!session) {
1409
+ strapi2.log.debug("[plugin-io] Token not found in session store");
1410
+ return null;
1411
+ }
1412
+ const now = Date.now();
1413
+ if (session.expiresAt < now) {
1414
+ sessionTokens.delete(tokenHash);
1415
+ strapi2.log.debug("[plugin-io] Token expired, removed from store");
1416
+ return null;
1417
+ }
1418
+ if (session.usageCount >= session.maxUsage) {
1419
+ strapi2.log.warn(`[plugin-io] Token usage limit exceeded for user ${session.userId}`);
1420
+ sessionTokens.delete(tokenHash);
1421
+ return null;
1422
+ }
1423
+ session.usageCount++;
1424
+ session.lastUsed = now;
1425
+ return session;
1426
+ },
1427
+ /**
1428
+ * Registers a socket as using a specific token
1429
+ * @param {string} socketId - Socket ID
1430
+ * @param {string} token - The token being used
1431
+ */
1432
+ registerSocket(socketId, token) {
1433
+ if (!socketId || !token) return;
1434
+ const tokenHash = hashToken(token);
1435
+ activeSockets.set(socketId, tokenHash);
1436
+ },
1437
+ /**
1438
+ * Unregisters a socket when it disconnects
1439
+ * @param {string} socketId - Socket ID
1440
+ */
1441
+ unregisterSocket(socketId) {
1442
+ activeSockets.delete(socketId);
1443
+ },
1444
+ /**
1445
+ * Invalidates all sessions for a specific user (e.g., on logout)
1446
+ * @param {number} userId - User ID to invalidate
1447
+ * @returns {number} Number of sessions invalidated
1448
+ */
1449
+ invalidateUserSessions(userId) {
1450
+ let invalidated = 0;
1451
+ for (const [tokenHash, session] of sessionTokens.entries()) {
1452
+ if (session.userId === userId) {
1453
+ sessionTokens.delete(tokenHash);
1454
+ invalidated++;
1455
+ }
1456
+ }
1457
+ refreshThrottle.delete(userId);
1458
+ strapi2.log.info(`[plugin-io] Invalidated ${invalidated} sessions for user ${userId}`);
1459
+ return invalidated;
1460
+ },
1461
+ /**
1462
+ * Gets session statistics (for monitoring) - internal method
1463
+ * @returns {object} Session statistics
1464
+ */
1465
+ getSessionStatsInternal() {
1466
+ const now = Date.now();
1467
+ let active = 0;
1468
+ let expiringSoon = 0;
1469
+ for (const session of sessionTokens.values()) {
1470
+ if (session.expiresAt > now) {
1471
+ active++;
1472
+ if (session.expiresAt - now < 2 * 60 * 1e3) {
1473
+ expiringSoon++;
1474
+ }
1475
+ }
1476
+ }
1477
+ return {
1478
+ activeSessions: active,
1479
+ expiringSoon,
1480
+ activeSocketConnections: activeSockets.size,
1481
+ sessionTTL: SESSION_TTL,
1482
+ refreshCooldown: REFRESH_COOLDOWN
1483
+ };
1484
+ },
1485
+ /**
1486
+ * HTTP Handler: Gets session statistics for admin monitoring
1487
+ * @param {object} ctx - Koa context
1488
+ */
1489
+ async getSessionStats(ctx) {
1490
+ const adminUser = ctx.state.user;
1491
+ if (!adminUser) {
1492
+ return ctx.unauthorized("Admin authentication required");
1493
+ }
1494
+ try {
1495
+ const stats = this.getSessionStatsInternal();
1496
+ ctx.body = { data: stats };
1497
+ } catch (error2) {
1498
+ strapi2.log.error("[plugin-io] Failed to get session stats:", error2);
1499
+ return ctx.internalServerError("Failed to get session statistics");
1500
+ }
1501
+ },
1502
+ /**
1503
+ * HTTP Handler: Invalidates all sessions for a specific user
1504
+ * @param {object} ctx - Koa context
1505
+ */
1506
+ async invalidateUserSessionsHandler(ctx) {
1507
+ const adminUser = ctx.state.user;
1508
+ if (!adminUser) {
1509
+ return ctx.unauthorized("Admin authentication required");
1510
+ }
1511
+ const { userId } = ctx.params;
1512
+ if (!userId) {
1513
+ return ctx.badRequest("User ID is required");
1514
+ }
1515
+ try {
1516
+ const userIdNum = parseInt(userId, 10);
1517
+ if (isNaN(userIdNum)) {
1518
+ return ctx.badRequest("Invalid user ID");
1519
+ }
1520
+ const invalidated = this.invalidateUserSessions(userIdNum);
1521
+ strapi2.log.info(`[plugin-io] Admin ${adminUser.id} invalidated ${invalidated} sessions for user ${userIdNum}`);
1522
+ ctx.body = {
1523
+ data: {
1524
+ userId: userIdNum,
1525
+ invalidatedSessions: invalidated,
1526
+ message: `Successfully invalidated ${invalidated} session(s)`
1527
+ }
1528
+ };
1529
+ } catch (error2) {
1530
+ strapi2.log.error("[plugin-io] Failed to invalidate user sessions:", error2);
1531
+ return ctx.internalServerError("Failed to invalidate sessions");
1532
+ }
1533
+ }
1534
+ });
1125
1535
  const settings$2 = settings$3;
1536
+ const presence$2 = presence$3;
1126
1537
  var controllers$1 = {
1127
- settings: settings$2
1538
+ settings: settings$2,
1539
+ presence: presence$2
1128
1540
  };
1129
1541
  var admin$1 = {
1130
1542
  type: "admin",
1131
1543
  routes: [
1544
+ // Presence Session - issues JWT token for Socket.IO connection
1545
+ {
1546
+ method: "POST",
1547
+ path: "/presence/session",
1548
+ handler: "presence.createSession",
1549
+ config: {
1550
+ policies: ["admin::isAuthenticatedAdmin"]
1551
+ }
1552
+ },
1132
1553
  {
1133
1554
  method: "GET",
1134
1555
  path: "/settings",
@@ -1200,6 +1621,24 @@ var admin$1 = {
1200
1621
  config: {
1201
1622
  policies: ["admin::isAuthenticatedAdmin"]
1202
1623
  }
1624
+ },
1625
+ // Security: Session statistics
1626
+ {
1627
+ method: "GET",
1628
+ path: "/security/sessions",
1629
+ handler: "presence.getSessionStats",
1630
+ config: {
1631
+ policies: ["admin::isAuthenticatedAdmin"]
1632
+ }
1633
+ },
1634
+ // Security: Invalidate user sessions (force logout)
1635
+ {
1636
+ method: "POST",
1637
+ path: "/security/invalidate/:userId",
1638
+ handler: "presence.invalidateUserSessionsHandler",
1639
+ config: {
1640
+ policies: ["admin::isAuthenticatedAdmin"]
1641
+ }
1203
1642
  }
1204
1643
  ]
1205
1644
  };
@@ -21468,9 +21907,9 @@ function padZeros(value, tok, options) {
21468
21907
  if (!tok.isPadded) {
21469
21908
  return value;
21470
21909
  }
21471
- let diff = Math.abs(tok.maxLen - String(value).length);
21910
+ let diff2 = Math.abs(tok.maxLen - String(value).length);
21472
21911
  let relax = options.relaxZeros !== false;
21473
- switch (diff) {
21912
+ switch (diff2) {
21474
21913
  case 0:
21475
21914
  return "";
21476
21915
  case 1:
@@ -21478,7 +21917,7 @@ function padZeros(value, tok, options) {
21478
21917
  case 2:
21479
21918
  return relax ? "0{0,2}" : "00";
21480
21919
  default: {
21481
- return relax ? `0{0,${diff}}` : `0{${diff}}`;
21920
+ return relax ? `0{0,${diff2}}` : `0{${diff2}}`;
21482
21921
  }
21483
21922
  }
21484
21923
  }
@@ -29401,6 +29840,66 @@ var strategies = ({ strapi: strapi2 }) => {
29401
29840
  const apiTokenService = getService({ type: "admin", plugin: "api-token" });
29402
29841
  const jwtService = getService({ name: "jwt", plugin: "users-permissions" });
29403
29842
  const userService = getService({ name: "user", plugin: "users-permissions" });
29843
+ const admin2 = {
29844
+ name: "io-admin",
29845
+ credentials: function(user) {
29846
+ return `${this.name}-${user.id}`;
29847
+ },
29848
+ /**
29849
+ * Authenticates admin user via session token
29850
+ * @param {object} auth - Auth object containing token
29851
+ * @param {object} socket - Socket instance for registration
29852
+ * @returns {object} User data if authenticated
29853
+ * @throws {UnauthorizedError} If authentication fails
29854
+ */
29855
+ authenticate: async function(auth, socket) {
29856
+ const token2 = auth.token;
29857
+ if (!token2 || typeof token2 !== "string") {
29858
+ strapi2.log.warn("[plugin-io] Admin auth failed: No token provided");
29859
+ throw new UnauthorizedError2("Invalid admin credentials");
29860
+ }
29861
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
29862
+ if (!uuidRegex.test(token2)) {
29863
+ strapi2.log.warn("[plugin-io] Admin auth failed: Invalid token format");
29864
+ throw new UnauthorizedError2("Invalid token format");
29865
+ }
29866
+ try {
29867
+ const presenceController = strapi2.plugin("io").controller("presence");
29868
+ const session = presenceController.consumeSessionToken(token2);
29869
+ if (!session) {
29870
+ strapi2.log.warn("[plugin-io] Admin auth failed: Token not valid or expired");
29871
+ throw new UnauthorizedError2("Invalid or expired session token");
29872
+ }
29873
+ if (socket?.id) {
29874
+ presenceController.registerSocket(socket.id, token2);
29875
+ }
29876
+ strapi2.log.info(`[plugin-io] Admin authenticated: User ID ${session.userId}`);
29877
+ return {
29878
+ id: session.userId,
29879
+ ...session.user
29880
+ };
29881
+ } catch (error2) {
29882
+ if (error2 instanceof UnauthorizedError2) {
29883
+ throw error2;
29884
+ }
29885
+ strapi2.log.error("[plugin-io] Admin session verification error:", error2.message);
29886
+ throw new UnauthorizedError2("Authentication failed");
29887
+ }
29888
+ },
29889
+ /**
29890
+ * Cleanup when socket disconnects
29891
+ * @param {object} socket - Socket instance
29892
+ */
29893
+ onDisconnect: function(socket) {
29894
+ if (socket?.id) {
29895
+ const presenceController = strapi2.plugin("io").controller("presence");
29896
+ presenceController.unregisterSocket(socket.id);
29897
+ }
29898
+ },
29899
+ getRoomName: function(user) {
29900
+ return `${this.name}-user-${user.id}`;
29901
+ }
29902
+ };
29404
29903
  const role = {
29405
29904
  name: "io-role",
29406
29905
  credentials: function(role2) {
@@ -29543,6 +30042,7 @@ var strategies = ({ strapi: strapi2 }) => {
29543
30042
  }
29544
30043
  };
29545
30044
  return {
30045
+ admin: admin2,
29546
30046
  role,
29547
30047
  token
29548
30048
  };
@@ -29636,12 +30136,12 @@ function transformEntry(entry, type2) {
29636
30136
  // meta: {},
29637
30137
  };
29638
30138
  }
29639
- const { pluginId: pluginId$1 } = pluginId_1;
30139
+ const { pluginId: pluginId$3 } = pluginId_1;
29640
30140
  var settings$1 = ({ strapi: strapi2 }) => {
29641
30141
  const getPluginStore = () => {
29642
30142
  return strapi2.store({
29643
30143
  type: "plugin",
29644
- name: pluginId$1
30144
+ name: pluginId$3
29645
30145
  });
29646
30146
  };
29647
30147
  const getDefaultSettings = () => ({
@@ -29744,6 +30244,41 @@ var settings$1 = ({ strapi: strapi2 }) => {
29744
30244
  enableConnectionLogging: true,
29745
30245
  enableEventLogging: false,
29746
30246
  maxEventLogSize: 100
30247
+ },
30248
+ // Presence System (Collaboration Awareness)
30249
+ presence: {
30250
+ enabled: true,
30251
+ // Enable presence tracking
30252
+ heartbeatInterval: 3e4,
30253
+ // Heartbeat interval in ms
30254
+ staleTimeout: 6e4,
30255
+ // Time before connection considered stale
30256
+ showAvatars: true,
30257
+ // Show user avatars in UI
30258
+ showTypingIndicator: true
30259
+ // Show typing indicators
30260
+ },
30261
+ // Live Preview (Real-time Draft Updates)
30262
+ livePreview: {
30263
+ enabled: true,
30264
+ // Enable live preview
30265
+ draftEvents: true,
30266
+ // Emit events for draft changes
30267
+ debounceMs: 300,
30268
+ // Debounce field changes
30269
+ maxSubscriptionsPerSocket: 50
30270
+ // Max preview subscriptions per socket
30271
+ },
30272
+ // Field-level Changes (Diff-based Updates)
30273
+ fieldLevelChanges: {
30274
+ enabled: true,
30275
+ // Enable field-level diff
30276
+ includeFullData: false,
30277
+ // Include full data alongside diff
30278
+ excludeFields: ["updatedAt", "updatedBy", "createdAt", "createdBy"],
30279
+ // Fields to exclude from diff
30280
+ maxDiffDepth: 3
30281
+ // Maximum nesting depth for diff
29747
30282
  }
29748
30283
  });
29749
30284
  return {
@@ -29784,7 +30319,7 @@ var settings$1 = ({ strapi: strapi2 }) => {
29784
30319
  getDefaultSettings
29785
30320
  };
29786
30321
  };
29787
- const { pluginId } = pluginId_1;
30322
+ const { pluginId: pluginId$2 } = pluginId_1;
29788
30323
  var monitoring$1 = ({ strapi: strapi2 }) => {
29789
30324
  let eventLog = [];
29790
30325
  let eventStats = {
@@ -29928,17 +30463,795 @@ var monitoring$1 = ({ strapi: strapi2 }) => {
29928
30463
  }
29929
30464
  };
29930
30465
  };
30466
+ const { pluginId: pluginId$1 } = pluginId_1;
30467
+ var presence$1 = ({ strapi: strapi2 }) => {
30468
+ const activeConnections = /* @__PURE__ */ new Map();
30469
+ const entityEditors = /* @__PURE__ */ new Map();
30470
+ let cleanupInterval = null;
30471
+ const getEntityKey = (uid, documentId) => `${uid}:${documentId}`;
30472
+ const getPresenceSettings = () => {
30473
+ const settings2 = strapi2.$ioSettings || {};
30474
+ return {
30475
+ enabled: settings2.presence?.enabled ?? true,
30476
+ heartbeatInterval: settings2.presence?.heartbeatInterval ?? 3e4,
30477
+ staleTimeout: settings2.presence?.staleTimeout ?? 6e4,
30478
+ showAvatars: settings2.presence?.showAvatars ?? true
30479
+ };
30480
+ };
30481
+ const broadcastPresenceUpdate = async (uid, documentId) => {
30482
+ const io2 = strapi2.$io?.server;
30483
+ if (!io2) return;
30484
+ const entityKey = getEntityKey(uid, documentId);
30485
+ const editorSocketIds = entityEditors.get(entityKey) || /* @__PURE__ */ new Set();
30486
+ const editors = [];
30487
+ for (const socketId of editorSocketIds) {
30488
+ const connection = activeConnections.get(socketId);
30489
+ if (connection?.user) {
30490
+ editors.push({
30491
+ socketId,
30492
+ user: {
30493
+ id: connection.user.id,
30494
+ username: connection.user.username,
30495
+ email: connection.user.email,
30496
+ firstname: connection.user.firstname,
30497
+ lastname: connection.user.lastname
30498
+ },
30499
+ joinedAt: connection.entities?.get(entityKey) || Date.now()
30500
+ });
30501
+ }
30502
+ }
30503
+ const roomName = `presence:${entityKey}`;
30504
+ io2.to(roomName).emit("presence:update", {
30505
+ uid,
30506
+ documentId,
30507
+ editors,
30508
+ count: editors.length,
30509
+ timestamp: Date.now()
30510
+ });
30511
+ strapi2.log.debug(`socket.io: Presence update for ${entityKey} - ${editors.length} editor(s)`);
30512
+ };
30513
+ return {
30514
+ /**
30515
+ * Registers a new socket connection for presence tracking
30516
+ * @param {string} socketId - Socket ID
30517
+ * @param {object} user - User object (can be null for anonymous)
30518
+ */
30519
+ registerConnection(socketId, user = null) {
30520
+ const settings2 = getPresenceSettings();
30521
+ if (!settings2.enabled) return;
30522
+ activeConnections.set(socketId, {
30523
+ user,
30524
+ entities: /* @__PURE__ */ new Map(),
30525
+ // entityKey -> joinedAt timestamp
30526
+ lastSeen: Date.now(),
30527
+ connectedAt: Date.now()
30528
+ });
30529
+ strapi2.log.debug(`socket.io: Presence registered for socket ${socketId}`);
30530
+ },
30531
+ /**
30532
+ * Unregisters a socket connection and cleans up all entity presence
30533
+ * @param {string} socketId - Socket ID
30534
+ */
30535
+ async unregisterConnection(socketId) {
30536
+ const connection = activeConnections.get(socketId);
30537
+ if (!connection) return;
30538
+ if (connection.entities) {
30539
+ for (const entityKey of connection.entities.keys()) {
30540
+ const [uid, documentId] = entityKey.split(":");
30541
+ await this.leaveEntity(socketId, uid, documentId, false);
30542
+ }
30543
+ }
30544
+ activeConnections.delete(socketId);
30545
+ strapi2.log.debug(`socket.io: Presence unregistered for socket ${socketId}`);
30546
+ },
30547
+ /**
30548
+ * User joins an entity for editing
30549
+ * @param {string} socketId - Socket ID
30550
+ * @param {string} uid - Content type UID
30551
+ * @param {string} documentId - Document ID
30552
+ * @returns {object} Join result with current editors
30553
+ */
30554
+ async joinEntity(socketId, uid, documentId) {
30555
+ const settings2 = getPresenceSettings();
30556
+ if (!settings2.enabled) {
30557
+ return { success: false, error: "Presence is disabled" };
30558
+ }
30559
+ const connection = activeConnections.get(socketId);
30560
+ if (!connection) {
30561
+ return { success: false, error: "Socket not registered for presence" };
30562
+ }
30563
+ const entityKey = getEntityKey(uid, documentId);
30564
+ if (!entityEditors.has(entityKey)) {
30565
+ entityEditors.set(entityKey, /* @__PURE__ */ new Set());
30566
+ }
30567
+ entityEditors.get(entityKey).add(socketId);
30568
+ connection.entities.set(entityKey, Date.now());
30569
+ connection.lastSeen = Date.now();
30570
+ const io2 = strapi2.$io?.server;
30571
+ const socket = io2?.sockets.sockets.get(socketId);
30572
+ if (socket) {
30573
+ socket.join(`presence:${entityKey}`);
30574
+ }
30575
+ await broadcastPresenceUpdate(uid, documentId);
30576
+ strapi2.log.info(`socket.io: User ${connection.user?.username || "anonymous"} joined entity ${entityKey}`);
30577
+ return {
30578
+ success: true,
30579
+ entityKey,
30580
+ editors: await this.getEntityEditors(uid, documentId)
30581
+ };
30582
+ },
30583
+ /**
30584
+ * User leaves an entity
30585
+ * @param {string} socketId - Socket ID
30586
+ * @param {string} uid - Content type UID
30587
+ * @param {string} documentId - Document ID
30588
+ * @param {boolean} broadcast - Whether to broadcast update (default: true)
30589
+ * @returns {object} Leave result
30590
+ */
30591
+ async leaveEntity(socketId, uid, documentId, broadcast = true) {
30592
+ const settings2 = getPresenceSettings();
30593
+ if (!settings2.enabled) {
30594
+ return { success: false, error: "Presence is disabled" };
30595
+ }
30596
+ const entityKey = getEntityKey(uid, documentId);
30597
+ const connection = activeConnections.get(socketId);
30598
+ const editors = entityEditors.get(entityKey);
30599
+ if (editors) {
30600
+ editors.delete(socketId);
30601
+ if (editors.size === 0) {
30602
+ entityEditors.delete(entityKey);
30603
+ }
30604
+ }
30605
+ if (connection?.entities) {
30606
+ connection.entities.delete(entityKey);
30607
+ }
30608
+ const io2 = strapi2.$io?.server;
30609
+ const socket = io2?.sockets.sockets.get(socketId);
30610
+ if (socket) {
30611
+ socket.leave(`presence:${entityKey}`);
30612
+ }
30613
+ if (broadcast) {
30614
+ await broadcastPresenceUpdate(uid, documentId);
30615
+ }
30616
+ strapi2.log.debug(`socket.io: Socket ${socketId} left entity ${entityKey}`);
30617
+ return { success: true, entityKey };
30618
+ },
30619
+ /**
30620
+ * Gets all editors currently editing an entity
30621
+ * @param {string} uid - Content type UID
30622
+ * @param {string} documentId - Document ID
30623
+ * @returns {Array} List of editors with user info
30624
+ */
30625
+ async getEntityEditors(uid, documentId) {
30626
+ const entityKey = getEntityKey(uid, documentId);
30627
+ const editorSocketIds = entityEditors.get(entityKey) || /* @__PURE__ */ new Set();
30628
+ const editors = [];
30629
+ for (const socketId of editorSocketIds) {
30630
+ const connection = activeConnections.get(socketId);
30631
+ if (connection?.user) {
30632
+ editors.push({
30633
+ socketId,
30634
+ user: {
30635
+ id: connection.user.id,
30636
+ username: connection.user.username,
30637
+ email: connection.user.email,
30638
+ firstname: connection.user.firstname,
30639
+ lastname: connection.user.lastname
30640
+ },
30641
+ joinedAt: connection.entities?.get(entityKey) || Date.now()
30642
+ });
30643
+ }
30644
+ }
30645
+ return editors;
30646
+ },
30647
+ /**
30648
+ * Updates heartbeat for a socket to keep presence alive
30649
+ * @param {string} socketId - Socket ID
30650
+ * @returns {object} Heartbeat result
30651
+ */
30652
+ heartbeat(socketId) {
30653
+ const connection = activeConnections.get(socketId);
30654
+ if (!connection) {
30655
+ return { success: false, error: "Socket not registered" };
30656
+ }
30657
+ connection.lastSeen = Date.now();
30658
+ return { success: true, lastSeen: connection.lastSeen };
30659
+ },
30660
+ /**
30661
+ * Cleans up stale connections that haven't sent heartbeat
30662
+ * @returns {number} Number of connections cleaned up
30663
+ */
30664
+ async cleanup() {
30665
+ const settings2 = getPresenceSettings();
30666
+ const staleTimeout = settings2.staleTimeout;
30667
+ const now = Date.now();
30668
+ let cleanedUp = 0;
30669
+ for (const [socketId, connection] of activeConnections) {
30670
+ if (now - connection.lastSeen > staleTimeout) {
30671
+ await this.unregisterConnection(socketId);
30672
+ cleanedUp++;
30673
+ }
30674
+ }
30675
+ if (cleanedUp > 0) {
30676
+ strapi2.log.info(`socket.io: Presence cleanup removed ${cleanedUp} stale connection(s)`);
30677
+ }
30678
+ return cleanedUp;
30679
+ },
30680
+ /**
30681
+ * Starts the cleanup interval
30682
+ */
30683
+ startCleanupInterval() {
30684
+ const settings2 = getPresenceSettings();
30685
+ if (!settings2.enabled) return;
30686
+ cleanupInterval = setInterval(() => {
30687
+ this.cleanup();
30688
+ }, 6e4);
30689
+ strapi2.log.debug("socket.io: Presence cleanup interval started");
30690
+ },
30691
+ /**
30692
+ * Stops the cleanup interval
30693
+ */
30694
+ stopCleanupInterval() {
30695
+ if (cleanupInterval) {
30696
+ clearInterval(cleanupInterval);
30697
+ cleanupInterval = null;
30698
+ }
30699
+ },
30700
+ /**
30701
+ * Gets presence statistics
30702
+ * @returns {object} Presence stats
30703
+ */
30704
+ getStats() {
30705
+ const totalConnections = activeConnections.size;
30706
+ const totalEntitiesBeingEdited = entityEditors.size;
30707
+ let authenticated = 0;
30708
+ let anonymous = 0;
30709
+ for (const connection of activeConnections.values()) {
30710
+ if (connection.user) {
30711
+ authenticated++;
30712
+ } else {
30713
+ anonymous++;
30714
+ }
30715
+ }
30716
+ return {
30717
+ totalConnections,
30718
+ authenticated,
30719
+ anonymous,
30720
+ totalEntitiesBeingEdited,
30721
+ entities: Array.from(entityEditors.entries()).map(([key, editors]) => ({
30722
+ entityKey: key,
30723
+ editorCount: editors.size
30724
+ }))
30725
+ };
30726
+ },
30727
+ /**
30728
+ * Gets all entities a user is currently editing
30729
+ * @param {string} socketId - Socket ID
30730
+ * @returns {Array} List of entity keys
30731
+ */
30732
+ getUserEntities(socketId) {
30733
+ const connection = activeConnections.get(socketId);
30734
+ if (!connection) return [];
30735
+ return Array.from(connection.entities.keys());
30736
+ },
30737
+ /**
30738
+ * Checks if an entity is being edited by anyone
30739
+ * @param {string} uid - Content type UID
30740
+ * @param {string} documentId - Document ID
30741
+ * @returns {boolean} True if entity has editors
30742
+ */
30743
+ isEntityBeingEdited(uid, documentId) {
30744
+ const entityKey = getEntityKey(uid, documentId);
30745
+ const editors = entityEditors.get(entityKey);
30746
+ return editors ? editors.size > 0 : false;
30747
+ },
30748
+ /**
30749
+ * Broadcasts a typing indicator for an entity
30750
+ * @param {string} socketId - Socket ID of typing user
30751
+ * @param {string} uid - Content type UID
30752
+ * @param {string} documentId - Document ID
30753
+ * @param {string} fieldName - Name of field being edited
30754
+ */
30755
+ broadcastTyping(socketId, uid, documentId, fieldName) {
30756
+ const io2 = strapi2.$io?.server;
30757
+ if (!io2) return;
30758
+ const connection = activeConnections.get(socketId);
30759
+ if (!connection?.user) return;
30760
+ const entityKey = getEntityKey(uid, documentId);
30761
+ const roomName = `presence:${entityKey}`;
30762
+ const socket = io2.sockets.sockets.get(socketId);
30763
+ if (socket) {
30764
+ socket.to(roomName).emit("presence:typing", {
30765
+ uid,
30766
+ documentId,
30767
+ user: {
30768
+ id: connection.user.id,
30769
+ username: connection.user.username
30770
+ },
30771
+ fieldName,
30772
+ timestamp: Date.now()
30773
+ });
30774
+ }
30775
+ }
30776
+ };
30777
+ };
30778
+ const { pluginId } = pluginId_1;
30779
+ var preview$1 = ({ strapi: strapi2 }) => {
30780
+ const previewSubscribers = /* @__PURE__ */ new Map();
30781
+ const socketState = /* @__PURE__ */ new Map();
30782
+ const getEntityKey = (uid, documentId) => `${uid}:${documentId}`;
30783
+ const getPreviewSettings = () => {
30784
+ const settings2 = strapi2.$ioSettings || {};
30785
+ return {
30786
+ enabled: settings2.livePreview?.enabled ?? true,
30787
+ draftEvents: settings2.livePreview?.draftEvents ?? true,
30788
+ debounceMs: settings2.livePreview?.debounceMs ?? 300,
30789
+ maxSubscriptionsPerSocket: settings2.livePreview?.maxSubscriptionsPerSocket ?? 50
30790
+ };
30791
+ };
30792
+ const emitToSubscribers = (uid, documentId, eventType, data) => {
30793
+ const io2 = strapi2.$io?.server;
30794
+ if (!io2) return;
30795
+ const entityKey = getEntityKey(uid, documentId);
30796
+ const subscribers = previewSubscribers.get(entityKey);
30797
+ if (!subscribers || subscribers.size === 0) return;
30798
+ const roomName = `preview:${entityKey}`;
30799
+ io2.to(roomName).emit(eventType, {
30800
+ uid,
30801
+ documentId,
30802
+ ...data,
30803
+ timestamp: Date.now()
30804
+ });
30805
+ strapi2.log.debug(`socket.io: Preview event '${eventType}' sent to ${subscribers.size} subscriber(s) for ${entityKey}`);
30806
+ };
30807
+ return {
30808
+ /**
30809
+ * Subscribes a socket to preview updates for an entity
30810
+ * @param {string} socketId - Socket ID
30811
+ * @param {string} uid - Content type UID
30812
+ * @param {string} documentId - Document ID
30813
+ * @returns {object} Subscription result
30814
+ */
30815
+ async subscribe(socketId, uid, documentId) {
30816
+ const settings2 = getPreviewSettings();
30817
+ if (!settings2.enabled) {
30818
+ return { success: false, error: "Live preview is disabled" };
30819
+ }
30820
+ const entityKey = getEntityKey(uid, documentId);
30821
+ const io2 = strapi2.$io?.server;
30822
+ const socket = io2?.sockets.sockets.get(socketId);
30823
+ if (!socket) {
30824
+ return { success: false, error: "Socket not found" };
30825
+ }
30826
+ const currentSubs = Array.from(socket.rooms).filter((r) => r.startsWith("preview:")).length;
30827
+ if (currentSubs >= settings2.maxSubscriptionsPerSocket) {
30828
+ return { success: false, error: `Maximum preview subscriptions (${settings2.maxSubscriptionsPerSocket}) reached` };
30829
+ }
30830
+ if (!previewSubscribers.has(entityKey)) {
30831
+ previewSubscribers.set(entityKey, /* @__PURE__ */ new Set());
30832
+ }
30833
+ previewSubscribers.get(entityKey).add(socketId);
30834
+ socket.join(`preview:${entityKey}`);
30835
+ if (!socketState.has(socketId)) {
30836
+ socketState.set(socketId, { debounceTimers: /* @__PURE__ */ new Map() });
30837
+ }
30838
+ strapi2.log.debug(`socket.io: Socket ${socketId} subscribed to preview for ${entityKey}`);
30839
+ try {
30840
+ const entity = await strapi2.documents(uid).findOne({ documentId });
30841
+ if (entity) {
30842
+ socket.emit("preview:initial", {
30843
+ uid,
30844
+ documentId,
30845
+ data: entity,
30846
+ timestamp: Date.now()
30847
+ });
30848
+ }
30849
+ } catch (err) {
30850
+ strapi2.log.warn(`socket.io: Could not fetch initial preview data for ${entityKey}: ${err.message}`);
30851
+ }
30852
+ return {
30853
+ success: true,
30854
+ entityKey,
30855
+ subscriberCount: previewSubscribers.get(entityKey).size
30856
+ };
30857
+ },
30858
+ /**
30859
+ * Unsubscribes a socket from preview updates
30860
+ * @param {string} socketId - Socket ID
30861
+ * @param {string} uid - Content type UID
30862
+ * @param {string} documentId - Document ID
30863
+ * @returns {object} Unsubscription result
30864
+ */
30865
+ unsubscribe(socketId, uid, documentId) {
30866
+ const entityKey = getEntityKey(uid, documentId);
30867
+ const subscribers = previewSubscribers.get(entityKey);
30868
+ if (subscribers) {
30869
+ subscribers.delete(socketId);
30870
+ if (subscribers.size === 0) {
30871
+ previewSubscribers.delete(entityKey);
30872
+ }
30873
+ }
30874
+ const io2 = strapi2.$io?.server;
30875
+ const socket = io2?.sockets.sockets.get(socketId);
30876
+ if (socket) {
30877
+ socket.leave(`preview:${entityKey}`);
30878
+ }
30879
+ const state = socketState.get(socketId);
30880
+ if (state?.debounceTimers.has(entityKey)) {
30881
+ clearTimeout(state.debounceTimers.get(entityKey));
30882
+ state.debounceTimers.delete(entityKey);
30883
+ }
30884
+ strapi2.log.debug(`socket.io: Socket ${socketId} unsubscribed from preview for ${entityKey}`);
30885
+ return { success: true, entityKey };
30886
+ },
30887
+ /**
30888
+ * Cleans up all subscriptions for a socket
30889
+ * @param {string} socketId - Socket ID
30890
+ */
30891
+ cleanupSocket(socketId) {
30892
+ for (const [entityKey, subscribers] of previewSubscribers) {
30893
+ if (subscribers.has(socketId)) {
30894
+ subscribers.delete(socketId);
30895
+ if (subscribers.size === 0) {
30896
+ previewSubscribers.delete(entityKey);
30897
+ }
30898
+ }
30899
+ }
30900
+ const state = socketState.get(socketId);
30901
+ if (state) {
30902
+ for (const timerId of state.debounceTimers.values()) {
30903
+ clearTimeout(timerId);
30904
+ }
30905
+ socketState.delete(socketId);
30906
+ }
30907
+ },
30908
+ /**
30909
+ * Emits a draft change event to preview subscribers
30910
+ * @param {string} uid - Content type UID
30911
+ * @param {string} documentId - Document ID
30912
+ * @param {object} data - Changed data
30913
+ * @param {object} diff - Field-level diff (optional)
30914
+ */
30915
+ emitDraftChange(uid, documentId, data, diff2 = null) {
30916
+ const settings2 = getPreviewSettings();
30917
+ if (!settings2.enabled || !settings2.draftEvents) return;
30918
+ emitToSubscribers(uid, documentId, "preview:change", {
30919
+ data,
30920
+ diff: diff2,
30921
+ isDraft: true
30922
+ });
30923
+ },
30924
+ /**
30925
+ * Emits a debounced field change event
30926
+ * @param {string} socketId - Socket ID of the editor
30927
+ * @param {string} uid - Content type UID
30928
+ * @param {string} documentId - Document ID
30929
+ * @param {string} fieldName - Name of changed field
30930
+ * @param {*} value - New field value
30931
+ */
30932
+ emitFieldChange(socketId, uid, documentId, fieldName, value) {
30933
+ const settings2 = getPreviewSettings();
30934
+ if (!settings2.enabled) return;
30935
+ const entityKey = getEntityKey(uid, documentId);
30936
+ const state = socketState.get(socketId);
30937
+ if (state?.debounceTimers.has(entityKey)) {
30938
+ clearTimeout(state.debounceTimers.get(entityKey));
30939
+ }
30940
+ const timerId = setTimeout(() => {
30941
+ emitToSubscribers(uid, documentId, "preview:field", {
30942
+ fieldName,
30943
+ value,
30944
+ editorSocketId: socketId
30945
+ });
30946
+ state?.debounceTimers.delete(entityKey);
30947
+ }, settings2.debounceMs);
30948
+ if (state) {
30949
+ state.debounceTimers.set(entityKey, timerId);
30950
+ }
30951
+ },
30952
+ /**
30953
+ * Emits publish event to preview subscribers
30954
+ * @param {string} uid - Content type UID
30955
+ * @param {string} documentId - Document ID
30956
+ * @param {object} data - Published data
30957
+ */
30958
+ emitPublish(uid, documentId, data) {
30959
+ emitToSubscribers(uid, documentId, "preview:publish", {
30960
+ data,
30961
+ isDraft: false
30962
+ });
30963
+ },
30964
+ /**
30965
+ * Emits unpublish event to preview subscribers
30966
+ * @param {string} uid - Content type UID
30967
+ * @param {string} documentId - Document ID
30968
+ */
30969
+ emitUnpublish(uid, documentId) {
30970
+ emitToSubscribers(uid, documentId, "preview:unpublish", {
30971
+ isDraft: true
30972
+ });
30973
+ },
30974
+ /**
30975
+ * Gets the number of preview subscribers for an entity
30976
+ * @param {string} uid - Content type UID
30977
+ * @param {string} documentId - Document ID
30978
+ * @returns {number} Subscriber count
30979
+ */
30980
+ getSubscriberCount(uid, documentId) {
30981
+ const entityKey = getEntityKey(uid, documentId);
30982
+ return previewSubscribers.get(entityKey)?.size || 0;
30983
+ },
30984
+ /**
30985
+ * Gets all entities with active preview subscribers
30986
+ * @returns {Array} List of entity keys with subscriber counts
30987
+ */
30988
+ getActivePreviewEntities() {
30989
+ const entities = [];
30990
+ for (const [entityKey, subscribers] of previewSubscribers) {
30991
+ const [uid, documentId] = entityKey.split(":");
30992
+ entities.push({
30993
+ uid,
30994
+ documentId,
30995
+ entityKey,
30996
+ subscriberCount: subscribers.size
30997
+ });
30998
+ }
30999
+ return entities;
31000
+ },
31001
+ /**
31002
+ * Checks if live preview is enabled
31003
+ * @returns {boolean} True if enabled
31004
+ */
31005
+ isEnabled() {
31006
+ return getPreviewSettings().enabled;
31007
+ },
31008
+ /**
31009
+ * Gets preview statistics
31010
+ * @returns {object} Preview stats
31011
+ */
31012
+ getStats() {
31013
+ let totalSubscriptions = 0;
31014
+ for (const subscribers of previewSubscribers.values()) {
31015
+ totalSubscriptions += subscribers.size;
31016
+ }
31017
+ return {
31018
+ totalEntitiesWithSubscribers: previewSubscribers.size,
31019
+ totalSubscriptions,
31020
+ entities: this.getActivePreviewEntities()
31021
+ };
31022
+ }
31023
+ };
31024
+ };
31025
+ var diff$1 = ({ strapi: strapi2 }) => {
31026
+ const getDiffSettings = () => {
31027
+ const settings2 = strapi2.$ioSettings || {};
31028
+ return {
31029
+ enabled: settings2.fieldLevelChanges?.enabled ?? true,
31030
+ includeFullData: settings2.fieldLevelChanges?.includeFullData ?? false,
31031
+ excludeFields: settings2.fieldLevelChanges?.excludeFields ?? ["updatedAt", "updatedBy", "createdAt", "createdBy"],
31032
+ maxDiffDepth: settings2.fieldLevelChanges?.maxDiffDepth ?? 3
31033
+ };
31034
+ };
31035
+ const isPlainObject2 = (value) => {
31036
+ return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
31037
+ };
31038
+ const isEqual2 = (a, b) => {
31039
+ if (a === b) return true;
31040
+ if (a === null || b === null) return a === b;
31041
+ if (typeof a !== typeof b) return false;
31042
+ if (a instanceof Date && b instanceof Date) {
31043
+ return a.getTime() === b.getTime();
31044
+ }
31045
+ if (Array.isArray(a) && Array.isArray(b)) {
31046
+ if (a.length !== b.length) return false;
31047
+ return a.every((item, index2) => isEqual2(item, b[index2]));
31048
+ }
31049
+ if (isPlainObject2(a) && isPlainObject2(b)) {
31050
+ const keysA = Object.keys(a);
31051
+ const keysB = Object.keys(b);
31052
+ if (keysA.length !== keysB.length) return false;
31053
+ return keysA.every((key) => isEqual2(a[key], b[key]));
31054
+ }
31055
+ return false;
31056
+ };
31057
+ const safeClone = (value) => {
31058
+ if (value === null || value === void 0) return value;
31059
+ if (value instanceof Date) return value.toISOString();
31060
+ if (Array.isArray(value)) return value.map(safeClone);
31061
+ if (isPlainObject2(value)) {
31062
+ const cloned = {};
31063
+ for (const [key, val] of Object.entries(value)) {
31064
+ cloned[key] = safeClone(val);
31065
+ }
31066
+ return cloned;
31067
+ }
31068
+ return value;
31069
+ };
31070
+ const calculateDiffInternal = (oldData, newData, options = {}, depth2 = 0) => {
31071
+ const { excludeFields = [], maxDiffDepth = 3 } = options;
31072
+ const diff2 = {};
31073
+ if (!oldData || !newData) {
31074
+ return { _replaced: true, old: safeClone(oldData), new: safeClone(newData) };
31075
+ }
31076
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(oldData || {}), ...Object.keys(newData || {})]);
31077
+ for (const key of allKeys) {
31078
+ if (excludeFields.includes(key)) continue;
31079
+ const oldValue = oldData?.[key];
31080
+ const newValue = newData?.[key];
31081
+ if (isEqual2(oldValue, newValue)) continue;
31082
+ if (isPlainObject2(oldValue) && isPlainObject2(newValue) && depth2 < maxDiffDepth) {
31083
+ const nestedDiff = calculateDiffInternal(oldValue, newValue, options, depth2 + 1);
31084
+ if (Object.keys(nestedDiff).length > 0) {
31085
+ diff2[key] = nestedDiff;
31086
+ }
31087
+ } else {
31088
+ diff2[key] = {
31089
+ old: safeClone(oldValue),
31090
+ new: safeClone(newValue)
31091
+ };
31092
+ }
31093
+ }
31094
+ return diff2;
31095
+ };
31096
+ return {
31097
+ /**
31098
+ * Calculates field-level diff between old and new data
31099
+ * @param {object} oldData - Previous data state
31100
+ * @param {object} newData - New data state
31101
+ * @returns {object} Diff result with changed fields and metadata
31102
+ */
31103
+ calculateDiff(oldData, newData) {
31104
+ const settings2 = getDiffSettings();
31105
+ if (!settings2.enabled) {
31106
+ return {
31107
+ enabled: false,
31108
+ hasChanges: !isEqual2(oldData, newData),
31109
+ diff: null,
31110
+ fullData: newData
31111
+ };
31112
+ }
31113
+ const diff2 = calculateDiffInternal(oldData, newData, {
31114
+ excludeFields: settings2.excludeFields,
31115
+ maxDiffDepth: settings2.maxDiffDepth
31116
+ });
31117
+ const changedFields = Object.keys(diff2);
31118
+ const hasChanges = changedFields.length > 0;
31119
+ const result = {
31120
+ enabled: true,
31121
+ hasChanges,
31122
+ changedFields,
31123
+ changedFieldCount: changedFields.length,
31124
+ diff: hasChanges ? diff2 : null,
31125
+ timestamp: Date.now()
31126
+ };
31127
+ if (settings2.includeFullData) {
31128
+ result.fullData = newData;
31129
+ }
31130
+ return result;
31131
+ },
31132
+ /**
31133
+ * Applies a diff to a target object
31134
+ * @param {object} target - Target object to apply diff to
31135
+ * @param {object} diff - Diff to apply
31136
+ * @returns {object} Updated target object
31137
+ */
31138
+ applyDiff(target, diff2) {
31139
+ if (!diff2 || typeof diff2 !== "object") return target;
31140
+ const result = { ...target };
31141
+ for (const [key, change] of Object.entries(diff2)) {
31142
+ if (change._replaced) {
31143
+ result[key] = change.new;
31144
+ } else if (change.old !== void 0 && change.new !== void 0) {
31145
+ result[key] = change.new;
31146
+ } else if (isPlainObject2(change)) {
31147
+ result[key] = this.applyDiff(result[key] || {}, change);
31148
+ }
31149
+ }
31150
+ return result;
31151
+ },
31152
+ /**
31153
+ * Validates if a diff is applicable to a content type
31154
+ * @param {string} uid - Content type UID
31155
+ * @param {object} diff - Diff to validate
31156
+ * @returns {object} Validation result
31157
+ */
31158
+ validateDiff(uid, diff2) {
31159
+ if (!diff2) {
31160
+ return { valid: true, errors: [] };
31161
+ }
31162
+ const contentType = strapi2.contentTypes[uid];
31163
+ if (!contentType) {
31164
+ return { valid: false, errors: [`Content type ${uid} not found`] };
31165
+ }
31166
+ const errors2 = [];
31167
+ const attributes = contentType.attributes || {};
31168
+ for (const field of Object.keys(diff2)) {
31169
+ if (!attributes[field] && field !== "id" && field !== "documentId") {
31170
+ errors2.push(`Field '${field}' does not exist in ${uid}`);
31171
+ }
31172
+ }
31173
+ return {
31174
+ valid: errors2.length === 0,
31175
+ errors: errors2
31176
+ };
31177
+ },
31178
+ /**
31179
+ * Creates an event payload with diff information
31180
+ * @param {string} eventType - Event type (create, update, delete)
31181
+ * @param {object} schema - Content type schema info
31182
+ * @param {object} oldData - Previous data (null for create)
31183
+ * @param {object} newData - New data (null for delete)
31184
+ * @returns {object} Event payload with diff
31185
+ */
31186
+ createEventPayload(eventType, schema2, oldData, newData) {
31187
+ const settings2 = getDiffSettings();
31188
+ if (eventType === "create") {
31189
+ return {
31190
+ event: eventType,
31191
+ schema: { singularName: schema2.singularName, uid: schema2.uid },
31192
+ data: newData,
31193
+ diff: null,
31194
+ timestamp: Date.now()
31195
+ };
31196
+ }
31197
+ if (eventType === "delete") {
31198
+ return {
31199
+ event: eventType,
31200
+ schema: { singularName: schema2.singularName, uid: schema2.uid },
31201
+ data: { id: oldData?.id, documentId: oldData?.documentId },
31202
+ deletedData: settings2.includeFullData ? oldData : null,
31203
+ diff: null,
31204
+ timestamp: Date.now()
31205
+ };
31206
+ }
31207
+ const diffResult = this.calculateDiff(oldData, newData);
31208
+ const payload = {
31209
+ event: eventType,
31210
+ schema: { singularName: schema2.singularName, uid: schema2.uid },
31211
+ documentId: newData?.documentId || newData?.id,
31212
+ diff: diffResult.diff,
31213
+ changedFields: diffResult.changedFields,
31214
+ hasChanges: diffResult.hasChanges,
31215
+ timestamp: Date.now()
31216
+ };
31217
+ if (settings2.includeFullData || !settings2.enabled) {
31218
+ payload.data = newData;
31219
+ }
31220
+ return payload;
31221
+ },
31222
+ /**
31223
+ * Checks if diff feature is enabled
31224
+ * @returns {boolean} True if enabled
31225
+ */
31226
+ isEnabled() {
31227
+ return getDiffSettings().enabled;
31228
+ },
31229
+ /**
31230
+ * Gets current diff settings
31231
+ * @returns {object} Current settings
31232
+ */
31233
+ getSettings() {
31234
+ return getDiffSettings();
31235
+ }
31236
+ };
31237
+ };
29931
31238
  const strategy = strategies;
29932
31239
  const sanitize = sanitize_1;
29933
31240
  const transform = transform$1;
29934
31241
  const settings = settings$1;
29935
31242
  const monitoring = monitoring$1;
31243
+ const presence = presence$1;
31244
+ const preview = preview$1;
31245
+ const diff = diff$1;
29936
31246
  var services$1 = {
29937
31247
  sanitize,
29938
31248
  strategy,
29939
31249
  transform,
29940
31250
  settings,
29941
- monitoring
31251
+ monitoring,
31252
+ presence,
31253
+ preview,
31254
+ diff
29942
31255
  };
29943
31256
  const bootstrap = bootstrap_1;
29944
31257
  const config = config$1;