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