@openmdm/core 0.8.0 → 0.9.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.d.ts +180 -3
- package/dist/index.js +233 -5
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +150 -2
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/dashboard.ts +40 -0
- package/src/device-identity.ts +338 -0
- package/src/index.ts +165 -5
- package/src/types.ts +155 -1
package/dist/types.d.ts
CHANGED
|
@@ -21,6 +21,21 @@ interface Device {
|
|
|
21
21
|
agentVersion?: string | null;
|
|
22
22
|
lastHeartbeat?: Date | null;
|
|
23
23
|
lastSync?: Date | null;
|
|
24
|
+
/**
|
|
25
|
+
* Base64-encoded SPKI public key the device registered on first
|
|
26
|
+
* enrollment. Requests from this device can be verified against
|
|
27
|
+
* this key via `verifyDeviceRequest` / `verifyEcdsaSignature` from
|
|
28
|
+
* `@openmdm/core`. `null` on devices that enrolled via the legacy
|
|
29
|
+
* HMAC path and have never been migrated.
|
|
30
|
+
*/
|
|
31
|
+
publicKey?: string | null;
|
|
32
|
+
/**
|
|
33
|
+
* How the device originally enrolled. `'hmac'` for the legacy
|
|
34
|
+
* shared-secret path; `'pinned-key'` for the device-pinned ECDSA
|
|
35
|
+
* path. `null` on pre-Phase-2b device rows that predate the
|
|
36
|
+
* column (treated as `'hmac'`).
|
|
37
|
+
*/
|
|
38
|
+
enrollmentMethod?: 'hmac' | 'pinned-key' | null;
|
|
24
39
|
batteryLevel?: number | null;
|
|
25
40
|
storageUsed?: number | null;
|
|
26
41
|
storageTotal?: number | null;
|
|
@@ -74,6 +89,10 @@ interface UpdateDeviceInput {
|
|
|
74
89
|
location?: DeviceLocation;
|
|
75
90
|
tags?: Record<string, string>;
|
|
76
91
|
metadata?: Record<string, unknown>;
|
|
92
|
+
/** Phase 2b — pin a new public key on first enroll. */
|
|
93
|
+
publicKey?: string | null;
|
|
94
|
+
/** Phase 2b — record which auth path the device enrolled via. */
|
|
95
|
+
enrollmentMethod?: 'hmac' | 'pinned-key' | null;
|
|
77
96
|
}
|
|
78
97
|
interface DeviceFilter {
|
|
79
98
|
status?: DeviceStatus | DeviceStatus[];
|
|
@@ -354,7 +373,41 @@ interface EnrollmentRequest {
|
|
|
354
373
|
agentPackage?: string;
|
|
355
374
|
method: EnrollmentMethod;
|
|
356
375
|
timestamp: string;
|
|
376
|
+
/**
|
|
377
|
+
* Signature over the canonical enrollment message.
|
|
378
|
+
*
|
|
379
|
+
* Phase 2a (HMAC path, backwards-compatible): hex-encoded
|
|
380
|
+
* HMAC-SHA256 of the nine-field pipe-delimited canonical form
|
|
381
|
+
* (see `concepts/enrollment`).
|
|
382
|
+
*
|
|
383
|
+
* Phase 2b (device-pinned-key path, preferred): base64-encoded
|
|
384
|
+
* DER ECDSA-P256 signature produced by the device's Keystore
|
|
385
|
+
* private key, over `canonicalEnrollmentMessage(...)` including
|
|
386
|
+
* the public key and challenge. The server distinguishes the
|
|
387
|
+
* two paths by whether `publicKey` is present on the request.
|
|
388
|
+
*/
|
|
357
389
|
signature: string;
|
|
390
|
+
/**
|
|
391
|
+
* Base64-encoded SPKI public key (EC P-256) the device generated
|
|
392
|
+
* in its Keystore. When present, enrollment follows the Phase 2b
|
|
393
|
+
* device-pinned-key path and `signature` must verify as an ECDSA
|
|
394
|
+
* signature against this key. The server pins this key on the
|
|
395
|
+
* device row on first successful enroll; any future enroll
|
|
396
|
+
* attempting a different key for the same `enrollmentId` is
|
|
397
|
+
* rejected with `PublicKeyMismatchError`.
|
|
398
|
+
*
|
|
399
|
+
* Omit for the legacy HMAC path. Callers that want to migrate a
|
|
400
|
+
* fleet gradually can run both paths in parallel.
|
|
401
|
+
*/
|
|
402
|
+
publicKey?: string;
|
|
403
|
+
/**
|
|
404
|
+
* Opaque challenge issued by `GET /agent/enroll/challenge`. Must
|
|
405
|
+
* be present whenever `publicKey` is present — the server uses
|
|
406
|
+
* it to prevent replay of captured enrollment payloads. The
|
|
407
|
+
* challenge is single-use: the server consumes it on first
|
|
408
|
+
* successful verify.
|
|
409
|
+
*/
|
|
410
|
+
attestationChallenge?: string;
|
|
358
411
|
policyId?: string;
|
|
359
412
|
groupId?: string;
|
|
360
413
|
}
|
|
@@ -369,6 +422,47 @@ interface EnrollmentResponse {
|
|
|
369
422
|
refreshToken?: string;
|
|
370
423
|
tokenExpiresAt?: Date;
|
|
371
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* Single-use nonce issued by `GET /agent/enroll/challenge` and
|
|
427
|
+
* consumed on first successful verify of a device-pinned-key
|
|
428
|
+
* enrollment. A persisted record; the `consume*` adapter methods
|
|
429
|
+
* enforce the single-use invariant.
|
|
430
|
+
*/
|
|
431
|
+
interface EnrollmentChallenge {
|
|
432
|
+
challenge: string;
|
|
433
|
+
expiresAt: Date;
|
|
434
|
+
consumedAt?: Date | null;
|
|
435
|
+
createdAt: Date;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Result of calling `verifyDeviceRequest`. Callers pattern-match on
|
|
439
|
+
* `ok` and, when `false`, on `reason` to decide their response
|
|
440
|
+
* shape:
|
|
441
|
+
*
|
|
442
|
+
* - `not-found` — unknown device id. Return 401.
|
|
443
|
+
* - `no-pinned-key` — device exists but never migrated off the
|
|
444
|
+
* legacy HMAC path. Caller should fall back
|
|
445
|
+
* to their HMAC verifier, or fail if
|
|
446
|
+
* they've completed their migration.
|
|
447
|
+
* - `signature-invalid` — signature did not verify against the
|
|
448
|
+
* pinned key. Return 401. Never re-pin
|
|
449
|
+
* in response to this.
|
|
450
|
+
*/
|
|
451
|
+
type DeviceIdentityVerification = {
|
|
452
|
+
ok: true;
|
|
453
|
+
device: Device;
|
|
454
|
+
} | {
|
|
455
|
+
ok: false;
|
|
456
|
+
reason: 'not-found';
|
|
457
|
+
} | {
|
|
458
|
+
ok: false;
|
|
459
|
+
reason: 'no-pinned-key';
|
|
460
|
+
device: Device;
|
|
461
|
+
} | {
|
|
462
|
+
ok: false;
|
|
463
|
+
reason: 'signature-invalid';
|
|
464
|
+
device: Device;
|
|
465
|
+
};
|
|
372
466
|
interface PushConfig {
|
|
373
467
|
provider: 'fcm' | 'mqtt' | 'websocket' | 'polling';
|
|
374
468
|
fcmSenderId?: string;
|
|
@@ -590,7 +684,7 @@ interface PushProviderConfig {
|
|
|
590
684
|
interface EnrollmentConfig {
|
|
591
685
|
/** Auto-enroll devices with valid signature */
|
|
592
686
|
autoEnroll?: boolean;
|
|
593
|
-
/** HMAC secret for device signature verification */
|
|
687
|
+
/** HMAC secret for device signature verification (Phase 2a path) */
|
|
594
688
|
deviceSecret: string;
|
|
595
689
|
/** Allowed enrollment methods */
|
|
596
690
|
allowedMethods?: EnrollmentMethod[];
|
|
@@ -602,6 +696,32 @@ interface EnrollmentConfig {
|
|
|
602
696
|
requireApproval?: boolean;
|
|
603
697
|
/** Custom enrollment validation */
|
|
604
698
|
validate?: (request: EnrollmentRequest) => Promise<boolean>;
|
|
699
|
+
/**
|
|
700
|
+
* Phase 2b device-pinned-key configuration. Optional — when
|
|
701
|
+
* omitted, enrollment continues to accept the Phase 2a HMAC path
|
|
702
|
+
* exclusively, matching pre-0.9 behaviour.
|
|
703
|
+
*/
|
|
704
|
+
pinnedKey?: PinnedKeyConfig;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Device-pinned-key enrollment options. See
|
|
708
|
+
* `docs/concepts/enrollment` for the full flow.
|
|
709
|
+
*/
|
|
710
|
+
interface PinnedKeyConfig {
|
|
711
|
+
/**
|
|
712
|
+
* Require every new enrollment to use the pinned-key path. When
|
|
713
|
+
* `true`, requests without `publicKey` are rejected. When `false`
|
|
714
|
+
* (the default), both paths coexist during rollout — the server
|
|
715
|
+
* pins a public key when one is provided, falls back to HMAC when
|
|
716
|
+
* it isn't.
|
|
717
|
+
*/
|
|
718
|
+
required?: boolean;
|
|
719
|
+
/**
|
|
720
|
+
* TTL for enrollment challenges, in seconds. Defaults to 300
|
|
721
|
+
* (5 minutes). Challenges are single-use; this only bounds how
|
|
722
|
+
* long an unused challenge stays valid.
|
|
723
|
+
*/
|
|
724
|
+
challengeTtlSeconds?: number;
|
|
605
725
|
}
|
|
606
726
|
interface DatabaseAdapter {
|
|
607
727
|
findDevice(id: string): Promise<Device | null>;
|
|
@@ -659,6 +779,34 @@ interface DatabaseAdapter {
|
|
|
659
779
|
getGroupEffectivePolicy?(groupId: string): Promise<Policy | null>;
|
|
660
780
|
moveGroup?(groupId: string, newParentId: string | null): Promise<Group>;
|
|
661
781
|
getGroupHierarchyStats?(): Promise<GroupHierarchyStats>;
|
|
782
|
+
/**
|
|
783
|
+
* Persist a new single-use enrollment challenge. The adapter
|
|
784
|
+
* should store it with `consumed_at = null` and enforce a
|
|
785
|
+
* primary-key constraint on `challenge` so duplicate inserts
|
|
786
|
+
* fail loudly.
|
|
787
|
+
*/
|
|
788
|
+
createEnrollmentChallenge?(challenge: EnrollmentChallenge): Promise<void>;
|
|
789
|
+
/**
|
|
790
|
+
* Look up a challenge by its opaque value. Returns `null` if not
|
|
791
|
+
* found. Does NOT filter on expiry — the core layer checks
|
|
792
|
+
* freshness so the adapter stays dumb.
|
|
793
|
+
*/
|
|
794
|
+
findEnrollmentChallenge?(challenge: string): Promise<EnrollmentChallenge | null>;
|
|
795
|
+
/**
|
|
796
|
+
* Atomically mark a challenge as consumed. Must set
|
|
797
|
+
* `consumed_at = now()` and return the updated row only when the
|
|
798
|
+
* challenge was previously unused. Adapters should implement
|
|
799
|
+
* this as a conditional UPDATE (e.g. Postgres
|
|
800
|
+
* `UPDATE ... WHERE consumed_at IS NULL RETURNING *`) so two
|
|
801
|
+
* concurrent consume attempts cannot both succeed.
|
|
802
|
+
*/
|
|
803
|
+
consumeEnrollmentChallenge?(challenge: string): Promise<EnrollmentChallenge | null>;
|
|
804
|
+
/**
|
|
805
|
+
* Delete expired, unconsumed challenges. Called periodically by
|
|
806
|
+
* the core layer; adapters can no-op if they rely on a TTL index
|
|
807
|
+
* elsewhere.
|
|
808
|
+
*/
|
|
809
|
+
pruneExpiredEnrollmentChallenges?(now: Date): Promise<number>;
|
|
662
810
|
findTenant?(id: string): Promise<Tenant | null>;
|
|
663
811
|
findTenantBySlug?(slug: string): Promise<Tenant | null>;
|
|
664
812
|
listTenants?(filter?: TenantFilter): Promise<TenantListResult>;
|
|
@@ -1544,4 +1692,4 @@ declare class ValidationError extends MDMError {
|
|
|
1544
1692
|
constructor(message: string, details?: unknown);
|
|
1545
1693
|
}
|
|
1546
1694
|
|
|
1547
|
-
export { type AppInstallationSummary, type AppRollback, type AppVersion, type Application, type ApplicationManager, ApplicationNotFoundError, type AuditAction, type AuditConfig, type AuditLog, type AuditLogFilter, type AuditLogListResult, type AuditManager, type AuditSummary, type AuthConfig, AuthenticationError, AuthorizationError, type AuthorizationManager, type Command, type CommandFilter, type CommandManager, CommandNotFoundError, type CommandResult, type CommandStatus, type CommandSuccessRates, type CommandType, type CreateAppRollbackInput, type CreateApplicationInput, type CreateAuditLogInput, type CreateDeviceInput, type CreateGroupInput, type CreatePolicyInput, type CreateRoleInput, type CreateScheduledTaskInput, type CreateTenantInput, type CreateUserInput, type DashboardManager, type DashboardStats, type DatabaseAdapter, type DeployTarget, type Device, type DeviceFilter, type DeviceListResult, type DeviceLocation, type DeviceManager, DeviceNotFoundError, type DeviceStatus, type DeviceStatusBreakdown, type EnqueueMessageInput, type EnrollmentConfig, EnrollmentError, type EnrollmentMethod, type EnrollmentRequest, type EnrollmentResponse, type EnrollmentTrendPoint, type EventFilter, type EventHandler, type EventPayloadMap, type EventType, type Group, type GroupHierarchyStats, type GroupManager, GroupNotFoundError, type GroupTreeNode, type HardwareControl, type Heartbeat, type InstalledApp, type LogContext, type Logger, type MDMConfig, MDMError, type MDMEvent, type MDMInstance, type MDMPlugin, type MaintenanceWindow, type MessageQueueManager, type PasswordPolicy, type Permission, type PermissionAction, type PermissionResource, type PluginMiddleware, type PluginRoute, type PluginStorageAdapter, type PluginStorageEntry, type Policy, type PolicyApplication, type PolicyManager, PolicyNotFoundError, type PolicySettings, type PushAdapter, type PushBatchResult, type PushConfig, type PushMessage, type PushProviderConfig, type PushResult, type PushToken, type QueueMessageStatus, type QueueStats, type QueuedMessage, type RegisterPushTokenInput, type Role, RoleNotFoundError, type ScheduleManager, type ScheduledTask, type ScheduledTaskFilter, type ScheduledTaskListResult, type ScheduledTaskStatus, type SendCommandInput, type StorageConfig, type SystemUpdatePolicy, type TaskExecution, type TaskSchedule, type TaskType, type Tenant, type TenantFilter, type TenantListResult, type TenantManager, TenantNotFoundError, type TenantSettings, type TenantStats, type TenantStatus, type TimeWindow, type UpdateApplicationInput, type UpdateDeviceInput, type UpdateGroupInput, type UpdatePolicyInput, type UpdateRoleInput, type UpdateScheduledTaskInput, type UpdateTenantInput, type UpdateUserInput, type User, type UserFilter, type UserListResult, UserNotFoundError, type UserWithRoles, ValidationError, type VpnConfig, type WebhookConfig, type WebhookDeliveryResult, type WebhookEndpoint, type WebhookManager, type WifiConfig };
|
|
1695
|
+
export { type AppInstallationSummary, type AppRollback, type AppVersion, type Application, type ApplicationManager, ApplicationNotFoundError, type AuditAction, type AuditConfig, type AuditLog, type AuditLogFilter, type AuditLogListResult, type AuditManager, type AuditSummary, type AuthConfig, AuthenticationError, AuthorizationError, type AuthorizationManager, type Command, type CommandFilter, type CommandManager, CommandNotFoundError, type CommandResult, type CommandStatus, type CommandSuccessRates, type CommandType, type CreateAppRollbackInput, type CreateApplicationInput, type CreateAuditLogInput, type CreateDeviceInput, type CreateGroupInput, type CreatePolicyInput, type CreateRoleInput, type CreateScheduledTaskInput, type CreateTenantInput, type CreateUserInput, type DashboardManager, type DashboardStats, type DatabaseAdapter, type DeployTarget, type Device, type DeviceFilter, type DeviceIdentityVerification, type DeviceListResult, type DeviceLocation, type DeviceManager, DeviceNotFoundError, type DeviceStatus, type DeviceStatusBreakdown, type EnqueueMessageInput, type EnrollmentChallenge, type EnrollmentConfig, EnrollmentError, type EnrollmentMethod, type EnrollmentRequest, type EnrollmentResponse, type EnrollmentTrendPoint, type EventFilter, type EventHandler, type EventPayloadMap, type EventType, type Group, type GroupHierarchyStats, type GroupManager, GroupNotFoundError, type GroupTreeNode, type HardwareControl, type Heartbeat, type InstalledApp, type LogContext, type Logger, type MDMConfig, MDMError, type MDMEvent, type MDMInstance, type MDMPlugin, type MaintenanceWindow, type MessageQueueManager, type PasswordPolicy, type Permission, type PermissionAction, type PermissionResource, type PinnedKeyConfig, type PluginMiddleware, type PluginRoute, type PluginStorageAdapter, type PluginStorageEntry, type Policy, type PolicyApplication, type PolicyManager, PolicyNotFoundError, type PolicySettings, type PushAdapter, type PushBatchResult, type PushConfig, type PushMessage, type PushProviderConfig, type PushResult, type PushToken, type QueueMessageStatus, type QueueStats, type QueuedMessage, type RegisterPushTokenInput, type Role, RoleNotFoundError, type ScheduleManager, type ScheduledTask, type ScheduledTaskFilter, type ScheduledTaskListResult, type ScheduledTaskStatus, type SendCommandInput, type StorageConfig, type SystemUpdatePolicy, type TaskExecution, type TaskSchedule, type TaskType, type Tenant, type TenantFilter, type TenantListResult, type TenantManager, TenantNotFoundError, type TenantSettings, type TenantStats, type TenantStatus, type TimeWindow, type UpdateApplicationInput, type UpdateDeviceInput, type UpdateGroupInput, type UpdatePolicyInput, type UpdateRoleInput, type UpdateScheduledTaskInput, type UpdateTenantInput, type UpdateUserInput, type User, type UserFilter, type UserListResult, UserNotFoundError, type UserWithRoles, ValidationError, type VpnConfig, type WebhookConfig, type WebhookDeliveryResult, type WebhookEndpoint, type WebhookManager, type WifiConfig };
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts"],"names":[],"mappings":";AAg0DO,IAAM,QAAA,GAAN,cAAuB,KAAA,CAAM;AAAA,EAClC,WAAA,CACE,OAAA,EACO,IAAA,EACA,UAAA,GAAqB,KACrB,OAAA,EACP;AACA,IAAA,KAAA,CAAM,OAAO,CAAA;AAJN,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AACA,IAAA,IAAA,CAAA,UAAA,GAAA,UAAA;AACA,IAAA,IAAA,CAAA,OAAA,GAAA,OAAA;AAGP,IAAA,IAAA,CAAK,IAAA,GAAO,UAAA;AAAA,EACd;AACF;AAEO,IAAM,mBAAA,GAAN,cAAkC,QAAA,CAAS;AAAA,EAChD,YAAY,QAAA,EAAkB;AAC5B,IAAA,KAAA,CAAM,CAAA,kBAAA,EAAqB,QAAQ,CAAA,CAAA,EAAI,kBAAA,EAAoB,GAAG,CAAA;AAAA,EAChE;AACF;AAEO,IAAM,mBAAA,GAAN,cAAkC,QAAA,CAAS;AAAA,EAChD,YAAY,QAAA,EAAkB;AAC5B,IAAA,KAAA,CAAM,CAAA,kBAAA,EAAqB,QAAQ,CAAA,CAAA,EAAI,kBAAA,EAAoB,GAAG,CAAA;AAAA,EAChE;AACF;AAEO,IAAM,wBAAA,GAAN,cAAuC,QAAA,CAAS;AAAA,EACrD,YAAY,UAAA,EAAoB;AAC9B,IAAA,KAAA,CAAM,CAAA,uBAAA,EAA0B,UAAU,CAAA,CAAA,EAAI,uBAAA,EAAyB,GAAG,CAAA;AAAA,EAC5E;AACF;AAEO,IAAM,oBAAA,GAAN,cAAmC,QAAA,CAAS;AAAA,EACjD,YAAY,SAAA,EAAmB;AAC7B,IAAA,KAAA,CAAM,CAAA,mBAAA,EAAsB,SAAS,CAAA,CAAA,EAAI,mBAAA,EAAqB,GAAG,CAAA;AAAA,EACnE;AACF;AAEO,IAAM,mBAAA,GAAN,cAAkC,QAAA,CAAS;AAAA,EAChD,YAAY,UAAA,EAAoB;AAC9B,IAAA,KAAA,CAAM,CAAA,kBAAA,EAAqB,UAAU,CAAA,CAAA,EAAI,kBAAA,EAAoB,GAAG,CAAA;AAAA,EAClE;AACF;AAEO,IAAM,iBAAA,GAAN,cAAgC,QAAA,CAAS;AAAA,EAC9C,YAAY,UAAA,EAAoB;AAC9B,IAAA,KAAA,CAAM,CAAA,gBAAA,EAAmB,UAAU,CAAA,CAAA,EAAI,gBAAA,EAAkB,GAAG,CAAA;AAAA,EAC9D;AACF;AAEO,IAAM,kBAAA,GAAN,cAAiC,QAAA,CAAS;AAAA,EAC/C,YAAY,UAAA,EAAoB;AAC9B,IAAA,KAAA,CAAM,CAAA,iBAAA,EAAoB,UAAU,CAAA,CAAA,EAAI,iBAAA,EAAmB,GAAG,CAAA;AAAA,EAChE;AACF;AAEO,IAAM,iBAAA,GAAN,cAAgC,QAAA,CAAS;AAAA,EAC9C,YAAY,UAAA,EAAoB;AAC9B,IAAA,KAAA,CAAM,CAAA,gBAAA,EAAmB,UAAU,CAAA,CAAA,EAAI,gBAAA,EAAkB,GAAG,CAAA;AAAA,EAC9D;AACF;AAEO,IAAM,eAAA,GAAN,cAA8B,QAAA,CAAS;AAAA,EAC5C,WAAA,CAAY,SAAiB,OAAA,EAAmB;AAC9C,IAAA,KAAA,CAAM,OAAA,EAAS,kBAAA,EAAoB,GAAA,EAAK,OAAO,CAAA;AAAA,EACjD;AACF;AAEO,IAAM,mBAAA,GAAN,cAAkC,QAAA,CAAS;AAAA,EAChD,WAAA,CAAY,UAAkB,yBAAA,EAA2B;AACvD,IAAA,KAAA,CAAM,OAAA,EAAS,wBAAwB,GAAG,CAAA;AAAA,EAC5C;AACF;AAEO,IAAM,kBAAA,GAAN,cAAiC,QAAA,CAAS;AAAA,EAC/C,WAAA,CAAY,UAAkB,eAAA,EAAiB;AAC7C,IAAA,KAAA,CAAM,OAAA,EAAS,uBAAuB,GAAG,CAAA;AAAA,EAC3C;AACF;AAEO,IAAM,eAAA,GAAN,cAA8B,QAAA,CAAS;AAAA,EAC5C,WAAA,CAAY,SAAiB,OAAA,EAAmB;AAC9C,IAAA,KAAA,CAAM,OAAA,EAAS,kBAAA,EAAoB,GAAA,EAAK,OAAO,CAAA;AAAA,EACjD;AACF","file":"types.js","sourcesContent":["/**\n * OpenMDM Core Types\n *\n * These types define the core data structures for the MDM system.\n * Designed to be database-agnostic and framework-agnostic.\n */\n\n// ============================================\n// Device Types\n// ============================================\n\nexport type DeviceStatus = 'pending' | 'enrolled' | 'unenrolled' | 'blocked';\n\nexport interface Device {\n id: string;\n externalId?: string | null;\n enrollmentId: string;\n status: DeviceStatus;\n\n // Device Info\n model?: string | null;\n manufacturer?: string | null;\n osVersion?: string | null;\n serialNumber?: string | null;\n imei?: string | null;\n macAddress?: string | null;\n androidId?: string | null;\n\n // MDM State\n policyId?: string | null;\n agentVersion?: string | null; // MDM agent version installed on device\n lastHeartbeat?: Date | null;\n lastSync?: Date | null;\n\n // Telemetry\n batteryLevel?: number | null;\n storageUsed?: number | null;\n storageTotal?: number | null;\n location?: DeviceLocation | null;\n installedApps?: InstalledApp[] | null;\n\n // Metadata\n tags?: Record<string, string> | null;\n metadata?: Record<string, unknown> | null;\n\n // Timestamps\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface DeviceLocation {\n latitude: number;\n longitude: number;\n accuracy?: number;\n timestamp: Date;\n}\n\nexport interface InstalledApp {\n packageName: string;\n version: string;\n versionCode?: number;\n installedAt?: Date;\n}\n\nexport interface CreateDeviceInput {\n enrollmentId: string;\n externalId?: string;\n model?: string;\n manufacturer?: string;\n osVersion?: string;\n serialNumber?: string;\n imei?: string;\n macAddress?: string;\n androidId?: string;\n policyId?: string;\n tags?: Record<string, string>;\n metadata?: Record<string, unknown>;\n}\n\nexport interface UpdateDeviceInput {\n externalId?: string | null;\n status?: DeviceStatus;\n policyId?: string | null;\n agentVersion?: string | null;\n model?: string;\n manufacturer?: string;\n osVersion?: string;\n batteryLevel?: number | null;\n storageUsed?: number | null;\n storageTotal?: number | null;\n lastHeartbeat?: Date;\n lastSync?: Date;\n installedApps?: InstalledApp[];\n location?: DeviceLocation;\n tags?: Record<string, string>;\n metadata?: Record<string, unknown>;\n}\n\nexport interface DeviceFilter {\n status?: DeviceStatus | DeviceStatus[];\n policyId?: string;\n groupId?: string;\n search?: string;\n tags?: Record<string, string>;\n limit?: number;\n offset?: number;\n}\n\nexport interface DeviceListResult {\n devices: Device[];\n total: number;\n limit: number;\n offset: number;\n}\n\n// ============================================\n// Policy Types\n// ============================================\n\nexport interface Policy {\n id: string;\n name: string;\n description?: string | null;\n isDefault: boolean;\n settings: PolicySettings;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface PolicySettings {\n // Kiosk Mode\n kioskMode?: boolean;\n mainApp?: string;\n allowedApps?: string[];\n kioskExitPassword?: string;\n\n // Lock Features\n lockStatusBar?: boolean;\n lockNavigationBar?: boolean;\n lockSettings?: boolean;\n lockPowerButton?: boolean;\n blockInstall?: boolean;\n blockUninstall?: boolean;\n\n // Hardware Controls\n bluetooth?: HardwareControl;\n wifi?: HardwareControl;\n gps?: HardwareControl;\n mobileData?: HardwareControl;\n camera?: HardwareControl;\n microphone?: HardwareControl;\n usb?: HardwareControl;\n nfc?: HardwareControl;\n\n // Update Settings\n systemUpdatePolicy?: SystemUpdatePolicy;\n updateWindow?: TimeWindow;\n\n // Security\n passwordPolicy?: PasswordPolicy;\n encryptionRequired?: boolean;\n factoryResetProtection?: boolean;\n safeBootDisabled?: boolean;\n\n // Telemetry\n heartbeatInterval?: number;\n locationReportInterval?: number;\n locationEnabled?: boolean;\n\n // Network\n wifiConfigs?: WifiConfig[];\n vpnConfig?: VpnConfig;\n\n // Applications\n applications?: PolicyApplication[];\n\n // Custom settings (for plugins)\n custom?: Record<string, unknown>;\n}\n\nexport type HardwareControl = 'on' | 'off' | 'user';\nexport type SystemUpdatePolicy = 'auto' | 'windowed' | 'postpone' | 'manual';\n\nexport interface TimeWindow {\n start: string; // \"HH:MM\"\n end: string; // \"HH:MM\"\n}\n\nexport interface PasswordPolicy {\n required: boolean;\n minLength?: number;\n complexity?: 'none' | 'numeric' | 'alphanumeric' | 'complex';\n maxFailedAttempts?: number;\n expirationDays?: number;\n historyLength?: number;\n}\n\nexport interface WifiConfig {\n ssid: string;\n securityType: 'none' | 'wep' | 'wpa' | 'wpa2' | 'wpa3';\n password?: string;\n hidden?: boolean;\n autoConnect?: boolean;\n}\n\nexport interface VpnConfig {\n type: 'pptp' | 'l2tp' | 'ipsec' | 'openvpn' | 'wireguard';\n server: string;\n username?: string;\n password?: string;\n certificate?: string;\n config?: Record<string, unknown>;\n}\n\nexport interface PolicyApplication {\n packageName: string;\n action: 'install' | 'update' | 'uninstall';\n version?: string;\n required?: boolean;\n autoUpdate?: boolean;\n}\n\nexport interface CreatePolicyInput {\n name: string;\n description?: string;\n isDefault?: boolean;\n settings: PolicySettings;\n}\n\nexport interface UpdatePolicyInput {\n name?: string;\n description?: string | null;\n isDefault?: boolean;\n settings?: PolicySettings;\n}\n\n// ============================================\n// Application Types\n// ============================================\n\nexport interface Application {\n id: string;\n name: string;\n packageName: string;\n version: string;\n versionCode: number;\n url: string;\n hash?: string | null;\n size?: number | null;\n minSdkVersion?: number | null;\n\n // Deployment settings\n showIcon: boolean;\n runAfterInstall: boolean;\n runAtBoot: boolean;\n isSystem: boolean;\n\n // State\n isActive: boolean;\n\n // Metadata\n metadata?: Record<string, unknown> | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface CreateApplicationInput {\n name: string;\n packageName: string;\n version: string;\n versionCode: number;\n url: string;\n hash?: string;\n size?: number;\n minSdkVersion?: number;\n showIcon?: boolean;\n runAfterInstall?: boolean;\n runAtBoot?: boolean;\n isSystem?: boolean;\n metadata?: Record<string, unknown>;\n}\n\nexport interface UpdateApplicationInput {\n name?: string;\n version?: string;\n versionCode?: number;\n url?: string;\n hash?: string | null;\n size?: number | null;\n minSdkVersion?: number | null;\n showIcon?: boolean;\n runAfterInstall?: boolean;\n runAtBoot?: boolean;\n isActive?: boolean;\n metadata?: Record<string, unknown> | null;\n}\n\nexport interface DeployTarget {\n devices?: string[];\n policies?: string[];\n groups?: string[];\n}\n\n// ============================================\n// App Version & Rollback Types\n// ============================================\n\nexport interface AppVersion {\n id: string;\n applicationId: string;\n packageName: string;\n version: string;\n versionCode: number;\n url: string;\n hash?: string | null;\n size?: number | null;\n releaseNotes?: string | null;\n isMinimumVersion: boolean;\n createdAt: Date;\n}\n\nexport interface AppRollback {\n id: string;\n deviceId: string;\n packageName: string;\n fromVersion: string;\n fromVersionCode: number;\n toVersion: string;\n toVersionCode: number;\n reason?: string | null;\n status: 'pending' | 'in_progress' | 'completed' | 'failed';\n error?: string | null;\n initiatedBy?: string | null;\n createdAt: Date;\n completedAt?: Date | null;\n}\n\nexport interface CreateAppRollbackInput {\n deviceId: string;\n packageName: string;\n toVersionCode: number;\n reason?: string;\n initiatedBy?: string;\n}\n\n// ============================================\n// Command Types\n// ============================================\n\nexport type CommandType =\n | 'reboot'\n | 'shutdown'\n | 'sync'\n | 'lock'\n | 'unlock'\n | 'wipe'\n | 'factoryReset'\n | 'installApp'\n | 'uninstallApp'\n | 'updateApp'\n | 'runApp'\n | 'clearAppData'\n | 'clearAppCache'\n | 'shell'\n | 'setPolicy'\n | 'grantPermissions'\n | 'exitKiosk'\n | 'enterKiosk'\n | 'setWifi'\n | 'screenshot'\n | 'getLocation'\n | 'setVolume'\n | 'sendNotification'\n | 'whitelistBattery' // Whitelist app from battery optimization (Doze)\n | 'enablePermissiveMode' // Enable permissive mode for debugging\n | 'setTimeZone' // Set device timezone\n | 'enableAdb' // Enable/disable ADB debugging\n | 'rollbackApp' // Rollback to previous app version\n | 'updateAgent' // Update MDM agent to a new version\n | 'custom';\n\nexport type CommandStatus =\n | 'pending'\n | 'sent'\n | 'acknowledged'\n | 'completed'\n | 'failed'\n | 'cancelled';\n\nexport interface Command {\n id: string;\n deviceId: string;\n type: CommandType;\n payload?: Record<string, unknown> | null;\n status: CommandStatus;\n result?: CommandResult | null;\n error?: string | null;\n createdAt: Date;\n sentAt?: Date | null;\n acknowledgedAt?: Date | null;\n completedAt?: Date | null;\n}\n\nexport interface CommandResult {\n success: boolean;\n message?: string;\n data?: unknown;\n}\n\nexport interface SendCommandInput {\n deviceId: string;\n type: CommandType;\n payload?: Record<string, unknown>;\n}\n\nexport interface CommandFilter {\n deviceId?: string;\n status?: CommandStatus | CommandStatus[];\n type?: CommandType | CommandType[];\n limit?: number;\n offset?: number;\n}\n\n// ============================================\n// Event Types\n// ============================================\n\nexport type EventType =\n | 'device.enrolled'\n | 'device.unenrolled'\n | 'device.blocked'\n | 'device.heartbeat'\n | 'device.locationUpdated'\n | 'device.statusChanged'\n | 'device.policyChanged'\n | 'app.installed'\n | 'app.uninstalled'\n | 'app.updated'\n | 'app.crashed'\n | 'app.started'\n | 'app.stopped'\n | 'policy.applied'\n | 'policy.failed'\n | 'command.received'\n | 'command.acknowledged'\n | 'command.completed'\n | 'command.failed'\n | 'security.tamper'\n | 'security.rootDetected'\n | 'security.screenLocked'\n | 'security.screenUnlocked'\n | 'custom';\n\nexport interface MDMEvent<T = unknown> {\n id: string;\n deviceId: string;\n type: EventType;\n payload: T;\n createdAt: Date;\n}\n\nexport interface EventFilter {\n deviceId?: string;\n type?: EventType | EventType[];\n startDate?: Date;\n endDate?: Date;\n limit?: number;\n offset?: number;\n}\n\n// ============================================\n// Group Types\n// ============================================\n\nexport interface Group {\n id: string;\n name: string;\n description?: string | null;\n policyId?: string | null;\n parentId?: string | null;\n metadata?: Record<string, unknown> | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface CreateGroupInput {\n name: string;\n description?: string;\n policyId?: string;\n parentId?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface UpdateGroupInput {\n name?: string;\n description?: string | null;\n policyId?: string | null;\n parentId?: string | null;\n metadata?: Record<string, unknown> | null;\n}\n\n// ============================================\n// Enrollment Types\n// ============================================\n\nexport type EnrollmentMethod =\n | 'qr'\n | 'nfc'\n | 'zero-touch'\n | 'knox'\n | 'manual'\n | 'app-only'\n | 'adb';\n\nexport interface EnrollmentRequest {\n // Device identifiers (at least one required)\n macAddress?: string;\n serialNumber?: string;\n imei?: string;\n androidId?: string;\n\n // Device info\n model: string;\n manufacturer: string;\n osVersion: string;\n sdkVersion?: number;\n\n // Agent info\n agentVersion?: string;\n agentPackage?: string;\n\n // Enrollment details\n method: EnrollmentMethod;\n timestamp: string;\n signature: string;\n\n // Optional pre-assigned policy/group\n policyId?: string;\n groupId?: string;\n}\n\nexport interface EnrollmentResponse {\n deviceId: string;\n enrollmentId: string;\n policyId?: string;\n policy?: Policy;\n serverUrl: string;\n pushConfig: PushConfig;\n token: string;\n refreshToken?: string;\n tokenExpiresAt?: Date;\n}\n\nexport interface PushConfig {\n provider: 'fcm' | 'mqtt' | 'websocket' | 'polling';\n fcmSenderId?: string;\n mqttUrl?: string;\n mqttTopic?: string;\n mqttUsername?: string;\n mqttPassword?: string;\n wsUrl?: string;\n pollingInterval?: number;\n}\n\n// ============================================\n// Telemetry Types\n// ============================================\n\nexport interface Heartbeat {\n deviceId: string;\n timestamp: Date;\n\n // Battery\n batteryLevel: number;\n isCharging: boolean;\n batteryHealth?: 'good' | 'overheat' | 'dead' | 'cold' | 'unknown';\n\n // Storage\n storageUsed: number;\n storageTotal: number;\n\n // Memory\n memoryUsed: number;\n memoryTotal: number;\n\n // Network\n networkType?: 'wifi' | 'cellular' | 'ethernet' | 'none';\n networkName?: string; // SSID or carrier\n signalStrength?: number;\n ipAddress?: string;\n\n // Location\n location?: DeviceLocation;\n\n // Apps\n installedApps: InstalledApp[];\n runningApps?: string[];\n\n // Security\n isRooted?: boolean;\n isEncrypted?: boolean;\n screenLockEnabled?: boolean;\n\n // Agent status\n agentVersion?: string;\n policyVersion?: string;\n lastPolicySync?: Date;\n}\n\n// ============================================\n// Push Token Types\n// ============================================\n\nexport interface PushToken {\n id: string;\n deviceId: string;\n provider: 'fcm' | 'mqtt' | 'websocket';\n token: string;\n isActive: boolean;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface RegisterPushTokenInput {\n deviceId: string;\n provider: 'fcm' | 'mqtt' | 'websocket';\n token: string;\n}\n\n// ============================================\n// Configuration Types\n// ============================================\n\nexport interface MDMConfig {\n /** Database adapter for persistence */\n database: DatabaseAdapter;\n\n /** Authentication/authorization configuration */\n auth?: AuthConfig;\n\n /** Push notification provider configuration */\n push?: PushProviderConfig;\n\n /** Device enrollment configuration */\n enrollment?: EnrollmentConfig;\n\n /** Server URL (used in enrollment responses) */\n serverUrl?: string;\n\n /** APK/file storage configuration */\n storage?: StorageConfig;\n\n /** Outbound webhook configuration */\n webhooks?: WebhookConfig;\n\n /** Plugins to extend functionality */\n plugins?: MDMPlugin[];\n\n /** Event handlers */\n onDeviceEnrolled?: (device: Device) => Promise<void>;\n onDeviceUnenrolled?: (device: Device) => Promise<void>;\n onDeviceBlocked?: (device: Device) => Promise<void>;\n onHeartbeat?: (device: Device, heartbeat: Heartbeat) => Promise<void>;\n onCommand?: (command: Command) => Promise<void>;\n onEvent?: (event: MDMEvent) => Promise<void>;\n\n // Enterprise features\n /** Multi-tenancy configuration */\n multiTenancy?: {\n enabled: boolean;\n defaultTenantId?: string;\n tenantResolver?: (context: unknown) => Promise<string | null>;\n };\n\n /** Authorization (RBAC) configuration */\n authorization?: {\n enabled: boolean;\n defaultRole?: string;\n };\n\n /** Audit logging configuration */\n audit?: {\n enabled: boolean;\n retentionDays?: number;\n };\n\n /** Scheduling configuration */\n scheduling?: {\n enabled: boolean;\n timezone?: string;\n };\n\n /** Plugin storage configuration */\n pluginStorage?: {\n adapter: 'database' | 'memory';\n };\n\n /**\n * Structured logger. Replaces OpenMDM's internal `console.*` calls\n * so log output lands in the host application's logging pipeline\n * (pino, winston, bunyan, OTEL collector, etc.) instead of raw\n * stderr.\n *\n * The shape is a strict subset of the pino / winston / bunyan\n * interface — any of those can be passed directly. If omitted, a\n * default logger that writes to the console with an `[openmdm]`\n * prefix is used. To silence OpenMDM entirely, pass a no-op\n * implementation (see `createSilentLogger()` in the package\n * exports).\n */\n logger?: Logger;\n}\n\n/**\n * Minimal structured-logger interface OpenMDM calls internally.\n *\n * The shape is deliberately the pino-compatible subset: an optional\n * context object as the first argument followed by a message string.\n * pino, winston, bunyan, and most other structured loggers accept\n * this shape natively.\n *\n * Implementations should be side-effect-free on unconfigured levels\n * (a production logger filtered to `info` should still accept\n * `.debug()` calls cheaply).\n */\nexport interface Logger {\n /** Human-ignorable, high-volume tracing. Off in production by default. */\n debug(context: LogContext, message: string): void;\n debug(message: string): void;\n\n /** Normal operational events. Enrollment, policy changes, command delivery. */\n info(context: LogContext, message: string): void;\n info(message: string): void;\n\n /** Something is wrong but the server is still running. Retries, fallbacks, degraded modes. */\n warn(context: LogContext, message: string): void;\n warn(message: string): void;\n\n /** Something failed and a request/operation did not complete. */\n error(context: LogContext, message: string): void;\n error(message: string): void;\n\n /**\n * Return a new logger with the given fields attached to every\n * subsequent call. Used by managers and plugins to scope logs to\n * a specific subsystem without repeating context.\n */\n child(bindings: LogContext): Logger;\n}\n\n/**\n * Arbitrary structured context attached to a log line. Values must\n * be JSON-serializable so host loggers can ship them to any backend.\n */\nexport type LogContext = Record<string, unknown>;\n\nexport interface StorageConfig {\n /** Storage provider (s3, local, custom) */\n provider: 's3' | 'local' | 'custom';\n\n /** S3 configuration */\n s3?: {\n bucket: string;\n region: string;\n accessKeyId?: string;\n secretAccessKey?: string;\n endpoint?: string; // For S3-compatible services\n presignedUrlExpiry?: number; // Seconds, default 3600\n };\n\n /** Local storage path */\n localPath?: string;\n\n /** Custom storage adapter */\n customAdapter?: {\n upload: (file: Buffer, key: string) => Promise<string>;\n getUrl: (key: string) => Promise<string>;\n delete: (key: string) => Promise<void>;\n };\n}\n\nexport interface WebhookConfig {\n /** Webhook endpoints to notify */\n endpoints?: WebhookEndpoint[];\n\n /** Retry configuration */\n retry?: {\n maxRetries?: number;\n initialDelay?: number;\n maxDelay?: number;\n };\n\n /** Sign webhooks with HMAC secret */\n signingSecret?: string;\n}\n\nexport interface WebhookEndpoint {\n /** Unique identifier */\n id: string;\n /** Webhook URL */\n url: string;\n /** Events to trigger this webhook */\n events: (EventType | '*')[];\n /** Custom headers */\n headers?: Record<string, string>;\n /** Whether endpoint is active */\n enabled: boolean;\n}\n\nexport interface AuthConfig {\n /** Get current user from request context */\n getUser: <T = unknown>(context: unknown) => Promise<T | null>;\n /** Check if user has admin privileges */\n isAdmin?: (user: unknown) => Promise<boolean>;\n /** Check if user can access specific device */\n canAccessDevice?: (user: unknown, deviceId: string) => Promise<boolean>;\n /** Device JWT secret (for device auth tokens) */\n deviceTokenSecret?: string;\n /** Device token expiration in seconds (default: 365 days) */\n deviceTokenExpiration?: number;\n}\n\nexport interface PushProviderConfig {\n provider: 'fcm' | 'mqtt' | 'websocket' | 'polling';\n\n // FCM configuration\n fcmCredentials?: string | Record<string, unknown>;\n fcmProjectId?: string;\n\n // MQTT configuration\n mqttUrl?: string;\n mqttUsername?: string;\n mqttPassword?: string;\n mqttTopicPrefix?: string;\n\n // WebSocket configuration\n wsPath?: string;\n wsPingInterval?: number;\n\n // Polling fallback\n pollingInterval?: number;\n}\n\nexport interface EnrollmentConfig {\n /** Auto-enroll devices with valid signature */\n autoEnroll?: boolean;\n /** HMAC secret for device signature verification */\n deviceSecret: string;\n /** Allowed enrollment methods */\n allowedMethods?: EnrollmentMethod[];\n /** Default policy for new devices */\n defaultPolicyId?: string;\n /** Default group for new devices */\n defaultGroupId?: string;\n /** Require manual approval for enrollment */\n requireApproval?: boolean;\n /** Custom enrollment validation */\n validate?: (request: EnrollmentRequest) => Promise<boolean>;\n}\n\n// ============================================\n// Adapter Interfaces\n// ============================================\n\nexport interface DatabaseAdapter {\n // Devices\n findDevice(id: string): Promise<Device | null>;\n findDeviceByEnrollmentId(enrollmentId: string): Promise<Device | null>;\n listDevices(filter?: DeviceFilter): Promise<DeviceListResult>;\n createDevice(data: CreateDeviceInput): Promise<Device>;\n updateDevice(id: string, data: UpdateDeviceInput): Promise<Device>;\n deleteDevice(id: string): Promise<void>;\n countDevices(filter?: DeviceFilter): Promise<number>;\n\n // Policies\n findPolicy(id: string): Promise<Policy | null>;\n findDefaultPolicy(): Promise<Policy | null>;\n listPolicies(): Promise<Policy[]>;\n createPolicy(data: CreatePolicyInput): Promise<Policy>;\n updatePolicy(id: string, data: UpdatePolicyInput): Promise<Policy>;\n deletePolicy(id: string): Promise<void>;\n\n // Applications\n findApplication(id: string): Promise<Application | null>;\n findApplicationByPackage(packageName: string, version?: string): Promise<Application | null>;\n listApplications(activeOnly?: boolean): Promise<Application[]>;\n createApplication(data: CreateApplicationInput): Promise<Application>;\n updateApplication(id: string, data: UpdateApplicationInput): Promise<Application>;\n deleteApplication(id: string): Promise<void>;\n\n // Commands\n findCommand(id: string): Promise<Command | null>;\n listCommands(filter?: CommandFilter): Promise<Command[]>;\n createCommand(data: SendCommandInput): Promise<Command>;\n updateCommand(id: string, data: Partial<Command>): Promise<Command | null>;\n getPendingCommands(deviceId: string): Promise<Command[]>;\n\n // Events\n createEvent(event: Omit<MDMEvent, 'id' | 'createdAt'>): Promise<MDMEvent>;\n listEvents(filter?: EventFilter): Promise<MDMEvent[]>;\n\n // Groups\n findGroup(id: string): Promise<Group | null>;\n listGroups(): Promise<Group[]>;\n createGroup(data: CreateGroupInput): Promise<Group>;\n updateGroup(id: string, data: UpdateGroupInput): Promise<Group>;\n deleteGroup(id: string): Promise<void>;\n listDevicesInGroup(groupId: string): Promise<Device[]>;\n addDeviceToGroup(deviceId: string, groupId: string): Promise<void>;\n removeDeviceFromGroup(deviceId: string, groupId: string): Promise<void>;\n getDeviceGroups(deviceId: string): Promise<Group[]>;\n\n // Push Tokens\n findPushToken(deviceId: string, provider: string): Promise<PushToken | null>;\n upsertPushToken(data: RegisterPushTokenInput): Promise<PushToken>;\n deletePushToken(deviceId: string, provider?: string): Promise<void>;\n\n // App Versions (optional - for version tracking)\n listAppVersions?(packageName: string): Promise<AppVersion[]>;\n createAppVersion?(data: Omit<AppVersion, 'id' | 'createdAt'>): Promise<AppVersion>;\n setMinimumVersion?(packageName: string, versionCode: number): Promise<void>;\n getMinimumVersion?(packageName: string): Promise<AppVersion | null>;\n\n // Rollback History (optional)\n createRollback?(data: CreateAppRollbackInput): Promise<AppRollback>;\n updateRollback?(id: string, data: Partial<AppRollback>): Promise<AppRollback>;\n listRollbacks?(filter?: { deviceId?: string; packageName?: string }): Promise<AppRollback[]>;\n\n // Group Hierarchy (optional)\n getGroupChildren?(parentId: string | null): Promise<Group[]>;\n getGroupAncestors?(groupId: string): Promise<Group[]>;\n getGroupDescendants?(groupId: string): Promise<Group[]>;\n getGroupTree?(rootId?: string): Promise<GroupTreeNode[]>;\n getGroupEffectivePolicy?(groupId: string): Promise<Policy | null>;\n moveGroup?(groupId: string, newParentId: string | null): Promise<Group>;\n getGroupHierarchyStats?(): Promise<GroupHierarchyStats>;\n\n // Tenants (optional - for multi-tenancy)\n findTenant?(id: string): Promise<Tenant | null>;\n findTenantBySlug?(slug: string): Promise<Tenant | null>;\n listTenants?(filter?: TenantFilter): Promise<TenantListResult>;\n createTenant?(data: CreateTenantInput): Promise<Tenant>;\n updateTenant?(id: string, data: UpdateTenantInput): Promise<Tenant>;\n deleteTenant?(id: string): Promise<void>;\n getTenantStats?(tenantId: string): Promise<TenantStats>;\n\n // Users (optional - for RBAC)\n findUser?(id: string): Promise<User | null>;\n findUserByEmail?(email: string, tenantId?: string): Promise<User | null>;\n listUsers?(filter?: UserFilter): Promise<UserListResult>;\n createUser?(data: CreateUserInput): Promise<User>;\n updateUser?(id: string, data: UpdateUserInput): Promise<User>;\n deleteUser?(id: string): Promise<void>;\n\n // Roles (optional - for RBAC)\n findRole?(id: string): Promise<Role | null>;\n listRoles?(tenantId?: string): Promise<Role[]>;\n createRole?(data: CreateRoleInput): Promise<Role>;\n updateRole?(id: string, data: UpdateRoleInput): Promise<Role>;\n deleteRole?(id: string): Promise<void>;\n assignRoleToUser?(userId: string, roleId: string): Promise<void>;\n removeRoleFromUser?(userId: string, roleId: string): Promise<void>;\n getUserRoles?(userId: string): Promise<Role[]>;\n\n // Audit Logs (optional - for compliance)\n createAuditLog?(data: CreateAuditLogInput): Promise<AuditLog>;\n listAuditLogs?(filter?: AuditLogFilter): Promise<AuditLogListResult>;\n deleteAuditLogs?(filter: { olderThan?: Date; tenantId?: string }): Promise<number>;\n\n // Scheduled Tasks (optional - for scheduling)\n findScheduledTask?(id: string): Promise<ScheduledTask | null>;\n listScheduledTasks?(filter?: ScheduledTaskFilter): Promise<ScheduledTaskListResult>;\n createScheduledTask?(data: CreateScheduledTaskInput): Promise<ScheduledTask>;\n updateScheduledTask?(id: string, data: UpdateScheduledTaskInput): Promise<ScheduledTask>;\n deleteScheduledTask?(id: string): Promise<void>;\n getUpcomingTasks?(hours: number): Promise<ScheduledTask[]>;\n createTaskExecution?(data: { taskId: string }): Promise<TaskExecution>;\n updateTaskExecution?(id: string, data: Partial<TaskExecution>): Promise<TaskExecution>;\n listTaskExecutions?(taskId: string, limit?: number): Promise<TaskExecution[]>;\n\n // Message Queue (optional - for persistent messaging)\n enqueueMessage?(data: EnqueueMessageInput): Promise<QueuedMessage>;\n dequeueMessages?(deviceId: string, limit?: number): Promise<QueuedMessage[]>;\n peekMessages?(deviceId: string, limit?: number): Promise<QueuedMessage[]>;\n acknowledgeMessage?(messageId: string): Promise<void>;\n failMessage?(messageId: string, error: string): Promise<void>;\n retryFailedMessages?(maxAttempts?: number): Promise<number>;\n purgeExpiredMessages?(): Promise<number>;\n getQueueStats?(tenantId?: string): Promise<QueueStats>;\n\n // Plugin Storage (optional)\n getPluginValue?(pluginName: string, key: string): Promise<unknown | null>;\n setPluginValue?(pluginName: string, key: string, value: unknown): Promise<void>;\n deletePluginValue?(pluginName: string, key: string): Promise<void>;\n listPluginKeys?(pluginName: string, prefix?: string): Promise<string[]>;\n clearPluginData?(pluginName: string): Promise<void>;\n\n // Dashboard (optional - for analytics)\n getDashboardStats?(tenantId?: string): Promise<DashboardStats>;\n getDeviceStatusBreakdown?(tenantId?: string): Promise<DeviceStatusBreakdown>;\n getEnrollmentTrend?(days: number, tenantId?: string): Promise<EnrollmentTrendPoint[]>;\n getCommandSuccessRates?(tenantId?: string): Promise<CommandSuccessRates>;\n getAppInstallationSummary?(tenantId?: string): Promise<AppInstallationSummary>;\n\n // Transactions (optional)\n transaction?<T>(fn: () => Promise<T>): Promise<T>;\n}\n\nexport interface PushAdapter {\n /** Send push message to a device */\n send(deviceId: string, message: PushMessage): Promise<PushResult>;\n /** Send push message to multiple devices */\n sendBatch(deviceIds: string[], message: PushMessage): Promise<PushBatchResult>;\n /** Register device push token */\n registerToken?(deviceId: string, token: string): Promise<void>;\n /** Unregister device push token */\n unregisterToken?(deviceId: string): Promise<void>;\n /** Subscribe device to topic */\n subscribe?(deviceId: string, topic: string): Promise<void>;\n /** Unsubscribe device from topic */\n unsubscribe?(deviceId: string, topic: string): Promise<void>;\n}\n\nexport interface PushMessage {\n type: string;\n payload?: Record<string, unknown>;\n priority?: 'high' | 'normal';\n ttl?: number;\n collapseKey?: string;\n}\n\nexport interface PushResult {\n success: boolean;\n messageId?: string;\n error?: string;\n}\n\nexport interface PushBatchResult {\n successCount: number;\n failureCount: number;\n results: Array<{ deviceId: string; result: PushResult }>;\n}\n\n// ============================================\n// Plugin Interface\n// ============================================\n\nexport interface MDMPlugin {\n /** Unique plugin name */\n name: string;\n /** Plugin version */\n version: string;\n\n /** Called when MDM is initialized */\n onInit?(mdm: MDMInstance): Promise<void>;\n\n /** Called when MDM is destroyed */\n onDestroy?(): Promise<void>;\n\n /** Additional routes to mount */\n routes?: PluginRoute[];\n\n /** Middleware to apply to all routes */\n middleware?: PluginMiddleware[];\n\n /** Extend enrollment process */\n onEnroll?(device: Device, request: EnrollmentRequest): Promise<void>;\n\n /** Extend device processing */\n onDeviceEnrolled?(device: Device): Promise<void>;\n onDeviceUnenrolled?(device: Device): Promise<void>;\n onHeartbeat?(device: Device, heartbeat: Heartbeat): Promise<void>;\n\n /** Extend policy processing */\n policySchema?: Record<string, unknown>;\n validatePolicy?(settings: PolicySettings): Promise<{ valid: boolean; errors?: string[] }>;\n applyPolicy?(device: Device, policy: Policy): Promise<void>;\n\n /** Extend command processing */\n commandTypes?: CommandType[];\n executeCommand?(device: Device, command: Command): Promise<CommandResult>;\n}\n\nexport interface PluginRoute {\n method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';\n path: string;\n handler: (context: unknown) => Promise<unknown>;\n auth?: boolean;\n admin?: boolean;\n}\n\nexport type PluginMiddleware = (\n context: unknown,\n next: () => Promise<unknown>\n) => Promise<unknown>;\n\n// ============================================\n// MDM Instance Interface\n// ============================================\n\nexport interface WebhookManager {\n /** Deliver an event to all matching webhook endpoints */\n deliver<T>(event: MDMEvent<T>): Promise<WebhookDeliveryResult[]>;\n /** Add a webhook endpoint at runtime */\n addEndpoint(endpoint: WebhookEndpoint): void;\n /** Remove a webhook endpoint */\n removeEndpoint(endpointId: string): void;\n /** Update a webhook endpoint */\n updateEndpoint(endpointId: string, updates: Partial<WebhookEndpoint>): void;\n /** Get all configured endpoints */\n getEndpoints(): WebhookEndpoint[];\n /** Test a webhook endpoint with a test payload */\n testEndpoint(endpointId: string): Promise<WebhookDeliveryResult>;\n}\n\nexport interface WebhookDeliveryResult {\n endpointId: string;\n success: boolean;\n statusCode?: number;\n error?: string;\n retryCount: number;\n deliveredAt?: Date;\n}\n\nexport interface MDMInstance {\n /** Device management */\n devices: DeviceManager;\n /** Policy management */\n policies: PolicyManager;\n /** Application management */\n apps: ApplicationManager;\n /** Command management */\n commands: CommandManager;\n /** Group management */\n groups: GroupManager;\n\n /** Tenant management (if multi-tenancy enabled) */\n tenants?: TenantManager;\n /** Authorization management (RBAC) */\n authorization?: AuthorizationManager;\n /** Audit logging */\n audit?: AuditManager;\n /** Scheduled task management */\n schedules?: ScheduleManager;\n /** Persistent message queue */\n messageQueue?: MessageQueueManager;\n /** Dashboard analytics */\n dashboard?: DashboardManager;\n /** Plugin storage */\n pluginStorage?: PluginStorageAdapter;\n\n /** Push notification service */\n push: PushAdapter;\n\n /** Webhook delivery (if configured) */\n webhooks?: WebhookManager;\n\n /** Database adapter */\n db: DatabaseAdapter;\n\n /** Structured logger. Already scoped to the `openmdm` namespace. Plugins should call `.child({...})` to scope further. */\n logger: Logger;\n\n /** Configuration */\n config: MDMConfig;\n\n /** Subscribe to events */\n on<T extends EventType>(event: T, handler: EventHandler<T>): () => void;\n /** Emit an event */\n emit<T extends EventType>(event: T, data: EventPayloadMap[T]): Promise<void>;\n\n /** Process device enrollment */\n enroll(request: EnrollmentRequest): Promise<EnrollmentResponse>;\n /** Process device heartbeat */\n processHeartbeat(deviceId: string, heartbeat: Heartbeat): Promise<void>;\n /** Verify device token */\n verifyDeviceToken(token: string): Promise<{ deviceId: string } | null>;\n\n /** Get loaded plugins */\n getPlugins(): MDMPlugin[];\n /** Get plugin by name */\n getPlugin(name: string): MDMPlugin | undefined;\n}\n\n// ============================================\n// Manager Interfaces\n// ============================================\n\nexport interface DeviceManager {\n get(id: string): Promise<Device | null>;\n getByEnrollmentId(enrollmentId: string): Promise<Device | null>;\n list(filter?: DeviceFilter): Promise<DeviceListResult>;\n create(data: CreateDeviceInput): Promise<Device>;\n update(id: string, data: UpdateDeviceInput): Promise<Device>;\n delete(id: string): Promise<void>;\n assignPolicy(deviceId: string, policyId: string | null): Promise<Device>;\n addToGroup(deviceId: string, groupId: string): Promise<void>;\n removeFromGroup(deviceId: string, groupId: string): Promise<void>;\n getGroups(deviceId: string): Promise<Group[]>;\n sendCommand(deviceId: string, input: Omit<SendCommandInput, 'deviceId'>): Promise<Command>;\n sync(deviceId: string): Promise<Command>;\n reboot(deviceId: string): Promise<Command>;\n lock(deviceId: string, message?: string): Promise<Command>;\n wipe(deviceId: string, preserveData?: boolean): Promise<Command>;\n}\n\nexport interface PolicyManager {\n get(id: string): Promise<Policy | null>;\n getDefault(): Promise<Policy | null>;\n list(): Promise<Policy[]>;\n create(data: CreatePolicyInput): Promise<Policy>;\n update(id: string, data: UpdatePolicyInput): Promise<Policy>;\n delete(id: string): Promise<void>;\n setDefault(id: string): Promise<Policy>;\n getDevices(policyId: string): Promise<Device[]>;\n applyToDevice(policyId: string, deviceId: string): Promise<void>;\n}\n\nexport interface ApplicationManager {\n get(id: string): Promise<Application | null>;\n getByPackage(packageName: string, version?: string): Promise<Application | null>;\n list(activeOnly?: boolean): Promise<Application[]>;\n register(data: CreateApplicationInput): Promise<Application>;\n update(id: string, data: UpdateApplicationInput): Promise<Application>;\n delete(id: string): Promise<void>;\n activate(id: string): Promise<Application>;\n deactivate(id: string): Promise<Application>;\n deploy(packageName: string, target: DeployTarget): Promise<void>;\n installOnDevice(packageName: string, deviceId: string, version?: string): Promise<Command>;\n uninstallFromDevice(packageName: string, deviceId: string): Promise<Command>;\n}\n\nexport interface CommandManager {\n get(id: string): Promise<Command | null>;\n list(filter?: CommandFilter): Promise<Command[]>;\n send(input: SendCommandInput): Promise<Command>;\n cancel(id: string): Promise<Command>;\n acknowledge(id: string): Promise<Command>;\n complete(id: string, result: CommandResult): Promise<Command>;\n fail(id: string, error: string): Promise<Command>;\n getPending(deviceId: string): Promise<Command[]>;\n}\n\nexport interface GroupManager {\n // Basic CRUD operations\n get(id: string): Promise<Group | null>;\n list(): Promise<Group[]>;\n create(data: CreateGroupInput): Promise<Group>;\n update(id: string, data: UpdateGroupInput): Promise<Group>;\n delete(id: string): Promise<void>;\n\n // Device management\n getDevices(groupId: string): Promise<Device[]>;\n addDevice(groupId: string, deviceId: string): Promise<void>;\n removeDevice(groupId: string, deviceId: string): Promise<void>;\n\n // Hierarchy operations\n getChildren(groupId: string): Promise<Group[]>;\n getTree(rootId?: string): Promise<GroupTreeNode[]>;\n getAncestors(groupId: string): Promise<Group[]>;\n getDescendants(groupId: string): Promise<Group[]>;\n move(groupId: string, newParentId: string | null): Promise<Group>;\n getEffectivePolicy(groupId: string): Promise<Policy | null>;\n getHierarchyStats(): Promise<GroupHierarchyStats>;\n}\n\n// ============================================\n// Group Hierarchy Types\n// ============================================\n\nexport interface GroupTreeNode extends Group {\n children: GroupTreeNode[];\n depth: number;\n path: string[];\n effectivePolicyId?: string | null;\n}\n\nexport interface GroupHierarchyStats {\n totalGroups: number;\n maxDepth: number;\n groupsWithDevices: number;\n groupsWithPolicies: number;\n}\n\n// ============================================\n// Tenant Types (Multi-tenancy)\n// ============================================\n\nexport type TenantStatus = 'active' | 'suspended' | 'pending';\n\nexport interface Tenant {\n id: string;\n name: string;\n slug: string;\n status: TenantStatus;\n settings?: TenantSettings | null;\n metadata?: Record<string, unknown> | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface TenantSettings {\n maxDevices?: number;\n maxUsers?: number;\n features?: string[];\n branding?: {\n logo?: string;\n primaryColor?: string;\n };\n}\n\nexport interface CreateTenantInput {\n name: string;\n slug: string;\n settings?: TenantSettings;\n metadata?: Record<string, unknown>;\n}\n\nexport interface UpdateTenantInput {\n name?: string;\n slug?: string;\n status?: TenantStatus;\n settings?: TenantSettings;\n metadata?: Record<string, unknown>;\n}\n\nexport interface TenantFilter {\n status?: TenantStatus;\n search?: string;\n limit?: number;\n offset?: number;\n}\n\nexport interface TenantListResult {\n tenants: Tenant[];\n total: number;\n limit: number;\n offset: number;\n}\n\nexport interface TenantStats {\n deviceCount: number;\n userCount: number;\n policyCount: number;\n appCount: number;\n}\n\n// ============================================\n// RBAC Types (Role-Based Access Control)\n// ============================================\n\nexport type PermissionAction = 'create' | 'read' | 'update' | 'delete' | 'manage' | '*';\nexport type PermissionResource = 'devices' | 'policies' | 'apps' | 'groups' | 'commands' | 'users' | 'roles' | 'tenants' | 'audit' | '*';\n\nexport interface Permission {\n action: PermissionAction;\n resource: PermissionResource;\n resourceId?: string;\n}\n\nexport interface Role {\n id: string;\n tenantId?: string | null;\n name: string;\n description?: string | null;\n permissions: Permission[];\n isSystem: boolean;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface CreateRoleInput {\n tenantId?: string;\n name: string;\n description?: string;\n permissions: Permission[];\n}\n\nexport interface UpdateRoleInput {\n name?: string;\n description?: string;\n permissions?: Permission[];\n}\n\nexport interface User {\n id: string;\n tenantId?: string | null;\n email: string;\n name?: string | null;\n status: 'active' | 'inactive' | 'pending';\n metadata?: Record<string, unknown> | null;\n lastLoginAt?: Date | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface UserWithRoles extends User {\n roles: Role[];\n}\n\nexport interface CreateUserInput {\n tenantId?: string;\n email: string;\n name?: string;\n status?: 'active' | 'inactive' | 'pending';\n metadata?: Record<string, unknown>;\n}\n\nexport interface UpdateUserInput {\n email?: string;\n name?: string;\n status?: 'active' | 'inactive' | 'pending';\n metadata?: Record<string, unknown>;\n}\n\nexport interface UserFilter {\n tenantId?: string;\n status?: 'active' | 'inactive' | 'pending';\n search?: string;\n limit?: number;\n offset?: number;\n}\n\nexport interface UserListResult {\n users: User[];\n total: number;\n limit: number;\n offset: number;\n}\n\n// ============================================\n// Audit Types\n// ============================================\n\nexport type AuditAction = 'create' | 'read' | 'update' | 'delete' | 'login' | 'logout' | 'enroll' | 'unenroll' | 'command' | 'export' | 'import' | 'custom';\n\nexport interface AuditLog {\n id: string;\n tenantId?: string | null;\n userId?: string | null;\n action: AuditAction;\n resource: string;\n resourceId?: string | null;\n status: 'success' | 'failure';\n error?: string | null;\n details?: Record<string, unknown> | null;\n ipAddress?: string | null;\n userAgent?: string | null;\n createdAt: Date;\n}\n\nexport interface CreateAuditLogInput {\n tenantId?: string;\n userId?: string;\n action: AuditAction;\n resource: string;\n resourceId?: string;\n status?: 'success' | 'failure';\n error?: string;\n details?: Record<string, unknown>;\n ipAddress?: string;\n userAgent?: string;\n}\n\nexport interface AuditConfig {\n enabled: boolean;\n retentionDays?: number;\n skipReadOperations?: boolean;\n logActions?: AuditAction[];\n logResources?: string[];\n}\n\nexport interface AuditSummary {\n totalLogs: number;\n byAction: Record<AuditAction, number>;\n byResource: Record<string, number>;\n byStatus: { success: number; failure: number };\n topUsers: Array<{ userId: string; count: number }>;\n recentFailures: AuditLog[];\n}\n\nexport interface AuditLogFilter {\n tenantId?: string;\n userId?: string;\n action?: string;\n resource?: string;\n resourceId?: string;\n startDate?: Date;\n endDate?: Date;\n limit?: number;\n offset?: number;\n}\n\nexport interface AuditLogListResult {\n logs: AuditLog[];\n total: number;\n limit: number;\n offset: number;\n}\n\n// ============================================\n// Schedule Types\n// ============================================\n\nexport type TaskType = 'command' | 'policy_update' | 'app_install' | 'maintenance' | 'custom';\nexport type ScheduledTaskStatus = 'active' | 'paused' | 'completed' | 'failed';\n\nexport interface MaintenanceWindow {\n daysOfWeek: number[];\n startTime: string;\n endTime: string;\n timezone: string;\n}\n\nexport interface TaskSchedule {\n type: 'once' | 'recurring' | 'window';\n executeAt?: Date;\n cron?: string;\n window?: MaintenanceWindow;\n}\n\nexport interface ScheduledTask {\n id: string;\n tenantId?: string | null;\n name: string;\n description?: string | null;\n taskType: TaskType;\n schedule: TaskSchedule;\n target?: DeployTarget;\n payload?: Record<string, unknown> | null;\n status: ScheduledTaskStatus;\n nextRunAt?: Date | null;\n lastRunAt?: Date | null;\n maxRetries: number;\n retryCount: number;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface CreateScheduledTaskInput {\n tenantId?: string;\n name: string;\n description?: string;\n taskType: TaskType;\n schedule: TaskSchedule;\n target?: DeployTarget;\n payload?: Record<string, unknown>;\n maxRetries?: number;\n}\n\nexport interface UpdateScheduledTaskInput {\n name?: string;\n description?: string;\n schedule?: TaskSchedule;\n target?: DeployTarget;\n payload?: Record<string, unknown>;\n status?: ScheduledTaskStatus;\n maxRetries?: number;\n}\n\nexport interface ScheduledTaskFilter {\n tenantId?: string;\n taskType?: TaskType | TaskType[];\n status?: ScheduledTaskStatus | ScheduledTaskStatus[];\n limit?: number;\n offset?: number;\n}\n\nexport interface ScheduledTaskListResult {\n tasks: ScheduledTask[];\n total: number;\n limit: number;\n offset: number;\n}\n\nexport interface TaskExecution {\n id: string;\n taskId: string;\n status: 'running' | 'completed' | 'failed';\n startedAt: Date;\n completedAt?: Date | null;\n devicesProcessed: number;\n devicesSucceeded: number;\n devicesFailed: number;\n error?: string | null;\n details?: Record<string, unknown> | null;\n}\n\n// ============================================\n// Message Queue Types\n// ============================================\n\nexport type QueueMessageStatus = 'pending' | 'processing' | 'delivered' | 'failed' | 'expired';\n\nexport interface QueuedMessage {\n id: string;\n tenantId?: string | null;\n deviceId: string;\n messageType: string;\n payload: Record<string, unknown>;\n priority: 'high' | 'normal' | 'low';\n status: QueueMessageStatus;\n attempts: number;\n maxAttempts: number;\n lastAttemptAt?: Date | null;\n lastError?: string | null;\n expiresAt?: Date | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface EnqueueMessageInput {\n tenantId?: string;\n deviceId: string;\n messageType: string;\n payload: Record<string, unknown>;\n priority?: 'high' | 'normal' | 'low';\n maxAttempts?: number;\n ttlSeconds?: number;\n}\n\nexport interface QueueStats {\n pending: number;\n processing: number;\n delivered: number;\n failed: number;\n expired: number;\n byDevice: Record<string, number>;\n oldestPending?: Date;\n}\n\n// ============================================\n// Dashboard Types\n// ============================================\n\nexport interface DashboardStats {\n devices: {\n total: number;\n enrolled: number;\n active: number;\n blocked: number;\n pending: number;\n };\n policies: {\n total: number;\n deployed: number;\n };\n applications: {\n total: number;\n deployed: number;\n };\n commands: {\n pendingCount: number;\n last24hTotal: number;\n last24hSuccess: number;\n last24hFailed: number;\n };\n groups: {\n total: number;\n withDevices: number;\n };\n}\n\nexport interface DeviceStatusBreakdown {\n byStatus: Record<DeviceStatus, number>;\n byOs: Record<string, number>;\n byManufacturer: Record<string, number>;\n byModel: Record<string, number>;\n}\n\nexport interface EnrollmentTrendPoint {\n date: Date;\n enrolled: number;\n unenrolled: number;\n netChange: number;\n totalDevices: number;\n}\n\nexport interface CommandSuccessRates {\n overall: {\n total: number;\n completed: number;\n failed: number;\n successRate: number;\n };\n byType: Record<string, {\n total: number;\n completed: number;\n failed: number;\n successRate: number;\n avgExecutionTimeMs?: number;\n }>;\n last24h: {\n total: number;\n completed: number;\n failed: number;\n pending: number;\n };\n}\n\nexport interface AppInstallationSummary {\n total: number;\n byStatus: Record<string, number>;\n recentFailures: Array<{\n packageName: string;\n deviceId: string;\n error: string;\n timestamp: Date;\n }>;\n topInstalled: Array<{\n packageName: string;\n name: string;\n installedCount: number;\n }>;\n}\n\n// ============================================\n// Plugin Storage Types\n// ============================================\n\nexport interface PluginStorageAdapter {\n get<T>(pluginName: string, key: string): Promise<T | null>;\n set<T>(pluginName: string, key: string, value: T): Promise<void>;\n delete(pluginName: string, key: string): Promise<void>;\n list(pluginName: string, prefix?: string): Promise<string[]>;\n clear(pluginName: string): Promise<void>;\n}\n\nexport interface PluginStorageEntry {\n pluginName: string;\n key: string;\n value: unknown;\n createdAt: Date;\n updatedAt: Date;\n}\n\n// ============================================\n// Enterprise Manager Interfaces\n// ============================================\n\nexport interface TenantManager {\n get(id: string): Promise<Tenant | null>;\n getBySlug(slug: string): Promise<Tenant | null>;\n list(filter?: TenantFilter): Promise<TenantListResult>;\n create(data: CreateTenantInput): Promise<Tenant>;\n update(id: string, data: UpdateTenantInput): Promise<Tenant>;\n delete(id: string, cascade?: boolean): Promise<void>;\n getStats(tenantId: string): Promise<TenantStats>;\n activate(id: string): Promise<Tenant>;\n deactivate(id: string): Promise<Tenant>;\n}\n\nexport interface AuthorizationManager {\n createRole(data: CreateRoleInput): Promise<Role>;\n getRole(id: string): Promise<Role | null>;\n listRoles(tenantId?: string): Promise<Role[]>;\n updateRole(id: string, data: UpdateRoleInput): Promise<Role>;\n deleteRole(id: string): Promise<void>;\n createUser(data: CreateUserInput): Promise<User>;\n getUser(id: string): Promise<UserWithRoles | null>;\n getUserByEmail(email: string, tenantId?: string): Promise<UserWithRoles | null>;\n listUsers(filter?: UserFilter): Promise<UserListResult>;\n updateUser(id: string, data: UpdateUserInput): Promise<User>;\n deleteUser(id: string): Promise<void>;\n assignRole(userId: string, roleId: string): Promise<void>;\n removeRole(userId: string, roleId: string): Promise<void>;\n getUserRoles(userId: string): Promise<Role[]>;\n can(userId: string, action: PermissionAction, resource: PermissionResource, resourceId?: string): Promise<boolean>;\n canAny(userId: string, permissions: Array<{ action: PermissionAction; resource: PermissionResource }>): Promise<boolean>;\n requirePermission(userId: string, action: PermissionAction, resource: PermissionResource, resourceId?: string): Promise<void>;\n isAdmin(userId: string): Promise<boolean>;\n}\n\nexport interface AuditManager {\n log(entry: CreateAuditLogInput): Promise<AuditLog>;\n list(filter?: AuditLogFilter): Promise<AuditLogListResult>;\n getByResource(resource: string, resourceId: string): Promise<AuditLog[]>;\n getByUser(userId: string, filter?: AuditLogFilter): Promise<AuditLogListResult>;\n export(filter: AuditLogFilter, format: 'json' | 'csv'): Promise<string>;\n purge(olderThanDays?: number): Promise<number>;\n getSummary(tenantId?: string, days?: number): Promise<AuditSummary>;\n}\n\nexport interface ScheduleManager {\n get(id: string): Promise<ScheduledTask | null>;\n list(filter?: ScheduledTaskFilter): Promise<ScheduledTaskListResult>;\n create(data: CreateScheduledTaskInput): Promise<ScheduledTask>;\n update(id: string, data: UpdateScheduledTaskInput): Promise<ScheduledTask>;\n delete(id: string): Promise<void>;\n pause(id: string): Promise<ScheduledTask>;\n resume(id: string): Promise<ScheduledTask>;\n runNow(id: string): Promise<TaskExecution>;\n getUpcoming(hours: number): Promise<ScheduledTask[]>;\n getExecutions(taskId: string, limit?: number): Promise<TaskExecution[]>;\n calculateNextRun(schedule: TaskSchedule): Date | null;\n}\n\nexport interface MessageQueueManager {\n enqueue(message: EnqueueMessageInput): Promise<QueuedMessage>;\n enqueueBatch(messages: EnqueueMessageInput[]): Promise<QueuedMessage[]>;\n dequeue(deviceId: string, limit?: number): Promise<QueuedMessage[]>;\n acknowledge(messageId: string): Promise<void>;\n fail(messageId: string, error: string): Promise<void>;\n retryFailed(maxAttempts?: number): Promise<number>;\n purgeExpired(): Promise<number>;\n getStats(tenantId?: string): Promise<QueueStats>;\n peek(deviceId: string, limit?: number): Promise<QueuedMessage[]>;\n}\n\nexport interface DashboardManager {\n getStats(tenantId?: string): Promise<DashboardStats>;\n getDeviceStatusBreakdown(tenantId?: string): Promise<DeviceStatusBreakdown>;\n getEnrollmentTrend(days: number, tenantId?: string): Promise<EnrollmentTrendPoint[]>;\n getCommandSuccessRates(tenantId?: string): Promise<CommandSuccessRates>;\n getAppInstallationSummary(tenantId?: string): Promise<AppInstallationSummary>;\n}\n\n// ============================================\n// Event Handler Types\n// ============================================\n\nexport type EventHandler<T extends EventType> = (\n event: MDMEvent<EventPayloadMap[T]>\n) => Promise<void> | void;\n\nexport interface EventPayloadMap {\n 'device.enrolled': { device: Device };\n 'device.unenrolled': { device: Device; reason?: string };\n 'device.blocked': { device: Device; reason: string };\n 'device.heartbeat': { device: Device; heartbeat: Heartbeat };\n 'device.locationUpdated': { device: Device; location: DeviceLocation };\n 'device.statusChanged': { device: Device; oldStatus: DeviceStatus; newStatus: DeviceStatus };\n 'device.policyChanged': { device: Device; oldPolicyId?: string; newPolicyId?: string };\n 'app.installed': { device: Device; app: InstalledApp };\n 'app.uninstalled': { device: Device; packageName: string };\n 'app.updated': { device: Device; app: InstalledApp; oldVersion: string };\n 'app.crashed': { device: Device; packageName: string; error?: string };\n 'app.started': { device: Device; packageName: string };\n 'app.stopped': { device: Device; packageName: string };\n 'policy.applied': { device: Device; policy: Policy };\n 'policy.failed': { device: Device; policy: Policy; error: string };\n 'command.received': { device: Device; command: Command };\n 'command.acknowledged': { device: Device; command: Command };\n 'command.completed': { device: Device; command: Command; result: CommandResult };\n 'command.failed': { device: Device; command: Command; error: string };\n 'security.tamper': { device: Device; type: string; details?: unknown };\n 'security.rootDetected': { device: Device };\n 'security.screenLocked': { device: Device };\n 'security.screenUnlocked': { device: Device };\n custom: Record<string, unknown>;\n}\n\n// ============================================\n// Error Types\n// ============================================\n\nexport class MDMError extends Error {\n constructor(\n message: string,\n public code: string,\n public statusCode: number = 500,\n public details?: unknown\n ) {\n super(message);\n this.name = 'MDMError';\n }\n}\n\nexport class DeviceNotFoundError extends MDMError {\n constructor(deviceId: string) {\n super(`Device not found: ${deviceId}`, 'DEVICE_NOT_FOUND', 404);\n }\n}\n\nexport class PolicyNotFoundError extends MDMError {\n constructor(policyId: string) {\n super(`Policy not found: ${policyId}`, 'POLICY_NOT_FOUND', 404);\n }\n}\n\nexport class ApplicationNotFoundError extends MDMError {\n constructor(identifier: string) {\n super(`Application not found: ${identifier}`, 'APPLICATION_NOT_FOUND', 404);\n }\n}\n\nexport class CommandNotFoundError extends MDMError {\n constructor(commandId: string) {\n super(`Command not found: ${commandId}`, 'COMMAND_NOT_FOUND', 404);\n }\n}\n\nexport class TenantNotFoundError extends MDMError {\n constructor(identifier: string) {\n super(`Tenant not found: ${identifier}`, 'TENANT_NOT_FOUND', 404);\n }\n}\n\nexport class RoleNotFoundError extends MDMError {\n constructor(identifier: string) {\n super(`Role not found: ${identifier}`, 'ROLE_NOT_FOUND', 404);\n }\n}\n\nexport class GroupNotFoundError extends MDMError {\n constructor(identifier: string) {\n super(`Group not found: ${identifier}`, 'GROUP_NOT_FOUND', 404);\n }\n}\n\nexport class UserNotFoundError extends MDMError {\n constructor(identifier: string) {\n super(`User not found: ${identifier}`, 'USER_NOT_FOUND', 404);\n }\n}\n\nexport class EnrollmentError extends MDMError {\n constructor(message: string, details?: unknown) {\n super(message, 'ENROLLMENT_ERROR', 400, details);\n }\n}\n\nexport class AuthenticationError extends MDMError {\n constructor(message: string = 'Authentication required') {\n super(message, 'AUTHENTICATION_ERROR', 401);\n }\n}\n\nexport class AuthorizationError extends MDMError {\n constructor(message: string = 'Access denied') {\n super(message, 'AUTHORIZATION_ERROR', 403);\n }\n}\n\nexport class ValidationError extends MDMError {\n constructor(message: string, details?: unknown) {\n super(message, 'VALIDATION_ERROR', 400, details);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/types.ts"],"names":[],"mappings":";AA09DO,IAAM,QAAA,GAAN,cAAuB,KAAA,CAAM;AAAA,EAClC,WAAA,CACE,OAAA,EACO,IAAA,EACA,UAAA,GAAqB,KACrB,OAAA,EACP;AACA,IAAA,KAAA,CAAM,OAAO,CAAA;AAJN,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AACA,IAAA,IAAA,CAAA,UAAA,GAAA,UAAA;AACA,IAAA,IAAA,CAAA,OAAA,GAAA,OAAA;AAGP,IAAA,IAAA,CAAK,IAAA,GAAO,UAAA;AAAA,EACd;AACF;AAEO,IAAM,mBAAA,GAAN,cAAkC,QAAA,CAAS;AAAA,EAChD,YAAY,QAAA,EAAkB;AAC5B,IAAA,KAAA,CAAM,CAAA,kBAAA,EAAqB,QAAQ,CAAA,CAAA,EAAI,kBAAA,EAAoB,GAAG,CAAA;AAAA,EAChE;AACF;AAEO,IAAM,mBAAA,GAAN,cAAkC,QAAA,CAAS;AAAA,EAChD,YAAY,QAAA,EAAkB;AAC5B,IAAA,KAAA,CAAM,CAAA,kBAAA,EAAqB,QAAQ,CAAA,CAAA,EAAI,kBAAA,EAAoB,GAAG,CAAA;AAAA,EAChE;AACF;AAEO,IAAM,wBAAA,GAAN,cAAuC,QAAA,CAAS;AAAA,EACrD,YAAY,UAAA,EAAoB;AAC9B,IAAA,KAAA,CAAM,CAAA,uBAAA,EAA0B,UAAU,CAAA,CAAA,EAAI,uBAAA,EAAyB,GAAG,CAAA;AAAA,EAC5E;AACF;AAEO,IAAM,oBAAA,GAAN,cAAmC,QAAA,CAAS;AAAA,EACjD,YAAY,SAAA,EAAmB;AAC7B,IAAA,KAAA,CAAM,CAAA,mBAAA,EAAsB,SAAS,CAAA,CAAA,EAAI,mBAAA,EAAqB,GAAG,CAAA;AAAA,EACnE;AACF;AAEO,IAAM,mBAAA,GAAN,cAAkC,QAAA,CAAS;AAAA,EAChD,YAAY,UAAA,EAAoB;AAC9B,IAAA,KAAA,CAAM,CAAA,kBAAA,EAAqB,UAAU,CAAA,CAAA,EAAI,kBAAA,EAAoB,GAAG,CAAA;AAAA,EAClE;AACF;AAEO,IAAM,iBAAA,GAAN,cAAgC,QAAA,CAAS;AAAA,EAC9C,YAAY,UAAA,EAAoB;AAC9B,IAAA,KAAA,CAAM,CAAA,gBAAA,EAAmB,UAAU,CAAA,CAAA,EAAI,gBAAA,EAAkB,GAAG,CAAA;AAAA,EAC9D;AACF;AAEO,IAAM,kBAAA,GAAN,cAAiC,QAAA,CAAS;AAAA,EAC/C,YAAY,UAAA,EAAoB;AAC9B,IAAA,KAAA,CAAM,CAAA,iBAAA,EAAoB,UAAU,CAAA,CAAA,EAAI,iBAAA,EAAmB,GAAG,CAAA;AAAA,EAChE;AACF;AAEO,IAAM,iBAAA,GAAN,cAAgC,QAAA,CAAS;AAAA,EAC9C,YAAY,UAAA,EAAoB;AAC9B,IAAA,KAAA,CAAM,CAAA,gBAAA,EAAmB,UAAU,CAAA,CAAA,EAAI,gBAAA,EAAkB,GAAG,CAAA;AAAA,EAC9D;AACF;AAEO,IAAM,eAAA,GAAN,cAA8B,QAAA,CAAS;AAAA,EAC5C,WAAA,CAAY,SAAiB,OAAA,EAAmB;AAC9C,IAAA,KAAA,CAAM,OAAA,EAAS,kBAAA,EAAoB,GAAA,EAAK,OAAO,CAAA;AAAA,EACjD;AACF;AAEO,IAAM,mBAAA,GAAN,cAAkC,QAAA,CAAS;AAAA,EAChD,WAAA,CAAY,UAAkB,yBAAA,EAA2B;AACvD,IAAA,KAAA,CAAM,OAAA,EAAS,wBAAwB,GAAG,CAAA;AAAA,EAC5C;AACF;AAEO,IAAM,kBAAA,GAAN,cAAiC,QAAA,CAAS;AAAA,EAC/C,WAAA,CAAY,UAAkB,eAAA,EAAiB;AAC7C,IAAA,KAAA,CAAM,OAAA,EAAS,uBAAuB,GAAG,CAAA;AAAA,EAC3C;AACF;AAEO,IAAM,eAAA,GAAN,cAA8B,QAAA,CAAS;AAAA,EAC5C,WAAA,CAAY,SAAiB,OAAA,EAAmB;AAC9C,IAAA,KAAA,CAAM,OAAA,EAAS,kBAAA,EAAoB,GAAA,EAAK,OAAO,CAAA;AAAA,EACjD;AACF","file":"types.js","sourcesContent":["/**\n * OpenMDM Core Types\n *\n * These types define the core data structures for the MDM system.\n * Designed to be database-agnostic and framework-agnostic.\n */\n\n// ============================================\n// Device Types\n// ============================================\n\nexport type DeviceStatus = 'pending' | 'enrolled' | 'unenrolled' | 'blocked';\n\nexport interface Device {\n id: string;\n externalId?: string | null;\n enrollmentId: string;\n status: DeviceStatus;\n\n // Device Info\n model?: string | null;\n manufacturer?: string | null;\n osVersion?: string | null;\n serialNumber?: string | null;\n imei?: string | null;\n macAddress?: string | null;\n androidId?: string | null;\n\n // MDM State\n policyId?: string | null;\n agentVersion?: string | null; // MDM agent version installed on device\n lastHeartbeat?: Date | null;\n lastSync?: Date | null;\n\n // Device identity (Phase 2b — device-pinned ECDSA P-256 key)\n /**\n * Base64-encoded SPKI public key the device registered on first\n * enrollment. Requests from this device can be verified against\n * this key via `verifyDeviceRequest` / `verifyEcdsaSignature` from\n * `@openmdm/core`. `null` on devices that enrolled via the legacy\n * HMAC path and have never been migrated.\n */\n publicKey?: string | null;\n /**\n * How the device originally enrolled. `'hmac'` for the legacy\n * shared-secret path; `'pinned-key'` for the device-pinned ECDSA\n * path. `null` on pre-Phase-2b device rows that predate the\n * column (treated as `'hmac'`).\n */\n enrollmentMethod?: 'hmac' | 'pinned-key' | null;\n\n // Telemetry\n batteryLevel?: number | null;\n storageUsed?: number | null;\n storageTotal?: number | null;\n location?: DeviceLocation | null;\n installedApps?: InstalledApp[] | null;\n\n // Metadata\n tags?: Record<string, string> | null;\n metadata?: Record<string, unknown> | null;\n\n // Timestamps\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface DeviceLocation {\n latitude: number;\n longitude: number;\n accuracy?: number;\n timestamp: Date;\n}\n\nexport interface InstalledApp {\n packageName: string;\n version: string;\n versionCode?: number;\n installedAt?: Date;\n}\n\nexport interface CreateDeviceInput {\n enrollmentId: string;\n externalId?: string;\n model?: string;\n manufacturer?: string;\n osVersion?: string;\n serialNumber?: string;\n imei?: string;\n macAddress?: string;\n androidId?: string;\n policyId?: string;\n tags?: Record<string, string>;\n metadata?: Record<string, unknown>;\n}\n\nexport interface UpdateDeviceInput {\n externalId?: string | null;\n status?: DeviceStatus;\n policyId?: string | null;\n agentVersion?: string | null;\n model?: string;\n manufacturer?: string;\n osVersion?: string;\n batteryLevel?: number | null;\n storageUsed?: number | null;\n storageTotal?: number | null;\n lastHeartbeat?: Date;\n lastSync?: Date;\n installedApps?: InstalledApp[];\n location?: DeviceLocation;\n tags?: Record<string, string>;\n metadata?: Record<string, unknown>;\n /** Phase 2b — pin a new public key on first enroll. */\n publicKey?: string | null;\n /** Phase 2b — record which auth path the device enrolled via. */\n enrollmentMethod?: 'hmac' | 'pinned-key' | null;\n}\n\nexport interface DeviceFilter {\n status?: DeviceStatus | DeviceStatus[];\n policyId?: string;\n groupId?: string;\n search?: string;\n tags?: Record<string, string>;\n limit?: number;\n offset?: number;\n}\n\nexport interface DeviceListResult {\n devices: Device[];\n total: number;\n limit: number;\n offset: number;\n}\n\n// ============================================\n// Policy Types\n// ============================================\n\nexport interface Policy {\n id: string;\n name: string;\n description?: string | null;\n isDefault: boolean;\n settings: PolicySettings;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface PolicySettings {\n // Kiosk Mode\n kioskMode?: boolean;\n mainApp?: string;\n allowedApps?: string[];\n kioskExitPassword?: string;\n\n // Lock Features\n lockStatusBar?: boolean;\n lockNavigationBar?: boolean;\n lockSettings?: boolean;\n lockPowerButton?: boolean;\n blockInstall?: boolean;\n blockUninstall?: boolean;\n\n // Hardware Controls\n bluetooth?: HardwareControl;\n wifi?: HardwareControl;\n gps?: HardwareControl;\n mobileData?: HardwareControl;\n camera?: HardwareControl;\n microphone?: HardwareControl;\n usb?: HardwareControl;\n nfc?: HardwareControl;\n\n // Update Settings\n systemUpdatePolicy?: SystemUpdatePolicy;\n updateWindow?: TimeWindow;\n\n // Security\n passwordPolicy?: PasswordPolicy;\n encryptionRequired?: boolean;\n factoryResetProtection?: boolean;\n safeBootDisabled?: boolean;\n\n // Telemetry\n heartbeatInterval?: number;\n locationReportInterval?: number;\n locationEnabled?: boolean;\n\n // Network\n wifiConfigs?: WifiConfig[];\n vpnConfig?: VpnConfig;\n\n // Applications\n applications?: PolicyApplication[];\n\n // Custom settings (for plugins)\n custom?: Record<string, unknown>;\n}\n\nexport type HardwareControl = 'on' | 'off' | 'user';\nexport type SystemUpdatePolicy = 'auto' | 'windowed' | 'postpone' | 'manual';\n\nexport interface TimeWindow {\n start: string; // \"HH:MM\"\n end: string; // \"HH:MM\"\n}\n\nexport interface PasswordPolicy {\n required: boolean;\n minLength?: number;\n complexity?: 'none' | 'numeric' | 'alphanumeric' | 'complex';\n maxFailedAttempts?: number;\n expirationDays?: number;\n historyLength?: number;\n}\n\nexport interface WifiConfig {\n ssid: string;\n securityType: 'none' | 'wep' | 'wpa' | 'wpa2' | 'wpa3';\n password?: string;\n hidden?: boolean;\n autoConnect?: boolean;\n}\n\nexport interface VpnConfig {\n type: 'pptp' | 'l2tp' | 'ipsec' | 'openvpn' | 'wireguard';\n server: string;\n username?: string;\n password?: string;\n certificate?: string;\n config?: Record<string, unknown>;\n}\n\nexport interface PolicyApplication {\n packageName: string;\n action: 'install' | 'update' | 'uninstall';\n version?: string;\n required?: boolean;\n autoUpdate?: boolean;\n}\n\nexport interface CreatePolicyInput {\n name: string;\n description?: string;\n isDefault?: boolean;\n settings: PolicySettings;\n}\n\nexport interface UpdatePolicyInput {\n name?: string;\n description?: string | null;\n isDefault?: boolean;\n settings?: PolicySettings;\n}\n\n// ============================================\n// Application Types\n// ============================================\n\nexport interface Application {\n id: string;\n name: string;\n packageName: string;\n version: string;\n versionCode: number;\n url: string;\n hash?: string | null;\n size?: number | null;\n minSdkVersion?: number | null;\n\n // Deployment settings\n showIcon: boolean;\n runAfterInstall: boolean;\n runAtBoot: boolean;\n isSystem: boolean;\n\n // State\n isActive: boolean;\n\n // Metadata\n metadata?: Record<string, unknown> | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface CreateApplicationInput {\n name: string;\n packageName: string;\n version: string;\n versionCode: number;\n url: string;\n hash?: string;\n size?: number;\n minSdkVersion?: number;\n showIcon?: boolean;\n runAfterInstall?: boolean;\n runAtBoot?: boolean;\n isSystem?: boolean;\n metadata?: Record<string, unknown>;\n}\n\nexport interface UpdateApplicationInput {\n name?: string;\n version?: string;\n versionCode?: number;\n url?: string;\n hash?: string | null;\n size?: number | null;\n minSdkVersion?: number | null;\n showIcon?: boolean;\n runAfterInstall?: boolean;\n runAtBoot?: boolean;\n isActive?: boolean;\n metadata?: Record<string, unknown> | null;\n}\n\nexport interface DeployTarget {\n devices?: string[];\n policies?: string[];\n groups?: string[];\n}\n\n// ============================================\n// App Version & Rollback Types\n// ============================================\n\nexport interface AppVersion {\n id: string;\n applicationId: string;\n packageName: string;\n version: string;\n versionCode: number;\n url: string;\n hash?: string | null;\n size?: number | null;\n releaseNotes?: string | null;\n isMinimumVersion: boolean;\n createdAt: Date;\n}\n\nexport interface AppRollback {\n id: string;\n deviceId: string;\n packageName: string;\n fromVersion: string;\n fromVersionCode: number;\n toVersion: string;\n toVersionCode: number;\n reason?: string | null;\n status: 'pending' | 'in_progress' | 'completed' | 'failed';\n error?: string | null;\n initiatedBy?: string | null;\n createdAt: Date;\n completedAt?: Date | null;\n}\n\nexport interface CreateAppRollbackInput {\n deviceId: string;\n packageName: string;\n toVersionCode: number;\n reason?: string;\n initiatedBy?: string;\n}\n\n// ============================================\n// Command Types\n// ============================================\n\nexport type CommandType =\n | 'reboot'\n | 'shutdown'\n | 'sync'\n | 'lock'\n | 'unlock'\n | 'wipe'\n | 'factoryReset'\n | 'installApp'\n | 'uninstallApp'\n | 'updateApp'\n | 'runApp'\n | 'clearAppData'\n | 'clearAppCache'\n | 'shell'\n | 'setPolicy'\n | 'grantPermissions'\n | 'exitKiosk'\n | 'enterKiosk'\n | 'setWifi'\n | 'screenshot'\n | 'getLocation'\n | 'setVolume'\n | 'sendNotification'\n | 'whitelistBattery' // Whitelist app from battery optimization (Doze)\n | 'enablePermissiveMode' // Enable permissive mode for debugging\n | 'setTimeZone' // Set device timezone\n | 'enableAdb' // Enable/disable ADB debugging\n | 'rollbackApp' // Rollback to previous app version\n | 'updateAgent' // Update MDM agent to a new version\n | 'custom';\n\nexport type CommandStatus =\n | 'pending'\n | 'sent'\n | 'acknowledged'\n | 'completed'\n | 'failed'\n | 'cancelled';\n\nexport interface Command {\n id: string;\n deviceId: string;\n type: CommandType;\n payload?: Record<string, unknown> | null;\n status: CommandStatus;\n result?: CommandResult | null;\n error?: string | null;\n createdAt: Date;\n sentAt?: Date | null;\n acknowledgedAt?: Date | null;\n completedAt?: Date | null;\n}\n\nexport interface CommandResult {\n success: boolean;\n message?: string;\n data?: unknown;\n}\n\nexport interface SendCommandInput {\n deviceId: string;\n type: CommandType;\n payload?: Record<string, unknown>;\n}\n\nexport interface CommandFilter {\n deviceId?: string;\n status?: CommandStatus | CommandStatus[];\n type?: CommandType | CommandType[];\n limit?: number;\n offset?: number;\n}\n\n// ============================================\n// Event Types\n// ============================================\n\nexport type EventType =\n | 'device.enrolled'\n | 'device.unenrolled'\n | 'device.blocked'\n | 'device.heartbeat'\n | 'device.locationUpdated'\n | 'device.statusChanged'\n | 'device.policyChanged'\n | 'app.installed'\n | 'app.uninstalled'\n | 'app.updated'\n | 'app.crashed'\n | 'app.started'\n | 'app.stopped'\n | 'policy.applied'\n | 'policy.failed'\n | 'command.received'\n | 'command.acknowledged'\n | 'command.completed'\n | 'command.failed'\n | 'security.tamper'\n | 'security.rootDetected'\n | 'security.screenLocked'\n | 'security.screenUnlocked'\n | 'custom';\n\nexport interface MDMEvent<T = unknown> {\n id: string;\n deviceId: string;\n type: EventType;\n payload: T;\n createdAt: Date;\n}\n\nexport interface EventFilter {\n deviceId?: string;\n type?: EventType | EventType[];\n startDate?: Date;\n endDate?: Date;\n limit?: number;\n offset?: number;\n}\n\n// ============================================\n// Group Types\n// ============================================\n\nexport interface Group {\n id: string;\n name: string;\n description?: string | null;\n policyId?: string | null;\n parentId?: string | null;\n metadata?: Record<string, unknown> | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface CreateGroupInput {\n name: string;\n description?: string;\n policyId?: string;\n parentId?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface UpdateGroupInput {\n name?: string;\n description?: string | null;\n policyId?: string | null;\n parentId?: string | null;\n metadata?: Record<string, unknown> | null;\n}\n\n// ============================================\n// Enrollment Types\n// ============================================\n\nexport type EnrollmentMethod =\n | 'qr'\n | 'nfc'\n | 'zero-touch'\n | 'knox'\n | 'manual'\n | 'app-only'\n | 'adb';\n\nexport interface EnrollmentRequest {\n // Device identifiers (at least one required)\n macAddress?: string;\n serialNumber?: string;\n imei?: string;\n androidId?: string;\n\n // Device info\n model: string;\n manufacturer: string;\n osVersion: string;\n sdkVersion?: number;\n\n // Agent info\n agentVersion?: string;\n agentPackage?: string;\n\n // Enrollment details\n method: EnrollmentMethod;\n timestamp: string;\n\n /**\n * Signature over the canonical enrollment message.\n *\n * Phase 2a (HMAC path, backwards-compatible): hex-encoded\n * HMAC-SHA256 of the nine-field pipe-delimited canonical form\n * (see `concepts/enrollment`).\n *\n * Phase 2b (device-pinned-key path, preferred): base64-encoded\n * DER ECDSA-P256 signature produced by the device's Keystore\n * private key, over `canonicalEnrollmentMessage(...)` including\n * the public key and challenge. The server distinguishes the\n * two paths by whether `publicKey` is present on the request.\n */\n signature: string;\n\n /**\n * Base64-encoded SPKI public key (EC P-256) the device generated\n * in its Keystore. When present, enrollment follows the Phase 2b\n * device-pinned-key path and `signature` must verify as an ECDSA\n * signature against this key. The server pins this key on the\n * device row on first successful enroll; any future enroll\n * attempting a different key for the same `enrollmentId` is\n * rejected with `PublicKeyMismatchError`.\n *\n * Omit for the legacy HMAC path. Callers that want to migrate a\n * fleet gradually can run both paths in parallel.\n */\n publicKey?: string;\n\n /**\n * Opaque challenge issued by `GET /agent/enroll/challenge`. Must\n * be present whenever `publicKey` is present — the server uses\n * it to prevent replay of captured enrollment payloads. The\n * challenge is single-use: the server consumes it on first\n * successful verify.\n */\n attestationChallenge?: string;\n\n // Optional pre-assigned policy/group\n policyId?: string;\n groupId?: string;\n}\n\nexport interface EnrollmentResponse {\n deviceId: string;\n enrollmentId: string;\n policyId?: string;\n policy?: Policy;\n serverUrl: string;\n pushConfig: PushConfig;\n token: string;\n refreshToken?: string;\n tokenExpiresAt?: Date;\n}\n\n// ============================================\n// Device Identity (Phase 2b)\n// ============================================\n\n/**\n * Single-use nonce issued by `GET /agent/enroll/challenge` and\n * consumed on first successful verify of a device-pinned-key\n * enrollment. A persisted record; the `consume*` adapter methods\n * enforce the single-use invariant.\n */\nexport interface EnrollmentChallenge {\n challenge: string;\n expiresAt: Date;\n consumedAt?: Date | null;\n createdAt: Date;\n}\n\n/**\n * Result of calling `verifyDeviceRequest`. Callers pattern-match on\n * `ok` and, when `false`, on `reason` to decide their response\n * shape:\n *\n * - `not-found` — unknown device id. Return 401.\n * - `no-pinned-key` — device exists but never migrated off the\n * legacy HMAC path. Caller should fall back\n * to their HMAC verifier, or fail if\n * they've completed their migration.\n * - `signature-invalid` — signature did not verify against the\n * pinned key. Return 401. Never re-pin\n * in response to this.\n */\nexport type DeviceIdentityVerification =\n | { ok: true; device: Device }\n | { ok: false; reason: 'not-found' }\n | { ok: false; reason: 'no-pinned-key'; device: Device }\n | { ok: false; reason: 'signature-invalid'; device: Device };\n\nexport interface PushConfig {\n provider: 'fcm' | 'mqtt' | 'websocket' | 'polling';\n fcmSenderId?: string;\n mqttUrl?: string;\n mqttTopic?: string;\n mqttUsername?: string;\n mqttPassword?: string;\n wsUrl?: string;\n pollingInterval?: number;\n}\n\n// ============================================\n// Telemetry Types\n// ============================================\n\nexport interface Heartbeat {\n deviceId: string;\n timestamp: Date;\n\n // Battery\n batteryLevel: number;\n isCharging: boolean;\n batteryHealth?: 'good' | 'overheat' | 'dead' | 'cold' | 'unknown';\n\n // Storage\n storageUsed: number;\n storageTotal: number;\n\n // Memory\n memoryUsed: number;\n memoryTotal: number;\n\n // Network\n networkType?: 'wifi' | 'cellular' | 'ethernet' | 'none';\n networkName?: string; // SSID or carrier\n signalStrength?: number;\n ipAddress?: string;\n\n // Location\n location?: DeviceLocation;\n\n // Apps\n installedApps: InstalledApp[];\n runningApps?: string[];\n\n // Security\n isRooted?: boolean;\n isEncrypted?: boolean;\n screenLockEnabled?: boolean;\n\n // Agent status\n agentVersion?: string;\n policyVersion?: string;\n lastPolicySync?: Date;\n}\n\n// ============================================\n// Push Token Types\n// ============================================\n\nexport interface PushToken {\n id: string;\n deviceId: string;\n provider: 'fcm' | 'mqtt' | 'websocket';\n token: string;\n isActive: boolean;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface RegisterPushTokenInput {\n deviceId: string;\n provider: 'fcm' | 'mqtt' | 'websocket';\n token: string;\n}\n\n// ============================================\n// Configuration Types\n// ============================================\n\nexport interface MDMConfig {\n /** Database adapter for persistence */\n database: DatabaseAdapter;\n\n /** Authentication/authorization configuration */\n auth?: AuthConfig;\n\n /** Push notification provider configuration */\n push?: PushProviderConfig;\n\n /** Device enrollment configuration */\n enrollment?: EnrollmentConfig;\n\n /** Server URL (used in enrollment responses) */\n serverUrl?: string;\n\n /** APK/file storage configuration */\n storage?: StorageConfig;\n\n /** Outbound webhook configuration */\n webhooks?: WebhookConfig;\n\n /** Plugins to extend functionality */\n plugins?: MDMPlugin[];\n\n /** Event handlers */\n onDeviceEnrolled?: (device: Device) => Promise<void>;\n onDeviceUnenrolled?: (device: Device) => Promise<void>;\n onDeviceBlocked?: (device: Device) => Promise<void>;\n onHeartbeat?: (device: Device, heartbeat: Heartbeat) => Promise<void>;\n onCommand?: (command: Command) => Promise<void>;\n onEvent?: (event: MDMEvent) => Promise<void>;\n\n // Enterprise features\n /** Multi-tenancy configuration */\n multiTenancy?: {\n enabled: boolean;\n defaultTenantId?: string;\n tenantResolver?: (context: unknown) => Promise<string | null>;\n };\n\n /** Authorization (RBAC) configuration */\n authorization?: {\n enabled: boolean;\n defaultRole?: string;\n };\n\n /** Audit logging configuration */\n audit?: {\n enabled: boolean;\n retentionDays?: number;\n };\n\n /** Scheduling configuration */\n scheduling?: {\n enabled: boolean;\n timezone?: string;\n };\n\n /** Plugin storage configuration */\n pluginStorage?: {\n adapter: 'database' | 'memory';\n };\n\n /**\n * Structured logger. Replaces OpenMDM's internal `console.*` calls\n * so log output lands in the host application's logging pipeline\n * (pino, winston, bunyan, OTEL collector, etc.) instead of raw\n * stderr.\n *\n * The shape is a strict subset of the pino / winston / bunyan\n * interface — any of those can be passed directly. If omitted, a\n * default logger that writes to the console with an `[openmdm]`\n * prefix is used. To silence OpenMDM entirely, pass a no-op\n * implementation (see `createSilentLogger()` in the package\n * exports).\n */\n logger?: Logger;\n}\n\n/**\n * Minimal structured-logger interface OpenMDM calls internally.\n *\n * The shape is deliberately the pino-compatible subset: an optional\n * context object as the first argument followed by a message string.\n * pino, winston, bunyan, and most other structured loggers accept\n * this shape natively.\n *\n * Implementations should be side-effect-free on unconfigured levels\n * (a production logger filtered to `info` should still accept\n * `.debug()` calls cheaply).\n */\nexport interface Logger {\n /** Human-ignorable, high-volume tracing. Off in production by default. */\n debug(context: LogContext, message: string): void;\n debug(message: string): void;\n\n /** Normal operational events. Enrollment, policy changes, command delivery. */\n info(context: LogContext, message: string): void;\n info(message: string): void;\n\n /** Something is wrong but the server is still running. Retries, fallbacks, degraded modes. */\n warn(context: LogContext, message: string): void;\n warn(message: string): void;\n\n /** Something failed and a request/operation did not complete. */\n error(context: LogContext, message: string): void;\n error(message: string): void;\n\n /**\n * Return a new logger with the given fields attached to every\n * subsequent call. Used by managers and plugins to scope logs to\n * a specific subsystem without repeating context.\n */\n child(bindings: LogContext): Logger;\n}\n\n/**\n * Arbitrary structured context attached to a log line. Values must\n * be JSON-serializable so host loggers can ship them to any backend.\n */\nexport type LogContext = Record<string, unknown>;\n\nexport interface StorageConfig {\n /** Storage provider (s3, local, custom) */\n provider: 's3' | 'local' | 'custom';\n\n /** S3 configuration */\n s3?: {\n bucket: string;\n region: string;\n accessKeyId?: string;\n secretAccessKey?: string;\n endpoint?: string; // For S3-compatible services\n presignedUrlExpiry?: number; // Seconds, default 3600\n };\n\n /** Local storage path */\n localPath?: string;\n\n /** Custom storage adapter */\n customAdapter?: {\n upload: (file: Buffer, key: string) => Promise<string>;\n getUrl: (key: string) => Promise<string>;\n delete: (key: string) => Promise<void>;\n };\n}\n\nexport interface WebhookConfig {\n /** Webhook endpoints to notify */\n endpoints?: WebhookEndpoint[];\n\n /** Retry configuration */\n retry?: {\n maxRetries?: number;\n initialDelay?: number;\n maxDelay?: number;\n };\n\n /** Sign webhooks with HMAC secret */\n signingSecret?: string;\n}\n\nexport interface WebhookEndpoint {\n /** Unique identifier */\n id: string;\n /** Webhook URL */\n url: string;\n /** Events to trigger this webhook */\n events: (EventType | '*')[];\n /** Custom headers */\n headers?: Record<string, string>;\n /** Whether endpoint is active */\n enabled: boolean;\n}\n\nexport interface AuthConfig {\n /** Get current user from request context */\n getUser: <T = unknown>(context: unknown) => Promise<T | null>;\n /** Check if user has admin privileges */\n isAdmin?: (user: unknown) => Promise<boolean>;\n /** Check if user can access specific device */\n canAccessDevice?: (user: unknown, deviceId: string) => Promise<boolean>;\n /** Device JWT secret (for device auth tokens) */\n deviceTokenSecret?: string;\n /** Device token expiration in seconds (default: 365 days) */\n deviceTokenExpiration?: number;\n}\n\nexport interface PushProviderConfig {\n provider: 'fcm' | 'mqtt' | 'websocket' | 'polling';\n\n // FCM configuration\n fcmCredentials?: string | Record<string, unknown>;\n fcmProjectId?: string;\n\n // MQTT configuration\n mqttUrl?: string;\n mqttUsername?: string;\n mqttPassword?: string;\n mqttTopicPrefix?: string;\n\n // WebSocket configuration\n wsPath?: string;\n wsPingInterval?: number;\n\n // Polling fallback\n pollingInterval?: number;\n}\n\nexport interface EnrollmentConfig {\n /** Auto-enroll devices with valid signature */\n autoEnroll?: boolean;\n /** HMAC secret for device signature verification (Phase 2a path) */\n deviceSecret: string;\n /** Allowed enrollment methods */\n allowedMethods?: EnrollmentMethod[];\n /** Default policy for new devices */\n defaultPolicyId?: string;\n /** Default group for new devices */\n defaultGroupId?: string;\n /** Require manual approval for enrollment */\n requireApproval?: boolean;\n /** Custom enrollment validation */\n validate?: (request: EnrollmentRequest) => Promise<boolean>;\n\n /**\n * Phase 2b device-pinned-key configuration. Optional — when\n * omitted, enrollment continues to accept the Phase 2a HMAC path\n * exclusively, matching pre-0.9 behaviour.\n */\n pinnedKey?: PinnedKeyConfig;\n}\n\n/**\n * Device-pinned-key enrollment options. See\n * `docs/concepts/enrollment` for the full flow.\n */\nexport interface PinnedKeyConfig {\n /**\n * Require every new enrollment to use the pinned-key path. When\n * `true`, requests without `publicKey` are rejected. When `false`\n * (the default), both paths coexist during rollout — the server\n * pins a public key when one is provided, falls back to HMAC when\n * it isn't.\n */\n required?: boolean;\n\n /**\n * TTL for enrollment challenges, in seconds. Defaults to 300\n * (5 minutes). Challenges are single-use; this only bounds how\n * long an unused challenge stays valid.\n */\n challengeTtlSeconds?: number;\n}\n\n// ============================================\n// Adapter Interfaces\n// ============================================\n\nexport interface DatabaseAdapter {\n // Devices\n findDevice(id: string): Promise<Device | null>;\n findDeviceByEnrollmentId(enrollmentId: string): Promise<Device | null>;\n listDevices(filter?: DeviceFilter): Promise<DeviceListResult>;\n createDevice(data: CreateDeviceInput): Promise<Device>;\n updateDevice(id: string, data: UpdateDeviceInput): Promise<Device>;\n deleteDevice(id: string): Promise<void>;\n countDevices(filter?: DeviceFilter): Promise<number>;\n\n // Policies\n findPolicy(id: string): Promise<Policy | null>;\n findDefaultPolicy(): Promise<Policy | null>;\n listPolicies(): Promise<Policy[]>;\n createPolicy(data: CreatePolicyInput): Promise<Policy>;\n updatePolicy(id: string, data: UpdatePolicyInput): Promise<Policy>;\n deletePolicy(id: string): Promise<void>;\n\n // Applications\n findApplication(id: string): Promise<Application | null>;\n findApplicationByPackage(packageName: string, version?: string): Promise<Application | null>;\n listApplications(activeOnly?: boolean): Promise<Application[]>;\n createApplication(data: CreateApplicationInput): Promise<Application>;\n updateApplication(id: string, data: UpdateApplicationInput): Promise<Application>;\n deleteApplication(id: string): Promise<void>;\n\n // Commands\n findCommand(id: string): Promise<Command | null>;\n listCommands(filter?: CommandFilter): Promise<Command[]>;\n createCommand(data: SendCommandInput): Promise<Command>;\n updateCommand(id: string, data: Partial<Command>): Promise<Command | null>;\n getPendingCommands(deviceId: string): Promise<Command[]>;\n\n // Events\n createEvent(event: Omit<MDMEvent, 'id' | 'createdAt'>): Promise<MDMEvent>;\n listEvents(filter?: EventFilter): Promise<MDMEvent[]>;\n\n // Groups\n findGroup(id: string): Promise<Group | null>;\n listGroups(): Promise<Group[]>;\n createGroup(data: CreateGroupInput): Promise<Group>;\n updateGroup(id: string, data: UpdateGroupInput): Promise<Group>;\n deleteGroup(id: string): Promise<void>;\n listDevicesInGroup(groupId: string): Promise<Device[]>;\n addDeviceToGroup(deviceId: string, groupId: string): Promise<void>;\n removeDeviceFromGroup(deviceId: string, groupId: string): Promise<void>;\n getDeviceGroups(deviceId: string): Promise<Group[]>;\n\n // Push Tokens\n findPushToken(deviceId: string, provider: string): Promise<PushToken | null>;\n upsertPushToken(data: RegisterPushTokenInput): Promise<PushToken>;\n deletePushToken(deviceId: string, provider?: string): Promise<void>;\n\n // App Versions (optional - for version tracking)\n listAppVersions?(packageName: string): Promise<AppVersion[]>;\n createAppVersion?(data: Omit<AppVersion, 'id' | 'createdAt'>): Promise<AppVersion>;\n setMinimumVersion?(packageName: string, versionCode: number): Promise<void>;\n getMinimumVersion?(packageName: string): Promise<AppVersion | null>;\n\n // Rollback History (optional)\n createRollback?(data: CreateAppRollbackInput): Promise<AppRollback>;\n updateRollback?(id: string, data: Partial<AppRollback>): Promise<AppRollback>;\n listRollbacks?(filter?: { deviceId?: string; packageName?: string }): Promise<AppRollback[]>;\n\n // Group Hierarchy (optional)\n getGroupChildren?(parentId: string | null): Promise<Group[]>;\n getGroupAncestors?(groupId: string): Promise<Group[]>;\n getGroupDescendants?(groupId: string): Promise<Group[]>;\n getGroupTree?(rootId?: string): Promise<GroupTreeNode[]>;\n getGroupEffectivePolicy?(groupId: string): Promise<Policy | null>;\n moveGroup?(groupId: string, newParentId: string | null): Promise<Group>;\n getGroupHierarchyStats?(): Promise<GroupHierarchyStats>;\n\n // Enrollment challenges (optional - for Phase 2b device-pinned-key)\n /**\n * Persist a new single-use enrollment challenge. The adapter\n * should store it with `consumed_at = null` and enforce a\n * primary-key constraint on `challenge` so duplicate inserts\n * fail loudly.\n */\n createEnrollmentChallenge?(challenge: EnrollmentChallenge): Promise<void>;\n /**\n * Look up a challenge by its opaque value. Returns `null` if not\n * found. Does NOT filter on expiry — the core layer checks\n * freshness so the adapter stays dumb.\n */\n findEnrollmentChallenge?(challenge: string): Promise<EnrollmentChallenge | null>;\n /**\n * Atomically mark a challenge as consumed. Must set\n * `consumed_at = now()` and return the updated row only when the\n * challenge was previously unused. Adapters should implement\n * this as a conditional UPDATE (e.g. Postgres\n * `UPDATE ... WHERE consumed_at IS NULL RETURNING *`) so two\n * concurrent consume attempts cannot both succeed.\n */\n consumeEnrollmentChallenge?(challenge: string): Promise<EnrollmentChallenge | null>;\n /**\n * Delete expired, unconsumed challenges. Called periodically by\n * the core layer; adapters can no-op if they rely on a TTL index\n * elsewhere.\n */\n pruneExpiredEnrollmentChallenges?(now: Date): Promise<number>;\n\n // Tenants (optional - for multi-tenancy)\n findTenant?(id: string): Promise<Tenant | null>;\n findTenantBySlug?(slug: string): Promise<Tenant | null>;\n listTenants?(filter?: TenantFilter): Promise<TenantListResult>;\n createTenant?(data: CreateTenantInput): Promise<Tenant>;\n updateTenant?(id: string, data: UpdateTenantInput): Promise<Tenant>;\n deleteTenant?(id: string): Promise<void>;\n getTenantStats?(tenantId: string): Promise<TenantStats>;\n\n // Users (optional - for RBAC)\n findUser?(id: string): Promise<User | null>;\n findUserByEmail?(email: string, tenantId?: string): Promise<User | null>;\n listUsers?(filter?: UserFilter): Promise<UserListResult>;\n createUser?(data: CreateUserInput): Promise<User>;\n updateUser?(id: string, data: UpdateUserInput): Promise<User>;\n deleteUser?(id: string): Promise<void>;\n\n // Roles (optional - for RBAC)\n findRole?(id: string): Promise<Role | null>;\n listRoles?(tenantId?: string): Promise<Role[]>;\n createRole?(data: CreateRoleInput): Promise<Role>;\n updateRole?(id: string, data: UpdateRoleInput): Promise<Role>;\n deleteRole?(id: string): Promise<void>;\n assignRoleToUser?(userId: string, roleId: string): Promise<void>;\n removeRoleFromUser?(userId: string, roleId: string): Promise<void>;\n getUserRoles?(userId: string): Promise<Role[]>;\n\n // Audit Logs (optional - for compliance)\n createAuditLog?(data: CreateAuditLogInput): Promise<AuditLog>;\n listAuditLogs?(filter?: AuditLogFilter): Promise<AuditLogListResult>;\n deleteAuditLogs?(filter: { olderThan?: Date; tenantId?: string }): Promise<number>;\n\n // Scheduled Tasks (optional - for scheduling)\n findScheduledTask?(id: string): Promise<ScheduledTask | null>;\n listScheduledTasks?(filter?: ScheduledTaskFilter): Promise<ScheduledTaskListResult>;\n createScheduledTask?(data: CreateScheduledTaskInput): Promise<ScheduledTask>;\n updateScheduledTask?(id: string, data: UpdateScheduledTaskInput): Promise<ScheduledTask>;\n deleteScheduledTask?(id: string): Promise<void>;\n getUpcomingTasks?(hours: number): Promise<ScheduledTask[]>;\n createTaskExecution?(data: { taskId: string }): Promise<TaskExecution>;\n updateTaskExecution?(id: string, data: Partial<TaskExecution>): Promise<TaskExecution>;\n listTaskExecutions?(taskId: string, limit?: number): Promise<TaskExecution[]>;\n\n // Message Queue (optional - for persistent messaging)\n enqueueMessage?(data: EnqueueMessageInput): Promise<QueuedMessage>;\n dequeueMessages?(deviceId: string, limit?: number): Promise<QueuedMessage[]>;\n peekMessages?(deviceId: string, limit?: number): Promise<QueuedMessage[]>;\n acknowledgeMessage?(messageId: string): Promise<void>;\n failMessage?(messageId: string, error: string): Promise<void>;\n retryFailedMessages?(maxAttempts?: number): Promise<number>;\n purgeExpiredMessages?(): Promise<number>;\n getQueueStats?(tenantId?: string): Promise<QueueStats>;\n\n // Plugin Storage (optional)\n getPluginValue?(pluginName: string, key: string): Promise<unknown | null>;\n setPluginValue?(pluginName: string, key: string, value: unknown): Promise<void>;\n deletePluginValue?(pluginName: string, key: string): Promise<void>;\n listPluginKeys?(pluginName: string, prefix?: string): Promise<string[]>;\n clearPluginData?(pluginName: string): Promise<void>;\n\n // Dashboard (optional - for analytics)\n getDashboardStats?(tenantId?: string): Promise<DashboardStats>;\n getDeviceStatusBreakdown?(tenantId?: string): Promise<DeviceStatusBreakdown>;\n getEnrollmentTrend?(days: number, tenantId?: string): Promise<EnrollmentTrendPoint[]>;\n getCommandSuccessRates?(tenantId?: string): Promise<CommandSuccessRates>;\n getAppInstallationSummary?(tenantId?: string): Promise<AppInstallationSummary>;\n\n // Transactions (optional)\n transaction?<T>(fn: () => Promise<T>): Promise<T>;\n}\n\nexport interface PushAdapter {\n /** Send push message to a device */\n send(deviceId: string, message: PushMessage): Promise<PushResult>;\n /** Send push message to multiple devices */\n sendBatch(deviceIds: string[], message: PushMessage): Promise<PushBatchResult>;\n /** Register device push token */\n registerToken?(deviceId: string, token: string): Promise<void>;\n /** Unregister device push token */\n unregisterToken?(deviceId: string): Promise<void>;\n /** Subscribe device to topic */\n subscribe?(deviceId: string, topic: string): Promise<void>;\n /** Unsubscribe device from topic */\n unsubscribe?(deviceId: string, topic: string): Promise<void>;\n}\n\nexport interface PushMessage {\n type: string;\n payload?: Record<string, unknown>;\n priority?: 'high' | 'normal';\n ttl?: number;\n collapseKey?: string;\n}\n\nexport interface PushResult {\n success: boolean;\n messageId?: string;\n error?: string;\n}\n\nexport interface PushBatchResult {\n successCount: number;\n failureCount: number;\n results: Array<{ deviceId: string; result: PushResult }>;\n}\n\n// ============================================\n// Plugin Interface\n// ============================================\n\nexport interface MDMPlugin {\n /** Unique plugin name */\n name: string;\n /** Plugin version */\n version: string;\n\n /** Called when MDM is initialized */\n onInit?(mdm: MDMInstance): Promise<void>;\n\n /** Called when MDM is destroyed */\n onDestroy?(): Promise<void>;\n\n /** Additional routes to mount */\n routes?: PluginRoute[];\n\n /** Middleware to apply to all routes */\n middleware?: PluginMiddleware[];\n\n /** Extend enrollment process */\n onEnroll?(device: Device, request: EnrollmentRequest): Promise<void>;\n\n /** Extend device processing */\n onDeviceEnrolled?(device: Device): Promise<void>;\n onDeviceUnenrolled?(device: Device): Promise<void>;\n onHeartbeat?(device: Device, heartbeat: Heartbeat): Promise<void>;\n\n /** Extend policy processing */\n policySchema?: Record<string, unknown>;\n validatePolicy?(settings: PolicySettings): Promise<{ valid: boolean; errors?: string[] }>;\n applyPolicy?(device: Device, policy: Policy): Promise<void>;\n\n /** Extend command processing */\n commandTypes?: CommandType[];\n executeCommand?(device: Device, command: Command): Promise<CommandResult>;\n}\n\nexport interface PluginRoute {\n method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';\n path: string;\n handler: (context: unknown) => Promise<unknown>;\n auth?: boolean;\n admin?: boolean;\n}\n\nexport type PluginMiddleware = (\n context: unknown,\n next: () => Promise<unknown>\n) => Promise<unknown>;\n\n// ============================================\n// MDM Instance Interface\n// ============================================\n\nexport interface WebhookManager {\n /** Deliver an event to all matching webhook endpoints */\n deliver<T>(event: MDMEvent<T>): Promise<WebhookDeliveryResult[]>;\n /** Add a webhook endpoint at runtime */\n addEndpoint(endpoint: WebhookEndpoint): void;\n /** Remove a webhook endpoint */\n removeEndpoint(endpointId: string): void;\n /** Update a webhook endpoint */\n updateEndpoint(endpointId: string, updates: Partial<WebhookEndpoint>): void;\n /** Get all configured endpoints */\n getEndpoints(): WebhookEndpoint[];\n /** Test a webhook endpoint with a test payload */\n testEndpoint(endpointId: string): Promise<WebhookDeliveryResult>;\n}\n\nexport interface WebhookDeliveryResult {\n endpointId: string;\n success: boolean;\n statusCode?: number;\n error?: string;\n retryCount: number;\n deliveredAt?: Date;\n}\n\nexport interface MDMInstance {\n /** Device management */\n devices: DeviceManager;\n /** Policy management */\n policies: PolicyManager;\n /** Application management */\n apps: ApplicationManager;\n /** Command management */\n commands: CommandManager;\n /** Group management */\n groups: GroupManager;\n\n /** Tenant management (if multi-tenancy enabled) */\n tenants?: TenantManager;\n /** Authorization management (RBAC) */\n authorization?: AuthorizationManager;\n /** Audit logging */\n audit?: AuditManager;\n /** Scheduled task management */\n schedules?: ScheduleManager;\n /** Persistent message queue */\n messageQueue?: MessageQueueManager;\n /** Dashboard analytics */\n dashboard?: DashboardManager;\n /** Plugin storage */\n pluginStorage?: PluginStorageAdapter;\n\n /** Push notification service */\n push: PushAdapter;\n\n /** Webhook delivery (if configured) */\n webhooks?: WebhookManager;\n\n /** Database adapter */\n db: DatabaseAdapter;\n\n /** Structured logger. Already scoped to the `openmdm` namespace. Plugins should call `.child({...})` to scope further. */\n logger: Logger;\n\n /** Configuration */\n config: MDMConfig;\n\n /** Subscribe to events */\n on<T extends EventType>(event: T, handler: EventHandler<T>): () => void;\n /** Emit an event */\n emit<T extends EventType>(event: T, data: EventPayloadMap[T]): Promise<void>;\n\n /** Process device enrollment */\n enroll(request: EnrollmentRequest): Promise<EnrollmentResponse>;\n /** Process device heartbeat */\n processHeartbeat(deviceId: string, heartbeat: Heartbeat): Promise<void>;\n /** Verify device token */\n verifyDeviceToken(token: string): Promise<{ deviceId: string } | null>;\n\n /** Get loaded plugins */\n getPlugins(): MDMPlugin[];\n /** Get plugin by name */\n getPlugin(name: string): MDMPlugin | undefined;\n}\n\n// ============================================\n// Manager Interfaces\n// ============================================\n\nexport interface DeviceManager {\n get(id: string): Promise<Device | null>;\n getByEnrollmentId(enrollmentId: string): Promise<Device | null>;\n list(filter?: DeviceFilter): Promise<DeviceListResult>;\n create(data: CreateDeviceInput): Promise<Device>;\n update(id: string, data: UpdateDeviceInput): Promise<Device>;\n delete(id: string): Promise<void>;\n assignPolicy(deviceId: string, policyId: string | null): Promise<Device>;\n addToGroup(deviceId: string, groupId: string): Promise<void>;\n removeFromGroup(deviceId: string, groupId: string): Promise<void>;\n getGroups(deviceId: string): Promise<Group[]>;\n sendCommand(deviceId: string, input: Omit<SendCommandInput, 'deviceId'>): Promise<Command>;\n sync(deviceId: string): Promise<Command>;\n reboot(deviceId: string): Promise<Command>;\n lock(deviceId: string, message?: string): Promise<Command>;\n wipe(deviceId: string, preserveData?: boolean): Promise<Command>;\n}\n\nexport interface PolicyManager {\n get(id: string): Promise<Policy | null>;\n getDefault(): Promise<Policy | null>;\n list(): Promise<Policy[]>;\n create(data: CreatePolicyInput): Promise<Policy>;\n update(id: string, data: UpdatePolicyInput): Promise<Policy>;\n delete(id: string): Promise<void>;\n setDefault(id: string): Promise<Policy>;\n getDevices(policyId: string): Promise<Device[]>;\n applyToDevice(policyId: string, deviceId: string): Promise<void>;\n}\n\nexport interface ApplicationManager {\n get(id: string): Promise<Application | null>;\n getByPackage(packageName: string, version?: string): Promise<Application | null>;\n list(activeOnly?: boolean): Promise<Application[]>;\n register(data: CreateApplicationInput): Promise<Application>;\n update(id: string, data: UpdateApplicationInput): Promise<Application>;\n delete(id: string): Promise<void>;\n activate(id: string): Promise<Application>;\n deactivate(id: string): Promise<Application>;\n deploy(packageName: string, target: DeployTarget): Promise<void>;\n installOnDevice(packageName: string, deviceId: string, version?: string): Promise<Command>;\n uninstallFromDevice(packageName: string, deviceId: string): Promise<Command>;\n}\n\nexport interface CommandManager {\n get(id: string): Promise<Command | null>;\n list(filter?: CommandFilter): Promise<Command[]>;\n send(input: SendCommandInput): Promise<Command>;\n cancel(id: string): Promise<Command>;\n acknowledge(id: string): Promise<Command>;\n complete(id: string, result: CommandResult): Promise<Command>;\n fail(id: string, error: string): Promise<Command>;\n getPending(deviceId: string): Promise<Command[]>;\n}\n\nexport interface GroupManager {\n // Basic CRUD operations\n get(id: string): Promise<Group | null>;\n list(): Promise<Group[]>;\n create(data: CreateGroupInput): Promise<Group>;\n update(id: string, data: UpdateGroupInput): Promise<Group>;\n delete(id: string): Promise<void>;\n\n // Device management\n getDevices(groupId: string): Promise<Device[]>;\n addDevice(groupId: string, deviceId: string): Promise<void>;\n removeDevice(groupId: string, deviceId: string): Promise<void>;\n\n // Hierarchy operations\n getChildren(groupId: string): Promise<Group[]>;\n getTree(rootId?: string): Promise<GroupTreeNode[]>;\n getAncestors(groupId: string): Promise<Group[]>;\n getDescendants(groupId: string): Promise<Group[]>;\n move(groupId: string, newParentId: string | null): Promise<Group>;\n getEffectivePolicy(groupId: string): Promise<Policy | null>;\n getHierarchyStats(): Promise<GroupHierarchyStats>;\n}\n\n// ============================================\n// Group Hierarchy Types\n// ============================================\n\nexport interface GroupTreeNode extends Group {\n children: GroupTreeNode[];\n depth: number;\n path: string[];\n effectivePolicyId?: string | null;\n}\n\nexport interface GroupHierarchyStats {\n totalGroups: number;\n maxDepth: number;\n groupsWithDevices: number;\n groupsWithPolicies: number;\n}\n\n// ============================================\n// Tenant Types (Multi-tenancy)\n// ============================================\n\nexport type TenantStatus = 'active' | 'suspended' | 'pending';\n\nexport interface Tenant {\n id: string;\n name: string;\n slug: string;\n status: TenantStatus;\n settings?: TenantSettings | null;\n metadata?: Record<string, unknown> | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface TenantSettings {\n maxDevices?: number;\n maxUsers?: number;\n features?: string[];\n branding?: {\n logo?: string;\n primaryColor?: string;\n };\n}\n\nexport interface CreateTenantInput {\n name: string;\n slug: string;\n settings?: TenantSettings;\n metadata?: Record<string, unknown>;\n}\n\nexport interface UpdateTenantInput {\n name?: string;\n slug?: string;\n status?: TenantStatus;\n settings?: TenantSettings;\n metadata?: Record<string, unknown>;\n}\n\nexport interface TenantFilter {\n status?: TenantStatus;\n search?: string;\n limit?: number;\n offset?: number;\n}\n\nexport interface TenantListResult {\n tenants: Tenant[];\n total: number;\n limit: number;\n offset: number;\n}\n\nexport interface TenantStats {\n deviceCount: number;\n userCount: number;\n policyCount: number;\n appCount: number;\n}\n\n// ============================================\n// RBAC Types (Role-Based Access Control)\n// ============================================\n\nexport type PermissionAction = 'create' | 'read' | 'update' | 'delete' | 'manage' | '*';\nexport type PermissionResource = 'devices' | 'policies' | 'apps' | 'groups' | 'commands' | 'users' | 'roles' | 'tenants' | 'audit' | '*';\n\nexport interface Permission {\n action: PermissionAction;\n resource: PermissionResource;\n resourceId?: string;\n}\n\nexport interface Role {\n id: string;\n tenantId?: string | null;\n name: string;\n description?: string | null;\n permissions: Permission[];\n isSystem: boolean;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface CreateRoleInput {\n tenantId?: string;\n name: string;\n description?: string;\n permissions: Permission[];\n}\n\nexport interface UpdateRoleInput {\n name?: string;\n description?: string;\n permissions?: Permission[];\n}\n\nexport interface User {\n id: string;\n tenantId?: string | null;\n email: string;\n name?: string | null;\n status: 'active' | 'inactive' | 'pending';\n metadata?: Record<string, unknown> | null;\n lastLoginAt?: Date | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface UserWithRoles extends User {\n roles: Role[];\n}\n\nexport interface CreateUserInput {\n tenantId?: string;\n email: string;\n name?: string;\n status?: 'active' | 'inactive' | 'pending';\n metadata?: Record<string, unknown>;\n}\n\nexport interface UpdateUserInput {\n email?: string;\n name?: string;\n status?: 'active' | 'inactive' | 'pending';\n metadata?: Record<string, unknown>;\n}\n\nexport interface UserFilter {\n tenantId?: string;\n status?: 'active' | 'inactive' | 'pending';\n search?: string;\n limit?: number;\n offset?: number;\n}\n\nexport interface UserListResult {\n users: User[];\n total: number;\n limit: number;\n offset: number;\n}\n\n// ============================================\n// Audit Types\n// ============================================\n\nexport type AuditAction = 'create' | 'read' | 'update' | 'delete' | 'login' | 'logout' | 'enroll' | 'unenroll' | 'command' | 'export' | 'import' | 'custom';\n\nexport interface AuditLog {\n id: string;\n tenantId?: string | null;\n userId?: string | null;\n action: AuditAction;\n resource: string;\n resourceId?: string | null;\n status: 'success' | 'failure';\n error?: string | null;\n details?: Record<string, unknown> | null;\n ipAddress?: string | null;\n userAgent?: string | null;\n createdAt: Date;\n}\n\nexport interface CreateAuditLogInput {\n tenantId?: string;\n userId?: string;\n action: AuditAction;\n resource: string;\n resourceId?: string;\n status?: 'success' | 'failure';\n error?: string;\n details?: Record<string, unknown>;\n ipAddress?: string;\n userAgent?: string;\n}\n\nexport interface AuditConfig {\n enabled: boolean;\n retentionDays?: number;\n skipReadOperations?: boolean;\n logActions?: AuditAction[];\n logResources?: string[];\n}\n\nexport interface AuditSummary {\n totalLogs: number;\n byAction: Record<AuditAction, number>;\n byResource: Record<string, number>;\n byStatus: { success: number; failure: number };\n topUsers: Array<{ userId: string; count: number }>;\n recentFailures: AuditLog[];\n}\n\nexport interface AuditLogFilter {\n tenantId?: string;\n userId?: string;\n action?: string;\n resource?: string;\n resourceId?: string;\n startDate?: Date;\n endDate?: Date;\n limit?: number;\n offset?: number;\n}\n\nexport interface AuditLogListResult {\n logs: AuditLog[];\n total: number;\n limit: number;\n offset: number;\n}\n\n// ============================================\n// Schedule Types\n// ============================================\n\nexport type TaskType = 'command' | 'policy_update' | 'app_install' | 'maintenance' | 'custom';\nexport type ScheduledTaskStatus = 'active' | 'paused' | 'completed' | 'failed';\n\nexport interface MaintenanceWindow {\n daysOfWeek: number[];\n startTime: string;\n endTime: string;\n timezone: string;\n}\n\nexport interface TaskSchedule {\n type: 'once' | 'recurring' | 'window';\n executeAt?: Date;\n cron?: string;\n window?: MaintenanceWindow;\n}\n\nexport interface ScheduledTask {\n id: string;\n tenantId?: string | null;\n name: string;\n description?: string | null;\n taskType: TaskType;\n schedule: TaskSchedule;\n target?: DeployTarget;\n payload?: Record<string, unknown> | null;\n status: ScheduledTaskStatus;\n nextRunAt?: Date | null;\n lastRunAt?: Date | null;\n maxRetries: number;\n retryCount: number;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface CreateScheduledTaskInput {\n tenantId?: string;\n name: string;\n description?: string;\n taskType: TaskType;\n schedule: TaskSchedule;\n target?: DeployTarget;\n payload?: Record<string, unknown>;\n maxRetries?: number;\n}\n\nexport interface UpdateScheduledTaskInput {\n name?: string;\n description?: string;\n schedule?: TaskSchedule;\n target?: DeployTarget;\n payload?: Record<string, unknown>;\n status?: ScheduledTaskStatus;\n maxRetries?: number;\n}\n\nexport interface ScheduledTaskFilter {\n tenantId?: string;\n taskType?: TaskType | TaskType[];\n status?: ScheduledTaskStatus | ScheduledTaskStatus[];\n limit?: number;\n offset?: number;\n}\n\nexport interface ScheduledTaskListResult {\n tasks: ScheduledTask[];\n total: number;\n limit: number;\n offset: number;\n}\n\nexport interface TaskExecution {\n id: string;\n taskId: string;\n status: 'running' | 'completed' | 'failed';\n startedAt: Date;\n completedAt?: Date | null;\n devicesProcessed: number;\n devicesSucceeded: number;\n devicesFailed: number;\n error?: string | null;\n details?: Record<string, unknown> | null;\n}\n\n// ============================================\n// Message Queue Types\n// ============================================\n\nexport type QueueMessageStatus = 'pending' | 'processing' | 'delivered' | 'failed' | 'expired';\n\nexport interface QueuedMessage {\n id: string;\n tenantId?: string | null;\n deviceId: string;\n messageType: string;\n payload: Record<string, unknown>;\n priority: 'high' | 'normal' | 'low';\n status: QueueMessageStatus;\n attempts: number;\n maxAttempts: number;\n lastAttemptAt?: Date | null;\n lastError?: string | null;\n expiresAt?: Date | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface EnqueueMessageInput {\n tenantId?: string;\n deviceId: string;\n messageType: string;\n payload: Record<string, unknown>;\n priority?: 'high' | 'normal' | 'low';\n maxAttempts?: number;\n ttlSeconds?: number;\n}\n\nexport interface QueueStats {\n pending: number;\n processing: number;\n delivered: number;\n failed: number;\n expired: number;\n byDevice: Record<string, number>;\n oldestPending?: Date;\n}\n\n// ============================================\n// Dashboard Types\n// ============================================\n\nexport interface DashboardStats {\n devices: {\n total: number;\n enrolled: number;\n active: number;\n blocked: number;\n pending: number;\n };\n policies: {\n total: number;\n deployed: number;\n };\n applications: {\n total: number;\n deployed: number;\n };\n commands: {\n pendingCount: number;\n last24hTotal: number;\n last24hSuccess: number;\n last24hFailed: number;\n };\n groups: {\n total: number;\n withDevices: number;\n };\n}\n\nexport interface DeviceStatusBreakdown {\n byStatus: Record<DeviceStatus, number>;\n byOs: Record<string, number>;\n byManufacturer: Record<string, number>;\n byModel: Record<string, number>;\n}\n\nexport interface EnrollmentTrendPoint {\n date: Date;\n enrolled: number;\n unenrolled: number;\n netChange: number;\n totalDevices: number;\n}\n\nexport interface CommandSuccessRates {\n overall: {\n total: number;\n completed: number;\n failed: number;\n successRate: number;\n };\n byType: Record<string, {\n total: number;\n completed: number;\n failed: number;\n successRate: number;\n avgExecutionTimeMs?: number;\n }>;\n last24h: {\n total: number;\n completed: number;\n failed: number;\n pending: number;\n };\n}\n\nexport interface AppInstallationSummary {\n total: number;\n byStatus: Record<string, number>;\n recentFailures: Array<{\n packageName: string;\n deviceId: string;\n error: string;\n timestamp: Date;\n }>;\n topInstalled: Array<{\n packageName: string;\n name: string;\n installedCount: number;\n }>;\n}\n\n// ============================================\n// Plugin Storage Types\n// ============================================\n\nexport interface PluginStorageAdapter {\n get<T>(pluginName: string, key: string): Promise<T | null>;\n set<T>(pluginName: string, key: string, value: T): Promise<void>;\n delete(pluginName: string, key: string): Promise<void>;\n list(pluginName: string, prefix?: string): Promise<string[]>;\n clear(pluginName: string): Promise<void>;\n}\n\nexport interface PluginStorageEntry {\n pluginName: string;\n key: string;\n value: unknown;\n createdAt: Date;\n updatedAt: Date;\n}\n\n// ============================================\n// Enterprise Manager Interfaces\n// ============================================\n\nexport interface TenantManager {\n get(id: string): Promise<Tenant | null>;\n getBySlug(slug: string): Promise<Tenant | null>;\n list(filter?: TenantFilter): Promise<TenantListResult>;\n create(data: CreateTenantInput): Promise<Tenant>;\n update(id: string, data: UpdateTenantInput): Promise<Tenant>;\n delete(id: string, cascade?: boolean): Promise<void>;\n getStats(tenantId: string): Promise<TenantStats>;\n activate(id: string): Promise<Tenant>;\n deactivate(id: string): Promise<Tenant>;\n}\n\nexport interface AuthorizationManager {\n createRole(data: CreateRoleInput): Promise<Role>;\n getRole(id: string): Promise<Role | null>;\n listRoles(tenantId?: string): Promise<Role[]>;\n updateRole(id: string, data: UpdateRoleInput): Promise<Role>;\n deleteRole(id: string): Promise<void>;\n createUser(data: CreateUserInput): Promise<User>;\n getUser(id: string): Promise<UserWithRoles | null>;\n getUserByEmail(email: string, tenantId?: string): Promise<UserWithRoles | null>;\n listUsers(filter?: UserFilter): Promise<UserListResult>;\n updateUser(id: string, data: UpdateUserInput): Promise<User>;\n deleteUser(id: string): Promise<void>;\n assignRole(userId: string, roleId: string): Promise<void>;\n removeRole(userId: string, roleId: string): Promise<void>;\n getUserRoles(userId: string): Promise<Role[]>;\n can(userId: string, action: PermissionAction, resource: PermissionResource, resourceId?: string): Promise<boolean>;\n canAny(userId: string, permissions: Array<{ action: PermissionAction; resource: PermissionResource }>): Promise<boolean>;\n requirePermission(userId: string, action: PermissionAction, resource: PermissionResource, resourceId?: string): Promise<void>;\n isAdmin(userId: string): Promise<boolean>;\n}\n\nexport interface AuditManager {\n log(entry: CreateAuditLogInput): Promise<AuditLog>;\n list(filter?: AuditLogFilter): Promise<AuditLogListResult>;\n getByResource(resource: string, resourceId: string): Promise<AuditLog[]>;\n getByUser(userId: string, filter?: AuditLogFilter): Promise<AuditLogListResult>;\n export(filter: AuditLogFilter, format: 'json' | 'csv'): Promise<string>;\n purge(olderThanDays?: number): Promise<number>;\n getSummary(tenantId?: string, days?: number): Promise<AuditSummary>;\n}\n\nexport interface ScheduleManager {\n get(id: string): Promise<ScheduledTask | null>;\n list(filter?: ScheduledTaskFilter): Promise<ScheduledTaskListResult>;\n create(data: CreateScheduledTaskInput): Promise<ScheduledTask>;\n update(id: string, data: UpdateScheduledTaskInput): Promise<ScheduledTask>;\n delete(id: string): Promise<void>;\n pause(id: string): Promise<ScheduledTask>;\n resume(id: string): Promise<ScheduledTask>;\n runNow(id: string): Promise<TaskExecution>;\n getUpcoming(hours: number): Promise<ScheduledTask[]>;\n getExecutions(taskId: string, limit?: number): Promise<TaskExecution[]>;\n calculateNextRun(schedule: TaskSchedule): Date | null;\n}\n\nexport interface MessageQueueManager {\n enqueue(message: EnqueueMessageInput): Promise<QueuedMessage>;\n enqueueBatch(messages: EnqueueMessageInput[]): Promise<QueuedMessage[]>;\n dequeue(deviceId: string, limit?: number): Promise<QueuedMessage[]>;\n acknowledge(messageId: string): Promise<void>;\n fail(messageId: string, error: string): Promise<void>;\n retryFailed(maxAttempts?: number): Promise<number>;\n purgeExpired(): Promise<number>;\n getStats(tenantId?: string): Promise<QueueStats>;\n peek(deviceId: string, limit?: number): Promise<QueuedMessage[]>;\n}\n\nexport interface DashboardManager {\n getStats(tenantId?: string): Promise<DashboardStats>;\n getDeviceStatusBreakdown(tenantId?: string): Promise<DeviceStatusBreakdown>;\n getEnrollmentTrend(days: number, tenantId?: string): Promise<EnrollmentTrendPoint[]>;\n getCommandSuccessRates(tenantId?: string): Promise<CommandSuccessRates>;\n getAppInstallationSummary(tenantId?: string): Promise<AppInstallationSummary>;\n}\n\n// ============================================\n// Event Handler Types\n// ============================================\n\nexport type EventHandler<T extends EventType> = (\n event: MDMEvent<EventPayloadMap[T]>\n) => Promise<void> | void;\n\nexport interface EventPayloadMap {\n 'device.enrolled': { device: Device };\n 'device.unenrolled': { device: Device; reason?: string };\n 'device.blocked': { device: Device; reason: string };\n 'device.heartbeat': { device: Device; heartbeat: Heartbeat };\n 'device.locationUpdated': { device: Device; location: DeviceLocation };\n 'device.statusChanged': { device: Device; oldStatus: DeviceStatus; newStatus: DeviceStatus };\n 'device.policyChanged': { device: Device; oldPolicyId?: string; newPolicyId?: string };\n 'app.installed': { device: Device; app: InstalledApp };\n 'app.uninstalled': { device: Device; packageName: string };\n 'app.updated': { device: Device; app: InstalledApp; oldVersion: string };\n 'app.crashed': { device: Device; packageName: string; error?: string };\n 'app.started': { device: Device; packageName: string };\n 'app.stopped': { device: Device; packageName: string };\n 'policy.applied': { device: Device; policy: Policy };\n 'policy.failed': { device: Device; policy: Policy; error: string };\n 'command.received': { device: Device; command: Command };\n 'command.acknowledged': { device: Device; command: Command };\n 'command.completed': { device: Device; command: Command; result: CommandResult };\n 'command.failed': { device: Device; command: Command; error: string };\n 'security.tamper': { device: Device; type: string; details?: unknown };\n 'security.rootDetected': { device: Device };\n 'security.screenLocked': { device: Device };\n 'security.screenUnlocked': { device: Device };\n custom: Record<string, unknown>;\n}\n\n// ============================================\n// Error Types\n// ============================================\n\nexport class MDMError extends Error {\n constructor(\n message: string,\n public code: string,\n public statusCode: number = 500,\n public details?: unknown\n ) {\n super(message);\n this.name = 'MDMError';\n }\n}\n\nexport class DeviceNotFoundError extends MDMError {\n constructor(deviceId: string) {\n super(`Device not found: ${deviceId}`, 'DEVICE_NOT_FOUND', 404);\n }\n}\n\nexport class PolicyNotFoundError extends MDMError {\n constructor(policyId: string) {\n super(`Policy not found: ${policyId}`, 'POLICY_NOT_FOUND', 404);\n }\n}\n\nexport class ApplicationNotFoundError extends MDMError {\n constructor(identifier: string) {\n super(`Application not found: ${identifier}`, 'APPLICATION_NOT_FOUND', 404);\n }\n}\n\nexport class CommandNotFoundError extends MDMError {\n constructor(commandId: string) {\n super(`Command not found: ${commandId}`, 'COMMAND_NOT_FOUND', 404);\n }\n}\n\nexport class TenantNotFoundError extends MDMError {\n constructor(identifier: string) {\n super(`Tenant not found: ${identifier}`, 'TENANT_NOT_FOUND', 404);\n }\n}\n\nexport class RoleNotFoundError extends MDMError {\n constructor(identifier: string) {\n super(`Role not found: ${identifier}`, 'ROLE_NOT_FOUND', 404);\n }\n}\n\nexport class GroupNotFoundError extends MDMError {\n constructor(identifier: string) {\n super(`Group not found: ${identifier}`, 'GROUP_NOT_FOUND', 404);\n }\n}\n\nexport class UserNotFoundError extends MDMError {\n constructor(identifier: string) {\n super(`User not found: ${identifier}`, 'USER_NOT_FOUND', 404);\n }\n}\n\nexport class EnrollmentError extends MDMError {\n constructor(message: string, details?: unknown) {\n super(message, 'ENROLLMENT_ERROR', 400, details);\n }\n}\n\nexport class AuthenticationError extends MDMError {\n constructor(message: string = 'Authentication required') {\n super(message, 'AUTHENTICATION_ERROR', 401);\n }\n}\n\nexport class AuthorizationError extends MDMError {\n constructor(message: string = 'Access denied') {\n super(message, 'AUTHORIZATION_ERROR', 403);\n }\n}\n\nexport class ValidationError extends MDMError {\n constructor(message: string, details?: unknown) {\n super(message, 'VALIDATION_ERROR', 400, details);\n }\n}\n"]}
|
package/package.json
CHANGED
package/src/dashboard.ts
CHANGED
|
@@ -16,6 +16,34 @@ import type {
|
|
|
16
16
|
DeviceStatus,
|
|
17
17
|
} from './types';
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Throws if a tenantId is supplied to a dashboard method whose
|
|
21
|
+
* database adapter does not implement the tenant-scoped version.
|
|
22
|
+
*
|
|
23
|
+
* The old behavior — silently ignoring the tenantId and returning
|
|
24
|
+
* global stats — was a data-leak footgun in multi-tenant deployments.
|
|
25
|
+
* We would rather fail loudly than return fleet-wide numbers to a
|
|
26
|
+
* caller who thought they were asking about one tenant.
|
|
27
|
+
*
|
|
28
|
+
* The audit recommended this as a backstop until core resources gain
|
|
29
|
+
* a real `tenantId` column. Once that lands, this guard becomes
|
|
30
|
+
* redundant — the fallback paths will be able to filter themselves.
|
|
31
|
+
*/
|
|
32
|
+
function assertNoTenantScopeRequested(
|
|
33
|
+
tenantId: string | undefined,
|
|
34
|
+
method: string,
|
|
35
|
+
): void {
|
|
36
|
+
if (tenantId) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`DashboardManager.${method} was called with a tenantId but the ` +
|
|
39
|
+
'database adapter does not implement tenant-scoped dashboard ' +
|
|
40
|
+
'queries. Implement the matching DatabaseAdapter method, or omit ' +
|
|
41
|
+
'tenantId to accept global stats. See ' +
|
|
42
|
+
'docs/proposals/tenant-rbac-audit.md for context.',
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
19
47
|
/**
|
|
20
48
|
* Create a DashboardManager instance
|
|
21
49
|
*/
|
|
@@ -27,6 +55,10 @@ export function createDashboardManager(db: DatabaseAdapter): DashboardManager {
|
|
|
27
55
|
return db.getDashboardStats(_tenantId);
|
|
28
56
|
}
|
|
29
57
|
|
|
58
|
+
// No tenant-scoped path available in the fallback. Refuse
|
|
59
|
+
// rather than silently returning global stats.
|
|
60
|
+
assertNoTenantScopeRequested(_tenantId, 'getStats');
|
|
61
|
+
|
|
30
62
|
// Fallback: compute from individual queries
|
|
31
63
|
const devices = await db.listDevices({
|
|
32
64
|
limit: 10000, // Get all for counting
|
|
@@ -94,6 +126,8 @@ export function createDashboardManager(db: DatabaseAdapter): DashboardManager {
|
|
|
94
126
|
return db.getDeviceStatusBreakdown(_tenantId);
|
|
95
127
|
}
|
|
96
128
|
|
|
129
|
+
assertNoTenantScopeRequested(_tenantId, 'getDeviceStatusBreakdown');
|
|
130
|
+
|
|
97
131
|
const devices = await db.listDevices({
|
|
98
132
|
limit: 10000,
|
|
99
133
|
});
|
|
@@ -139,6 +173,8 @@ export function createDashboardManager(db: DatabaseAdapter): DashboardManager {
|
|
|
139
173
|
return db.getEnrollmentTrend(days, _tenantId);
|
|
140
174
|
}
|
|
141
175
|
|
|
176
|
+
assertNoTenantScopeRequested(_tenantId, 'getEnrollmentTrend');
|
|
177
|
+
|
|
142
178
|
// Generate trend data from event history
|
|
143
179
|
const now = new Date();
|
|
144
180
|
const startDate = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
|
|
@@ -215,6 +251,8 @@ export function createDashboardManager(db: DatabaseAdapter): DashboardManager {
|
|
|
215
251
|
return db.getCommandSuccessRates(_tenantId);
|
|
216
252
|
}
|
|
217
253
|
|
|
254
|
+
assertNoTenantScopeRequested(_tenantId, 'getCommandSuccessRates');
|
|
255
|
+
|
|
218
256
|
const now = new Date();
|
|
219
257
|
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
220
258
|
|
|
@@ -276,6 +314,8 @@ export function createDashboardManager(db: DatabaseAdapter): DashboardManager {
|
|
|
276
314
|
return db.getAppInstallationSummary(_tenantId);
|
|
277
315
|
}
|
|
278
316
|
|
|
317
|
+
assertNoTenantScopeRequested(_tenantId, 'getAppInstallationSummary');
|
|
318
|
+
|
|
279
319
|
// Get all apps
|
|
280
320
|
const apps = await db.listApplications();
|
|
281
321
|
const appMap = new Map(apps.map((a) => [a.packageName, a]));
|