@strapi-community/plugin-io 5.0.6 → 5.1.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"],
@@ -400,6 +413,11 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
400
413
  }
401
414
  });
402
415
  }
416
+ const presenceService = strapi2.plugin(pluginId$6).service("presence");
417
+ const previewService = strapi2.plugin(pluginId$6).service("preview");
418
+ if (settings2.presence?.enabled !== false) {
419
+ presenceService.startCleanupInterval();
420
+ }
403
421
  io2.server.on("connection", (socket) => {
404
422
  const clientIp = socket.handshake.address || "unknown";
405
423
  const username = socket.user?.username || "anonymous";
@@ -411,6 +429,10 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
411
429
  user: socket.user || null
412
430
  });
413
431
  }
432
+ if (settings2.presence?.enabled !== false) {
433
+ const user = socket.user || socket.adminUser;
434
+ presenceService.registerConnection(socket.id, user);
435
+ }
414
436
  if (settings2.rooms?.autoJoinByRole) {
415
437
  const userRole = socket.user?.role || "public";
416
438
  const rooms = settings2.rooms.autoJoinByRole[userRole] || [];
@@ -455,6 +477,70 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
455
477
  const rooms = Array.from(socket.rooms).filter((r) => r !== socket.id);
456
478
  if (callback) callback({ success: true, rooms });
457
479
  });
