/** * OpenMDM Core Types * * These types define the core data structures for the MDM system. * Designed to be database-agnostic and framework-agnostic. */ // ============================================ // Device Types // ============================================ export type DeviceStatus = 'pending' | 'enrolled' | 'unenrolled' | 'blocked'; export interface Device { id: string; externalId?: string | null; enrollmentId: string; status: DeviceStatus; // Device Info model?: string | null; manufacturer?: string | null; osVersion?: string | null; serialNumber?: string | null; imei?: string | null; macAddress?: string | null; androidId?: string | null; // MDM State policyId?: string | null; agentVersion?: string | null; // MDM agent version installed on device lastHeartbeat?: Date | null; lastSync?: Date | null; // Device identity (Phase 2b — device-pinned ECDSA P-256 key) /** * Base64-encoded SPKI public key the device registered on first * enrollment. Requests from this device can be verified against * this key via `verifyDeviceRequest` / `verifyEcdsaSignature` from * `@openmdm/core`. `null` on devices that enrolled via the legacy * HMAC path and have never been migrated. */ publicKey?: string | null; /** * How the device originally enrolled. `'hmac'` for the legacy * shared-secret path; `'pinned-key'` for the device-pinned ECDSA * path. `null` on pre-Phase-2b device rows that predate the * column (treated as `'hmac'`). */ enrollmentMethod?: 'hmac' | 'pinned-key' | null; // Telemetry batteryLevel?: number | null; storageUsed?: number | null; storageTotal?: number | null; location?: DeviceLocation | null; installedApps?: InstalledApp[] | null; // Metadata tags?: Record | null; metadata?: Record | null; // Timestamps createdAt: Date; updatedAt: Date; } export interface DeviceLocation { latitude: number; longitude: number; accuracy?: number; timestamp: Date; } export interface InstalledApp { packageName: string; version: string; versionCode?: number; installedAt?: Date; } export interface CreateDeviceInput { enrollmentId: string; externalId?: string; model?: string; manufacturer?: string; osVersion?: string; serialNumber?: string; imei?: string; macAddress?: string; androidId?: string; policyId?: string; tags?: Record; metadata?: Record; } export interface UpdateDeviceInput { externalId?: string | null; status?: DeviceStatus; policyId?: string | null; agentVersion?: string | null; model?: string; manufacturer?: string; osVersion?: string; batteryLevel?: number | null; storageUsed?: number | null; storageTotal?: number | null; lastHeartbeat?: Date; lastSync?: Date; installedApps?: InstalledApp[]; location?: DeviceLocation; tags?: Record; metadata?: Record; /** Phase 2b — pin a new public key on first enroll. */ publicKey?: string | null; /** Phase 2b — record which auth path the device enrolled via. */ enrollmentMethod?: 'hmac' | 'pinned-key' | null; } export interface DeviceFilter { status?: DeviceStatus | DeviceStatus[]; policyId?: string; groupId?: string; search?: string; tags?: Record; limit?: number; offset?: number; } export interface DeviceListResult { devices: Device[]; total: number; limit: number; offset: number; } // ============================================ // Policy Types // ============================================ export interface Policy { id: string; name: string; description?: string | null; isDefault: boolean; settings: PolicySettings; createdAt: Date; updatedAt: Date; } export interface PolicySettings { // Kiosk Mode kioskMode?: boolean; mainApp?: string; allowedApps?: string[]; kioskExitPassword?: string; // Lock Features lockStatusBar?: boolean; lockNavigationBar?: boolean; lockSettings?: boolean; lockPowerButton?: boolean; blockInstall?: boolean; blockUninstall?: boolean; // Hardware Controls bluetooth?: HardwareControl; wifi?: HardwareControl; gps?: HardwareControl; mobileData?: HardwareControl; camera?: HardwareControl; microphone?: HardwareControl; usb?: HardwareControl; nfc?: HardwareControl; // Update Settings systemUpdatePolicy?: SystemUpdatePolicy; updateWindow?: TimeWindow; // Security passwordPolicy?: PasswordPolicy; encryptionRequired?: boolean; factoryResetProtection?: boolean; safeBootDisabled?: boolean; // Telemetry heartbeatInterval?: number; locationReportInterval?: number; locationEnabled?: boolean; // Network wifiConfigs?: WifiConfig[]; vpnConfig?: VpnConfig; // Applications applications?: PolicyApplication[]; // Custom settings (for plugins) custom?: Record; } export type HardwareControl = 'on' | 'off' | 'user'; export type SystemUpdatePolicy = 'auto' | 'windowed' | 'postpone' | 'manual'; export interface TimeWindow { start: string; // "HH:MM" end: string; // "HH:MM" } export interface PasswordPolicy { required: boolean; minLength?: number; complexity?: 'none' | 'numeric' | 'alphanumeric' | 'complex'; maxFailedAttempts?: number; expirationDays?: number; historyLength?: number; } export interface WifiConfig { ssid: string; securityType: 'none' | 'wep' | 'wpa' | 'wpa2' | 'wpa3'; password?: string; hidden?: boolean; autoConnect?: boolean; } export interface VpnConfig { type: 'pptp' | 'l2tp' | 'ipsec' | 'openvpn' | 'wireguard'; server: string; username?: string; password?: string; certificate?: string; config?: Record; } export interface PolicyApplication { packageName: string; action: 'install' | 'update' | 'uninstall'; version?: string; required?: boolean; autoUpdate?: boolean; } export interface CreatePolicyInput { name: string; description?: string; isDefault?: boolean; settings: PolicySettings; } export interface UpdatePolicyInput { name?: string; description?: string | null; isDefault?: boolean; settings?: PolicySettings; } // ============================================ // Application Types // ============================================ export interface Application { id: string; name: string; packageName: string; version: string; versionCode: number; url: string; hash?: string | null; size?: number | null; minSdkVersion?: number | null; // Deployment settings showIcon: boolean; runAfterInstall: boolean; runAtBoot: boolean; isSystem: boolean; // State isActive: boolean; // Metadata metadata?: Record | null; createdAt: Date; updatedAt: Date; } export interface CreateApplicationInput { name: string; packageName: string; version: string; versionCode: number; url: string; hash?: string; size?: number; minSdkVersion?: number; showIcon?: boolean; runAfterInstall?: boolean; runAtBoot?: boolean; isSystem?: boolean; metadata?: Record; } export interface UpdateApplicationInput { name?: string; version?: string; versionCode?: number; url?: string; hash?: string | null; size?: number | null; minSdkVersion?: number | null; showIcon?: boolean; runAfterInstall?: boolean; runAtBoot?: boolean; isActive?: boolean; metadata?: Record | null; } export interface DeployTarget { devices?: string[]; policies?: string[]; groups?: string[]; } // ============================================ // App Version & Rollback Types // ============================================ export interface AppVersion { id: string; applicationId: string; packageName: string; version: string; versionCode: number; url: string; hash?: string | null; size?: number | null; releaseNotes?: string | null; isMinimumVersion: boolean; createdAt: Date; } export interface AppRollback { id: string; deviceId: string; packageName: string; fromVersion: string; fromVersionCode: number; toVersion: string; toVersionCode: number; reason?: string | null; status: 'pending' | 'in_progress' | 'completed' | 'failed'; error?: string | null; initiatedBy?: string | null; createdAt: Date; completedAt?: Date | null; } export interface CreateAppRollbackInput { deviceId: string; packageName: string; toVersionCode: number; reason?: string; initiatedBy?: string; } // ============================================ // Command Types // ============================================ export type CommandType = | 'reboot' | 'shutdown' | 'sync' | 'lock' | 'unlock' | 'wipe' | 'factoryReset' | 'installApp' | 'uninstallApp' | 'updateApp' | 'runApp' | 'clearAppData' | 'clearAppCache' | 'shell' | 'setPolicy' | 'grantPermissions' | 'exitKiosk' | 'enterKiosk' | 'setWifi' | 'screenshot' | 'getLocation' | 'setVolume' | 'sendNotification' | 'whitelistBattery' // Whitelist app from battery optimization (Doze) | 'enablePermissiveMode' // Enable permissive mode for debugging | 'setTimeZone' // Set device timezone | 'enableAdb' // Enable/disable ADB debugging | 'rollbackApp' // Rollback to previous app version | 'updateAgent' // Update MDM agent to a new version | 'custom'; export type CommandStatus = | 'pending' | 'sent' | 'acknowledged' | 'completed' | 'failed' | 'cancelled'; export interface Command { id: string; deviceId: string; type: CommandType; payload?: Record | null; status: CommandStatus; result?: CommandResult | null; error?: string | null; createdAt: Date; sentAt?: Date | null; acknowledgedAt?: Date | null; completedAt?: Date | null; } export interface CommandResult { success: boolean; message?: string; data?: unknown; } export interface SendCommandInput { deviceId: string; type: CommandType; payload?: Record; } export interface CommandFilter { deviceId?: string; status?: CommandStatus | CommandStatus[]; type?: CommandType | CommandType[]; limit?: number; offset?: number; } // ============================================ // Event Types // ============================================ export type EventType = | 'device.enrolled' | 'device.unenrolled' | 'device.blocked' | 'device.heartbeat' | 'device.locationUpdated' | 'device.statusChanged' | 'device.policyChanged' | 'app.installed' | 'app.uninstalled' | 'app.updated' | 'app.crashed' | 'app.started' | 'app.stopped' | 'policy.applied' | 'policy.failed' | 'command.received' | 'command.acknowledged' | 'command.completed' | 'command.failed' | 'security.tamper' | 'security.rootDetected' | 'security.screenLocked' | 'security.screenUnlocked' | 'custom'; export interface MDMEvent { id: string; deviceId: string; type: EventType; payload: T; createdAt: Date; } export interface EventFilter { deviceId?: string; type?: EventType | EventType[]; startDate?: Date; endDate?: Date; limit?: number; offset?: number; } // ============================================ // Group Types // ============================================ export interface Group { id: string; name: string; description?: string | null; policyId?: string | null; parentId?: string | null; metadata?: Record | null; createdAt: Date; updatedAt: Date; } export interface CreateGroupInput { name: string; description?: string; policyId?: string; parentId?: string; metadata?: Record; } export interface UpdateGroupInput { name?: string; description?: string | null; policyId?: string | null; parentId?: string | null; metadata?: Record | null; } // ============================================ // Enrollment Types // ============================================ export type EnrollmentMethod = | 'qr' | 'nfc' | 'zero-touch' | 'knox' | 'manual' | 'app-only' | 'adb'; export interface EnrollmentRequest { // Device identifiers (at least one required) macAddress?: string; serialNumber?: string; imei?: string; androidId?: string; // Device info model: string; manufacturer: string; osVersion: string; sdkVersion?: number; // Agent info agentVersion?: string; agentPackage?: string; // Enrollment details method: EnrollmentMethod; timestamp: string; /** * Signature over the canonical enrollment message. * * Phase 2a (HMAC path, backwards-compatible): hex-encoded * HMAC-SHA256 of the nine-field pipe-delimited canonical form * (see `concepts/enrollment`). * * Phase 2b (device-pinned-key path, preferred): base64-encoded * DER ECDSA-P256 signature produced by the device's Keystore * private key, over `canonicalEnrollmentMessage(...)` including * the public key and challenge. The server distinguishes the * two paths by whether `publicKey` is present on the request. */ signature: string; /** * Base64-encoded SPKI public key (EC P-256) the device generated * in its Keystore. When present, enrollment follows the Phase 2b * device-pinned-key path and `signature` must verify as an ECDSA * signature against this key. The server pins this key on the * device row on first successful enroll; any future enroll * attempting a different key for the same `enrollmentId` is * rejected with `PublicKeyMismatchError`. * * Omit for the legacy HMAC path. Callers that want to migrate a * fleet gradually can run both paths in parallel. */ publicKey?: string; /** * Opaque challenge issued by `GET /agent/enroll/challenge`. Must * be present whenever `publicKey` is present — the server uses * it to prevent replay of captured enrollment payloads. The * challenge is single-use: the server consumes it on first * successful verify. */ attestationChallenge?: string; // Optional pre-assigned policy/group policyId?: string; groupId?: string; } export interface EnrollmentResponse { deviceId: string; enrollmentId: string; policyId?: string; policy?: Policy; serverUrl: string; pushConfig: PushConfig; token: string; refreshToken?: string; tokenExpiresAt?: Date; } // ============================================ // Device Identity (Phase 2b) // ============================================ /** * Single-use nonce issued by `GET /agent/enroll/challenge` and * consumed on first successful verify of a device-pinned-key * enrollment. A persisted record; the `consume*` adapter methods * enforce the single-use invariant. */ export interface EnrollmentChallenge { challenge: string; expiresAt: Date; consumedAt?: Date | null; createdAt: Date; } /** * Result of calling `verifyDeviceRequest`. Callers pattern-match on * `ok` and, when `false`, on `reason` to decide their response * shape: * * - `not-found` — unknown device id. Return 401. * - `no-pinned-key` — device exists but never migrated off the * legacy HMAC path. Caller should fall back * to their HMAC verifier, or fail if * they've completed their migration. * - `signature-invalid` — signature did not verify against the * pinned key. Return 401. Never re-pin * in response to this. */ export type DeviceIdentityVerification = | { ok: true; device: Device } | { ok: false; reason: 'not-found' } | { ok: false; reason: 'no-pinned-key'; device: Device } | { ok: false; reason: 'signature-invalid'; device: Device }; export interface PushConfig { provider: 'fcm' | 'mqtt' | 'websocket' | 'polling'; fcmSenderId?: string; mqttUrl?: string; mqttTopic?: string; mqttUsername?: string; mqttPassword?: string; wsUrl?: string; pollingInterval?: number; } // ============================================ // Telemetry Types // ============================================ export interface Heartbeat { deviceId: string; timestamp: Date; // Battery batteryLevel: number; isCharging: boolean; batteryHealth?: 'good' | 'overheat' | 'dead' | 'cold' | 'unknown'; // Storage storageUsed: number; storageTotal: number; // Memory memoryUsed: number; memoryTotal: number; // Network networkType?: 'wifi' | 'cellular' | 'ethernet' | 'none'; networkName?: string; // SSID or carrier signalStrength?: number; ipAddress?: string; // Location location?: DeviceLocation; // Apps installedApps: InstalledApp[]; runningApps?: string[]; // Security isRooted?: boolean; isEncrypted?: boolean; screenLockEnabled?: boolean; // Agent status agentVersion?: string; policyVersion?: string; lastPolicySync?: Date; } // ============================================ // Push Token Types // ============================================ export interface PushToken { id: string; deviceId: string; provider: 'fcm' | 'mqtt' | 'websocket'; token: string; isActive: boolean; createdAt: Date; updatedAt: Date; } export interface RegisterPushTokenInput { deviceId: string; provider: 'fcm' | 'mqtt' | 'websocket'; token: string; } // ============================================ // Configuration Types // ============================================ export interface MDMConfig { /** Database adapter for persistence */ database: DatabaseAdapter; /** Authentication/authorization configuration */ auth?: AuthConfig; /** Push notification provider configuration */ push?: PushProviderConfig; /** Device enrollment configuration */ enrollment?: EnrollmentConfig; /** Server URL (used in enrollment responses) */ serverUrl?: string; /** APK/file storage configuration */ storage?: StorageConfig; /** Outbound webhook configuration */ webhooks?: WebhookConfig; /** Plugins to extend functionality */ plugins?: MDMPlugin[]; /** Event handlers */ onDeviceEnrolled?: (device: Device) => Promise; onDeviceUnenrolled?: (device: Device) => Promise; onDeviceBlocked?: (device: Device) => Promise; onHeartbeat?: (device: Device, heartbeat: Heartbeat) => Promise; onCommand?: (command: Command) => Promise; onEvent?: (event: MDMEvent) => Promise; // Enterprise features /** Multi-tenancy configuration */ multiTenancy?: { enabled: boolean; defaultTenantId?: string; tenantResolver?: (context: unknown) => Promise; }; /** Authorization (RBAC) configuration */ authorization?: { enabled: boolean; defaultRole?: string; }; /** Audit logging configuration */ audit?: { enabled: boolean; retentionDays?: number; }; /** Scheduling configuration */ scheduling?: { enabled: boolean; timezone?: string; }; /** Plugin storage configuration */ pluginStorage?: { adapter: 'database' | 'memory'; }; /** * Structured logger. Replaces OpenMDM's internal `console.*` calls * so log output lands in the host application's logging pipeline * (pino, winston, bunyan, OTEL collector, etc.) instead of raw * stderr. * * The shape is a strict subset of the pino / winston / bunyan * interface — any of those can be passed directly. If omitted, a * default logger that writes to the console with an `[openmdm]` * prefix is used. To silence OpenMDM entirely, pass a no-op * implementation (see `createSilentLogger()` in the package * exports). */ logger?: Logger; } /** * Minimal structured-logger interface OpenMDM calls internally. * * The shape is deliberately the pino-compatible subset: an optional * context object as the first argument followed by a message string. * pino, winston, bunyan, and most other structured loggers accept * this shape natively. * * Implementations should be side-effect-free on unconfigured levels * (a production logger filtered to `info` should still accept * `.debug()` calls cheaply). */ export interface Logger { /** Human-ignorable, high-volume tracing. Off in production by default. */ debug(context: LogContext, message: string): void; debug(message: string): void; /** Normal operational events. Enrollment, policy changes, command delivery. */ info(context: LogContext, message: string): void; info(message: string): void; /** Something is wrong but the server is still running. Retries, fallbacks, degraded modes. */ warn(context: LogContext, message: string): void; warn(message: string): void; /** Something failed and a request/operation did not complete. */ error(context: LogContext, message: string): void; error(message: string): void; /** * Return a new logger with the given fields attached to every * subsequent call. Used by managers and plugins to scope logs to * a specific subsystem without repeating context. */ child(bindings: LogContext): Logger; } /** * Arbitrary structured context attached to a log line. Values must * be JSON-serializable so host loggers can ship them to any backend. */ export type LogContext = Record; export interface StorageConfig { /** Storage provider (s3, local, custom) */ provider: 's3' | 'local' | 'custom'; /** S3 configuration */ s3?: { bucket: string; region: string; accessKeyId?: string; secretAccessKey?: string; endpoint?: string; // For S3-compatible services presignedUrlExpiry?: number; // Seconds, default 3600 }; /** Local storage path */ localPath?: string; /** Custom storage adapter */ customAdapter?: { upload: (file: Buffer, key: string) => Promise; getUrl: (key: string) => Promise; delete: (key: string) => Promise; }; } export interface WebhookConfig { /** Webhook endpoints to notify */ endpoints?: WebhookEndpoint[]; /** Retry configuration */ retry?: { maxRetries?: number; initialDelay?: number; maxDelay?: number; }; /** Sign webhooks with HMAC secret */ signingSecret?: string; } export interface WebhookEndpoint { /** Unique identifier */ id: string; /** Webhook URL */ url: string; /** Events to trigger this webhook */ events: (EventType | '*')[]; /** Custom headers */ headers?: Record; /** Whether endpoint is active */ enabled: boolean; } export interface AuthConfig { /** Get current user from request context */ getUser: (context: unknown) => Promise; /** Check if user has admin privileges */ isAdmin?: (user: unknown) => Promise; /** Check if user can access specific device */ canAccessDevice?: (user: unknown, deviceId: string) => Promise; /** Device JWT secret (for device auth tokens) */ deviceTokenSecret?: string; /** Device token expiration in seconds (default: 365 days) */ deviceTokenExpiration?: number; } export interface PushProviderConfig { provider: 'fcm' | 'mqtt' | 'websocket' | 'polling'; // FCM configuration fcmCredentials?: string | Record; fcmProjectId?: string; // MQTT configuration mqttUrl?: string; mqttUsername?: string; mqttPassword?: string; mqttTopicPrefix?: string; // WebSocket configuration wsPath?: string; wsPingInterval?: number; // Polling fallback pollingInterval?: number; } export interface EnrollmentConfig { /** Auto-enroll devices with valid signature */ autoEnroll?: boolean; /** HMAC secret for device signature verification (Phase 2a path) */ deviceSecret: string; /** Allowed enrollment methods */ allowedMethods?: EnrollmentMethod[]; /** Default policy for new devices */ defaultPolicyId?: string; /** Default group for new devices */ defaultGroupId?: string; /** Require manual approval for enrollment */ requireApproval?: boolean; /** Custom enrollment validation */ validate?: (request: EnrollmentRequest) => Promise; /** * Phase 2b device-pinned-key configuration. Optional — when * omitted, enrollment continues to accept the Phase 2a HMAC path * exclusively, matching pre-0.9 behaviour. */ pinnedKey?: PinnedKeyConfig; } /** * Device-pinned-key enrollment options. See * `docs/concepts/enrollment` for the full flow. */ export interface PinnedKeyConfig { /** * Require every new enrollment to use the pinned-key path. When * `true`, requests without `publicKey` are rejected. When `false` * (the default), both paths coexist during rollout — the server * pins a public key when one is provided, falls back to HMAC when * it isn't. */ required?: boolean; /** * TTL for enrollment challenges, in seconds. Defaults to 300 * (5 minutes). Challenges are single-use; this only bounds how * long an unused challenge stays valid. */ challengeTtlSeconds?: number; } // ============================================ // Adapter Interfaces // ============================================ export interface DatabaseAdapter { // Devices findDevice(id: string): Promise; findDeviceByEnrollmentId(enrollmentId: string): Promise; listDevices(filter?: DeviceFilter): Promise; createDevice(data: CreateDeviceInput): Promise; updateDevice(id: string, data: UpdateDeviceInput): Promise; deleteDevice(id: string): Promise; countDevices(filter?: DeviceFilter): Promise; // Policies findPolicy(id: string): Promise; findDefaultPolicy(): Promise; listPolicies(): Promise; createPolicy(data: CreatePolicyInput): Promise; updatePolicy(id: string, data: UpdatePolicyInput): Promise; deletePolicy(id: string): Promise; // Applications findApplication(id: string): Promise; findApplicationByPackage(packageName: string, version?: string): Promise; listApplications(activeOnly?: boolean): Promise; createApplication(data: CreateApplicationInput): Promise; updateApplication(id: string, data: UpdateApplicationInput): Promise; deleteApplication(id: string): Promise; // Commands findCommand(id: string): Promise; listCommands(filter?: CommandFilter): Promise; createCommand(data: SendCommandInput): Promise; updateCommand(id: string, data: Partial): Promise; getPendingCommands(deviceId: string): Promise; // Events createEvent(event: Omit): Promise; listEvents(filter?: EventFilter): Promise; // Groups findGroup(id: string): Promise; listGroups(): Promise; createGroup(data: CreateGroupInput): Promise; updateGroup(id: string, data: UpdateGroupInput): Promise; deleteGroup(id: string): Promise; listDevicesInGroup(groupId: string): Promise; addDeviceToGroup(deviceId: string, groupId: string): Promise; removeDeviceFromGroup(deviceId: string, groupId: string): Promise; getDeviceGroups(deviceId: string): Promise; // Push Tokens findPushToken(deviceId: string, provider: string): Promise; upsertPushToken(data: RegisterPushTokenInput): Promise; deletePushToken(deviceId: string, provider?: string): Promise; // App Versions (optional - for version tracking) listAppVersions?(packageName: string): Promise; createAppVersion?(data: Omit): Promise; setMinimumVersion?(packageName: string, versionCode: number): Promise; getMinimumVersion?(packageName: string): Promise; // Rollback History (optional) createRollback?(data: CreateAppRollbackInput): Promise; updateRollback?(id: string, data: Partial): Promise; listRollbacks?(filter?: { deviceId?: string; packageName?: string }): Promise; // Group Hierarchy (optional) getGroupChildren?(parentId: string | null): Promise; getGroupAncestors?(groupId: string): Promise; getGroupDescendants?(groupId: string): Promise; getGroupTree?(rootId?: string): Promise; getGroupEffectivePolicy?(groupId: string): Promise; moveGroup?(groupId: string, newParentId: string | null): Promise; getGroupHierarchyStats?(): Promise; // Enrollment challenges (optional - for Phase 2b device-pinned-key) /** * Persist a new single-use enrollment challenge. The adapter * should store it with `consumed_at = null` and enforce a * primary-key constraint on `challenge` so duplicate inserts * fail loudly. */ createEnrollmentChallenge?(challenge: EnrollmentChallenge): Promise; /** * Look up a challenge by its opaque value. Returns `null` if not * found. Does NOT filter on expiry — the core layer checks * freshness so the adapter stays dumb. */ findEnrollmentChallenge?(challenge: string): Promise; /** * Atomically mark a challenge as consumed. Must set * `consumed_at = now()` and return the updated row only when the * challenge was previously unused. Adapters should implement * this as a conditional UPDATE (e.g. Postgres * `UPDATE ... WHERE consumed_at IS NULL RETURNING *`) so two * concurrent consume attempts cannot both succeed. */ consumeEnrollmentChallenge?(challenge: string): Promise; /** * Delete expired, unconsumed challenges. Called periodically by * the core layer; adapters can no-op if they rely on a TTL index * elsewhere. */ pruneExpiredEnrollmentChallenges?(now: Date): Promise; // Tenants (optional - for multi-tenancy) findTenant?(id: string): Promise; findTenantBySlug?(slug: string): Promise; listTenants?(filter?: TenantFilter): Promise; createTenant?(data: CreateTenantInput): Promise; updateTenant?(id: string, data: UpdateTenantInput): Promise; deleteTenant?(id: string): Promise; getTenantStats?(tenantId: string): Promise; // Users (optional - for RBAC) findUser?(id: string): Promise; findUserByEmail?(email: string, tenantId?: string): Promise; listUsers?(filter?: UserFilter): Promise; createUser?(data: CreateUserInput): Promise; updateUser?(id: string, data: UpdateUserInput): Promise; deleteUser?(id: string): Promise; // Roles (optional - for RBAC) findRole?(id: string): Promise; listRoles?(tenantId?: string): Promise; createRole?(data: CreateRoleInput): Promise; updateRole?(id: string, data: UpdateRoleInput): Promise; deleteRole?(id: string): Promise; assignRoleToUser?(userId: string, roleId: string): Promise; removeRoleFromUser?(userId: string, roleId: string): Promise; getUserRoles?(userId: string): Promise; // Audit Logs (optional - for compliance) createAuditLog?(data: CreateAuditLogInput): Promise; listAuditLogs?(filter?: AuditLogFilter): Promise; deleteAuditLogs?(filter: { olderThan?: Date; tenantId?: string }): Promise; // Scheduled Tasks (optional - for scheduling) findScheduledTask?(id: string): Promise; listScheduledTasks?(filter?: ScheduledTaskFilter): Promise; createScheduledTask?(data: CreateScheduledTaskInput): Promise; updateScheduledTask?(id: string, data: UpdateScheduledTaskInput): Promise; deleteScheduledTask?(id: string): Promise; getUpcomingTasks?(hours: number): Promise; createTaskExecution?(data: { taskId: string }): Promise; updateTaskExecution?(id: string, data: Partial): Promise; listTaskExecutions?(taskId: string, limit?: number): Promise; // Message Queue (optional - for persistent messaging) enqueueMessage?(data: EnqueueMessageInput): Promise; dequeueMessages?(deviceId: string, limit?: number): Promise; peekMessages?(deviceId: string, limit?: number): Promise; acknowledgeMessage?(messageId: string): Promise; failMessage?(messageId: string, error: string): Promise; retryFailedMessages?(maxAttempts?: number): Promise; purgeExpiredMessages?(): Promise; getQueueStats?(tenantId?: string): Promise; // Plugin Storage (optional) getPluginValue?(pluginName: string, key: string): Promise; setPluginValue?(pluginName: string, key: string, value: unknown): Promise; deletePluginValue?(pluginName: string, key: string): Promise; listPluginKeys?(pluginName: string, prefix?: string): Promise; clearPluginData?(pluginName: string): Promise; // Dashboard (optional - for analytics) getDashboardStats?(tenantId?: string): Promise; getDeviceStatusBreakdown?(tenantId?: string): Promise; getEnrollmentTrend?(days: number, tenantId?: string): Promise; getCommandSuccessRates?(tenantId?: string): Promise; getAppInstallationSummary?(tenantId?: string): Promise; // Transactions (optional) transaction?(fn: () => Promise): Promise; } export interface PushAdapter { /** Send push message to a device */ send(deviceId: string, message: PushMessage): Promise; /** Send push message to multiple devices */ sendBatch(deviceIds: string[], message: PushMessage): Promise; /** Register device push token */ registerToken?(deviceId: string, token: string): Promise; /** Unregister device push token */ unregisterToken?(deviceId: string): Promise; /** Subscribe device to topic */ subscribe?(deviceId: string, topic: string): Promise; /** Unsubscribe device from topic */ unsubscribe?(deviceId: string, topic: string): Promise; } export interface PushMessage { type: string; payload?: Record; priority?: 'high' | 'normal'; ttl?: number; collapseKey?: string; } export interface PushResult { success: boolean; messageId?: string; error?: string; } export interface PushBatchResult { successCount: number; failureCount: number; results: Array<{ deviceId: string; result: PushResult }>; } // ============================================ // Plugin Interface // ============================================ export interface MDMPlugin { /** Unique plugin name */ name: string; /** Plugin version */ version: string; /** Called when MDM is initialized */ onInit?(mdm: MDMInstance): Promise; /** Called when MDM is destroyed */ onDestroy?(): Promise; /** Additional routes to mount */ routes?: PluginRoute[]; /** Middleware to apply to all routes */ middleware?: PluginMiddleware[]; /** Extend enrollment process */ onEnroll?(device: Device, request: EnrollmentRequest): Promise; /** Extend device processing */ onDeviceEnrolled?(device: Device): Promise; onDeviceUnenrolled?(device: Device): Promise; onHeartbeat?(device: Device, heartbeat: Heartbeat): Promise; /** Extend policy processing */ policySchema?: Record; validatePolicy?(settings: PolicySettings): Promise<{ valid: boolean; errors?: string[] }>; applyPolicy?(device: Device, policy: Policy): Promise; /** Extend command processing */ commandTypes?: CommandType[]; executeCommand?(device: Device, command: Command): Promise; } export interface PluginRoute { method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; path: string; handler: (context: unknown) => Promise; auth?: boolean; admin?: boolean; } export type PluginMiddleware = ( context: unknown, next: () => Promise ) => Promise; // ============================================ // MDM Instance Interface // ============================================ export interface WebhookManager { /** Deliver an event to all matching webhook endpoints */ deliver(event: MDMEvent): Promise; /** Add a webhook endpoint at runtime */ addEndpoint(endpoint: WebhookEndpoint): void; /** Remove a webhook endpoint */ removeEndpoint(endpointId: string): void; /** Update a webhook endpoint */ updateEndpoint(endpointId: string, updates: Partial): void; /** Get all configured endpoints */ getEndpoints(): WebhookEndpoint[]; /** Test a webhook endpoint with a test payload */ testEndpoint(endpointId: string): Promise; } export interface WebhookDeliveryResult { endpointId: string; success: boolean; statusCode?: number; error?: string; retryCount: number; deliveredAt?: Date; } export interface MDMInstance { /** Device management */ devices: DeviceManager; /** Policy management */ policies: PolicyManager; /** Application management */ apps: ApplicationManager; /** Command management */ commands: CommandManager; /** Group management */ groups: GroupManager; /** Tenant management (if multi-tenancy enabled) */ tenants?: TenantManager; /** Authorization management (RBAC) */ authorization?: AuthorizationManager; /** Audit logging */ audit?: AuditManager; /** Scheduled task management */ schedules?: ScheduleManager; /** Persistent message queue */ messageQueue?: MessageQueueManager; /** Dashboard analytics */ dashboard?: DashboardManager; /** Plugin storage */ pluginStorage?: PluginStorageAdapter; /** Push notification service */ push: PushAdapter; /** Webhook delivery (if configured) */ webhooks?: WebhookManager; /** Database adapter */ db: DatabaseAdapter; /** Structured logger. Already scoped to the `openmdm` namespace. Plugins should call `.child({...})` to scope further. */ logger: Logger; /** Configuration */ config: MDMConfig; /** Subscribe to events */ on(event: T, handler: EventHandler): () => void; /** Emit an event */ emit(event: T, data: EventPayloadMap[T]): Promise; /** Process device enrollment */ enroll(request: EnrollmentRequest): Promise; /** Process device heartbeat */ processHeartbeat(deviceId: string, heartbeat: Heartbeat): Promise; /** Verify device token */ verifyDeviceToken(token: string): Promise<{ deviceId: string } | null>; /** Get loaded plugins */ getPlugins(): MDMPlugin[]; /** Get plugin by name */ getPlugin(name: string): MDMPlugin | undefined; } // ============================================ // Manager Interfaces // ============================================ export interface DeviceManager { get(id: string): Promise; getByEnrollmentId(enrollmentId: string): Promise; list(filter?: DeviceFilter): Promise; create(data: CreateDeviceInput): Promise; update(id: string, data: UpdateDeviceInput): Promise; delete(id: string): Promise; assignPolicy(deviceId: string, policyId: string | null): Promise; addToGroup(deviceId: string, groupId: string): Promise; removeFromGroup(deviceId: string, groupId: string): Promise; getGroups(deviceId: string): Promise; sendCommand(deviceId: string, input: Omit): Promise; sync(deviceId: string): Promise; reboot(deviceId: string): Promise; lock(deviceId: string, message?: string): Promise; wipe(deviceId: string, preserveData?: boolean): Promise; } export interface PolicyManager { get(id: string): Promise; getDefault(): Promise; list(): Promise; create(data: CreatePolicyInput): Promise; update(id: string, data: UpdatePolicyInput): Promise; delete(id: string): Promise; setDefault(id: string): Promise; getDevices(policyId: string): Promise; applyToDevice(policyId: string, deviceId: string): Promise; } export interface ApplicationManager { get(id: string): Promise; getByPackage(packageName: string, version?: string): Promise; list(activeOnly?: boolean): Promise; register(data: CreateApplicationInput): Promise; update(id: string, data: UpdateApplicationInput): Promise; delete(id: string): Promise; activate(id: string): Promise; deactivate(id: string): Promise; deploy(packageName: string, target: DeployTarget): Promise; installOnDevice(packageName: string, deviceId: string, version?: string): Promise; uninstallFromDevice(packageName: string, deviceId: string): Promise; } export interface CommandManager { get(id: string): Promise; list(filter?: CommandFilter): Promise; send(input: SendCommandInput): Promise; cancel(id: string): Promise; acknowledge(id: string): Promise; complete(id: string, result: CommandResult): Promise; fail(id: string, error: string): Promise; getPending(deviceId: string): Promise; } export interface GroupManager { // Basic CRUD operations get(id: string): Promise; list(): Promise; create(data: CreateGroupInput): Promise; update(id: string, data: UpdateGroupInput): Promise; delete(id: string): Promise; // Device management getDevices(groupId: string): Promise; addDevice(groupId: string, deviceId: string): Promise; removeDevice(groupId: string, deviceId: string): Promise; // Hierarchy operations getChildren(groupId: string): Promise; getTree(rootId?: string): Promise; getAncestors(groupId: string): Promise; getDescendants(groupId: string): Promise; move(groupId: string, newParentId: string | null): Promise; getEffectivePolicy(groupId: string): Promise; getHierarchyStats(): Promise; } // ============================================ // Group Hierarchy Types // ============================================ export interface GroupTreeNode extends Group { children: GroupTreeNode[]; depth: number; path: string[]; effectivePolicyId?: string | null; } export interface GroupHierarchyStats { totalGroups: number; maxDepth: number; groupsWithDevices: number; groupsWithPolicies: number; } // ============================================ // Tenant Types (Multi-tenancy) // ============================================ export type TenantStatus = 'active' | 'suspended' | 'pending'; export interface Tenant { id: string; name: string; slug: string; status: TenantStatus; settings?: TenantSettings | null; metadata?: Record | null; createdAt: Date; updatedAt: Date; } export interface TenantSettings { maxDevices?: number; maxUsers?: number; features?: string[]; branding?: { logo?: string; primaryColor?: string; }; } export interface CreateTenantInput { name: string; slug: string; settings?: TenantSettings; metadata?: Record; } export interface UpdateTenantInput { name?: string; slug?: string; status?: TenantStatus; settings?: TenantSettings; metadata?: Record; } export interface TenantFilter { status?: TenantStatus; search?: string; limit?: number; offset?: number; } export interface TenantListResult { tenants: Tenant[]; total: number; limit: number; offset: number; } export interface TenantStats { deviceCount: number; userCount: number; policyCount: number; appCount: number; } // ============================================ // RBAC Types (Role-Based Access Control) // ============================================ export type PermissionAction = 'create' | 'read' | 'update' | 'delete' | 'manage' | '*'; export type PermissionResource = 'devices' | 'policies' | 'apps' | 'groups' | 'commands' | 'users' | 'roles' | 'tenants' | 'audit' | '*'; export interface Permission { action: PermissionAction; resource: PermissionResource; resourceId?: string; } export interface Role { id: string; tenantId?: string | null; name: string; description?: string | null; permissions: Permission[]; isSystem: boolean; createdAt: Date; updatedAt: Date; } export interface CreateRoleInput { tenantId?: string; name: string; description?: string; permissions: Permission[]; } export interface UpdateRoleInput { name?: string; description?: string; permissions?: Permission[]; } export interface User { id: string; tenantId?: string | null; email: string; name?: string | null; status: 'active' | 'inactive' | 'pending'; metadata?: Record | null; lastLoginAt?: Date | null; createdAt: Date; updatedAt: Date; } export interface UserWithRoles extends User { roles: Role[]; } export interface CreateUserInput { tenantId?: string; email: string; name?: string; status?: 'active' | 'inactive' | 'pending'; metadata?: Record; } export interface UpdateUserInput { email?: string; name?: string; status?: 'active' | 'inactive' | 'pending'; metadata?: Record; } export interface UserFilter { tenantId?: string; status?: 'active' | 'inactive' | 'pending'; search?: string; limit?: number; offset?: number; } export interface UserListResult { users: User[]; total: number; limit: number; offset: number; } // ============================================ // Audit Types // ============================================ export type AuditAction = 'create' | 'read' | 'update' | 'delete' | 'login' | 'logout' | 'enroll' | 'unenroll' | 'command' | 'export' | 'import' | 'custom'; export interface AuditLog { id: string; tenantId?: string | null; userId?: string | null; action: AuditAction; resource: string; resourceId?: string | null; status: 'success' | 'failure'; error?: string | null; details?: Record | null; ipAddress?: string | null; userAgent?: string | null; createdAt: Date; } export interface CreateAuditLogInput { tenantId?: string; userId?: string; action: AuditAction; resource: string; resourceId?: string; status?: 'success' | 'failure'; error?: string; details?: Record; ipAddress?: string; userAgent?: string; } export interface AuditConfig { enabled: boolean; retentionDays?: number; skipReadOperations?: boolean; logActions?: AuditAction[]; logResources?: string[]; } export interface AuditSummary { totalLogs: number; byAction: Record; byResource: Record; byStatus: { success: number; failure: number }; topUsers: Array<{ userId: string; count: number }>; recentFailures: AuditLog[]; } export interface AuditLogFilter { tenantId?: string; userId?: string; action?: string; resource?: string; resourceId?: string; startDate?: Date; endDate?: Date; limit?: number; offset?: number; } export interface AuditLogListResult { logs: AuditLog[]; total: number; limit: number; offset: number; } // ============================================ // Schedule Types // ============================================ export type TaskType = 'command' | 'policy_update' | 'app_install' | 'maintenance' | 'custom'; export type ScheduledTaskStatus = 'active' | 'paused' | 'completed' | 'failed'; export interface MaintenanceWindow { daysOfWeek: number[]; startTime: string; endTime: string; timezone: string; } export interface TaskSchedule { type: 'once' | 'recurring' | 'window'; executeAt?: Date; cron?: string; window?: MaintenanceWindow; } export interface ScheduledTask { id: string; tenantId?: string | null; name: string; description?: string | null; taskType: TaskType; schedule: TaskSchedule; target?: DeployTarget; payload?: Record | null; status: ScheduledTaskStatus; nextRunAt?: Date | null; lastRunAt?: Date | null; maxRetries: number; retryCount: number; createdAt: Date; updatedAt: Date; } export interface CreateScheduledTaskInput { tenantId?: string; name: string; description?: string; taskType: TaskType; schedule: TaskSchedule; target?: DeployTarget; payload?: Record; maxRetries?: number; } export interface UpdateScheduledTaskInput { name?: string; description?: string; schedule?: TaskSchedule; target?: DeployTarget; payload?: Record; status?: ScheduledTaskStatus; maxRetries?: number; } export interface ScheduledTaskFilter { tenantId?: string; taskType?: TaskType | TaskType[]; status?: ScheduledTaskStatus | ScheduledTaskStatus[]; limit?: number; offset?: number; } export interface ScheduledTaskListResult { tasks: ScheduledTask[]; total: number; limit: number; offset: number; } export interface TaskExecution { id: string; taskId: string; status: 'running' | 'completed' | 'failed'; startedAt: Date; completedAt?: Date | null; devicesProcessed: number; devicesSucceeded: number; devicesFailed: number; error?: string | null; details?: Record | null; } // ============================================ // Message Queue Types // ============================================ export type QueueMessageStatus = 'pending' | 'processing' | 'delivered' | 'failed' | 'expired'; export interface QueuedMessage { id: string; tenantId?: string | null; deviceId: string; messageType: string; payload: Record; priority: 'high' | 'normal' | 'low'; status: QueueMessageStatus; attempts: number; maxAttempts: number; lastAttemptAt?: Date | null; lastError?: string | null; expiresAt?: Date | null; createdAt: Date; updatedAt: Date; } export interface EnqueueMessageInput { tenantId?: string; deviceId: string; messageType: string; payload: Record; priority?: 'high' | 'normal' | 'low'; maxAttempts?: number; ttlSeconds?: number; } export interface QueueStats { pending: number; processing: number; delivered: number; failed: number; expired: number; byDevice: Record; oldestPending?: Date; } // ============================================ // Dashboard Types // ============================================ export interface DashboardStats { devices: { total: number; enrolled: number; active: number; blocked: number; pending: number; }; policies: { total: number; deployed: number; }; applications: { total: number; deployed: number; }; commands: { pendingCount: number; last24hTotal: number; last24hSuccess: number; last24hFailed: number; }; groups: { total: number; withDevices: number; }; } export interface DeviceStatusBreakdown { byStatus: Record; byOs: Record; byManufacturer: Record; byModel: Record; } export interface EnrollmentTrendPoint { date: Date; enrolled: number; unenrolled: number; netChange: number; totalDevices: number; } export interface CommandSuccessRates { overall: { total: number; completed: number; failed: number; successRate: number; }; byType: Record; last24h: { total: number; completed: number; failed: number; pending: number; }; } export interface AppInstallationSummary { total: number; byStatus: Record; recentFailures: Array<{ packageName: string; deviceId: string; error: string; timestamp: Date; }>; topInstalled: Array<{ packageName: string; name: string; installedCount: number; }>; } // ============================================ // Plugin Storage Types // ============================================ export interface PluginStorageAdapter { get(pluginName: string, key: string): Promise; set(pluginName: string, key: string, value: T): Promise; delete(pluginName: string, key: string): Promise; list(pluginName: string, prefix?: string): Promise; clear(pluginName: string): Promise; } export interface PluginStorageEntry { pluginName: string; key: string; value: unknown; createdAt: Date; updatedAt: Date; } // ============================================ // Enterprise Manager Interfaces // ============================================ export interface TenantManager { get(id: string): Promise; getBySlug(slug: string): Promise; list(filter?: TenantFilter): Promise; create(data: CreateTenantInput): Promise; update(id: string, data: UpdateTenantInput): Promise; delete(id: string, cascade?: boolean): Promise; getStats(tenantId: string): Promise; activate(id: string): Promise; deactivate(id: string): Promise; } export interface AuthorizationManager { createRole(data: CreateRoleInput): Promise; getRole(id: string): Promise; listRoles(tenantId?: string): Promise; updateRole(id: string, data: UpdateRoleInput): Promise; deleteRole(id: string): Promise; createUser(data: CreateUserInput): Promise; getUser(id: string): Promise; getUserByEmail(email: string, tenantId?: string): Promise; listUsers(filter?: UserFilter): Promise; updateUser(id: string, data: UpdateUserInput): Promise; deleteUser(id: string): Promise; assignRole(userId: string, roleId: string): Promise; removeRole(userId: string, roleId: string): Promise; getUserRoles(userId: string): Promise; can(userId: string, action: PermissionAction, resource: PermissionResource, resourceId?: string): Promise; canAny(userId: string, permissions: Array<{ action: PermissionAction; resource: PermissionResource }>): Promise; requirePermission(userId: string, action: PermissionAction, resource: PermissionResource, resourceId?: string): Promise; isAdmin(userId: string): Promise; } export interface AuditManager { log(entry: CreateAuditLogInput): Promise; list(filter?: AuditLogFilter): Promise; getByResource(resource: string, resourceId: string): Promise; getByUser(userId: string, filter?: AuditLogFilter): Promise; export(filter: AuditLogFilter, format: 'json' | 'csv'): Promise; purge(olderThanDays?: number): Promise; getSummary(tenantId?: string, days?: number): Promise; } export interface ScheduleManager { get(id: string): Promise; list(filter?: ScheduledTaskFilter): Promise; create(data: CreateScheduledTaskInput): Promise; update(id: string, data: UpdateScheduledTaskInput): Promise; delete(id: string): Promise; pause(id: string): Promise; resume(id: string): Promise; runNow(id: string): Promise; getUpcoming(hours: number): Promise; getExecutions(taskId: string, limit?: number): Promise; calculateNextRun(schedule: TaskSchedule): Date | null; } export interface MessageQueueManager { enqueue(message: EnqueueMessageInput): Promise; enqueueBatch(messages: EnqueueMessageInput[]): Promise; dequeue(deviceId: string, limit?: number): Promise; acknowledge(messageId: string): Promise; fail(messageId: string, error: string): Promise; retryFailed(maxAttempts?: number): Promise; purgeExpired(): Promise; getStats(tenantId?: string): Promise; peek(deviceId: string, limit?: number): Promise; } export interface DashboardManager { getStats(tenantId?: string): Promise; getDeviceStatusBreakdown(tenantId?: string): Promise; getEnrollmentTrend(days: number, tenantId?: string): Promise; getCommandSuccessRates(tenantId?: string): Promise; getAppInstallationSummary(tenantId?: string): Promise; } // ============================================ // Event Handler Types // ============================================ export type EventHandler = ( event: MDMEvent ) => Promise | void; export interface EventPayloadMap { 'device.enrolled': { device: Device }; 'device.unenrolled': { device: Device; reason?: string }; 'device.blocked': { device: Device; reason: string }; 'device.heartbeat': { device: Device; heartbeat: Heartbeat }; 'device.locationUpdated': { device: Device; location: DeviceLocation }; 'device.statusChanged': { device: Device; oldStatus: DeviceStatus; newStatus: DeviceStatus }; 'device.policyChanged': { device: Device; oldPolicyId?: string; newPolicyId?: string }; 'app.installed': { device: Device; app: InstalledApp }; 'app.uninstalled': { device: Device; packageName: string }; 'app.updated': { device: Device; app: InstalledApp; oldVersion: string }; 'app.crashed': { device: Device; packageName: string; error?: string }; 'app.started': { device: Device; packageName: string }; 'app.stopped': { device: Device; packageName: string }; 'policy.applied': { device: Device; policy: Policy }; 'policy.failed': { device: Device; policy: Policy; error: string }; 'command.received': { device: Device; command: Command }; 'command.acknowledged': { device: Device; command: Command }; 'command.completed': { device: Device; command: Command; result: CommandResult }; 'command.failed': { device: Device; command: Command; error: string }; 'security.tamper': { device: Device; type: string; details?: unknown }; 'security.rootDetected': { device: Device }; 'security.screenLocked': { device: Device }; 'security.screenUnlocked': { device: Device }; custom: Record; } // ============================================ // Error Types // ============================================ export class MDMError extends Error { constructor( message: string, public code: string, public statusCode: number = 500, public details?: unknown ) { super(message); this.name = 'MDMError'; } } export class DeviceNotFoundError extends MDMError { constructor(deviceId: string) { super(`Device not found: ${deviceId}`, 'DEVICE_NOT_FOUND', 404); } } export class PolicyNotFoundError extends MDMError { constructor(policyId: string) { super(`Policy not found: ${policyId}`, 'POLICY_NOT_FOUND', 404); } } export class ApplicationNotFoundError extends MDMError { constructor(identifier: string) { super(`Application not found: ${identifier}`, 'APPLICATION_NOT_FOUND', 404); } } export class CommandNotFoundError extends MDMError { constructor(commandId: string) { super(`Command not found: ${commandId}`, 'COMMAND_NOT_FOUND', 404); } } export class TenantNotFoundError extends MDMError { constructor(identifier: string) { super(`Tenant not found: ${identifier}`, 'TENANT_NOT_FOUND', 404); } } export class RoleNotFoundError extends MDMError { constructor(identifier: string) { super(`Role not found: ${identifier}`, 'ROLE_NOT_FOUND', 404); } } export class GroupNotFoundError extends MDMError { constructor(identifier: string) { super(`Group not found: ${identifier}`, 'GROUP_NOT_FOUND', 404); } } export class UserNotFoundError extends MDMError { constructor(identifier: string) { super(`User not found: ${identifier}`, 'USER_NOT_FOUND', 404); } } export class EnrollmentError extends MDMError { constructor(message: string, details?: unknown) { super(message, 'ENROLLMENT_ERROR', 400, details); } } export class AuthenticationError extends MDMError { constructor(message: string = 'Authentication required') { super(message, 'AUTHENTICATION_ERROR', 401); } } export class AuthorizationError extends MDMError { constructor(message: string = 'Access denied') { super(message, 'AUTHORIZATION_ERROR', 403); } } export class ValidationError extends MDMError { constructor(message: string, details?: unknown) { super(message, 'VALIDATION_ERROR', 400, details); } }