@strapi-community/plugin-io 5.3.1 → 5.3.3

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.
@@ -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$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" }) {
86
+ const pluginId$a = pluginPkg.strapi.name;
87
+ var pluginId_1 = { pluginId: pluginId$a };
88
+ const { pluginId: pluginId$9 } = pluginId_1;
89
+ function getService$3({ name, plugin = pluginId$9, type: type2 = "plugin" }) {
90
90
  let serviceUID = `${type2}::${plugin}`;
91
91
  if (name && name.length) {
92
92
  serviceUID += `.${name}`;
@@ -129,7 +129,7 @@ async function handshake$2(socket, next) {
129
129
  room = strategyService["role"].getRoomName(role);
130
130
  }
131
131
  if (room) {
132
- socket.join(room.replace(" ", "-"));
132
+ socket.join(room.replaceAll(" ", "-"));
133
133
  } else {
134
134
  throw new Error("No valid room found");
135
135
  }
@@ -156,7 +156,7 @@ var constants$7 = {
156
156
  const { Server } = require$$0__default.default;
157
157
  const { handshake } = middleware;
158
158
  const { getService: getService$1 } = getService_1;
159
- const { pluginId: pluginId$7 } = pluginId_1;
159
+ const { pluginId: pluginId$8 } = pluginId_1;
160
160
  const { API_TOKEN_TYPE: API_TOKEN_TYPE$1 } = constants$7;
161
161
  let SocketIO$2 = class SocketIO {
162
162
  constructor(options) {
@@ -198,7 +198,7 @@ let SocketIO$2 = class SocketIO {
198
198
  });
199
199
  const roomName = strategy2.getRoomName(room);
200
200
  const data = transformService.response({ data: sanitizedData, schema: schema2 });
201
- this._socket.to(roomName.replace(" ", "-")).emit(eventName, { ...data });
201
+ this._socket.to(roomName.replaceAll(" ", "-")).emit(eventName, { ...data });
202
202
  if (entityRoomName) {
203
203
  this._socket.to(entityRoomName).emit(eventName, { ...data });
204
204
  }
@@ -234,21 +234,25 @@ function requireSanitizeSensitiveFields() {
234
234
  hasRequiredSanitizeSensitiveFields = 1;
235
235
  const SENSITIVE_FIELDS = [
236
236
  "password",
237
+ "passwordHash",
237
238
  "resetPasswordToken",
238
239
  "registrationToken",
239
240
  "confirmationToken",
240
241
  "privateKey",
241
242
  "secretKey",
242
243
  "apiKey",
243
- "secret",
244
- "hash"
244
+ "secret"
245
245
  ];
246
- function deepSanitize(obj) {
246
+ const MAX_SANITIZE_DEPTH = 20;
247
+ function deepSanitize(obj, depth2 = 0) {
247
248
  if (!obj || typeof obj !== "object") {
248
249
  return obj;
249
250
  }
251
+ if (depth2 >= MAX_SANITIZE_DEPTH) {
252
+ return obj;
253
+ }
250
254
  if (Array.isArray(obj)) {
251
- return obj.map((item) => deepSanitize(item));
255
+ return obj.map((item) => deepSanitize(item, depth2 + 1));
252
256
  }
253
257
  const sanitized = {};
254
258
  for (const [key, value] of Object.entries(obj)) {
@@ -256,7 +260,7 @@ function requireSanitizeSensitiveFields() {
256
260
  continue;
257
261
  }
258
262
  if (value && typeof value === "object") {
259
- sanitized[key] = deepSanitize(value);
263
+ sanitized[key] = deepSanitize(value, depth2 + 1);
260
264
  } else {
261
265
  sanitized[key] = value;
262
266
  }
@@ -276,11 +280,57 @@ function requireSanitizeSensitiveFields() {
276
280
  return sanitizeSensitiveFields;
277
281
  }
278
282
  const { SocketIO: SocketIO2 } = structures;
279
- const { pluginId: pluginId$6 } = pluginId_1;
283
+ const { pluginId: pluginId$7 } = pluginId_1;
284
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
285
+ const MAX_FIELD_VALUE_SIZE = 1e5;
286
+ function safeHandler(handler, expectsObject = true) {
287
+ return function(...args) {
288
+ try {
289
+ if (expectsObject) {
290
+ const data = args[0];
291
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
292
+ const callback = typeof args[args.length - 1] === "function" ? args[args.length - 1] : null;
293
+ if (callback) callback({ success: false, error: "Invalid payload" });
294
+ return;
295
+ }
296
+ }
297
+ const result = handler.apply(this, args);
298
+ if (result && typeof result.catch === "function") {
299
+ result.catch((err) => {
300
+ strapi.log.error(`socket.io: Unhandled error in event handler: ${err.message}`);
301
+ const callback = typeof args[args.length - 1] === "function" ? args[args.length - 1] : null;
302
+ if (callback) callback({ success: false, error: "Internal error" });
303
+ });
304
+ }
305
+ } catch (err) {
306
+ strapi.log.error(`socket.io: Error in event handler: ${err.message}`);
307
+ const callback = typeof args[args.length - 1] === "function" ? args[args.length - 1] : null;
308
+ if (callback) callback({ success: false, error: "Internal error" });
309
+ }
310
+ };
311
+ }
312
+ function resolveClientIp(socket) {
313
+ const xff = socket.handshake.headers?.["x-forwarded-for"];
314
+ if (xff) {
315
+ return xff.split(",")[0].trim();
316
+ }
317
+ return socket.handshake.address;
318
+ }
319
+ function redactUrl(url) {
320
+ try {
321
+ const parsed = new URL(url);
322
+ if (parsed.password) {
323
+ parsed.password = "***";
324
+ }
325
+ return parsed.toString();
326
+ } catch {
327
+ return url.replace(/:([^@/]+)@/, ":***@");
328
+ }
329
+ }
280
330
  async function bootstrapIO$1({ strapi: strapi2 }) {
281
- const settingsService = strapi2.plugin(pluginId$6).service("settings");
331
+ const settingsService = strapi2.plugin(pluginId$7).service("settings");
282
332
  const settings2 = await settingsService.getSettings();
283
- const monitoringService = strapi2.plugin(pluginId$6).service("monitoring");
333
+ const monitoringService = strapi2.plugin(pluginId$7).service("monitoring");
284
334
  const serverOptions = {
285
335
  cors: {
286
336
  origin: settings2.cors?.origins || ["http://localhost:3000"],
@@ -292,7 +342,7 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
292
342
  connectTimeout: settings2.connection?.connectionTimeout || 45e3,
293
343
  maxHttpBufferSize: 1e6,
294
344
  transports: ["websocket", "polling"],
295
- allowEIO3: true
345
+ allowEIO3: settings2.connection?.allowEIO3 ?? false
296
346
  };
297
347
  if (settings2.redis?.enabled) {
298
348
  try {
@@ -304,13 +354,15 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
304
354
  const subClient = pubClient.duplicate();
305
355
  await Promise.all([pubClient.connect(), subClient.connect()]);
306
356
  serverOptions.adapter = createAdapter(pubClient, subClient);
307
- strapi2.log.info(`socket.io: Redis adapter enabled (${settings2.redis.url})`);
357
+ serverOptions._redisClients = { pubClient, subClient };
358
+ strapi2.log.info(`socket.io: Redis adapter enabled (${redactUrl(settings2.redis.url)})`);
308
359
  } catch (err) {
309
360
  strapi2.log.error(`socket.io: Redis adapter failed: ${err.message}`);
310
361
  }
311
362
  }
312
363
  const io2 = new SocketIO2(serverOptions);
313
364
  strapi2.$io = io2;
365
+ strapi2.$io._redisClients = serverOptions._redisClients || null;
314
366
  strapi2.$ioSettings = settings2;
315
367
  const sanitizeSensitiveFields2 = requireSanitizeSensitiveFields();
316
368
  sanitizeSensitiveFields2({ strapi: strapi2 });
@@ -336,7 +388,7 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
336
388
  }
337
389
  strapi2.$io.namespaces = namespaces;
338
390
  io2.server.use(async (socket, next) => {
339
- const clientIp = socket.handshake.address;
391
+ const clientIp = resolveClientIp(socket);
340
392
  if (settings2.security?.ipWhitelist?.length > 0) {
341
393
  if (!settings2.security.ipWhitelist.includes(clientIp)) {
342
394
  return next(new Error("IP not whitelisted"));
@@ -349,30 +401,22 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
349
401
  if (currentConnections >= (settings2.connection?.maxConnections || 1e3)) {
350
402
  return next(new Error("Max connections reached"));
351
403
  }
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;
404
+ const token = socket.handshake.auth?.token;
355
405
  if (token) {
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) {
361
- socket.user = {
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
367
- };
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})`);
371
- } else {
372
- strapi2.log.warn(`socket.io: Admin session token invalid or expired`);
373
- }
374
- } catch (err) {
375
- strapi2.log.warn(`socket.io: Admin session verification failed: ${err.message}`);
406
+ const isAdminToken = UUID_REGEX.test(token);
407
+ if (isAdminToken) {
408
+ if (socket.adminUser) {
409
+ const admin2 = socket.adminUser;
410
+ socket.user = {
411
+ id: admin2.id,
412
+ username: `${admin2.firstname || ""} ${admin2.lastname || ""}`.trim() || `Admin ${admin2.id}`,
413
+ email: admin2.email || `admin-${admin2.id}`,
414
+ role: "strapi-super-admin",
415
+ isAdmin: true
416
+ };
417
+ strapi2.log.info(`socket.io: Admin connected - ${socket.user.username} (ID: ${admin2.id})`);
418
+ } else {
419
+ strapi2.log.warn("socket.io: Admin token present but handshake did not authenticate");
376
420
  }
377
421
  } else {
378
422
  try {
@@ -438,13 +482,13 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
438
482
  }
439
483
  });
440
484
  }
441
- const presenceService = strapi2.plugin(pluginId$6).service("presence");
442
- const previewService = strapi2.plugin(pluginId$6).service("preview");
485
+ const presenceService = strapi2.plugin(pluginId$7).service("presence");
486
+ const previewService = strapi2.plugin(pluginId$7).service("preview");
443
487
  if (settings2.presence?.enabled !== false) {
444
488
  presenceService.startCleanupInterval();
445
489
  }
446
490
  io2.server.on("connection", (socket) => {
447
- const clientIp = socket.handshake.address || "unknown";
491
+ const clientIp = resolveClientIp(socket);
448
492
  const username = socket.user?.username || "anonymous";
449
493
  if (settings2.monitoring?.enableConnectionLogging) {
450
494
  strapi2.log.info(`socket.io: Client connected (id: ${socket.id}, user: ${username}, ip: ${clientIp})`);
@@ -490,19 +534,24 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
490
534
  if (callback) callback({ success: true, room: roomName });
491
535
  });
492
536
  socket.on("leave-room", (roomName, callback) => {
493
- if (typeof roomName === "string" && roomName.length > 0) {
494
- socket.leave(roomName);
495
- strapi2.log.debug(`socket.io: Socket ${socket.id} left room: ${roomName}`);
496
- if (callback) callback({ success: true, room: roomName });
497
- } else {
537
+ if (typeof roomName !== "string" || roomName.length === 0) {
498
538
  if (callback) callback({ success: false, error: "Invalid room name" });
539
+ return;
540
+ }
541
+ const sanitizedRoom = roomName.replace(/[^a-zA-Z0-9\-_:]/g, "");
542
+ if (sanitizedRoom !== roomName) {
543
+ if (callback) callback({ success: false, error: "Room name contains invalid characters" });
544
+ return;
499
545
  }
546
+ socket.leave(roomName);
547
+ strapi2.log.debug(`socket.io: Socket ${socket.id} left room: ${roomName}`);
548
+ if (callback) callback({ success: true, room: roomName });
500
549
  });
501
550
  socket.on("get-rooms", (callback) => {
502
551
  const rooms = Array.from(socket.rooms).filter((r) => r !== socket.id);
503
552
  if (callback) callback({ success: true, rooms });
504
553
  });
505
- socket.on("presence:join", async ({ uid, documentId }, callback) => {
554
+ socket.on("presence:join", safeHandler(async ({ uid, documentId }, callback) => {
506
555
  if (settings2.presence?.enabled === false) {
507
556
  if (callback) callback({ success: false, error: "Presence is disabled" });
508
557
  return;
@@ -513,8 +562,8 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
513
562
  }
514
563
  const result = await presenceService.joinEntity(socket.id, uid, documentId);
515
564
  if (callback) callback(result);
516
- });
517
- socket.on("presence:leave", async ({ uid, documentId }, callback) => {
565
+ }));
566
+ socket.on("presence:leave", safeHandler(async ({ uid, documentId }, callback) => {
518
567
  if (settings2.presence?.enabled === false) {
519
568
  if (callback) callback({ success: false, error: "Presence is disabled" });
520
569
  return;
@@ -525,24 +574,26 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
525
574
  }
526
575
  const result = await presenceService.leaveEntity(socket.id, uid, documentId);
527
576
  if (callback) callback(result);
528
- });
577
+ }));
529
578
  socket.on("presence:heartbeat", (callback) => {
530
579
  const result = presenceService.heartbeat(socket.id);
531
580
  if (callback) callback(result);
532
581
  });
533
- socket.on("presence:typing", ({ uid, documentId, fieldName }) => {
582
+ socket.on("presence:typing", safeHandler(({ uid, documentId, fieldName }) => {
534
583
  if (settings2.presence?.enabled === false) return;
584
+ if (!socket.user) return;
585
+ if (typeof fieldName !== "string" || fieldName.length > 200) return;
535
586
  presenceService.broadcastTyping(socket.id, uid, documentId, fieldName);
536
- });
537
- socket.on("presence:check", async ({ uid, documentId }, callback) => {
587
+ }));
588
+ socket.on("presence:check", safeHandler(async ({ uid, documentId }, callback) => {
538
589
  if (settings2.presence?.enabled === false) {
539
590
  if (callback) callback({ success: false, error: "Presence is disabled" });
540
591
  return;
541
592
  }
542
593
  const editors = await presenceService.getEntityEditors(uid, documentId);
543
594
  if (callback) callback({ success: true, editors, isBeingEdited: editors.length > 0 });
544
- });
545
- socket.on("preview:subscribe", async ({ uid, documentId }, callback) => {
595
+ }));
596
+ socket.on("preview:subscribe", safeHandler(async ({ uid, documentId }, callback) => {
546
597
  if (settings2.livePreview?.enabled === false) {
547
598
  if (callback) callback({ success: false, error: "Live preview is disabled" });
548
599
  return;
@@ -551,22 +602,33 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
551
602
  if (callback) callback({ success: false, error: "uid and documentId are required" });
552
603
  return;
553
604
  }
605
+ const userRole = socket.userRole || "public";
606
+ const rolePerms = (settings2.rolePermissions || {})[userRole] || {};
607
+ const ctPerms = rolePerms.contentTypes?.[uid];
608
+ if (!ctPerms && settings2.security?.requireAuthentication) {
609
+ if (callback) callback({ success: false, error: "Permission denied" });
610
+ return;
611
+ }
554
612
  const result = await previewService.subscribe(socket.id, uid, documentId);
555
613
  if (callback) callback(result);
556
- });
557
- socket.on("preview:unsubscribe", ({ uid, documentId }, callback) => {
614
+ }));
615
+ socket.on("preview:unsubscribe", safeHandler(({ uid, documentId }, callback) => {
558
616
  if (!uid || !documentId) {
559
617
  if (callback) callback({ success: false, error: "uid and documentId are required" });
560
618
  return;
561
619
  }
562
620
  const result = previewService.unsubscribe(socket.id, uid, documentId);
563
621
  if (callback) callback(result);
564
- });
565
- socket.on("preview:field-change", ({ uid, documentId, fieldName, value }) => {
622
+ }));
623
+ socket.on("preview:field-change", safeHandler(({ uid, documentId, fieldName, value }) => {
566
624
  if (settings2.livePreview?.enabled === false) return;
625
+ if (!socket.user) return;
626
+ if (typeof fieldName !== "string" || fieldName.length > 200) return;
627
+ const serialized = typeof value === "string" ? value : JSON.stringify(value);
628
+ if (serialized && serialized.length > MAX_FIELD_VALUE_SIZE) return;
567
629
  previewService.emitFieldChange(socket.id, uid, documentId, fieldName, value);
568
- });
569
- socket.on("subscribe-entity", async ({ uid, id }, callback) => {
630
+ }));
631
+ socket.on("subscribe-entity", safeHandler(async ({ uid, id }, callback) => {
570
632
  if (settings2.entitySubscriptions?.enabled === false) {
571
633
  if (callback) callback({ success: false, error: "Entity subscriptions are disabled" });
572
634
  return;
@@ -641,8 +703,8 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
641
703
  }
642
704
  strapi2.log.debug(`socket.io: Socket ${socket.id} subscribed to entity: ${entityRoomName}`);
643
705
  if (callback) callback({ success: true, room: entityRoomName, uid, id });
644
- });
645
- socket.on("unsubscribe-entity", ({ uid, id }, callback) => {
706
+ }));
707
+ socket.on("unsubscribe-entity", safeHandler(({ uid, id }, callback) => {
646
708
  if (settings2.entitySubscriptions?.enabled === false) {
647
709
  if (callback) callback({ success: false, error: "Entity subscriptions are disabled" });
648
710
  return;
@@ -663,9 +725,9 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
663
725
  }
664
726
  strapi2.log.debug(`socket.io: Socket ${socket.id} unsubscribed from entity: ${entityRoomName}`);
665
727
  if (callback) callback({ success: true, room: entityRoomName, uid, id });
666
- });
728
+ }));
667
729
  socket.on("get-entity-subscriptions", (callback) => {
668
- const rooms = Array.from(socket.rooms).filter((r) => r !== socket.id && r.includes(":")).map((room) => {
730
+ const rooms = Array.from(socket.rooms).filter((r) => r !== socket.id && /^(api|plugin)::/.test(r) && !r.startsWith("presence:") && !r.startsWith("preview:")).map((room) => {
669
731
  const lastColonIndex = room.lastIndexOf(":");
670
732
  const uid = room.substring(0, lastColonIndex);
671
733
  const id = room.substring(lastColonIndex + 1);
@@ -673,7 +735,7 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
673
735
  });
674
736
  if (callback) callback({ success: true, subscriptions: rooms });
675
737
  });
676
- socket.on("private-message", ({ to, message }, callback) => {
738
+ socket.on("private-message", safeHandler(({ to, message }, callback) => {
677
739
  if (settings2.rooms?.enablePrivateRooms === false) {
678
740
  strapi2.log.warn(`socket.io: Private messages disabled for socket ${socket.id}`);
679
741
  if (callback) callback({ success: false, error: "Private messages are disabled" });
@@ -701,7 +763,7 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
701
763
  });
702
764
  strapi2.log.debug(`socket.io: Private message from ${socket.id} to ${to}`);
703
765
  if (callback) callback({ success: true });
704
- });
766
+ }));
705
767
  socket.on("disconnect", async (reason) => {
706
768
  if (settings2.monitoring?.enableConnectionLogging) {
707
769
  strapi2.log.info(`socket.io: Client disconnected (id: ${socket.id}, user: ${username}, reason: ${reason})`);
@@ -718,7 +780,7 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
718
780
  previewService.cleanupSocket(socket.id);
719
781
  }
720
782
  try {
721
- const presenceController = strapi2.plugin(pluginId$6).controller("presence");
783
+ const presenceController = strapi2.plugin(pluginId$7).controller("presence");
722
784
  if (presenceController?.unregisterSocket) {
723
785
  presenceController.unregisterSocket(socket.id);
724
786
  }
@@ -986,7 +1048,7 @@ function getTransactionCtx() {
986
1048
  }
987
1049
  return transactionCtx;
988
1050
  }
989
- const { pluginId: pluginId$5 } = pluginId_1;
1051
+ const { pluginId: pluginId$6 } = pluginId_1;
990
1052
  function scheduleAfterTransaction(callback, delay = 0) {
991
1053
  const runner = () => setTimeout(callback, delay);
992
1054
  const ctx = getTransactionCtx();
@@ -1096,8 +1158,8 @@ async function bootstrapLifecycles$1({ strapi: strapi2 }) {
1096
1158
  const modelInfo = { singularName: event.model.singularName, uid: event.model.uid };
1097
1159
  scheduleAfterTransaction(() => {
1098
1160
  try {
1099
- const diffService = strapi2.plugin(pluginId$5).service("diff");
1100
- const previewService = strapi2.plugin(pluginId$5).service("preview");
1161
+ const diffService = strapi2.plugin(pluginId$6).service("diff");
1162
+ const previewService = strapi2.plugin(pluginId$6).service("preview");
1101
1163
  const fieldLevelEnabled = strapi2.$ioSettings?.fieldLevelChanges?.enabled !== false;
1102
1164
  let eventPayload;
1103
1165
  if (fieldLevelEnabled && previousData && diffService) {
@@ -1214,14 +1276,14 @@ var config$1 = {
1214
1276
  validator(config2) {
1215
1277
  }
1216
1278
  };
1217
- const { pluginId: pluginId$4 } = pluginId_1;
1279
+ const { pluginId: pluginId$5 } = pluginId_1;
1218
1280
  var settings$3 = ({ strapi: strapi2 }) => ({
1219
1281
  /**
1220
1282
  * GET /io/settings
1221
1283
  * Retrieve current plugin settings
1222
1284
  */
1223
1285
  async getSettings(ctx) {
1224
- const settingsService = strapi2.plugin(pluginId$4).service("settings");
1286
+ const settingsService = strapi2.plugin(pluginId$5).service("settings");
1225
1287
  const settings2 = await settingsService.getSettings();
1226
1288
  ctx.body = { data: settings2 };
1227
1289
  },
@@ -1230,8 +1292,33 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1230
1292
  * Update plugin settings and hot-reload Socket.IO
1231
1293
  */
1232
1294
  async updateSettings(ctx) {
1233
- const settingsService = strapi2.plugin(pluginId$4).service("settings");
1295
+ const settingsService = strapi2.plugin(pluginId$5).service("settings");
1234
1296
  const { body } = ctx.request;
1297
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
1298
+ return ctx.badRequest("Request body must be a JSON object");
1299
+ }
1300
+ const ALLOWED_KEYS = [
1301
+ "enabled",
1302
+ "cors",
1303
+ "connection",
1304
+ "security",
1305
+ "contentTypes",
1306
+ "events",
1307
+ "rooms",
1308
+ "entitySubscriptions",
1309
+ "rolePermissions",
1310
+ "redis",
1311
+ "namespaces",
1312
+ "middleware",
1313
+ "monitoring",
1314
+ "presence",
1315
+ "livePreview",
1316
+ "fieldLevelChanges"
1317
+ ];
1318
+ const unknownKeys = Object.keys(body).filter((k) => !ALLOWED_KEYS.includes(k));
1319
+ if (unknownKeys.length > 0) {
1320
+ return ctx.badRequest(`Unknown settings keys: ${unknownKeys.join(", ")}`);
1321
+ }
1235
1322
  await settingsService.getSettings();
1236
1323
  const updatedSettings = await settingsService.setSettings(body);
1237
1324
  strapi2.$ioSettings = updatedSettings;
@@ -1263,7 +1350,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1263
1350
  * Get connection and event statistics
1264
1351
  */
1265
1352
  async getStats(ctx) {
1266
- const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
1353
+ const monitoringService = strapi2.plugin(pluginId$5).service("monitoring");
1267
1354
  const connectionStats = monitoringService.getConnectionStats();
1268
1355
  const eventStats = monitoringService.getEventStats();
1269
1356
  ctx.body = {
@@ -1278,7 +1365,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1278
1365
  * Get recent event log
1279
1366
  */
1280
1367
  async getEventLog(ctx) {
1281
- const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
1368
+ const monitoringService = strapi2.plugin(pluginId$5).service("monitoring");
1282
1369
  const limit = parseInt(ctx.query.limit) || 50;
1283
1370
  const log = monitoringService.getEventLog(limit);
1284
1371
  ctx.body = { data: log };
@@ -1288,10 +1375,11 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1288
1375
  * Send a test event
1289
1376
  */
1290
1377
  async sendTestEvent(ctx) {
1291
- const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
1378
+ const monitoringService = strapi2.plugin(pluginId$5).service("monitoring");
1292
1379
  const { eventName, data } = ctx.request.body;
1293
1380
  try {
1294
- const result = monitoringService.sendTestEvent(eventName || "test", data || {});
1381
+ const safeName = (eventName || "test").replace(/[^a-zA-Z0-9:._-]/g, "").substring(0, 50);
1382
+ const result = monitoringService.sendTestEvent(safeName, data || {});
1295
1383
  ctx.body = { data: result };
1296
1384
  } catch (error2) {
1297
1385
  ctx.throw(500, error2.message);
@@ -1302,7 +1390,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1302
1390
  * Reset monitoring statistics
1303
1391
  */
1304
1392
  async resetStats(ctx) {
1305
- const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
1393
+ const monitoringService = strapi2.plugin(pluginId$5).service("monitoring");
1306
1394
  monitoringService.resetStats();
1307
1395
  ctx.body = { data: { success: true } };
1308
1396
  },
@@ -1326,7 +1414,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1326
1414
  * Get lightweight stats for dashboard widget
1327
1415
  */
1328
1416
  async getMonitoringStats(ctx) {
1329
- const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
1417
+ const monitoringService = strapi2.plugin(pluginId$5).service("monitoring");
1330
1418
  const connectionStats = monitoringService.getConnectionStats();
1331
1419
  const eventStats = monitoringService.getEventStats();
1332
1420
  ctx.body = {
@@ -1352,10 +1440,11 @@ const refreshThrottle = /* @__PURE__ */ new Map();
1352
1440
  const SESSION_TTL = 10 * 60 * 1e3;
1353
1441
  const REFRESH_COOLDOWN = 3 * 1e3;
1354
1442
  const CLEANUP_INTERVAL = 2 * 60 * 1e3;
1443
+ let tokenCleanupInterval = null;
1355
1444
  const hashToken = (token) => {
1356
1445
  return createHash("sha256").update(token).digest("hex");
1357
1446
  };
1358
- setInterval(() => {
1447
+ const runTokenCleanup = () => {
1359
1448
  const now = Date.now();
1360
1449
  let cleaned = 0;
1361
1450
  for (const [tokenHash, session] of sessionTokens.entries()) {
@@ -1372,8 +1461,23 @@ setInterval(() => {
1372
1461
  if (cleaned > 0) {
1373
1462
  console.log(`[plugin-io] [CLEANUP] Removed ${cleaned} expired session tokens`);
1374
1463
  }
1375
- }, CLEANUP_INTERVAL);
1464
+ };
1465
+ const startTokenCleanup = () => {
1466
+ if (!tokenCleanupInterval) {
1467
+ tokenCleanupInterval = setInterval(runTokenCleanup, CLEANUP_INTERVAL);
1468
+ }
1469
+ };
1470
+ const stopTokenCleanup = () => {
1471
+ if (tokenCleanupInterval) {
1472
+ clearInterval(tokenCleanupInterval);
1473
+ tokenCleanupInterval = null;
1474
+ }
1475
+ };
1376
1476
  var presence$3 = ({ strapi: strapi2 }) => ({
1477
+ /**
1478
+ * Stops the background token cleanup interval (called on plugin destroy)
1479
+ */
1480
+ stopTokenCleanup,
1377
1481
  /**
1378
1482
  * Creates a session token for admin users to connect to Socket.IO
1379
1483
  * Implements rate limiting and secure token storage
@@ -1392,6 +1496,7 @@ var presence$3 = ({ strapi: strapi2 }) => ({
1392
1496
  strapi2.log.warn(`[plugin-io] Rate limit: User ${adminUser.id} must wait ${waitTime}s`);
1393
1497
  return ctx.tooManyRequests(`Please wait ${waitTime} seconds before requesting a new session`);
1394
1498
  }
1499
+ startTokenCleanup();
1395
1500
  try {
1396
1501
  const token = randomUUID();
1397
1502
  const tokenHash = hashToken(token);
@@ -1492,6 +1597,26 @@ var presence$3 = ({ strapi: strapi2 }) => ({
1492
1597
  strapi2.log.info(`[plugin-io] Invalidated ${invalidated} sessions for user ${userId}`);
1493
1598
  return invalidated;
1494
1599
  },
1600
+ /**
1601
+ * Returns all active (non-expired) admin sessions
1602
+ * Used by admin strategy for broadcasting to connected admin users
1603
+ * @returns {Array} Array of active session objects
1604
+ */
1605
+ getActiveSessions() {
1606
+ const now = Date.now();
1607
+ const activeSessions = [];
1608
+ for (const session of sessionTokens.values()) {
1609
+ if (session.expiresAt > now) {
1610
+ activeSessions.push({
1611
+ userId: session.userId,
1612
+ user: session.user,
1613
+ createdAt: session.createdAt,
1614
+ expiresAt: session.expiresAt
1615
+ });
1616
+ }
1617
+ }
1618
+ return activeSessions;
1619
+ },
1495
1620
  /**
1496
1621
  * Gets session statistics (for monitoring) - internal method
1497
1622
  * @returns {object} Session statistics
@@ -1963,7 +2088,7 @@ lodash_min.exports;
1963
2088
  function Q(n2) {
1964
2089
  return n2.match(Fr) || [];
1965
2090
  }
1966
- var X, nn = "4.17.21", tn = 200, rn = "Unsupported core-js use. Try https://npms.io/search?q=ponyfill.", en = "Expected a function", un = "Invalid `variable` option passed into `_.template`", on = "__lodash_hash_undefined__", fn = 500, cn = "__lodash_placeholder__", an = 1, ln = 2, sn = 4, hn = 1, pn = 2, _n = 1, vn = 2, gn = 4, yn = 8, dn = 16, bn = 32, wn = 64, mn = 128, xn = 256, jn = 512, An = 30, kn = "...", On = 800, In = 16, Rn = 1, zn = 2, En = 3, Sn = 1 / 0, Wn = 9007199254740991, Ln = 17976931348623157e292, Cn = NaN, Un = 4294967295, Bn = Un - 1, Tn = Un >>> 1, $n = [["ary", mn], ["bind", _n], ["bindKey", vn], ["curry", yn], ["curryRight", dn], ["flip", jn], ["partial", bn], ["partialRight", wn], ["rearg", xn]], Dn = "[object Arguments]", Mn = "[object Array]", Fn = "[object AsyncFunction]", Nn = "[object Boolean]", Pn = "[object Date]", qn = "[object DOMException]", Zn = "[object Error]", Kn = "[object Function]", Vn = "[object GeneratorFunction]", Gn = "[object Map]", Hn = "[object Number]", Jn = "[object Null]", Yn = "[object Object]", Qn = "[object Promise]", Xn = "[object Proxy]", nt = "[object RegExp]", tt = "[object Set]", rt = "[object String]", et = "[object Symbol]", ut = "[object Undefined]", it = "[object WeakMap]", ot = "[object WeakSet]", ft = "[object ArrayBuffer]", ct = "[object DataView]", at = "[object Float32Array]", lt = "[object Float64Array]", st = "[object Int8Array]", ht = "[object Int16Array]", pt = "[object Int32Array]", _t = "[object Uint8Array]", vt = "[object Uint8ClampedArray]", gt = "[object Uint16Array]", yt = "[object Uint32Array]", dt = /\b__p \+= '';/g, bt = /\b(__p \+=) '' \+/g, wt = /(__e\(.*?\)|\b__t\)) \+\n'';/g, mt = /&(?:amp|lt|gt|quot|#39);/g, xt = /[&<>"']/g, jt = RegExp(mt.source), At = RegExp(xt.source), kt = /<%-([\s\S]+?)%>/g, Ot = /<%([\s\S]+?)%>/g, It = /<%=([\s\S]+?)%>/g, Rt = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/, zt = /^\w*$/, Et = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g, St = /[\\^$.*+?()[\]{}|]/g, Wt = RegExp(St.source), Lt = /^\s+/, Ct = /\s/, Ut = /\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/, Bt = /\{\n\/\* \[wrapped with (.+)\] \*/, Tt = /,? & /, $t = /[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g, Dt = /[()=,{}\[\]\/\s]/, Mt = /\\(\\)?/g, Ft = /\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g, Nt = /\w*$/, Pt = /^[-+]0x[0-9a-f]+$/i, qt = /^0b[01]+$/i, Zt = /^\[object .+?Constructor\]$/, Kt = /^0o[0-7]+$/i, Vt = /^(?:0|[1-9]\d*)$/, Gt = /[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g, Ht = /($^)/, Jt = /['\n\r\u2028\u2029\\]/g, Yt = "\\ud800-\\udfff", Qt = "\\u0300-\\u036f", Xt = "\\ufe20-\\ufe2f", nr = "\\u20d0-\\u20ff", tr = Qt + Xt + nr, rr = "\\u2700-\\u27bf", er = "a-z\\xdf-\\xf6\\xf8-\\xff", ur = "\\xac\\xb1\\xd7\\xf7", ir = "\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf", or = "\\u2000-\\u206f", fr = " \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000", cr = "A-Z\\xc0-\\xd6\\xd8-\\xde", ar = "\\ufe0e\\ufe0f", lr = ur + ir + or + fr, sr = "['’]", hr = "[" + Yt + "]", pr = "[" + lr + "]", _r = "[" + tr + "]", vr = "\\d+", gr = "[" + rr + "]", yr = "[" + er + "]", dr = "[^" + Yt + lr + vr + rr + er + cr + "]", br = "\\ud83c[\\udffb-\\udfff]", wr = "(?:" + _r + "|" + br + ")", mr = "[^" + Yt + "]", xr = "(?:\\ud83c[\\udde6-\\uddff]){2}", jr = "[\\ud800-\\udbff][\\udc00-\\udfff]", Ar = "[" + cr + "]", kr = "\\u200d", Or = "(?:" + yr + "|" + dr + ")", Ir = "(?:" + Ar + "|" + dr + ")", Rr = "(?:" + sr + "(?:d|ll|m|re|s|t|ve))?", zr = "(?:" + sr + "(?:D|LL|M|RE|S|T|VE))?", Er = wr + "?", Sr = "[" + ar + "]?", Wr = "(?:" + kr + "(?:" + [mr, xr, jr].join("|") + ")" + Sr + Er + ")*", Lr = "\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])", Cr = "\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])", Ur = Sr + Er + Wr, Br = "(?:" + [gr, xr, jr].join("|") + ")" + Ur, Tr = "(?:" + [mr + _r + "?", _r, xr, jr, hr].join("|") + ")", $r = RegExp(sr, "g"), Dr = RegExp(_r, "g"), Mr = RegExp(br + "(?=" + br + ")|" + Tr + Ur, "g"), Fr = RegExp([Ar + "?" + yr + "+" + Rr + "(?=" + [pr, Ar, "$"].join("|") + ")", Ir + "+" + zr + "(?=" + [pr, Ar + Or, "$"].join("|") + ")", Ar + "?" + Or + "+" + Rr, Ar + "+" + zr, Cr, Lr, vr, Br].join("|"), "g"), Nr = RegExp("[" + kr + Yt + tr + ar + "]"), Pr = /[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/, qr = ["Array", "Buffer", "DataView", "Date", "Error", "Float32Array", "Float64Array", "Function", "Int8Array", "Int16Array", "Int32Array", "Map", "Math", "Object", "Promise", "RegExp", "Set", "String", "Symbol", "TypeError", "Uint8Array", "Uint8ClampedArray", "Uint16Array", "Uint32Array", "WeakMap", "_", "clearTimeout", "isFinite", "parseInt", "setTimeout"], Zr = -1, Kr = {};
2091
+ var X, nn = "4.17.23", tn = 200, rn = "Unsupported core-js use. Try https://npms.io/search?q=ponyfill.", en = "Expected a function", un = "Invalid `variable` option passed into `_.template`", on = "__lodash_hash_undefined__", fn = 500, cn = "__lodash_placeholder__", an = 1, ln = 2, sn = 4, hn = 1, pn = 2, _n = 1, vn = 2, gn = 4, yn = 8, dn = 16, bn = 32, wn = 64, mn = 128, xn = 256, jn = 512, An = 30, kn = "...", On = 800, In = 16, Rn = 1, zn = 2, En = 3, Sn = 1 / 0, Wn = 9007199254740991, Ln = 17976931348623157e292, Cn = NaN, Un = 4294967295, Bn = Un - 1, Tn = Un >>> 1, $n = [["ary", mn], ["bind", _n], ["bindKey", vn], ["curry", yn], ["curryRight", dn], ["flip", jn], ["partial", bn], ["partialRight", wn], ["rearg", xn]], Dn = "[object Arguments]", Mn = "[object Array]", Fn = "[object AsyncFunction]", Nn = "[object Boolean]", Pn = "[object Date]", qn = "[object DOMException]", Zn = "[object Error]", Kn = "[object Function]", Vn = "[object GeneratorFunction]", Gn = "[object Map]", Hn = "[object Number]", Jn = "[object Null]", Yn = "[object Object]", Qn = "[object Promise]", Xn = "[object Proxy]", nt = "[object RegExp]", tt = "[object Set]", rt = "[object String]", et = "[object Symbol]", ut = "[object Undefined]", it = "[object WeakMap]", ot = "[object WeakSet]", ft = "[object ArrayBuffer]", ct = "[object DataView]", at = "[object Float32Array]", lt = "[object Float64Array]", st = "[object Int8Array]", ht = "[object Int16Array]", pt = "[object Int32Array]", _t = "[object Uint8Array]", vt = "[object Uint8ClampedArray]", gt = "[object Uint16Array]", yt = "[object Uint32Array]", dt = /\b__p \+= '';/g, bt = /\b(__p \+=) '' \+/g, wt = /(__e\(.*?\)|\b__t\)) \+\n'';/g, mt = /&(?:amp|lt|gt|quot|#39);/g, xt = /[&<>"']/g, jt = RegExp(mt.source), At = RegExp(xt.source), kt = /<%-([\s\S]+?)%>/g, Ot = /<%([\s\S]+?)%>/g, It = /<%=([\s\S]+?)%>/g, Rt = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/, zt = /^\w*$/, Et = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g, St = /[\\^$.*+?()[\]{}|]/g, Wt = RegExp(St.source), Lt = /^\s+/, Ct = /\s/, Ut = /\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/, Bt = /\{\n\/\* \[wrapped with (.+)\] \*/, Tt = /,? & /, $t = /[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g, Dt = /[()=,{}\[\]\/\s]/, Mt = /\\(\\)?/g, Ft = /\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g, Nt = /\w*$/, Pt = /^[-+]0x[0-9a-f]+$/i, qt = /^0b[01]+$/i, Zt = /^\[object .+?Constructor\]$/, Kt = /^0o[0-7]+$/i, Vt = /^(?:0|[1-9]\d*)$/, Gt = /[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g, Ht = /($^)/, Jt = /['\n\r\u2028\u2029\\]/g, Yt = "\\ud800-\\udfff", Qt = "\\u0300-\\u036f", Xt = "\\ufe20-\\ufe2f", nr = "\\u20d0-\\u20ff", tr = Qt + Xt + nr, rr = "\\u2700-\\u27bf", er = "a-z\\xdf-\\xf6\\xf8-\\xff", ur = "\\xac\\xb1\\xd7\\xf7", ir = "\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf", or = "\\u2000-\\u206f", fr = " \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000", cr = "A-Z\\xc0-\\xd6\\xd8-\\xde", ar = "\\ufe0e\\ufe0f", lr = ur + ir + or + fr, sr = "['’]", hr = "[" + Yt + "]", pr = "[" + lr + "]", _r = "[" + tr + "]", vr = "\\d+", gr = "[" + rr + "]", yr = "[" + er + "]", dr = "[^" + Yt + lr + vr + rr + er + cr + "]", br = "\\ud83c[\\udffb-\\udfff]", wr = "(?:" + _r + "|" + br + ")", mr = "[^" + Yt + "]", xr = "(?:\\ud83c[\\udde6-\\uddff]){2}", jr = "[\\ud800-\\udbff][\\udc00-\\udfff]", Ar = "[" + cr + "]", kr = "\\u200d", Or = "(?:" + yr + "|" + dr + ")", Ir = "(?:" + Ar + "|" + dr + ")", Rr = "(?:" + sr + "(?:d|ll|m|re|s|t|ve))?", zr = "(?:" + sr + "(?:D|LL|M|RE|S|T|VE))?", Er = wr + "?", Sr = "[" + ar + "]?", Wr = "(?:" + kr + "(?:" + [mr, xr, jr].join("|") + ")" + Sr + Er + ")*", Lr = "\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])", Cr = "\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])", Ur = Sr + Er + Wr, Br = "(?:" + [gr, xr, jr].join("|") + ")" + Ur, Tr = "(?:" + [mr + _r + "?", _r, xr, jr, hr].join("|") + ")", $r = RegExp(sr, "g"), Dr = RegExp(_r, "g"), Mr = RegExp(br + "(?=" + br + ")|" + Tr + Ur, "g"), Fr = RegExp([Ar + "?" + yr + "+" + Rr + "(?=" + [pr, Ar, "$"].join("|") + ")", Ir + "+" + zr + "(?=" + [pr, Ar + Or, "$"].join("|") + ")", Ar + "?" + Or + "+" + Rr, Ar + "+" + zr, Cr, Lr, vr, Br].join("|"), "g"), Nr = RegExp("[" + kr + Yt + tr + ar + "]"), Pr = /[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/, qr = ["Array", "Buffer", "DataView", "Date", "Error", "Float32Array", "Float64Array", "Function", "Int8Array", "Int16Array", "Int32Array", "Map", "Math", "Object", "Promise", "RegExp", "Set", "String", "Symbol", "TypeError", "Uint8Array", "Uint8ClampedArray", "Uint16Array", "Uint32Array", "WeakMap", "_", "clearTimeout", "isFinite", "parseInt", "setTimeout"], Zr = -1, Kr = {};
1967
2092
  Kr[at] = Kr[lt] = Kr[st] = Kr[ht] = Kr[pt] = Kr[_t] = Kr[vt] = Kr[gt] = Kr[yt] = true, Kr[Dn] = Kr[Mn] = Kr[ft] = Kr[Nn] = Kr[ct] = Kr[Pn] = Kr[Zn] = Kr[Kn] = Kr[Gn] = Kr[Hn] = Kr[Yn] = Kr[nt] = Kr[tt] = Kr[rt] = Kr[it] = false;
1968
2093
  var Vr = {};
1969
2094
  Vr[Dn] = Vr[Mn] = Vr[ft] = Vr[ct] = Vr[Nn] = Vr[Pn] = Vr[at] = Vr[lt] = Vr[st] = Vr[ht] = Vr[pt] = Vr[Gn] = Vr[Hn] = Vr[Yn] = Vr[nt] = Vr[tt] = Vr[rt] = Vr[et] = Vr[_t] = Vr[vt] = Vr[gt] = Vr[yt] = true, Vr[Zn] = Vr[Kn] = Vr[it] = false;
@@ -2816,7 +2941,21 @@ lodash_min.exports;
2816
2941
  return a2;
2817
2942
  }
2818
2943
  function yu(n2, t2) {
2819
- return t2 = ku(t2, n2), n2 = Gi(n2, t2), null == n2 || delete n2[no(jo(t2))];
2944
+ t2 = ku(t2, n2);
2945
+ var r2 = -1, e2 = t2.length;
2946
+ if (!e2) return true;
2947
+ for (var u2 = null == n2 || "object" != typeof n2 && "function" != typeof n2; ++r2 < e2; ) {
2948
+ var i2 = t2[r2];
2949
+ if ("string" == typeof i2) {
2950
+ if ("__proto__" === i2 && !bl.call(n2, "__proto__")) return false;
2951
+ if ("constructor" === i2 && r2 + 1 < e2 && "string" == typeof t2[r2 + 1] && "prototype" === t2[r2 + 1]) {
2952
+ if (u2 && 0 === r2) continue;
2953
+ return false;
2954
+ }
2955
+ }
2956
+ }
2957
+ var o2 = Gi(n2, t2);
2958
+ return null == o2 || delete o2[no(jo(t2))];
2820
2959
  }
2821
2960
  function du(n2, t2, r2, e2) {
2822
2961
  return fu(n2, t2, r2(_e2(n2, t2)), e2);
@@ -5668,7 +5807,7 @@ lodash.exports;
5668
5807
  (function(module2, exports$1) {
5669
5808
  (function() {
5670
5809
  var undefined$1;
5671
- var VERSION = "4.17.21";
5810
+ var VERSION = "4.17.23";
5672
5811
  var LARGE_ARRAY_SIZE2 = 200;
5673
5812
  var CORE_ERROR_TEXT = "Unsupported core-js use. Try https://npms.io/search?q=ponyfill.", FUNC_ERROR_TEXT2 = "Expected a function", INVALID_TEMPL_VAR_ERROR_TEXT = "Invalid `variable` option passed into `_.template`";
5674
5813
  var HASH_UNDEFINED2 = "__lodash_hash_undefined__";
@@ -7596,8 +7735,28 @@ lodash.exports;
7596
7735
  }
7597
7736
  function baseUnset(object2, path2) {
7598
7737
  path2 = castPath2(path2, object2);
7599
- object2 = parent(object2, path2);
7600
- return object2 == null || delete object2[toKey2(last(path2))];
7738
+ var index2 = -1, length = path2.length;
7739
+ if (!length) {
7740
+ return true;
7741
+ }
7742
+ var isRootPrimitive = object2 == null || typeof object2 !== "object" && typeof object2 !== "function";
7743
+ while (++index2 < length) {
7744
+ var key = path2[index2];
7745
+ if (typeof key !== "string") {
7746
+ continue;
7747
+ }
7748
+ if (key === "__proto__" && !hasOwnProperty2.call(object2, "__proto__")) {
7749
+ return false;
7750
+ }
7751
+ if (key === "constructor" && index2 + 1 < length && typeof path2[index2 + 1] === "string" && path2[index2 + 1] === "prototype") {
7752
+ if (isRootPrimitive && index2 === 0) {
7753
+ continue;
7754
+ }
7755
+ return false;
7756
+ }
7757
+ }
7758
+ var obj = parent(object2, path2);
7759
+ return obj == null || delete obj[toKey2(last(path2))];
7601
7760
  }
7602
7761
  function baseUpdate(object2, path2, updater, customizer) {
7603
7762
  return baseSet(object2, path2, updater(baseGet2(object2, path2)), customizer);
@@ -11250,73 +11409,76 @@ function envFn(key, defaultValue) {
11250
11409
  function getKey(key) {
11251
11410
  return process.env[key] ?? "";
11252
11411
  }
11253
- const utils$9 = {
11254
- int(key, defaultValue) {
11255
- if (!___default.has(process.env, key)) {
11256
- return defaultValue;
11257
- }
11258
- return parseInt(getKey(key), 10);
11259
- },
11260
- float(key, defaultValue) {
11261
- if (!___default.has(process.env, key)) {
11262
- return defaultValue;
11263
- }
11264
- return parseFloat(getKey(key));
11265
- },
11266
- bool(key, defaultValue) {
11267
- if (!___default.has(process.env, key)) {
11268
- return defaultValue;
11269
- }
11270
- return getKey(key) === "true";
11271
- },
11272
- json(key, defaultValue) {
11273
- if (!___default.has(process.env, key)) {
11274
- return defaultValue;
11275
- }
11276
- try {
11277
- return JSON.parse(getKey(key));
11278
- } catch (error2) {
11279
- if (error2 instanceof Error) {
11280
- throw new Error(`Invalid json environment variable ${key}: ${error2.message}`);
11281
- }
11282
- throw error2;
11283
- }
11284
- },
11285
- array(key, defaultValue) {
11286
- if (!___default.has(process.env, key)) {
11287
- return defaultValue;
11288
- }
11289
- let value = getKey(key);
11290
- if (value.startsWith("[") && value.endsWith("]")) {
11291
- value = value.substring(1, value.length - 1);
11292
- }
11293
- return value.split(",").map((v) => {
11294
- return ___default.trim(___default.trim(v, " "), '"');
11295
- });
11296
- },
11297
- date(key, defaultValue) {
11298
- if (!___default.has(process.env, key)) {
11299
- return defaultValue;
11300
- }
11301
- return new Date(getKey(key));
11302
- },
11303
- /**
11304
- * Gets a value from env that matches oneOf provided values
11305
- * @param {string} key
11306
- * @param {string[]} expectedValues
11307
- * @param {string|undefined} defaultValue
11308
- * @returns {string|undefined}
11309
- */
11310
- oneOf(key, expectedValues, defaultValue) {
11311
- if (!expectedValues) {
11312
- throw new Error(`env.oneOf requires expectedValues`);
11313
- }
11314
- if (defaultValue && !expectedValues.includes(defaultValue)) {
11315
- throw new Error(`env.oneOf requires defaultValue to be included in expectedValues`);
11412
+ function int$1(key, defaultValue) {
11413
+ if (!___default.has(process.env, key)) {
11414
+ return defaultValue;
11415
+ }
11416
+ return parseInt(getKey(key), 10);
11417
+ }
11418
+ function float$1(key, defaultValue) {
11419
+ if (!___default.has(process.env, key)) {
11420
+ return defaultValue;
11421
+ }
11422
+ return parseFloat(getKey(key));
11423
+ }
11424
+ function bool$1(key, defaultValue) {
11425
+ if (!___default.has(process.env, key)) {
11426
+ return defaultValue;
11427
+ }
11428
+ return getKey(key) === "true";
11429
+ }
11430
+ function json$1(key, defaultValue) {
11431
+ if (!___default.has(process.env, key)) {
11432
+ return defaultValue;
11433
+ }
11434
+ try {
11435
+ return JSON.parse(getKey(key));
11436
+ } catch (error2) {
11437
+ if (error2 instanceof Error) {
11438
+ throw new Error(`Invalid json environment variable ${key}: ${error2.message}`);
11316
11439
  }
11317
- const rawValue = env(key, defaultValue);
11318
- return expectedValues.includes(rawValue) ? rawValue : defaultValue;
11440
+ throw error2;
11441
+ }
11442
+ }
11443
+ function array$1(key, defaultValue) {
11444
+ if (!___default.has(process.env, key)) {
11445
+ return defaultValue;
11446
+ }
11447
+ let value = getKey(key);
11448
+ if (value.startsWith("[") && value.endsWith("]")) {
11449
+ value = value.substring(1, value.length - 1);
11450
+ }
11451
+ return value.split(",").map((v) => {
11452
+ return ___default.trim(___default.trim(v, " "), '"');
11453
+ });
11454
+ }
11455
+ function date$1(key, defaultValue) {
11456
+ if (!___default.has(process.env, key)) {
11457
+ return defaultValue;
11319
11458
  }
11459
+ return new Date(getKey(key));
11460
+ }
11461
+ function oneOf(key, expectedValues, defaultValue) {
11462
+ if (!expectedValues) {
11463
+ throw new Error(`env.oneOf requires expectedValues`);
11464
+ }
11465
+ if (defaultValue && !expectedValues.includes(defaultValue)) {
11466
+ throw new Error(`env.oneOf requires defaultValue to be included in expectedValues`);
11467
+ }
11468
+ const rawValue = env(key, defaultValue);
11469
+ if (rawValue !== void 0 && expectedValues.includes(rawValue)) {
11470
+ return rawValue;
11471
+ }
11472
+ return defaultValue;
11473
+ }
11474
+ const utils$9 = {
11475
+ int: int$1,
11476
+ float: float$1,
11477
+ bool: bool$1,
11478
+ json: json$1,
11479
+ array: array$1,
11480
+ date: date$1,
11481
+ oneOf
11320
11482
  };
11321
11483
  const env = Object.assign(envFn, utils$9);
11322
11484
  const SINGLE_TYPE = "singleType";
@@ -11341,6 +11503,22 @@ const constants$6 = {
11341
11503
  SINGLE_TYPE,
11342
11504
  COLLECTION_TYPE
11343
11505
  };
11506
+ const ID_FIELDS = [
11507
+ ID_ATTRIBUTE$4,
11508
+ DOC_ID_ATTRIBUTE$4
11509
+ ];
11510
+ const MORPH_TO_KEYS = [
11511
+ "__type"
11512
+ ];
11513
+ const DYNAMIC_ZONE_KEYS = [
11514
+ "__component"
11515
+ ];
11516
+ const RELATION_OPERATION_KEYS = [
11517
+ "connect",
11518
+ "disconnect",
11519
+ "set",
11520
+ "options"
11521
+ ];
11344
11522
  const getTimestamps = (model) => {
11345
11523
  const attributes = [];
11346
11524
  if (fp.has(CREATED_AT_ATTRIBUTE, model.attributes)) {
@@ -11444,10 +11622,10 @@ const HAS_RELATION_REORDERING = [
11444
11622
  "oneToMany"
11445
11623
  ];
11446
11624
  const hasRelationReordering = (attribute) => isRelationalAttribute(attribute) && HAS_RELATION_REORDERING.includes(attribute.relation);
11447
- const isComponentAttribute = (attribute) => [
11625
+ const isComponentAttribute = (attribute) => !!attribute && [
11448
11626
  "component",
11449
11627
  "dynamiczone"
11450
- ].includes(attribute?.type);
11628
+ ].includes(attribute.type);
11451
11629
  const isDynamicZoneAttribute = (attribute) => !!attribute && attribute.type === "dynamiczone";
11452
11630
  const isMorphToRelationalAttribute = (attribute) => {
11453
11631
  return !!attribute && isRelationalAttribute(attribute) && attribute.relation?.startsWith?.("morphTo");
@@ -11484,6 +11662,10 @@ const getContentTypeRoutePrefix = (contentType) => {
11484
11662
  };
11485
11663
  const contentTypes$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
11486
11664
  __proto__: null,
11665
+ DYNAMIC_ZONE_KEYS,
11666
+ ID_FIELDS,
11667
+ MORPH_TO_KEYS,
11668
+ RELATION_OPERATION_KEYS,
11487
11669
  constants: constants$6,
11488
11670
  getComponentAttributes,
11489
11671
  getContentTypeRoutePrefix,
@@ -11669,12 +11851,22 @@ const providerFactory = (options = {}) => {
11669
11851
  }
11670
11852
  };
11671
11853
  };
11854
+ const parallelWithOrderedErrors = async (promises) => {
11855
+ const results = await Promise.allSettled(promises);
11856
+ for (let i = 0; i < results.length; i += 1) {
11857
+ const result = results[i];
11858
+ if (result.status === "rejected") {
11859
+ throw result.reason;
11860
+ }
11861
+ }
11862
+ return results.map((r) => r.value);
11863
+ };
11672
11864
  const traverseEntity = async (visitor2, options, entity) => {
11673
11865
  const { path: path2 = {
11674
11866
  raw: null,
11675
11867
  attribute: null,
11676
11868
  rawWithIndices: null
11677
- }, schema: schema2, getModel } = options;
11869
+ }, schema: schema2, getModel, allowedExtraRootKeys } = options;
11678
11870
  let parent = options.parent;
11679
11871
  const traverseMorphRelationTarget = async (visitor3, path3, entry) => {
11680
11872
  const targetSchema = getModel(entry.__type);
@@ -11682,7 +11874,8 @@ const traverseEntity = async (visitor2, options, entity) => {
11682
11874
  schema: targetSchema,
11683
11875
  path: path3,
11684
11876
  getModel,
11685
- parent
11877
+ parent,
11878
+ allowedExtraRootKeys
11686
11879
  };
11687
11880
  return traverseEntity(visitor3, traverseOptions, entry);
11688
11881
  };
@@ -11691,7 +11884,8 @@ const traverseEntity = async (visitor2, options, entity) => {
11691
11884
  schema: schema3,
11692
11885
  path: path3,
11693
11886
  getModel,
11694
- parent
11887
+ parent,
11888
+ allowedExtraRootKeys
11695
11889
  };
11696
11890
  return traverseEntity(visitor3, traverseOptions, entry);
11697
11891
  };
@@ -11702,7 +11896,8 @@ const traverseEntity = async (visitor2, options, entity) => {
11702
11896
  schema: targetSchema,
11703
11897
  path: path3,
11704
11898
  getModel,
11705
- parent
11899
+ parent,
11900
+ allowedExtraRootKeys
11706
11901
  };
11707
11902
  return traverseEntity(visitor3, traverseOptions, entry);
11708
11903
  };
@@ -11711,7 +11906,8 @@ const traverseEntity = async (visitor2, options, entity) => {
11711
11906
  schema: schema3,
11712
11907
  path: path3,
11713
11908
  getModel,
11714
- parent
11909
+ parent,
11910
+ allowedExtraRootKeys
11715
11911
  };
11716
11912
  return traverseEntity(visitor3, traverseOptions, entry);
11717
11913
  };
@@ -11721,7 +11917,8 @@ const traverseEntity = async (visitor2, options, entity) => {
11721
11917
  schema: targetSchema,
11722
11918
  path: path3,
11723
11919
  getModel,
11724
- parent
11920
+ parent,
11921
+ allowedExtraRootKeys
11725
11922
  };
11726
11923
  return traverseEntity(visitor3, traverseOptions, entry);
11727
11924
  };
@@ -11752,7 +11949,8 @@ const traverseEntity = async (visitor2, options, entity) => {
11752
11949
  attribute,
11753
11950
  path: newPath,
11754
11951
  getModel,
11755
- parent
11952
+ parent,
11953
+ allowedExtraRootKeys
11756
11954
  };
11757
11955
  await visitor2(visitorOptions, visitorUtils);
11758
11956
  const value = copy[key];
@@ -11769,15 +11967,13 @@ const traverseEntity = async (visitor2, options, entity) => {
11769
11967
  const isMorphRelation = attribute.relation.toLowerCase().startsWith("morph");
11770
11968
  const method = isMorphRelation ? traverseMorphRelationTarget : traverseRelationTarget(getModel(attribute.target));
11771
11969
  if (fp.isArray(value)) {
11772
- const res = new Array(value.length);
11773
- for (let i2 = 0; i2 < value.length; i2 += 1) {
11970
+ copy[key] = await parallelWithOrderedErrors(value.map((item, i2) => {
11774
11971
  const arrayPath = {
11775
11972
  ...newPath,
11776
11973
  rawWithIndices: fp.isNil(newPath.rawWithIndices) ? `${i2}` : `${newPath.rawWithIndices}.${i2}`
11777
11974
  };
11778
- res[i2] = await method(visitor2, arrayPath, value[i2]);
11779
- }
11780
- copy[key] = res;
11975
+ return method(visitor2, arrayPath, item);
11976
+ }));
11781
11977
  } else {
11782
11978
  copy[key] = await method(visitor2, newPath, value);
11783
11979
  }
@@ -11791,15 +11987,13 @@ const traverseEntity = async (visitor2, options, entity) => {
11791
11987
  path: newPath
11792
11988
  };
11793
11989
  if (fp.isArray(value)) {
11794
- const res = new Array(value.length);
11795
- for (let i2 = 0; i2 < value.length; i2 += 1) {
11990
+ copy[key] = await parallelWithOrderedErrors(value.map((item, i2) => {
11796
11991
  const arrayPath = {
11797
11992
  ...newPath,
11798
11993
  rawWithIndices: fp.isNil(newPath.rawWithIndices) ? `${i2}` : `${newPath.rawWithIndices}.${i2}`
11799
11994
  };
11800
- res[i2] = await traverseMediaTarget(visitor2, arrayPath, value[i2]);
11801
- }
11802
- copy[key] = res;
11995
+ return traverseMediaTarget(visitor2, arrayPath, item);
11996
+ }));
11803
11997
  } else {
11804
11998
  copy[key] = await traverseMediaTarget(visitor2, newPath, value);
11805
11999
  }
@@ -11814,15 +12008,13 @@ const traverseEntity = async (visitor2, options, entity) => {
11814
12008
  };
11815
12009
  const targetSchema = getModel(attribute.component);
11816
12010
  if (fp.isArray(value)) {
11817
- const res = new Array(value.length);
11818
- for (let i2 = 0; i2 < value.length; i2 += 1) {
12011
+ copy[key] = await parallelWithOrderedErrors(value.map((item, i2) => {
11819
12012
  const arrayPath = {
11820
12013
  ...newPath,
11821
12014
  rawWithIndices: fp.isNil(newPath.rawWithIndices) ? `${i2}` : `${newPath.rawWithIndices}.${i2}`
11822
12015
  };
11823
- res[i2] = await traverseComponent(visitor2, arrayPath, targetSchema, value[i2]);
11824
- }
11825
- copy[key] = res;
12016
+ return traverseComponent(visitor2, arrayPath, targetSchema, item);
12017
+ }));
11826
12018
  } else {
11827
12019
  copy[key] = await traverseComponent(visitor2, newPath, targetSchema, value);
11828
12020
  }
@@ -11835,15 +12027,13 @@ const traverseEntity = async (visitor2, options, entity) => {
11835
12027
  attribute,
11836
12028
  path: newPath
11837
12029
  };
11838
- const res = new Array(value.length);
11839
- for (let i2 = 0; i2 < value.length; i2 += 1) {
12030
+ copy[key] = await parallelWithOrderedErrors(value.map((item, i2) => {
11840
12031
  const arrayPath = {
11841
12032
  ...newPath,
11842
12033
  rawWithIndices: fp.isNil(newPath.rawWithIndices) ? `${i2}` : `${newPath.rawWithIndices}.${i2}`
11843
12034
  };
11844
- res[i2] = await visitDynamicZoneEntry(visitor2, arrayPath, value[i2]);
11845
- }
11846
- copy[key] = res;
12035
+ return visitDynamicZoneEntry(visitor2, arrayPath, item);
12036
+ }));
11847
12037
  continue;
11848
12038
  }
11849
12039
  }
@@ -12589,6 +12779,23 @@ const generateInstallId = (projectId, installId) => {
12589
12779
  return require$$1__default.default.randomUUID();
12590
12780
  }
12591
12781
  };
12782
+ const createModelCache = (getModelFn) => {
12783
+ const cache = /* @__PURE__ */ new Map();
12784
+ return {
12785
+ getModel(uid) {
12786
+ const cached = cache.get(uid);
12787
+ if (cached) {
12788
+ return cached;
12789
+ }
12790
+ const model = getModelFn(uid);
12791
+ cache.set(uid, model);
12792
+ return model;
12793
+ },
12794
+ clear() {
12795
+ cache.clear();
12796
+ }
12797
+ };
12798
+ };
12592
12799
  var map$2;
12593
12800
  try {
12594
12801
  map$2 = Map;
@@ -14088,27 +14295,11 @@ Cache.prototype.set = function(key, value) {
14088
14295
  return this._values[key] = value;
14089
14296
  };
14090
14297
  var SPLIT_REGEX = /[^.^\]^[]+|(?=\[\]|\.\.)/g, DIGIT_REGEX = /^\d+$/, LEAD_DIGIT_REGEX = /^\d/, SPEC_CHAR_REGEX = /[~`!#$%\^&*+=\-\[\]\\';,/{}|\\":<>\?]/g, CLEAN_QUOTES_REGEX = /^\s*(['"]?)(.*?)(\1)\s*$/, MAX_CACHE_SIZE = 512;
14091
- var pathCache = new Cache(MAX_CACHE_SIZE), setCache = new Cache(MAX_CACHE_SIZE), getCache = new Cache(MAX_CACHE_SIZE);
14298
+ var pathCache = new Cache(MAX_CACHE_SIZE);
14299
+ new Cache(MAX_CACHE_SIZE);
14300
+ var getCache = new Cache(MAX_CACHE_SIZE);
14092
14301
  var propertyExpr = {
14093
- Cache,
14094
14302
  split,
14095
- normalizePath,
14096
- setter: function(path2) {
14097
- var parts = normalizePath(path2);
14098
- return setCache.get(path2) || setCache.set(path2, function setter(obj, value) {
14099
- var index2 = 0;
14100
- var len = parts.length;
14101
- var data = obj;
14102
- while (index2 < len - 1) {
14103
- var part = parts[index2];
14104
- if (part === "__proto__" || part === "constructor" || part === "prototype") {
14105
- return obj;
14106
- }
14107
- data = data[parts[index2++]];
14108
- }
14109
- data[parts[index2]] = value;
14110
- });
14111
- },
14112
14303
  getter: function(path2, safe) {
14113
14304
  var parts = normalizePath(path2);
14114
14305
  return getCache.get(path2) || getCache.set(path2, function getter(data) {
@@ -14120,11 +14311,6 @@ var propertyExpr = {
14120
14311
  return data;
14121
14312
  });
14122
14313
  },
14123
- join: function(segments) {
14124
- return segments.reduce(function(path2, part) {
14125
- return path2 + (isQuoted(part) || DIGIT_REGEX.test(part) ? "[" + part + "]" : (path2 ? "." : "") + part);
14126
- }, "");
14127
- },
14128
14314
  forEach: function(path2, cb, thisArg) {
14129
14315
  forEach(Array.isArray(path2) ? path2 : split(path2), cb, thisArg);
14130
14316
  }
@@ -16954,10 +17140,10 @@ const createTransformer = ({ getModel }) => {
16954
17140
  }
16955
17141
  return pageVal;
16956
17142
  };
16957
- const convertPageSizeQueryParams = (pageSize, page) => {
17143
+ const convertPageSizeQueryParams = (pageSize, _page) => {
16958
17144
  const pageSizeVal = fp.toNumber(pageSize);
16959
17145
  if (!fp.isInteger(pageSizeVal) || pageSizeVal <= 0) {
16960
- throw new PaginationError(`Invalid 'pageSize' parameter. Expected an integer > 0, received: ${page}`);
17146
+ throw new PaginationError(`Invalid 'pageSize' parameter. Expected an integer > 0, received: ${pageSize}`);
16961
17147
  }
16962
17148
  return pageSizeVal;
16963
17149
  };
@@ -17132,7 +17318,7 @@ const createTransformer = ({ getModel }) => {
17132
17318
  query.page = convertPageQueryParams(page);
17133
17319
  }
17134
17320
  if (!fp.isNil(pageSize)) {
17135
- query.pageSize = convertPageSizeQueryParams(pageSize, page);
17321
+ query.pageSize = convertPageSizeQueryParams(pageSize);
17136
17322
  }
17137
17323
  if (!fp.isNil(start)) {
17138
17324
  query.offset = convertStartQueryParams(start);
@@ -17280,7 +17466,7 @@ const createTransformer = ({ getModel }) => {
17280
17466
  query.page = convertPageQueryParams(page);
17281
17467
  }
17282
17468
  if (!fp.isNil(pageSize)) {
17283
- query.pageSize = convertPageSizeQueryParams(pageSize, page);
17469
+ query.pageSize = convertPageSizeQueryParams(pageSize);
17284
17470
  }
17285
17471
  if (!fp.isNil(start)) {
17286
17472
  query.offset = convertStartQueryParams(start);
@@ -17307,6 +17493,43 @@ const convertQueryParams = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.
17307
17493
  __proto__: null,
17308
17494
  createTransformer
17309
17495
  }, Symbol.toStringTag, { value: "Module" }));
17496
+ const SHARED_QUERY_PARAM_KEYS = [
17497
+ "filters",
17498
+ "sort",
17499
+ "fields",
17500
+ "populate",
17501
+ "status",
17502
+ "locale",
17503
+ "page",
17504
+ "pageSize",
17505
+ "start",
17506
+ "limit",
17507
+ "_q",
17508
+ "hasPublishedVersion"
17509
+ ];
17510
+ const ALLOWED_QUERY_PARAM_KEYS = [
17511
+ ...SHARED_QUERY_PARAM_KEYS,
17512
+ "pagination",
17513
+ "count",
17514
+ "ordering"
17515
+ ];
17516
+ const RESERVED_INPUT_PARAM_KEYS = [
17517
+ constants$6.ID_ATTRIBUTE,
17518
+ constants$6.DOC_ID_ATTRIBUTE
17519
+ ];
17520
+ function getExtraQueryKeysFromRoute(route) {
17521
+ if (!route?.request?.query) return [];
17522
+ const coreKeys = new Set(ALLOWED_QUERY_PARAM_KEYS);
17523
+ return Object.keys(route.request.query).filter((key) => !coreKeys.has(key));
17524
+ }
17525
+ function getExtraRootKeysFromRouteBody(route) {
17526
+ const bodySchema = route?.request?.body?.["application/json"];
17527
+ if (!bodySchema || typeof bodySchema !== "object") return [];
17528
+ if ("shape" in bodySchema && typeof bodySchema.shape === "object") {
17529
+ return Object.keys(bodySchema.shape);
17530
+ }
17531
+ return [];
17532
+ }
17310
17533
  var indentString$2 = (string2, count = 1, options) => {
17311
17534
  options = {
17312
17535
  indent: " ",
@@ -17694,6 +17917,35 @@ var removeRestrictedFields = (restrictedFields = null) => ({ key, path: { attrib
17694
17917
  remove(key);
17695
17918
  }
17696
17919
  };
17920
+ const removeUnrecognizedFields = ({ key, attribute, path: path2, schema: schema2, parent, allowedExtraRootKeys }, { remove }) => {
17921
+ if (attribute) {
17922
+ return;
17923
+ }
17924
+ if (path2.attribute === null) {
17925
+ if (ID_FIELDS.includes(key)) {
17926
+ return;
17927
+ }
17928
+ if (allowedExtraRootKeys?.includes(key)) {
17929
+ return;
17930
+ }
17931
+ remove(key);
17932
+ return;
17933
+ }
17934
+ if (isMorphToRelationalAttribute(parent?.attribute) && MORPH_TO_KEYS.includes(key)) {
17935
+ return;
17936
+ }
17937
+ if (isComponentSchema(schema2) && isDynamicZoneAttribute(parent?.attribute) && DYNAMIC_ZONE_KEYS.includes(key)) {
17938
+ return;
17939
+ }
17940
+ if ((isRelationalAttribute(parent?.attribute) || isMediaAttribute(parent?.attribute)) && RELATION_OPERATION_KEYS.includes(key)) {
17941
+ return;
17942
+ }
17943
+ const canUseID = isRelationalAttribute(parent?.attribute) || isMediaAttribute(parent?.attribute) || isComponentAttribute(parent?.attribute);
17944
+ if (canUseID && ID_FIELDS.includes(key)) {
17945
+ return;
17946
+ }
17947
+ remove(key);
17948
+ };
17697
17949
  const visitor$4 = ({ schema: schema2, key, value }, { set: set2 }) => {
17698
17950
  if (key === "" && value === "*") {
17699
17951
  const { attributes } = schema2;
@@ -17718,7 +17970,8 @@ const index$5 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.definePrope
17718
17970
  removePassword: visitor$8,
17719
17971
  removePrivate: visitor$7,
17720
17972
  removeRestrictedFields,
17721
- removeRestrictedRelations
17973
+ removeRestrictedRelations,
17974
+ removeUnrecognizedFields
17722
17975
  }, Symbol.toStringTag, { value: "Module" }));
17723
17976
  const DEFAULT_PATH = {
17724
17977
  raw: null,
@@ -18569,15 +18822,18 @@ const sanitizers = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.definePr
18569
18822
  }, Symbol.toStringTag, { value: "Module" }));
18570
18823
  const createAPISanitizers = (opts) => {
18571
18824
  const { getModel } = opts;
18572
- const sanitizeInput = (data, schema2, { auth } = {}) => {
18825
+ const sanitizeInput = (data, schema2, { auth, strictParams = false, route } = {}) => {
18573
18826
  if (!schema2) {
18574
18827
  throw new Error("Missing schema in sanitizeInput");
18575
18828
  }
18576
18829
  if (fp.isArray(data)) {
18577
18830
  return Promise.all(data.map((entry) => sanitizeInput(entry, schema2, {
18578
- auth
18831
+ auth,
18832
+ strictParams,
18833
+ route
18579
18834
  })));
18580
18835
  }
18836
+ const allowedExtraRootKeys = getExtraRootKeysFromRouteBody(route);
18581
18837
  const nonWritableAttributes = getNonWritableAttributes(schema2);
18582
18838
  const transforms = [
18583
18839
  // Remove first level ID in inputs
@@ -18589,6 +18845,13 @@ const createAPISanitizers = (opts) => {
18589
18845
  getModel
18590
18846
  })
18591
18847
  ];
18848
+ if (strictParams) {
18849
+ transforms.push(traverseEntity$1(removeUnrecognizedFields, {
18850
+ schema: schema2,
18851
+ getModel,
18852
+ allowedExtraRootKeys
18853
+ }));
18854
+ }
18592
18855
  if (auth) {
18593
18856
  transforms.push(traverseEntity$1(removeRestrictedRelations(auth), {
18594
18857
  schema: schema2,
@@ -18596,6 +18859,28 @@ const createAPISanitizers = (opts) => {
18596
18859
  }));
18597
18860
  }
18598
18861
  opts?.sanitizers?.input?.forEach((sanitizer) => transforms.push(sanitizer(schema2)));
18862
+ const routeBodySanitizeTransform = async (data2) => {
18863
+ if (!data2 || typeof data2 !== "object" || Array.isArray(data2)) return data2;
18864
+ const obj = data2;
18865
+ const bodySchema = route?.request?.body?.["application/json"];
18866
+ if (bodySchema && typeof bodySchema === "object" && "shape" in bodySchema) {
18867
+ const shape = bodySchema.shape;
18868
+ for (const key of Object.keys(shape)) {
18869
+ if (key === "data" || !(key in obj)) continue;
18870
+ const zodSchema = shape[key];
18871
+ if (zodSchema && typeof zodSchema.safeParse === "function") {
18872
+ const result = zodSchema.safeParse(obj[key]);
18873
+ if (result.success) {
18874
+ obj[key] = result.data;
18875
+ } else {
18876
+ delete obj[key];
18877
+ }
18878
+ }
18879
+ }
18880
+ }
18881
+ return data2;
18882
+ };
18883
+ transforms.push(routeBodySanitizeTransform);
18599
18884
  return pipe$1(...transforms)(data);
18600
18885
  };
18601
18886
  const sanitizeOutput = async (data, schema2, { auth } = {}) => {
@@ -18626,7 +18911,7 @@ const createAPISanitizers = (opts) => {
18626
18911
  opts?.sanitizers?.output?.forEach((sanitizer) => transforms.push(sanitizer(schema2)));
18627
18912
  return pipe$1(...transforms)(data);
18628
18913
  };
18629
- const sanitizeQuery = async (query, schema2, { auth } = {}) => {
18914
+ const sanitizeQuery = async (query, schema2, { auth, strictParams = false, route } = {}) => {
18630
18915
  if (!schema2) {
18631
18916
  throw new Error("Missing schema in sanitizeQuery");
18632
18917
  }
@@ -18656,6 +18941,30 @@ const createAPISanitizers = (opts) => {
18656
18941
  populate: await sanitizePopulate(populate2, schema2)
18657
18942
  });
18658
18943
  }
18944
+ const extraQueryKeys = getExtraQueryKeysFromRoute(route);
18945
+ const routeQuerySchema = route?.request?.query;
18946
+ if (routeQuerySchema) {
18947
+ for (const key of extraQueryKeys) {
18948
+ if (key in query) {
18949
+ const zodSchema = routeQuerySchema[key];
18950
+ if (zodSchema && typeof zodSchema.safeParse === "function") {
18951
+ const result = zodSchema.safeParse(query[key]);
18952
+ if (result.success) {
18953
+ sanitizedQuery[key] = result.data;
18954
+ } else {
18955
+ delete sanitizedQuery[key];
18956
+ }
18957
+ }
18958
+ }
18959
+ }
18960
+ }
18961
+ if (strictParams) {
18962
+ const allowedKeys = [
18963
+ ...ALLOWED_QUERY_PARAM_KEYS,
18964
+ ...extraQueryKeys
18965
+ ];
18966
+ return fp.pick(allowedKeys, sanitizedQuery);
18967
+ }
18659
18968
  return sanitizedQuery;
18660
18969
  };
18661
18970
  const sanitizeFilters = (filters2, schema2, { auth } = {}) => {
@@ -18958,54 +19267,38 @@ var throwRestrictedFields = (restrictedFields = null) => ({ key, path: { attribu
18958
19267
  });
18959
19268
  }
18960
19269
  };
18961
- const ID_FIELDS = [
18962
- constants$6.DOC_ID_ATTRIBUTE,
18963
- constants$6.DOC_ID_ATTRIBUTE
18964
- ];
18965
- const ALLOWED_ROOT_LEVEL_FIELDS = [
18966
- ...ID_FIELDS
18967
- ];
18968
- const MORPH_TO_ALLOWED_FIELDS = [
18969
- "__type"
18970
- ];
18971
- const DYNAMIC_ZONE_ALLOWED_FIELDS = [
18972
- "__component"
18973
- ];
18974
- const RELATION_REORDERING_FIELDS = [
18975
- "connect",
18976
- "disconnect",
18977
- "set",
18978
- "options"
18979
- ];
18980
- const throwUnrecognizedFields = ({ key, attribute, path: path2, schema: schema2, parent }) => {
19270
+ const throwUnrecognizedFields = ({ key, attribute, path: path2, schema: schema2, parent, allowedExtraRootKeys }, _visitorUtils) => {
18981
19271
  if (attribute) {
18982
19272
  return;
18983
19273
  }
18984
19274
  if (path2.attribute === null) {
18985
- if (ALLOWED_ROOT_LEVEL_FIELDS.includes(key)) {
19275
+ if (ID_FIELDS.includes(key)) {
19276
+ return;
19277
+ }
19278
+ if (allowedExtraRootKeys?.includes(key)) {
18986
19279
  return;
18987
19280
  }
18988
19281
  return throwInvalidKey({
18989
19282
  key,
18990
- path: attribute
19283
+ path: path2.attribute
18991
19284
  });
18992
19285
  }
18993
- if (isMorphToRelationalAttribute(parent?.attribute) && MORPH_TO_ALLOWED_FIELDS.includes(key)) {
19286
+ if (isMorphToRelationalAttribute(parent?.attribute) && MORPH_TO_KEYS.includes(key)) {
18994
19287
  return;
18995
19288
  }
18996
- if (isComponentSchema(schema2) && isDynamicZoneAttribute(parent?.attribute) && DYNAMIC_ZONE_ALLOWED_FIELDS.includes(key)) {
19289
+ if (isComponentSchema(schema2) && isDynamicZoneAttribute(parent?.attribute) && DYNAMIC_ZONE_KEYS.includes(key)) {
18997
19290
  return;
18998
19291
  }
18999
- if (hasRelationReordering(parent?.attribute) && RELATION_REORDERING_FIELDS.includes(key)) {
19292
+ if ((isRelationalAttribute(parent?.attribute) || isMediaAttribute(parent?.attribute)) && RELATION_OPERATION_KEYS.includes(key)) {
19000
19293
  return;
19001
19294
  }
19002
- const canUseID = isRelationalAttribute(parent?.attribute) || isMediaAttribute(parent?.attribute);
19003
- if (canUseID && !ID_FIELDS.includes(key)) {
19295
+ const canUseID = isRelationalAttribute(parent?.attribute) || isMediaAttribute(parent?.attribute) || isComponentAttribute(parent?.attribute);
19296
+ if (canUseID && ID_FIELDS.includes(key)) {
19004
19297
  return;
19005
19298
  }
19006
19299
  throwInvalidKey({
19007
19300
  key,
19008
- path: attribute
19301
+ path: path2.attribute
19009
19302
  });
19010
19303
  };
19011
19304
  const index$3 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
@@ -19327,16 +19620,16 @@ const validators = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.definePr
19327
19620
  const { ID_ATTRIBUTE, DOC_ID_ATTRIBUTE } = constants$6;
19328
19621
  const createAPIValidators = (opts) => {
19329
19622
  const { getModel } = opts || {};
19330
- const validateInput = async (data, schema2, { auth } = {}) => {
19623
+ const validateInput = async (data, schema2, options = {}) => {
19624
+ const { auth, route } = options;
19331
19625
  if (!schema2) {
19332
19626
  throw new Error("Missing schema in validateInput");
19333
19627
  }
19334
19628
  if (fp.isArray(data)) {
19335
- await Promise.all(data.map((entry) => validateInput(entry, schema2, {
19336
- auth
19337
- })));
19629
+ await Promise.all(data.map((entry) => validateInput(entry, schema2, options)));
19338
19630
  return;
19339
19631
  }
19632
+ const allowedExtraRootKeys = getExtraRootKeysFromRouteBody(route);
19340
19633
  const nonWritableAttributes = getNonWritableAttributes(schema2);
19341
19634
  const transforms = [
19342
19635
  (data2) => {
@@ -19359,10 +19652,11 @@ const createAPIValidators = (opts) => {
19359
19652
  schema: schema2,
19360
19653
  getModel
19361
19654
  }),
19362
- // unrecognized attributes
19655
+ // unrecognized attributes (allowedExtraRootKeys = registered input param keys)
19363
19656
  traverseEntity$1(throwUnrecognizedFields, {
19364
19657
  schema: schema2,
19365
- getModel
19658
+ getModel,
19659
+ allowedExtraRootKeys
19366
19660
  })
19367
19661
  ];
19368
19662
  if (auth) {
@@ -19374,6 +19668,28 @@ const createAPIValidators = (opts) => {
19374
19668
  opts?.validators?.input?.forEach((validator) => transforms.push(validator(schema2)));
19375
19669
  try {
19376
19670
  await pipe$1(...transforms)(data);
19671
+ if (fp.isObject(data) && route?.request?.body?.["application/json"]) {
19672
+ const bodySchema = route.request.body["application/json"];
19673
+ if (typeof bodySchema === "object" && "shape" in bodySchema) {
19674
+ const shape = bodySchema.shape;
19675
+ const dataObj = data;
19676
+ for (const key of Object.keys(shape)) {
19677
+ if (key === "data" || !(key in dataObj)) continue;
19678
+ const zodSchema = shape[key];
19679
+ if (zodSchema && typeof zodSchema.parse === "function") {
19680
+ const result = zodSchema.safeParse(dataObj[key]);
19681
+ if (!result.success) {
19682
+ throw new ValidationError2(result.error?.message ?? "Validation failed", {
19683
+ key,
19684
+ path: null,
19685
+ source: "body",
19686
+ param: key
19687
+ });
19688
+ }
19689
+ }
19690
+ }
19691
+ }
19692
+ }
19377
19693
  } catch (e) {
19378
19694
  if (e instanceof ValidationError2) {
19379
19695
  e.details.source = "body";
@@ -19381,10 +19697,52 @@ const createAPIValidators = (opts) => {
19381
19697
  throw e;
19382
19698
  }
19383
19699
  };
19384
- const validateQuery = async (query, schema2, { auth } = {}) => {
19700
+ const validateQuery = async (query, schema2, { auth, strictParams = false, route } = {}) => {
19385
19701
  if (!schema2) {
19386
19702
  throw new Error("Missing schema in validateQuery");
19387
19703
  }
19704
+ if (strictParams) {
19705
+ const extraQueryKeys = getExtraQueryKeysFromRoute(route);
19706
+ const allowedKeys = [
19707
+ ...ALLOWED_QUERY_PARAM_KEYS,
19708
+ ...extraQueryKeys
19709
+ ];
19710
+ for (const key of Object.keys(query)) {
19711
+ if (!allowedKeys.includes(key)) {
19712
+ try {
19713
+ throwInvalidKey({
19714
+ key,
19715
+ path: null
19716
+ });
19717
+ } catch (e) {
19718
+ if (e instanceof ValidationError2) {
19719
+ e.details.source = "query";
19720
+ e.details.param = key;
19721
+ }
19722
+ throw e;
19723
+ }
19724
+ }
19725
+ }
19726
+ const routeQuerySchema = route?.request?.query;
19727
+ if (routeQuerySchema) {
19728
+ for (const key of extraQueryKeys) {
19729
+ if (key in query) {
19730
+ const zodSchema = routeQuerySchema[key];
19731
+ if (zodSchema && typeof zodSchema.parse === "function") {
19732
+ const result = zodSchema.safeParse(query[key]);
19733
+ if (!result.success) {
19734
+ throw new ValidationError2(result.error?.message ?? "Invalid query param", {
19735
+ key,
19736
+ path: null,
19737
+ source: "query",
19738
+ param: key
19739
+ });
19740
+ }
19741
+ }
19742
+ }
19743
+ }
19744
+ }
19745
+ }
19388
19746
  const { filters: filters2, sort: sort2, fields: fields2, populate: populate2 } = query;
19389
19747
  if (filters2) {
19390
19748
  await validateFilters2(filters2, schema2, {
@@ -28120,7 +28478,14 @@ var preferredPm = async function preferredPM(pkgPath) {
28120
28478
  };
28121
28479
  }
28122
28480
  try {
28123
- if (typeof findYarnWorkspaceRoot(pkgPath) === "string") {
28481
+ const workspaceRoot = findYarnWorkspaceRoot(pkgPath);
28482
+ if (typeof workspaceRoot === "string") {
28483
+ if (await pathExists(path.join(workspaceRoot, "package-lock.json"))) {
28484
+ return {
28485
+ name: "npm",
28486
+ version: ">=7"
28487
+ };
28488
+ }
28124
28489
  return {
28125
28490
  name: "yarn",
28126
28491
  version: "*"
@@ -29846,13 +30211,17 @@ const extendMiddlewareConfiguration = (middlewares2, middleware2) => {
29846
30211
  };
29847
30212
  const dist = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
29848
30213
  __proto__: null,
30214
+ ALLOWED_QUERY_PARAM_KEYS,
29849
30215
  AbstractRouteValidator,
29850
30216
  CSP_DEFAULTS,
30217
+ RESERVED_INPUT_PARAM_KEYS,
30218
+ SHARED_QUERY_PARAM_KEYS,
29851
30219
  arrays,
29852
30220
  async,
29853
30221
  augmentSchema,
29854
30222
  contentTypes: contentTypes$1,
29855
30223
  createContentApiRoutesFactory,
30224
+ createModelCache,
29856
30225
  dates,
29857
30226
  env,
29858
30227
  errors,
@@ -29898,7 +30267,8 @@ const dist = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty
29898
30267
  validateYupSchema,
29899
30268
  validateYupSchemaSync,
29900
30269
  validateZod,
29901
- yup
30270
+ yup,
30271
+ z: z.z
29902
30272
  }, Symbol.toStringTag, { value: "Module" }));
29903
30273
  const require$$0 = /* @__PURE__ */ getAugmentedNamespace(dist);
29904
30274
  const { castArray, isNil: isNil$1, pipe, every } = fp;
@@ -29968,6 +30338,40 @@ var strategies = ({ strapi: strapi2 }) => {
29968
30338
  },
29969
30339
  getRoomName: function(user) {
29970
30340
  return `${this.name}-user-${user.id}`;
30341
+ },
30342
+ /**
30343
+ * Admin users have full access - verify always passes
30344
+ * @param {object} auth - Auth object
30345
+ * @param {object} config - Config with scope
30346
+ */
30347
+ verify: function(auth, config2) {
30348
+ return;
30349
+ },
30350
+ /**
30351
+ * Returns active admin sessions for broadcast
30352
+ * Admin users have full access, so we return them with full_access permissions
30353
+ * @returns {Promise<Array>} Array of admin session objects with permissions
30354
+ */
30355
+ getRooms: async function() {
30356
+ try {
30357
+ const presenceController = strapi2.plugin("io").controller("presence");
30358
+ if (!presenceController?.getActiveSessions) {
30359
+ return [];
30360
+ }
30361
+ const activeSessions = presenceController.getActiveSessions();
30362
+ return activeSessions.map((session) => ({
30363
+ id: session.userId,
30364
+ name: `admin-${session.userId}`,
30365
+ type: "full-access",
30366
+ // Grants full access to all content types
30367
+ permissions: [],
30368
+ // Empty permissions array - full-access type bypasses permission check
30369
+ ...session
30370
+ }));
30371
+ } catch (error2) {
30372
+ strapi2.log.warn("[plugin-io] Admin getRooms error:", error2.message);
30373
+ return [];
30374
+ }
29971
30375
  }
29972
30376
  };
29973
30377
  const role = {
@@ -30206,12 +30610,12 @@ function transformEntry(entry, type2) {
30206
30610
  // meta: {},
30207
30611
  };
30208
30612
  }
30209
- const { pluginId: pluginId$3 } = pluginId_1;
30613
+ const { pluginId: pluginId$4 } = pluginId_1;
30210
30614
  var settings$1 = ({ strapi: strapi2 }) => {
30211
30615
  const getPluginStore = () => {
30212
30616
  return strapi2.store({
30213
30617
  type: "plugin",
30214
- name: pluginId$3
30618
+ name: pluginId$4
30215
30619
  });
30216
30620
  };
30217
30621
  const getDefaultSettings = () => ({
@@ -30351,6 +30755,19 @@ var settings$1 = ({ strapi: strapi2 }) => {
30351
30755
  // Maximum nesting depth for diff
30352
30756
  }
30353
30757
  });
30758
+ const deepMerge = (target, source) => {
30759
+ const result = { ...target };
30760
+ for (const key of Object.keys(source)) {
30761
+ const targetVal = target[key];
30762
+ const sourceVal = source[key];
30763
+ if (targetVal && sourceVal && typeof targetVal === "object" && !Array.isArray(targetVal) && typeof sourceVal === "object" && !Array.isArray(sourceVal)) {
30764
+ result[key] = deepMerge(targetVal, sourceVal);
30765
+ } else {
30766
+ result[key] = sourceVal;
30767
+ }
30768
+ }
30769
+ return result;
30770
+ };
30354
30771
  return {
30355
30772
  /**
30356
30773
  * Get current settings (merged with defaults)
@@ -30373,10 +30790,7 @@ var settings$1 = ({ strapi: strapi2 }) => {
30373
30790
  async setSettings(newSettings) {
30374
30791
  const pluginStore = getPluginStore();
30375
30792
  const currentSettings = await this.getSettings();
30376
- const updatedSettings = {
30377
- ...currentSettings,
30378
- ...newSettings
30379
- };
30793
+ const updatedSettings = deepMerge(currentSettings, newSettings);
30380
30794
  await pluginStore.set({
30381
30795
  key: "settings",
30382
30796
  value: updatedSettings
@@ -30389,7 +30803,7 @@ var settings$1 = ({ strapi: strapi2 }) => {
30389
30803
  getDefaultSettings
30390
30804
  };
30391
30805
  };
30392
- const { pluginId: pluginId$2 } = pluginId_1;
30806
+ const { pluginId: pluginId$3 } = pluginId_1;
30393
30807
  var monitoring$1 = ({ strapi: strapi2 }) => {
30394
30808
  let eventLog = [];
30395
30809
  let eventStats = {
@@ -30495,8 +30909,10 @@ var monitoring$1 = ({ strapi: strapi2 }) => {
30495
30909
  */
30496
30910
  getEventsPerSecond() {
30497
30911
  const now = Date.now();
30498
- const elapsed = (now - eventStats.lastReset) / 1e3;
30499
- return elapsed > 0 ? (eventStats.totalEvents / elapsed).toFixed(2) : 0;
30912
+ const windowMs = 6e4;
30913
+ const recentEvents = eventLog.filter((e) => now - e.timestamp < windowMs);
30914
+ const elapsed = Math.min((now - eventStats.lastReset) / 1e3, windowMs / 1e3);
30915
+ return elapsed > 0 ? Number((recentEvents.length / elapsed).toFixed(2)) : 0;
30500
30916
  },
30501
30917
  /**
30502
30918
  * Reset statistics
@@ -30517,13 +30933,14 @@ var monitoring$1 = ({ strapi: strapi2 }) => {
30517
30933
  if (!io2) {
30518
30934
  throw new Error("Socket.IO not initialized");
30519
30935
  }
30936
+ const safeName = `test:${eventName.replace(/[^a-zA-Z0-9:._-]/g, "")}`;
30520
30937
  const testData = {
30521
30938
  ...data,
30522
30939
  timestamp: Date.now(),
30523
30940
  test: true
30524
30941
  };
30525
- io2.emit(eventName, testData);
30526
- this.logEvent("test", { eventName, data: testData });
30942
+ io2.emit(safeName, testData);
30943
+ this.logEvent("test", { eventName: safeName, data: testData });
30527
30944
  return {
30528
30945
  success: true,
30529
30946
  eventName,
@@ -30533,7 +30950,7 @@ var monitoring$1 = ({ strapi: strapi2 }) => {
30533
30950
  }
30534
30951
  };
30535
30952
  };
30536
- const { pluginId: pluginId$1 } = pluginId_1;
30953
+ const { pluginId: pluginId$2 } = pluginId_1;
30537
30954
  var presence$1 = ({ strapi: strapi2 }) => {
30538
30955
  const activeConnections = /* @__PURE__ */ new Map();
30539
30956
  const entityEditors = /* @__PURE__ */ new Map();
@@ -30935,7 +31352,7 @@ var presence$1 = ({ strapi: strapi2 }) => {
30935
31352
  }
30936
31353
  };
30937
31354
  };
30938
- const { pluginId } = pluginId_1;
31355
+ const { pluginId: pluginId$1 } = pluginId_1;
30939
31356
  var preview$1 = ({ strapi: strapi2 }) => {
30940
31357
  const previewSubscribers = /* @__PURE__ */ new Map();
30941
31358
  const socketState = /* @__PURE__ */ new Map();
@@ -31197,22 +31614,23 @@ var diff$1 = ({ strapi: strapi2 }) => {
31197
31614
  const isPlainObject2 = (value) => {
31198
31615
  return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
31199
31616
  };
31200
- const isEqual2 = (a, b) => {
31617
+ const isEqual2 = (a, b, depth2 = 0) => {
31201
31618
  if (a === b) return true;
31202
31619
  if (a === null || b === null) return a === b;
31203
31620
  if (typeof a !== typeof b) return false;
31621
+ if (depth2 > 30) return JSON.stringify(a) === JSON.stringify(b);
31204
31622
  if (a instanceof Date && b instanceof Date) {
31205
31623
  return a.getTime() === b.getTime();
31206
31624
  }
31207
31625
  if (Array.isArray(a) && Array.isArray(b)) {
31208
31626
  if (a.length !== b.length) return false;
31209
- return a.every((item, index2) => isEqual2(item, b[index2]));
31627
+ return a.every((item, index2) => isEqual2(item, b[index2], depth2 + 1));
31210
31628
  }
31211
31629
  if (isPlainObject2(a) && isPlainObject2(b)) {
31212
31630
  const keysA = Object.keys(a);
31213
31631
  const keysB = Object.keys(b);
31214
31632
  if (keysA.length !== keysB.length) return false;
31215
- return keysA.every((key) => isEqual2(a[key], b[key]));
31633
+ return keysA.every((key) => isEqual2(a[key], b[key], depth2 + 1));
31216
31634
  }
31217
31635
  return false;
31218
31636
  };
@@ -31397,6 +31815,142 @@ var diff$1 = ({ strapi: strapi2 }) => {
31397
31815
  }
31398
31816
  };
31399
31817
  };
31818
+ var security = ({ strapi: strapi2 }) => {
31819
+ const rateLimitStore = /* @__PURE__ */ new Map();
31820
+ const connectionLimitStore = /* @__PURE__ */ new Map();
31821
+ const cleanupInterval = setInterval(() => {
31822
+ const now = Date.now();
31823
+ const maxAge = 60 * 1e3;
31824
+ for (const [key, value] of rateLimitStore.entries()) {
31825
+ if (now - value.resetTime > maxAge) {
31826
+ rateLimitStore.delete(key);
31827
+ }
31828
+ }
31829
+ for (const [key, value] of connectionLimitStore.entries()) {
31830
+ if (now - value.lastSeen > maxAge) {
31831
+ connectionLimitStore.delete(key);
31832
+ }
31833
+ }
31834
+ }, 60 * 1e3);
31835
+ return {
31836
+ /**
31837
+ * Stops the background cleanup interval (called on plugin destroy)
31838
+ */
31839
+ stopCleanupInterval() {
31840
+ clearInterval(cleanupInterval);
31841
+ },
31842
+ /**
31843
+ * Check if request should be rate limited
31844
+ * @param {string} identifier - Unique identifier (IP, user ID, etc.)
31845
+ * @param {Object} options - Rate limit options
31846
+ * @param {number} options.maxRequests - Maximum requests allowed
31847
+ * @param {number} options.windowMs - Time window in milliseconds
31848
+ * @returns {Object} - { allowed: boolean, remaining: number, resetTime: number }
31849
+ */
31850
+ checkRateLimit(identifier, options = {}) {
31851
+ const { maxRequests = 100, windowMs = 60 * 1e3 } = options;
31852
+ const now = Date.now();
31853
+ const key = `ratelimit_${identifier}`;
31854
+ let record = rateLimitStore.get(key);
31855
+ if (!record || now - record.resetTime > windowMs) {
31856
+ record = {
31857
+ count: 1,
31858
+ resetTime: now,
31859
+ windowMs
31860
+ };
31861
+ rateLimitStore.set(key, record);
31862
+ return {
31863
+ allowed: true,
31864
+ remaining: maxRequests - 1,
31865
+ resetTime: now + windowMs
31866
+ };
31867
+ }
31868
+ if (record.count >= maxRequests) {
31869
+ return {
31870
+ allowed: false,
31871
+ remaining: 0,
31872
+ resetTime: record.resetTime + windowMs,
31873
+ retryAfter: record.resetTime + windowMs - now
31874
+ };
31875
+ }
31876
+ record.count++;
31877
+ rateLimitStore.set(key, record);
31878
+ return {
31879
+ allowed: true,
31880
+ remaining: maxRequests - record.count,
31881
+ resetTime: record.resetTime + windowMs
31882
+ };
31883
+ },
31884
+ /**
31885
+ * Check connection limits per IP/user
31886
+ * @param {string} identifier - Unique identifier
31887
+ * @param {number} maxConnections - Maximum allowed connections
31888
+ * @returns {boolean} - Whether connection is allowed
31889
+ */
31890
+ checkConnectionLimit(identifier, maxConnections = 5) {
31891
+ const key = `connlimit_${identifier}`;
31892
+ const record = connectionLimitStore.get(key);
31893
+ const now = Date.now();
31894
+ if (!record) {
31895
+ connectionLimitStore.set(key, {
31896
+ count: 1,
31897
+ lastSeen: now
31898
+ });
31899
+ return true;
31900
+ }
31901
+ if (record.count >= maxConnections) {
31902
+ strapi2.log.warn(`[Socket.IO Security] Connection limit exceeded for ${identifier}`);
31903
+ return false;
31904
+ }
31905
+ record.count++;
31906
+ record.lastSeen = now;
31907
+ connectionLimitStore.set(key, record);
31908
+ return true;
31909
+ },
31910
+ /**
31911
+ * Release a connection slot
31912
+ * @param {string} identifier - Unique identifier
31913
+ */
31914
+ releaseConnection(identifier) {
31915
+ const key = `connlimit_${identifier}`;
31916
+ const record = connectionLimitStore.get(key);
31917
+ if (record) {
31918
+ record.count = Math.max(0, record.count - 1);
31919
+ if (record.count === 0) {
31920
+ connectionLimitStore.delete(key);
31921
+ } else {
31922
+ connectionLimitStore.set(key, record);
31923
+ }
31924
+ }
31925
+ },
31926
+ /**
31927
+ * Validate event name to prevent injection
31928
+ * @param {string} eventName - Event name to validate
31929
+ * @returns {boolean} - Whether event name is valid
31930
+ */
31931
+ validateEventName(eventName) {
31932
+ const validPattern = /^[a-zA-Z0-9:._-]+$/;
31933
+ return validPattern.test(eventName) && eventName.length < 100;
31934
+ },
31935
+ /**
31936
+ * Get current statistics
31937
+ * @returns {Object} - Statistics object
31938
+ */
31939
+ getStats() {
31940
+ return {
31941
+ rateLimitEntries: rateLimitStore.size,
31942
+ connectionLimitEntries: connectionLimitStore.size
31943
+ };
31944
+ },
31945
+ /**
31946
+ * Clear all rate limit data
31947
+ */
31948
+ clear() {
31949
+ rateLimitStore.clear();
31950
+ connectionLimitStore.clear();
31951
+ }
31952
+ };
31953
+ };
31400
31954
  const strategy = strategies;
31401
31955
  const sanitize = sanitize_1;
31402
31956
  const transform = transform$1;
@@ -31413,20 +31967,58 @@ var services$1 = {
31413
31967
  monitoring,
31414
31968
  presence,
31415
31969
  preview,
31416
- diff
31970
+ diff,
31971
+ security
31417
31972
  };
31418
31973
  const bootstrap = bootstrap_1;
31419
31974
  const config = config$1;
31420
31975
  const controllers = controllers$1;
31421
31976
  const routes = routes$1;
31422
31977
  const services = services$1;
31423
- const destroy = async () => {
31424
- };
31978
+ const { pluginId } = pluginId_1;
31425
31979
  const register = async () => {
31426
31980
  };
31427
31981
  const contentTypes = {};
31428
31982
  const middlewares = {};
31429
31983
  const policies = {};
31984
+ const destroy = async ({ strapi: strapi2 }) => {
31985
+ try {
31986
+ const presenceService = strapi2.plugin(pluginId)?.service("presence");
31987
+ if (presenceService?.stopCleanupInterval) {
31988
+ presenceService.stopCleanupInterval();
31989
+ }
31990
+ const presenceController = strapi2.plugin(pluginId)?.controller("presence");
31991
+ if (presenceController?.stopTokenCleanup) {
31992
+ presenceController.stopTokenCleanup();
31993
+ }
31994
+ const securityService = strapi2.plugin(pluginId)?.service("security");
31995
+ if (securityService?.stopCleanupInterval) {
31996
+ securityService.stopCleanupInterval();
31997
+ }
31998
+ const io2 = strapi2.$io?.server;
31999
+ if (io2) {
32000
+ io2.disconnectSockets(true);
32001
+ await new Promise((resolve) => {
32002
+ io2.close((err) => {
32003
+ if (err) {
32004
+ strapi2.log.warn(`socket.io: Error closing server: ${err.message}`);
32005
+ }
32006
+ resolve();
32007
+ });
32008
+ });
32009
+ }
32010
+ const redisClients = strapi2.$io?._redisClients;
32011
+ if (redisClients) {
32012
+ await Promise.allSettled([
32013
+ redisClients.pubClient?.quit?.(),
32014
+ redisClients.subClient?.quit?.()
32015
+ ]);
32016
+ }
32017
+ strapi2.log.info("socket.io: Plugin destroyed – all handles released");
32018
+ } catch (err) {
32019
+ strapi2.log.error(`socket.io: Error during destroy: ${err.message}`);
32020
+ }
32021
+ };
31430
32022
  var server = {
31431
32023
  register,
31432
32024
  bootstrap,