480
+ socket.on("presence:join", async ({ uid, documentId }, callback) => {
481
+ if (settings2.presence?.enabled === false) {
482
+ if (callback) callback({ success: false, error: "Presence is disabled" });
483
+ return;
484
+ }
485
+ if (!uid || !documentId) {
486
+ if (callback) callback({ success: false, error: "uid and documentId are required" });
487
+ return;
488
+ }
489
+ const result = await presenceService.joinEntity(socket.id, uid, documentId);
490
+ if (callback) callback(result);
491
+ });
492
+ socket.on("presence:leave", async ({ uid, documentId }, callback) => {
493
+ if (settings2.presence?.enabled === false) {
494
+ if (callback) callback({ success: false, error: "Presence is disabled" });
495
+ return;
496
+ }
497
+ if (!uid || !documentId) {
498
+ if (callback) callback({ success: false, error: "uid and documentId are required" });
499
+ return;
500
+ }
501
+ const result = await presenceService.leaveEntity(socket.id, uid, documentId);
502
+ if (callback) callback(result);
503
+ });
504
+ socket.on("presence:heartbeat", (callback) => {
505
+ const result = presenceService.heartbeat(socket.id);
506
+ if (callback) callback(result);
507
+ });
508
+ socket.on("presence:typing", ({ uid, documentId, fieldName }) => {
509
+ if (settings2.presence?.enabled === false) return;
510
+ presenceService.broadcastTyping(socket.id, uid, documentId, fieldName);
511
+ });
512
+ socket.on("presence:check", async ({ uid, documentId }, callback) => {
513
+ if (settings2.presence?.enabled === false) {
514
+ if (callback) callback({ success: false, error: "Presence is disabled" });
515
+ return;
516
+ }
517
+ const editors = await presenceService.getEntityEditors(uid, documentId);
518
+ if (callback) callback({ success: true, editors, isBeingEdited: editors.length > 0 });
519
+ });
520
+ socket.on("preview:subscribe", async ({ uid, documentId }, callback) => {
521
+ if (settings2.livePreview?.enabled === false) {
522
+ if (callback) callback({ success: false, error: "Live preview is disabled" });
523
+ return;
524
+ }
525
+ if (!uid || !documentId) {
526
+ if (callback) callback({ success: false, error: "uid and documentId are required" });
527
+ return;
528
+ }
529
+ const result = await previewService.subscribe(socket.id, uid, documentId);
530
+ if (callback) callback(result);
531
+ });
532
+ socket.on("preview:unsubscribe", ({ uid, documentId }, callback) => {
533
+ if (!uid || !documentId) {
534
+ if (callback) callback({ success: false, error: "uid and documentId are required" });
535
+ return;
536
+ }
537
+ const result = previewService.unsubscribe(socket.id, uid, documentId);
538
+ if (callback) callback(result);
539
+ });
540
+ socket.on("preview:field-change", ({ uid, documentId, fieldName, value }) => {
541
+ if (settings2.livePreview?.enabled === false) return;
542
+ previewService.emitFieldChange(socket.id, uid, documentId, fieldName, value);
543
+ });
458
544
  socket.on("subscribe-entity", async ({ uid, id }, callback) => {
459
545
  if (settings2.entitySubscriptions?.enabled === false) {
460
546
  if (callback) callback({ success: false, error: "Entity subscriptions are disabled" });
@@ -589,7 +675,7 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
589
675
  strapi2.log.debug(`socket.io: Private message from ${socket.id} to ${to}`);
590
676
  if (callback) callback({ success: true });
591
677
  });
592
- socket.on("disconnect", (reason) => {
678
+ socket.on("disconnect", async (reason) => {
593
679
  if (settings2.monitoring?.enableConnectionLogging) {
594
680
  strapi2.log.info(`socket.io: Client disconnected (id: ${socket.id}, user: ${username}, reason: ${reason})`);
595
681
  monitoringService.logEvent("disconnect", {
@@ -598,6 +684,12 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
598
684
  user: socket.user || null
599
685
  });
600
686
  }
687
+ if (settings2.presence?.enabled !== false) {
688
+ await presenceService.unregisterConnection(socket.id);
689
+ }
690
+ if (settings2.livePreview?.enabled !== false) {
691
+ previewService.cleanupSocket(socket.id);
692
+ }
601
693
  });
602
694
  socket.on("error", (error2) => {
603
695
  strapi2.log.error(`socket.io: Socket error (id: ${socket.id}): ${error2.message}`);
@@ -741,17 +833,52 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
741
833
  }
742
834
  });
743
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
+ };
744
868
  const origins = settings2.cors?.origins?.join(", ") || "http://localhost:3000";
745
869
  const features = [];
746
870
  if (settings2.redis?.enabled) features.push("Redis");
747
871
  if (settings2.namespaces?.enabled) features.push(`Namespaces(${Object.keys(settings2.namespaces.list || {}).length})`);
748
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");
749
876
  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}`);
877
+ strapi2.log.info(` - Origins: ${origins}`);
878
+ strapi2.log.info(` - Content Types: ${enabledContentTypes}`);
879
+ strapi2.log.info(` - Max Connections: ${settings2.connection?.maxConnections || 1e3}`);
753
880
  if (features.length > 0) {
754
- strapi2.log.info(` Features: ${features.join(", ")}`);
881
+ strapi2.log.info(` - Features: ${features.join(", ")}`);
755
882
  }
756
883
  }
757
884
  var io = { bootstrapIO: bootstrapIO$1 };
@@ -825,7 +952,7 @@ function getTransactionCtx() {
825
952
  }
826
953
  return transactionCtx;
827
954
  }
828
- const { pluginId: pluginId$3 } = pluginId_1;
955
+ const { pluginId: pluginId$5 } = pluginId_1;
829
956
  function scheduleAfterTransaction(callback, delay = 0) {
830
957
  const runner = () => setTimeout(callback, delay);
831
958
  const ctx = getTransactionCtx();
@@ -912,17 +1039,47 @@ async function bootstrapLifecycles$1({ strapi: strapi2 }) {
912
1039
  }, 50);
913
1040
  }
914
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
+ };
915
1058
  subscriber.afterUpdate = async (event) => {
916
1059
  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
- };
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 };
923
1063
  scheduleAfterTransaction(() => {
924
1064
  try {
925
- 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
+ }
926
1083
  } catch (error2) {
927
1084
  strapi2.log.debug(`socket.io: Could not emit update event for ${uid}:`, error2.message);
928
1085
  }
@@ -1023,14 +1180,14 @@ var config$1 = {
1023
1180
  validator(config2) {
1024
1181
  }
1025
1182
  };
1026
- const { pluginId: pluginId$2 } = pluginId_1;
1183
+ const { pluginId: pluginId$4 } = pluginId_1;
1027
1184
  var settings$3 = ({ strapi: strapi2 }) => ({
1028
1185
  /**
1029
1186
  * GET /io/settings
1030
1187
  * Retrieve current plugin settings
1031
1188
  */
1032
1189
  async getSettings(ctx) {
1033
- const settingsService = strapi2.plugin(pluginId$2).service("settings");
1190
+ const settingsService = strapi2.plugin(pluginId$4).service("settings");
1034
1191
  const settings2 = await settingsService.getSettings();
1035
1192
  ctx.body = { data: settings2 };
1036
1193
  },
@@ -1039,7 +1196,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1039
1196
  * Update plugin settings and hot-reload Socket.IO
1040
1197
  */
1041
1198
  async updateSettings(ctx) {
1042
- const settingsService = strapi2.plugin(pluginId$2).service("settings");
1199
+ const settingsService = strapi2.plugin(pluginId$4).service("settings");
1043
1200
  const { body } = ctx.request;
1044
1201
  await settingsService.getSettings();
1045
1202
  const updatedSettings = await settingsService.setSettings(body);
@@ -1072,7 +1229,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1072
1229
  * Get connection and event statistics
1073
1230
  */
1074
1231
  async getStats(ctx) {
1075
- const monitoringService = strapi2.plugin(pluginId$2).service("monitoring");
1232
+ const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
1076
1233
  const connectionStats = monitoringService.getConnectionStats();
1077
1234
  const eventStats = monitoringService.getEventStats();
1078
1235
  ctx.body = {
@@ -1087,7 +1244,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1087
1244
  * Get recent event log
1088
1245
  */
1089
1246
  async getEventLog(ctx) {
1090
- const monitoringService = strapi2.plugin(pluginId$2).service("monitoring");
1247
+ const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
1091
1248
  const limit = parseInt(ctx.query.limit) || 50;
1092
1249
  const log = monitoringService.getEventLog(limit);
1093
1250
  ctx.body = { data: log };
@@ -1097,7 +1254,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1097
1254
  * Send a test event
1098
1255
  */
1099
1256
  async sendTestEvent(ctx) {
1100
- const monitoringService = strapi2.plugin(pluginId$2).service("monitoring");
1257
+ const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
1101
1258
  const { eventName, data } = ctx.request.body;
1102
1259
  try {
1103
1260
  const result = monitoringService.sendTestEvent(eventName || "test", data || {});
@@ -1111,7 +1268,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1111
1268
  * Reset monitoring statistics
1112
1269
  */
1113
1270
  async resetStats(ctx) {
1114
- const monitoringService = strapi2.plugin(pluginId$2).service("monitoring");
1271
+ const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
1115
1272
  monitoringService.resetStats();
1116
1273
  ctx.body = { data: { success: true } };
1117
1274
  },
@@ -1135,7 +1292,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1135
1292
  * Get lightweight stats for dashboard widget
1136
1293
  */
1137
1294
  async getMonitoringStats(ctx) {
1138
- const monitoringService = strapi2.plugin(pluginId$2).service("monitoring");
1295
+ const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
1139
1296
  const connectionStats = monitoringService.getConnectionStats();
1140
1297
  const eventStats = monitoringService.getEventStats();
1141
1298
  ctx.body = {
@@ -1154,13 +1311,95 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1154
1311
  };
1155
1312
  }
1156
1313
  });
1314
+ const { randomUUID } = require$$1__default.default;
1315
+ const sessionTokens = /* @__PURE__ */ new Map();
1316
+ setInterval(() => {
1317
+ const now = Date.now();
1318
+ for (const [token, session] of sessionTokens.entries()) {
1319
+ if (session.expiresAt < now) {
1320
+ sessionTokens.delete(token);
1321
+ }
1322
+ }
1323
+ }, 5 * 60 * 1e3);
1324
+ var presence$3 = ({ strapi: strapi2 }) => ({
1325
+ /**
1326
+ * Creates a session token for admin users to connect to Socket.IO
1327
+ * @param {object} ctx - Koa context
1328
+ */
1329
+ async createSession(ctx) {
1330
+ const adminUser = ctx.state.user;
1331
+ if (!adminUser) {
1332
+ strapi2.log.warn("[plugin-io] Presence session requested without admin user");
1333
+ return ctx.unauthorized("Admin authentication required");
1334
+ }
1335
+ try {
1336
+ const token = randomUUID();
1337
+ const expiresAt = Date.now() + 2 * 60 * 1e3;
1338
+ sessionTokens.set(token, {
1339
+ token,
1340
+ user: {
1341
+ id: adminUser.id,
1342
+ email: adminUser.email,
1343
+ firstname: adminUser.firstname,
1344
+ lastname: adminUser.lastname
1345
+ },
1346
+ expiresAt
1347
+ });
1348
+ strapi2.log.info(`[plugin-io] Presence session created for admin user: ${adminUser.email}`);
1349
+ ctx.body = {
1350
+ token,
1351
+ user: {
1352
+ id: adminUser.id,
1353
+ email: adminUser.email,
1354
+ firstname: adminUser.firstname,
1355
+ lastname: adminUser.lastname
1356
+ },
1357
+ wsPath: "/socket.io",
1358
+ wsUrl: `${ctx.protocol}://${ctx.host}`
1359
+ };
1360
+ } catch (error2) {
1361
+ strapi2.log.error("[plugin-io] Failed to create presence session:", error2);
1362
+ return ctx.internalServerError("Failed to create session");
1363
+ }
1364
+ },
1365
+ /**
1366
+ * Validates and consumes a session token (one-time use)
1367
+ * @param {string} token - Session token to validate
1368
+ * @returns {object|null} Session data or null if invalid/expired
1369
+ */
1370
+ consumeSessionToken(token) {
1371
+ if (!token) {
1372
+ return null;
1373
+ }
1374
+ const session = sessionTokens.get(token);
1375
+ if (!session) {
1376
+ return null;
1377
+ }
1378
+ if (session.expiresAt < Date.now()) {
1379
+ sessionTokens.delete(token);
1380
+ return null;
1381
+ }
1382
+ return session;
1383
+ }
1384
+ });
1157
1385
  const settings$2 = settings$3;
