magic-editor-x 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +890 -0
  3. package/dist/_chunks/App-B1FgOsWa.mjs +2143 -0
  4. package/dist/_chunks/App-mtrlABtd.js +2146 -0
  5. package/dist/_chunks/LicensePage-BnyWSrWs.js +375 -0
  6. package/dist/_chunks/LicensePage-CWH-AFR-.mjs +373 -0
  7. package/dist/_chunks/LiveCollaborationPanel-DbDHwr2C.js +222 -0
  8. package/dist/_chunks/LiveCollaborationPanel-ryjcDAA7.mjs +220 -0
  9. package/dist/_chunks/Settings-Bk9bxJTy.js +440 -0
  10. package/dist/_chunks/Settings-D-V2MLVm.mjs +438 -0
  11. package/dist/_chunks/de-CSrHZWEb.mjs +295 -0
  12. package/dist/_chunks/de-CzSo1oD2.js +295 -0
  13. package/dist/_chunks/en-DuQun2v4.mjs +295 -0
  14. package/dist/_chunks/en-DxIkVPUh.js +295 -0
  15. package/dist/_chunks/es-DAQ_97zx.js +273 -0
  16. package/dist/_chunks/es-DEB0CA8S.mjs +273 -0
  17. package/dist/_chunks/fr-Bqkhvdx2.mjs +273 -0
  18. package/dist/_chunks/fr-ChPabvNP.js +273 -0
  19. package/dist/_chunks/getTranslation-C4uWR0DB.mjs +50985 -0
  20. package/dist/_chunks/getTranslation-D35vbDap.js +51001 -0
  21. package/dist/_chunks/index-B5MzUyo0.mjs +2541 -0
  22. package/dist/_chunks/index-BRVqbnOb.mjs +4450 -0
  23. package/dist/_chunks/index-BiLy_f7C.js +2540 -0
  24. package/dist/_chunks/index-CQx7-dFP.js +4472 -0
  25. package/dist/_chunks/pt-BMoYltav.mjs +273 -0
  26. package/dist/_chunks/pt-Cm74LpyZ.js +273 -0
  27. package/dist/_chunks/tools-CjnQJ9w2.mjs +2155 -0
  28. package/dist/_chunks/tools-DNt2tioN.js +2186 -0
  29. package/dist/admin/index.js +3 -0
  30. package/dist/admin/index.mjs +4 -0
  31. package/dist/server/index.js +2554 -0
  32. package/dist/server/index.mjs +2544 -0
  33. package/dist/style.css +164 -0
  34. package/package.json +122 -0
  35. package/pics/collab-magiceditorX.png +0 -0
  36. package/pics/editorX.png +0 -0
  37. package/pics/liveCollabwidget1.png +0 -0
