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