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