@@ -0,0 +1,2554 @@
1
+ "use strict";
2
+ const require$$0 = require("open-graph-scraper");
3
+ const require$$1 = require("fs");
4
+ const require$$2$1 = require("path");
5
+ const require$$3 = require("https");
6
+ const require$$4 = require("http");
7
+ const require$$5 = require("url");
8
+ const require$$0$1 = require("crypto");
9
+ const require$$1$1 = require("socket.io");
10
+ const require$$2$2 = require("yjs");
11
+ const require$$1$2 = require("os");
12
+ const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
13
+ const require$$0__default = /* @__PURE__ */ _interopDefault(require$$0);
14
+ const require$$1__default = /* @__PURE__ */ _interopDefault(require$$1);
15
+ const require$$2__default = /* @__PURE__ */ _interopDefault(require$$2$1);
16
+ const require$$3__default = /* @__PURE__ */ _interopDefault(require$$3);
17
+ const require$$4__default = /* @__PURE__ */ _interopDefault(require$$4);
18
+ const require$$5__default = /* @__PURE__ */ _interopDefault(require$$5);
19
+ const require$$0__default$1 = /* @__PURE__ */ _interopDefault(require$$0$1);
20
+ const require$$1__default$1 = /* @__PURE__ */ _interopDefault(require$$1$1);
21
+ const require$$2__default$1 = /* @__PURE__ */ _interopDefault(require$$2$2);
22
+ const require$$1__default$2 = /* @__PURE__ */ _interopDefault(require$$1$2);
23
+ function getDefaultExportFromCjs(x) {
24
+ return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
25
+ }
26
+ var bootstrap$1 = async ({ strapi: strapi2 }) => {
27
+ try {
28
+ const contentTypes2 = [
29
+ "plugin::magic-editor-x.collab-session",
30
+ "plugin::magic-editor-x.document-snapshot",
31
+ "plugin::magic-editor-x.collab-permission"
32
+ ];
33
+ for (const contentType of contentTypes2) {
34
+ const exists = strapi2.contentType(contentType);
35
+ if (exists) {
36
+ strapi2.log.info(`[Magic Editor X] [SUCCESS] Content type registered: ${contentType}`);
37
+ } else {
38
+ strapi2.log.warn(`[Magic Editor X] [WARNING] Content type NOT found: ${contentType}`);
39
+ }
40
+ }
41
+ try {
42
+ const staleSessions = await strapi2.documents("plugin::magic-editor-x.collab-session").findMany({
43
+ limit: 1e3
44
+ });
45
+ if (staleSessions && staleSessions.length > 0) {
46
+ for (const session of staleSessions) {
47
+ await strapi2.documents("plugin::magic-editor-x.collab-session").delete({
48
+ documentId: session.documentId
49
+ });
50
+ }
51
+ strapi2.log.info(`[Magic Editor X] [CLEANUP] Cleaned up ${staleSessions.length} stale sessions`);
52
+ }
53
+ } catch (cleanupError) {
54
+ strapi2.log.debug("[Magic Editor X] Session cleanup skipped:", cleanupError.message);
55
+ }
56
+ try {
57
+ const now = /* @__PURE__ */ new Date();
58
+ const expiredPerms = await strapi2.documents("plugin::magic-editor-x.collab-permission").findMany({
59
+ filters: {
60
+ expiresAt: { $lt: now, $ne: null }
61
+ },
62
+ limit: 1e3
63
+ });
64
+ if (expiredPerms && expiredPerms.length > 0) {
65
+ for (const perm of expiredPerms) {
66
+ await strapi2.documents("plugin::magic-editor-x.collab-permission").delete({
67
+ documentId: perm.documentId
68
+ });
69
+ }
70
+ strapi2.log.info(`[Magic Editor X] [CLEANUP] Cleaned up ${expiredPerms.length} expired permissions`);
71
+ }
72
+ } catch (cleanupError) {
73
+ strapi2.log.debug("[Magic Editor X] Permission cleanup skipped:", cleanupError.message);
74
+ }
75
+ if (strapi2.$io) {
76
+ strapi2.log.info("[Magic Editor X] [INFO] strapi-plugin-io detected - running in compatibility mode");
77
+ }
78
+ await strapi2.plugin("magic-editor-x").service("realtimeService").initSocketServer();
79
+ strapi2.log.info("[Magic Editor X] [SUCCESS] Realtime server started");
80
+ } catch (error) {
81
+ strapi2.log.error("[Magic Editor X] [ERROR] Bootstrap failed:", error);
82
+ }
83
+ strapi2.log.info("[Magic Editor X] Plugin bootstrapped");
84
+ };
85
+ var destroy$1 = async ({ strapi: strapi2 }) => {
86
+ try {
87
+ await strapi2.plugin("magic-editor-x").service("realtimeService").close();
88
+ } catch (error) {
89
+ strapi2.log.error("[Magic Editor X] Failed to gracefully shutdown realtime server", error);
90
+ }
91
+ strapi2.log.info("[Magic Editor X] Plugin destroyed");
92
+ };
93
+ var register$1 = ({ strapi: strapi2 }) => {
94
+ strapi2.customFields.register({
95
+ name: "richtext",
96
+ plugin: "magic-editor-x",
97
+ type: "text",
98
+ // Stores JSON content as text
99
+ inputSize: {
100
+ default: 12,
101
+ // Full width
102
+ isResizable: true
103
+ }
104
+ });
105
+ strapi2.log.info("[Magic Editor X] Custom field registered");
106
+ };
107
+ var config$1 = {
108
+ default: {
109
+ // Default configuration options
110
+ enabledTools: [
111
+ "header",
112
+ "paragraph",
113
+ "list",
114
+ "checklist",
115
+ "quote",
116
+ "warning",
117
+ "code",
118
+ "delimiter",
119
+ "table",
120
+ "embed",
121
+ "raw",
122
+ "image",
123
+ "mediaLib",
124
+ "linkTool",
125
+ "marker",
126
+ "inlineCode",
127
+ "underline"
128
+ ],
129
+ // Link preview timeout (ms)
130
+ linkPreviewTimeout: 1e4,
131
+ // Max image upload size (bytes)
132
+ maxImageSize: 10 * 1024 * 1024,
133
+ // 10MB
134
+ // Allowed image types
135
+ allowedImageTypes: ["image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml"],
136
+ // Realtime collaboration defaults
137
+ collaboration: {
138
+ enabled: true,
139
+ sessionTTL: 2 * 60 * 1e3,
140
+ // 2 minutes
141
+ wsPath: "/magic-editor-x/realtime",
142
+ wsUrl: null,
143
+ allowedOrigins: [],
144
+ allowedAdminRoles: ["strapi-super-admin"],
145
+ allowedAdminUserIds: []
146
+ }
147
+ },
148
+ validator: (config2) => {
149
+ if (config2.linkPreviewTimeout && typeof config2.linkPreviewTimeout !== "number") {
150
+ throw new Error("[Magic Editor X] linkPreviewTimeout must be a number");
151
+ }
152
+ if (config2.maxImageSize && typeof config2.maxImageSize !== "number") {
153
+ throw new Error("[Magic Editor X] maxImageSize must be a number");
154
+ }
155
+ if (config2.collaboration) {
156
+ if (typeof config2.collaboration.enabled !== "boolean") {
157
+ throw new Error("[Magic Editor X] collaboration.enabled must be a boolean");
158
+ }
159
+ if (config2.collaboration.sessionTTL && typeof config2.collaboration.sessionTTL !== "number") {
160
+ throw new Error("[Magic Editor X] collaboration.sessionTTL must be a number");
161
+ }
162
+ ["allowedOrigins", "allowedAdminRoles", "allowedAdminUserIds"].forEach((key) => {
163
+ if (config2.collaboration[key] && !Array.isArray(config2.collaboration[key])) {
164
+ throw new Error(`[Magic Editor X] collaboration.${key} must be an array`);
165
+ }
166
+ });
167
+ }
168
+ }
169
+ };
170
+ var contentTypes$1 = {
171
+ /**
172
+ * Collaboration Sessions
173
+ * Tracks active realtime editing sessions
174
+ *
175
+ * NOTE: Sessions are transient and should NOT be transferred.
176
+ * They are cleared on server restart anyway.
177
+ */
178
+ "collab-session": {
179
+ schema: {
180
+ kind: "collectionType",
181
+ collectionName: "magic_editor_collab_sessions",
182
+ info: {
183
+ singularName: "collab-session",
184
+ pluralName: "collab-sessions",
185
+ displayName: "Collaboration Session",
186
+ description: "Active realtime collaboration sessions"
187
+ },
188
+ options: {
189
+ draftAndPublish: false
190
+ },
191
+ pluginOptions: {
192
+ "content-manager": {
193
+ visible: false
194
+ },
195
+ "content-type-builder": {
196
+ visible: false
197
+ },
198
+ // Exclude from Strapi Transfer - sessions are transient
199
+ "import-export-entries": {
200
+ idField: "roomId"
201
+ }
202
+ },
203
+ attributes: {
204
+ roomId: {
205
+ type: "string",
206
+ required: true,
207
+ unique: true
208
+ },
209
+ contentType: {
210
+ type: "string",
211
+ required: true
212
+ },
213
+ entryId: {
214
+ type: "string",
215
+ required: true
216
+ },
217
+ fieldName: {
218
+ type: "string",
219
+ required: true
220
+ },
221
+ activeUsers: {
222
+ type: "json",
223
+ default: []
224
+ },
225
+ lastActivity: {
226
+ type: "datetime"
227
+ },
228
+ yjsState: {
229
+ type: "text",
230
+ default: null
231
+ }
232
+ }
233
+ }
234
+ },
235
+ /**
236
+ * Document Snapshots
237
+ * Periodic snapshots for version history and recovery
238
+ *
239
+ * NOTE: Snapshots contain Yjs binary data and admin::user references.
240
+ * Transfer is possible but user relations may break.
241
+ */
242
+ "document-snapshot": {
243
+ schema: {
244
+ kind: "collectionType",
245
+ collectionName: "magic_editor_doc_snapshots",
246
+ info: {
247
+ singularName: "document-snapshot",
248
+ pluralName: "document-snapshots",
249
+ displayName: "Document Snapshot",
250
+ description: "Document version snapshots"
251
+ },
252
+ options: {
253
+ draftAndPublish: false
254
+ },
255
+ pluginOptions: {
256
+ "content-manager": {
257
+ visible: false
258
+ },
259
+ "content-type-builder": {
260
+ visible: false
261
+ },
262
+ // Transfer config - use roomId+version as unique identifier
263
+ "import-export-entries": {
264
+ idField: "roomId"
265
+ }
266
+ },
267
+ attributes: {
268
+ roomId: {
269
+ type: "string",
270
+ required: true
271
+ },
272
+ contentType: {
273
+ type: "string",
274
+ required: true
275
+ },
276
+ entryId: {
277
+ type: "string",
278
+ required: true
279
+ },
280
+ fieldName: {
281
+ type: "string",
282
+ required: true
283
+ },
284
+ version: {
285
+ type: "integer",
286
+ required: true
287
+ },
288
+ yjsSnapshot: {
289
+ type: "text",
290
+ required: true
291
+ },
292
+ jsonContent: {
293
+ type: "json"
294
+ },
295
+ createdBy: {
296
+ type: "relation",
297
+ relation: "oneToOne",
298
+ target: "admin::user"
299
+ },
300
+ createdAt: {
301
+ type: "datetime"
302
+ }
303
+ }
304
+ }
305
+ },
306
+ /**
307
+ * Collaboration Permissions
308
+ * Access control for realtime editing
309
+ *
310
+ * NOTE: Permissions reference admin::user which are environment-specific.
311
+ * On transfer, user relations will need to be re-created manually.
312
+ * The contentType field can be used to match permissions.
313
+ */
314
+ "collab-permission": {
315
+ schema: {
316
+ kind: "collectionType",
317
+ collectionName: "magic_editor_collab_permissions",
318
+ info: {
319
+ singularName: "collab-permission",
320
+ pluralName: "collab-permissions",
321
+ displayName: "Collaboration Permission",
322
+ description: "User permissions for realtime editing"
323
+ },
324
+ options: {
325
+ draftAndPublish: false
326
+ },
327
+ pluginOptions: {
328
+ "content-manager": {
329
+ visible: false
330
+ // Hidden - use plugin settings page instead
331
+ },
332
+ "content-type-builder": {
333
+ visible: false
334
+ },
335
+ // Transfer note: admin::user relations won't transfer
336
+ // Permissions should be re-created in target environment
337
+ "import-export-entries": {
338
+ idField: "id"
339
+ }
340
+ },
341
+ attributes: {
342
+ contentType: {
343
+ type: "string",
344
+ required: false,
345
+ // null = all content types
346
+ default: "*"
347
+ },
348
+ entryId: {
349
+ type: "string"
350
+ },
351
+ fieldName: {
352
+ type: "string"
353
+ },
354
+ user: {
355
+ type: "relation",
356
+ relation: "oneToOne",
357
+ target: "admin::user",
358
+ required: true
359
+ },
360
+ role: {
361
+ type: "enumeration",
362
+ enum: ["viewer", "editor", "owner"],
363
+ default: "editor"
364
+ },
365
+ expiresAt: {
366
+ type: "datetime"
367
+ },
368
+ grantedBy: {
369
+ type: "relation",
370
+ relation: "oneToOne",
371
+ target: "admin::user"
372
+ }
373
+ }
374
+ }
375
+ }
376
+ };
377
+ var editorController = ({ strapi: strapi2 }) => ({
378
+ /**
379
+ * Fetch link metadata (OpenGraph) for URL preview
380
+ * GET /api/magic-editor-x/link?url=https://example.com
381
+ */
382
+ async fetchLinkMeta(ctx) {
383
+ try {
384
+ const { url } = ctx.query;
385
+ if (!url) {
386
+ return ctx.send({
387
+ success: 0,
388
+ message: "URL parameter is required"
389
+ }, 400);
390
+ }
391
+ const result = await strapi2.plugin("magic-editor-x").service("editorService").fetchLinkMeta(url);
392
+ ctx.send(result);
393
+ } catch (error) {
394
+ strapi2.log.error("[Magic Editor X] Link fetch error:", error);
395
+ ctx.send({
396
+ success: 0,
397
+ message: error.message || "Failed to fetch link metadata"
398
+ }, 500);
399
+ }
400
+ },
401
+ /**
402
+ * Upload image by file
403
+ * POST /api/magic-editor-x/image/byFile
404
+ * Multipart form data with files.image
405
+ */
406
+ async uploadByFile(ctx) {
407
+ try {
408
+ const result = await strapi2.plugin("magic-editor-x").service("editorService").uploadByFile(ctx);
409
+ ctx.send(result);
410
+ } catch (error) {
411
+ strapi2.log.error("[Magic Editor X] File upload error:", error);
412
+ ctx.send({
413
+ success: 0,
414
+ message: error.message || "Failed to upload file"
415
+ }, 500);
416
+ }
417
+ },
418
+ /**
419
+ * Upload image by URL
420
+ * POST /api/magic-editor-x/image/byUrl
421
+ * JSON body with url field
422
+ */
423
+ async uploadByUrl(ctx) {
424
+ try {
425
+ const { url } = ctx.request.body;
426
+ if (!url) {
427
+ return ctx.send({
428
+ success: 0,
429
+ message: "URL is required"
430
+ }, 400);
431
+ }
432
+ const result = await strapi2.plugin("magic-editor-x").service("editorService").uploadByUrl(url);
433
+ ctx.send(result);
434
+ } catch (error) {
435
+ strapi2.log.error("[Magic Editor X] URL upload error:", error);
436
+ ctx.send({
437
+ success: 0,
438
+ message: error.message || "Failed to upload from URL"
439
+ }, 500);
440
+ }
441
+ },
442
+ /**
443
+ * Upload file (for Attaches Tool)
444
+ * POST /api/magic-editor-x/file/upload
445
+ * Multipart form data with file
446
+ */
447
+ async uploadFile(ctx) {
448
+ try {
449
+ const result = await strapi2.plugin("magic-editor-x").service("editorService").uploadAttachment(ctx);
450
+ ctx.send(result);
451
+ } catch (error) {
452
+ strapi2.log.error("[Magic Editor X] Attachment upload error:", error);
453
+ ctx.send({
454
+ success: 0,
455
+ message: error.message || "Failed to upload attachment"
456
+ }, 500);
457
+ }
458
+ }
459
+ });
460
+ const pluginId$2 = "magic-editor-x";
461
+ const sanitizeInitialValue = (value) => {
462
+ if (!value) {
463
+ return "";
464
+ }
465
+ if (typeof value === "string") {
466
+ return value;
467
+ }
468
+ try {
469
+ return JSON.stringify(value);
470
+ } catch (error) {
471
+ return "";
472
+ }
473
+ };
474
+ const verifyAdminToken = async (strapi2, ctx) => {
475
+ const authHeader = ctx.request.header.authorization;
476
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
477
+ return null;
478
+ }
479
+ const token = authHeader.substring(7);
480
+ try {
481
+ const decoded = await strapi2.admin.services.token.decodeJwtToken(token);
482
+ if (!decoded || !decoded.id) {
483
+ return null;
484
+ }
485
+ const adminUser = await strapi2.query("admin::user").findOne({
486
+ where: { id: decoded.id },
487
+ select: ["id", "firstname", "lastname", "email", "isActive"]
488
+ });
489
+ if (!adminUser || !adminUser.isActive) {
490
+ return null;
491
+ }
492
+ return adminUser;
493
+ } catch (error) {
494
+ strapi2.log.warn("[Realtime] Admin token verification failed:", error.message);
495
+ return null;
496
+ }
497
+ };
498
+ var realtimeController = ({ strapi: strapi2 }) => ({
499
+ /**
500
+ * POST /magic-editor-x/realtime/session
501
+ * Issues a short-lived collaboration token for the socket handshake.
502
+ */
503
+ async createSession(ctx) {
504
+ const { roomId, fieldName, meta = {}, initialValue } = ctx.request.body || {};
505
+ strapi2.log.info("[Realtime] createSession called with:", { roomId, fieldName });
506
+ if (!roomId || !fieldName) {
507
+ strapi2.log.warn("[Realtime] Missing roomId or fieldName");
508
+ return ctx.badRequest("roomId and fieldName are required");
509
+ }
510
+ let adminUser = ctx.state?.user;
511
+ if (!adminUser || !adminUser.email) {
512
+ adminUser = await verifyAdminToken(strapi2, ctx);
513
+ }
514
+ if (!adminUser) {
515
+ strapi2.log.warn("[Realtime] No admin user in context or invalid token");
516
+ return ctx.unauthorized("Admin authentication required");
517
+ }
518
+ strapi2.log.info("[Realtime] Admin user:", {
519
+ id: adminUser.id,
520
+ email: adminUser.email,
521
+ firstname: adminUser.firstname,
522
+ lastname: adminUser.lastname
523
+ });
524
+ const accessService2 = strapi2.plugin(pluginId$2).service("accessService");
525
+ let extractedContentType = null;
526
+ let extractedDocumentId = null;
527
+ if (roomId) {
528
+ const parts = roomId.split("|");
529
+ if (parts.length >= 1) {
530
+ const contentType = parts[0];
531
+ if (contentType && contentType.includes("::")) {
532
+ extractedContentType = contentType;
533
+ }
534
+ if (parts[1]) {
535
+ extractedDocumentId = parts[1];
536
+ }
537
+ }
538
+ strapi2.log.info("[Realtime] Parsed roomId:", {
539
+ contentType: extractedContentType,
540
+ documentId: extractedDocumentId,
541
+ roomId
542
+ });
543
+ }
544
+ const access = await accessService2.canUseCollaboration(adminUser, extractedContentType);
545
+ strapi2.log.info("[Realtime] Access check result:", {
546
+ allowed: access.allowed,
547
+ reason: access.reason || "none",
548
+ role: access.role || "none",
549
+ userId: adminUser.id,
550
+ userEmail: adminUser.email,
551
+ contentType: extractedContentType
552
+ });
553
+ if (!access.allowed) {
554
+ if (access.reason === "permission-required") {
555
+ return ctx.forbidden(
556
+ "Du benötigst eine Freigabe für die Echtzeit-Bearbeitung. Bitte kontaktiere einen Super Admin, um Zugriff zu erhalten."
557
+ );
558
+ }
559
+ return ctx.forbidden(access.reason || "Realtime collaboration is not enabled for your role");
560
+ }
561
+ const realtimeService2 = strapi2.plugin(pluginId$2).service("realtimeService");
562
+ try {
563
+ const session = realtimeService2.issueSession({
564
+ roomId,
565
+ fieldName,
566
+ meta,
567
+ user: adminUser,
568
+ initialValue: sanitizeInitialValue(initialValue)
569
+ });
570
+ strapi2.log.info("[Realtime] [SUCCESS] Session created successfully with role:", access.role);
571
+ ctx.body = {
572
+ ...session,
573
+ role: access.role || "viewer",
574
+ // Default to viewer if no role
575
+ canEdit: ["editor", "owner"].includes(access.role)
576
+ };
577
+ } catch (error) {
578
+ if (error.message === "collaboration-disabled") {
579
+ return ctx.forbidden("Realtime collaboration is disabled");
580
+ }
581
+ strapi2.log.error("[Magic Editor X] Failed to create realtime session", error);
582
+ ctx.internalServerError("Unable to create realtime session");
583
+ }
584
+ }
585
+ });
586
+ var collaborationController = ({ strapi: strapi2 }) => ({
587
+ /**
588
+ * List admin users for collaboration
589
+ */
590
+ async listAdminUsers(ctx) {
591
+ try {
592
+ const users = await strapi2.query("admin::user").findMany({
593
+ where: { isActive: true },
594
+ select: ["id", "firstname", "lastname", "email", "username"],
595
+ limit: 100
596
+ });
597
+ ctx.body = { data: users };
598
+ } catch (error) {
599
+ strapi2.log.error("[Collab] Error listing admin users:", error);
600
+ ctx.throw(500, error);
601
+ }
602
+ },
603
+ /**
604
+ * List all collaboration permissions
605
+ * Using Document Service API (strapi.documents) for Strapi v5
606
+ */
607
+ async listPermissions(ctx) {
608
+ try {
609
+ const permissions = await strapi2.documents("plugin::magic-editor-x.collab-permission").findMany({
610
+ populate: ["user", "grantedBy"],
611
+ sort: [{ createdAt: "desc" }]
612
+ });
613
+ ctx.body = { data: permissions };
614
+ } catch (error) {
615
+ ctx.throw(500, error);
616
+ }
617
+ },
618
+ /**
619
+ * Create collaboration permission
620
+ * Checks license limits before creating
621
+ */
622
+ async createPermission(ctx) {
623
+ try {
624
+ const { userId, role, contentType, entryId, fieldName, expiresAt } = ctx.request.body;
625
+ strapi2.log.info("[Collab] Creating permission with data:", { userId, role, contentType });
626
+ if (!userId || !role) {
627
+ strapi2.log.warn("[Collab] Missing userId or role");
628
+ return ctx.badRequest("userId and role are required");
629
+ }
630
+ const accessService2 = strapi2.plugin("magic-editor-x").service("accessService");
631
+ const limitCheck = await accessService2.checkCollaboratorLimit();
632
+ if (!limitCheck.canAdd) {
633
+ strapi2.log.warn("[Collab] Collaborator limit reached:", limitCheck);
634
+ return ctx.forbidden({
635
+ error: "Collaborator limit reached",
636
+ message: `You have reached the maximum of ${limitCheck.max} collaborators for your plan. Upgrade to add more.`,
637
+ current: limitCheck.current,
638
+ max: limitCheck.max,
639
+ upgradeRequired: true
640
+ });
641
+ }
642
+ const user = await strapi2.query("admin::user").findOne({
643
+ where: { id: userId }
644
+ });
645
+ if (!user) {
646
+ strapi2.log.warn("[Collab] User not found:", userId);
647
+ return ctx.badRequest("User not found");
648
+ }
649
+ strapi2.log.info("[Collab] Creating permission for user:", user.email);
650
+ const permission = await strapi2.documents("plugin::magic-editor-x.collab-permission").create({
651
+ data: {
652
+ user: userId,
653
+ role,
654
+ // null = all content types, store null not '*'
655
+ contentType: contentType === "*" || !contentType ? null : contentType,
656
+ entryId: entryId || null,
657
+ fieldName: fieldName || null,
658
+ expiresAt: expiresAt || null,
659
+ grantedBy: ctx.state.user.id
660
+ }
661
+ });
662
+ strapi2.log.info("[Collab] Permission created successfully:", permission.documentId);
663
+ ctx.body = { data: permission };
664
+ } catch (error) {
665
+ strapi2.log.error("[Collab] Error creating permission:", error);
666
+ ctx.throw(500, error.message || "Failed to create permission");
667
+ }
668
+ },
669
+ /**
670
+ * Update collaboration permission
671
+ * Using Document Service API (strapi.documents) for Strapi v5
672
+ * Note: Uses documentId instead of numeric id
673
+ */
674
+ async updatePermission(ctx) {
675
+ try {
676
+ const { id } = ctx.params;
677
+ const { role, contentType, entryId, fieldName, expiresAt } = ctx.request.body;
678
+ const updateData = {};
679
+ if (role !== void 0) {
680
+ updateData.role = role;
681
+ }
682
+ if (contentType !== void 0) {
683
+ updateData.contentType = contentType === "*" || contentType === null ? null : contentType;
684
+ }
685
+ if (entryId !== void 0) {
686
+ updateData.entryId = entryId;
687
+ }
688
+ if (fieldName !== void 0) {
689
+ updateData.fieldName = fieldName;
690
+ }
691
+ if (expiresAt !== void 0) {
692
+ updateData.expiresAt = expiresAt;
693
+ }
694
+ strapi2.log.info("[Collab] Updating permission:", { documentId: id, updateData });
695
+ const permission = await strapi2.documents("plugin::magic-editor-x.collab-permission").update({
696
+ documentId: id,
697
+ data: updateData
698
+ });
699
+ ctx.body = { data: permission };
700
+ } catch (error) {
701
+ strapi2.log.error("[Collab] Error updating permission:", error);
702
+ ctx.throw(500, error);
703
+ }
704
+ },
705
+ /**
706
+ * Delete collaboration permission
707
+ * Using Document Service API (strapi.documents) for Strapi v5
708
+ * Note: Uses documentId instead of numeric id
709
+ */
710
+ async deletePermission(ctx) {
711
+ try {
712
+ const { id } = ctx.params;
713
+ await strapi2.documents("plugin::magic-editor-x.collab-permission").delete({
714
+ documentId: id
715
+ });
716
+ ctx.body = { data: { documentId: id } };
717
+ } catch (error) {
718
+ ctx.throw(500, error);
719
+ }
720
+ },
721
+ /**
722
+ * Check if user can access a specific document
723
+ */
724
+ async checkAccess(ctx) {
725
+ try {
726
+ const { roomId, action } = ctx.query;
727
+ const userId = ctx.state.user.id;
728
+ if (!roomId) {
729
+ return ctx.badRequest("roomId is required");
730
+ }
731
+ const canAccess = await strapi2.plugin("magic-editor-x").service("accessService").canAccessRoom(userId, roomId, action || "view");
732
+ ctx.body = { data: { canAccess } };
733
+ } catch (error) {
734
+ ctx.throw(500, error);
735
+ }
736
+ }
737
+ });
738
+ var licenseController = ({ strapi: strapi2 }) => ({
739
+ /**
740
+ * Auto-create a FREE license with logged-in admin user data
741
+ */
742
+ async autoCreate(ctx) {
743
+ try {
744
+ const adminUser = ctx.state.user;
745
+ if (!adminUser) {
746
+ return ctx.unauthorized("No admin user logged in");
747
+ }
748
+ const licenseService2 = strapi2.plugin("magic-editor-x").service("licenseService");
749
+ const license2 = await licenseService2.createLicense({
750
+ email: adminUser.email,
751
+ firstName: adminUser.firstname || "Admin",
752
+ lastName: adminUser.lastname || "User"
753
+ });
754
+ if (!license2) {
755
+ return ctx.badRequest("Failed to create license");
756
+ }
757
+ await licenseService2.storeLicenseKey(license2.licenseKey);
758
+ const pingInterval = licenseService2.startPinging(license2.licenseKey, 15);
759
+ strapi2.licenseGuardEditorX = {
760
+ licenseKey: license2.licenseKey,
761
+ pingInterval,
762
+ data: license2,
763
+ tier: "free"
764
+ };
765
+ return ctx.send({
766
+ success: true,
767
+ message: "License automatically created and activated",
768
+ data: license2
769
+ });
770
+ } catch (error) {
771
+ strapi2.log.error("[Magic Editor X] Error auto-creating license:", error);
772
+ return ctx.badRequest("Error creating license");
773
+ }
774
+ },
775
+ /**
776
+ * Get current license status
777
+ */
778
+ async getStatus(ctx) {
779
+ try {
780
+ const licenseService2 = strapi2.plugin("magic-editor-x").service("licenseService");
781
+ const licenseKey = await licenseService2.getStoredLicenseKey();
782
+ if (!licenseKey) {
783
+ return ctx.send({
784
+ success: false,
785
+ demo: true,
786
+ valid: false,
787
+ tier: "free",
788
+ message: "No license found. Running in FREE mode."
789
+ });
790
+ }
791
+ const verification = await licenseService2.verifyLicense(licenseKey);
792
+ const license2 = await licenseService2.getLicenseByKey(licenseKey);
793
+ const tier = await licenseService2.getCurrentTier();
794
+ return ctx.send({
795
+ success: true,
796
+ valid: verification.valid,
797
+ demo: false,
798
+ tier,
799
+ data: {
800
+ licenseKey,
801
+ email: license2?.email || null,
802
+ firstName: license2?.firstName || null,
803
+ lastName: license2?.lastName || null,
804
+ isActive: license2?.isActive || false,
805
+ isExpired: license2?.isExpired || false,
806
+ isOnline: license2?.isOnline || false,
807
+ expiresAt: license2?.expiresAt,
808
+ lastPingAt: license2?.lastPingAt,
809
+ deviceName: license2?.deviceName,
810
+ features: {
811
+ premium: license2?.featurePremium || false,
812
+ advanced: license2?.featureAdvanced || false,
813
+ enterprise: license2?.featureEnterprise || false
814
+ }
815
+ }
816
+ });
817
+ } catch (error) {
818
+ strapi2.log.error("[Magic Editor X] Error getting license status:", error);
819
+ return ctx.badRequest("Error getting license status");
820
+ }
821
+ },
822
+ /**
823
+ * Store and validate an existing license key
824
+ */
825
+ async storeKey(ctx) {
826
+ try {
827
+ const { licenseKey, email } = ctx.request.body;
828
+ if (!licenseKey || !licenseKey.trim()) {
829
+ return ctx.badRequest("License key is required");
830
+ }
831
+ if (!email || !email.trim()) {
832
+ return ctx.badRequest("Email address is required");
833
+ }
834
+ const trimmedKey = licenseKey.trim();
835
+ const trimmedEmail = email.trim().toLowerCase();
836
+ const licenseService2 = strapi2.plugin("magic-editor-x").service("licenseService");
837
+ const verification = await licenseService2.verifyLicense(trimmedKey);
838
+ if (!verification.valid) {
839
+ strapi2.log.warn(`[Magic Editor X] [WARNING] Invalid license key attempted: ${trimmedKey.substring(0, 8)}...`);
840
+ return ctx.badRequest("Invalid or expired license key");
841
+ }
842
+ const license2 = await licenseService2.getLicenseByKey(trimmedKey);
843
+ if (!license2) {
844
+ return ctx.badRequest("License not found");
845
+ }
846
+ if (license2.email.toLowerCase() !== trimmedEmail) {
847
+ strapi2.log.warn(`[Magic Editor X] [WARNING] Email mismatch for license key`);
848
+ return ctx.badRequest("Email address does not match this license key");
849
+ }
850
+ await licenseService2.storeLicenseKey(trimmedKey);
851
+ const pingInterval = licenseService2.startPinging(trimmedKey, 15);
852
+ const tier = await licenseService2.getCurrentTier();
853
+ strapi2.licenseGuardEditorX = {
854
+ licenseKey: trimmedKey,
855
+ pingInterval,
856
+ data: verification.data,
857
+ tier
858
+ };
859
+ strapi2.log.info(`[Magic Editor X] [SUCCESS] License validated and stored`);
860
+ return ctx.send({
861
+ success: true,
862
+ message: "License activated successfully",
863
+ tier,
864
+ data: verification.data
865
+ });
866
+ } catch (error) {
867
+ strapi2.log.error("[Magic Editor X] Error storing license key:", error);
868
+ return ctx.badRequest("Error storing license key");
869
+ }
870
+ },
871
+ /**
872
+ * Get license limits and available features
873
+ */
874
+ async getLimits(ctx) {
875
+ try {
876
+ const licenseService2 = strapi2.plugin("magic-editor-x").service("licenseService");
877
+ const tier = await licenseService2.getCurrentTier();
878
+ const tierConfig = licenseService2.getTierConfig(tier);
879
+ const collaboratorCheck = await licenseService2.canAddCollaborator();
880
+ ctx.body = {
881
+ success: true,
882
+ tier,
883
+ tierName: tierConfig.name,
884
+ limits: {
885
+ collaborators: {
886
+ current: collaboratorCheck.current,
887
+ max: collaboratorCheck.max,
888
+ unlimited: collaboratorCheck.unlimited,
889
+ canAdd: collaboratorCheck.canAdd
890
+ }
891
+ },
892
+ features: tierConfig.features
893
+ };
894
+ } catch (error) {
895
+ strapi2.log.error("[Magic Editor X] Error getting license limits:", error);
896
+ ctx.throw(500, "Error getting license limits");
897
+ }
898
+ },
899
+ /**
900
+ * Check if user can add a collaborator (used by frontend)
901
+ */
902
+ async canAddCollaborator(ctx) {
903
+ try {
904
+ const licenseService2 = strapi2.plugin("magic-editor-x").service("licenseService");
905
+ const result = await licenseService2.canAddCollaborator();
906
+ ctx.body = {
907
+ success: true,
908
+ ...result
909
+ };
910
+ } catch (error) {
911
+ strapi2.log.error("[Magic Editor X] Error checking collaborator limit:", error);
912
+ ctx.throw(500, "Error checking collaborator limit");
913
+ }
914
+ }
915
+ });
916
+ const editor = editorController;
917
+ const realtime = realtimeController;
918
+ const collaboration = collaborationController;
919
+ const license$1 = licenseController;
920
+ var controllers$1 = {
921
+ editor,
922
+ realtime,
923
+ collaboration,
924
+ license: license$1
925
+ };
926
+ var middlewares$1 = {};
927
+ var policies$1 = {};
928
+ var admin$1 = {
929
+ type: "admin",
930
+ routes: [
931
+ // Collaboration Session
932
+ {
933
+ method: "POST",
934
+ path: "/collab/session",
935
+ handler: "realtime.createSession",
936
+ config: {
937
+ policies: []
938
+ }
939
+ },
940
+ // Collaboration Users & Permissions
941
+ {
942
+ method: "GET",
943
+ path: "/collaboration/users",
944
+ handler: "collaboration.listAdminUsers",
945
+ config: {
946
+ policies: ["admin::isAuthenticatedAdmin"]
947
+ }
948
+ },
949
+ {
950
+ method: "GET",
951
+ path: "/collaboration/permissions",
952
+ handler: "collaboration.listPermissions",
953
+ config: {
954
+ policies: ["admin::isAuthenticatedAdmin"]
955
+ }
956
+ },
957
+ {
958
+ method: "POST",
959
+ path: "/collaboration/permissions",
960
+ handler: "collaboration.createPermission",
961
+ config: {
962
+ policies: ["admin::isAuthenticatedAdmin"]
963
+ }
964
+ },
965
+ {
966
+ method: "PUT",
967
+ path: "/collaboration/permissions/:id",
968
+ handler: "collaboration.updatePermission",
969
+ config: {
970
+ policies: ["admin::isAuthenticatedAdmin"]
971
+ }
972
+ },
973
+ {
974
+ method: "DELETE",
975
+ path: "/collaboration/permissions/:id",
976
+ handler: "collaboration.deletePermission",
977
+ config: {
978
+ policies: ["admin::isAuthenticatedAdmin"]
979
+ }
980
+ },
981
+ {
982
+ method: "GET",
983
+ path: "/collaboration/check-access",
984
+ handler: "collaboration.checkAccess",
985
+ config: {
986
+ policies: ["admin::isAuthenticatedAdmin"]
987
+ }
988
+ },
989
+ // License Management
990
+ {
991
+ method: "GET",
992
+ path: "/license/status",
993
+ handler: "license.getStatus",
994
+ config: {
995
+ policies: ["admin::isAuthenticatedAdmin"]
996
+ }
997
+ },
998
+ {
999
+ method: "POST",
1000
+ path: "/license/auto-create",
1001
+ handler: "license.autoCreate",
1002
+ config: {
1003
+ policies: ["admin::isAuthenticatedAdmin"]
1004
+ }
1005
+ },
1006
+ {
1007
+ method: "POST",
1008
+ path: "/license/store-key",
1009
+ handler: "license.storeKey",
1010
+ config: {
1011
+ policies: ["admin::isAuthenticatedAdmin"]
1012
+ }
1013
+ },
1014
+ {
1015
+ method: "GET",
1016
+ path: "/license/limits",
1017
+ handler: "license.getLimits",
1018
+ config: {
1019
+ policies: ["admin::isAuthenticatedAdmin"]
1020
+ }
1021
+ },
1022
+ {
1023
+ method: "GET",
1024
+ path: "/license/can-add-collaborator",
1025
+ handler: "license.canAddCollaborator",
1026
+ config: {
1027
+ policies: ["admin::isAuthenticatedAdmin"]
1028
+ }
1029
+ }
1030
+ ]
1031
+ };
1032
+ var contentApi$1 = {
1033
+ type: "content-api",
1034
+ routes: [
1035
+ /**
1036
+ * Link Preview Endpoint
1037
+ * GET /api/magic-editor-x/link?url=https://example.com
1038
+ * Returns OpenGraph metadata for URL
1039
+ */
1040
+ {
1041
+ method: "GET",
1042
+ path: "/link",
1043
+ handler: "editor.fetchLinkMeta",
1044
+ config: {
1045
+ description: "Fetch link metadata (OpenGraph) for URL preview",
1046
+ auth: false,
1047
+ policies: []
1048
+ }
1049
+ },
1050
+ /**
1051
+ * Upload Image by File
1052
+ * POST /api/magic-editor-x/image/byFile
1053
+ * Multipart form data with files.image
1054
+ */
1055
+ {
1056
+ method: "POST",
1057
+ path: "/image/byFile",
1058
+ handler: "editor.uploadByFile",
1059
+ config: {
1060
+ description: "Upload image by file to Strapi Media Library",
1061
+ auth: false,
1062
+ policies: []
1063
+ }
1064
+ },
1065
+ /**
1066
+ * Upload Image by URL
1067
+ * POST /api/magic-editor-x/image/byUrl
1068
+ * JSON body with url field
1069
+ */
1070
+ {
1071
+ method: "POST",
1072
+ path: "/image/byUrl",
1073
+ handler: "editor.uploadByUrl",
1074
+ config: {
1075
+ description: "Upload image by URL to Strapi Media Library",
1076
+ auth: false,
1077
+ policies: []
1078
+ }
1079
+ },
1080
+ /**
1081
+ * Upload File (for Attaches Tool)
1082
+ * POST /api/magic-editor-x/file/upload
1083
+ * Multipart form data with file
1084
+ */
1085
+ {
1086
+ method: "POST",
1087
+ path: "/file/upload",
1088
+ handler: "editor.uploadFile",
1089
+ config: {
1090
+ description: "Upload file to Strapi Media Library (for Attaches)",
1091
+ auth: false,
1092
+ policies: []
1093
+ }
1094
+ }
1095
+ ]
1096
+ };
1097
+ const admin = admin$1;
1098
+ const contentApi = contentApi$1;
1099
+ var routes$1 = {
1100
+ admin,
1101
+ "content-api": contentApi
1102
+ };
1103
+ const ogs = require$$0__default.default;
1104
+ const fs = require$$1__default.default;
1105
+ const path = require$$2__default.default;
1106
+ const https = require$$3__default.default;
1107
+ const http = require$$4__default.default;
1108
+ const { URL } = require$$5__default.default;
1109
+ var editorService$1 = ({ strapi: strapi2 }) => ({
1110
+ /**
1111
+ * Fetch OpenGraph metadata for a URL
1112
+ * @param {string} url - URL to fetch metadata from
1113
+ * @returns {object} EditorJS compatible link data
1114
+ */
1115
+ async fetchLinkMeta(url) {
1116
+ try {
1117
+ const options = {
1118
+ url,
1119
+ timeout: 1e4,
1120
+ fetchOptions: {
1121
+ headers: {
1122
+ "User-Agent": "Mozilla/5.0 (compatible; MagicEditorX/1.0)"
1123
+ }
1124
+ }
1125
+ };
1126
+ const { result, error } = await ogs(options);
1127
+ if (error) {
1128
+ strapi2.log.warn("[Magic Editor X] OGS error:", error);
1129
+ return {
1130
+ success: 1,
1131
+ meta: {
1132
+ title: url,
1133
+ description: "",
1134
+ image: void 0
1135
+ }
1136
+ };
1137
+ }
1138
+ let imageUrl = void 0;
1139
+ if (result.ogImage) {
1140
+ if (Array.isArray(result.ogImage) && result.ogImage.length > 0) {
1141
+ imageUrl = { url: result.ogImage[0].url };
1142
+ } else if (result.ogImage.url) {
1143
+ imageUrl = { url: result.ogImage.url };
1144
+ }
1145
+ }
1146
+ return {
1147
+ success: 1,
1148
+ meta: {
1149
+ title: result.ogTitle || result.dcTitle || url,
1150
+ description: result.ogDescription || result.dcDescription || "",
1151
+ image: imageUrl,
1152
+ siteName: result.ogSiteName || "",
1153
+ url: result.ogUrl || url
1154
+ }
1155
+ };
1156
+ } catch (error) {
1157
+ strapi2.log.error("[Magic Editor X] Link meta fetch error:", error);
1158
+ return {
1159
+ success: 1,
1160
+ meta: {
1161
+ title: url,
1162
+ description: "",
1163
+ image: void 0
1164
+ }
1165
+ };
1166
+ }
1167
+ },
1168
+ /**
1169
+ * Upload image from multipart form data
1170
+ * @param {object} ctx - Koa context
1171
+ * @returns {object} EditorJS compatible upload result
1172
+ */
1173
+ async uploadByFile(ctx) {
1174
+ try {
1175
+ const { files: files2 } = ctx.request;
1176
+ if (!files2 || !files2["files.image"]) {
1177
+ throw new Error("No file provided");
1178
+ }
1179
+ const file = files2["files.image"];
1180
+ const uploadService = strapi2.plugin("upload").service("upload");
1181
+ const uploadedFiles = await uploadService.upload({
1182
+ data: {},
1183
+ files: Array.isArray(file) ? file : [file]
1184
+ });
1185
+ const uploadedFile = uploadedFiles[0];
1186
+ return {
1187
+ success: 1,
1188
+ file: {
1189
+ url: uploadedFile.url,
1190
+ name: uploadedFile.name,
1191
+ size: uploadedFile.size,
1192
+ width: uploadedFile.width,
1193
+ height: uploadedFile.height,
1194
+ mime: uploadedFile.mime,
1195
+ formats: uploadedFile.formats
1196
+ }
1197
+ };
1198
+ } catch (error) {
1199
+ strapi2.log.error("[Magic Editor X] File upload error:", error);
1200
+ throw error;
1201
+ }
1202
+ },
1203
+ /**
1204
+ * Download and upload image from URL
1205
+ * @param {string} imageUrl - URL of image to download
1206
+ * @returns {object} EditorJS compatible upload result
1207
+ */
1208
+ async uploadByUrl(imageUrl) {
1209
+ try {
1210
+ const parsedUrl = new URL(imageUrl);
1211
+ const pathname = parsedUrl.pathname;
1212
+ const ext = path.extname(pathname) || ".jpg";
1213
+ const name2 = path.basename(pathname, ext) || "image";
1214
+ const filename = `${name2}${ext}`;
1215
+ const tempDir = path.join(strapi2.dirs.static.public, "uploads", "temp");
1216
+ if (!fs.existsSync(tempDir)) {
1217
+ fs.mkdirSync(tempDir, { recursive: true });
1218
+ }
1219
+ const tempFilePath = path.join(tempDir, `${Date.now()}-${filename}`);
1220
+ const buffer = await this.downloadFile(imageUrl);
1221
+ await fs.promises.writeFile(tempFilePath, buffer);
1222
+ const stats = await fs.promises.stat(tempFilePath);
1223
+ const fileData = {
1224
+ path: tempFilePath,
1225
+ name: filename,
1226
+ type: this.getMimeType(ext),
1227
+ size: stats.size
1228
+ };
1229
+ const uploadService = strapi2.plugin("upload").service("upload");
1230
+ const uploadedFiles = await uploadService.upload({
1231
+ data: {},
1232
+ files: fileData
1233
+ });
1234
+ try {
1235
+ await fs.promises.unlink(tempFilePath);
1236
+ } catch (unlinkError) {
1237
+ strapi2.log.warn("[Magic Editor X] Could not delete temp file:", unlinkError);
1238
+ }
1239
+ const uploadedFile = uploadedFiles[0];
1240
+ return {
1241
+ success: 1,
1242
+ file: {
1243
+ url: uploadedFile.url,
1244
+ name: uploadedFile.name,
1245
+ size: uploadedFile.size,
1246
+ width: uploadedFile.width,
1247
+ height: uploadedFile.height,
1248
+ mime: uploadedFile.mime,
1249
+ formats: uploadedFile.formats
1250
+ }
1251
+ };
1252
+ } catch (error) {
1253
+ strapi2.log.error("[Magic Editor X] URL upload error:", error);
1254
+ throw error;
1255
+ }
1256
+ },
1257
+ /**
1258
+ * Download file from URL
1259
+ * @param {string} url - URL to download from
1260
+ * @returns {Promise<Buffer>} File buffer
1261
+ */
1262
+ downloadFile(url) {
1263
+ return new Promise((resolve, reject) => {
1264
+ const protocol = url.startsWith("https") ? https : http;
1265
+ const request = protocol.get(url, {
1266
+ headers: {
1267
+ "User-Agent": "Mozilla/5.0 (compatible; MagicEditorX/1.0)"
1268
+ }
1269
+ }, (response) => {
1270
+ if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
1271
+ return this.downloadFile(response.headers.location).then(resolve).catch(reject);
1272
+ }
1273
+ if (response.statusCode !== 200) {
1274
+ reject(new Error(`Failed to download: ${response.statusCode}`));
1275
+ return;
1276
+ }
1277
+ const chunks = [];
1278
+ response.on("data", (chunk) => chunks.push(chunk));
1279
+ response.on("end", () => resolve(Buffer.concat(chunks)));
1280
+ response.on("error", reject);
1281
+ });
1282
+ request.on("error", reject);
1283
+ request.setTimeout(3e4, () => {
1284
+ request.destroy();
1285
+ reject(new Error("Download timeout"));
1286
+ });
1287
+ });
1288
+ },
1289
+ /**
1290
+ * Get MIME type from file extension
1291
+ * @param {string} ext - File extension
1292
+ * @returns {string} MIME type
1293
+ */
1294
+ getMimeType(ext) {
1295
+ const mimeTypes = {
1296
+ ".jpg": "image/jpeg",
1297
+ ".jpeg": "image/jpeg",
1298
+ ".png": "image/png",
1299
+ ".gif": "image/gif",
1300
+ ".webp": "image/webp",
1301
+ ".svg": "image/svg+xml",
1302
+ ".ico": "image/x-icon",
1303
+ ".bmp": "image/bmp",
1304
+ ".tiff": "image/tiff",
1305
+ ".tif": "image/tiff",
1306
+ // Documents
1307
+ ".pdf": "application/pdf",
1308
+ ".doc": "application/msword",
1309
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1310
+ ".xls": "application/vnd.ms-excel",
1311
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1312
+ ".ppt": "application/vnd.ms-powerpoint",
1313
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1314
+ // Archives
1315
+ ".zip": "application/zip",
1316
+ ".rar": "application/vnd.rar",
1317
+ ".7z": "application/x-7z-compressed",
1318
+ ".tar": "application/x-tar",
1319
+ ".gz": "application/gzip",
1320
+ // Text
1321
+ ".txt": "text/plain",
1322
+ ".csv": "text/csv",
1323
+ ".json": "application/json",
1324
+ ".xml": "application/xml",
1325
+ // Audio
1326
+ ".mp3": "audio/mpeg",
1327
+ ".wav": "audio/wav",
1328
+ ".ogg": "audio/ogg",
1329
+ // Video
1330
+ ".mp4": "video/mp4",
1331
+ ".webm": "video/webm",
1332
+ ".avi": "video/x-msvideo"
1333
+ };
1334
+ return mimeTypes[ext.toLowerCase()] || "application/octet-stream";
1335
+ },
1336
+ /**
1337
+ * Upload attachment file (for Attaches Tool)
1338
+ * @param {object} ctx - Koa context
1339
+ * @returns {object} EditorJS compatible attachment result
1340
+ */
1341
+ async uploadAttachment(ctx) {
1342
+ try {
1343
+ const { files: files2 } = ctx.request;
1344
+ const file = files2?.file || files2?.["files.file"] || Object.values(files2 || {})[0];
1345
+ if (!file) {
1346
+ throw new Error("No file provided");
1347
+ }
1348
+ const uploadService = strapi2.plugin("upload").service("upload");
1349
+ const uploadedFiles = await uploadService.upload({
1350
+ data: {},
1351
+ files: Array.isArray(file) ? file : [file]
1352
+ });
1353
+ const uploadedFile = uploadedFiles[0];
1354
+ const ext = path.extname(uploadedFile.name);
1355
+ return {
1356
+ success: 1,
1357
+ file: {
1358
+ url: uploadedFile.url,
1359
+ name: uploadedFile.name,
1360
+ title: uploadedFile.name,
1361
+ size: uploadedFile.size,
1362
+ extension: ext.replace(".", "")
1363
+ }
1364
+ };
1365
+ } catch (error) {
1366
+ strapi2.log.error("[Magic Editor X] Attachment upload error:", error);
1367
+ throw error;
1368
+ }
1369
+ }
1370
+ });
1371
+ const { randomUUID } = require$$0__default$1.default;
1372
+ const { Server } = require$$1__default$1.default;
1373
+ const Y = require$$2__default$1.default;
1374
+ const pluginId$1 = "magic-editor-x";
1375
+ const DEFAULT_SOCKET_PATH = "/magic-editor-x/realtime";
1376
+ const DEFAULT_CORS_CONFIG = {
1377
+ origin: "*",
1378
+ methods: ["GET", "POST"],
1379
+ credentials: true
1380
+ };
1381
+ const isPluginIoActive = (strapi2) => {
1382
+ try {
1383
+ return !!strapi2.$io;
1384
+ } catch {
1385
+ return false;
1386
+ }
1387
+ };
1388
+ var realtimeService$1 = ({ strapi: strapi2 }) => {
1389
+ const rooms = /* @__PURE__ */ new Map();
1390
+ const sessionTokens = /* @__PURE__ */ new Map();
1391
+ let io;
1392
+ let cleanupInterval;
1393
+ const getConfig = () => strapi2.config.get(`plugin::${pluginId$1}`, {});
1394
+ const cleanupStaleRooms = () => {
1395
+ if (!io) return;
1396
+ const now = Date.now();
1397
+ const STALE_THRESHOLD = 60 * 60 * 1e3;
1398
+ let removedCount = 0;
1399
+ rooms.forEach((room, roomId) => {
1400
+ const socketsInRoom = io.sockets.adapter.rooms.get(roomId);
1401
+ const connectionCount = socketsInRoom ? socketsInRoom.size : 0;
1402
+ if (connectionCount === 0) {
1403
+ if (now - room.updatedAt > STALE_THRESHOLD) {
1404
+ room.doc.destroy();
1405
+ rooms.delete(roomId);
1406
+ removedCount++;
1407
+ }
1408
+ }
1409
+ });
1410
+ if (removedCount > 0) {
1411
+ strapi2.log.info(`[Magic Editor X] [CLEANUP] Removed ${removedCount} stale rooms`);
1412
+ }
1413
+ };
1414
+ if (!cleanupInterval) {
1415
+ cleanupInterval = setInterval(cleanupStaleRooms, 15 * 60 * 1e3);
1416
+ }
1417
+ const ensureRoom = (roomId) => {
1418
+ if (!rooms.has(roomId)) {
1419
+ const doc = new Y.Doc();
1420
+ rooms.set(roomId, {
1421
+ roomId,
1422
+ doc,
1423
+ initialized: false,
1424
+ createdAt: Date.now(),
1425
+ updatedAt: Date.now(),
1426
+ meta: {}
1427
+ });
1428
+ doc.on("update", () => {
1429
+ const room = rooms.get(roomId);
1430
+ if (room) {
1431
+ room.initialized = true;
1432
+ room.updatedAt = Date.now();
1433
+ }
1434
+ });
1435
+ }
1436
+ return rooms.get(roomId);
1437
+ };
1438
+ const initializeDoc = (roomId, initialValue) => {
1439
+ const room = ensureRoom(roomId);
1440
+ const blocksMap = room.doc.getMap("blocks");
1441
+ const isDocEmpty = blocksMap.size === 0;
1442
+ if (!initialValue || !isDocEmpty && room.initialized) {
1443
+ return room;
1444
+ }
1445
+ try {
1446
+ const data = JSON.parse(initialValue);
1447
+ const blocks = data?.blocks || [];
1448
+ const time = data?.time || Date.now();
1449
+ room.doc.transact(() => {
1450
+ const metaMap = room.doc.getMap("meta");
1451
+ for (const block of blocks) {
1452
+ if (block.id) {
1453
+ blocksMap.set(block.id, JSON.stringify(block));
1454
+ }
1455
+ }
1456
+ metaMap.set("time", time);
1457
+ metaMap.set("blockOrder", JSON.stringify(blocks.map((b) => b.id)));
1458
+ }, "bootstrap");
1459
+ room.initialized = true;
1460
+ strapi2.log.info(`[Magic Editor X] [INIT] Initialized room ${roomId} with ${blocks.length} blocks`);
1461
+ } catch (error) {
1462
+ strapi2.log.error(`[Magic Editor X] Failed to initialize Y.Doc for room ${roomId}`, error);
1463
+ }
1464
+ return room;
1465
+ };
1466
+ const getStateUpdate = (roomId) => {
1467
+ const room = ensureRoom(roomId);
1468
+ try {
1469
+ return Y.encodeStateAsUpdate(room.doc);
1470
+ } catch (error) {
1471
+ strapi2.log.error(`[Magic Editor X] Failed to encode state for room ${roomId}`, error);
1472
+ return null;
1473
+ }
1474
+ };
1475
+ const applyUpdate2 = (roomId, update, origin = "remote") => {
1476
+ if (!update) {
1477
+ return;
1478
+ }
1479
+ const room = ensureRoom(roomId);
1480
+ try {
1481
+ Y.applyUpdate(room.doc, update, origin);
1482
+ } catch (error) {
1483
+ strapi2.log.error(`[Magic Editor X] Failed to apply update for room ${roomId}`, error);
1484
+ }
1485
+ };
1486
+ const issueSession = ({ roomId, fieldName, meta = {}, user, initialValue = "" }) => {
1487
+ const pluginConfig = getConfig();
1488
+ const collabConfig = pluginConfig.collaboration || {};
1489
+ if (collabConfig.enabled === false) {
1490
+ throw new Error("collaboration-disabled");
1491
+ }
1492
+ initializeDoc(roomId, initialValue);
1493
+ const token = randomUUID();
1494
+ const expiresAt = Date.now() + (collabConfig.sessionTTL || 2 * 60 * 1e3);
1495
+ sessionTokens.set(token, {
1496
+ token,
1497
+ roomId,
1498
+ fieldName,
1499
+ meta,
1500
+ user: {
1501
+ id: user.id,
1502
+ firstname: user.firstname,
1503
+ lastname: user.lastname,
1504
+ email: user.email,
1505
+ roles: user.roles?.map((role) => ({
1506
+ id: role.id,
1507
+ code: role.code,
1508
+ name: role.name
1509
+ })) || []
1510
+ },
1511
+ expiresAt
1512
+ });
1513
+ const actualPath = io?._magicEditorPath || collabConfig.wsPath || DEFAULT_SOCKET_PATH;
1514
+ return {
1515
+ token,
1516
+ roomId,
1517
+ expiresAt,
1518
+ wsPath: actualPath,
1519
+ wsUrl: collabConfig.wsUrl || void 0,
1520
+ approvals: {
1521
+ roleApproved: true
1522
+ }
1523
+ };
1524
+ };
1525
+ const consumeSessionToken = (token) => {
1526
+ if (!token) {
1527
+ return null;
1528
+ }
1529
+ const session = sessionTokens.get(token);
1530
+ if (!session) {
1531
+ return null;
1532
+ }
1533
+ if (session.expiresAt < Date.now()) {
1534
+ sessionTokens.delete(token);
1535
+ return null;
1536
+ }
1537
+ sessionTokens.delete(token);
1538
+ return session;
1539
+ };
1540
+ const initSocketServer = () => {
1541
+ const pluginConfig = getConfig();
1542
+ const collabConfig = pluginConfig.collaboration || {};
1543
+ if (collabConfig.enabled === false) {
1544
+ strapi2.log.info("[Magic Editor X] Realtime server disabled (collaboration.enabled=false)");
1545
+ return null;
1546
+ }
1547
+ if (io) {
1548
+ return io;
1549
+ }
1550
+ const httpServer = strapi2.server.httpServer;
1551
+ if (!httpServer) {
1552
+ strapi2.log.warn("[Magic Editor X] HTTP server not ready. Realtime collaboration skipped.");
1553
+ return null;
1554
+ }
1555
+ if (isPluginIoActive(strapi2)) {
1556
+ strapi2.log.info("[Magic Editor X] [INFO] strapi-plugin-io detected - using separate namespace");
1557
+ }
1558
+ const wsPath = collabConfig.wsPath || DEFAULT_SOCKET_PATH;
1559
+ if (wsPath === "/socket.io") {
1560
+ strapi2.log.warn('[Magic Editor X] [WARNING] wsPath "/socket.io" conflicts with strapi-plugin-io!');
1561
+ strapi2.log.warn("[Magic Editor X] Using default path instead: " + DEFAULT_SOCKET_PATH);
1562
+ }
1563
+ const finalPath = wsPath === "/socket.io" ? DEFAULT_SOCKET_PATH : wsPath;
1564
+ strapi2.log.info(`[Magic Editor X] [SOCKET] Starting Socket.io server on path: ${finalPath}`);
1565
+ io = new Server(httpServer, {
1566
+ path: finalPath,
1567
+ cors: collabConfig.cors || DEFAULT_CORS_CONFIG,
1568
+ transports: ["websocket", "polling"],
1569
+ allowEIO3: true,
1570
+ // Backward compatibility
1571
+ // Avoid conflicts with other Socket.io instances
1572
+ serveClient: false,
1573
+ // Don't serve socket.io client files
1574
+ connectTimeout: 45e3
1575
+ });
1576
+ io._magicEditorPath = finalPath;
1577
+ io.on("connection", (socket) => {
1578
+ const token = socket.handshake.auth?.token;
1579
+ strapi2.log.info(`[Magic Editor X] [SOCKET] Client connecting with token: ${token ? "valid" : "missing"}`);
1580
+ const session = consumeSessionToken(token);
1581
+ if (!session) {
1582
+ strapi2.log.warn("[Magic Editor X] [WARNING] Invalid or expired token");
1583
+ socket.emit("collab:error", { code: "INVALID_TOKEN", message: "Invalid or expired session token" });
1584
+ socket.disconnect(true);
1585
+ return;
1586
+ }
1587
+ const { roomId, user } = session;
1588
+ socket.data.user = user;
1589
+ socket.data.roomId = roomId;
1590
+ socket.join(roomId);
1591
+ strapi2.log.info(`[Magic Editor X] [SUCCESS] User ${user.email} joined room: ${roomId}`);
1592
+ const initialState = getStateUpdate(roomId);
1593
+ if (initialState) {
1594
+ const stateArray = Array.from(initialState);
1595
+ socket.emit("collab:sync", stateArray);
1596
+ strapi2.log.info(`[Magic Editor X] [SYNC] Sent initial state (${stateArray.length} bytes)`);
1597
+ }
1598
+ const socketsInRoom = io.sockets.adapter.rooms.get(roomId);
1599
+ if (socketsInRoom) {
1600
+ const existingPeers = [];
1601
+ for (const socketId of socketsInRoom) {
1602
+ const peerSocket = io.sockets.sockets.get(socketId);
1603
+ if (peerSocket && peerSocket.data.user && peerSocket.id !== socket.id) {
1604
+ existingPeers.push(peerSocket.data.user);
1605
+ }
1606
+ }
1607
+ existingPeers.forEach((peerUser) => {
1608
+ socket.emit("collab:presence", { type: "join", user: peerUser });
1609
+ });
1610
+ strapi2.log.info(`[Magic Editor X] [PEERS] Sent ${existingPeers.length} existing peers to new user`);
1611
+ }
1612
+ socket.to(roomId).emit("collab:presence", { type: "join", user });
1613
+ socket.on("collab:update", (update) => {
1614
+ try {
1615
+ const updateBuffer = new Uint8Array(update);
1616
+ applyUpdate2(roomId, updateBuffer, "remote");
1617
+ socket.to(roomId).emit("collab:update", update);
1618
+ strapi2.log.debug(`[Magic Editor X] [BROADCAST] Update broadcast to room ${roomId}`);
1619
+ } catch (error) {
1620
+ strapi2.log.error("[Magic Editor X] Failed to process update:", error);
1621
+ socket.emit("collab:error", { code: "UPDATE_FAILED", message: "Failed to process update" });
1622
+ }
1623
+ });
1624
+ socket.on("collab:awareness", (payload) => {
1625
+ socket.to(roomId).emit("collab:awareness", { user, payload });
1626
+ });
1627
+ socket.on("disconnect", (reason) => {
1628
+ strapi2.log.info(`[Magic Editor X] [DISCONNECT] User ${user.email} left room ${roomId} (${reason})`);
1629
+ socket.to(roomId).emit("collab:presence", { type: "leave", user });
1630
+ });
1631
+ socket.on("error", (error) => {
1632
+ strapi2.log.error("[Magic Editor X] Socket error:", error);
1633
+ });
1634
+ });
1635
+ strapi2.log.info("[Magic Editor X] [SUCCESS] Realtime collaboration server ready");
1636
+ return io;
1637
+ };
1638
+ const close = async () => {
1639
+ if (cleanupInterval) {
1640
+ clearInterval(cleanupInterval);
1641
+ cleanupInterval = null;
1642
+ }
1643
+ if (io) {
1644
+ await io.close();
1645
+ io = null;
1646
+ }
1647
+ sessionTokens.clear();
1648
+ rooms.forEach((room) => room.doc.destroy());
1649
+ rooms.clear();
1650
+ };
1651
+ return {
1652
+ issueSession,
1653
+ consumeSessionToken,
1654
+ applyUpdate: applyUpdate2,
1655
+ initSocketServer,
1656
+ close
1657
+ };
1658
+ };
1659
+ const pluginId = "magic-editor-x";
1660
+ const getRoleCodes = (user) => {
1661
+ if (!user?.roles) {
1662
+ return [];
1663
+ }
1664
+ return user.roles.map((role) => role?.code || role?.name).filter(Boolean);
1665
+ };
1666
+ var accessService$1 = ({ strapi: strapi2 }) => {
1667
+ const getConfig = () => strapi2.config.get(`plugin::${pluginId}`, {});
1668
+ return {
1669
+ /**
1670
+ * Prüft ob ein User Collaboration nutzen darf
1671
+ * Standard: Nur Super Admins haben automatisch Zugriff
1672
+ * Alle anderen brauchen explizite Freigabe über collab-permission
1673
+ *
1674
+ * @param {Object} user - Der Admin User
1675
+ * @param {string} contentType - Optional: Der spezifische Content Type (z.B. 'api::article.article')
1676
+ */
1677
+ async canUseCollaboration(user, contentType = null) {
1678
+ if (!user) {
1679
+ strapi2.log.warn("[Access Service] No user provided");
1680
+ return { allowed: false, reason: "not-authenticated", role: null };
1681
+ }
1682
+ strapi2.log.info("[Access Service] Checking access for user:", user.email, "contentType:", contentType);
1683
+ const config2 = getConfig();
1684
+ const collab = config2.collaboration || {};
1685
+ if (collab.enabled === false) {
1686
+ strapi2.log.info("[Access Service] Collaboration is disabled");
1687
+ return { allowed: false, reason: "collaboration-disabled", role: null };
1688
+ }
1689
+ const userRoleCodes = getRoleCodes(user);
1690
+ const isSuperAdmin = userRoleCodes.includes("strapi-super-admin");
1691
+ strapi2.log.info("[Access Service] User roles:", userRoleCodes, "isSuperAdmin:", isSuperAdmin);
1692
+ if (isSuperAdmin) {
1693
+ strapi2.log.info("[Access Service] [SUCCESS] Super Admin access granted");
1694
+ return { allowed: true, reason: "super-admin", role: "owner" };
1695
+ }
1696
+ try {
1697
+ const permissions = await strapi2.documents("plugin::magic-editor-x.collab-permission").findMany({
1698
+ filters: {
1699
+ user: { id: user.id }
1700
+ }
1701
+ });
1702
+ strapi2.log.info("[Access Service] Found permissions:", permissions?.length || 0);
1703
+ if (permissions && permissions.length > 0) {
1704
+ let bestPermission = null;
1705
+ for (const perm of permissions) {
1706
+ strapi2.log.info("[Access Service] Checking permission:", {
1707
+ permContentType: perm.contentType,
1708
+ requestedContentType: contentType,
1709
+ role: perm.role,
1710
+ expiresAt: perm.expiresAt
1711
+ });
1712
+ if (perm.expiresAt && new Date(perm.expiresAt) < /* @__PURE__ */ new Date()) {
1713
+ strapi2.log.info("[Access Service] Permission expired, skipping");
1714
+ continue;
1715
+ }
1716
+ if (!perm.contentType || perm.contentType === "*" || perm.contentType === "") {
1717
+ strapi2.log.info("[Access Service] ✅ Global permission found");
1718
+ if (!bestPermission || this.getRoleLevel(perm.role) > this.getRoleLevel(bestPermission.role)) {
1719
+ bestPermission = perm;
1720
+ }
1721
+ } else if (contentType && perm.contentType === contentType) {
1722
+ strapi2.log.info("[Access Service] ✅ Specific content type match");
1723
+ bestPermission = perm;
1724
+ break;
1725
+ } else if (!contentType || contentType === "unknown") {
1726
+ strapi2.log.info("[Access Service] ✅ Unknown/null contentType - accepting any permission");
1727
+ if (!bestPermission || this.getRoleLevel(perm.role) > this.getRoleLevel(bestPermission.role)) {
1728
+ bestPermission = perm;
1729
+ }
1730
+ }
1731
+ }
1732
+ if (bestPermission) {
1733
+ strapi2.log.info("[Access Service] ✅ Permission granted via collab-permission, role:", bestPermission.role);
1734
+ return {
1735
+ allowed: true,
1736
+ reason: "explicit-permission",
1737
+ role: bestPermission.role,
1738
+ permission: bestPermission
1739
+ };
1740
+ }
1741
+ }
1742
+ } catch (error) {
1743
+ strapi2.log.error("[Access Service] Error checking permissions:", error);
1744
+ }
1745
+ strapi2.log.info("[Access Service] [DENIED] No permission found for user");
1746
+ return { allowed: false, reason: "permission-required", role: null };
1747
+ },
1748
+ /**
1749
+ * Hilfsfunktion: Gibt Rollen-Level zurück (höher = mehr Rechte)
1750
+ */
1751
+ getRoleLevel(role) {
1752
+ const levels = { viewer: 1, editor: 2, owner: 3 };
1753
+ return levels[role] || 0;
1754
+ },
1755
+ /**
1756
+ * Checks if a new collaborator can be added based on license limits
1757
+ * @returns {Promise<object>} Result with canAdd, current, max, and unlimited flags
1758
+ */
1759
+ async checkCollaboratorLimit() {
1760
+ try {
1761
+ const licenseService2 = strapi2.plugin("magic-editor-x").service("licenseService");
1762
+ return await licenseService2.canAddCollaborator();
1763
+ } catch (error) {
1764
+ strapi2.log.error("[Access Service] Error checking collaborator limit:", error);
1765
+ return {
1766
+ canAdd: true,
1767
+ current: 0,
1768
+ max: 2,
1769
+ unlimited: false,
1770
+ error: true
1771
+ };
1772
+ }
1773
+ },
1774
+ /**
1775
+ * Prüft ob User Zugriff auf einen bestimmten Room hat
1776
+ */
1777
+ async canAccessRoom(userId, roomId, action = "view") {
1778
+ try {
1779
+ const user = await strapi2.query("admin::user").findOne({
1780
+ where: { id: userId },
1781
+ populate: ["roles"]
1782
+ });
1783
+ if (!user) {
1784
+ return false;
1785
+ }
1786
+ const userRoleCodes = getRoleCodes(user);
1787
+ const isSuperAdmin = userRoleCodes.includes("strapi-super-admin");
1788
+ if (isSuperAdmin) {
1789
+ return true;
1790
+ }
1791
+ let contentTypeFromRoom = null;
1792
+ if (roomId) {
1793
+ const parts = roomId.split("|");
1794
+ if (parts.length >= 1 && parts[0]?.includes("::")) {
1795
+ contentTypeFromRoom = parts[0];
1796
+ }
1797
+ }
1798
+ const permissions = await strapi2.documents("plugin::magic-editor-x.collab-permission").findMany({
1799
+ filters: {
1800
+ user: { id: userId }
1801
+ }
1802
+ });
1803
+ if (!permissions || permissions.length === 0) {
1804
+ return false;
1805
+ }
1806
+ const hasValidPermission = permissions.some((perm) => {
1807
+ if (!perm.contentType || perm.contentType === "*") {
1808
+ return true;
1809
+ }
1810
+ if (contentTypeFromRoom && perm.contentType === contentTypeFromRoom) {
1811
+ return true;
1812
+ }
1813
+ return false;
1814
+ });
1815
+ if (!hasValidPermission) {
1816
+ return false;
1817
+ }
1818
+ const permission = permissions[0];
1819
+ if (action === "view") {
1820
+ return ["viewer", "editor", "owner"].includes(permission.role);
1821
+ }
1822
+ if (action === "edit") {
1823
+ return ["editor", "owner"].includes(permission.role);
1824
+ }
1825
+ if (action === "manage") {
1826
+ return permission.role === "owner";
1827
+ }
1828
+ return false;
1829
+ } catch (error) {
1830
+ strapi2.log.error("[Access Service] Error checking room access:", error);
1831
+ return false;
1832
+ }
1833
+ }
1834
+ };
1835
+ };
1836
+ const { encodeStateAsUpdate, encodeStateVector, applyUpdate } = require$$2__default$1.default;
1837
+ var snapshotService$1 = ({ strapi: strapi2 }) => ({
1838
+ /**
1839
+ * Create snapshot from Y.Doc
1840
+ */
1841
+ async createSnapshot(roomId, contentType, entryId, fieldName, ydoc, userId) {
1842
+ try {
1843
+ const latestSnapshots = await strapi2.documents("plugin::magic-editor-x.document-snapshot").findMany({
1844
+ filters: { roomId },
1845
+ sort: [{ version: "desc" }],
1846
+ limit: 1
1847
+ });
1848
+ const nextVersion = latestSnapshots?.[0]?.version ? latestSnapshots[0].version + 1 : 1;
1849
+ const yjsState = encodeStateAsUpdate(ydoc);
1850
+ const yjsSnapshot = Buffer.from(yjsState).toString("base64");
1851
+ let jsonContent = null;
1852
+ try {
1853
+ const text = ydoc.getText("content");
1854
+ jsonContent = text.toString();
1855
+ } catch (e) {
1856
+ strapi2.log.warn("[Snapshot] Could not extract JSON content:", e);
1857
+ }
1858
+ const snapshot = await strapi2.documents("plugin::magic-editor-x.document-snapshot").create({
1859
+ data: {
1860
+ roomId,
1861
+ contentType,
1862
+ entryId,
1863
+ fieldName,
1864
+ version: nextVersion,
1865
+ yjsSnapshot,
1866
+ jsonContent: jsonContent ? JSON.parse(jsonContent) : null,
1867
+ createdBy: userId,
1868
+ createdAt: /* @__PURE__ */ new Date()
1869
+ }
1870
+ });
1871
+ strapi2.log.info(`[Snapshot] Created v${nextVersion} for ${roomId}`);
1872
+ return snapshot;
1873
+ } catch (error) {
1874
+ strapi2.log.error("[Snapshot] Error creating snapshot:", error);
1875
+ throw error;
1876
+ }
1877
+ },
1878
+ /**
1879
+ * List snapshots for a room
1880
+ */
1881
+ async listSnapshots(roomId, limit = 50) {
1882
+ return await strapi2.documents("plugin::magic-editor-x.document-snapshot").findMany({
1883
+ filters: { roomId },
1884
+ sort: [{ version: "desc" }],
1885
+ limit,
1886
+ populate: ["createdBy"]
1887
+ });
1888
+ },
1889
+ /**
1890
+ * Restore snapshot to Y.Doc
1891
+ * Note: Uses documentId instead of numeric id
1892
+ */
1893
+ async restoreSnapshot(snapshotDocumentId, ydoc) {
1894
+ try {
1895
+ const snapshot = await strapi2.documents("plugin::magic-editor-x.document-snapshot").findOne({
1896
+ documentId: snapshotDocumentId
1897
+ });
1898
+ if (!snapshot) {
1899
+ throw new Error("Snapshot not found");
1900
+ }
1901
+ const yjsState = Buffer.from(snapshot.yjsSnapshot, "base64");
1902
+ applyUpdate(ydoc, yjsState);
1903
+ strapi2.log.info(`[Snapshot] Restored v${snapshot.version} for ${snapshot.roomId}`);
1904
+ return snapshot;
1905
+ } catch (error) {
1906
+ strapi2.log.error("[Snapshot] Error restoring snapshot:", error);
1907
+ throw error;
1908
+ }
1909
+ },
1910
+ /**
1911
+ * Auto-cleanup old snapshots (keep last N versions)
1912
+ */
1913
+ async cleanupSnapshots(roomId, keepLast = 10) {
1914
+ try {
1915
+ const snapshots = await this.listSnapshots(roomId, 1e3);
1916
+ if (snapshots.length <= keepLast) {
1917
+ return { deleted: 0 };
1918
+ }
1919
+ const toDelete = snapshots.slice(keepLast);
1920
+ for (const snapshot of toDelete) {
1921
+ await strapi2.documents("plugin::magic-editor-x.document-snapshot").delete({
1922
+ documentId: snapshot.documentId
1923
+ });
1924
+ }
1925
+ strapi2.log.info(`[Snapshot] Cleaned up ${toDelete.length} old snapshots for ${roomId}`);
1926
+ return { deleted: toDelete.length };
1927
+ } catch (error) {
1928
+ strapi2.log.error("[Snapshot] Error cleaning up snapshots:", error);
1929
+ throw error;
1930
+ }
1931
+ }
1932
+ });
1933
+ const name = "magic-editor-x";
1934
+ const version = "1.0.1";
1935
+ const description = "Advanced block-based editor for Strapi v5 with Editor.js, Media Library integration, and real-time collaboration support";
1936
+ const keywords = [
1937
+ "strapi",
1938
+ "plugin",
1939
+ "editor-js",
1940
+ "wysiwyg",
1941
+ "block-editor",
1942
+ "strapi-v5"
1943
+ ];
1944
+ const type = "commonjs";
1945
+ const exports$1 = {
1946
+ "./package.json": "./package.json",
1947
+ "./strapi-admin": {
1948
+ source: "./admin/src/index.js",
1949
+ "import": "./dist/admin/index.mjs",
1950
+ require: "./dist/admin/index.js",
1951
+ "default": "./dist/admin/index.js"
1952
+ },
1953
+ "./strapi-server": {
1954
+ source: "./server/src/index.js",
1955
+ "import": "./dist/server/index.mjs",
1956
+ require: "./dist/server/index.js",
1957
+ "default": "./dist/server/index.js"
1958
+ }
1959
+ };
1960
+ const files = [
1961
+ "dist",
1962
+ "README.md",
1963
+ "LICENSE",
1964
+ "pics"
1965
+ ];
1966
+ const scripts = {
1967
+ build: "strapi-plugin build",
1968
+ watch: "strapi-plugin watch",
1969
+ "watch:link": "strapi-plugin watch:link",
1970
+ verify: "strapi-plugin verify"
1971
+ };
1972
+ const dependencies = {
1973
+ "@calumk/editorjs-codeflask": "^1.0.10",
1974
+ "@editorjs/attaches": "^1.3.0",
1975
+ "@editorjs/checklist": "^1.6.0",
1976
+ "@editorjs/code": "^2.9.3",
1977
+ "@editorjs/delimiter": "^1.4.2",
1978
+ "@editorjs/editorjs": "^2.31.0",
1979
+ "@editorjs/embed": "^2.7.6",
1980
+ "@editorjs/header": "^2.8.8",
1981
+ "@editorjs/image": "^2.10.1",
1982
+ "@editorjs/inline-code": "^1.5.1",
1983
+ "@editorjs/link": "^2.6.2",
1984
+ "@editorjs/marker": "^1.4.0",
1985
+ "@editorjs/nested-list": "^1.4.3",
1986
+ "@editorjs/paragraph": "^2.11.6",
1987
+ "@editorjs/personality": "^2.0.2",
1988
+ "@editorjs/quote": "^2.7.2",
1989
+ "@editorjs/raw": "^2.5.0",
1990
+ "@editorjs/simple-image": "^1.6.0",
1991
+ "@editorjs/table": "^2.4.2",
1992
+ "@editorjs/text-variant-tune": "^1.0.2",
1993
+ "@editorjs/underline": "^1.1.0",
1994
+ "@editorjs/warning": "^1.4.0",
1995
+ "@heroicons/react": "^2.2.0",
1996
+ "@sotaproject/strikethrough": "^1.0.1",
1997
+ "editorjs-alert": "^1.1.4",
1998
+ "editorjs-drag-drop": "^1.1.16",
1999
+ "editorjs-indent-tune": "^1.4.3",
2000
+ "editorjs-text-alignment-blocktune": "^1.0.3",
2001
+ "editorjs-toggle-block": "^0.3.16",
2002
+ "editorjs-tooltip": "^1.2.2",
2003
+ "editorjs-undo": "^2.0.28",
2004
+ "open-graph-scraper": "^6.8.3",
2005
+ prismjs: "^1.30.0",
2006
+ "socket.io": "^4.8.1",
2007
+ "socket.io-client": "^4.8.1",
2008
+ "y-indexeddb": "^9.0.12",
2009
+ "y-socket.io": "^1.1.3",
2010
+ yjs: "^13.6.15"
2011
+ };
2012
+ const devDependencies = {
2013
+ "@semantic-release/changelog": "^6.0.3",
2014
+ "@semantic-release/commit-analyzer": "^13.0.0",
2015
+ "@semantic-release/git": "^10.0.1",
2016
+ "@semantic-release/github": "^11.0.1",
2017
+ "@semantic-release/npm": "^12.0.1",
2018
+ "@semantic-release/release-notes-generator": "^14.0.1",
2019
+ "@strapi/design-system": "^2.0.0-rc.30",
2020
+ "@strapi/icons": "^2.0.0-rc.30",
2021
+ "@strapi/sdk-plugin": "^5.3.2",
2022
+ "@strapi/strapi": "^5.31.2",
2023
+ prettier: "^3.7.3",
2024
+ react: "^18.3.1",
2025
+ "react-dom": "^18.3.1",
2026
+ "react-router-dom": "^6.30.2",
2027
+ "semantic-release": "^25.0.2",
2028
+ "styled-components": "^6.1.19"
2029
+ };
2030
+ const peerDependencies = {
2031
+ "@strapi/sdk-plugin": "^5.3.2",
2032
+ "@strapi/strapi": "^5.31.2",
2033
+ react: "^18.3.1",
2034
+ "react-dom": "^18.3.1",
2035
+ "react-router-dom": "^6.30.2",
2036
+ "styled-components": "^6.1.19"
2037
+ };
2038
+ const overrides = {
2039
+ prismjs: "^1.30.0"
2040
+ };
2041
+ const strapi = {
2042
+ kind: "plugin",
2043
+ name: "magic-editor-x",
2044
+ displayName: "Magic Editor X",
2045
+ description: "Advanced block-based editor with Editor.js for Strapi v5"
2046
+ };
2047
+ const license = "MIT";
2048
+ const author = "Schero D. <schero1894@gmail.com>";
2049
+ const repository = {
2050
+ type: "git",
2051
+ url: "https://github.com/Schero94/magic-editor-x.git"
2052
+ };
2053
+ const require$$2 = {
2054
+ name,
2055
+ version,
2056
+ description,
2057
+ keywords,
2058
+ type,
2059
+ exports: exports$1,
2060
+ files,
2061
+ scripts,
2062
+ dependencies,
2063
+ devDependencies,
2064
+ peerDependencies,
2065
+ overrides,
2066
+ strapi,
2067
+ license,
2068
+ author,
2069
+ repository
2070
+ };
2071
+ const crypto = require$$0__default$1.default;
2072
+ const os = require$$1__default$2.default;
2073
+ const LICENSE_SERVER_URL = "https://magicapi.fitlex.me";
2074
+ const PLUGIN_NAME = "magic-editor-x";
2075
+ const PRODUCT_NAME = "Magic Editor X - Collaborative Editor";
2076
+ const TIERS = {
2077
+ free: {
2078
+ name: "FREE",
2079
+ maxCollaborators: 2,
2080
+ features: {
2081
+ editor: true,
2082
+ allTools: true,
2083
+ collaboration: true,
2084
+ ai: false
2085
+ }
2086
+ },
2087
+ premium: {
2088
+ name: "PREMIUM",
2089
+ maxCollaborators: 10,
2090
+ features: {
2091
+ editor: true,
2092
+ allTools: true,
2093
+ collaboration: true,
2094
+ ai: true
2095
+ }
2096
+ },
2097
+ advanced: {
2098
+ name: "ADVANCED",
2099
+ maxCollaborators: -1,
2100
+ // Unlimited
2101
+ features: {
2102
+ editor: true,
2103
+ allTools: true,
2104
+ collaboration: true,
2105
+ ai: true
2106
+ }
2107
+ },
2108
+ enterprise: {
2109
+ name: "ENTERPRISE",
2110
+ maxCollaborators: -1,
2111
+ // Unlimited
2112
+ features: {
2113
+ editor: true,
2114
+ allTools: true,
2115
+ collaboration: true,
2116
+ ai: true,
2117
+ prioritySupport: true
2118
+ }
2119
+ }
2120
+ };
2121
+ var licenseService$1 = ({ strapi: strapi2 }) => ({
2122
+ /**
2123
+ * Get license server URL
2124
+ * @returns {string} License server URL
2125
+ */
2126
+ getLicenseServerUrl() {
2127
+ return LICENSE_SERVER_URL;
2128
+ },
2129
+ /**
2130
+ * Generate unique device ID based on hardware
2131
+ * @returns {string} 32-character device ID
2132
+ */
2133
+ generateDeviceId() {
2134
+ try {
2135
+ const networkInterfaces = os.networkInterfaces();
2136
+ const macAddresses = [];
2137
+ Object.values(networkInterfaces).forEach((interfaces) => {
2138
+ interfaces?.forEach((iface) => {
2139
+ if (iface.mac && iface.mac !== "00:00:00:00:00:00") {
2140
+ macAddresses.push(iface.mac);
2141
+ }
2142
+ });
2143
+ });
2144
+ const identifier = `${macAddresses.join("-")}-${os.hostname()}`;
2145
+ return crypto.createHash("sha256").update(identifier).digest("hex").substring(0, 32);
2146
+ } catch (error) {
2147
+ return crypto.randomBytes(16).toString("hex");
2148
+ }
2149
+ },
2150
+ /**
2151
+ * Get device hostname
2152
+ * @returns {string} Device name
2153
+ */
2154
+ getDeviceName() {
2155
+ try {
2156
+ return os.hostname() || "Unknown Device";
2157
+ } catch (error) {
2158
+ return "Unknown Device";
2159
+ }
2160
+ },
2161
+ /**
2162
+ * Get external IP address
2163
+ * @returns {string} IP address
2164
+ */
2165
+ getIpAddress() {
2166
+ try {
2167
+ const networkInterfaces = os.networkInterfaces();
2168
+ for (const name2 of Object.keys(networkInterfaces)) {
2169
+ const interfaces = networkInterfaces[name2];
2170
+ if (interfaces) {
2171
+ for (const iface of interfaces) {
2172
+ if (iface.family === "IPv4" && !iface.internal) {
2173
+ return iface.address;
2174
+ }
2175
+ }
2176
+ }
2177
+ }
2178
+ return "127.0.0.1";
2179
+ } catch (error) {
2180
+ return "127.0.0.1";
2181
+ }
2182
+ },
2183
+ /**
2184
+ * Get user agent string for license requests
2185
+ * @returns {string} User agent
2186
+ */
2187
+ getUserAgent() {
2188
+ try {
2189
+ const pluginPkg = require$$2;
2190
+ const pluginVersion = pluginPkg.version || "1.0.0";
2191
+ const strapiVersion = strapi2.config.get("info.strapi") || "5.0.0";
2192
+ return `MagicEditorX/${pluginVersion} Strapi/${strapiVersion} Node/${process.version} ${os.platform()}/${os.release()}`;
2193
+ } catch (error) {
2194
+ return `MagicEditorX/1.0.0 Node/${process.version}`;
2195
+ }
2196
+ },
2197
+ /**
2198
+ * Create a new license
2199
+ * @param {object} params - License parameters
2200
+ * @param {string} params.email - User email
2201
+ * @param {string} params.firstName - User first name
2202
+ * @param {string} params.lastName - User last name
2203
+ * @returns {Promise<object|null>} Created license or null
2204
+ */
2205
+ async createLicense({ email, firstName, lastName }) {
2206
+ try {
2207
+ const deviceId = this.generateDeviceId();
2208
+ const deviceName = this.getDeviceName();
2209
+ const ipAddress = this.getIpAddress();
2210
+ const userAgent = this.getUserAgent();
2211
+ const response = await fetch(`${LICENSE_SERVER_URL}/api/licenses/create`, {
2212
+ method: "POST",
2213
+ headers: { "Content-Type": "application/json" },
2214
+ body: JSON.stringify({
2215
+ email,
2216
+ firstName,
2217
+ lastName,
2218
+ deviceName,
2219
+ deviceId,
2220
+ ipAddress,
2221
+ userAgent,
2222
+ pluginName: PLUGIN_NAME,
2223
+ productName: PRODUCT_NAME
2224
+ })
2225
+ });
2226
+ const data = await response.json();
2227
+ if (data.success) {
2228
+ strapi2.log.info(`[Magic Editor X] [SUCCESS] License created: ${data.data.licenseKey}`);
2229
+ return data.data;
2230
+ } else {
2231
+ strapi2.log.error("[Magic Editor X] [ERROR] License creation failed:", data);
2232
+ return null;
2233
+ }
2234
+ } catch (error) {
2235
+ strapi2.log.error("[Magic Editor X] [ERROR] Error creating license:", error);
2236
+ return null;
2237
+ }
2238
+ },
2239
+ /**
2240
+ * Verify a license key
2241
+ * @param {string} licenseKey - License key to verify
2242
+ * @param {boolean} allowGracePeriod - Allow offline grace period
2243
+ * @returns {Promise<object>} Verification result
2244
+ */
2245
+ async verifyLicense(licenseKey, allowGracePeriod = false) {
2246
+ try {
2247
+ const controller = new AbortController();
2248
+ const timeoutId = setTimeout(() => controller.abort(), 5e3);
2249
+ const response = await fetch(`${LICENSE_SERVER_URL}/api/licenses/verify`, {
2250
+ method: "POST",
2251
+ headers: { "Content-Type": "application/json" },
2252
+ body: JSON.stringify({
2253
+ licenseKey,
2254
+ pluginName: PLUGIN_NAME,
2255
+ productName: PRODUCT_NAME
2256
+ }),
2257
+ signal: controller.signal
2258
+ });
2259
+ clearTimeout(timeoutId);
2260
+ const data = await response.json();
2261
+ if (data.success && data.data) {
2262
+ return { valid: true, data: data.data, gracePeriod: false };
2263
+ } else {
2264
+ return { valid: false, data: null };
2265
+ }
2266
+ } catch (error) {
2267
+ if (allowGracePeriod) {
2268
+ strapi2.log.warn("[Magic Editor X] [WARNING] License verification timeout - grace period active");
2269
+ return { valid: true, data: null, gracePeriod: true };
2270
+ }
2271
+ strapi2.log.error("[Magic Editor X] [ERROR] License verification error:", error.message);
2272
+ return { valid: false, data: null };
2273
+ }
2274
+ },
2275
+ /**
2276
+ * Get license details by key
2277
+ * @param {string} licenseKey - License key
2278
+ * @returns {Promise<object|null>} License data or null
2279
+ */
2280
+ async getLicenseByKey(licenseKey) {
2281
+ try {
2282
+ const response = await fetch(`${LICENSE_SERVER_URL}/api/licenses/key/${licenseKey}`, {
2283
+ method: "GET",
2284
+ headers: { "Content-Type": "application/json" }
2285
+ });
2286
+ const data = await response.json();
2287
+ if (data.success && data.data) {
2288
+ return data.data;
2289
+ }
2290
+ return null;
2291
+ } catch (error) {
2292
+ strapi2.log.error("[Magic Editor X] Error fetching license by key:", error);
2293
+ return null;
2294
+ }
2295
+ },
2296
+ /**
2297
+ * Ping license server to update online status
2298
+ * @param {string} licenseKey - License key
2299
+ * @returns {Promise<object|null>} Ping result or null
2300
+ */
2301
+ async pingLicense(licenseKey) {
2302
+ try {
2303
+ const deviceId = this.generateDeviceId();
2304
+ const deviceName = this.getDeviceName();
2305
+ const ipAddress = this.getIpAddress();
2306
+ const userAgent = this.getUserAgent();
2307
+ const response = await fetch(`${LICENSE_SERVER_URL}/api/licenses/ping`, {
2308
+ method: "POST",
2309
+ headers: { "Content-Type": "application/json" },
2310
+ body: JSON.stringify({
2311
+ licenseKey,
2312
+ deviceId,
2313
+ deviceName,
2314
+ ipAddress,
2315
+ userAgent,
2316
+ pluginName: PLUGIN_NAME
2317
+ })
2318
+ });
2319
+ const data = await response.json();
2320
+ return data.success ? data.data : null;
2321
+ } catch (error) {
2322
+ return null;
2323
+ }
2324
+ },
2325
+ /**
2326
+ * Store license key in plugin store
2327
+ * @param {string} licenseKey - License key to store
2328
+ */
2329
+ async storeLicenseKey(licenseKey) {
2330
+ const pluginStore = strapi2.store({
2331
+ type: "plugin",
2332
+ name: "magic-editor-x"
2333
+ });
2334
+ await pluginStore.set({ key: "licenseKey", value: licenseKey });
2335
+ strapi2.log.info(`[Magic Editor X] [SUCCESS] License key stored: ${licenseKey.substring(0, 8)}...`);
2336
+ },
2337
+ /**
2338
+ * Get stored license key
2339
+ * @returns {Promise<string|null>} License key or null
2340
+ */
2341
+ async getStoredLicenseKey() {
2342
+ const pluginStore = strapi2.store({
2343
+ type: "plugin",
2344
+ name: "magic-editor-x"
2345
+ });
2346
+ return await pluginStore.get({ key: "licenseKey" });
2347
+ },
2348
+ /**
2349
+ * Start automatic license pinging
2350
+ * @param {string} licenseKey - License key
2351
+ * @param {number} intervalMinutes - Ping interval in minutes
2352
+ * @returns {NodeJS.Timeout} Interval handle
2353
+ */
2354
+ startPinging(licenseKey, intervalMinutes = 15) {
2355
+ this.pingLicense(licenseKey);
2356
+ const interval = setInterval(async () => {
2357
+ try {
2358
+ await this.pingLicense(licenseKey);
2359
+ } catch (error) {
2360
+ }
2361
+ }, intervalMinutes * 60 * 1e3);
2362
+ return interval;
2363
+ },
2364
+ /**
2365
+ * Get current license data from store and server
2366
+ * @returns {Promise<object|null>} License data or null
2367
+ */
2368
+ async getCurrentLicense() {
2369
+ try {
2370
+ const licenseKey = await this.getStoredLicenseKey();
2371
+ if (!licenseKey) {
2372
+ return null;
2373
+ }
2374
+ const license2 = await this.getLicenseByKey(licenseKey);
2375
+ return license2;
2376
+ } catch (error) {
2377
+ strapi2.log.error(`[Magic Editor X] [ERROR] Error loading license:`, error);
2378
+ return null;
2379
+ }
2380
+ },
2381
+ /**
2382
+ * Get current tier based on license
2383
+ * @returns {Promise<string>} Tier name (free, premium, advanced, enterprise)
2384
+ */
2385
+ async getCurrentTier() {
2386
+ const license2 = await this.getCurrentLicense();
2387
+ if (!license2) {
2388
+ return "free";
2389
+ }
2390
+ if (license2.featureEnterprise === true) return "enterprise";
2391
+ if (license2.featureAdvanced === true) return "advanced";
2392
+ if (license2.featurePremium === true) return "premium";
2393
+ return "free";
2394
+ },
2395
+ /**
2396
+ * Get tier configuration
2397
+ * @param {string} tierName - Tier name
2398
+ * @returns {object} Tier configuration
2399
+ */
2400
+ getTierConfig(tierName) {
2401
+ return TIERS[tierName] || TIERS.free;
2402
+ },
2403
+ /**
2404
+ * Get maximum allowed collaborators for current license
2405
+ * @returns {Promise<number>} Max collaborators (-1 for unlimited)
2406
+ */
2407
+ async getMaxCollaborators() {
2408
+ const tier = await this.getCurrentTier();
2409
+ const config2 = this.getTierConfig(tier);
2410
+ return config2.maxCollaborators;
2411
+ },
2412
+ /**
2413
+ * Check if a specific feature is available
2414
+ * @param {string} featureName - Feature name
2415
+ * @returns {Promise<boolean>} Feature availability
2416
+ */
2417
+ async hasFeature(featureName) {
2418
+ const tier = await this.getCurrentTier();
2419
+ const config2 = this.getTierConfig(tier);
2420
+ return config2.features[featureName] === true;
2421
+ },
2422
+ /**
2423
+ * Check if user can add more collaborators
2424
+ * @returns {Promise<object>} Check result with canAdd and current/max counts
2425
+ */
2426
+ async canAddCollaborator() {
2427
+ const maxCollaborators = await this.getMaxCollaborators();
2428
+ const currentCount = await strapi2.documents("plugin::magic-editor-x.collab-permission").count();
2429
+ const canAdd = maxCollaborators === -1 || currentCount < maxCollaborators;
2430
+ return {
2431
+ canAdd,
2432
+ current: currentCount,
2433
+ max: maxCollaborators,
2434
+ unlimited: maxCollaborators === -1
2435
+ };
2436
+ },
2437
+ /**
2438
+ * Initialize license service on plugin startup
2439
+ * @returns {Promise<object>} Initialization result
2440
+ */
2441
+ async initialize() {
2442
+ try {
2443
+ strapi2.log.info("[Magic Editor X] [INIT] Initializing License Service...");
2444
+ const licenseKey = await this.getStoredLicenseKey();
2445
+ if (!licenseKey) {
2446
+ strapi2.log.info("[Magic Editor X] [FREE] No license found - Running in FREE mode (2 Collaborators)");
2447
+ return {
2448
+ valid: false,
2449
+ demo: true,
2450
+ tier: "free",
2451
+ data: null
2452
+ };
2453
+ }
2454
+ const pluginStore = strapi2.store({
2455
+ type: "plugin",
2456
+ name: "magic-editor-x"
2457
+ });
2458
+ const lastValidated = await pluginStore.get({ key: "lastValidated" });
2459
+ const now = /* @__PURE__ */ new Date();
2460
+ const gracePeriodHours = 24;
2461
+ let withinGracePeriod = false;
2462
+ if (lastValidated) {
2463
+ const lastValidatedDate = new Date(lastValidated);
2464
+ const hoursSinceValidation = (now.getTime() - lastValidatedDate.getTime()) / (1e3 * 60 * 60);
2465
+ withinGracePeriod = hoursSinceValidation < gracePeriodHours;
2466
+ }
2467
+ const verification = await this.verifyLicense(licenseKey, withinGracePeriod);
2468
+ if (verification.valid) {
2469
+ const license2 = await this.getLicenseByKey(licenseKey);
2470
+ const tier = await this.getCurrentTier();
2471
+ const tierConfig = this.getTierConfig(tier);
2472
+ await pluginStore.set({
2473
+ key: "lastValidated",
2474
+ value: now.toISOString()
2475
+ });
2476
+ const pingInterval = this.startPinging(licenseKey, 15);
2477
+ strapi2.licenseGuardEditorX = {
2478
+ licenseKey,
2479
+ pingInterval,
2480
+ data: verification.data,
2481
+ tier
2482
+ };
2483
+ strapi2.log.info("==================================================================");
2484
+ strapi2.log.info("[SUCCESS] MAGIC EDITOR X LICENSE ACTIVE");
2485
+ strapi2.log.info(` License: ${licenseKey.substring(0, 15)}...`);
2486
+ strapi2.log.info(` Tier: ${tierConfig.name}`);
2487
+ strapi2.log.info(` Collaborators: ${tierConfig.maxCollaborators === -1 ? "Unlimited" : tierConfig.maxCollaborators}`);
2488
+ strapi2.log.info(` User: ${license2?.firstName} ${license2?.lastName}`);
2489
+ strapi2.log.info("==================================================================");
2490
+ return {
2491
+ valid: true,
2492
+ demo: false,
2493
+ tier,
2494
+ data: verification.data,
2495
+ gracePeriod: verification.gracePeriod || false
2496
+ };
2497
+ } else {
2498
+ strapi2.log.warn("[Magic Editor X] [WARNING] License validation failed - Running in FREE mode");
2499
+ return {
2500
+ valid: false,
2501
+ demo: true,
2502
+ tier: "free",
2503
+ error: "Invalid or expired license",
2504
+ data: null
2505
+ };
2506
+ }
2507
+ } catch (error) {
2508
+ strapi2.log.error("[Magic Editor X] [ERROR] Error initializing License Service:", error);
2509
+ return {
2510
+ valid: false,
2511
+ demo: true,
2512
+ tier: "free",
2513
+ error: error.message,
2514
+ data: null
2515
+ };
2516
+ }
2517
+ }
2518
+ });
2519
+ const editorService = editorService$1;
2520
+ const realtimeService = realtimeService$1;
2521
+ const accessService = accessService$1;
2522
+ const snapshotService = snapshotService$1;
2523
+ const licenseService = licenseService$1;
2524
+ var services$1 = {
2525
+ editorService,
2526
+ realtimeService,
2527
+ accessService,
2528
+ snapshotService,
2529
+ licenseService
2530
+ };
2531
+ const bootstrap = bootstrap$1;
2532
+ const destroy = destroy$1;
2533
+ const register = register$1;
2534
+ const config = config$1;
2535
+ const contentTypes = contentTypes$1;
2536
+ const controllers = controllers$1;
2537
+ const middlewares = middlewares$1;
2538
+ const policies = policies$1;
2539
+ const routes = routes$1;
2540
+ const services = services$1;
2541
+ var src = {
2542
+ bootstrap,
2543
+ destroy,
2544
+ register,
2545
+ config,
2546
+ controllers,
2547
+ contentTypes,
2548
+ middlewares,
2549
+ policies,
2550
+ routes,
2551
+ services
2552
+ };
2553
+ const index = /* @__PURE__ */ getDefaultExportFromCjs(src);
2554
+ module.exports = index;