@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.
@@ -16,6 +16,7 @@ import require$$0$c from "constants";
16
16
  import { Writable } from "node:stream";
17
17
  import { z as z$1 } from "zod";
18
18
  import * as z from "zod/v4";
19
+ import { z as z$2 } from "zod/v4";
19
20
  var commonjsGlobal = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : {};
20
21
  function getDefaultExportFromCjs(x) {
21
22
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
@@ -51,10 +52,10 @@ const require$$0$3 = {
51
52
  strapi: strapi$1
52
53
  };
53
54
  const pluginPkg = require$$0$3;
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" }) {
55
+ const pluginId$a = pluginPkg.strapi.name;
56
+ var pluginId_1 = { pluginId: pluginId$a };
57
+ const { pluginId: pluginId$9 } = pluginId_1;
58
+ function getService$3({ name, plugin = pluginId$9, type: type2 = "plugin" }) {
58
59
  let serviceUID = `${type2}::${plugin}`;
59
60
  if (name && name.length) {
60
61
  serviceUID += `.${name}`;
@@ -97,7 +98,7 @@ async function handshake$2(socket, next) {
97
98
  room = strategyService["role"].getRoomName(role);
98
99
  }
99
100
  if (room) {
100
- socket.join(room.replace(" ", "-"));
101
+ socket.join(room.replaceAll(" ", "-"));
101
102
  } else {
102
103
  throw new Error("No valid room found");
103
104
  }
@@ -124,7 +125,7 @@ var constants$7 = {
124
125
  const { Server } = require$$0$4;
125
126
  const { handshake } = middleware;
126
127
  const { getService: getService$1 } = getService_1;
127
- const { pluginId: pluginId$7 } = pluginId_1;
128
+ const { pluginId: pluginId$8 } = pluginId_1;
128
129
  const { API_TOKEN_TYPE: API_TOKEN_TYPE$1 } = constants$7;
129
130
  let SocketIO$2 = class SocketIO {
130
131
  constructor(options) {
@@ -166,7 +167,7 @@ let SocketIO$2 = class SocketIO {
166
167
  });
167
168
  const roomName = strategy2.getRoomName(room);
168
169
  const data = transformService.response({ data: sanitizedData, schema: schema2 });
169
- this._socket.to(roomName.replace(" ", "-")).emit(eventName, { ...data });
170
+ this._socket.to(roomName.replaceAll(" ", "-")).emit(eventName, { ...data });
170
171
  if (entityRoomName) {
171
172
  this._socket.to(entityRoomName).emit(eventName, { ...data });
172
173
  }
@@ -202,21 +203,25 @@ function requireSanitizeSensitiveFields() {
202
203
  hasRequiredSanitizeSensitiveFields = 1;
203
204
  const SENSITIVE_FIELDS = [
204
205
  "password",
206
+ "passwordHash",
205
207
  "resetPasswordToken",
206
208
  "registrationToken",
207
209
  "confirmationToken",
208
210
  "privateKey",
209
211
  "secretKey",
210
212
  "apiKey",
211
- "secret",
212
- "hash"
213
+ "secret"
213
214
  ];
214
- function deepSanitize(obj) {
215
+ const MAX_SANITIZE_DEPTH = 20;
216
+ function deepSanitize(obj, depth2 = 0) {
215
217
  if (!obj || typeof obj !== "object") {
216
218
  return obj;
217
219
  }
220
+ if (depth2 >= MAX_SANITIZE_DEPTH) {
221
+ return obj;
222
+ }
218
223
  if (Array.isArray(obj)) {
219
- return obj.map((item) => deepSanitize(item));
224
+ return obj.map((item) => deepSanitize(item, depth2 + 1));
220
225
  }
221
226
  const sanitized = {};
222
227
  for (const [key, value] of Object.entries(obj)) {
@@ -224,7 +229,7 @@ function requireSanitizeSensitiveFields() {
224
229
  continue;
225
230
  }
226
231
  if (value && typeof value === "object") {
227
- sanitized[key] = deepSanitize(value);
232
+ sanitized[key] = deepSanitize(value, depth2 + 1);
228
233
  } else {
229
234
  sanitized[key] = value;
230
235
  }
@@ -244,11 +249,57 @@ function requireSanitizeSensitiveFields() {
244
249
  return sanitizeSensitiveFields;
245
250
  }
246
251
  const { SocketIO: SocketIO2 } = structures;
247
- const { pluginId: pluginId$6 } = pluginId_1;
252
+ const { pluginId: pluginId$7 } = pluginId_1;
253
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
254
+ const MAX_FIELD_VALUE_SIZE = 1e5;
255
+ function safeHandler(handler, expectsObject = true) {
256
+ return function(...args) {
257
+ try {
258
+ if (expectsObject) {
259
+ const data = args[0];
260
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
261
+ const callback = typeof args[args.length - 1] === "function" ? args[args.length - 1] : null;
262
+ if (callback) callback({ success: false, error: "Invalid payload" });
263
+ return;
264
+ }
265
+ }
266
+ const result = handler.apply(this, args);
267
+ if (result && typeof result.catch === "function") {
268
+ result.catch((err) => {
269
+ strapi.log.error(`socket.io: Unhandled error in event handler: ${err.message}`);
270
+ const callback = typeof args[args.length - 1] === "function" ? args[args.length - 1] : null;
271
+ if (callback) callback({ success: false, error: "Internal error" });
272
+ });
273
+ }
274
+ } catch (err) {
275
+ strapi.log.error(`socket.io: Error in event handler: ${err.message}`);
276
+ const callback = typeof args[args.length - 1] === "function" ? args[args.length - 1] : null;
277
+ if (callback) callback({ success: false, error: "Internal error" });
278
+ }
279
+ };
280
+ }
281
+ function resolveClientIp(socket) {
282
+ const xff = socket.handshake.headers?.["x-forwarded-for"];
283
+ if (xff) {
284
+ return xff.split(",")[0].trim();
285
+ }
286
+ return socket.handshake.address;
287
+ }
288
+ function redactUrl(url) {
289
+ try {
290
+ const parsed = new URL(url);
291
+ if (parsed.password) {
292
+ parsed.password = "***";
293
+ }
294
+ return parsed.toString();
295
+ } catch {
296
+ return url.replace(/:([^@/]+)@/, ":***@");
297
+ }
298
+ }
248
299
  async function bootstrapIO$1({ strapi: strapi2 }) {
249
- const settingsService = strapi2.plugin(pluginId$6).service("settings");
300
+ const settingsService = strapi2.plugin(pluginId$7).service("settings");
250
301
  const settings2 = await settingsService.getSettings();
251
- const monitoringService = strapi2.plugin(pluginId$6).service("monitoring");
302
+ const monitoringService = strapi2.plugin(pluginId$7).service("monitoring");
252
303
  const serverOptions = {
253
304
  cors: {
254
305
  origin: settings2.cors?.origins || ["http://localhost:3000"],
@@ -260,7 +311,7 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
260
311
  connectTimeout: settings2.connection?.connectionTimeout || 45e3,
261
312
  maxHttpBufferSize: 1e6,
262
313
  transports: ["websocket", "polling"],
263
- allowEIO3: true
314
+ allowEIO3: settings2.connection?.allowEIO3 ?? false
264
315
  };
265
316
  if (settings2.redis?.enabled) {
266
317
  try {
@@ -272,13 +323,15 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
272
323
  const subClient = pubClient.duplicate();
273
324
  await Promise.all([pubClient.connect(), subClient.connect()]);
274
325
  serverOptions.adapter = createAdapter(pubClient, subClient);
275
- strapi2.log.info(`socket.io: Redis adapter enabled (${settings2.redis.url})`);
326
+ serverOptions._redisClients = { pubClient, subClient };
327
+ strapi2.log.info(`socket.io: Redis adapter enabled (${redactUrl(settings2.redis.url)})`);
276
328
  } catch (err) {
277
329
  strapi2.log.error(`socket.io: Redis adapter failed: ${err.message}`);
278
330
  }
279
331
  }
280
332
  const io2 = new SocketIO2(serverOptions);
281
333
  strapi2.$io = io2;
334
+ strapi2.$io._redisClients = serverOptions._redisClients || null;
282
335
  strapi2.$ioSettings = settings2;
283
336
  const sanitizeSensitiveFields2 = requireSanitizeSensitiveFields();
284
337
  sanitizeSensitiveFields2({ strapi: strapi2 });
@@ -304,7 +357,7 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
304
357
  }
305
358
  strapi2.$io.namespaces = namespaces;
306
359
  io2.server.use(async (socket, next) => {
307
- const clientIp = socket.handshake.address;
360
+ const clientIp = resolveClientIp(socket);
308
361
  if (settings2.security?.ipWhitelist?.length > 0) {
309
362
  if (!settings2.security.ipWhitelist.includes(clientIp)) {
310
363
  return next(new Error("IP not whitelisted"));
@@ -317,30 +370,22 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
317
370
  if (currentConnections >= (settings2.connection?.maxConnections || 1e3)) {
318
371
  return next(new Error("Max connections reached"));
319
372
  }
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;
373
+ const token = socket.handshake.auth?.token;
323
374
  if (token) {
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) {
329
- socket.user = {
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
335
- };
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})`);
339
- } else {
340
- strapi2.log.warn(`socket.io: Admin session token invalid or expired`);
341
- }
342
- } catch (err) {
343
- strapi2.log.warn(`socket.io: Admin session verification failed: ${err.message}`);
375
+ const isAdminToken = UUID_REGEX.test(token);
376
+ if (isAdminToken) {
377
+ if (socket.adminUser) {
378
+ const admin2 = socket.adminUser;
379
+ socket.user = {
380
+ id: admin2.id,
381
+ username: `${admin2.firstname || ""} ${admin2.lastname || ""}`.trim() || `Admin ${admin2.id}`,
382
+ email: admin2.email || `admin-${admin2.id}`,
383
+ role: "strapi-super-admin",
384
+ isAdmin: true
385
+ };
386
+ strapi2.log.info(`socket.io: Admin connected - ${socket.user.username} (ID: ${admin2.id})`);
387
+ } else {
388
+ strapi2.log.warn("socket.io: Admin token present but handshake did not authenticate");
344
389
  }
345
390
  } else {
346
391
  try {
@@ -406,13 +451,13 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
406
451
  }
407
452
  });
408
453
  }
409
- const presenceService = strapi2.plugin(pluginId$6).service("presence");
410
- const previewService = strapi2.plugin(pluginId$6).service("preview");
454
+ const presenceService = strapi2.plugin(pluginId$7).service("presence");
455
+ const previewService = strapi2.plugin(pluginId$7).service("preview");
411
456
  if (settings2.presence?.enabled !== false) {
412
457
  presenceService.startCleanupInterval();
413
458
  }
414
459
  io2.server.on("connection", (socket) => {
415
- const clientIp = socket.handshake.address || "unknown";
460
+ const clientIp = resolveClientIp(socket);
416
461
  const username = socket.user?.username || "anonymous";
417
462
  if (settings2.monitoring?.enableConnectionLogging) {
418
463
  strapi2.log.info(`socket.io: Client connected (id: ${socket.id}, user: ${username}, ip: ${clientIp})`);
@@ -458,19 +503,24 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
458
503
  if (callback) callback({ success: true, room: roomName });
459
504
  });
460
505
  socket.on("leave-room", (roomName, callback) => {
461
- if (typeof roomName === "string" && roomName.length > 0) {
462
- socket.leave(roomName);
463
- strapi2.log.debug(`socket.io: Socket ${socket.id} left room: ${roomName}`);
464
- if (callback) callback({ success: true, room: roomName });
465
- } else {
506
+ if (typeof roomName !== "string" || roomName.length === 0) {
466
507
  if (callback) callback({ success: false, error: "Invalid room name" });
508
+ return;
509
+ }
510
+ const sanitizedRoom = roomName.replace(/[^a-zA-Z0-9\-_:]/g, "");
511
+ if (sanitizedRoom !== roomName) {
512
+ if (callback) callback({ success: false, error: "Room name contains invalid characters" });
513
+ return;
467
514
  }
515
+ socket.leave(roomName);
516
+ strapi2.log.debug(`socket.io: Socket ${socket.id} left room: ${roomName}`);
517
+ if (callback) callback({ success: true, room: roomName });
468
518
  });
469
519
  socket.on("get-rooms", (callback) => {
470
520
  const rooms = Array.from(socket.rooms).filter((r) => r !== socket.id);
471
521
  if (callback) callback({ success: true, rooms });
472
522
  });
473
- socket.on("presence:join", async ({ uid, documentId }, callback) => {
523
+ socket.on("presence:join", safeHandler(async ({ uid, documentId }, callback) => {
474
524
  if (settings2.presence?.enabled === false) {
475
525
  if (callback) callback({ success: false, error: "Presence is disabled" });
476
526
  return;
@@ -481,8 +531,8 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
481
531
  }
482
532
  const result = await presenceService.joinEntity(socket.id, uid, documentId);
483
533
  if (callback) callback(result);
484
- });
485
- socket.on("presence:leave", async ({ uid, documentId }, callback) => {
534
+ }));
535
+ socket.on("presence:leave", safeHandler(async ({ uid, documentId }, callback) => {
486
536
  if (settings2.presence?.enabled === false) {
487
537
  if (callback) callback({ success: false, error: "Presence is disabled" });
488
538
  return;
@@ -493,24 +543,26 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
493
543
  }
494
544
  const result = await presenceService.leaveEntity(socket.id, uid, documentId);
495
545
  if (callback) callback(result);
496
- });
546
+ }));
497
547
  socket.on("presence:heartbeat", (callback) => {
498
548
  const result = presenceService.heartbeat(socket.id);
499
549
  if (callback) callback(result);
500
550
  });
501
- socket.on("presence:typing", ({ uid, documentId, fieldName }) => {
551
+ socket.on("presence:typing", safeHandler(({ uid, documentId, fieldName }) => {
502
552
  if (settings2.presence?.enabled === false) return;
553
+ if (!socket.user) return;
554
+ if (typeof fieldName !== "string" || fieldName.length > 200) return;
503
555
  presenceService.broadcastTyping(socket.id, uid, documentId, fieldName);
504
- });
505
- socket.on("presence:check", async ({ uid, documentId }, callback) => {
556
+ }));
557
+ socket.on("presence:check", safeHandler(async ({ uid, documentId }, callback) => {
506
558
  if (settings2.presence?.enabled === false) {
507
559
  if (callback) callback({ success: false, error: "Presence is disabled" });
508
560
  return;
509
561
  }
510
562
  const editors = await presenceService.getEntityEditors(uid, documentId);
511
563
  if (callback) callback({ success: true, editors, isBeingEdited: editors.length > 0 });
512
- });
513
- socket.on("preview:subscribe", async ({ uid, documentId }, callback) => {
564
+ }));
565
+ socket.on("preview:subscribe", safeHandler(async ({ uid, documentId }, callback) => {
514
566
  if (settings2.livePreview?.enabled === false) {
515
567
  if (callback) callback({ success: false, error: "Live preview is disabled" });
516
568
  return;
@@ -519,22 +571,33 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
519
571
  if (callback) callback({ success: false, error: "uid and documentId are required" });
520
572
  return;
521
573
  }
574
+ const userRole = socket.userRole || "public";
575
+ const rolePerms = (settings2.rolePermissions || {})[userRole] || {};
576
+ const ctPerms = rolePerms.contentTypes?.[uid];
577
+ if (!ctPerms && settings2.security?.requireAuthentication) {
578
+ if (callback) callback({ success: false, error: "Permission denied" });
579
+ return;
580
+ }
522
581
  const result = await previewService.subscribe(socket.id, uid, documentId);
523
582
  if (callback) callback(result);
524
- });
525
- socket.on("preview:unsubscribe", ({ uid, documentId }, callback) => {
583
+ }));
584
+ socket.on("preview:unsubscribe", safeHandler(({ uid, documentId }, callback) => {
526
585
  if (!uid || !documentId) {
527
586
  if (callback) callback({ success: false, error: "uid and documentId are required" });
528
587
  return;
529
588
  }
530
589
  const result = previewService.unsubscribe(socket.id, uid, documentId);
531
590
  if (callback) callback(result);
532
- });
533
- socket.on("preview:field-change", ({ uid, documentId, fieldName, value }) => {
591
+ }));
592
+ socket.on("preview:field-change", safeHandler(({ uid, documentId, fieldName, value }) => {
534
593
  if (settings2.livePreview?.enabled === false) return;
594
+ if (!socket.user) return;
595
+ if (typeof fieldName !== "string" || fieldName.length > 200) return;
596
+ const serialized = typeof value === "string" ? value : JSON.stringify(value);
597
+ if (serialized && serialized.length > MAX_FIELD_VALUE_SIZE) return;
535
598
  previewService.emitFieldChange(socket.id, uid, documentId, fieldName, value);
536
- });
537
- socket.on("subscribe-entity", async ({ uid, id }, callback) => {
599
+ }));
600
+ socket.on("subscribe-entity", safeHandler(async ({ uid, id }, callback) => {
538
601
  if (settings2.entitySubscriptions?.enabled === false) {
539
602
  if (callback) callback({ success: false, error: "Entity subscriptions are disabled" });
540
603
  return;
@@ -609,8 +672,8 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
609
672
  }
610
673
  strapi2.log.debug(`socket.io: Socket ${socket.id} subscribed to entity: ${entityRoomName}`);
611
674
  if (callback) callback({ success: true, room: entityRoomName, uid, id });
612
- });
613
- socket.on("unsubscribe-entity", ({ uid, id }, callback) => {
675
+ }));
676
+ socket.on("unsubscribe-entity", safeHandler(({ uid, id }, callback) => {
614
677
  if (settings2.entitySubscriptions?.enabled === false) {
615
678
  if (callback) callback({ success: false, error: "Entity subscriptions are disabled" });
616
679
  return;
@@ -631,9 +694,9 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
631
694
  }
632
695
  strapi2.log.debug(`socket.io: Socket ${socket.id} unsubscribed from entity: ${entityRoomName}`);
633
696
  if (callback) callback({ success: true, room: entityRoomName, uid, id });
634
- });
697
+ }));
635
698
  socket.on("get-entity-subscriptions", (callback) => {
636
- const rooms = Array.from(socket.rooms).filter((r) => r !== socket.id && r.includes(":")).map((room) => {
699
+ const rooms = Array.from(socket.rooms).filter((r) => r !== socket.id && /^(api|plugin)::/.test(r) && !r.startsWith("presence:") && !r.startsWith("preview:")).map((room) => {
637
700
  const lastColonIndex = room.lastIndexOf(":");
638
701
  const uid = room.substring(0, lastColonIndex);
639
702
  const id = room.substring(lastColonIndex + 1);
@@ -641,7 +704,7 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
641
704
  });
642
705
  if (callback) callback({ success: true, subscriptions: rooms });
643
706
  });
644
- socket.on("private-message", ({ to, message }, callback) => {
707
+ socket.on("private-message", safeHandler(({ to, message }, callback) => {
645
708
  if (settings2.rooms?.enablePrivateRooms === false) {
646
709
  strapi2.log.warn(`socket.io: Private messages disabled for socket ${socket.id}`);
647
710
  if (callback) callback({ success: false, error: "Private messages are disabled" });
@@ -669,7 +732,7 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
669
732
  });
670
733
  strapi2.log.debug(`socket.io: Private message from ${socket.id} to ${to}`);
671
734
  if (callback) callback({ success: true });
672
- });
735
+ }));
673
736
  socket.on("disconnect", async (reason) => {
674
737
  if (settings2.monitoring?.enableConnectionLogging) {
675
738
  strapi2.log.info(`socket.io: Client disconnected (id: ${socket.id}, user: ${username}, reason: ${reason})`);
@@ -686,7 +749,7 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
686
749
  previewService.cleanupSocket(socket.id);
687
750
  }
688
751
  try {
689
- const presenceController = strapi2.plugin(pluginId$6).controller("presence");
752
+ const presenceController = strapi2.plugin(pluginId$7).controller("presence");
690
753
  if (presenceController?.unregisterSocket) {
691
754
  presenceController.unregisterSocket(socket.id);
692
755
  }
@@ -954,7 +1017,7 @@ function getTransactionCtx() {
954
1017
  }
955
1018
  return transactionCtx;
956
1019
  }
957
- const { pluginId: pluginId$5 } = pluginId_1;
1020
+ const { pluginId: pluginId$6 } = pluginId_1;
958
1021
  function scheduleAfterTransaction(callback, delay = 0) {
959
1022
  const runner = () => setTimeout(callback, delay);
960
1023
  const ctx = getTransactionCtx();
@@ -1064,8 +1127,8 @@ async function bootstrapLifecycles$1({ strapi: strapi2 }) {
1064
1127
  const modelInfo = { singularName: event.model.singularName, uid: event.model.uid };
1065
1128
  scheduleAfterTransaction(() => {
1066
1129
  try {
1067
- const diffService = strapi2.plugin(pluginId$5).service("diff");
1068
- const previewService = strapi2.plugin(pluginId$5).service("preview");
1130
+ const diffService = strapi2.plugin(pluginId$6).service("diff");
1131
+ const previewService = strapi2.plugin(pluginId$6).service("preview");
1069
1132
  const fieldLevelEnabled = strapi2.$ioSettings?.fieldLevelChanges?.enabled !== false;
1070
1133
  let eventPayload;
1071
1134
  if (fieldLevelEnabled && previousData && diffService) {
@@ -1182,14 +1245,14 @@ var config$1 = {
1182
1245
  validator(config2) {
1183
1246
  }
1184
1247
  };
1185
- const { pluginId: pluginId$4 } = pluginId_1;
1248
+ const { pluginId: pluginId$5 } = pluginId_1;
1186
1249
  var settings$3 = ({ strapi: strapi2 }) => ({
1187
1250
  /**
1188
1251
  * GET /io/settings
1189
1252
  * Retrieve current plugin settings
1190
1253
  */
1191
1254
  async getSettings(ctx) {
1192
- const settingsService = strapi2.plugin(pluginId$4).service("settings");
1255
+ const settingsService = strapi2.plugin(pluginId$5).service("settings");
1193
1256
  const settings2 = await settingsService.getSettings();
1194
1257
  ctx.body = { data: settings2 };
1195
1258
  },
@@ -1198,8 +1261,33 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1198
1261
  * Update plugin settings and hot-reload Socket.IO
1199
1262
  */
1200
1263
  async updateSettings(ctx) {
1201
- const settingsService = strapi2.plugin(pluginId$4).service("settings");
1264
+ const settingsService = strapi2.plugin(pluginId$5).service("settings");
1202
1265
  const { body } = ctx.request;
1266
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
1267
+ return ctx.badRequest("Request body must be a JSON object");
1268
+ }
1269
+ const ALLOWED_KEYS = [
1270
+ "enabled",
1271
+ "cors",
1272
+ "connection",
1273
+ "security",
1274
+ "contentTypes",
1275
+ "events",
1276
+ "rooms",
1277
+ "entitySubscriptions",
1278
+ "rolePermissions",
1279
+ "redis",
1280
+ "namespaces",
1281
+ "middleware",
1282
+ "monitoring",
1283
+ "presence",
1284
+ "livePreview",
1285
+ "fieldLevelChanges"
1286
+ ];
1287
+ const unknownKeys = Object.keys(body).filter((k) => !ALLOWED_KEYS.includes(k));
1288
+ if (unknownKeys.length > 0) {
1289
+ return ctx.badRequest(`Unknown settings keys: ${unknownKeys.join(", ")}`);
1290
+ }
1203
1291
  await settingsService.getSettings();
1204
1292
  const updatedSettings = await settingsService.setSettings(body);
1205
1293
  strapi2.$ioSettings = updatedSettings;
@@ -1231,7 +1319,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1231
1319
  * Get connection and event statistics
1232
1320
  */
1233
1321
  async getStats(ctx) {
1234
- const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
1322
+ const monitoringService = strapi2.plugin(pluginId$5).service("monitoring");
1235
1323
  const connectionStats = monitoringService.getConnectionStats();
1236
1324
  const eventStats = monitoringService.getEventStats();
1237
1325
  ctx.body = {
@@ -1246,7 +1334,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1246
1334
  * Get recent event log
1247
1335
  */
1248
1336
  async getEventLog(ctx) {
1249
- const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
1337
+ const monitoringService = strapi2.plugin(pluginId$5).service("monitoring");
1250
1338
  const limit = parseInt(ctx.query.limit) || 50;
1251
1339
  const log = monitoringService.getEventLog(limit);
1252
1340
  ctx.body = { data: log };
@@ -1256,10 +1344,11 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1256
1344
  * Send a test event
1257
1345
  */
1258
1346
  async sendTestEvent(ctx) {
1259
- const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
1347
+ const monitoringService = strapi2.plugin(pluginId$5).service("monitoring");
1260
1348
  const { eventName, data } = ctx.request.body;
1261
1349
  try {
1262
- const result = monitoringService.sendTestEvent(eventName || "test", data || {});
1350
+ const safeName = (eventName || "test").replace(/[^a-zA-Z0-9:._-]/g, "").substring(0, 50);
1351
+ const result = monitoringService.sendTestEvent(safeName, data || {});
1263
1352
  ctx.body = { data: result };
1264
1353
  } catch (error2) {
1265
1354
  ctx.throw(500, error2.message);
@@ -1270,7 +1359,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1270
1359
  * Reset monitoring statistics
1271
1360
  */
1272
1361
  async resetStats(ctx) {
1273
- const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
1362
+ const monitoringService = strapi2.plugin(pluginId$5).service("monitoring");
1274
1363
  monitoringService.resetStats();
1275
1364
  ctx.body = { data: { success: true } };
1276
1365
  },
@@ -1294,7 +1383,7 @@ var settings$3 = ({ strapi: strapi2 }) => ({
1294
1383
  * Get lightweight stats for dashboard widget
1295
1384
  */
1296
1385
  async getMonitoringStats(ctx) {
1297
- const monitoringService = strapi2.plugin(pluginId$4).service("monitoring");
1386
+ const monitoringService = strapi2.plugin(pluginId$5).service("monitoring");
1298
1387
  const connectionStats = monitoringService.getConnectionStats();
1299
1388
  const eventStats = monitoringService.getEventStats();
1300
1389
  ctx.body = {
@@ -1320,10 +1409,11 @@ const refreshThrottle = /* @__PURE__ */ new Map();
1320
1409
  const SESSION_TTL = 10 * 60 * 1e3;
1321
1410
  const REFRESH_COOLDOWN = 3 * 1e3;
1322
1411
  const CLEANUP_INTERVAL = 2 * 60 * 1e3;
1412
+ let tokenCleanupInterval = null;
1323
1413
  const hashToken = (token) => {
1324
1414
  return createHash("sha256").update(token).digest("hex");
1325
1415
  };
1326
- setInterval(() => {
1416
+ const runTokenCleanup = () => {
1327
1417
  const now = Date.now();
1328
1418
  let cleaned = 0;
1329
1419
  for (const [tokenHash, session] of sessionTokens.entries()) {
@@ -1340,8 +1430,23 @@ setInterval(() => {
1340
1430
  if (cleaned > 0) {
1341
1431
  console.log(`[plugin-io] [CLEANUP] Removed ${cleaned} expired session tokens`);
1342
1432
  }
1343
- }, CLEANUP_INTERVAL);
1433
+ };
1434
+ const startTokenCleanup = () => {
1435
+ if (!tokenCleanupInterval) {
1436
+ tokenCleanupInterval = setInterval(runTokenCleanup, CLEANUP_INTERVAL);
1437
+ }
1438
+ };
1439
+ const stopTokenCleanup = () => {
1440
+ if (tokenCleanupInterval) {
1441
+ clearInterval(tokenCleanupInterval);
1442
+ tokenCleanupInterval = null;
1443
+ }
1444
+ };
1344
1445
  var presence$3 = ({ strapi: strapi2 }) => ({
1446
+ /**
1447
+ * Stops the background token cleanup interval (called on plugin destroy)
1448
+ */
1449
+ stopTokenCleanup,
1345
1450
  /**
1346
1451
  * Creates a session token for admin users to connect to Socket.IO
1347
1452
  * Implements rate limiting and secure token storage
@@ -1360,6 +1465,7 @@ var presence$3 = ({ strapi: strapi2 }) => ({
1360
1465
  strapi2.log.warn(`[plugin-io] Rate limit: User ${adminUser.id} must wait ${waitTime}s`);
1361
1466
  return ctx.tooManyRequests(`Please wait ${waitTime} seconds before requesting a new session`);
1362
1467
  }
1468
+ startTokenCleanup();
1363
1469
  try {
1364
1470
  const token = randomUUID();
1365
1471
  const tokenHash = hashToken(token);
@@ -1460,6 +1566,26 @@ var presence$3 = ({ strapi: strapi2 }) => ({
1460
1566
  strapi2.log.info(`[plugin-io] Invalidated ${invalidated} sessions for user ${userId}`);
1461
1567
  return invalidated;
1462
1568
  },
1569
+ /**
1570
+ * Returns all active (non-expired) admin sessions
1571
+ * Used by admin strategy for broadcasting to connected admin users
1572
+ * @returns {Array} Array of active session objects
1573
+ */
1574
+ getActiveSessions() {
1575
+ const now = Date.now();
1576
+ const activeSessions = [];
1577
+ for (const session of sessionTokens.values()) {
1578
+ if (session.expiresAt > now) {
1579
+ activeSessions.push({
1580
+ userId: session.userId,
1581
+ user: session.user,
1582
+ createdAt: session.createdAt,
1583
+ expiresAt: session.expiresAt
1584
+ });
1585
+ }
1586
+ }
1587
+ return activeSessions;
1588
+ },
1463
1589
  /**
1464
1590
  * Gets session statistics (for monitoring) - internal method
1465
1591
  * @returns {object} Session statistics
@@ -1931,7 +2057,7 @@ lodash_min.exports;
1931
2057
  function Q(n2) {
1932
2058
  return n2.match(Fr) || [];
1933
2059
  }
1934
- 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 = {};
2060
+ 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 = {};
1935
2061
  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;
1936
2062
  var Vr = {};
1937
2063
  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;
@@ -2784,7 +2910,21 @@ lodash_min.exports;
2784
2910
  return a2;
2785
2911
  }
2786
2912
  function yu(n2, t2) {
2787
- return t2 = ku(t2, n2), n2 = Gi(n2, t2), null == n2 || delete n2[no(jo(t2))];
2913
+ t2 = ku(t2, n2);
2914
+ var r2 = -1, e2 = t2.length;
2915
+ if (!e2) return true;
2916
+ for (var u2 = null == n2 || "object" != typeof n2 && "function" != typeof n2; ++r2 < e2; ) {
2917
+ var i2 = t2[r2];
2918
+ if ("string" == typeof i2) {
2919
+ if ("__proto__" === i2 && !bl.call(n2, "__proto__")) return false;
2920
+ if ("constructor" === i2 && r2 + 1 < e2 && "string" == typeof t2[r2 + 1] && "prototype" === t2[r2 + 1]) {
2921
+ if (u2 && 0 === r2) continue;
2922
+ return false;
2923
+ }
2924
+ }
2925
+ }
2926
+ var o2 = Gi(n2, t2);
2927
+ return null == o2 || delete o2[no(jo(t2))];
2788
2928
  }
2789
2929
  function du(n2, t2, r2, e2) {
2790
2930
  return fu(n2, t2, r2(_e2(n2, t2)), e2);
@@ -5636,7 +5776,7 @@ lodash.exports;
5636
5776
  (function(module, exports$1) {
5637
5777
  (function() {
5638
5778
  var undefined$1;
5639
- var VERSION = "4.17.21";
5779
+ var VERSION = "4.17.23";
5640
5780
  var LARGE_ARRAY_SIZE2 = 200;
5641
5781
  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`";
5642
5782
  var HASH_UNDEFINED2 = "__lodash_hash_undefined__";
@@ -7564,8 +7704,28 @@ lodash.exports;
7564
7704
  }
7565
7705
  function baseUnset(object2, path2) {
7566
7706
  path2 = castPath2(path2, object2);
7567
- object2 = parent(object2, path2);
7568
- return object2 == null || delete object2[toKey2(last(path2))];
7707
+ var index2 = -1, length = path2.length;
7708
+ if (!length) {
7709
+ return true;
7710
+ }
7711
+ var isRootPrimitive = object2 == null || typeof object2 !== "object" && typeof object2 !== "function";
7712
+ while (++index2 < length) {
7713
+ var key = path2[index2];
7714
+ if (typeof key !== "string") {
7715
+ continue;
7716
+ }
7717
+ if (key === "__proto__" && !hasOwnProperty2.call(object2, "__proto__")) {
7718
+ return false;
7719
+ }
7720
+ if (key === "constructor" && index2 + 1 < length && typeof path2[index2 + 1] === "string" && path2[index2 + 1] === "prototype") {
7721
+ if (isRootPrimitive && index2 === 0) {
7722
+ continue;
7723
+ }
7724
+ return false;
7725
+ }
7726
+ }
7727
+ var obj = parent(object2, path2);
7728
+ return obj == null || delete obj[toKey2(last(path2))];
7569
7729
  }
7570
7730
  function baseUpdate(object2, path2, updater, customizer) {
7571
7731
  return baseSet(object2, path2, updater(baseGet2(object2, path2)), customizer);
@@ -11218,73 +11378,76 @@ function envFn(key, defaultValue) {
11218
11378
  function getKey(key) {
11219
11379
  return process.env[key] ?? "";
11220
11380
  }
11221
- const utils$9 = {
11222
- int(key, defaultValue) {
11223
- if (!___default.has(process.env, key)) {
11224
- return defaultValue;
11225
- }
11226
- return parseInt(getKey(key), 10);
11227
- },
11228
- float(key, defaultValue) {
11229
- if (!___default.has(process.env, key)) {
11230
- return defaultValue;
11231
- }
11232
- return parseFloat(getKey(key));
11233
- },
11234
- bool(key, defaultValue) {
11235
- if (!___default.has(process.env, key)) {
11236
- return defaultValue;
11237
- }
11238
- return getKey(key) === "true";
11239
- },
11240
- json(key, defaultValue) {
11241
- if (!___default.has(process.env, key)) {
11242
- return defaultValue;
11243
- }
11244
- try {
11245
- return JSON.parse(getKey(key));
11246
- } catch (error2) {
11247
- if (error2 instanceof Error) {
11248
- throw new Error(`Invalid json environment variable ${key}: ${error2.message}`);
11249
- }
11250
- throw error2;
11251
- }
11252
- },
11253
- array(key, defaultValue) {
11254
- if (!___default.has(process.env, key)) {
11255
- return defaultValue;
11256
- }
11257
- let value = getKey(key);
11258
- if (value.startsWith("[") && value.endsWith("]")) {
11259
- value = value.substring(1, value.length - 1);
11260
- }
11261
- return value.split(",").map((v) => {
11262
- return ___default.trim(___default.trim(v, " "), '"');
11263
- });
11264
- },
11265
- date(key, defaultValue) {
11266
- if (!___default.has(process.env, key)) {
11267
- return defaultValue;
11268
- }
11269
- return new Date(getKey(key));
11270
- },
11271
- /**
11272
- * Gets a value from env that matches oneOf provided values
11273
- * @param {string} key
11274
- * @param {string[]} expectedValues
11275
- * @param {string|undefined} defaultValue
11276
- * @returns {string|undefined}
11277
- */
11278
- oneOf(key, expectedValues, defaultValue) {
11279
- if (!expectedValues) {
11280
- throw new Error(`env.oneOf requires expectedValues`);
11281
- }
11282
- if (defaultValue && !expectedValues.includes(defaultValue)) {
11283
- throw new Error(`env.oneOf requires defaultValue to be included in expectedValues`);
11381
+ function int$1(key, defaultValue) {
11382
+ if (!___default.has(process.env, key)) {
11383
+ return defaultValue;
11384
+ }
11385
+ return parseInt(getKey(key), 10);
11386
+ }
11387
+ function float$1(key, defaultValue) {
11388
+ if (!___default.has(process.env, key)) {
11389
+ return defaultValue;
11390
+ }
11391
+ return parseFloat(getKey(key));
11392
+ }
11393
+ function bool$1(key, defaultValue) {
11394
+ if (!___default.has(process.env, key)) {
11395
+ return defaultValue;
11396
+ }
11397
+ return getKey(key) === "true";
11398
+ }
11399
+ function json$1(key, defaultValue) {
11400
+ if (!___default.has(process.env, key)) {
11401
+ return defaultValue;
11402
+ }
11403
+ try {
11404
+ return JSON.parse(getKey(key));
11405
+ } catch (error2) {
11406
+ if (error2 instanceof Error) {
11407
+ throw new Error(`Invalid json environment variable ${key}: ${error2.message}`);
11284
11408
  }
11285
- const rawValue = env(key, defaultValue);
11286
- return expectedValues.includes(rawValue) ? rawValue : defaultValue;
11409
+ throw error2;
11410
+ }
11411
+ }
11412
+ function array$1(key, defaultValue) {
11413
+ if (!___default.has(process.env, key)) {
11414
+ return defaultValue;
11415
+ }
11416
+ let value = getKey(key);
11417
+ if (value.startsWith("[") && value.endsWith("]")) {
11418
+ value = value.substring(1, value.length - 1);
11419
+ }
11420
+ return value.split(",").map((v) => {
11421
+ return ___default.trim(___default.trim(v, " "), '"');
11422
+ });
11423
+ }
11424
+ function date$1(key, defaultValue) {
11425
+ if (!___default.has(process.env, key)) {
11426
+ return defaultValue;
11287
11427
  }
11428
+ return new Date(getKey(key));
11429
+ }
11430
+ function oneOf(key, expectedValues, defaultValue) {
11431
+ if (!expectedValues) {
11432
+ throw new Error(`env.oneOf requires expectedValues`);
11433
+ }
11434
+ if (defaultValue && !expectedValues.includes(defaultValue)) {
11435
+ throw new Error(`env.oneOf requires defaultValue to be included in expectedValues`);
11436
+ }
11437
+ const rawValue = env(key, defaultValue);
11438
+ if (rawValue !== void 0 && expectedValues.includes(rawValue)) {
11439
+ return rawValue;
11440
+ }
11441
+ return defaultValue;
11442
+ }
11443
+ const utils$9 = {
11444
+ int: int$1,
11445
+ float: float$1,
11446
+ bool: bool$1,
11447
+ json: json$1,
11448
+ array: array$1,
11449
+ date: date$1,
11450
+ oneOf
11288
11451
  };
11289
11452
  const env = Object.assign(envFn, utils$9);
11290
11453
  const SINGLE_TYPE = "singleType";
@@ -11309,6 +11472,22 @@ const constants$6 = {
11309
11472
  SINGLE_TYPE,
11310
11473
  COLLECTION_TYPE
11311
11474
  };
11475
+ const ID_FIELDS = [
11476
+ ID_ATTRIBUTE$4,
11477
+ DOC_ID_ATTRIBUTE$4
11478
+ ];
11479
+ const MORPH_TO_KEYS = [
11480
+ "__type"
11481
+ ];
11482
+ const DYNAMIC_ZONE_KEYS = [
11483
+ "__component"
11484
+ ];
11485
+ const RELATION_OPERATION_KEYS = [
11486
+ "connect",
11487
+ "disconnect",
11488
+ "set",
11489
+ "options"
11490
+ ];
11312
11491
  const getTimestamps = (model) => {
11313
11492
  const attributes = [];
11314
11493
  if (fp.has(CREATED_AT_ATTRIBUTE, model.attributes)) {
@@ -11412,10 +11591,10 @@ const HAS_RELATION_REORDERING = [
11412
11591
  "oneToMany"
11413
11592
  ];
11414
11593
  const hasRelationReordering = (attribute) => isRelationalAttribute(attribute) && HAS_RELATION_REORDERING.includes(attribute.relation);
11415
- const isComponentAttribute = (attribute) => [
11594
+ const isComponentAttribute = (attribute) => !!attribute && [
11416
11595
  "component",
11417
11596
  "dynamiczone"
11418
- ].includes(attribute?.type);
11597
+ ].includes(attribute.type);
11419
11598
  const isDynamicZoneAttribute = (attribute) => !!attribute && attribute.type === "dynamiczone";
11420
11599
  const isMorphToRelationalAttribute = (attribute) => {
11421
11600
  return !!attribute && isRelationalAttribute(attribute) && attribute.relation?.startsWith?.("morphTo");
@@ -11452,6 +11631,10 @@ const getContentTypeRoutePrefix = (contentType) => {
11452
11631
  };
11453
11632
  const contentTypes$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
11454
11633
  __proto__: null,
11634
+ DYNAMIC_ZONE_KEYS,
11635
+ ID_FIELDS,
11636
+ MORPH_TO_KEYS,
11637
+ RELATION_OPERATION_KEYS,
11455
11638
  constants: constants$6,
11456
11639
  getComponentAttributes,
11457
11640
  getContentTypeRoutePrefix,
@@ -11637,12 +11820,22 @@ const providerFactory = (options = {}) => {
11637
11820
  }
11638
11821
  };
11639
11822
  };
11823
+ const parallelWithOrderedErrors = async (promises) => {
11824
+ const results = await Promise.allSettled(promises);
11825
+ for (let i = 0; i < results.length; i += 1) {
11826
+ const result = results[i];
11827
+ if (result.status === "rejected") {
11828
+ throw result.reason;
11829
+ }
11830
+ }
11831
+ return results.map((r) => r.value);
11832
+ };
11640
11833
  const traverseEntity = async (visitor2, options, entity) => {
11641
11834
  const { path: path2 = {
11642
11835
  raw: null,
11643
11836
  attribute: null,
11644
11837
  rawWithIndices: null
11645
- }, schema: schema2, getModel } = options;
11838
+ }, schema: schema2, getModel, allowedExtraRootKeys } = options;
11646
11839
  let parent = options.parent;
11647
11840
  const traverseMorphRelationTarget = async (visitor3, path3, entry) => {
11648
11841
  const targetSchema = getModel(entry.__type);
@@ -11650,7 +11843,8 @@ const traverseEntity = async (visitor2, options, entity) => {
11650
11843
  schema: targetSchema,
11651
11844
  path: path3,
11652
11845
  getModel,
11653
- parent
11846
+ parent,
11847
+ allowedExtraRootKeys
11654
11848
  };
11655
11849
  return traverseEntity(visitor3, traverseOptions, entry);
11656
11850
  };
@@ -11659,7 +11853,8 @@ const traverseEntity = async (visitor2, options, entity) => {
11659
11853
  schema: schema3,
11660
11854
  path: path3,
11661
11855
  getModel,
11662
- parent
11856
+ parent,
11857
+ allowedExtraRootKeys
11663
11858
  };
11664
11859
  return traverseEntity(visitor3, traverseOptions, entry);
11665
11860
  };
@@ -11670,7 +11865,8 @@ const traverseEntity = async (visitor2, options, entity) => {
11670
11865
  schema: targetSchema,
11671
11866
  path: path3,
11672
11867
  getModel,
11673
- parent
11868
+ parent,
11869
+ allowedExtraRootKeys
11674
11870
  };
11675
11871
  return traverseEntity(visitor3, traverseOptions, entry);
11676
11872
  };
@@ -11679,7 +11875,8 @@ const traverseEntity = async (visitor2, options, entity) => {
11679
11875
  schema: schema3,
11680
11876
  path: path3,
11681
11877
  getModel,
11682
- parent
11878
+ parent,
11879
+ allowedExtraRootKeys
11683
11880
  };
11684
11881
  return traverseEntity(visitor3, traverseOptions, entry);
11685
11882
  };
@@ -11689,7 +11886,8 @@ const traverseEntity = async (visitor2, options, entity) => {
11689
11886
  schema: targetSchema,
11690
11887
  path: path3,
11691
11888
  getModel,
11692
- parent
11889
+ parent,
11890
+ allowedExtraRootKeys
11693
11891
  };
11694
11892
  return traverseEntity(visitor3, traverseOptions, entry);
11695
11893
  };
@@ -11720,7 +11918,8 @@ const traverseEntity = async (visitor2, options, entity) => {
11720
11918
  attribute,
11721
11919
  path: newPath,
11722
11920
  getModel,
11723
- parent
11921
+ parent,
11922
+ allowedExtraRootKeys
11724
11923
  };
11725
11924
  await visitor2(visitorOptions, visitorUtils);
11726
11925
  const value = copy[key];
@@ -11737,15 +11936,13 @@ const traverseEntity = async (visitor2, options, entity) => {
11737
11936
  const isMorphRelation = attribute.relation.toLowerCase().startsWith("morph");
11738
11937
  const method = isMorphRelation ? traverseMorphRelationTarget : traverseRelationTarget(getModel(attribute.target));
11739
11938
  if (fp.isArray(value)) {
11740
- const res = new Array(value.length);
11741
- for (let i2 = 0; i2 < value.length; i2 += 1) {
11939
+ copy[key] = await parallelWithOrderedErrors(value.map((item, i2) => {
11742
11940
  const arrayPath = {
11743
11941
  ...newPath,
11744
11942
  rawWithIndices: fp.isNil(newPath.rawWithIndices) ? `${i2}` : `${newPath.rawWithIndices}.${i2}`
11745
11943
  };
11746
- res[i2] = await method(visitor2, arrayPath, value[i2]);
11747
- }
11748
- copy[key] = res;
11944
+ return method(visitor2, arrayPath, item);
11945
+ }));
11749
11946
  } else {
11750
11947
  copy[key] = await method(visitor2, newPath, value);
11751
11948
  }
@@ -11759,15 +11956,13 @@ const traverseEntity = async (visitor2, options, entity) => {
11759
11956
  path: newPath
11760
11957
  };
11761
11958
  if (fp.isArray(value)) {
11762
- const res = new Array(value.length);
11763
- for (let i2 = 0; i2 < value.length; i2 += 1) {
11959
+ copy[key] = await parallelWithOrderedErrors(value.map((item, i2) => {
11764
11960
  const arrayPath = {
11765
11961
  ...newPath,
11766
11962
  rawWithIndices: fp.isNil(newPath.rawWithIndices) ? `${i2}` : `${newPath.rawWithIndices}.${i2}`
11767
11963
  };
11768
- res[i2] = await traverseMediaTarget(visitor2, arrayPath, value[i2]);
11769
- }
11770
- copy[key] = res;
11964
+ return traverseMediaTarget(visitor2, arrayPath, item);
11965
+ }));
11771
11966
  } else {
11772
11967
  copy[key] = await traverseMediaTarget(visitor2, newPath, value);
11773
11968
  }
@@ -11782,15 +11977,13 @@ const traverseEntity = async (visitor2, options, entity) => {
11782
11977
  };
11783
11978
  const targetSchema = getModel(attribute.component);
11784
11979
  if (fp.isArray(value)) {
11785
- const res = new Array(value.length);
11786
- for (let i2 = 0; i2 < value.length; i2 += 1) {
11980
+ copy[key] = await parallelWithOrderedErrors(value.map((item, i2) => {
11787
11981
  const arrayPath = {
11788
11982
  ...newPath,
11789
11983
  rawWithIndices: fp.isNil(newPath.rawWithIndices) ? `${i2}` : `${newPath.rawWithIndices}.${i2}`
11790
11984
  };
11791
- res[i2] = await traverseComponent(visitor2, arrayPath, targetSchema, value[i2]);
11792
- }
11793
- copy[key] = res;
11985
+ return traverseComponent(visitor2, arrayPath, targetSchema, item);
11986
+ }));
11794
11987
  } else {
11795
11988
  copy[key] = await traverseComponent(visitor2, newPath, targetSchema, value);
11796
11989
  }
@@ -11803,15 +11996,13 @@ const traverseEntity = async (visitor2, options, entity) => {
11803
11996
  attribute,
11804
11997
  path: newPath
11805
11998
  };
11806
- const res = new Array(value.length);
11807
- for (let i2 = 0; i2 < value.length; i2 += 1) {
11999
+ copy[key] = await parallelWithOrderedErrors(value.map((item, i2) => {
11808
12000
  const arrayPath = {
11809
12001
  ...newPath,
11810
12002
  rawWithIndices: fp.isNil(newPath.rawWithIndices) ? `${i2}` : `${newPath.rawWithIndices}.${i2}`
11811
12003
  };
11812
- res[i2] = await visitDynamicZoneEntry(visitor2, arrayPath, value[i2]);
11813
- }
11814
- copy[key] = res;
12004
+ return visitDynamicZoneEntry(visitor2, arrayPath, item);
12005
+ }));
11815
12006
  continue;
11816
12007
  }
11817
12008
  }
@@ -12557,6 +12748,23 @@ const generateInstallId = (projectId, installId) => {
12557
12748
  return require$$1.randomUUID();
12558
12749
  }
12559
12750
  };
12751
+ const createModelCache = (getModelFn) => {
12752
+ const cache = /* @__PURE__ */ new Map();
12753
+ return {
12754
+ getModel(uid) {
12755
+ const cached = cache.get(uid);
12756
+ if (cached) {
12757
+ return cached;
12758
+ }
12759
+ const model = getModelFn(uid);
12760
+ cache.set(uid, model);
12761
+ return model;
12762
+ },
12763
+ clear() {
12764
+ cache.clear();
12765
+ }
12766
+ };
12767
+ };
12560
12768
  var map$2;
12561
12769
  try {
12562
12770
  map$2 = Map;
@@ -14056,27 +14264,11 @@ Cache.prototype.set = function(key, value) {
14056
14264
  return this._values[key] = value;
14057
14265
  };
14058
14266
  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;
14059
- var pathCache = new Cache(MAX_CACHE_SIZE), setCache = new Cache(MAX_CACHE_SIZE), getCache = new Cache(MAX_CACHE_SIZE);
14267
+ var pathCache = new Cache(MAX_CACHE_SIZE);
14268
+ new Cache(MAX_CACHE_SIZE);
14269
+ var getCache = new Cache(MAX_CACHE_SIZE);
14060
14270
  var propertyExpr = {
14061
- Cache,
14062
14271
  split,
14063
- normalizePath,
14064
- setter: function(path2) {
14065
- var parts = normalizePath(path2);
14066
- return setCache.get(path2) || setCache.set(path2, function setter(obj, value) {
14067
- var index2 = 0;
14068
- var len = parts.length;
14069
- var data = obj;
14070
- while (index2 < len - 1) {
14071
- var part = parts[index2];
14072
- if (part === "__proto__" || part === "constructor" || part === "prototype") {
14073
- return obj;
14074
- }
14075
- data = data[parts[index2++]];
14076
- }
14077
- data[parts[index2]] = value;
14078
- });
14079
- },
14080
14272
  getter: function(path2, safe) {
14081
14273
  var parts = normalizePath(path2);
14082
14274
  return getCache.get(path2) || getCache.set(path2, function getter(data) {
@@ -14088,11 +14280,6 @@ var propertyExpr = {
14088
14280
  return data;
14089
14281
  });
14090
14282
  },
14091
- join: function(segments) {
14092
- return segments.reduce(function(path2, part) {
14093
- return path2 + (isQuoted(part) || DIGIT_REGEX.test(part) ? "[" + part + "]" : (path2 ? "." : "") + part);
14094
- }, "");
14095
- },
14096
14283
  forEach: function(path2, cb, thisArg) {
14097
14284
  forEach(Array.isArray(path2) ? path2 : split(path2), cb, thisArg);
14098
14285
  }
@@ -16922,10 +17109,10 @@ const createTransformer = ({ getModel }) => {
16922
17109
  }
16923
17110
  return pageVal;
16924
17111
  };
16925
- const convertPageSizeQueryParams = (pageSize, page) => {
17112
+ const convertPageSizeQueryParams = (pageSize, _page) => {
16926
17113
  const pageSizeVal = fp.toNumber(pageSize);
16927
17114
  if (!fp.isInteger(pageSizeVal) || pageSizeVal <= 0) {
16928
- throw new PaginationError(`Invalid 'pageSize' parameter. Expected an integer > 0, received: ${page}`);
17115
+ throw new PaginationError(`Invalid 'pageSize' parameter. Expected an integer > 0, received: ${pageSize}`);
16929
17116
  }
16930
17117
  return pageSizeVal;
16931
17118
  };
@@ -17100,7 +17287,7 @@ const createTransformer = ({ getModel }) => {
17100
17287
  query.page = convertPageQueryParams(page);
17101
17288
  }
17102
17289
  if (!fp.isNil(pageSize)) {
17103
- query.pageSize = convertPageSizeQueryParams(pageSize, page);
17290
+ query.pageSize = convertPageSizeQueryParams(pageSize);
17104
17291
  }
17105
17292
  if (!fp.isNil(start)) {
17106
17293
  query.offset = convertStartQueryParams(start);
@@ -17248,7 +17435,7 @@ const createTransformer = ({ getModel }) => {
17248
17435
  query.page = convertPageQueryParams(page);
17249
17436
  }
17250
17437
  if (!fp.isNil(pageSize)) {
17251
- query.pageSize = convertPageSizeQueryParams(pageSize, page);
17438
+ query.pageSize = convertPageSizeQueryParams(pageSize);
17252
17439
  }
17253
17440
  if (!fp.isNil(start)) {
17254
17441
  query.offset = convertStartQueryParams(start);
@@ -17275,6 +17462,43 @@ const convertQueryParams = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.
17275
17462
  __proto__: null,
17276
17463
  createTransformer
17277
17464
  }, Symbol.toStringTag, { value: "Module" }));
17465
+ const SHARED_QUERY_PARAM_KEYS = [
17466
+ "filters",
17467
+ "sort",
17468
+ "fields",
17469
+ "populate",
17470
+ "status",
17471
+ "locale",
17472
+ "page",
17473
+ "pageSize",
17474
+ "start",
17475
+ "limit",
17476
+ "_q",
17477
+ "hasPublishedVersion"
17478
+ ];
17479
+ const ALLOWED_QUERY_PARAM_KEYS = [
17480
+ ...SHARED_QUERY_PARAM_KEYS,
17481
+ "pagination",
17482
+ "count",
17483
+ "ordering"
17484
+ ];
17485
+ const RESERVED_INPUT_PARAM_KEYS = [
17486
+ constants$6.ID_ATTRIBUTE,
17487
+ constants$6.DOC_ID_ATTRIBUTE
17488
+ ];
17489
+ function getExtraQueryKeysFromRoute(route) {
17490
+ if (!route?.request?.query) return [];
17491
+ const coreKeys = new Set(ALLOWED_QUERY_PARAM_KEYS);
17492
+ return Object.keys(route.request.query).filter((key) => !coreKeys.has(key));
17493
+ }
17494
+ function getExtraRootKeysFromRouteBody(route) {
17495
+ const bodySchema = route?.request?.body?.["application/json"];
17496
+ if (!bodySchema || typeof bodySchema !== "object") return [];
17497
+ if ("shape" in bodySchema && typeof bodySchema.shape === "object") {
17498
+ return Object.keys(bodySchema.shape);
17499
+ }
17500
+ return [];
17501
+ }
17278
17502
  var indentString$2 = (string2, count = 1, options) => {
17279
17503
  options = {
17280
17504
  indent: " ",
@@ -17662,6 +17886,35 @@ var removeRestrictedFields = (restrictedFields = null) => ({ key, path: { attrib
17662
17886
  remove(key);
17663
17887
  }
17664
17888
  };
17889
+ const removeUnrecognizedFields = ({ key, attribute, path: path2, schema: schema2, parent, allowedExtraRootKeys }, { remove }) => {
17890
+ if (attribute) {
17891
+ return;
17892
+ }
17893
+ if (path2.attribute === null) {
17894
+ if (ID_FIELDS.includes(key)) {
17895
+ return;
17896
+ }
17897
+ if (allowedExtraRootKeys?.includes(key)) {
17898
+ return;
17899
+ }
17900
+ remove(key);
17901
+ return;
17902
+ }
17903
+ if (isMorphToRelationalAttribute(parent?.attribute) && MORPH_TO_KEYS.includes(key)) {
17904
+ return;
17905
+ }
17906
+ if (isComponentSchema(schema2) && isDynamicZoneAttribute(parent?.attribute) && DYNAMIC_ZONE_KEYS.includes(key)) {
17907
+ return;
17908
+ }
17909
+ if ((isRelationalAttribute(parent?.attribute) || isMediaAttribute(parent?.attribute)) && RELATION_OPERATION_KEYS.includes(key)) {
17910
+ return;
17911
+ }
17912
+ const canUseID = isRelationalAttribute(parent?.attribute) || isMediaAttribute(parent?.attribute) || isComponentAttribute(parent?.attribute);
17913
+ if (canUseID && ID_FIELDS.includes(key)) {
17914
+ return;
17915
+ }
17916
+ remove(key);
17917
+ };
17665
17918
  const visitor$4 = ({ schema: schema2, key, value }, { set: set2 }) => {
17666
17919
  if (key === "" && value === "*") {
17667
17920
  const { attributes } = schema2;
@@ -17686,7 +17939,8 @@ const index$5 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.definePrope
17686
17939
  removePassword: visitor$8,
17687
17940
  removePrivate: visitor$7,
17688
17941
  removeRestrictedFields,
17689
- removeRestrictedRelations
17942
+ removeRestrictedRelations,
17943
+ removeUnrecognizedFields
17690
17944
  }, Symbol.toStringTag, { value: "Module" }));
17691
17945
  const DEFAULT_PATH = {
17692
17946
  raw: null,
@@ -18537,15 +18791,18 @@ const sanitizers = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.definePr
18537
18791
  }, Symbol.toStringTag, { value: "Module" }));
18538
18792
  const createAPISanitizers = (opts) => {
18539
18793
  const { getModel } = opts;
18540
- const sanitizeInput = (data, schema2, { auth } = {}) => {
18794
+ const sanitizeInput = (data, schema2, { auth, strictParams = false, route } = {}) => {
18541
18795
  if (!schema2) {
18542
18796
  throw new Error("Missing schema in sanitizeInput");
18543
18797
  }
18544
18798
  if (fp.isArray(data)) {
18545
18799
  return Promise.all(data.map((entry) => sanitizeInput(entry, schema2, {
18546
- auth
18800
+ auth,
18801
+ strictParams,
18802
+ route
18547
18803
  })));
18548
18804
  }
18805
+ const allowedExtraRootKeys = getExtraRootKeysFromRouteBody(route);
18549
18806
  const nonWritableAttributes = getNonWritableAttributes(schema2);
18550
18807
  const transforms = [
18551
18808
  // Remove first level ID in inputs
@@ -18557,6 +18814,13 @@ const createAPISanitizers = (opts) => {
18557
18814
  getModel
18558
18815
  })
18559
18816
  ];
18817
+ if (strictParams) {
18818
+ transforms.push(traverseEntity$1(removeUnrecognizedFields, {
18819
+ schema: schema2,
18820
+ getModel,
18821
+ allowedExtraRootKeys
18822
+ }));
18823
+ }
18560
18824
  if (auth) {
18561
18825
  transforms.push(traverseEntity$1(removeRestrictedRelations(auth), {
18562
18826
  schema: schema2,
@@ -18564,6 +18828,28 @@ const createAPISanitizers = (opts) => {
18564
18828
  }));
18565
18829
  }
18566
18830
  opts?.sanitizers?.input?.forEach((sanitizer) => transforms.push(sanitizer(schema2)));
18831
+ const routeBodySanitizeTransform = async (data2) => {
18832
+ if (!data2 || typeof data2 !== "object" || Array.isArray(data2)) return data2;
18833
+ const obj = data2;
18834
+ const bodySchema = route?.request?.body?.["application/json"];
18835
+ if (bodySchema && typeof bodySchema === "object" && "shape" in bodySchema) {
18836
+ const shape = bodySchema.shape;
18837
+ for (const key of Object.keys(shape)) {
18838
+ if (key === "data" || !(key in obj)) continue;
18839
+ const zodSchema = shape[key];
18840
+ if (zodSchema && typeof zodSchema.safeParse === "function") {
18841
+ const result = zodSchema.safeParse(obj[key]);
18842
+ if (result.success) {
18843
+ obj[key] = result.data;
18844
+ } else {
18845
+ delete obj[key];
18846
+ }
18847
+ }
18848
+ }
18849
+ }
18850
+ return data2;
18851
+ };
18852
+ transforms.push(routeBodySanitizeTransform);
18567
18853
  return pipe$1(...transforms)(data);
18568
18854
  };
18569
18855
  const sanitizeOutput = async (data, schema2, { auth } = {}) => {
@@ -18594,7 +18880,7 @@ const createAPISanitizers = (opts) => {
18594
18880
  opts?.sanitizers?.output?.forEach((sanitizer) => transforms.push(sanitizer(schema2)));
18595
18881
  return pipe$1(...transforms)(data);
18596
18882
  };
18597
- const sanitizeQuery = async (query, schema2, { auth } = {}) => {
18883
+ const sanitizeQuery = async (query, schema2, { auth, strictParams = false, route } = {}) => {
18598
18884
  if (!schema2) {
18599
18885
  throw new Error("Missing schema in sanitizeQuery");
18600
18886
  }
@@ -18624,6 +18910,30 @@ const createAPISanitizers = (opts) => {
18624
18910
  populate: await sanitizePopulate(populate2, schema2)
18625
18911
  });
18626
18912
  }
18913
+ const extraQueryKeys = getExtraQueryKeysFromRoute(route);
18914
+ const routeQuerySchema = route?.request?.query;
18915
+ if (routeQuerySchema) {
18916
+ for (const key of extraQueryKeys) {
18917
+ if (key in query) {
18918
+ const zodSchema = routeQuerySchema[key];
18919
+ if (zodSchema && typeof zodSchema.safeParse === "function") {
18920
+ const result = zodSchema.safeParse(query[key]);
18921
+ if (result.success) {
18922
+ sanitizedQuery[key] = result.data;
18923
+ } else {
18924
+ delete sanitizedQuery[key];
18925
+ }
18926
+ }
18927
+ }
18928
+ }
18929
+ }
18930
+ if (strictParams) {
18931
+ const allowedKeys = [
18932
+ ...ALLOWED_QUERY_PARAM_KEYS,
18933
+ ...extraQueryKeys
18934
+ ];
18935
+ return fp.pick(allowedKeys, sanitizedQuery);
18936
+ }
18627
18937
  return sanitizedQuery;
18628
18938
  };
18629
18939
  const sanitizeFilters = (filters2, schema2, { auth } = {}) => {
@@ -18926,54 +19236,38 @@ var throwRestrictedFields = (restrictedFields = null) => ({ key, path: { attribu
18926
19236
  });
18927
19237
  }
18928
19238
  };
18929
- const ID_FIELDS = [
18930
- constants$6.DOC_ID_ATTRIBUTE,
18931
- constants$6.DOC_ID_ATTRIBUTE
18932
- ];
18933
- const ALLOWED_ROOT_LEVEL_FIELDS = [
18934
- ...ID_FIELDS
18935
- ];
18936
- const MORPH_TO_ALLOWED_FIELDS = [
18937
- "__type"
18938
- ];
18939
- const DYNAMIC_ZONE_ALLOWED_FIELDS = [
18940
- "__component"
18941
- ];
18942
- const RELATION_REORDERING_FIELDS = [
18943
- "connect",
18944
- "disconnect",
18945
- "set",
18946
- "options"
18947
- ];
18948
- const throwUnrecognizedFields = ({ key, attribute, path: path2, schema: schema2, parent }) => {
19239
+ const throwUnrecognizedFields = ({ key, attribute, path: path2, schema: schema2, parent, allowedExtraRootKeys }, _visitorUtils) => {
18949
19240
  if (attribute) {
18950
19241
  return;
18951
19242
  }
18952
19243
  if (path2.attribute === null) {
18953
- if (ALLOWED_ROOT_LEVEL_FIELDS.includes(key)) {
19244
+ if (ID_FIELDS.includes(key)) {
19245
+ return;
19246
+ }
19247
+ if (allowedExtraRootKeys?.includes(key)) {
18954
19248
  return;
18955
19249
  }
18956
19250
  return throwInvalidKey({
18957
19251
  key,
18958
- path: attribute
19252
+ path: path2.attribute
18959
19253
  });
18960
19254
  }
18961
- if (isMorphToRelationalAttribute(parent?.attribute) && MORPH_TO_ALLOWED_FIELDS.includes(key)) {
19255
+ if (isMorphToRelationalAttribute(parent?.attribute) && MORPH_TO_KEYS.includes(key)) {
18962
19256
  return;
18963
19257
  }
18964
- if (isComponentSchema(schema2) && isDynamicZoneAttribute(parent?.attribute) && DYNAMIC_ZONE_ALLOWED_FIELDS.includes(key)) {
19258
+ if (isComponentSchema(schema2) && isDynamicZoneAttribute(parent?.attribute) && DYNAMIC_ZONE_KEYS.includes(key)) {
18965
19259
  return;
18966
19260
  }
18967
- if (hasRelationReordering(parent?.attribute) && RELATION_REORDERING_FIELDS.includes(key)) {
19261
+ if ((isRelationalAttribute(parent?.attribute) || isMediaAttribute(parent?.attribute)) && RELATION_OPERATION_KEYS.includes(key)) {
18968
19262
  return;
18969
19263
  }
18970
- const canUseID = isRelationalAttribute(parent?.attribute) || isMediaAttribute(parent?.attribute);
18971
- if (canUseID && !ID_FIELDS.includes(key)) {
19264
+ const canUseID = isRelationalAttribute(parent?.attribute) || isMediaAttribute(parent?.attribute) || isComponentAttribute(parent?.attribute);
19265
+ if (canUseID && ID_FIELDS.includes(key)) {
18972
19266
  return;
18973
19267
  }
18974
19268
  throwInvalidKey({
18975
19269
  key,
18976
- path: attribute
19270
+ path: path2.attribute
18977
19271
  });
18978
19272
  };
18979
19273
  const index$3 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
@@ -19295,16 +19589,16 @@ const validators = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.definePr
19295
19589
  const { ID_ATTRIBUTE, DOC_ID_ATTRIBUTE } = constants$6;
19296
19590
  const createAPIValidators = (opts) => {
19297
19591
  const { getModel } = opts || {};
19298
- const validateInput = async (data, schema2, { auth } = {}) => {
19592
+ const validateInput = async (data, schema2, options = {}) => {
19593
+ const { auth, route } = options;
19299
19594
  if (!schema2) {
19300
19595
  throw new Error("Missing schema in validateInput");
19301
19596
  }
19302
19597
  if (fp.isArray(data)) {
19303
- await Promise.all(data.map((entry) => validateInput(entry, schema2, {
19304
- auth
19305
- })));
19598
+ await Promise.all(data.map((entry) => validateInput(entry, schema2, options)));
19306
19599
  return;
19307
19600
  }
19601
+ const allowedExtraRootKeys = getExtraRootKeysFromRouteBody(route);
19308
19602
  const nonWritableAttributes = getNonWritableAttributes(schema2);
19309
19603
  const transforms = [
19310
19604
  (data2) => {
@@ -19327,10 +19621,11 @@ const createAPIValidators = (opts) => {
19327
19621
  schema: schema2,
19328
19622
  getModel
19329
19623
  }),
19330
- // unrecognized attributes
19624
+ // unrecognized attributes (allowedExtraRootKeys = registered input param keys)
19331
19625
  traverseEntity$1(throwUnrecognizedFields, {
19332
19626
  schema: schema2,
19333
- getModel
19627
+ getModel,
19628
+ allowedExtraRootKeys
19334
19629
  })
19335
19630
  ];
19336
19631
  if (auth) {
@@ -19342,6 +19637,28 @@ const createAPIValidators = (opts) => {
19342
19637
  opts?.validators?.input?.forEach((validator) => transforms.push(validator(schema2)));
19343
19638
  try {
19344
19639
  await pipe$1(...transforms)(data);
19640
+ if (fp.isObject(data) && route?.request?.body?.["application/json"]) {
19641
+ const bodySchema = route.request.body["application/json"];
19642
+ if (typeof bodySchema === "object" && "shape" in bodySchema) {
19643
+ const shape = bodySchema.shape;
19644
+ const dataObj = data;
19645
+ for (const key of Object.keys(shape)) {
19646
+ if (key === "data" || !(key in dataObj)) continue;
19647
+ const zodSchema = shape[key];
19648
+ if (zodSchema && typeof zodSchema.parse === "function") {
19649
+ const result = zodSchema.safeParse(dataObj[key]);
19650
+ if (!result.success) {
19651
+ throw new ValidationError2(result.error?.message ?? "Validation failed", {
19652
+ key,
19653
+ path: null,
19654
+ source: "body",
19655
+ param: key
19656
+ });
19657
+ }
19658
+ }
19659
+ }
19660
+ }
19661
+ }
19345
19662
  } catch (e) {
19346
19663
  if (e instanceof ValidationError2) {
19347
19664
  e.details.source = "body";
@@ -19349,10 +19666,52 @@ const createAPIValidators = (opts) => {
19349
19666
  throw e;
19350
19667
  }
19351
19668
  };
19352
- const validateQuery = async (query, schema2, { auth } = {}) => {
19669
+ const validateQuery = async (query, schema2, { auth, strictParams = false, route } = {}) => {
19353
19670
  if (!schema2) {
19354
19671
  throw new Error("Missing schema in validateQuery");
19355
19672
  }
19673
+ if (strictParams) {
19674
+ const extraQueryKeys = getExtraQueryKeysFromRoute(route);
19675
+ const allowedKeys = [
19676
+ ...ALLOWED_QUERY_PARAM_KEYS,
19677
+ ...extraQueryKeys
19678
+ ];
19679
+ for (const key of Object.keys(query)) {
19680
+ if (!allowedKeys.includes(key)) {
19681
+ try {
19682
+ throwInvalidKey({
19683
+ key,
19684
+ path: null
19685
+ });
19686
+ } catch (e) {
19687
+ if (e instanceof ValidationError2) {
19688
+ e.details.source = "query";
19689
+ e.details.param = key;
19690
+ }
19691
+ throw e;
19692
+ }
19693
+ }
19694
+ }
19695
+ const routeQuerySchema = route?.request?.query;
19696
+ if (routeQuerySchema) {
19697
+ for (const key of extraQueryKeys) {
19698
+ if (key in query) {
19699
+ const zodSchema = routeQuerySchema[key];
19700
+ if (zodSchema && typeof zodSchema.parse === "function") {
19701
+ const result = zodSchema.safeParse(query[key]);
19702
+ if (!result.success) {
19703
+ throw new ValidationError2(result.error?.message ?? "Invalid query param", {
19704
+ key,
19705
+ path: null,
19706
+ source: "query",
19707
+ param: key
19708
+ });
19709
+ }
19710
+ }
19711
+ }
19712
+ }
19713
+ }
19714
+ }
19356
19715
  const { filters: filters2, sort: sort2, fields: fields2, populate: populate2 } = query;
19357
19716
  if (filters2) {
19358
19717
  await validateFilters2(filters2, schema2, {
@@ -28088,7 +28447,14 @@ var preferredPm = async function preferredPM(pkgPath) {
28088
28447
  };
28089
28448
  }
28090
28449
  try {
28091
- if (typeof findYarnWorkspaceRoot(pkgPath) === "string") {
28450
+ const workspaceRoot = findYarnWorkspaceRoot(pkgPath);
28451
+ if (typeof workspaceRoot === "string") {
28452
+ if (await pathExists(path.join(workspaceRoot, "package-lock.json"))) {
28453
+ return {
28454
+ name: "npm",
28455
+ version: ">=7"
28456
+ };
28457
+ }
28092
28458
  return {
28093
28459
  name: "yarn",
28094
28460
  version: "*"
@@ -29814,13 +30180,17 @@ const extendMiddlewareConfiguration = (middlewares2, middleware2) => {
29814
30180
  };
29815
30181
  const dist = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
29816
30182
  __proto__: null,
30183
+ ALLOWED_QUERY_PARAM_KEYS,
29817
30184
  AbstractRouteValidator,
29818
30185
  CSP_DEFAULTS,
30186
+ RESERVED_INPUT_PARAM_KEYS,
30187
+ SHARED_QUERY_PARAM_KEYS,
29819
30188
  arrays,
29820
30189
  async,
29821
30190
  augmentSchema,
29822
30191
  contentTypes: contentTypes$1,
29823
30192
  createContentApiRoutesFactory,
30193
+ createModelCache,
29824
30194
  dates,
29825
30195
  env,
29826
30196
  errors,
@@ -29866,7 +30236,8 @@ const dist = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty
29866
30236
  validateYupSchema,
29867
30237
  validateYupSchemaSync,
29868
30238
  validateZod,
29869
- yup
30239
+ yup,
30240
+ z: z$2
29870
30241
  }, Symbol.toStringTag, { value: "Module" }));
29871
30242
  const require$$0 = /* @__PURE__ */ getAugmentedNamespace(dist);
29872
30243
  const { castArray, isNil: isNil$1, pipe, every } = fp;
@@ -29936,6 +30307,40 @@ var strategies = ({ strapi: strapi2 }) => {
29936
30307
  },
29937
30308
  getRoomName: function(user) {
29938
30309
  return `${this.name}-user-${user.id}`;
30310
+ },
30311
+ /**
30312
+ * Admin users have full access - verify always passes
30313
+ * @param {object} auth - Auth object
30314
+ * @param {object} config - Config with scope
30315
+ */
30316
+ verify: function(auth, config2) {
30317
+ return;
30318
+ },
30319
+ /**
30320
+ * Returns active admin sessions for broadcast
30321
+ * Admin users have full access, so we return them with full_access permissions
30322
+ * @returns {Promise<Array>} Array of admin session objects with permissions
30323
+ */
30324
+ getRooms: async function() {
30325
+ try {
30326
+ const presenceController = strapi2.plugin("io").controller("presence");
30327
+ if (!presenceController?.getActiveSessions) {
30328
+ return [];
30329
+ }
30330
+ const activeSessions = presenceController.getActiveSessions();
30331
+ return activeSessions.map((session) => ({
30332
+ id: session.userId,
30333
+ name: `admin-${session.userId}`,
30334
+ type: "full-access",
30335
+ // Grants full access to all content types
30336
+ permissions: [],
30337
+ // Empty permissions array - full-access type bypasses permission check
30338
+ ...session
30339
+ }));
30340
+ } catch (error2) {
30341
+ strapi2.log.warn("[plugin-io] Admin getRooms error:", error2.message);
30342
+ return [];
30343
+ }
29939
30344
  }
29940
30345
  };
29941
30346
  const role = {
@@ -30174,12 +30579,12 @@ function transformEntry(entry, type2) {
30174
30579
  // meta: {},
30175
30580
  };
30176
30581
  }
30177
- const { pluginId: pluginId$3 } = pluginId_1;
30582
+ const { pluginId: pluginId$4 } = pluginId_1;
30178
30583
  var settings$1 = ({ strapi: strapi2 }) => {
30179
30584
  const getPluginStore = () => {
30180
30585
  return strapi2.store({
30181
30586
  type: "plugin",
30182
- name: pluginId$3
30587
+ name: pluginId$4
30183
30588
  });
30184
30589
  };
30185
30590
  const getDefaultSettings = () => ({
@@ -30319,6 +30724,19 @@ var settings$1 = ({ strapi: strapi2 }) => {
30319
30724
  // Maximum nesting depth for diff
30320
30725
  }
30321
30726
  });
30727
+ const deepMerge = (target, source) => {
30728
+ const result = { ...target };
30729
+ for (const key of Object.keys(source)) {
30730
+ const targetVal = target[key];
30731
+ const sourceVal = source[key];
30732
+ if (targetVal && sourceVal && typeof targetVal === "object" && !Array.isArray(targetVal) && typeof sourceVal === "object" && !Array.isArray(sourceVal)) {
30733
+ result[key] = deepMerge(targetVal, sourceVal);
30734
+ } else {
30735
+ result[key] = sourceVal;
30736
+ }
30737
+ }
30738
+ return result;
30739
+ };
30322
30740
  return {
30323
30741
  /**
30324
30742
  * Get current settings (merged with defaults)
@@ -30341,10 +30759,7 @@ var settings$1 = ({ strapi: strapi2 }) => {
30341
30759
  async setSettings(newSettings) {
30342
30760
  const pluginStore = getPluginStore();
30343
30761
  const currentSettings = await this.getSettings();
30344
- const updatedSettings = {
30345
- ...currentSettings,
30346
- ...newSettings
30347
- };
30762
+ const updatedSettings = deepMerge(currentSettings, newSettings);
30348
30763
  await pluginStore.set({
30349
30764
  key: "settings",
30350
30765
  value: updatedSettings
@@ -30357,7 +30772,7 @@ var settings$1 = ({ strapi: strapi2 }) => {
30357
30772
  getDefaultSettings
30358
30773
  };
30359
30774
  };
30360
- const { pluginId: pluginId$2 } = pluginId_1;
30775
+ const { pluginId: pluginId$3 } = pluginId_1;
30361
30776
  var monitoring$1 = ({ strapi: strapi2 }) => {
30362
30777
  let eventLog = [];
30363
30778
  let eventStats = {
@@ -30463,8 +30878,10 @@ var monitoring$1 = ({ strapi: strapi2 }) => {
30463
30878
  */
30464
30879
  getEventsPerSecond() {
30465
30880
  const now = Date.now();
30466
- const elapsed = (now - eventStats.lastReset) / 1e3;
30467
- return elapsed > 0 ? (eventStats.totalEvents / elapsed).toFixed(2) : 0;
30881
+ const windowMs = 6e4;
30882
+ const recentEvents = eventLog.filter((e) => now - e.timestamp < windowMs);
30883
+ const elapsed = Math.min((now - eventStats.lastReset) / 1e3, windowMs / 1e3);
30884
+ return elapsed > 0 ? Number((recentEvents.length / elapsed).toFixed(2)) : 0;
30468
30885
  },
30469
30886
  /**
30470
30887
  * Reset statistics
@@ -30485,13 +30902,14 @@ var monitoring$1 = ({ strapi: strapi2 }) => {
30485
30902
  if (!io2) {
30486
30903
  throw new Error("Socket.IO not initialized");
30487
30904
  }
30905
+ const safeName = `test:${eventName.replace(/[^a-zA-Z0-9:._-]/g, "")}`;
30488
30906
  const testData = {
30489
30907
  ...data,
30490
30908
  timestamp: Date.now(),
30491
30909
  test: true
30492
30910
  };
30493
- io2.emit(eventName, testData);
30494
- this.logEvent("test", { eventName, data: testData });
30911
+ io2.emit(safeName, testData);
30912
+ this.logEvent("test", { eventName: safeName, data: testData });
30495
30913
  return {
30496
30914
  success: true,
30497
30915
  eventName,
@@ -30501,7 +30919,7 @@ var monitoring$1 = ({ strapi: strapi2 }) => {
30501
30919
  }
30502
30920
  };
30503
30921
  };
30504
- const { pluginId: pluginId$1 } = pluginId_1;
30922
+ const { pluginId: pluginId$2 } = pluginId_1;
30505
30923
  var presence$1 = ({ strapi: strapi2 }) => {
30506
30924
  const activeConnections = /* @__PURE__ */ new Map();
30507
30925
  const entityEditors = /* @__PURE__ */ new Map();
@@ -30903,7 +31321,7 @@ var presence$1 = ({ strapi: strapi2 }) => {
30903
31321
  }
30904
31322
  };
30905
31323
  };
30906
- const { pluginId } = pluginId_1;
31324
+ const { pluginId: pluginId$1 } = pluginId_1;
30907
31325
  var preview$1 = ({ strapi: strapi2 }) => {
30908
31326
  const previewSubscribers = /* @__PURE__ */ new Map();
30909
31327
  const socketState = /* @__PURE__ */ new Map();
@@ -31165,22 +31583,23 @@ var diff$1 = ({ strapi: strapi2 }) => {
31165
31583
  const isPlainObject2 = (value) => {
31166
31584
  return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
31167
31585
  };
31168
- const isEqual2 = (a, b) => {
31586
+ const isEqual2 = (a, b, depth2 = 0) => {
31169
31587
  if (a === b) return true;
31170
31588
  if (a === null || b === null) return a === b;
31171
31589
  if (typeof a !== typeof b) return false;
31590
+ if (depth2 > 30) return JSON.stringify(a) === JSON.stringify(b);
31172
31591
  if (a instanceof Date && b instanceof Date) {
31173
31592
  return a.getTime() === b.getTime();
31174
31593
  }
31175
31594
  if (Array.isArray(a) && Array.isArray(b)) {
31176
31595
  if (a.length !== b.length) return false;
31177
- return a.every((item, index2) => isEqual2(item, b[index2]));
31596
+ return a.every((item, index2) => isEqual2(item, b[index2], depth2 + 1));
31178
31597
  }
31179
31598
  if (isPlainObject2(a) && isPlainObject2(b)) {
31180
31599
  const keysA = Object.keys(a);
31181
31600
  const keysB = Object.keys(b);
31182
31601
  if (keysA.length !== keysB.length) return false;
31183
- return keysA.every((key) => isEqual2(a[key], b[key]));
31602
+ return keysA.every((key) => isEqual2(a[key], b[key], depth2 + 1));
31184
31603
  }
31185
31604
  return false;
31186
31605
  };
@@ -31365,6 +31784,142 @@ var diff$1 = ({ strapi: strapi2 }) => {
31365
31784
  }
31366
31785
  };
31367
31786
  };
31787
+ var security = ({ strapi: strapi2 }) => {
31788
+ const rateLimitStore = /* @__PURE__ */ new Map();
31789
+ const connectionLimitStore = /* @__PURE__ */ new Map();
31790
+ const cleanupInterval = setInterval(() => {
31791
+ const now = Date.now();
31792
+ const maxAge = 60 * 1e3;
31793
+ for (const [key, value] of rateLimitStore.entries()) {
31794
+ if (now - value.resetTime > maxAge) {
31795
+ rateLimitStore.delete(key);
31796
+ }
31797
+ }
31798
+ for (const [key, value] of connectionLimitStore.entries()) {
31799
+ if (now - value.lastSeen > maxAge) {
31800
+ connectionLimitStore.delete(key);
31801
+ }
31802
+ }
31803
+ }, 60 * 1e3);
31804
+ return {
31805
+ /**
31806
+ * Stops the background cleanup interval (called on plugin destroy)
31807
+ */
31808
+ stopCleanupInterval() {
31809
+ clearInterval(cleanupInterval);
31810
+ },
31811
+ /**
31812
+ * Check if request should be rate limited
31813
+ * @param {string} identifier - Unique identifier (IP, user ID, etc.)
31814
+ * @param {Object} options - Rate limit options
31815
+ * @param {number} options.maxRequests - Maximum requests allowed
31816
+ * @param {number} options.windowMs - Time window in milliseconds
31817
+ * @returns {Object} - { allowed: boolean, remaining: number, resetTime: number }
31818
+ */
31819
+ checkRateLimit(identifier, options = {}) {
31820
+ const { maxRequests = 100, windowMs = 60 * 1e3 } = options;
31821
+ const now = Date.now();
31822
+ const key = `ratelimit_${identifier}`;
31823
+ let record = rateLimitStore.get(key);
31824
+ if (!record || now - record.resetTime > windowMs) {
31825
+ record = {
31826
+ count: 1,
31827
+ resetTime: now,
31828
+ windowMs
31829
+ };
31830
+ rateLimitStore.set(key, record);
31831
+ return {
31832
+ allowed: true,
31833
+ remaining: maxRequests - 1,
31834
+ resetTime: now + windowMs
31835
+ };
31836
+ }
31837
+ if (record.count >= maxRequests) {
31838
+ return {
31839
+ allowed: false,
31840
+ remaining: 0,
31841
+ resetTime: record.resetTime + windowMs,
31842
+ retryAfter: record.resetTime + windowMs - now
31843
+ };
31844
+ }
31845
+ record.count++;
31846
+ rateLimitStore.set(key, record);
31847
+ return {
31848
+ allowed: true,
31849
+ remaining: maxRequests - record.count,
31850
+ resetTime: record.resetTime + windowMs
31851
+ };
31852
+ },
31853
+ /**
31854
+ * Check connection limits per IP/user
31855
+ * @param {string} identifier - Unique identifier
31856
+ * @param {number} maxConnections - Maximum allowed connections
31857
+ * @returns {boolean} - Whether connection is allowed
31858
+ */
31859
+ checkConnectionLimit(identifier, maxConnections = 5) {
31860
+ const key = `connlimit_${identifier}`;
31861
+ const record = connectionLimitStore.get(key);
31862
+ const now = Date.now();
31863
+ if (!record) {
31864
+ connectionLimitStore.set(key, {
31865
+ count: 1,
31866
+ lastSeen: now
31867
+ });
31868
+ return true;
31869
+ }
31870
+ if (record.count >= maxConnections) {
31871
+ strapi2.log.warn(`[Socket.IO Security] Connection limit exceeded for ${identifier}`);
31872
+ return false;
31873
+ }
31874
+ record.count++;
31875
+ record.lastSeen = now;
31876
+ connectionLimitStore.set(key, record);
31877
+ return true;
31878
+ },
31879
+ /**
31880
+ * Release a connection slot
31881
+ * @param {string} identifier - Unique identifier
31882
+ */
31883
+ releaseConnection(identifier) {
31884
+ const key = `connlimit_${identifier}`;
31885
+ const record = connectionLimitStore.get(key);
31886
+ if (record) {
31887
+ record.count = Math.max(0, record.count - 1);
31888
+ if (record.count === 0) {
31889
+ connectionLimitStore.delete(key);
31890
+ } else {
31891
+ connectionLimitStore.set(key, record);
31892
+ }
31893
+ }
31894
+ },
31895
+ /**
31896
+ * Validate event name to prevent injection
31897
+ * @param {string} eventName - Event name to validate
31898
+ * @returns {boolean} - Whether event name is valid
31899
+ */
31900
+ validateEventName(eventName) {
31901
+ const validPattern = /^[a-zA-Z0-9:._-]+$/;
31902
+ return validPattern.test(eventName) && eventName.length < 100;
31903
+ },
31904
+ /**
31905
+ * Get current statistics
31906
+ * @returns {Object} - Statistics object
31907
+ */
31908
+ getStats() {
31909
+ return {
31910
+ rateLimitEntries: rateLimitStore.size,
31911
+ connectionLimitEntries: connectionLimitStore.size
31912
+ };
31913
+ },
31914
+ /**
31915
+ * Clear all rate limit data
31916
+ */
31917
+ clear() {
31918
+ rateLimitStore.clear();
31919
+ connectionLimitStore.clear();
31920
+ }
31921
+ };
31922
+ };
31368
31923
  const strategy = strategies;
31369
31924
  const sanitize = sanitize_1;
31370
31925
  const transform = transform$1;
@@ -31381,20 +31936,58 @@ var services$1 = {
31381
31936
  monitoring,
31382
31937
  presence,
31383
31938
  preview,
31384
- diff
31939
+ diff,
31940
+ security
31385
31941
  };
31386
31942
  const bootstrap = bootstrap_1;
31387
31943
  const config = config$1;
31388
31944
  const controllers = controllers$1;
31389
31945
  const routes = routes$1;
31390
31946
  const services = services$1;
31391
- const destroy = async () => {
31392
- };
31947
+ const { pluginId } = pluginId_1;
31393
31948
  const register = async () => {
31394
31949
  };
31395
31950
  const contentTypes = {};
31396
31951
  const middlewares = {};
31397
31952
  const policies = {};
31953
+ const destroy = async ({ strapi: strapi2 }) => {
31954
+ try {
31955
+ const presenceService = strapi2.plugin(pluginId)?.service("presence");
31956
+ if (presenceService?.stopCleanupInterval) {
31957
+ presenceService.stopCleanupInterval();
31958
+ }
31959
+ const presenceController = strapi2.plugin(pluginId)?.controller("presence");
31960
+ if (presenceController?.stopTokenCleanup) {
31961
+ presenceController.stopTokenCleanup();
31962
+ }
31963
+ const securityService = strapi2.plugin(pluginId)?.service("security");
31964
+ if (securityService?.stopCleanupInterval) {
31965
+ securityService.stopCleanupInterval();
31966
+ }
31967
+ const io2 = strapi2.$io?.server;
31968
+ if (io2) {
31969
+ io2.disconnectSockets(true);
31970
+ await new Promise((resolve) => {
31971
+ io2.close((err) => {
31972
+ if (err) {
31973
+ strapi2.log.warn(`socket.io: Error closing server: ${err.message}`);
31974
+ }
31975
+ resolve();
31976
+ });
31977
+ });
31978
+ }
31979
+ const redisClients = strapi2.$io?._redisClients;
31980
+ if (redisClients) {
31981
+ await Promise.allSettled([
31982
+ redisClients.pubClient?.quit?.(),
31983
+ redisClients.subClient?.quit?.()
31984
+ ]);
31985
+ }
31986
+ strapi2.log.info("socket.io: Plugin destroyed – all handles released");
31987
+ } catch (err) {
31988
+ strapi2.log.error(`socket.io: Error during destroy: ${err.message}`);
31989
+ }
31990
+ };
31398
31991
  var server = {
31399
31992
  register,
31400
31993
  bootstrap,