import { WebhookConfig, Logger, MDMEvent, WebhookEndpoint, EventType, MDMInstance, DeviceIdentityVerification, DatabaseAdapter, TenantManager, AuthorizationManager, AuditConfig, AuditManager, ScheduleManager, MessageQueueManager, DashboardManager, PluginStorageAdapter, MDMConfig, EnrollmentRequest } from './types.js'; export { AppInstallationSummary, AppRollback, AppVersion, Application, ApplicationManager, ApplicationNotFoundError, AuditAction, AuditLog, AuditLogFilter, AuditLogListResult, AuditSummary, AuthConfig, AuthenticationError, AuthorizationError, Command, CommandFilter, CommandManager, CommandNotFoundError, CommandResult, CommandStatus, CommandSuccessRates, CommandType, CreateAppRollbackInput, CreateApplicationInput, CreateAuditLogInput, CreateDeviceInput, CreateGroupInput, CreatePolicyInput, CreateRoleInput, CreateScheduledTaskInput, CreateTenantInput, CreateUserInput, DashboardStats, DeployTarget, Device, DeviceFilter, DeviceListResult, DeviceLocation, DeviceManager, DeviceNotFoundError, DeviceStatus, DeviceStatusBreakdown, EnqueueMessageInput, EnrollmentChallenge, EnrollmentConfig, EnrollmentError, EnrollmentMethod, EnrollmentResponse, EnrollmentTrendPoint, EventFilter, EventHandler, EventPayloadMap, Group, GroupHierarchyStats, GroupManager, GroupNotFoundError, GroupTreeNode, HardwareControl, Heartbeat, InstalledApp, LogContext, MDMError, MDMPlugin, MaintenanceWindow, PasswordPolicy, Permission, PermissionAction, PermissionResource, PinnedKeyConfig, PluginMiddleware, PluginRoute, PluginStorageEntry, Policy, PolicyApplication, PolicyManager, PolicyNotFoundError, PolicySettings, PushAdapter, PushBatchResult, PushConfig, PushMessage, PushProviderConfig, PushResult, PushToken, QueueMessageStatus, QueueStats, QueuedMessage, RegisterPushTokenInput, Role, RoleNotFoundError, ScheduledTask, ScheduledTaskFilter, ScheduledTaskListResult, ScheduledTaskStatus, SendCommandInput, StorageConfig, SystemUpdatePolicy, TaskExecution, TaskSchedule, TaskType, Tenant, TenantFilter, TenantListResult, TenantNotFoundError, TenantSettings, TenantStats, TenantStatus, TimeWindow, UpdateApplicationInput, UpdateDeviceInput, UpdateGroupInput, UpdatePolicyInput, UpdateRoleInput, UpdateScheduledTaskInput, UpdateTenantInput, UpdateUserInput, User, UserFilter, UserListResult, UserNotFoundError, UserWithRoles, ValidationError, VpnConfig, WebhookDeliveryResult, WebhookManager, WifiConfig } from './types.js'; export { ColumnDefinition, ColumnType, IndexDefinition, SchemaDefinition, TableDefinition, camelToSnake, getColumnNames, getPrimaryKey, getTableNames, mdmSchema, snakeToCamel, transformToCamelCase, transformToSnakeCase } from './schema.js'; import { KeyObject } from 'crypto'; /** * OpenMDM Agent Wire Protocol v2. * * A unified response envelope for every `/agent/*` endpoint, plus the * version-selection rules that let the server serve v1 and v2 clients * simultaneously during a fleet rollout. * * ## Background * * Until now, agent-facing handlers returned either a bare JSON body * on success or raised an `HTTPException(401|404|5xx)` on failure. * The agent had to interpret five different HTTP status codes and * infer what to do about each — which in practice meant "on auth * error, wipe local enrollment state and re-enroll". That single * ambiguity produced the auto-unenroll behavior we saw in production: * a transient 401 or 404 was indistinguishable from "you are really * unenrolled", so the agent self-destructed. * * ## Protocol v2 * * Every agent-facing endpoint replies with HTTP 200 and a body of * shape {@link AgentResponse}: * * ```json * { "ok": true, "action": "none", "data": { ... } } * { "ok": false, "action": "retry", "message": "..." } * { "ok": false, "action": "reauth", "message": "..." } * { "ok": false, "action": "unenroll", "message": "..." } * ``` * * - `ok` is the boolean the agent checks first. * - `action` is the *only* field the agent reads to decide what to do * next. There is exactly one handler per action on the client, so * adding a new server response path is a matter of picking an * existing action. * - `data` carries the handler-specific payload (heartbeat response, * policy update, etc.) on success. * - `message` is a human-readable hint, for logs. * * HTTP 5xx is still used for real infrastructure failures (the Lambda * timed out, the database connection dropped, etc.). v2 envelopes are * reserved for *application-level* failures the agent can reason about. * * ## Versioning and rollout * * The agent opts into v2 by sending the header * `X-Openmdm-Protocol: 2` on every request. When absent, the server * falls back to the legacy v1 behavior — bare JSON on success, * `HTTPException(401|404|…)` on failure — so a fleet still running * older APKs keeps working during rollout. * * After the fleet has been upgraded, v1 can be dropped in a future * major release by ignoring the header and always emitting v2. */ /** * Instruction the server gives the agent on how to react to this * response. This is the entire client-side decision space. * * - `none`: happy path. The agent consumes `data` and continues. * - `retry`: transient problem. The agent re-tries later without * touching local state. * - `reauth`: the agent's access token is no longer valid. It should * call the refresh flow. It must NOT wipe enrollment state. * - `unenroll`: the server-side record for this device is gone or * blocked and the agent's credentials will never work again. The * agent should stop making requests and surface this to the user. * In Phase 2b this will be further softened: the agent will attempt * a hardware-identity-based rebind before treating this as terminal. */ type AgentAction = 'none' | 'retry' | 'reauth' | 'unenroll'; /** * Unified response envelope for every `/agent/*` endpoint under * protocol v2. * * Successful responses carry `data`; failure responses carry * `message`. The envelope never carries both the happy-path payload * and an error hint at the same time. */ type AgentResponse = { ok: true; action: 'none'; data: T; } | { ok: false; action: Exclude; message?: string; }; /** * HTTP header an agent sends to opt into protocol v2. Case-insensitive * on the wire; use the constant to avoid typos. */ declare const AGENT_PROTOCOL_HEADER = "X-Openmdm-Protocol"; /** * Current wire-protocol version. Agents that send * `X-Openmdm-Protocol: 2` get envelope responses. Absent or older * values are served with the legacy flat shape. */ declare const AGENT_PROTOCOL_V2 = "2"; /** * Helper: build a success envelope. */ declare function agentOk(data: T): AgentResponse; /** * Helper: build a failure envelope. */ declare function agentFail(action: Exclude, message?: string): AgentResponse; /** * Returns `true` iff the caller should be served protocol v2. The * input is the value of the {@link AGENT_PROTOCOL_HEADER} header, * which may be undefined. */ declare function wantsAgentProtocolV2(headerValue: string | undefined | null): boolean; /** * OpenMDM Webhook Delivery System * * Handles outbound webhook delivery with HMAC signing and retry logic. */ interface WebhookDeliveryResult { endpointId: string; success: boolean; statusCode?: number; error?: string; retryCount: number; deliveredAt?: Date; } interface WebhookPayload { id: string; event: EventType; timestamp: string; data: T; } 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; } /** * Create a webhook manager instance */ declare function createWebhookManager(config: WebhookConfig, logger?: Logger): WebhookManager; /** * Verify a webhook signature from incoming requests * (Utility for consumers to verify our webhooks) */ declare function verifyWebhookSignature(payload: string, signature: string, secret: string): boolean; /** * OpenMDM Logger * * Default logger implementations and helpers. Production users are * expected to pass their own pino/winston/bunyan instance via * `createMDM({ logger })`; these defaults are for development and * for the zero-config path. */ /** * Console-backed logger. Writes JSON-ish lines to stdout/stderr with * an `[openmdm]` prefix so they stand out in a mixed-log stream. * * This is the zero-config default — it intentionally does the * minimum viable thing. Hosts running in production should replace * it with a real structured logger. */ declare function createConsoleLogger(scope?: string[]): Logger; /** * No-op logger. Use to silence OpenMDM entirely — e.g. in tests or in * environments where log noise is inappropriate. */ declare function createSilentLogger(): Logger; /** * OpenMDM Device Identity * * Device-pinned asymmetric identity, using an ECDSA P-256 keypair the * device generates in its own Keystore and registers with the server on * first enrollment. After pinning, every consumer can verify a signed * request against the same pinned public key — no shared HMAC secret, * no APK extraction footgun, no dependence on Google hardware * attestation (which most non-GMS fleet hardware cannot produce). * * This module is the reusable primitive. `@openmdm/core` uses it to * gate `/agent/enroll` and will use it for `/agent/*` in Phase 2c. * External consumers (midiamob's `deviceValidation.ts`, other custom * servers) import the same functions to verify requests against the * same pinned key — one device identity, many consumers. * * Why zero dependencies: Node's built-in `node:crypto` supports EC * P-256 SPKI import and `crypto.verify('sha256', ...)` over DER-encoded * signatures, which is the default format the Android Keystore * produces. We deliberately do not pull in `@peculiar/*` or `node-forge` * for this primitive — the surface area we need is small enough that * the built-in is the right call. * * @see docs/concepts/enrollment for the full flow * @see docs/proposals/phase-2b-rollout for the Android + rollout story */ /** * Import an EC P-256 public key from base64-encoded SubjectPublicKeyInfo * (SPKI) bytes — the standard on-wire format the Android Keystore * produces when you call `certificate.publicKey.encoded` on a * `KeyStore.getCertificate(alias)` result. * * Throws `InvalidPublicKeyError` on any parse failure. This is a * security boundary — we do NOT return `null` on malformed input, * because a caller that forgot to handle the null case would silently * treat bad keys as "no key configured" and fall through to an * insecure path. */ declare function importPublicKeyFromSpki(spkiBase64: string): KeyObject; /** * Verify an ECDSA-P256 signature over a message using a previously- * imported or raw SPKI public key. * * Signature must be DER-encoded — the default Android Keystore * produces DER, and `Signature.sign()` on JVM/Kotlin returns DER, so * this matches what every reasonable agent sends on the wire. * * Returns `true` iff the signature is valid. Never throws on a bad * signature (that is the whole point of a verify call). Throws only * on an invalid public-key encoding, because that indicates a caller * bug rather than a forged request. */ declare function verifyEcdsaSignature(publicKey: KeyObject | string, message: string, signatureBase64: string): boolean; /** * Build the canonical message that an enrollment signature covers. * * Staying in lockstep with `@openmdm/client` and with the Android * agent is load-bearing — any change here is a wire break across * every enrolled device. The contract test in * `packages/core/tests/device-identity.test.ts` guards against drift. * * Shape (order matters): * * publicKey | * model | manufacturer | osVersion | * serialNumber | imei | macAddress | androidId | * method | timestamp | challenge * * The public key is prepended (rather than appended) because it's the * field most likely to be the whole point of the message — putting it * first makes the signature's intent visible at a glance in logs. */ declare function canonicalEnrollmentMessage(parts: { publicKey: string; model: string; manufacturer: string; osVersion: string; serialNumber?: string; imei?: string; macAddress?: string; androidId?: string; method: string; timestamp: string; challenge: string; }): string; /** * Build the canonical message that a *post-enrollment* request * signature covers. Consumers (openmdm's `/agent/*` routes, * midiamob's `deviceValidation.ts`, any custom server) call this * with the fields they want committed to the signature. * * The shape is deliberately narrower than the enrollment form — only * the parts every request has in common. * * deviceId | timestamp | body | nonce * * `nonce` is optional; pass an empty string when the request does not * carry a challenge. Replay protection on non-enrollment traffic is * the caller's job — if your server already has a timestamp window * check, you don't need a nonce per request. */ declare function canonicalDeviceRequestMessage(parts: { deviceId: string; timestamp: string; body: string; nonce?: string; }): string; /** * Verify a signed request from an enrolled device against the * public key pinned on that device's row. * * This is the primitive every consumer of device-pinned-key identity * calls. It performs exactly the checks required to know the request * came from the device that originally enrolled, in constant-ish * time: * * 1. Look up the device by id. * 2. Confirm the device has a pinned public key (refusing silently * if not — a device without a pinned key is still on the legacy * HMAC path and cannot be verified here). * 3. Verify the ECDSA signature over the provided canonical message. * * Returns a tagged union so callers can react to the specific failure * mode: * * - `not-found` — the device id doesn't exist. Almost always a bug * in the caller, or a stolen/revoked device id. * Return 401 to the client. * - `no-pinned-key` — the device is still on the HMAC path. Callers * should fall through to their legacy verifier * (or fail, if the caller has already migrated). * - `signature-invalid` — the signature did not verify against the * pinned key. Return 401. **Do NOT** re-pin the * submitted public key in response to a failure * here — that's how re-pinning becomes a hijack. */ declare function verifyDeviceRequest(opts: { mdm: MDMInstance; deviceId: string; canonicalMessage: string; signatureBase64: string; }): Promise; /** * Thrown when a submitted public key cannot be parsed. This is a * caller-facing error — the device sent something that is not a * well-formed SPKI EC P-256 public key. */ declare class InvalidPublicKeyError extends Error { readonly cause?: Error | undefined; readonly code = "INVALID_PUBLIC_KEY"; constructor(message: string, cause?: Error | undefined); } /** * Thrown when a device attempts to re-enroll with a public key that * does not match the one originally pinned for its enrollment id. * * This is the core "device identity continuity" check. The server * will NEVER automatically re-pin on mismatch — rebinding a device * identity requires an explicit admin action (future work). */ declare class PublicKeyMismatchError extends Error { readonly deviceId: string; readonly code = "PUBLIC_KEY_MISMATCH"; constructor(deviceId: string); } /** * Thrown when an enrollment attempts to use a challenge that is * missing, expired, or already consumed. */ declare class ChallengeInvalidError extends Error { readonly challenge?: string | undefined; readonly code = "CHALLENGE_INVALID"; constructor(message: string, challenge?: string | undefined); } /** * OpenMDM Tenant Manager * * Provides multi-tenancy support for the MDM system. * Enables organization isolation, tenant management, and resource quotas. */ /** * Create a TenantManager instance */ declare function createTenantManager(db: DatabaseAdapter): TenantManager; /** * OpenMDM Authorization Manager * * Provides Role-Based Access Control (RBAC) for the MDM system. * Enables fine-grained permission management for users and resources. */ /** * Create an AuthorizationManager instance */ declare function createAuthorizationManager(db: DatabaseAdapter): AuthorizationManager; /** * OpenMDM Audit Manager * * Provides audit logging for compliance and tracking. * Records all significant operations for security auditing. */ /** * Create an AuditManager instance */ declare function createAuditManager(db: DatabaseAdapter, config?: AuditConfig): AuditManager; /** * OpenMDM Schedule Manager * * Provides scheduled task management for the MDM system. * Enables scheduling of recurring operations, maintenance windows, and one-time tasks. */ /** * Create a ScheduleManager instance */ declare function createScheduleManager(db: DatabaseAdapter): ScheduleManager; /** * OpenMDM Message Queue Manager * * Provides persistent message queue management for the MDM system. * Ensures reliable message delivery with retry and expiration handling. */ /** * Create a MessageQueueManager instance */ declare function createMessageQueueManager(db: DatabaseAdapter): MessageQueueManager; /** * OpenMDM Dashboard Manager * * Provides analytics and statistics for the MDM dashboard. * Aggregates data from devices, commands, and applications. */ /** * Create a DashboardManager instance */ declare function createDashboardManager(db: DatabaseAdapter): DashboardManager; /** * OpenMDM Plugin Storage Manager * * Provides persistent storage for plugin state. * Supports both database-backed and in-memory storage. */ /** * Create a PluginStorageAdapter backed by the database */ declare function createPluginStorageAdapter(db: DatabaseAdapter): PluginStorageAdapter; /** * Create an in-memory PluginStorageAdapter for testing */ declare function createMemoryPluginStorageAdapter(): PluginStorageAdapter; /** * Plugin storage utilities */ /** * Create a namespaced key for plugin storage */ declare function createPluginKey(namespace: string, ...parts: string[]): string; /** * Parse a namespaced key */ declare function parsePluginKey(key: string): { namespace: string; parts: string[]; }; /** * OpenMDM Core * * A flexible, embeddable MDM (Mobile Device Management) SDK. * Inspired by better-auth's design philosophy. * * @example * ```typescript * import { createMDM } from '@openmdm/core'; * import { drizzleAdapter } from '@openmdm/drizzle-adapter'; * * const mdm = createMDM({ * database: drizzleAdapter(db), * enrollment: { * deviceSecret: process.env.DEVICE_HMAC_SECRET!, * autoEnroll: true, * }, * }); * * // Use in your routes * const devices = await mdm.devices.list(); * ``` */ /** * Create an MDM instance with the given configuration. */ declare function createMDM(config: MDMConfig): MDMInstance; declare function verifyEnrollmentSignature(request: EnrollmentRequest, secret: string): boolean; export { AGENT_PROTOCOL_HEADER, AGENT_PROTOCOL_V2, type AgentAction, type AgentResponse, AuditConfig, AuditManager, AuthorizationManager, ChallengeInvalidError, DashboardManager, DatabaseAdapter, DeviceIdentityVerification, EnrollmentRequest, EventType, InvalidPublicKeyError, Logger, MDMConfig, MDMEvent, MDMInstance, MessageQueueManager, PluginStorageAdapter, PublicKeyMismatchError, ScheduleManager, TenantManager, WebhookConfig, WebhookEndpoint, type WebhookPayload, agentFail, agentOk, canonicalDeviceRequestMessage, canonicalEnrollmentMessage, createAuditManager, createAuthorizationManager, createConsoleLogger, createDashboardManager, createMDM, createMemoryPluginStorageAdapter, createMessageQueueManager, createPluginKey, createPluginStorageAdapter, createScheduleManager, createSilentLogger, createTenantManager, createWebhookManager, importPublicKeyFromSpki, parsePluginKey, verifyDeviceRequest, verifyEcdsaSignature, verifyEnrollmentSignature, verifyWebhookSignature, wantsAgentProtocolV2 };