@openmdm/core 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -27,6 +27,26 @@ var ApplicationNotFoundError = class extends MDMError {
27
27
  super(`Application not found: ${identifier}`, "APPLICATION_NOT_FOUND", 404);
28
28
  }
29
29
  };
30
+ var TenantNotFoundError = class extends MDMError {
31
+ constructor(identifier) {
32
+ super(`Tenant not found: ${identifier}`, "TENANT_NOT_FOUND", 404);
33
+ }
34
+ };
35
+ var RoleNotFoundError = class extends MDMError {
36
+ constructor(identifier) {
37
+ super(`Role not found: ${identifier}`, "ROLE_NOT_FOUND", 404);
38
+ }
39
+ };
40
+ var GroupNotFoundError = class extends MDMError {
41
+ constructor(identifier) {
42
+ super(`Group not found: ${identifier}`, "GROUP_NOT_FOUND", 404);
43
+ }
44
+ };
45
+ var UserNotFoundError = class extends MDMError {
46
+ constructor(identifier) {
47
+ super(`User not found: ${identifier}`, "USER_NOT_FOUND", 404);
48
+ }
49
+ };
30
50
  var EnrollmentError = class extends MDMError {
31
51
  constructor(message, details) {
32
52
  super(message, "ENROLLMENT_ERROR", 400, details);
@@ -162,56 +182,1170 @@ function createWebhookManager(config) {
162
182
  );
163
183
  }
164
184
  }