1386
+ const presence$2 = presence$3;
1158
1387
  var controllers$1 = {
1159
- settings: settings$2
1388
+ settings: settings$2,
1389
+ presence: presence$2
1160
1390
  };
1161
1391
  var admin$1 = {
1162
1392
  type: "admin",
1163
1393
  routes: [
1394
+ // Presence Session - issues JWT token for Socket.IO connection
1395
+ {
1396
+ method: "POST",
1397
+ path: "/presence/session",
1398
+ handler: "presence.createSession",
1399
+ config: {
1400
+ policies: ["admin::isAuthenticatedAdmin"]
1401
+ }
1402
+ },
1164
1403
  {
1165
1404
  method: "GET",
1166
1405
  path: "/settings",
@@ -21500,9 +21739,9 @@ function padZeros(value, tok, options) {
21500
21739
  if (!tok.isPadded) {
21501
21740
  return value;
21502
21741
  }
21503
- let diff = Math.abs(tok.maxLen - String(value).length);
21742
+ let diff2 = Math.abs(tok.maxLen - String(value).length);
21504
21743
  let relax = options.relaxZeros !== false;
21505
- switch (diff) {
21744
+ switch (diff2) {
21506
21745
  case 0:
21507
21746
  return "";
21508
21747
  case 1:
@@ -21510,7 +21749,7 @@ function padZeros(value, tok, options) {
21510
21749
  case 2:
21511
21750
  return relax ? "0{0,2}" : "00";
21512
21751
  default: {
21513
- return relax ? `0{0,${diff}}` : `0{${diff}}`;
21752
+ return relax ? `0{0,${diff2}}` : `0{${diff2}}`;
21514
21753
  }
21515
21754
  }
21516
21755
  }
@@ -29433,6 +29672,32 @@ var strategies = ({ strapi: strapi2 }) => {
29433
29672
  const apiTokenService = getService({ type: "admin", plugin: "api-token" });
29434
29673
  const jwtService = getService({ name: "jwt", plugin: "users-permissions" });
29435
29674
  const userService = getService({ name: "user", plugin: "users-permissions" });
29675
+ const admin2 = {
29676
+ name: "io-admin",
29677
+ credentials: function(user) {
29678
+ return `${this.name}-${user.id}`;
29679
+ },
29680
+ authenticate: async function(auth) {
29681
+ const token2 = auth.token;
29682
+ if (!token2) {
29683
+ throw new UnauthorizedError2("Invalid admin credentials");
29684
+ }
29685
+ try {
29686
+ const presenceController = strapi2.plugin("io").controller("presence");
29687
+ const session = presenceController.consumeSessionToken(token2);
29688
+ if (!session) {
29689
+ throw new UnauthorizedError2("Invalid or expired session token");
29690
+ }
29691
+ return session.user;
29692
+ } catch (error2) {
29693
+ strapi2.log.warn("[plugin-io] Admin session verification failed:", error2.message);
29694
+ throw new UnauthorizedError2("Invalid admin credentials");
29695
+ }
29696
+ },
29697
+ getRoomName: function(user) {
29698
+ return `${this.name}-user-${user.id}`;
29699
+ }
29700
+ };
29436
29701
  const role = {
29437
29702
  name: "io-role",
29438
29703
  credentials: function(role2) {
@@ -29575,6 +29840,7 @@ var strategies = ({ strapi: strapi2 }) => {
29575
29840
  }
29576
29841
  };
29577
29842
  return {
29843
+ admin: admin2,
29578
29844
  role,
29579
29845
  token
29580
29846
  };
@@ -29668,12 +29934,12 @@ function transformEntry(entry, type2) {
29668
29934
  // meta: {},
29669
29935
  };
29670
29936
  }
29671
- const { pluginId: pluginId$1 } = pluginId_1;
29937
+ const { pluginId: pluginId$3 } = pluginId_1;
29672
29938
  var settings$1 = ({ strapi: strapi2 }) => {
29673
29939
  const getPluginStore = () => {
29674
29940
  return strapi2.store({
29675
29941
  type: "plugin",
29676
- name: pluginId$1
29942
+ name: pluginId$3
29677
29943
  });
29678
29944
  };
29679
29945
  const getDefaultSettings = () => ({
@@ -29776,6 +30042,41 @@ var settings$1 = ({ strapi: strapi2 }) => {
29776
30042
  enableConnectionLogging: true,
29777
30043
  enableEventLogging: false,
29778
30044
  maxEventLogSize: 100
30045
+ },
30046
+ // Presence System (Collaboration Awareness)
30047
+ presence: {
30048
+ enabled: true,
30049
+ // Enable presence tracking
30050
+ heartbeatInterval: 3e4,
30051
+ // Heartbeat interval in ms
30052
+ staleTimeout: 6e4,
30053
+ // Time before connection considered stale
30054
+ showAvatars: true,
30055
+ // Show user avatars in UI
30056
+ showTypingIndicator: true
30057
+ // Show typing indicators
30058
+ },
30059
+ // Live Preview (Real-time Draft Updates)
30060
+ livePreview: {
30061
+ enabled: true,
30062
+ // Enable live preview
30063
+ draftEvents: true,
30064
+ // Emit events for draft changes
30065
+ debounceMs: 300,
30066
+ // Debounce field changes
30067
+ maxSubscriptionsPerSocket: 50
30068
+ // Max preview subscriptions per socket
30069
+ },
30070
+ // Field-level Changes (Diff-based Updates)
30071
+ fieldLevelChanges: {
30072
+ enabled: true,
30073
+ // Enable field-level diff
30074
+ includeFullData: false,
30075
+ // Include full data alongside diff
30076
+ excludeFields: ["updatedAt", "updatedBy", "createdAt", "createdBy"],
30077
+ // Fields to exclude from diff
30078
+ maxDiffDepth: 3
30079
+ // Maximum nesting depth for diff
29779
30080
  }
29780
30081
  });
29781
30082
  return {
@@ -29816,7 +30117,7 @@ var settings$1 = ({ strapi: strapi2 }) => {
29816
30117
  getDefaultSettings
29817
30118
  };
29818
30119
  };
29819
- const { pluginId } = pluginId_1;
30120
+ const { pluginId: pluginId$2 } = pluginId_1;
29820
30121
  var monitoring$1 = ({ strapi: strapi2 }) => {
29821
30122
  let eventLog = [];
29822
30123
  let eventStats = {
@@ -29960,17 +30261,795 @@ var monitoring$1 = ({ strapi: strapi2 }) => {
29960
30261
  }
29961
30262
  };
29962
30263
  };
30264
+ const { pluginId: pluginId$1 } = pluginId_1;
30265
+ var presence$1 = ({ strapi: strapi2 }) => {
30266
+ const activeConnections = /* @__PURE__ */ new Map();
30267
+ const entityEditors = /* @__PURE__ */ new Map();
30268
+ let cleanupInterval = null;
30269
+ const getEntityKey = (uid, documentId) => `${uid}:${documentId}`;
30270
+ const getPresenceSettings = () => {
30271
+ const settings2 = strapi2.$ioSettings || {};
30272
+ return {
30273
+ enabled: settings2.presence?.enabled ?? true,
30274
+ heartbeatInterval: settings2.presence?.heartbeatInterval ?? 3e4,
30275
+ staleTimeout: settings2.presence?.staleTimeout ?? 6e4,
30276
+ showAvatars: settings2.presence?.showAvatars ?? true
30277
+ };
30278
+ };
30279
+ const broadcastPresenceUpdate = async (uid, documentId) => {
30280
+ const io2 = strapi2.$io?.server;
30281
+ if (!io2) return;
30282
+ const entityKey = getEntityKey(uid, documentId);
30283
+ const editorSocketIds = entityEditors.get(entityKey) || /* @__PURE__ */ new Set();
30284
+ const editors = [];
30285
+ for (const socketId of editorSocketIds) {
30286
+ const connection = activeConnections.get(socketId);
30287
+ if (connection?.user) {
30288
+ editors.push({
30289
+ socketId,
30290
+ user: {
30291
+ id: connection.user.id,
30292
+ username: connection.user.username,
30293
+ email: connection.user.email,
30294
+ firstname: connection.user.firstname,
30295
+ lastname: connection.user.lastname
30296
+ },
30297
+ joinedAt: connection.entities?.get(entityKey) || Date.now()
30298
+ });
30299
+ }
30300
+ }
30301
+ const roomName = `presence:${entityKey}`;
30302
+ io2.to(roomName).emit("presence:update", {
30303
+ uid,
30304
+ documentId,
30305
+ editors,
30306
+ count: editors.length,
30307
+ timestamp: Date.now()
30308
+ });
30309
+ strapi2.log.debug(`socket.io: Presence update for ${entityKey} - ${editors.length} editor(s)`);
30310
+ };
30311
+ return {
30312
+ /**
30313
+ * Registers a new socket connection for presence tracking
30314
+ * @param {string} socketId - Socket ID
30315
+ * @param {object} user - User object (can be null for anonymous)
30316
+ */
30317
+ registerConnection(socketId, user = null) {
30318
+ const settings2 = getPresenceSettings();
30319
+ if (!settings2.enabled) return;
30320
+ activeConnections.set(socketId, {
30321
+ user,
30322
+ entities: /* @__PURE__ */ new Map(),
30323
+ // entityKey -> joinedAt timestamp
30324
+ lastSeen: Date.now(),
30325
+ connectedAt: Date.now()
30326
+ });
30327
+ strapi2.log.debug(`socket.io: Presence registered for socket ${socketId}`);
30328
+ },
30329
+ /**
30330
+ * Unregisters a socket connection and cleans up all entity presence
30331
+ * @param {string} socketId - Socket ID
30332
+ */
30333
+ async unregisterConnection(socketId) {
30334
+ const connection = activeConnections.get(socketId);
30335
+ if (!connection) return;
30336
+ if (connection.entities) {
30337
+ for (const entityKey of connection.entities.keys()) {
30338
+ const [uid, documentId] = entityKey.split(":");
30339
+ await this.leaveEntity(socketId, uid, documentId, false);
30340
+ }
30341
+ }
30342
+ activeConnections.delete(socketId);
30343
+ strapi2.log.debug(`socket.io: Presence unregistered for socket ${socketId}`);
30344
+ },
30345
+ /**
30346
+ * User joins an entity for editing
30347
+ * @param {string} socketId - Socket ID
30348
+ * @param {string} uid - Content type UID
30349
+ * @param {string} documentId - Document ID
30350
+ * @returns {object} Join result with current editors
30351
+ */
30352
+ async joinEntity(socketId, uid, documentId) {
30353
+ const settings2 = getPresenceSettings();
30354
+ if (!settings2.enabled) {
30355
+ return { success: false, error: "Presence is disabled" };
30356
+ }
30357
+ const connection = activeConnections.get(socketId);
30358
+ if (!connection) {
30359
+ return { success: false, error: "Socket not registered for presence" };
30360
+ }
30361
+ const entityKey = getEntityKey(uid, documentId);
30362
+ if (!entityEditors.has(entityKey)) {
30363
+ entityEditors.set(entityKey, /* @__PURE__ */ new Set());
30364
+ }
30365
+ entityEditors.get(entityKey).add(socketId);
30366
+ connection.entities.set(entityKey, Date.now());
30367
+ connection.lastSeen = Date.now();
30368
+ const io2 = strapi2.$io?.server;
30369
+ const socket = io2?.sockets.sockets.get(socketId);
30370
+ if (socket) {
30371
+ socket.join(`presence:${entityKey}`);
30372
+ }
30373
+ await broadcastPresenceUpdate(uid, documentId);
30374
+ strapi2.log.info(`socket.io: User ${connection.user?.username || "anonymous"} joined entity ${entityKey}`);
30375
+ return {
30376
+ success: true,
30377
+ entityKey,
30378
+ editors: await this.getEntityEditors(uid, documentId)
30379
+ };
30380
+ },
30381
+ /**
30382
+ * User leaves an entity
30383
+ * @param {string} socketId - Socket ID
30384
+ * @param {string} uid - Content type UID
30385
+ * @param {string} documentId - Document ID
30386
+ * @param {boolean} broadcast - Whether to broadcast update (default: true)
30387
+ * @returns {object} Leave result
30388
+ */
30389
+ async leaveEntity(socketId, uid, documentId, broadcast = true) {
30390
+ const settings2 = getPresenceSettings();
30391
+ if (!settings2.enabled) {
30392
+ return { success: false, error: "Presence is disabled" };
30393
+ }
30394
+ const entityKey = getEntityKey(uid, documentId);
30395
+ const connection = activeConnections.get(socketId);
30396
+ const editors = entityEditors.get(entityKey);
30397
+ if (editors) {
30398
+ editors.delete(socketId);
30399
+ if (editors.size === 0) {
30400
+ entityEditors.delete(entityKey);
30401
+ }
30402
+ }
30403
+ if (connection?.entities) {
30404
+ connection.entities.delete(entityKey);
30405
+ }
30406
+ const io2 = strapi2.$io?.server;
30407
+ const socket = io2?.sockets.sockets.get(socketId);
30408
+ if (socket) {
30409
+ socket.leave(`presence:${entityKey}`);
30410
+ }
30411
+ if (broadcast) {
30412
+ await broadcastPresenceUpdate(uid, documentId);
30413
+ }
30414
+ strapi2.log.debug(`socket.io: Socket ${socketId} left entity ${entityKey}`);
30415
+ return { success: true, entityKey };
30416
+ },
30417
+ /**
30418
+ * Gets all editors currently editing an entity
30419
+ * @param {string} uid - Content type UID
30420
+ * @param {string} documentId - Document ID
30421
+ * @returns {Array} List of editors with user info
30422
+ */
30423
+ async getEntityEditors(uid, documentId) {
30424
+ const entityKey = getEntityKey(uid, documentId);
30425
+ const editorSocketIds = entityEditors.get(entityKey) || /* @__PURE__ */ new Set();
30426
+ const editors = [];
30427
+ for (const socketId of editorSocketIds) {
30428
+ const connection = activeConnections.get(socketId);
30429
+ if (connection?.user) {
30430
+ editors.push({
30431
+ socketId,
30432
+ user: {
30433
+ id: connection.user.id,
30434
+ username: connection.user.username,
30435
+ email: connection.user.email,
30436
+ firstname: connection.user.firstname,
30437
+ lastname: connection.user.lastname
30438
+ },
30439
+ joinedAt: connection.entities?.get(entityKey) || Date.now()
30440
+ });
30441
+ }
30442
+ }
30443
+ return editors;
30444
+ },
30445
+ /**
30446
+ * Updates heartbeat for a socket to keep presence alive
30447
+ * @param {string} socketId - Socket ID
30448
+ * @returns {object} Heartbeat result
30449
+ */
30450
+ heartbeat(socketId) {
30451
+ const connection = activeConnections.get(socketId);
30452
+ if (!connection) {
30453
+ return { success: false, error: "Socket not registered" };
30454
+ }
30455
+ connection.lastSeen = Date.now();
30456
+ return { success: true, lastSeen: connection.lastSeen };
30457
+ },
30458
+ /**
30459
+ * Cleans up stale connections that haven't sent heartbeat
30460
+ * @returns {number} Number of connections cleaned up
30461
+ */
30462
+ async cleanup() {
30463
+ const settings2 = getPresenceSettings();
30464
+ const staleTimeout = settings2.staleTimeout;
30465
+ const now = Date.now();
30466
+ let cleanedUp = 0;
30467
+ for (const [socketId, connection] of activeConnections) {
30468
+ if (now - connection.lastSeen > staleTimeout) {
30469
+ await this.unregisterConnection(socketId);
30470
+ cleanedUp++;
30471
+ }
30472
+ }
30473
+ if (cleanedUp > 0) {
30474
+ strapi2.log.info(`socket.io: Presence cleanup removed ${cleanedUp} stale connection(s)`);
30475
+ }
30476
+ return cleanedUp;
30477
+ },
30478
+ /**
30479
+ * Starts the cleanup interval
30480
+ */
30481
+ startCleanupInterval() {
30482
+ const settings2 = getPresenceSettings();
30483
+ if (!settings2.enabled) return;
30484
+ cleanupInterval = setInterval(() => {
30485
+ this.cleanup();
30486
+ }, 6e4);
30487
+ strapi2.log.debug("socket.io: Presence cleanup interval started");
30488
+ },
30489
+ /**
30490
+ * Stops the cleanup interval
30491
+ */
30492
+ stopCleanupInterval() {
30493
+ if (cleanupInterval) {
30494
+ clearInterval(cleanupInterval);
30495
+ cleanupInterval = null;
30496
+ }
30497
+ },
30498
+ /**
30499
+ * Gets presence statistics
30500
+ * @returns {object} Presence stats
30501
+ */
30502
+ getStats() {
30503
+ const totalConnections = activeConnections.size;
30504
+ const totalEntitiesBeingEdited = entityEditors.size;
30505
+ let authenticated = 0;
30506
+ let anonymous = 0;
30507
+ for (const connection of activeConnections.values()) {
30508
+ if (connection.user) {
30509
+ authenticated++;
30510
+ } else {
30511
+ anonymous++;
30512
+ }
30513
+ }
30514
+ return {
30515
+ totalConnections,
30516
+ authenticated,
30517
+ anonymous,
30518
+ totalEntitiesBeingEdited,
30519
+ entities: Array.from(entityEditors.entries()).map(([key, editors]) => ({
30520
+ entityKey: key,
30521
+ editorCount: editors.size
30522
+ }))
30523
+ };
30524
+ },
30525
+ /**
30526
+ * Gets all entities a user is currently editing
30527
+ * @param {string} socketId - Socket ID
30528
+ * @returns {Array} List of entity keys
30529
+ */
30530
+ getUserEntities(socketId) {
30531
+ const connection = activeConnections.get(socketId);
30532
+ if (!connection) return [];
30533
+ return Array.from(connection.entities.keys());
30534
+ },
30535
+ /**
30536
+ * Checks if an entity is being edited by anyone
30537
+ * @param {string} uid - Content type UID
30538
+ * @param {string} documentId - Document ID
30539
+ * @returns {boolean} True if entity has editors
30540
+ */
30541
+ isEntityBeingEdited(uid, documentId) {
30542
+ const entityKey = getEntityKey(uid, documentId);
30543
+ const editors = entityEditors.get(entityKey);
30544
+ return editors ? editors.size > 0 : false;
30545
+ },
30546
+ /**
30547
+ * Broadcasts a typing indicator for an entity
30548
+ * @param {string} socketId - Socket ID of typing user
30549
+ * @param {string} uid - Content type UID
30550
+ * @param {string} documentId - Document ID
30551
+ * @param {string} fieldName - Name of field being edited
30552
+ */
30553
+ broadcastTyping(socketId, uid, documentId, fieldName) {
30554
+ const io2 = strapi2.$io?.server;
30555
+ if (!io2) return;
30556
+ const connection = activeConnections.get(socketId);
30557
+ if (!connection?.user) return;
30558
+ const entityKey = getEntityKey(uid, documentId);
30559
+ const roomName = `presence:${entityKey}`;
30560
+ const socket = io2.sockets.sockets.get(socketId);
30561
+ if (socket) {
30562
+ socket.to(roomName).emit("presence:typing", {
30563
+ uid,
30564
+ documentId,
30565
+ user: {
30566
+ id: connection.user.id,
30567
+ username: connection.user.username
30568
+ },
30569
+ fieldName,
30570
+ timestamp: Date.now()
30571
+ });
30572
+ }
30573
+ }
30574
+ };
30575
+ };
30576
+ const { pluginId } = pluginId_1;
30577
+ var preview$1 = ({ strapi: strapi2 }) => {
30578
+ const previewSubscribers = /* @__PURE__ */ new Map();
30579
+ const socketState = /* @__PURE__ */ new Map();
30580
+ const getEntityKey = (uid, documentId) => `${uid}:${documentId}`;
30581
+ const getPreviewSettings = () => {
30582
+ const settings2 = strapi2.$ioSettings || {};
30583
+ return {
30584
+ enabled: settings2.livePreview?.enabled ?? true,
30585
+ draftEvents: settings2.livePreview?.draftEvents ?? true,
30586
+ debounceMs: settings2.livePreview?.debounceMs ?? 300,
30587
+ maxSubscriptionsPerSocket: settings2.livePreview?.maxSubscriptionsPerSocket ?? 50
30588
+ };
30589
+ };
30590
+ const emitToSubscribers = (uid, documentId, eventType, data) => {
30591
+ const io2 = strapi2.$io?.server;
30592
+ if (!io2) return;
30593
+ const entityKey = getEntityKey(uid, documentId);
30594
+ const subscribers = previewSubscribers.get(entityKey);
30595
+ if (!subscribers || subscribers.size === 0) return;
30596
+ const roomName = `preview:${entityKey}`;
30597
+ io2.to(roomName).emit(eventType, {
30598
+ uid,
30599
+ documentId,
30600
+ ...data,
30601
+ timestamp: Date.now()
30602
+ });
30603
+ strapi2.log.debug(`socket.io: Preview event '${eventType}' sent to ${subscribers.size} subscriber(s) for ${entityKey}`);
30604
+ };
30605
+ return {
30606
+ /**
30607
+ * Subscribes a socket to preview updates for an entity
30608
+ * @param {string} socketId - Socket ID
30609
+ * @param {string} uid - Content type UID
30610
+ * @param {string} documentId - Document ID
30611
+ * @returns {object} Subscription result
30612
+ */
30613
+ async subscribe(socketId, uid, documentId) {
30614
+ const settings2 = getPreviewSettings();
30615
+ if (!settings2.enabled) {
30616
+ return { success: false, error: "Live preview is disabled" };
30617
+ }
30618
+ const entityKey = getEntityKey(uid, documentId);
30619
+ const io2 = strapi2.$io?.server;
30620
+ const socket = io2?.sockets.sockets.get(socketId);
30621
+ if (!socket) {
30622
+ return { success: false, error: "Socket not found" };
30623
+ }
30624
+ const currentSubs = Array.from(socket.rooms).filter((r) => r.startsWith("preview:")).length;
30625
+ if (currentSubs >= settings2.maxSubscriptionsPerSocket) {
30626
+ return { success: false, error: `Maximum preview subscriptions (${settings2.maxSubscriptionsPerSocket}) reached` };
30627
+ }
30628
+ if (!previewSubscribers.has(entityKey)) {
30629
+ previewSubscribers.set(entityKey, /* @__PURE__ */ new Set());
30630
+ }
30631
+ previewSubscribers.get(entityKey).add(socketId);
30632
+ socket.join(`preview:${entityKey}`);
30633
+ if (!socketState.has(socketId)) {
30634
+ socketState.set(socketId, { debounceTimers: /* @__PURE__ */ new Map() });
30635
+ }
30636
+ strapi2.log.debug(`socket.io: Socket ${socketId} subscribed to preview for ${entityKey}`);
30637
+ try {
30638
+ const entity = await strapi2.documents(uid).findOne({ documentId });
30639
+ if (entity) {
30640
+ socket.emit("preview:initial", {
30641
+ uid,
30642
+ documentId,
30643
+ data: entity,
30644
+ timestamp: Date.now()
30645
+ });
30646
+ }
30647
+ } catch (err) {
30648
+ strapi2.log.warn(`socket.io: Could not fetch initial preview data for ${entityKey}: ${err.message}`);
30649
+ }
30650
+ return {
30651
+ success: true,
30652
+ entityKey,
30653
+ subscriberCount: previewSubscribers.get(entityKey).size
30654
+ };
30655
+ },
30656
+ /**
30657
+ * Unsubscribes a socket from preview updates
30658
+ * @param {string} socketId - Socket ID
30659
+ * @param {string} uid - Content type UID
30660
+ * @param {string} documentId - Document ID
30661
+ * @returns {object} Unsubscription result
30662
+ */
30663
+ unsubscribe(socketId, uid, documentId) {
30664
+ const entityKey = getEntityKey(uid, documentId);
30665
+ const subscribers = previewSubscribers.get(entityKey);
30666
+ if (subscribers) {
30667
+ subscribers.delete(socketId);
30668
+ if (subscribers.size === 0) {
30669
+ previewSubscribers.delete(entityKey);
30670
+ }
30671
+ }
30672
+ const io2 = strapi2.$io?.server;
30673
+ const socket = io2?.sockets.sockets.get(socketId);
30674
+ if (socket) {
30675
+ socket.leave(`preview:${entityKey}`);
30676
+ }
30677
+ const state = socketState.get(socketId);
30678
+ if (state?.debounceTimers.has(entityKey)) {
30679
+ clearTimeout(state.debounceTimers.get(entityKey));
30680
+ state.debounceTimers.delete(entityKey);
30681
+ }
30682
+ strapi2.log.debug(`socket.io: Socket ${socketId} unsubscribed from preview for ${entityKey}`);
30683
+ return { success: true, entityKey };
30684
+ },
30685
+ /**
30686
+ * Cleans up all subscriptions for a socket
30687
+ * @param {string} socketId - Socket ID
30688
+ */
30689
+ cleanupSocket(socketId) {
30690
+ for (const [entityKey, subscribers] of previewSubscribers) {
30691
+ if (subscribers.has(socketId)) {
30692
+ subscribers.delete(socketId);
30693
+ if (subscribers.size === 0) {
30694
+ previewSubscribers.delete(entityKey);
30695
+ }
30696
+ }
30697
+ }
30698
+ const state = socketState.get(socketId);
30699
+ if (state) {
30700
+ for (const timerId of state.debounceTimers.values()) {
30701
+ clearTimeout(timerId);
30702
+ }
30703
+ socketState.delete(socketId);
30704
+ }
30705
+ },
30706
+ /**
30707
+ * Emits a draft change event to preview subscribers
30708
+ * @param {string} uid - Content type UID
30709
+ * @param {string} documentId - Document ID
30710
+ * @param {object} data - Changed data
30711
+ * @param {object} diff - Field-level diff (optional)
30712
+ */
30713
+ emitDraftChange(uid, documentId, data, diff2 = null) {
30714
+ const settings2 = getPreviewSettings();
30715
+ if (!settings2.enabled || !settings2.draftEvents) return;
30716
+ emitToSubscribers(uid, documentId, "preview:change", {
30717
+ data,
30718
+ diff: diff2,
30719
+ isDraft: true
30720
+ });
30721
+ },
30722
+ /**
30723
+ * Emits a debounced field change event
30724
+ * @param {string} socketId - Socket ID of the editor
30725
+ * @param {string} uid - Content type UID
30726
+ * @param {string} documentId - Document ID
30727
+ * @param {string} fieldName - Name of changed field
30728
+ * @param {*} value - New field value
30729
+ */
30730
+ emitFieldChange(socketId, uid, documentId, fieldName, value) {
30731
+ const settings2 = getPreviewSettings();
30732
+ if (!settings2.enabled) return;
30733
+ const entityKey = getEntityKey(uid, documentId);
30734
+ const state = socketState.get(socketId);
30735
+ if (state?.debounceTimers.has(entityKey)) {
30736
+ clearTimeout(state.debounceTimers.get(entityKey));
30737
+ }
30738
+ const timerId = setTimeout(() => {
30739
+ emitToSubscribers(uid, documentId, "preview:field", {
30740
+ fieldName,
30741
+ value,
30742
+ editorSocketId: socketId
30743
+ });
30744
+ state?.debounceTimers.delete(entityKey);
30745
+ }, settings2.debounceMs);
30746
+ if (state) {
30747
+ state.debounceTimers.set(entityKey, timerId);
30748
+ }
30749
+ },
30750
+ /**
30751
+ * Emits publish event to preview subscribers
30752
+ * @param {string} uid - Content type UID
30753
+ * @param {string} documentId - Document ID
30754
+ * @param {object} data - Published data
30755
+ */
30756
+ emitPublish(uid, documentId, data) {
30757
+ emitToSubscribers(uid, documentId, "preview:publish", {
30758
+ data,
30759
+ isDraft: false
30760
+ });
30761
+ },
30762
+ /**
30763
+ * Emits unpublish event to preview subscribers
30764
+ * @param {string} uid - Content type UID
30765
+ * @param {string} documentId - Document ID
30766
+ */
30767
+ emitUnpublish(uid, documentId) {
30768
+ emitToSubscribers(uid, documentId, "preview:unpublish", {
30769
+ isDraft: true
30770
+ });
30771
+ },
30772
+ /**
30773
+ * Gets the number of preview subscribers for an entity
30774
+ * @param {string} uid - Content type UID
30775
+ * @param {string} documentId - Document ID
30776
+ * @returns {number} Subscriber count
30777
+ */
30778
+ getSubscriberCount(uid, documentId) {
30779
+ const entityKey = getEntityKey(uid, documentId);
30780
+ return previewSubscribers.get(entityKey)?.size || 0;
30781
+ },
30782
+ /**
30783
+ * Gets all entities with active preview subscribers
30784
+ * @returns {Array} List of entity keys with subscriber counts
30785
+ */
30786
+ getActivePreviewEntities() {
30787
+ const entities = [];
30788
+ for (const [entityKey, subscribers] of previewSubscribers) {
30789
+ const [uid, documentId] = entityKey.split(":");
30790
+ entities.push({
30791
+ uid,
30792
+ documentId,
30793
+ entityKey,
30794
+ subscriberCount: subscribers.size
30795
+ });
30796
+ }
30797
+ return entities;
30798
+ },
30799
+ /**
30800
+ * Checks if live preview is enabled
30801
+ * @returns {boolean} True if enabled
30802
+ */
30803
+ isEnabled() {
30804
+ return getPreviewSettings().enabled;
30805
+ },
30806
+ /**
30807
+ * Gets preview statistics
30808
+ * @returns {object} Preview stats
30809
+ */
30810
+ getStats() {
30811
+ let totalSubscriptions = 0;
30812
+ for (const subscribers of previewSubscribers.values()) {
30813
+ totalSubscriptions += subscribers.size;
30814
+ }
30815
+ return {
30816
+ totalEntitiesWithSubscribers: previewSubscribers.size,
30817
+ totalSubscriptions,
30818
+ entities: this.getActivePreviewEntities()
30819
+ };
30820
+ }
30821
+ };
30822
+ };
30823
+ var diff$1 = ({ strapi: strapi2 }) => {
30824
+ const getDiffSettings = () => {
30825
+ const settings2 = strapi2.$ioSettings || {};
30826
+ return {
30827
+ enabled: settings2.fieldLevelChanges?.enabled ?? true,
30828
+ includeFullData: settings2.fieldLevelChanges?.includeFullData ?? false,
30829
+ excludeFields: settings2.fieldLevelChanges?.excludeFields ?? ["updatedAt", "updatedBy", "createdAt", "createdBy"],
30830
+ maxDiffDepth: settings2.fieldLevelChanges?.maxDiffDepth ?? 3
30831
+ };
30832
+ };
30833
+ const isPlainObject2 = (value) => {
30834
+ return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
30835
+ };
30836
+ const isEqual2 = (a, b) => {
30837
+ if (a === b) return true;
30838
+ if (a === null || b === null) return a === b;
30839
+ if (typeof a !== typeof b) return false;
30840
+ if (a instanceof Date && b instanceof Date) {
30841
+ return a.getTime() === b.getTime();
30842
+ }
30843
+ if (Array.isArray(a) && Array.isArray(b)) {
30844
+ if (a.length !== b.length) return false;
30845
+ return a.every((item, index2) => isEqual2(item, b[index2]));
30846
+ }
30847
+ if (isPlainObject2(a) && isPlainObject2(b)) {
30848
+ const keysA = Object.keys(a);
30849
+ const keysB = Object.keys(b);
30850
+ if (keysA.length !== keysB.length) return false;
30851
+ return keysA.every((key) => isEqual2(a[key], b[key]));
30852
+ }
30853
+ return false;
30854
+ };
30855
+ const safeClone = (value) => {
30856
+ if (value === null || value === void 0) return value;
30857
+ if (value instanceof Date) return value.toISOString();
30858
+ if (Array.isArray(value)) return value.map(safeClone);
30859
+ if (isPlainObject2(value)) {
30860
+ const cloned = {};
30861
+ for (const [key, val] of Object.entries(value)) {
30862
+ cloned[key] = safeClone(val);
30863
+ }
30864
+ return cloned;
30865
+ }
30866
+ return value;
30867
+ };
30868
+ const calculateDiffInternal = (oldData, newData, options = {}, depth2 = 0) => {
30869
+ const { excludeFields = [], maxDiffDepth = 3 } = options;
30870
+ const diff2 = {};
30871
+ if (!oldData || !newData) {
30872
+ return { _replaced: true, old: safeClone(oldData), new: safeClone(newData) };
30873
+ }
30874
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(oldData || {}), ...Object.keys(newData || {})]);
30875
+ for (const key of allKeys) {
30876
+ if (excludeFields.includes(key)) continue;
30877
+ const oldValue = oldData?.[key];
30878
+ const newValue = newData?.[key];
30879
+ if (isEqual2(oldValue, newValue)) continue;
30880
+ if (isPlainObject2(oldValue) && isPlainObject2(newValue) && depth2 < maxDiffDepth) {
30881
+ const nestedDiff = calculateDiffInternal(oldValue, newValue, options, depth2 + 1);
30882
+ if (Object.keys(nestedDiff).length > 0) {
30883
+ diff2[key] = nestedDiff;
30884
+ }
30885
+ } else {
30886
+ diff2[key] = {
30887
+ old: safeClone(oldValue),
30888
+ new: safeClone(newValue)
30889
+ };
30890
+ }
30891
+ }
30892
+ return diff2;
30893
+ };
30894
+ return {
30895
+ /**
30896
+ * Calculates field-level diff between old and new data
30897
+ * @param {object} oldData - Previous data state
30898
+ * @param {object} newData - New data state
30899
+ * @returns {object} Diff result with changed fields and metadata
30900
+ */
30901
+ calculateDiff(oldData, newData) {
30902
+ const settings2 = getDiffSettings();
30903
+ if (!settings2.enabled) {
30904
+ return {
30905
+ enabled: false,
30906
+ hasChanges: !isEqual2(oldData, newData),
30907
+ diff: null,
30908
+ fullData: newData
30909
+ };
30910
+ }
30911
+ const diff2 = calculateDiffInternal(oldData, newData, {
30912
+ excludeFields: settings2.excludeFields,
30913
+ maxDiffDepth: settings2.maxDiffDepth
30914
+ });
30915
+ const changedFields = Object.keys(diff2);
30916
+ const hasChanges = changedFields.length > 0;
30917
+ const result = {
30918
+ enabled: true,
30919
+ hasChanges,
30920
+ changedFields,
30921
+ changedFieldCount: changedFields.length,
30922
+ diff: hasChanges ? diff2 : null,
30923
+ timestamp: Date.now()
30924
+ };
30925
+ if (settings2.includeFullData) {
30926
+ result.fullData = newData;
30927
+ }
30928
+ return result;
30929
+ },
30930
+ /**
30931
+ * Applies a diff to a target object
30932
+ * @param {object} target - Target object to apply diff to
30933
+ * @param {object} diff - Diff to apply
30934
+ * @returns {object} Updated target object
30935
+ */
30936
+ applyDiff(target, diff2) {
30937
+ if (!diff2 || typeof diff2 !== "object") return target;
30938
+ const result = { ...target };
30939
+ for (const [key, change] of Object.entries(diff2)) {
30940
+ if (change._replaced) {
30941
+ result[key] = change.new;
30942
+ } else if (change.old !== void 0 && change.new !== void 0) {
30943
+ result[key] = change.new;
30944
+ } else if (isPlainObject2(change)) {
30945
+ result[key] = this.applyDiff(result[key] || {}, change);
30946
+ }
30947
+ }
30948
+ return result;
30949
+ },
30950
+ /**
30951
+ * Validates if a diff is applicable to a content type
30952
+ * @param {string} uid - Content type UID
30953
+ * @param {object} diff - Diff to validate
30954
+ * @returns {object} Validation result
30955
+ */
30956
+ validateDiff(uid, diff2) {
30957
+ if (!diff2) {
30958
+ return { valid: true, errors: [] };
30959
+ }
30960
+ const contentType = strapi2.contentTypes[uid];
30961
+ if (!contentType) {
30962
+ return { valid: false, errors: [`Content type ${uid} not found`] };
30963
+ }
30964
+ const errors2 = [];
30965
+ const attributes = contentType.attributes || {};
30966
+ for (const field of Object.keys(diff2)) {
30967
+ if (!attributes[field] && field !== "id" && field !== "documentId") {
30968
+ errors2.push(`Field '${field}' does not exist in ${uid}`);
30969
+ }
30970
+ }
30971
+ return {
30972
+ valid: errors2.length === 0,
30973
+ errors: errors2
30974
+ };
30975
+ },
30976
+ /**
30977
+ * Creates an event payload with diff information
30978
+ * @param {string} eventType - Event type (create, update, delete)
30979
+ * @param {object} schema - Content type schema info
30980
+ * @param {object} oldData - Previous data (null for create)
30981
+ * @param {object} newData - New data (null for delete)
30982
+ * @returns {object} Event payload with diff
30983
+ */
30984
+ createEventPayload(eventType, schema2, oldData, newData) {
30985
+ const settings2 = getDiffSettings();
30986
+ if (eventType === "create") {
30987
+ return {
30988
+ event: eventType,
30989
+ schema: { singularName: schema2.singularName, uid: schema2.uid },
30990
+ data: newData,
30991
+ diff: null,
30992
+ timestamp: Date.now()
30993
+ };
30994
+ }
30995
+ if (eventType === "delete") {
30996
+ return {
30997
+ event: eventType,
30998
+ schema: { singularName: schema2.singularName, uid: schema2.uid },
30999
+ data: { id: oldData?.id, documentId: oldData?.documentId },
31000
+ deletedData: settings2.includeFullData ? oldData : null,
31001
+ diff: null,
31002
+ timestamp: Date.now()
31003
+ };
31004
+ }
31005
+ const diffResult = this.calculateDiff(oldData, newData);
31006
+ const payload = {
31007
+ event: eventType,
31008
+ schema: { singularName: schema2.singularName, uid: schema2.uid },
31009
+ documentId: newData?.documentId || newData?.id,
31010
+ diff: diffResult.diff,
31011
+ changedFields: diffResult.changedFields,
31012
+ hasChanges: diffResult.hasChanges,
31013
+ timestamp: Date.now()
31014
+ };
31015
+ if (settings2.includeFullData || !settings2.enabled) {
31016
+ payload.data = newData;
31017
+ }
31018
+ return payload;
31019
+ },
31020
+ /**
31021
+ * Checks if diff feature is enabled
31022
+ * @returns {boolean} True if enabled
31023
+ */
31024
+ isEnabled() {
31025
+ return getDiffSettings().enabled;
31026
+ },
31027
+ /**
31028
+ * Gets current diff settings
31029
+ * @returns {object} Current settings
31030
+ */
31031
+ getSettings() {
31032
+ return getDiffSettings();
31033
+ }
31034
+ };
31035
+ };
29963
31036
  const strategy = strategies;
29964
31037
  const sanitize = sanitize_1;
29965
31038
  const transform = transform$1;
29966
31039
  const settings = settings$1;
29967
31040
  const monitoring = monitoring$1;
31041
+ const presence = presence$1;
31042
+ const preview = preview$1;
31043
+ const diff = diff$1;
29968
31044
  var services$1 = {
29969
31045
  sanitize,
29970
31046
  strategy,
29971
31047
  transform,
29972
31048
  settings,
29973
- monitoring
31049
+ monitoring,
31050
+ presence,
31051
+ preview,
31052
+ diff
29974
31053
  };
29975
31054
  const bootstrap = bootstrap_1;
29976
31055
  const config = config$1;