165
- return results;
185
+ return results;
186
+ },
187
+ addEndpoint(endpoint) {
188
+ endpoints.set(endpoint.id, endpoint);
189
+ },
190
+ removeEndpoint(endpointId) {
191
+ endpoints.delete(endpointId);
192
+ },
193
+ updateEndpoint(endpointId, updates) {
194
+ const existing = endpoints.get(endpointId);
195
+ if (existing) {
196
+ endpoints.set(endpointId, { ...existing, ...updates });
197
+ }
198
+ },
199
+ getEndpoints() {
200
+ return Array.from(endpoints.values());
201
+ },
202
+ async testEndpoint(endpointId) {
203
+ const endpoint = endpoints.get(endpointId);
204
+ if (!endpoint) {
205
+ return {
206
+ endpointId,
207
+ success: false,
208
+ error: "Endpoint not found",
209
+ retryCount: 0
210
+ };
211
+ }
212
+ const testPayload = {
213
+ id: randomUUID(),
214
+ event: "device.heartbeat",
215
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
216
+ data: {
217
+ test: true,
218
+ message: "OpenMDM webhook test"
219
+ }
220
+ };
221
+ return deliverToEndpoint(endpoint, testPayload);
222
+ }
223
+ };
224
+ }
225
+ function verifyWebhookSignature(payload, signature, secret) {
226
+ const expectedSignature = `sha256=${createHmac("sha256", secret).update(payload).digest("hex")}`;
227
+ if (signature.length !== expectedSignature.length) {
228
+ return false;
229
+ }
230
+ let result = 0;
231
+ for (let i = 0; i < signature.length; i++) {
232
+ result |= signature.charCodeAt(i) ^ expectedSignature.charCodeAt(i);
233
+ }
234
+ return result === 0;
235
+ }
236
+
237
+ // src/tenant.ts
238
+ function validateSlug(slug) {
239
+ const slugRegex = /^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$/;
240
+ return slugRegex.test(slug);
241
+ }
242
+ function createTenantManager(db) {
243
+ return {
244
+ async get(id) {
245
+ if (!db.findTenant) {
246
+ throw new Error("Database adapter does not support tenant operations");
247
+ }
248
+ return db.findTenant(id);
249
+ },
250
+ async getBySlug(slug) {
251
+ if (!db.findTenantBySlug) {
252
+ throw new Error("Database adapter does not support tenant operations");
253
+ }
254
+ return db.findTenantBySlug(slug);
255
+ },
256
+ async list(filter) {
257
+ if (!db.listTenants) {
258
+ throw new Error("Database adapter does not support tenant operations");
259
+ }
260
+ return db.listTenants(filter);
261
+ },
262
+ async create(data) {
263
+ if (!db.createTenant || !db.findTenantBySlug) {
264
+ throw new Error("Database adapter does not support tenant operations");
265
+ }
266
+ if (!validateSlug(data.slug)) {
267
+ throw new ValidationError(
268
+ "Invalid slug format. Must be 3-50 lowercase alphanumeric characters with hyphens.",
269
+ { slug: data.slug }
270
+ );
271
+ }
272
+ const existing = await db.findTenantBySlug(data.slug);
273
+ if (existing) {
274
+ throw new ValidationError(`Tenant with slug '${data.slug}' already exists`, {
275
+ slug: data.slug
276
+ });
277
+ }
278
+ return db.createTenant({
279
+ ...data,
280
+ slug: data.slug.toLowerCase()
281
+ });
282
+ },
283
+ async update(id, data) {
284
+ if (!db.updateTenant || !db.findTenant || !db.findTenantBySlug) {
285
+ throw new Error("Database adapter does not support tenant operations");
286
+ }
287
+ const tenant = await db.findTenant(id);
288
+ if (!tenant) {
289
+ throw new TenantNotFoundError(id);
290
+ }
291
+ if (data.slug) {
292
+ if (!validateSlug(data.slug)) {
293
+ throw new ValidationError(
294
+ "Invalid slug format. Must be 3-50 lowercase alphanumeric characters with hyphens.",
295
+ { slug: data.slug }
296
+ );
297
+ }
298
+ const existing = await db.findTenantBySlug(data.slug);
299
+ if (existing && existing.id !== id) {
300
+ throw new ValidationError(`Tenant with slug '${data.slug}' already exists`, {
301
+ slug: data.slug
302
+ });
303
+ }
304
+ data.slug = data.slug.toLowerCase();
305
+ }
306
+ return db.updateTenant(id, data);
307
+ },
308
+ async delete(id, cascade = false) {
309
+ if (!db.deleteTenant || !db.findTenant) {
310
+ throw new Error("Database adapter does not support tenant operations");
311
+ }
312
+ const tenant = await db.findTenant(id);
313
+ if (!tenant) {
314
+ throw new TenantNotFoundError(id);
315
+ }
316
+ await db.deleteTenant(id);
317
+ },
318
+ async getStats(tenantId) {
319
+ if (!db.getTenantStats || !db.findTenant) {
320
+ throw new Error("Database adapter does not support tenant operations");
321
+ }
322
+ const tenant = await db.findTenant(tenantId);
323
+ if (!tenant) {
324
+ throw new TenantNotFoundError(tenantId);
325
+ }
326
+ return db.getTenantStats(tenantId);
327
+ },
328
+ async activate(id) {
329
+ if (!db.updateTenant || !db.findTenant) {
330
+ throw new Error("Database adapter does not support tenant operations");
331
+ }
332
+ const tenant = await db.findTenant(id);
333
+ if (!tenant) {
334
+ throw new TenantNotFoundError(id);
335
+ }
336
+ if (tenant.status === "active") {
337
+ return tenant;
338
+ }
339
+ return db.updateTenant(id, { status: "active" });
340
+ },
341
+ async deactivate(id) {
342
+ if (!db.updateTenant || !db.findTenant) {
343
+ throw new Error("Database adapter does not support tenant operations");
344
+ }
345
+ const tenant = await db.findTenant(id);
346
+ if (!tenant) {
347
+ throw new TenantNotFoundError(id);
348
+ }
349
+ if (tenant.status === "suspended") {
350
+ return tenant;
351
+ }
352
+ return db.updateTenant(id, { status: "suspended" });
353
+ }
354
+ };
355
+ }
356
+
357
+ // src/authorization.ts
358
+ function actionMatches(required, granted) {
359
+ if (granted === "*") return true;
360
+ if (granted === "manage") {
361
+ return ["create", "read", "update", "delete", "manage"].includes(required);
362
+ }
363
+ return required === granted;
364
+ }
365
+ function resourceMatches(required, granted) {
366
+ if (granted === "*") return true;
367
+ return required === granted;
368
+ }
369
+ function permissionMatches(required, granted) {
370
+ return actionMatches(required.action, granted.action) && resourceMatches(required.resource, granted.resource);
371
+ }
372
+ function hasPermission(permissions, action, resource) {
373
+ return permissions.some((p) => permissionMatches({ action, resource }, p));
374
+ }
375
+ function isAdminPermission(permissions) {
376
+ return permissions.some(
377
+ (p) => p.action === "*" && p.resource === "*"
378
+ );
379
+ }
380
+ function validateEmail(email) {
381
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
382
+ return emailRegex.test(email);
383
+ }
384
+ function createAuthorizationManager(db) {
385
+ async function getAllUserPermissions(userId) {
386
+ if (!db.getUserRoles) {
387
+ throw new Error("Database adapter does not support RBAC operations");
388
+ }
389
+ const roles = await db.getUserRoles(userId);
390
+ const permissions = [];
391
+ for (const role of roles) {
392
+ permissions.push(...role.permissions);
393
+ }
394
+ return permissions;
395
+ }
396
+ return {
397
+ // ========================================
398
+ // Role Management
399
+ // ========================================
400
+ async createRole(data) {
401
+ if (!db.createRole) {
402
+ throw new Error("Database adapter does not support RBAC operations");
403
+ }
404
+ if (!data.permissions || !Array.isArray(data.permissions)) {
405
+ throw new ValidationError("Permissions must be an array");
406
+ }
407
+ for (const permission of data.permissions) {
408
+ if (!permission.action || !permission.resource) {
409
+ throw new ValidationError("Each permission must have action and resource");
410
+ }
411
+ }
412
+ return db.createRole(data);
413
+ },
414
+ async getRole(id) {
415
+ if (!db.findRole) {
416
+ throw new Error("Database adapter does not support RBAC operations");
417
+ }
418
+ return db.findRole(id);
419
+ },
420
+ async listRoles(tenantId) {
421
+ if (!db.listRoles) {
422
+ throw new Error("Database adapter does not support RBAC operations");
423
+ }
424
+ return db.listRoles(tenantId);
425
+ },
426
+ async updateRole(id, data) {
427
+ if (!db.updateRole || !db.findRole) {
428
+ throw new Error("Database adapter does not support RBAC operations");
429
+ }
430
+ const role = await db.findRole(id);
431
+ if (!role) {
432
+ throw new RoleNotFoundError(id);
433
+ }
434
+ if (role.isSystem) {
435
+ throw new AuthorizationError("Cannot modify system roles");
436
+ }
437
+ if (data.permissions) {
438
+ if (!Array.isArray(data.permissions)) {
439
+ throw new ValidationError("Permissions must be an array");
440
+ }
441
+ for (const permission of data.permissions) {
442
+ if (!permission.action || !permission.resource) {
443
+ throw new ValidationError("Each permission must have action and resource");
444
+ }
445
+ }
446
+ }
447
+ return db.updateRole(id, data);
448
+ },
449
+ async deleteRole(id) {
450
+ if (!db.deleteRole || !db.findRole) {
451
+ throw new Error("Database adapter does not support RBAC operations");
452
+ }
453
+ const role = await db.findRole(id);
454
+ if (!role) {
455
+ throw new RoleNotFoundError(id);
456
+ }
457
+ if (role.isSystem) {
458
+ throw new AuthorizationError("Cannot delete system roles");
459
+ }
460
+ await db.deleteRole(id);
461
+ },
462
+ // ========================================
463
+ // User Management
464
+ // ========================================
465
+ async createUser(data) {
466
+ if (!db.createUser || !db.findUserByEmail) {
467
+ throw new Error("Database adapter does not support RBAC operations");
468
+ }
469
+ if (!validateEmail(data.email)) {
470
+ throw new ValidationError("Invalid email format", { email: data.email });
471
+ }
472
+ const existing = await db.findUserByEmail(data.email, data.tenantId);
473
+ if (existing) {
474
+ throw new ValidationError(`User with email '${data.email}' already exists`, {
475
+ email: data.email
476
+ });
477
+ }
478
+ return db.createUser({
479
+ ...data,
480
+ email: data.email.toLowerCase()
481
+ });
482
+ },
483
+ async getUser(id) {
484
+ if (!db.findUser || !db.getUserRoles) {
485
+ throw new Error("Database adapter does not support RBAC operations");
486
+ }
487
+ const user = await db.findUser(id);
488
+ if (!user) return null;
489
+ const roles = await db.getUserRoles(id);
490
+ return { ...user, roles };
491
+ },
492
+ async getUserByEmail(email, tenantId) {
493
+ if (!db.findUserByEmail || !db.getUserRoles) {
494
+ throw new Error("Database adapter does not support RBAC operations");
495
+ }
496
+ const user = await db.findUserByEmail(email.toLowerCase(), tenantId);
497
+ if (!user) return null;
498
+ const roles = await db.getUserRoles(user.id);
499
+ return { ...user, roles };
500
+ },
501
+ async listUsers(filter) {
502
+ if (!db.listUsers) {
503
+ throw new Error("Database adapter does not support RBAC operations");
504
+ }
505
+ return db.listUsers(filter);
506
+ },
507
+ async updateUser(id, data) {
508
+ if (!db.updateUser || !db.findUser) {
509
+ throw new Error("Database adapter does not support RBAC operations");
510
+ }
511
+ const user = await db.findUser(id);
512
+ if (!user) {
513
+ throw new UserNotFoundError(id);
514
+ }
515
+ if (data.email) {
516
+ if (!validateEmail(data.email)) {
517
+ throw new ValidationError("Invalid email format", { email: data.email });
518
+ }
519
+ data.email = data.email.toLowerCase();
520
+ }
521
+ return db.updateUser(id, data);
522
+ },
523
+ async deleteUser(id) {
524
+ if (!db.deleteUser || !db.findUser) {
525
+ throw new Error("Database adapter does not support RBAC operations");
526
+ }
527
+ const user = await db.findUser(id);
528
+ if (!user) {
529
+ throw new UserNotFoundError(id);
530
+ }
531
+ await db.deleteUser(id);
532
+ },
533
+ // ========================================
534
+ // Role Assignment
535
+ // ========================================
536
+ async assignRole(userId, roleId) {
537
+ if (!db.assignRoleToUser || !db.findUser || !db.findRole) {
538
+ throw new Error("Database adapter does not support RBAC operations");
539
+ }
540
+ const user = await db.findUser(userId);
541
+ if (!user) {
542
+ throw new UserNotFoundError(userId);
543
+ }
544
+ const role = await db.findRole(roleId);
545
+ if (!role) {
546
+ throw new RoleNotFoundError(roleId);
547
+ }
548
+ if (role.tenantId && user.tenantId && role.tenantId !== user.tenantId) {
549
+ throw new AuthorizationError("Role belongs to a different tenant");
550
+ }
551
+ await db.assignRoleToUser(userId, roleId);
552
+ },
553
+ async removeRole(userId, roleId) {
554
+ if (!db.removeRoleFromUser || !db.findUser) {
555
+ throw new Error("Database adapter does not support RBAC operations");
556
+ }
557
+ const user = await db.findUser(userId);
558
+ if (!user) {
559
+ throw new UserNotFoundError(userId);
560
+ }
561
+ await db.removeRoleFromUser(userId, roleId);
562
+ },
563
+ async getUserRoles(userId) {
564
+ if (!db.getUserRoles || !db.findUser) {
565
+ throw new Error("Database adapter does not support RBAC operations");
566
+ }
567
+ const user = await db.findUser(userId);
568
+ if (!user) {
569
+ throw new UserNotFoundError(userId);
570
+ }
571
+ return db.getUserRoles(userId);
572
+ },
573
+ // ========================================
574
+ // Permission Checking
575
+ // ========================================
576
+ async can(userId, action, resource, _resourceId) {
577
+ if (!db.findUser) {
578
+ throw new Error("Database adapter does not support RBAC operations");
579
+ }
580
+ const user = await db.findUser(userId);
581
+ if (!user) return false;
582
+ if (user.status !== "active") return false;
583
+ const permissions = await getAllUserPermissions(userId);
584
+ return hasPermission(permissions, action, resource);
585
+ },
586
+ async requirePermission(userId, action, resource, resourceId) {
587
+ const allowed = await this.can(userId, action, resource, resourceId);
588
+ if (!allowed) {
589
+ throw new AuthorizationError(
590
+ `Permission denied: ${action} on ${resource}${resourceId ? ` (${resourceId})` : ""}`
591
+ );
592
+ }
593
+ },
594
+ async canAny(userId, permissions) {
595
+ if (!db.findUser) {
596
+ throw new Error("Database adapter does not support RBAC operations");
597
+ }
598
+ const user = await db.findUser(userId);
599
+ if (!user) return false;
600
+ if (user.status !== "active") return false;
601
+ const userPermissions = await getAllUserPermissions(userId);
602
+ return permissions.some(
603
+ (required) => hasPermission(userPermissions, required.action, required.resource)
604
+ );
605
+ },
606
+ async isAdmin(userId) {
607
+ if (!db.findUser) {
608
+ throw new Error("Database adapter does not support RBAC operations");
609
+ }
610
+ const user = await db.findUser(userId);
611
+ if (!user || user.status !== "active") return false;
612
+ const permissions = await getAllUserPermissions(userId);
613
+ return isAdminPermission(permissions);
614
+ }
615
+ };
616
+ }
617
+
618
+ // src/audit.ts
619
+ var DEFAULT_RETENTION_DAYS = 90;
620
+ function auditLogToCsvRow(log) {
621
+ const fields = [
622
+ log.id,
623
+ log.tenantId || "",
624
+ log.userId || "",
625
+ log.action,
626
+ log.resource,
627
+ log.resourceId || "",
628
+ log.status,
629
+ log.ipAddress || "",
630
+ log.userAgent || "",
631
+ log.error || "",
632
+ log.createdAt.toISOString(),
633
+ JSON.stringify(log.details || {})
634
+ ];
635
+ return fields.map((f) => `"${String(f).replace(/"/g, '""')}"`).join(",");
636
+ }
637
+ var CSV_HEADER = "id,tenant_id,user_id,action,resource,resource_id,status,ip_address,user_agent,error,created_at,details";
638
+ function createAuditManager(db, config) {
639
+ const retentionDays = config?.retentionDays ?? DEFAULT_RETENTION_DAYS;
640
+ function shouldLog(action, resource) {
641
+ if (!config?.enabled) return false;
642
+ if (config.skipReadOperations && action === "read") {
643
+ return false;
644
+ }
645
+ if (config.logActions && config.logActions.length > 0) {
646
+ if (!config.logActions.includes(action)) {
647
+ return false;
648
+ }
649
+ }
650
+ if (config.logResources && config.logResources.length > 0) {
651
+ if (!config.logResources.includes(resource)) {
652
+ return false;
653
+ }
654
+ }
655
+ return true;
656
+ }
657
+ return {
658
+ async log(entry) {
659
+ if (!db.createAuditLog) {
660
+ throw new Error("Database adapter does not support audit operations");
661
+ }
662
+ if (config && !shouldLog(entry.action, entry.resource)) {
663
+ return {
664
+ id: "skipped",
665
+ ...entry,
666
+ createdAt: /* @__PURE__ */ new Date()
667
+ };
668
+ }
669
+ return db.createAuditLog(entry);
670
+ },
671
+ async list(filter) {
672
+ if (!db.listAuditLogs) {
673
+ throw new Error("Database adapter does not support audit operations");
674
+ }
675
+ return db.listAuditLogs(filter);
676
+ },
677
+ async getByResource(resource, resourceId) {
678
+ if (!db.listAuditLogs) {
679
+ throw new Error("Database adapter does not support audit operations");
680
+ }
681
+ const result = await db.listAuditLogs({
682
+ resource,
683
+ resourceId,
684
+ limit: 1e3
685
+ // Reasonable limit for resource-specific queries
686
+ });
687
+ return result.logs;
688
+ },
689
+ async getByUser(userId, filter) {
690
+ if (!db.listAuditLogs) {
691
+ throw new Error("Database adapter does not support audit operations");
692
+ }
693
+ return db.listAuditLogs({
694
+ ...filter,
695
+ userId
696
+ });
697
+ },
698
+ async export(filter, format) {
699
+ if (!db.listAuditLogs) {
700
+ throw new Error("Database adapter does not support audit operations");
701
+ }
702
+ const allLogs = [];
703
+ let offset = 0;
704
+ const batchSize = 1e3;
705
+ while (true) {
706
+ const result = await db.listAuditLogs({
707
+ ...filter,
708
+ limit: batchSize,
709
+ offset
710
+ });
711
+ allLogs.push(...result.logs);
712
+ if (result.logs.length < batchSize || allLogs.length >= 1e5) {
713
+ break;
714
+ }
715
+ offset += batchSize;
716
+ }
717
+ if (format === "json") {
718
+ return JSON.stringify(allLogs, null, 2);
719
+ }
720
+ const rows = allLogs.map(auditLogToCsvRow);
721
+ return [CSV_HEADER, ...rows].join("\n");
722
+ },
723
+ async purge(olderThanDays) {
724
+ if (!db.deleteAuditLogs) {
725
+ throw new Error("Database adapter does not support audit operations");
726
+ }
727
+ const days = olderThanDays ?? retentionDays;
728
+ const cutoffDate = /* @__PURE__ */ new Date();
729
+ cutoffDate.setDate(cutoffDate.getDate() - days);
730
+ return db.deleteAuditLogs({ olderThan: cutoffDate });
731
+ },
732
+ async getSummary(tenantId, days = 30) {
733
+ if (!db.listAuditLogs) {
734
+ throw new Error("Database adapter does not support audit operations");
735
+ }
736
+ const startDate = /* @__PURE__ */ new Date();
737
+ startDate.setDate(startDate.getDate() - days);
738
+ const result = await db.listAuditLogs({
739
+ tenantId,
740
+ startDate,
741
+ limit: 1e4
742
+ // Reasonable limit for summary
743
+ });
744
+ const logs = result.logs;
745
+ const byAction = {};
746
+ const byResource = {};
747
+ const byStatus = { success: 0, failure: 0 };
748
+ const userCounts = {};
749
+ const recentFailures = [];
750
+ for (const log of logs) {
751
+ byAction[log.action] = (byAction[log.action] || 0) + 1;
752
+ byResource[log.resource] = (byResource[log.resource] || 0) + 1;
753
+ byStatus[log.status]++;
754
+ if (log.userId) {
755
+ userCounts[log.userId] = (userCounts[log.userId] || 0) + 1;
756
+ }
757
+ if (log.status === "failure" && recentFailures.length < 10) {
758
+ recentFailures.push(log);
759
+ }
760
+ }
761
+ const topUsers = Object.entries(userCounts).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([userId, count]) => ({ userId, count }));
762
+ return {
763
+ totalLogs: result.total,
764
+ byAction,
765
+ byResource,
766
+ byStatus,
767
+ topUsers,
768
+ recentFailures
769
+ };
770
+ }
771
+ };
772
+ }
773
+
774
+ // src/schedule.ts
775
+ function parseCronNextRun(cron, from = /* @__PURE__ */ new Date()) {
776
+ try {
777
+ const parts = cron.trim().split(/\s+/);
778
+ if (parts.length !== 5) return null;
779
+ const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
780
+ const now = new Date(from);
781
+ const next = new Date(now);
782
+ next.setSeconds(0);
783
+ next.setMilliseconds(0);
784
+ next.setMinutes(next.getMinutes() + 1);
785
+ const maxIterations = 365 * 24 * 60;
786
+ for (let i = 0; i < maxIterations; i++) {
787
+ const matches = matchesCronField(minute, next.getMinutes()) && matchesCronField(hour, next.getHours()) && matchesCronField(dayOfMonth, next.getDate()) && matchesCronField(month, next.getMonth() + 1) && matchesCronField(dayOfWeek, next.getDay());
788
+ if (matches) {
789
+ return next;
790
+ }
791
+ next.setMinutes(next.getMinutes() + 1);
792
+ }
793
+ return null;
794
+ } catch {
795
+ return null;
796
+ }
797
+ }
798
+ function matchesCronField(pattern, value) {
799
+ if (pattern === "*") return true;
800
+ if (pattern.startsWith("*/")) {
801
+ const step = parseInt(pattern.slice(2), 10);
802
+ return value % step === 0;
803
+ }
804
+ if (pattern.includes("-")) {
805
+ const [start, end] = pattern.split("-").map((n) => parseInt(n, 10));
806
+ return value >= start && value <= end;
807
+ }
808
+ if (pattern.includes(",")) {
809
+ const values = pattern.split(",").map((n) => parseInt(n, 10));
810
+ return values.includes(value);
811
+ }
812
+ return parseInt(pattern, 10) === value;
813
+ }
814
+ function calculateNextWindowRun(window, from = /* @__PURE__ */ new Date()) {
815
+ if (!window) return null;
816
+ const [startHour, startMin] = window.startTime.split(":").map(Number);
817
+ for (let dayOffset = 0; dayOffset <= 7; dayOffset++) {
818
+ const candidate = new Date(from);
819
+ candidate.setDate(candidate.getDate() + dayOffset);
820
+ candidate.setHours(startHour, startMin, 0, 0);
821
+ if (candidate <= from) continue;
822
+ if (window.daysOfWeek.includes(candidate.getDay())) {
823
+ return candidate;
824
+ }
825
+ }
826
+ return null;
827
+ }
828
+ function createScheduleManager(db) {
829
+ function calculateNextRun(schedule) {
830
+ const now = /* @__PURE__ */ new Date();
831
+ switch (schedule.type) {
832
+ case "once":
833
+ if (schedule.executeAt && new Date(schedule.executeAt) > now) {
834
+ return new Date(schedule.executeAt);
835
+ }
836
+ return null;
837
+ case "recurring":
838
+ if (schedule.cron) {
839
+ return parseCronNextRun(schedule.cron, now);
840
+ }
841
+ return null;
842
+ case "window":
843
+ if (schedule.window) {
844
+ return calculateNextWindowRun(schedule.window, now);
845
+ }
846
+ return null;
847
+ default:
848
+ return null;
849
+ }
850
+ }
851
+ return {
852
+ async get(id) {
853
+ if (!db.findScheduledTask) {
854
+ throw new Error("Database adapter does not support task scheduling");
855
+ }
856
+ return db.findScheduledTask(id);
857
+ },
858
+ async list(filter) {
859
+ if (!db.listScheduledTasks) {
860
+ throw new Error("Database adapter does not support task scheduling");
861
+ }
862
+ return db.listScheduledTasks(filter);
863
+ },
864
+ async create(data) {
865
+ if (!db.createScheduledTask) {
866
+ throw new Error("Database adapter does not support task scheduling");
867
+ }
868
+ const nextRunAt = calculateNextRun(data.schedule);
869
+ const task = await db.createScheduledTask({
870
+ ...data
871
+ // Note: nextRunAt is set by the database adapter based on schedule
872
+ });
873
+ if (nextRunAt && db.updateScheduledTask) {
874
+ return db.updateScheduledTask(task.id, {
875
+ ...data
876
+ });
877
+ }
878
+ return task;
879
+ },
880
+ async update(id, data) {
881
+ if (!db.updateScheduledTask || !db.findScheduledTask) {
882
+ throw new Error("Database adapter does not support task scheduling");
883
+ }
884
+ const existing = await db.findScheduledTask(id);
885
+ if (!existing) {
886
+ throw new Error(`Scheduled task not found: ${id}`);
887
+ }
888
+ return db.updateScheduledTask(id, data);
889
+ },
890
+ async delete(id) {
891
+ if (!db.deleteScheduledTask) {
892
+ throw new Error("Database adapter does not support task scheduling");
893
+ }
894
+ await db.deleteScheduledTask(id);
895
+ },
896
+ async pause(id) {
897
+ if (!db.updateScheduledTask || !db.findScheduledTask) {
898
+ throw new Error("Database adapter does not support task scheduling");
899
+ }
900
+ const task = await db.findScheduledTask(id);
901
+ if (!task) {
902
+ throw new Error(`Scheduled task not found: ${id}`);
903
+ }
904
+ if (task.status === "paused") {
905
+ return task;
906
+ }
907
+ return db.updateScheduledTask(id, { status: "paused" });
908
+ },
909
+ async resume(id) {
910
+ if (!db.updateScheduledTask || !db.findScheduledTask) {
911
+ throw new Error("Database adapter does not support task scheduling");
912
+ }
913
+ const task = await db.findScheduledTask(id);
914
+ if (!task) {
915
+ throw new Error(`Scheduled task not found: ${id}`);
916
+ }
917
+ if (task.status !== "paused") {
918
+ return task;
919
+ }
920
+ calculateNextRun(task.schedule);
921
+ return db.updateScheduledTask(id, { status: "active" });
922
+ },
923
+ async runNow(id) {
924
+ if (!db.findScheduledTask || !db.createTaskExecution || !db.updateScheduledTask) {
925
+ throw new Error("Database adapter does not support task scheduling");
926
+ }
927
+ const task = await db.findScheduledTask(id);
928
+ if (!task) {
929
+ throw new Error(`Scheduled task not found: ${id}`);
930
+ }
931
+ const execution = await db.createTaskExecution({ taskId: id });
932
+ await db.updateScheduledTask(id, {});
933
+ return execution;
934
+ },
935
+ async getUpcoming(hours) {
936
+ if (!db.getUpcomingTasks) {
937
+ throw new Error("Database adapter does not support task scheduling");
938
+ }
939
+ return db.getUpcomingTasks(hours);
940
+ },
941
+ async getExecutions(taskId, limit = 10) {
942
+ if (!db.listTaskExecutions) {
943
+ throw new Error("Database adapter does not support task scheduling");
944
+ }
945
+ return db.listTaskExecutions(taskId, limit);
946
+ },
947
+ calculateNextRun
948
+ };
949
+ }
950
+
951
+ // src/queue.ts
952
+ var DEFAULT_MAX_ATTEMPTS = 3;
953
+ var DEFAULT_TTL_SECONDS = 86400;
954
+ function createMessageQueueManager(db) {
955
+ return {
956
+ async enqueue(message) {
957
+ if (!db.enqueueMessage) {
958
+ throw new Error("Database adapter does not support message queue");
959
+ }
960
+ const enrichedMessage = {
961
+ ...message,
962
+ priority: message.priority ?? "normal",
963
+ maxAttempts: message.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
964
+ ttlSeconds: message.ttlSeconds ?? DEFAULT_TTL_SECONDS
965
+ };
966
+ return db.enqueueMessage(enrichedMessage);
967
+ },
968
+ async enqueueBatch(messages) {
969
+ if (!db.enqueueMessage) {
970
+ throw new Error("Database adapter does not support message queue");
971
+ }
972
+ const results = [];
973
+ for (const message of messages) {
974
+ const enrichedMessage = {
975
+ ...message,
976
+ priority: message.priority ?? "normal",
977
+ maxAttempts: message.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
978
+ ttlSeconds: message.ttlSeconds ?? DEFAULT_TTL_SECONDS
979
+ };
980
+ const queued = await db.enqueueMessage(enrichedMessage);
981
+ results.push(queued);
982
+ }
983
+ return results;
984
+ },
985
+ async dequeue(deviceId, limit = 10) {
986
+ if (!db.dequeueMessages) {
987
+ throw new Error("Database adapter does not support message queue");
988
+ }
989
+ return db.dequeueMessages(deviceId, limit);
990
+ },
991
+ async acknowledge(messageId) {
992
+ if (!db.acknowledgeMessage) {
993
+ throw new Error("Database adapter does not support message queue");
994
+ }
995
+ await db.acknowledgeMessage(messageId);
996
+ },
997
+ async fail(messageId, error) {
998
+ if (!db.failMessage) {
999
+ throw new Error("Database adapter does not support message queue");
1000
+ }
1001
+ await db.failMessage(messageId, error);
1002
+ },
1003
+ async retryFailed(maxAttempts = DEFAULT_MAX_ATTEMPTS) {
1004
+ if (!db.retryFailedMessages) {
1005
+ throw new Error("Database adapter does not support message queue");
1006
+ }
1007
+ return db.retryFailedMessages(maxAttempts);
1008
+ },
1009
+ async purgeExpired() {
1010
+ if (!db.purgeExpiredMessages) {
1011
+ throw new Error("Database adapter does not support message queue");
1012
+ }
1013
+ return db.purgeExpiredMessages();
1014
+ },
1015
+ async getStats(tenantId) {
1016
+ if (!db.getQueueStats) {
1017
+ throw new Error("Database adapter does not support message queue");
1018
+ }
1019
+ return db.getQueueStats(tenantId);
1020
+ },
1021
+ async peek(deviceId, limit = 10) {
1022
+ if (!db.peekMessages) {
1023
+ throw new Error("Database adapter does not support message queue");
1024
+ }
1025
+ return db.peekMessages(deviceId, limit);
1026
+ }
1027
+ };
1028
+ }
1029
+
1030
+ // src/dashboard.ts
1031
+ function createDashboardManager(db) {
1032
+ return {
1033
+ async getStats(_tenantId) {
1034
+ if (db.getDashboardStats) {
1035
+ return db.getDashboardStats(_tenantId);
1036
+ }
1037
+ const devices = await db.listDevices({
1038
+ limit: 1e4
1039
+ // Get all for counting
1040
+ });
1041
+ const deviceStats = {
1042
+ total: devices.total,
1043
+ enrolled: devices.devices.filter((d) => d.status === "enrolled").length,
1044
+ active: devices.devices.filter((d) => d.status === "enrolled").length,
1045
+ // 'active' = 'enrolled' for dashboard
1046
+ blocked: devices.devices.filter((d) => d.status === "blocked").length,
1047
+ pending: devices.devices.filter((d) => d.status === "pending").length
1048
+ };
1049
+ const allPolicies = await db.listPolicies();
1050
+ const policyStats = {
1051
+ total: allPolicies.length,
1052
+ deployed: allPolicies.filter((p) => p.isDefault).length
1053
+ };
1054
+ const allApps = await db.listApplications();
1055
+ const appStats = {
1056
+ total: allApps.length,
1057
+ deployed: allApps.length
1058
+ // All apps in db are considered deployed
1059
+ };
1060
+ const now = /* @__PURE__ */ new Date();
1061
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1e3);
1062
+ const allCommands = await db.listCommands({ limit: 1e4 });
1063
+ const pendingCommands = allCommands.filter((c) => c.status === "pending");
1064
+ const last24hCommands = allCommands.filter(
1065
+ (c) => new Date(c.createdAt) >= yesterday
1066
+ );
1067
+ const commandStats = {
1068
+ pendingCount: pendingCommands.length,
1069
+ last24hTotal: last24hCommands.length,
1070
+ last24hSuccess: last24hCommands.filter((c) => c.status === "completed").length,
1071
+ last24hFailed: last24hCommands.filter((c) => c.status === "failed").length
1072
+ };
1073
+ const allGroups = await db.listGroups();
1074
+ let groupsWithDevices = 0;
1075
+ for (const group of allGroups) {
1076
+ const groupDevices = await db.listDevicesInGroup(group.id);
1077
+ if (groupDevices.length > 0) groupsWithDevices++;
1078
+ }
1079
+ return {
1080
+ devices: deviceStats,
1081
+ policies: policyStats,
1082
+ applications: appStats,
1083
+ commands: commandStats,
1084
+ groups: {
1085
+ total: allGroups.length,
1086
+ withDevices: groupsWithDevices
1087
+ }
1088
+ };
1089
+ },
1090
+ async getDeviceStatusBreakdown(_tenantId) {
1091
+ if (db.getDeviceStatusBreakdown) {
1092
+ return db.getDeviceStatusBreakdown(_tenantId);
1093
+ }
1094
+ const devices = await db.listDevices({
1095
+ limit: 1e4
1096
+ });
1097
+ const byStatus = {
1098
+ pending: 0,
1099
+ enrolled: 0,
1100
+ blocked: 0,
1101
+ unenrolled: 0
1102
+ };
1103
+ const byOs = {};
1104
+ const byManufacturer = {};
1105
+ const byModel = {};
1106
+ for (const device of devices.devices) {
1107
+ byStatus[device.status]++;
1108
+ const osKey = device.osVersion || "Unknown";
1109
+ byOs[osKey] = (byOs[osKey] || 0) + 1;
1110
+ const mfr = device.manufacturer || "Unknown";
1111
+ byManufacturer[mfr] = (byManufacturer[mfr] || 0) + 1;
1112
+ const model = device.model || "Unknown";
1113
+ byModel[model] = (byModel[model] || 0) + 1;
1114
+ }
1115
+ return {
1116
+ byStatus,
1117
+ byOs,
1118
+ byManufacturer,
1119
+ byModel
1120
+ };
1121
+ },
1122
+ async getEnrollmentTrend(days, _tenantId) {
1123
+ if (db.getEnrollmentTrend) {
1124
+ return db.getEnrollmentTrend(days, _tenantId);
1125
+ }
1126
+ const now = /* @__PURE__ */ new Date();
1127
+ const startDate = new Date(now.getTime() - days * 24 * 60 * 60 * 1e3);
1128
+ const events = await db.listEvents({
1129
+ type: "device.enrolled",
1130
+ startDate,
1131
+ limit: 1e4
1132
+ });
1133
+ const unenrollEvents = await db.listEvents({
1134
+ type: "device.unenrolled",
1135
+ startDate,
1136
+ limit: 1e4
1137
+ });
1138
+ const trendByDate = /* @__PURE__ */ new Map();
1139
+ for (let i = 0; i < days; i++) {
1140
+ const date = new Date(startDate.getTime() + i * 24 * 60 * 60 * 1e3);
1141
+ const dateKey = date.toISOString().split("T")[0];
1142
+ trendByDate.set(dateKey, { enrolled: 0, unenrolled: 0 });
1143
+ }
1144
+ for (const event of events) {
1145
+ const dateKey = new Date(event.createdAt).toISOString().split("T")[0];
1146
+ const entry = trendByDate.get(dateKey);
1147
+ if (entry) {
1148
+ entry.enrolled++;
1149
+ }
1150
+ }
1151
+ for (const event of unenrollEvents) {
1152
+ const dateKey = new Date(event.createdAt).toISOString().split("T")[0];
1153
+ const entry = trendByDate.get(dateKey);
1154
+ if (entry) {
1155
+ entry.unenrolled++;
1156
+ }
1157
+ }
1158
+ const initialDevices = await db.listDevices({
1159
+ limit: 1e4
1160
+ });
1161
+ let runningTotal = initialDevices.total;
1162
+ const result = [];
1163
+ const sortedDates = Array.from(trendByDate.keys()).sort();
1164
+ for (const dateKey of sortedDates) {
1165
+ const entry = trendByDate.get(dateKey);
1166
+ const netChange = entry.enrolled - entry.unenrolled;
1167
+ runningTotal += netChange;
1168
+ result.push({
1169
+ date: new Date(dateKey),
1170
+ enrolled: entry.enrolled,
1171
+ unenrolled: entry.unenrolled,
1172
+ netChange,
1173
+ totalDevices: runningTotal
1174
+ });
1175
+ }
1176
+ return result;
1177
+ },
1178
+ async getCommandSuccessRates(_tenantId) {
1179
+ if (db.getCommandSuccessRates) {
1180
+ return db.getCommandSuccessRates(_tenantId);
1181
+ }
1182
+ const now = /* @__PURE__ */ new Date();
1183
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1e3);
1184
+ const commands = await db.listCommands({ limit: 1e4 });
1185
+ const completed = commands.filter((c) => c.status === "completed").length;
1186
+ const failed = commands.filter((c) => c.status === "failed").length;
1187
+ const total = commands.length;
1188
+ const byType = {};
1189
+ for (const cmd of commands) {
1190
+ if (!byType[cmd.type]) {
1191
+ byType[cmd.type] = {
1192
+ total: 0,
1193
+ completed: 0,
1194
+ failed: 0,
1195
+ successRate: 0
1196
+ };
1197
+ }
1198
+ byType[cmd.type].total++;
1199
+ if (cmd.status === "completed") byType[cmd.type].completed++;
1200
+ if (cmd.status === "failed") byType[cmd.type].failed++;
1201
+ }
1202
+ for (const type of Object.keys(byType)) {
1203
+ const stats = byType[type];
1204
+ const finishedCount = stats.completed + stats.failed;
1205
+ stats.successRate = finishedCount > 0 ? stats.completed / finishedCount * 100 : 0;
1206
+ }
1207
+ const last24hCommands = commands.filter(
1208
+ (c) => new Date(c.createdAt) >= yesterday
1209
+ );
1210
+ return {
1211
+ overall: {
1212
+ total,
1213
+ completed,
1214
+ failed,
1215
+ successRate: completed + failed > 0 ? completed / (completed + failed) * 100 : 0
1216
+ },
1217
+ byType,
1218
+ last24h: {
1219
+ total: last24hCommands.length,
1220
+ completed: last24hCommands.filter((c) => c.status === "completed").length,
1221
+ failed: last24hCommands.filter((c) => c.status === "failed").length,
1222
+ pending: last24hCommands.filter((c) => c.status === "pending").length
1223
+ }
1224
+ };
166
1225
  },
167
- addEndpoint(endpoint) {
168
- endpoints.set(endpoint.id, endpoint);
1226
+ async getAppInstallationSummary(_tenantId) {
1227
+ if (db.getAppInstallationSummary) {
1228
+ return db.getAppInstallationSummary(_tenantId);
1229
+ }
1230
+ const apps = await db.listApplications();
1231
+ const appMap = new Map(apps.map((a) => [a.packageName, a]));
1232
+ const byStatus = {
1233
+ installed: 0,
1234
+ installing: 0,
1235
+ failed: 0,
1236
+ pending: 0
1237
+ };
1238
+ const installCounts = {};
1239
+ const devices = await db.listDevices({
1240
+ limit: 1e4
1241
+ });
1242
+ for (const device of devices.devices) {
1243
+ if (device.installedApps) {
1244
+ for (const app of device.installedApps) {
1245
+ const key = app.packageName;
1246
+ installCounts[key] = (installCounts[key] || 0) + 1;
1247
+ byStatus["installed"]++;
1248
+ }
1249
+ }
1250
+ }
1251
+ const topInstalled = Object.entries(installCounts).sort(([, a], [, b]) => b - a).slice(0, 10).map(([packageName, count]) => ({
1252
+ packageName,
1253
+ name: appMap.get(packageName)?.name || packageName,
1254
+ installedCount: count
1255
+ }));
1256
+ return {
1257
+ total: Object.values(byStatus).reduce((a, b) => a + b, 0),
1258
+ byStatus,
1259
+ recentFailures: [],
1260
+ // Would need installation status tracking
1261
+ topInstalled
1262
+ };
1263
+ }
1264
+ };
1265
+ }
1266
+
1267
+ // src/plugin-storage.ts
1268
+ function createPluginStorageAdapter(db) {
1269
+ return {
1270
+ async get(pluginName, key) {
1271
+ if (db.getPluginValue) {
1272
+ const value = await db.getPluginValue(pluginName, key);
1273
+ return value;
1274
+ }
1275
+ console.warn("Plugin storage not supported by database adapter");
1276
+ return null;
169
1277
  },
170
- removeEndpoint(endpointId) {
171
- endpoints.delete(endpointId);
1278
+ async set(pluginName, key, value) {
1279
+ if (db.setPluginValue) {
1280
+ await db.setPluginValue(pluginName, key, value);
1281
+ return;
1282
+ }
1283
+ console.warn("Plugin storage not supported by database adapter");
172
1284
  },
173
- updateEndpoint(endpointId, updates) {
174
- const existing = endpoints.get(endpointId);
175
- if (existing) {
176
- endpoints.set(endpointId, { ...existing, ...updates });
1285
+ async delete(pluginName, key) {
1286
+ if (db.deletePluginValue) {
1287
+ await db.deletePluginValue(pluginName, key);
1288
+ return;
177
1289
  }
1290
+ console.warn("Plugin storage not supported by database adapter");
178
1291
  },
179
- getEndpoints() {
180
- return Array.from(endpoints.values());
1292
+ async list(pluginName, prefix) {
1293
+ if (db.listPluginKeys) {
1294
+ return db.listPluginKeys(pluginName, prefix);
1295
+ }
1296
+ console.warn("Plugin storage not supported by database adapter");
1297
+ return [];
181
1298
  },
182
- async testEndpoint(endpointId) {
183
- const endpoint = endpoints.get(endpointId);
184
- if (!endpoint) {
185
- return {
186
- endpointId,
187
- success: false,
188
- error: "Endpoint not found",
189
- retryCount: 0
190
- };
1299
+ async clear(pluginName) {
1300
+ if (db.clearPluginData) {
1301
+ await db.clearPluginData(pluginName);
1302
+ return;
191
1303
  }
192
- const testPayload = {
193
- id: randomUUID(),
194
- event: "device.heartbeat",
195
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
196
- data: {
197
- test: true,
198
- message: "OpenMDM webhook test"
199
- }
200
- };
201
- return deliverToEndpoint(endpoint, testPayload);
1304
+ console.warn("Plugin storage not supported by database adapter");
202
1305
  }
203
1306
  };
204
1307
  }
205
- function verifyWebhookSignature(payload, signature, secret) {
206
- const expectedSignature = `sha256=${createHmac("sha256", secret).update(payload).digest("hex")}`;
207
- if (signature.length !== expectedSignature.length) {
208
- return false;
209
- }
210
- let result = 0;
211
- for (let i = 0; i < signature.length; i++) {
212
- result |= signature.charCodeAt(i) ^ expectedSignature.charCodeAt(i);
1308
+ function createMemoryPluginStorageAdapter() {
1309
+ const store = /* @__PURE__ */ new Map();
1310
+ function getPluginStore(pluginName) {
1311
+ if (!store.has(pluginName)) {
1312
+ store.set(pluginName, /* @__PURE__ */ new Map());
1313
+ }
1314
+ return store.get(pluginName);
213
1315
  }
214
- return result === 0;
1316
+ return {
1317
+ async get(pluginName, key) {
1318
+ const pluginStore = getPluginStore(pluginName);
1319
+ const value = pluginStore.get(key);
1320
+ return value === void 0 ? null : value;
1321
+ },
1322
+ async set(pluginName, key, value) {
1323
+ const pluginStore = getPluginStore(pluginName);
1324
+ pluginStore.set(key, value);
1325
+ },
1326
+ async delete(pluginName, key) {
1327
+ const pluginStore = getPluginStore(pluginName);
1328
+ pluginStore.delete(key);
1329
+ },
1330
+ async list(pluginName, prefix) {
1331
+ const pluginStore = getPluginStore(pluginName);
1332
+ const keys = Array.from(pluginStore.keys());
1333
+ if (prefix) {
1334
+ return keys.filter((k) => k.startsWith(prefix));
1335
+ }
1336
+ return keys;
1337
+ },
1338
+ async clear(pluginName) {
1339
+ store.delete(pluginName);
1340
+ }
1341
+ };
1342
+ }
1343
+ function createPluginKey(namespace, ...parts) {
1344
+ return [namespace, ...parts].join(":");
1345
+ }
1346
+ function parsePluginKey(key) {
1347
+ const [namespace, ...parts] = key.split(":");
1348
+ return { namespace, parts };
215
1349
  }
216
1350
 
217
1351
  // src/schema.ts
@@ -586,6 +1720,265 @@ var mdmSchema = {
586
1720
  { columns: ["status"] },
587
1721
  { columns: ["created_at"] }
588
1722
  ]
1723
+ },
1724
+ // ----------------------------------------
1725
+ // Tenants Table (Multi-tenancy)
1726
+ // ----------------------------------------
1727
+ mdm_tenants: {
1728
+ columns: {
1729
+ id: { type: "string", primaryKey: true },
1730
+ name: { type: "string" },
1731
+ slug: { type: "string", unique: true },
1732
+ status: {
1733
+ type: "enum",
1734
+ enumValues: ["active", "suspended", "pending"],
1735
+ default: "pending"
1736
+ },
1737
+ settings: { type: "json", nullable: true },
1738
+ metadata: { type: "json", nullable: true },
1739
+ created_at: { type: "datetime", default: "now" },
1740
+ updated_at: { type: "datetime", default: "now" }
1741
+ },
1742
+ indexes: [
1743
+ { columns: ["slug"], unique: true },
1744
+ { columns: ["status"] }
1745
+ ]
1746
+ },
1747
+ // ----------------------------------------
1748
+ // Roles Table (RBAC)
1749
+ // ----------------------------------------
1750
+ mdm_roles: {
1751
+ columns: {
1752
+ id: { type: "string", primaryKey: true },
1753
+ tenant_id: {
1754
+ type: "string",
1755
+ nullable: true,
1756
+ references: { table: "mdm_tenants", column: "id", onDelete: "cascade" }
1757
+ },
1758
+ name: { type: "string" },
1759
+ description: { type: "text", nullable: true },
1760
+ permissions: { type: "json" },
1761
+ is_system: { type: "boolean", default: false },
1762
+ created_at: { type: "datetime", default: "now" },
1763
+ updated_at: { type: "datetime", default: "now" }
1764
+ },
1765
+ indexes: [
1766
+ { columns: ["tenant_id"] },
1767
+ { columns: ["name"] },
1768
+ { columns: ["tenant_id", "name"], unique: true }
1769
+ ]
1770
+ },
1771
+ // ----------------------------------------
1772
+ // Users Table (RBAC)
1773
+ // ----------------------------------------
1774
+ mdm_users: {
1775
+ columns: {
1776
+ id: { type: "string", primaryKey: true },
1777
+ tenant_id: {
1778
+ type: "string",
1779
+ nullable: true,
1780
+ references: { table: "mdm_tenants", column: "id", onDelete: "cascade" }
1781
+ },
1782
+ email: { type: "string" },
1783
+ name: { type: "string", nullable: true },
1784
+ status: {
1785
+ type: "enum",
1786
+ enumValues: ["active", "inactive", "pending"],
1787
+ default: "pending"
1788
+ },
1789
+ metadata: { type: "json", nullable: true },
1790
+ last_login_at: { type: "datetime", nullable: true },
1791
+ created_at: { type: "datetime", default: "now" },
1792
+ updated_at: { type: "datetime", default: "now" }
1793
+ },
1794
+ indexes: [
1795
+ { columns: ["tenant_id"] },
1796
+ { columns: ["email"] },
1797
+ { columns: ["tenant_id", "email"], unique: true },
1798
+ { columns: ["status"] }
1799
+ ]
1800
+ },
1801
+ // ----------------------------------------
1802
+ // User Roles (Many-to-Many)
1803
+ // ----------------------------------------
1804
+ mdm_user_roles: {
1805
+ columns: {
1806
+ user_id: {
1807
+ type: "string",
1808
+ references: { table: "mdm_users", column: "id", onDelete: "cascade" }
1809
+ },
1810
+ role_id: {
1811
+ type: "string",
1812
+ references: { table: "mdm_roles", column: "id", onDelete: "cascade" }
1813
+ },
1814
+ created_at: { type: "datetime", default: "now" }
1815
+ },
1816
+ indexes: [
1817
+ { columns: ["user_id", "role_id"], unique: true },
1818
+ { columns: ["user_id"] },
1819
+ { columns: ["role_id"] }
1820
+ ]
1821
+ },
1822
+ // ----------------------------------------
1823
+ // Audit Logs Table
1824
+ // ----------------------------------------
1825
+ mdm_audit_logs: {
1826
+ columns: {
1827
+ id: { type: "string", primaryKey: true },
1828
+ tenant_id: {
1829
+ type: "string",
1830
+ nullable: true,
1831
+ references: { table: "mdm_tenants", column: "id", onDelete: "cascade" }
1832
+ },
1833
+ user_id: {
1834
+ type: "string",
1835
+ nullable: true,
1836
+ references: { table: "mdm_users", column: "id", onDelete: "set null" }
1837
+ },
1838
+ action: { type: "string" },
1839
+ resource: { type: "string" },
1840
+ resource_id: { type: "string", nullable: true },
1841
+ details: { type: "json", nullable: true },
1842
+ ip_address: { type: "string", nullable: true },
1843
+ user_agent: { type: "text", nullable: true },
1844
+ created_at: { type: "datetime", default: "now" }
1845
+ },
1846
+ indexes: [
1847
+ { columns: ["tenant_id"] },
1848
+ { columns: ["user_id"] },
1849
+ { columns: ["action"] },
1850
+ { columns: ["resource"] },
1851
+ { columns: ["resource", "resource_id"] },
1852
+ { columns: ["created_at"] }
1853
+ ]
1854
+ },
1855
+ // ----------------------------------------
1856
+ // Scheduled Tasks Table
1857
+ // ----------------------------------------
1858
+ mdm_scheduled_tasks: {
1859
+ columns: {
1860
+ id: { type: "string", primaryKey: true },
1861
+ tenant_id: {
1862
+ type: "string",
1863
+ nullable: true,
1864
+ references: { table: "mdm_tenants", column: "id", onDelete: "cascade" }
1865
+ },
1866
+ name: { type: "string" },
1867
+ description: { type: "text", nullable: true },
1868
+ task_type: {
1869
+ type: "enum",
1870
+ enumValues: ["command", "policy_update", "app_install", "maintenance", "custom"]
1871
+ },
1872
+ schedule: { type: "json" },
1873
+ target: { type: "json", nullable: true },
1874
+ payload: { type: "json", nullable: true },
1875
+ status: {
1876
+ type: "enum",
1877
+ enumValues: ["active", "paused", "completed", "failed"],
1878
+ default: "active"
1879
+ },
1880
+ next_run_at: { type: "datetime", nullable: true },
1881
+ last_run_at: { type: "datetime", nullable: true },
1882
+ max_retries: { type: "integer", default: 3 },
1883
+ retry_count: { type: "integer", default: 0 },
1884
+ created_at: { type: "datetime", default: "now" },
1885
+ updated_at: { type: "datetime", default: "now" }
1886
+ },
1887
+ indexes: [
1888
+ { columns: ["tenant_id"] },
1889
+ { columns: ["task_type"] },
1890
+ { columns: ["status"] },
1891
+ { columns: ["next_run_at"] }
1892
+ ]
1893
+ },
1894
+ // ----------------------------------------
1895
+ // Task Executions Table
1896
+ // ----------------------------------------
1897
+ mdm_task_executions: {
1898
+ columns: {
1899
+ id: { type: "string", primaryKey: true },
1900
+ task_id: {
1901
+ type: "string",
1902
+ references: { table: "mdm_scheduled_tasks", column: "id", onDelete: "cascade" }
1903
+ },
1904
+ status: {
1905
+ type: "enum",
1906
+ enumValues: ["running", "completed", "failed"],
1907
+ default: "running"
1908
+ },
1909
+ started_at: { type: "datetime", default: "now" },
1910
+ completed_at: { type: "datetime", nullable: true },
1911
+ devices_processed: { type: "integer", default: 0 },
1912
+ devices_succeeded: { type: "integer", default: 0 },
1913
+ devices_failed: { type: "integer", default: 0 },
1914
+ error: { type: "text", nullable: true },
1915
+ details: { type: "json", nullable: true }
1916
+ },
1917
+ indexes: [
1918
+ { columns: ["task_id"] },
1919
+ { columns: ["status"] },
1920
+ { columns: ["started_at"] }
1921
+ ]
1922
+ },
1923
+ // ----------------------------------------
1924
+ // Message Queue Table
1925
+ // ----------------------------------------
1926
+ mdm_message_queue: {
1927
+ columns: {
1928
+ id: { type: "string", primaryKey: true },
1929
+ tenant_id: {
1930
+ type: "string",
1931
+ nullable: true,
1932
+ references: { table: "mdm_tenants", column: "id", onDelete: "cascade" }
1933
+ },
1934
+ device_id: {
1935
+ type: "string",
1936
+ references: { table: "mdm_devices", column: "id", onDelete: "cascade" }
1937
+ },
1938
+ message_type: { type: "string" },
1939
+ payload: { type: "json" },
1940
+ priority: {
1941
+ type: "enum",
1942
+ enumValues: ["high", "normal", "low"],
1943
+ default: "normal"
1944
+ },
1945
+ status: {
1946
+ type: "enum",
1947
+ enumValues: ["pending", "processing", "delivered", "failed", "expired"],
1948
+ default: "pending"
1949
+ },
1950
+ attempts: { type: "integer", default: 0 },
1951
+ max_attempts: { type: "integer", default: 3 },
1952
+ last_attempt_at: { type: "datetime", nullable: true },
1953
+ last_error: { type: "text", nullable: true },
1954
+ expires_at: { type: "datetime", nullable: true },
1955
+ created_at: { type: "datetime", default: "now" },
1956
+ updated_at: { type: "datetime", default: "now" }
1957
+ },
1958
+ indexes: [
1959
+ { columns: ["tenant_id"] },
1960
+ { columns: ["device_id"] },
1961
+ { columns: ["status"] },
1962
+ { columns: ["priority"] },
1963
+ { columns: ["expires_at"] },
1964
+ { columns: ["device_id", "status", "priority"] }
1965
+ ]
1966
+ },
1967
+ // ----------------------------------------
1968
+ // Plugin Storage Table
1969
+ // ----------------------------------------
1970
+ mdm_plugin_storage: {
1971
+ columns: {
1972
+ plugin_name: { type: "string" },
1973
+ key: { type: "string" },
1974
+ value: { type: "json" },
1975
+ created_at: { type: "datetime", default: "now" },
1976
+ updated_at: { type: "datetime", default: "now" }
1977
+ },
1978
+ indexes: [
1979
+ { columns: ["plugin_name", "key"], unique: true },
1980
+ { columns: ["plugin_name"] }
1981
+ ]
589
1982
  }
590
1983
  }
591
1984
  };
@@ -632,6 +2025,13 @@ function createMDM(config) {
632
2025
  const eventHandlers = /* @__PURE__ */ new Map();
633
2026
  const pushAdapter = push ? createPushAdapter(push, database) : createStubPushAdapter();
634
2027
  const webhookManager = webhooksConfig ? createWebhookManager(webhooksConfig) : void 0;
2028
+ const tenantManager = config.multiTenancy?.enabled ? createTenantManager(database) : void 0;
2029
+ const authorizationManager = config.authorization?.enabled ? createAuthorizationManager(database) : void 0;
2030
+ const auditManager = config.audit?.enabled ? createAuditManager(database) : void 0;
2031
+ const scheduleManager = config.scheduling?.enabled ? createScheduleManager(database) : void 0;
2032
+ const messageQueueManager = database.enqueueMessage ? createMessageQueueManager(database) : void 0;
2033
+ const dashboardManager = createDashboardManager(database);
2034
+ const pluginStorageAdapter = config.pluginStorage?.adapter === "database" ? createPluginStorageAdapter(database) : config.pluginStorage?.adapter === "memory" ? createMemoryPluginStorageAdapter() : void 0;
635
2035
  const on = (event, handler) => {
636
2036
  if (!eventHandlers.has(event)) {
637
2037
  eventHandlers.set(event, /* @__PURE__ */ new Set());
@@ -1034,6 +2434,110 @@ function createMDM(config) {
1034
2434
  async getChildren(groupId) {
1035
2435
  const allGroups = await database.listGroups();
1036
2436
  return allGroups.filter((g) => g.parentId === groupId);
2437
+ },
2438
+ async getTree(rootId) {
2439
+ if (database.getGroupTree) {
2440
+ return database.getGroupTree(rootId);
2441
+ }
2442
+ const allGroups = await database.listGroups();
2443
+ new Map(allGroups.map((g) => [g.id, g]));
2444
+ const buildNode = (group, depth, path) => {
2445
+ const children = allGroups.filter((g) => g.parentId === group.id).map((child) => buildNode(child, depth + 1, [...path, group.id]));
2446
+ return {
2447
+ ...group,
2448
+ children,
2449
+ depth,
2450
+ path,
2451
+ effectivePolicyId: group.policyId
2452
+ };
2453
+ };
2454
+ const roots = allGroups.filter(
2455
+ (g) => rootId ? g.id === rootId : !g.parentId
2456
+ );
2457
+ return roots.map((root) => buildNode(root, 0, []));
2458
+ },
2459
+ async getAncestors(groupId) {
2460
+ if (database.getGroupAncestors) {
2461
+ return database.getGroupAncestors(groupId);
2462
+ }
2463
+ const ancestors = [];
2464
+ const allGroups = await database.listGroups();
2465
+ const groupMap = new Map(allGroups.map((g) => [g.id, g]));
2466
+ let current = groupMap.get(groupId);
2467
+ while (current?.parentId) {
2468
+ const parent = groupMap.get(current.parentId);
2469
+ if (parent) {
2470
+ ancestors.push(parent);
2471
+ current = parent;
2472
+ } else {
2473
+ break;
2474
+ }
2475
+ }
2476
+ return ancestors;
2477
+ },
2478
+ async getDescendants(groupId) {
2479
+ if (database.getGroupDescendants) {
2480
+ return database.getGroupDescendants(groupId);
2481
+ }
2482
+ const allGroups = await database.listGroups();
2483
+ const descendants = [];
2484
+ const findDescendants = (parentId) => {
2485
+ const children = allGroups.filter((g) => g.parentId === parentId);
2486
+ for (const child of children) {
2487
+ descendants.push(child);
2488
+ findDescendants(child.id);
2489
+ }
2490
+ };
2491
+ findDescendants(groupId);
2492
+ return descendants;
2493
+ },
2494
+ async move(groupId, newParentId) {
2495
+ if (newParentId) {
2496
+ const ancestors = await this.getAncestors(newParentId);
2497
+ if (ancestors.some((a) => a.id === groupId)) {
2498
+ throw new Error("Cannot move group: would create circular reference");
2499
+ }
2500
+ }
2501
+ return database.updateGroup(groupId, { parentId: newParentId });
2502
+ },
2503
+ async getEffectivePolicy(groupId) {
2504
+ if (database.getGroupEffectivePolicy) {
2505
+ return database.getGroupEffectivePolicy(groupId);
2506
+ }
2507
+ const group = await database.findGroup(groupId);
2508
+ if (!group) return null;
2509
+ if (group.policyId) {
2510
+ return database.findPolicy(group.policyId);
2511
+ }
2512
+ const ancestors = await this.getAncestors(groupId);
2513
+ for (const ancestor of ancestors) {
2514
+ if (ancestor.policyId) {
2515
+ return database.findPolicy(ancestor.policyId);
2516
+ }
2517
+ }
2518
+ return null;
2519
+ },
2520
+ async getHierarchyStats() {
2521
+ if (database.getGroupHierarchyStats) {
2522
+ return database.getGroupHierarchyStats();
2523
+ }
2524
+ const allGroups = await database.listGroups();
2525
+ let maxDepth = 0;
2526
+ let groupsWithDevices = 0;
2527
+ let groupsWithPolicies = 0;
2528
+ for (const group of allGroups) {
2529
+ const ancestors = await this.getAncestors(group.id);
2530
+ maxDepth = Math.max(maxDepth, ancestors.length);
2531
+ const devices2 = await database.listDevicesInGroup(group.id);
2532
+ if (devices2.length > 0) groupsWithDevices++;
2533
+ if (group.policyId) groupsWithPolicies++;
2534
+ }
2535
+ return {
2536
+ totalGroups: allGroups.length,
2537
+ maxDepth,
2538
+ groupsWithDevices,
2539
+ groupsWithPolicies
2540
+ };
1037
2541
  }
1038
2542
  };
1039
2543
  const enroll = async (request) => {
@@ -1245,7 +2749,15 @@ function createMDM(config) {
1245
2749
  processHeartbeat,
1246
2750
  verifyDeviceToken,
1247
2751
  getPlugins,
1248
- getPlugin
2752
+ getPlugin,
2753
+ // Enterprise managers (optional)
2754
+ tenants: tenantManager,
2755
+ authorization: authorizationManager,
2756
+ audit: auditManager,
2757
+ schedules: scheduleManager,
2758
+ messageQueue: messageQueueManager,
2759
+ dashboard: dashboardManager,
2760
+ pluginStorage: pluginStorageAdapter
1249
2761
  };
1250
2762
  (async () => {
1251
2763
  for (const plugin of plugins) {
@@ -1363,6 +2875,6 @@ function generateDeviceToken(deviceId, secret, expirationSeconds) {
1363
2875
  return `${header}.${payload}.${signature}`;
1364
2876
  }
1365
2877
 
1366
- export { ApplicationNotFoundError, AuthenticationError, AuthorizationError, DeviceNotFoundError, EnrollmentError, MDMError, PolicyNotFoundError, ValidationError, camelToSnake, createMDM, createWebhookManager, getColumnNames, getPrimaryKey, getTableNames, mdmSchema, snakeToCamel, transformToCamelCase, transformToSnakeCase, verifyWebhookSignature };
2878
+ export { ApplicationNotFoundError, AuthenticationError, AuthorizationError, DeviceNotFoundError, EnrollmentError, GroupNotFoundError, MDMError, PolicyNotFoundError, RoleNotFoundError, TenantNotFoundError, UserNotFoundError, ValidationError, camelToSnake, createAuditManager, createAuthorizationManager, createDashboardManager, createMDM, createMemoryPluginStorageAdapter, createMessageQueueManager, createPluginKey, createPluginStorageAdapter, createScheduleManager, createTenantManager, createWebhookManager, getColumnNames, getPrimaryKey, getTableNames, mdmSchema, parsePluginKey, snakeToCamel, transformToCamelCase, transformToSnakeCase, verifyWebhookSignature };
1367
2879
  //# sourceMappingURL=index.js.map
1368
2880
  //# sourceMappingURL=index.js.map