@openmdm/core 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1368 @@
1
+ import { randomUUID, createHmac, timingSafeEqual } from 'crypto';
2
+
3
+ // src/index.ts
4
+
5
+ // src/types.ts
6
+ var MDMError = class extends Error {
7
+ constructor(message, code, statusCode = 500, details) {
8
+ super(message);
9
+ this.code = code;
10
+ this.statusCode = statusCode;
11
+ this.details = details;
12
+ this.name = "MDMError";
13
+ }
14
+ };
15
+ var DeviceNotFoundError = class extends MDMError {
16
+ constructor(deviceId) {
17
+ super(`Device not found: ${deviceId}`, "DEVICE_NOT_FOUND", 404);
18
+ }
19
+ };
20
+ var PolicyNotFoundError = class extends MDMError {
21
+ constructor(policyId) {
22
+ super(`Policy not found: ${policyId}`, "POLICY_NOT_FOUND", 404);
23
+ }
24
+ };
25
+ var ApplicationNotFoundError = class extends MDMError {
26
+ constructor(identifier) {
27
+ super(`Application not found: ${identifier}`, "APPLICATION_NOT_FOUND", 404);
28
+ }
29
+ };
30
+ var EnrollmentError = class extends MDMError {
31
+ constructor(message, details) {
32
+ super(message, "ENROLLMENT_ERROR", 400, details);
33
+ }
34
+ };
35
+ var AuthenticationError = class extends MDMError {
36
+ constructor(message = "Authentication required") {
37
+ super(message, "AUTHENTICATION_ERROR", 401);
38
+ }
39
+ };
40
+ var AuthorizationError = class extends MDMError {
41
+ constructor(message = "Access denied") {
42
+ super(message, "AUTHORIZATION_ERROR", 403);
43
+ }
44
+ };
45
+ var ValidationError = class extends MDMError {
46
+ constructor(message, details) {
47
+ super(message, "VALIDATION_ERROR", 400, details);
48
+ }
49
+ };
50
+ var DEFAULT_RETRY_CONFIG = {
51
+ maxRetries: 3,
52
+ initialDelay: 1e3,
53
+ maxDelay: 3e4
54
+ };
55
+ function createWebhookManager(config) {
56
+ const endpoints = /* @__PURE__ */ new Map();
57
+ const retryConfig = { ...DEFAULT_RETRY_CONFIG, ...config.retry };
58
+ if (config.endpoints) {
59
+ for (const endpoint of config.endpoints) {
60
+ endpoints.set(endpoint.id, endpoint);
61
+ }
62
+ }
63
+ function signPayload(payload, secret) {
64
+ return createHmac("sha256", secret).update(payload).digest("hex");
65
+ }
66
+ function getBackoffDelay(retryCount) {
67
+ const delay = retryConfig.initialDelay * Math.pow(2, retryCount);
68
+ return Math.min(delay, retryConfig.maxDelay);
69
+ }
70
+ function shouldDeliverToEndpoint(endpoint, eventType) {
71
+ if (!endpoint.enabled) {
72
+ return false;
73
+ }
74
+ if (endpoint.events.includes("*")) {
75
+ return true;
76
+ }
77
+ return endpoint.events.includes(eventType);
78
+ }
79
+ async function deliverToEndpoint(endpoint, payload) {
80
+ const payloadString = JSON.stringify(payload);
81
+ let lastError;
82
+ let lastStatusCode;
83
+ for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {
84
+ try {
85
+ const headers = {
86
+ "Content-Type": "application/json",
87
+ "X-OpenMDM-Event": payload.event,
88
+ "X-OpenMDM-Delivery": payload.id,
89
+ "X-OpenMDM-Timestamp": payload.timestamp,
90
+ ...endpoint.headers
91
+ };
92
+ if (config.signingSecret) {
93
+ const signature = signPayload(payloadString, config.signingSecret);
94
+ headers["X-OpenMDM-Signature"] = `sha256=${signature}`;
95
+ }
96
+ const response = await fetch(endpoint.url, {
97
+ method: "POST",
98
+ headers,
99
+ body: payloadString,
100
+ signal: AbortSignal.timeout(3e4)
101
+ // 30 second timeout
102
+ });
103
+ lastStatusCode = response.status;
104
+ if (response.ok) {
105
+ return {
106
+ endpointId: endpoint.id,
107
+ success: true,
108
+ statusCode: response.status,
109
+ retryCount: attempt,
110
+ deliveredAt: /* @__PURE__ */ new Date()
111
+ };
112
+ }
113
+ if (response.status >= 400 && response.status < 500 && response.status !== 429) {
114
+ return {
115
+ endpointId: endpoint.id,
116
+ success: false,
117
+ statusCode: response.status,
118
+ error: `HTTP ${response.status}: ${response.statusText}`,
119
+ retryCount: attempt
120
+ };
121
+ }
122
+ lastError = `HTTP ${response.status}: ${response.statusText}`;
123
+ } catch (error) {
124
+ lastError = error instanceof Error ? error.message : String(error);
125
+ }
126
+ if (attempt < retryConfig.maxRetries) {
127
+ const delay = getBackoffDelay(attempt);
128
+ await new Promise((resolve) => setTimeout(resolve, delay));
129
+ }
130
+ }
131
+ return {
132
+ endpointId: endpoint.id,
133
+ success: false,
134
+ statusCode: lastStatusCode,
135
+ error: lastError || "Max retries exceeded",
136
+ retryCount: retryConfig.maxRetries
137
+ };
138
+ }
139
+ return {
140
+ async deliver(event) {
141
+ const matchingEndpoints = Array.from(endpoints.values()).filter(
142
+ (ep) => shouldDeliverToEndpoint(ep, event.type)
143
+ );
144
+ if (matchingEndpoints.length === 0) {
145
+ return [];
146
+ }
147
+ const payload = {
148
+ id: randomUUID(),
149
+ event: event.type,
150
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
151
+ data: event.payload
152
+ };
153
+ const deliveryPromises = matchingEndpoints.map(
154
+ (endpoint) => deliverToEndpoint(endpoint, payload)
155
+ );
156
+ const results = await Promise.all(deliveryPromises);
157
+ for (const result of results) {
158
+ if (!result.success) {
159
+ console.error(
160
+ `[OpenMDM] Webhook delivery failed to endpoint ${result.endpointId}:`,
161
+ result.error
162
+ );
163
+ }
164
+ }
165
+ return results;
166
+ },
167
+ addEndpoint(endpoint) {
168
+ endpoints.set(endpoint.id, endpoint);
169
+ },
170
+ removeEndpoint(endpointId) {
171
+ endpoints.delete(endpointId);
172
+ },
173
+ updateEndpoint(endpointId, updates) {
174
+ const existing = endpoints.get(endpointId);
175
+ if (existing) {
176
+ endpoints.set(endpointId, { ...existing, ...updates });
177
+ }
178
+ },
179
+ getEndpoints() {
180
+ return Array.from(endpoints.values());
181
+ },
182
+ async testEndpoint(endpointId) {
183
+ const endpoint = endpoints.get(endpointId);
184
+ if (!endpoint) {
185
+ return {
186
+ endpointId,
187
+ success: false,
188
+ error: "Endpoint not found",
189
+ retryCount: 0
190
+ };
191
+ }
192
+ const testPayload = {
193
+ id: randomUUID(),
194
+ event: "device.heartbeat",
195
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
196
+ data: {
197
+ test: true,
198
+ message: "OpenMDM webhook test"
199
+ }
200
+ };
201
+ return deliverToEndpoint(endpoint, testPayload);
202
+ }
203
+ };
204
+ }
205
+ function verifyWebhookSignature(payload, signature, secret) {
206
+ const expectedSignature = `sha256=${createHmac("sha256", secret).update(payload).digest("hex")}`;
207
+ if (signature.length !== expectedSignature.length) {
208
+ return false;
209
+ }
210
+ let result = 0;
211
+ for (let i = 0; i < signature.length; i++) {
212
+ result |= signature.charCodeAt(i) ^ expectedSignature.charCodeAt(i);
213
+ }
214
+ return result === 0;
215
+ }
216
+
217
+ // src/schema.ts
218
+ var mdmSchema = {
219
+ tables: {
220
+ // ----------------------------------------
221
+ // Devices Table
222
+ // ----------------------------------------
223
+ mdm_devices: {
224
+ columns: {
225
+ id: { type: "string", primaryKey: true },
226
+ external_id: { type: "string", nullable: true },
227
+ enrollment_id: { type: "string", unique: true },
228
+ status: {
229
+ type: "enum",
230
+ enumValues: ["pending", "enrolled", "unenrolled", "blocked"],
231
+ default: "pending"
232
+ },
233
+ // Device Info
234
+ model: { type: "string", nullable: true },
235
+ manufacturer: { type: "string", nullable: true },
236
+ os_version: { type: "string", nullable: true },
237
+ serial_number: { type: "string", nullable: true },
238
+ imei: { type: "string", nullable: true },
239
+ mac_address: { type: "string", nullable: true },
240
+ android_id: { type: "string", nullable: true },
241
+ // MDM State
242
+ policy_id: {
243
+ type: "string",
244
+ nullable: true,
245
+ references: { table: "mdm_policies", column: "id", onDelete: "set null" }
246
+ },
247
+ last_heartbeat: { type: "datetime", nullable: true },
248
+ last_sync: { type: "datetime", nullable: true },
249
+ // Telemetry (denormalized for quick access)
250
+ battery_level: { type: "integer", nullable: true },
251
+ storage_used: { type: "bigint", nullable: true },
252
+ storage_total: { type: "bigint", nullable: true },
253
+ latitude: { type: "string", nullable: true },
254
+ // Stored as string for precision
255
+ longitude: { type: "string", nullable: true },
256
+ location_timestamp: { type: "datetime", nullable: true },
257
+ // JSON fields
258
+ installed_apps: { type: "json", nullable: true },
259
+ tags: { type: "json", nullable: true },
260
+ metadata: { type: "json", nullable: true },
261
+ // Timestamps
262
+ created_at: { type: "datetime", default: "now" },
263
+ updated_at: { type: "datetime", default: "now" }
264
+ },
265
+ indexes: [
266
+ { columns: ["enrollment_id"], unique: true },
267
+ { columns: ["status"] },
268
+ { columns: ["policy_id"] },
269
+ { columns: ["last_heartbeat"] },
270
+ { columns: ["mac_address"] },
271
+ { columns: ["serial_number"] }
272
+ ]
273
+ },
274
+ // ----------------------------------------
275
+ // Policies Table
276
+ // ----------------------------------------
277
+ mdm_policies: {
278
+ columns: {
279
+ id: { type: "string", primaryKey: true },
280
+ name: { type: "string" },
281
+ description: { type: "text", nullable: true },
282
+ is_default: { type: "boolean", default: false },
283
+ settings: { type: "json" },
284
+ created_at: { type: "datetime", default: "now" },
285
+ updated_at: { type: "datetime", default: "now" }
286
+ },
287
+ indexes: [
288
+ { columns: ["name"] },
289
+ { columns: ["is_default"] }
290
+ ]
291
+ },
292
+ // ----------------------------------------
293
+ // Applications Table
294
+ // ----------------------------------------
295
+ mdm_applications: {
296
+ columns: {
297
+ id: { type: "string", primaryKey: true },
298
+ name: { type: "string" },
299
+ package_name: { type: "string" },
300
+ version: { type: "string" },
301
+ version_code: { type: "integer" },
302
+ url: { type: "string" },
303
+ hash: { type: "string", nullable: true },
304
+ // SHA-256
305
+ size: { type: "bigint", nullable: true },
306
+ min_sdk_version: { type: "integer", nullable: true },
307
+ // Deployment settings
308
+ show_icon: { type: "boolean", default: true },
309
+ run_after_install: { type: "boolean", default: false },
310
+ run_at_boot: { type: "boolean", default: false },
311
+ is_system: { type: "boolean", default: false },
312
+ // State
313
+ is_active: { type: "boolean", default: true },
314
+ // Metadata
315
+ metadata: { type: "json", nullable: true },
316
+ created_at: { type: "datetime", default: "now" },
317
+ updated_at: { type: "datetime", default: "now" }
318
+ },
319
+ indexes: [
320
+ { columns: ["package_name"] },
321
+ { columns: ["package_name", "version"], unique: true },
322
+ { columns: ["is_active"] }
323
+ ]
324
+ },
325
+ // ----------------------------------------
326
+ // Commands Table
327
+ // ----------------------------------------
328
+ mdm_commands: {
329
+ columns: {
330
+ id: { type: "string", primaryKey: true },
331
+ device_id: {
332
+ type: "string",
333
+ references: { table: "mdm_devices", column: "id", onDelete: "cascade" }
334
+ },
335
+ type: { type: "string" },
336
+ payload: { type: "json", nullable: true },
337
+ status: {
338
+ type: "enum",
339
+ enumValues: ["pending", "sent", "acknowledged", "completed", "failed", "cancelled"],
340
+ default: "pending"
341
+ },
342
+ result: { type: "json", nullable: true },
343
+ error: { type: "text", nullable: true },
344
+ created_at: { type: "datetime", default: "now" },
345
+ sent_at: { type: "datetime", nullable: true },
346
+ acknowledged_at: { type: "datetime", nullable: true },
347
+ completed_at: { type: "datetime", nullable: true }
348
+ },
349
+ indexes: [
350
+ { columns: ["device_id"] },
351
+ { columns: ["status"] },
352
+ { columns: ["device_id", "status"] },
353
+ { columns: ["created_at"] }
354
+ ]
355
+ },
356
+ // ----------------------------------------
357
+ // Events Table
358
+ // ----------------------------------------
359
+ mdm_events: {
360
+ columns: {
361
+ id: { type: "string", primaryKey: true },
362
+ device_id: {
363
+ type: "string",
364
+ references: { table: "mdm_devices", column: "id", onDelete: "cascade" }
365
+ },
366
+ type: { type: "string" },
367
+ payload: { type: "json" },
368
+ created_at: { type: "datetime", default: "now" }
369
+ },
370
+ indexes: [
371
+ { columns: ["device_id"] },
372
+ { columns: ["type"] },
373
+ { columns: ["device_id", "type"] },
374
+ { columns: ["created_at"] }
375
+ ]
376
+ },
377
+ // ----------------------------------------
378
+ // Groups Table
379
+ // ----------------------------------------
380
+ mdm_groups: {
381
+ columns: {
382
+ id: { type: "string", primaryKey: true },
383
+ name: { type: "string" },
384
+ description: { type: "text", nullable: true },
385
+ policy_id: {
386
+ type: "string",
387
+ nullable: true,
388
+ references: { table: "mdm_policies", column: "id", onDelete: "set null" }
389
+ },
390
+ parent_id: {
391
+ type: "string",
392
+ nullable: true,
393
+ references: { table: "mdm_groups", column: "id", onDelete: "set null" }
394
+ },
395
+ metadata: { type: "json", nullable: true },
396
+ created_at: { type: "datetime", default: "now" },
397
+ updated_at: { type: "datetime", default: "now" }
398
+ },
399
+ indexes: [
400
+ { columns: ["name"] },
401
+ { columns: ["policy_id"] },
402
+ { columns: ["parent_id"] }
403
+ ]
404
+ },
405
+ // ----------------------------------------
406
+ // Device Groups (Many-to-Many)
407
+ // ----------------------------------------
408
+ mdm_device_groups: {
409
+ columns: {
410
+ device_id: {
411
+ type: "string",
412
+ references: { table: "mdm_devices", column: "id", onDelete: "cascade" }
413
+ },
414
+ group_id: {
415
+ type: "string",
416
+ references: { table: "mdm_groups", column: "id", onDelete: "cascade" }
417
+ },
418
+ created_at: { type: "datetime", default: "now" }
419
+ },
420
+ indexes: [
421
+ { columns: ["device_id", "group_id"], unique: true },
422
+ { columns: ["group_id"] }
423
+ ]
424
+ },
425
+ // ----------------------------------------
426
+ // Push Tokens (for FCM/MQTT registration)
427
+ // ----------------------------------------
428
+ mdm_push_tokens: {
429
+ columns: {
430
+ id: { type: "string", primaryKey: true },
431
+ device_id: {
432
+ type: "string",
433
+ references: { table: "mdm_devices", column: "id", onDelete: "cascade" }
434
+ },
435
+ provider: {
436
+ type: "enum",
437
+ enumValues: ["fcm", "mqtt", "websocket"]
438
+ },
439
+ token: { type: "string" },
440
+ is_active: { type: "boolean", default: true },
441
+ created_at: { type: "datetime", default: "now" },
442
+ updated_at: { type: "datetime", default: "now" }
443
+ },
444
+ indexes: [
445
+ { columns: ["device_id"] },
446
+ { columns: ["provider", "token"], unique: true },
447
+ { columns: ["is_active"] }
448
+ ]
449
+ },
450
+ // ----------------------------------------
451
+ // Application Deployments (Which apps go to which policies/groups)
452
+ // ----------------------------------------
453
+ mdm_app_deployments: {
454
+ columns: {
455
+ id: { type: "string", primaryKey: true },
456
+ application_id: {
457
+ type: "string",
458
+ references: { table: "mdm_applications", column: "id", onDelete: "cascade" }
459
+ },
460
+ // Target can be policy or group
461
+ target_type: {
462
+ type: "enum",
463
+ enumValues: ["policy", "group"]
464
+ },
465
+ target_id: { type: "string" },
466
+ action: {
467
+ type: "enum",
468
+ enumValues: ["install", "update", "uninstall"],
469
+ default: "install"
470
+ },
471
+ is_required: { type: "boolean", default: false },
472
+ created_at: { type: "datetime", default: "now" }
473
+ },
474
+ indexes: [
475
+ { columns: ["application_id"] },
476
+ { columns: ["target_type", "target_id"] }
477
+ ]
478
+ },
479
+ // ----------------------------------------
480
+ // App Versions (Version history for rollback support)
481
+ // ----------------------------------------
482
+ mdm_app_versions: {
483
+ columns: {
484
+ id: { type: "string", primaryKey: true },
485
+ application_id: {
486
+ type: "string",
487
+ references: { table: "mdm_applications", column: "id", onDelete: "cascade" }
488
+ },
489
+ package_name: { type: "string" },
490
+ version: { type: "string" },
491
+ version_code: { type: "integer" },
492
+ url: { type: "string" },
493
+ hash: { type: "string", nullable: true },
494
+ size: { type: "bigint", nullable: true },
495
+ release_notes: { type: "text", nullable: true },
496
+ is_minimum_version: { type: "boolean", default: false },
497
+ created_at: { type: "datetime", default: "now" }
498
+ },
499
+ indexes: [
500
+ { columns: ["application_id"] },
501
+ { columns: ["package_name"] },
502
+ { columns: ["package_name", "version_code"], unique: true },
503
+ { columns: ["is_minimum_version"] }
504
+ ]
505
+ },
506
+ // ----------------------------------------
507
+ // App Rollbacks (Rollback history and status)
508
+ // ----------------------------------------
509
+ mdm_rollbacks: {
510
+ columns: {
511
+ id: { type: "string", primaryKey: true },
512
+ device_id: {
513
+ type: "string",
514
+ references: { table: "mdm_devices", column: "id", onDelete: "cascade" }
515
+ },
516
+ package_name: { type: "string" },
517
+ from_version: { type: "string" },
518
+ from_version_code: { type: "integer" },
519
+ to_version: { type: "string" },
520
+ to_version_code: { type: "integer" },
521
+ reason: { type: "text", nullable: true },
522
+ status: {
523
+ type: "enum",
524
+ enumValues: ["pending", "in_progress", "completed", "failed"],
525
+ default: "pending"
526
+ },
527
+ error: { type: "text", nullable: true },
528
+ initiated_by: { type: "string", nullable: true },
529
+ created_at: { type: "datetime", default: "now" },
530
+ completed_at: { type: "datetime", nullable: true }
531
+ },
532
+ indexes: [
533
+ { columns: ["device_id"] },
534
+ { columns: ["package_name"] },
535
+ { columns: ["device_id", "package_name"] },
536
+ { columns: ["status"] },
537
+ { columns: ["created_at"] }
538
+ ]
539
+ },
540
+ // ----------------------------------------
541
+ // Webhook Endpoints (For outbound webhook configuration storage)
542
+ // ----------------------------------------
543
+ mdm_webhook_endpoints: {
544
+ columns: {
545
+ id: { type: "string", primaryKey: true },
546
+ url: { type: "string" },
547
+ events: { type: "json" },
548
+ // Array of event types or ['*']
549
+ headers: { type: "json", nullable: true },
550
+ enabled: { type: "boolean", default: true },
551
+ description: { type: "text", nullable: true },
552
+ created_at: { type: "datetime", default: "now" },
553
+ updated_at: { type: "datetime", default: "now" }
554
+ },
555
+ indexes: [
556
+ { columns: ["enabled"] }
557
+ ]
558
+ },
559
+ // ----------------------------------------
560
+ // Webhook Deliveries (Delivery history and status)
561
+ // ----------------------------------------
562
+ mdm_webhook_deliveries: {
563
+ columns: {
564
+ id: { type: "string", primaryKey: true },
565
+ endpoint_id: {
566
+ type: "string",
567
+ references: { table: "mdm_webhook_endpoints", column: "id", onDelete: "cascade" }
568
+ },
569
+ event_id: { type: "string" },
570
+ event_type: { type: "string" },
571
+ payload: { type: "json" },
572
+ status: {
573
+ type: "enum",
574
+ enumValues: ["pending", "success", "failed"],
575
+ default: "pending"
576
+ },
577
+ status_code: { type: "integer", nullable: true },
578
+ error: { type: "text", nullable: true },
579
+ retry_count: { type: "integer", default: 0 },
580
+ created_at: { type: "datetime", default: "now" },
581
+ delivered_at: { type: "datetime", nullable: true }
582
+ },
583
+ indexes: [
584
+ { columns: ["endpoint_id"] },
585
+ { columns: ["event_type"] },
586
+ { columns: ["status"] },
587
+ { columns: ["created_at"] }
588
+ ]
589
+ }
590
+ }
591
+ };
592
+ function getTableNames() {
593
+ return Object.keys(mdmSchema.tables);
594
+ }
595
+ function getColumnNames(tableName) {
596
+ const table = mdmSchema.tables[tableName];
597
+ if (!table) throw new Error(`Table ${tableName} not found in schema`);
598
+ return Object.keys(table.columns);
599
+ }
600
+ function getPrimaryKey(tableName) {
601
+ const table = mdmSchema.tables[tableName];
602
+ if (!table) throw new Error(`Table ${tableName} not found in schema`);
603
+ for (const [name, def] of Object.entries(table.columns)) {
604
+ if (def.primaryKey) return name;
605
+ }
606
+ return null;
607
+ }
608
+ function snakeToCamel(str) {
609
+ return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
610
+ }
611
+ function camelToSnake(str) {
612
+ return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
613
+ }
614
+ function transformToCamelCase(obj) {
615
+ const result = {};
616
+ for (const [key, value] of Object.entries(obj)) {
617
+ result[snakeToCamel(key)] = value;
618
+ }
619
+ return result;
620
+ }
621
+ function transformToSnakeCase(obj) {
622
+ const result = {};
623
+ for (const [key, value] of Object.entries(obj)) {
624
+ result[camelToSnake(key)] = value;
625
+ }
626
+ return result;
627
+ }
628
+
629
+ // src/index.ts
630
+ function createMDM(config) {
631
+ const { database, push, enrollment, webhooks: webhooksConfig, plugins = [] } = config;
632
+ const eventHandlers = /* @__PURE__ */ new Map();
633
+ const pushAdapter = push ? createPushAdapter(push, database) : createStubPushAdapter();
634
+ const webhookManager = webhooksConfig ? createWebhookManager(webhooksConfig) : void 0;
635
+ const on = (event, handler) => {
636
+ if (!eventHandlers.has(event)) {
637
+ eventHandlers.set(event, /* @__PURE__ */ new Set());
638
+ }
639
+ const handlers = eventHandlers.get(event);
640
+ handlers.add(handler);
641
+ return () => {
642
+ handlers.delete(handler);
643
+ };
644
+ };
645
+ const emit = async (event, data) => {
646
+ const handlers = eventHandlers.get(event);
647
+ const eventRecord = {
648
+ id: randomUUID(),
649
+ deviceId: data.device?.id || data.deviceId || "",
650
+ type: event,
651
+ payload: data,
652
+ createdAt: /* @__PURE__ */ new Date()
653
+ };
654
+ try {
655
+ await database.createEvent({
656
+ deviceId: eventRecord.deviceId,
657
+ type: eventRecord.type,
658
+ payload: eventRecord.payload
659
+ });
660
+ } catch (error) {
661
+ console.error("[OpenMDM] Failed to persist event:", error);
662
+ }
663
+ if (webhookManager) {
664
+ webhookManager.deliver(eventRecord).catch((error) => {
665
+ console.error("[OpenMDM] Webhook delivery error:", error);
666
+ });
667
+ }
668
+ if (handlers) {
669
+ for (const handler of handlers) {
670
+ try {
671
+ await handler(eventRecord);
672
+ } catch (error) {
673
+ console.error(`[OpenMDM] Event handler error for ${event}:`, error);
674
+ }
675
+ }
676
+ }
677
+ if (config.onEvent) {
678
+ try {
679
+ await config.onEvent(eventRecord);
680
+ } catch (error) {
681
+ console.error("[OpenMDM] onEvent hook error:", error);
682
+ }
683
+ }
684
+ };
685
+ const devices = {
686
+ async get(id) {
687
+ return database.findDevice(id);
688
+ },
689
+ async getByEnrollmentId(enrollmentId) {
690
+ return database.findDeviceByEnrollmentId(enrollmentId);
691
+ },
692
+ async list(filter) {
693
+ return database.listDevices(filter);
694
+ },
695
+ async create(data) {
696
+ const device = await database.createDevice(data);
697
+ await emit("device.enrolled", { device });
698
+ if (config.onDeviceEnrolled) {
699
+ await config.onDeviceEnrolled(device);
700
+ }
701
+ return device;
702
+ },
703
+ async update(id, data) {
704
+ const oldDevice = await database.findDevice(id);
705
+ if (!oldDevice) {
706
+ throw new DeviceNotFoundError(id);
707
+ }
708
+ const device = await database.updateDevice(id, data);
709
+ if (data.status && data.status !== oldDevice.status) {
710
+ await emit("device.statusChanged", {
711
+ device,
712
+ oldStatus: oldDevice.status,
713
+ newStatus: data.status
714
+ });
715
+ }
716
+ if (data.policyId !== void 0 && data.policyId !== oldDevice.policyId) {
717
+ await emit("device.policyChanged", {
718
+ device,
719
+ oldPolicyId: oldDevice.policyId || void 0,
720
+ newPolicyId: data.policyId || void 0
721
+ });
722
+ }
723
+ return device;
724
+ },
725
+ async delete(id) {
726
+ const device = await database.findDevice(id);
727
+ if (device) {
728
+ await database.deleteDevice(id);
729
+ await emit("device.unenrolled", { device });
730
+ if (config.onDeviceUnenrolled) {
731
+ await config.onDeviceUnenrolled(device);
732
+ }
733
+ }
734
+ },
735
+ async assignPolicy(deviceId, policyId) {
736
+ const device = await this.update(deviceId, { policyId });
737
+ await pushAdapter.send(deviceId, {
738
+ type: "policy.updated",
739
+ payload: { policyId },
740
+ priority: "high"
741
+ });
742
+ return device;
743
+ },
744
+ async addToGroup(deviceId, groupId) {
745
+ await database.addDeviceToGroup(deviceId, groupId);
746
+ },
747
+ async removeFromGroup(deviceId, groupId) {
748
+ await database.removeDeviceFromGroup(deviceId, groupId);
749
+ },
750
+ async getGroups(deviceId) {
751
+ return database.getDeviceGroups(deviceId);
752
+ },
753
+ async sendCommand(deviceId, input) {
754
+ const command = await database.createCommand({
755
+ ...input,
756
+ deviceId
757
+ });
758
+ const pushResult = await pushAdapter.send(deviceId, {
759
+ type: `command.${input.type}`,
760
+ payload: {
761
+ commandId: command.id,
762
+ type: input.type,
763
+ ...input.payload
764
+ },
765
+ priority: "high"
766
+ });
767
+ if (pushResult.success) {
768
+ await database.updateCommand(command.id, {
769
+ status: "sent",
770
+ sentAt: /* @__PURE__ */ new Date()
771
+ });
772
+ }
773
+ if (config.onCommand) {
774
+ await config.onCommand(command);
775
+ }
776
+ return database.findCommand(command.id);
777
+ },
778
+ async sync(deviceId) {
779
+ return this.sendCommand(deviceId, { type: "sync" });
780
+ },
781
+ async reboot(deviceId) {
782
+ return this.sendCommand(deviceId, { type: "reboot" });
783
+ },
784
+ async lock(deviceId, message) {
785
+ return this.sendCommand(deviceId, {
786
+ type: "lock",
787
+ payload: message ? { message } : void 0
788
+ });
789
+ },
790
+ async wipe(deviceId, preserveData) {
791
+ return this.sendCommand(deviceId, {
792
+ type: preserveData ? "wipe" : "factoryReset",
793
+ payload: { preserveData }
794
+ });
795
+ }
796
+ };
797
+ const policies = {
798
+ async get(id) {
799
+ return database.findPolicy(id);
800
+ },
801
+ async getDefault() {
802
+ return database.findDefaultPolicy();
803
+ },
804
+ async list() {
805
+ return database.listPolicies();
806
+ },
807
+ async create(data) {
808
+ if (data.isDefault) {
809
+ const existingPolicies = await database.listPolicies();
810
+ for (const policy of existingPolicies) {
811
+ if (policy.isDefault) {
812
+ await database.updatePolicy(policy.id, { isDefault: false });
813
+ }
814
+ }
815
+ }
816
+ return database.createPolicy(data);
817
+ },
818
+ async update(id, data) {
819
+ if (data.isDefault) {
820
+ const existingPolicies = await database.listPolicies();
821
+ for (const policy2 of existingPolicies) {
822
+ if (policy2.isDefault && policy2.id !== id) {
823
+ await database.updatePolicy(policy2.id, { isDefault: false });
824
+ }
825
+ }
826
+ }
827
+ const policy = await database.updatePolicy(id, data);
828
+ const devicesResult = await database.listDevices({ policyId: id });
829
+ if (devicesResult.devices.length > 0) {
830
+ const deviceIds = devicesResult.devices.map((d) => d.id);
831
+ await pushAdapter.sendBatch(deviceIds, {
832
+ type: "policy.updated",
833
+ payload: { policyId: id },
834
+ priority: "high"
835
+ });
836
+ }
837
+ return policy;
838
+ },
839
+ async delete(id) {
840
+ const devicesResult = await database.listDevices({ policyId: id });
841
+ if (devicesResult.devices.length > 0) {
842
+ for (const device of devicesResult.devices) {
843
+ await database.updateDevice(device.id, { policyId: null });
844
+ }
845
+ }
846
+ await database.deletePolicy(id);
847
+ },
848
+ async setDefault(id) {
849
+ return this.update(id, { isDefault: true });
850
+ },
851
+ async getDevices(policyId) {
852
+ const result = await database.listDevices({ policyId });
853
+ return result.devices;
854
+ },
855
+ async applyToDevice(policyId, deviceId) {
856
+ await devices.assignPolicy(deviceId, policyId);
857
+ }
858
+ };
859
+ const apps = {
860
+ async get(id) {
861
+ return database.findApplication(id);
862
+ },
863
+ async getByPackage(packageName, version) {
864
+ return database.findApplicationByPackage(packageName, version);
865
+ },
866
+ async list(activeOnly) {
867
+ return database.listApplications(activeOnly);
868
+ },
869
+ async register(data) {
870
+ return database.createApplication(data);
871
+ },
872
+ async update(id, data) {
873
+ return database.updateApplication(id, data);
874
+ },
875
+ async delete(id) {
876
+ await database.deleteApplication(id);
877
+ },
878
+ async activate(id) {
879
+ return database.updateApplication(id, { isActive: true });
880
+ },
881
+ async deactivate(id) {
882
+ return database.updateApplication(id, { isActive: false });
883
+ },
884
+ async deploy(packageName, target) {
885
+ const app = await database.findApplicationByPackage(packageName);
886
+ if (!app) {
887
+ throw new ApplicationNotFoundError(packageName);
888
+ }
889
+ const deviceIds = [];
890
+ if (target.devices) {
891
+ deviceIds.push(...target.devices);
892
+ }
893
+ if (target.groups) {
894
+ for (const groupId of target.groups) {
895
+ const groupDevices = await database.listDevicesInGroup(groupId);
896
+ deviceIds.push(...groupDevices.map((d) => d.id));
897
+ }
898
+ }
899
+ if (target.policies) {
900
+ for (const policyId of target.policies) {
901
+ const result = await database.listDevices({ policyId });
902
+ deviceIds.push(...result.devices.map((d) => d.id));
903
+ }
904
+ }
905
+ const uniqueDeviceIds = [...new Set(deviceIds)];
906
+ if (uniqueDeviceIds.length > 0) {
907
+ await pushAdapter.sendBatch(uniqueDeviceIds, {
908
+ type: "command.installApp",
909
+ payload: {
910
+ packageName: app.packageName,
911
+ version: app.version,
912
+ versionCode: app.versionCode,
913
+ url: app.url,
914
+ hash: app.hash
915
+ },
916
+ priority: "high"
917
+ });
918
+ for (const deviceId of uniqueDeviceIds) {
919
+ await database.createCommand({
920
+ deviceId,
921
+ type: "installApp",
922
+ payload: {
923
+ packageName: app.packageName,
924
+ version: app.version,
925
+ url: app.url
926
+ }
927
+ });
928
+ }
929
+ }
930
+ },
931
+ async installOnDevice(packageName, deviceId, version) {
932
+ const app = await database.findApplicationByPackage(packageName, version);
933
+ if (!app) {
934
+ throw new ApplicationNotFoundError(packageName);
935
+ }
936
+ return devices.sendCommand(deviceId, {
937
+ type: "installApp",
938
+ payload: {
939
+ packageName: app.packageName,
940
+ version: app.version,
941
+ versionCode: app.versionCode,
942
+ url: app.url,
943
+ hash: app.hash
944
+ }
945
+ });
946
+ },
947
+ async uninstallFromDevice(packageName, deviceId) {
948
+ return devices.sendCommand(deviceId, {
949
+ type: "uninstallApp",
950
+ payload: { packageName }
951
+ });
952
+ }
953
+ };
954
+ const commands = {
955
+ async get(id) {
956
+ return database.findCommand(id);
957
+ },
958
+ async list(filter) {
959
+ return database.listCommands(filter);
960
+ },
961
+ async send(input) {
962
+ return devices.sendCommand(input.deviceId, {
963
+ type: input.type,
964
+ payload: input.payload
965
+ });
966
+ },
967
+ async cancel(id) {
968
+ return database.updateCommand(id, { status: "cancelled" });
969
+ },
970
+ async acknowledge(id) {
971
+ const command = await database.updateCommand(id, {
972
+ status: "acknowledged",
973
+ acknowledgedAt: /* @__PURE__ */ new Date()
974
+ });
975
+ const device = await database.findDevice(command.deviceId);
976
+ if (device) {
977
+ await emit("command.acknowledged", { device, command });
978
+ }
979
+ return command;
980
+ },
981
+ async complete(id, result) {
982
+ const command = await database.updateCommand(id, {
983
+ status: "completed",
984
+ result,
985
+ completedAt: /* @__PURE__ */ new Date()
986
+ });
987
+ const device = await database.findDevice(command.deviceId);
988
+ if (device) {
989
+ await emit("command.completed", { device, command, result });
990
+ }
991
+ return command;
992
+ },
993
+ async fail(id, error) {
994
+ const command = await database.updateCommand(id, {
995
+ status: "failed",
996
+ error,
997
+ completedAt: /* @__PURE__ */ new Date()
998
+ });
999
+ const device = await database.findDevice(command.deviceId);
1000
+ if (device) {
1001
+ await emit("command.failed", { device, command, error });
1002
+ }
1003
+ return command;
1004
+ },
1005
+ async getPending(deviceId) {
1006
+ return database.getPendingCommands(deviceId);
1007
+ }
1008
+ };
1009
+ const groups = {
1010
+ async get(id) {
1011
+ return database.findGroup(id);
1012
+ },
1013
+ async list() {
1014
+ return database.listGroups();
1015
+ },
1016
+ async create(data) {
1017
+ return database.createGroup(data);
1018
+ },
1019
+ async update(id, data) {
1020
+ return database.updateGroup(id, data);
1021
+ },
1022
+ async delete(id) {
1023
+ await database.deleteGroup(id);
1024
+ },
1025
+ async getDevices(groupId) {
1026
+ return database.listDevicesInGroup(groupId);
1027
+ },
1028
+ async addDevice(groupId, deviceId) {
1029
+ await database.addDeviceToGroup(deviceId, groupId);
1030
+ },
1031
+ async removeDevice(groupId, deviceId) {
1032
+ await database.removeDeviceFromGroup(deviceId, groupId);
1033
+ },
1034
+ async getChildren(groupId) {
1035
+ const allGroups = await database.listGroups();
1036
+ return allGroups.filter((g) => g.parentId === groupId);
1037
+ }
1038
+ };
1039
+ const enroll = async (request) => {
1040
+ if (enrollment?.allowedMethods && !enrollment.allowedMethods.includes(request.method)) {
1041
+ throw new EnrollmentError(
1042
+ `Enrollment method '${request.method}' is not allowed`
1043
+ );
1044
+ }
1045
+ if (enrollment?.deviceSecret) {
1046
+ const isValid = verifyEnrollmentSignature(
1047
+ request,
1048
+ enrollment.deviceSecret
1049
+ );
1050
+ if (!isValid) {
1051
+ throw new EnrollmentError("Invalid enrollment signature");
1052
+ }
1053
+ }
1054
+ if (enrollment?.validate) {
1055
+ const isValid = await enrollment.validate(request);
1056
+ if (!isValid) {
1057
+ throw new EnrollmentError("Enrollment validation failed");
1058
+ }
1059
+ }
1060
+ const enrollmentId = request.macAddress || request.serialNumber || request.imei || request.androidId;
1061
+ if (!enrollmentId) {
1062
+ throw new EnrollmentError(
1063
+ "Device must provide at least one identifier (macAddress, serialNumber, imei, or androidId)"
1064
+ );
1065
+ }
1066
+ let device = await database.findDeviceByEnrollmentId(enrollmentId);
1067
+ if (device) {
1068
+ device = await database.updateDevice(device.id, {
1069
+ status: "enrolled",
1070
+ model: request.model,
1071
+ manufacturer: request.manufacturer,
1072
+ osVersion: request.osVersion,
1073
+ lastSync: /* @__PURE__ */ new Date()
1074
+ });
1075
+ } else if (enrollment?.autoEnroll) {
1076
+ device = await database.createDevice({
1077
+ enrollmentId,
1078
+ model: request.model,
1079
+ manufacturer: request.manufacturer,
1080
+ osVersion: request.osVersion,
1081
+ serialNumber: request.serialNumber,
1082
+ imei: request.imei,
1083
+ macAddress: request.macAddress,
1084
+ androidId: request.androidId,
1085
+ policyId: request.policyId || enrollment.defaultPolicyId
1086
+ });
1087
+ if (enrollment.defaultGroupId) {
1088
+ await database.addDeviceToGroup(device.id, enrollment.defaultGroupId);
1089
+ }
1090
+ } else if (enrollment?.requireApproval) {
1091
+ device = await database.createDevice({
1092
+ enrollmentId,
1093
+ model: request.model,
1094
+ manufacturer: request.manufacturer,
1095
+ osVersion: request.osVersion,
1096
+ serialNumber: request.serialNumber,
1097
+ imei: request.imei,
1098
+ macAddress: request.macAddress,
1099
+ androidId: request.androidId
1100
+ });
1101
+ } else {
1102
+ throw new EnrollmentError(
1103
+ "Device not registered and auto-enroll is disabled"
1104
+ );
1105
+ }
1106
+ let policy = null;
1107
+ if (device.policyId) {
1108
+ policy = await database.findPolicy(device.policyId);
1109
+ }
1110
+ if (!policy) {
1111
+ policy = await database.findDefaultPolicy();
1112
+ }
1113
+ const tokenSecret = config.auth?.deviceTokenSecret || enrollment?.deviceSecret || "";
1114
+ const tokenExpiration = config.auth?.deviceTokenExpiration || 365 * 24 * 60 * 60;
1115
+ const token = generateDeviceToken(device.id, tokenSecret, tokenExpiration);
1116
+ await emit("device.enrolled", { device });
1117
+ if (config.onDeviceEnrolled) {
1118
+ await config.onDeviceEnrolled(device);
1119
+ }
1120
+ for (const plugin of plugins) {
1121
+ if (plugin.onEnroll) {
1122
+ await plugin.onEnroll(device, request);
1123
+ }
1124
+ if (plugin.onDeviceEnrolled) {
1125
+ await plugin.onDeviceEnrolled(device);
1126
+ }
1127
+ }
1128
+ return {
1129
+ deviceId: device.id,
1130
+ enrollmentId: device.enrollmentId,
1131
+ policyId: policy?.id,
1132
+ policy: policy || void 0,
1133
+ serverUrl: config.serverUrl || "",
1134
+ pushConfig: {
1135
+ provider: push?.provider || "polling",
1136
+ fcmSenderId: push?.fcmCredentials?.project_id,
1137
+ mqttUrl: push?.mqttUrl,
1138
+ mqttTopic: push?.mqttTopicPrefix ? `${push.mqttTopicPrefix}/${device.id}` : `openmdm/devices/${device.id}`,
1139
+ pollingInterval: push?.pollingInterval || 60
1140
+ },
1141
+ token,
1142
+ tokenExpiresAt: new Date(Date.now() + tokenExpiration * 1e3)
1143
+ };
1144
+ };
1145
+ const processHeartbeat = async (deviceId, heartbeat) => {
1146
+ const device = await database.findDevice(deviceId);
1147
+ if (!device) {
1148
+ throw new DeviceNotFoundError(deviceId);
1149
+ }
1150
+ const updateData = {
1151
+ lastHeartbeat: heartbeat.timestamp,
1152
+ batteryLevel: heartbeat.batteryLevel,
1153
+ storageUsed: heartbeat.storageUsed,
1154
+ storageTotal: heartbeat.storageTotal,
1155
+ installedApps: heartbeat.installedApps
1156
+ };
1157
+ if (heartbeat.location) {
1158
+ updateData.location = heartbeat.location;
1159
+ }
1160
+ const updatedDevice = await database.updateDevice(deviceId, updateData);
1161
+ await emit("device.heartbeat", { device: updatedDevice, heartbeat });
1162
+ if (heartbeat.location) {
1163
+ await emit("device.locationUpdated", {
1164
+ device: updatedDevice,
1165
+ location: heartbeat.location
1166
+ });
1167
+ }
1168
+ if (device.installedApps && heartbeat.installedApps) {
1169
+ const oldApps = new Map(
1170
+ device.installedApps.map((a) => [a.packageName, a])
1171
+ );
1172
+ const newApps = new Map(
1173
+ heartbeat.installedApps.map((a) => [a.packageName, a])
1174
+ );
1175
+ for (const [pkg, app] of newApps) {
1176
+ const oldApp = oldApps.get(pkg);
1177
+ if (!oldApp) {
1178
+ await emit("app.installed", { device: updatedDevice, app });
1179
+ } else if (oldApp.version !== app.version) {
1180
+ await emit("app.updated", {
1181
+ device: updatedDevice,
1182
+ app,
1183
+ oldVersion: oldApp.version
1184
+ });
1185
+ }
1186
+ }
1187
+ for (const [pkg] of oldApps) {
1188
+ if (!newApps.has(pkg)) {
1189
+ await emit("app.uninstalled", {
1190
+ device: updatedDevice,
1191
+ packageName: pkg
1192
+ });
1193
+ }
1194
+ }
1195
+ }
1196
+ if (config.onHeartbeat) {
1197
+ await config.onHeartbeat(updatedDevice, heartbeat);
1198
+ }
1199
+ for (const plugin of plugins) {
1200
+ if (plugin.onHeartbeat) {
1201
+ await plugin.onHeartbeat(updatedDevice, heartbeat);
1202
+ }
1203
+ }
1204
+ };
1205
+ const verifyDeviceToken = async (token) => {
1206
+ try {
1207
+ const tokenSecret = config.auth?.deviceTokenSecret || enrollment?.deviceSecret || "";
1208
+ const parts = token.split(".");
1209
+ if (parts.length !== 3) {
1210
+ return null;
1211
+ }
1212
+ const [header, payload, signature] = parts;
1213
+ const expectedSignature = createHmac("sha256", tokenSecret).update(`${header}.${payload}`).digest("base64url");
1214
+ if (signature !== expectedSignature) {
1215
+ return null;
1216
+ }
1217
+ const decoded = JSON.parse(
1218
+ Buffer.from(payload, "base64url").toString("utf-8")
1219
+ );
1220
+ if (decoded.exp && decoded.exp < Math.floor(Date.now() / 1e3)) {
1221
+ return null;
1222
+ }
1223
+ return { deviceId: decoded.sub };
1224
+ } catch {
1225
+ return null;
1226
+ }
1227
+ };
1228
+ const getPlugins = () => plugins;
1229
+ const getPlugin = (name) => {
1230
+ return plugins.find((p) => p.name === name);
1231
+ };
1232
+ const instance = {
1233
+ devices,
1234
+ policies,
1235
+ apps,
1236
+ commands,
1237
+ groups,
1238
+ push: pushAdapter,
1239
+ webhooks: webhookManager,
1240
+ db: database,
1241
+ config,
1242
+ on,
1243
+ emit,
1244
+ enroll,
1245
+ processHeartbeat,
1246
+ verifyDeviceToken,
1247
+ getPlugins,
1248
+ getPlugin
1249
+ };
1250
+ (async () => {
1251
+ for (const plugin of plugins) {
1252
+ if (plugin.onInit) {
1253
+ try {
1254
+ await plugin.onInit(instance);
1255
+ console.log(`[OpenMDM] Plugin initialized: ${plugin.name}`);
1256
+ } catch (error) {
1257
+ console.error(
1258
+ `[OpenMDM] Failed to initialize plugin ${plugin.name}:`,
1259
+ error
1260
+ );
1261
+ }
1262
+ }
1263
+ }
1264
+ })();
1265
+ return instance;
1266
+ }
1267
+ function createPushAdapter(config, database) {
1268
+ if (!config) {
1269
+ return createStubPushAdapter();
1270
+ }
1271
+ return {
1272
+ async send(deviceId, message) {
1273
+ console.log(
1274
+ `[OpenMDM] Push to ${deviceId}: ${message.type}`,
1275
+ message.payload
1276
+ );
1277
+ return { success: true, messageId: randomUUID() };
1278
+ },
1279
+ async sendBatch(deviceIds, message) {
1280
+ console.log(
1281
+ `[OpenMDM] Push to ${deviceIds.length} devices: ${message.type}`
1282
+ );
1283
+ const results = deviceIds.map((deviceId) => ({
1284
+ deviceId,
1285
+ result: { success: true, messageId: randomUUID() }
1286
+ }));
1287
+ return {
1288
+ successCount: deviceIds.length,
1289
+ failureCount: 0,
1290
+ results
1291
+ };
1292
+ },
1293
+ async registerToken(deviceId, token) {
1294
+ if (config.provider === "polling") {
1295
+ return;
1296
+ }
1297
+ await database.upsertPushToken({
1298
+ deviceId,
1299
+ provider: config.provider,
1300
+ token
1301
+ });
1302
+ },
1303
+ async unregisterToken(deviceId) {
1304
+ if (config.provider === "polling") {
1305
+ return;
1306
+ }
1307
+ await database.deletePushToken(deviceId, config.provider);
1308
+ }
1309
+ };
1310
+ }
1311
+ function createStubPushAdapter() {
1312
+ return {
1313
+ async send(deviceId, message) {
1314
+ console.log(`[OpenMDM] Push (stub): ${deviceId} <- ${message.type}`);
1315
+ return { success: true, messageId: "stub" };
1316
+ },
1317
+ async sendBatch(deviceIds, message) {
1318
+ console.log(
1319
+ `[OpenMDM] Push (stub): ${deviceIds.length} devices <- ${message.type}`
1320
+ );
1321
+ return {
1322
+ successCount: deviceIds.length,
1323
+ failureCount: 0,
1324
+ results: deviceIds.map((deviceId) => ({
1325
+ deviceId,
1326
+ result: { success: true, messageId: "stub" }
1327
+ }))
1328
+ };
1329
+ }
1330
+ };
1331
+ }
1332
+ function verifyEnrollmentSignature(request, secret) {
1333
+ const { signature, ...data } = request;
1334
+ if (!signature) {
1335
+ return false;
1336
+ }
1337
+ const identifier = data.macAddress || data.serialNumber || data.imei || data.androidId || "";
1338
+ const message = `${identifier}:${data.timestamp}`;
1339
+ const expectedSignature = createHmac("sha256", secret).update(message).digest("hex");
1340
+ try {
1341
+ return timingSafeEqual(
1342
+ Buffer.from(signature, "hex"),
1343
+ Buffer.from(expectedSignature, "hex")
1344
+ );
1345
+ } catch {
1346
+ return false;
1347
+ }
1348
+ }
1349
+ function generateDeviceToken(deviceId, secret, expirationSeconds) {
1350
+ const header = Buffer.from(
1351
+ JSON.stringify({ alg: "HS256", typ: "JWT" })
1352
+ ).toString("base64url");
1353
+ const now = Math.floor(Date.now() / 1e3);
1354
+ const payload = Buffer.from(
1355
+ JSON.stringify({
1356
+ sub: deviceId,
1357
+ iat: now,
1358
+ exp: now + expirationSeconds,
1359
+ iss: "openmdm"
1360
+ })
1361
+ ).toString("base64url");
1362
+ const signature = createHmac("sha256", secret).update(`${header}.${payload}`).digest("base64url");
1363
+ return `${header}.${payload}.${signature}`;
1364
+ }
1365
+
1366
+ export { ApplicationNotFoundError, AuthenticationError, AuthorizationError, DeviceNotFoundError, EnrollmentError, MDMError, PolicyNotFoundError, ValidationError, camelToSnake, createMDM, createWebhookManager, getColumnNames, getPrimaryKey, getTableNames, mdmSchema, snakeToCamel, transformToCamelCase, transformToSnakeCase, verifyWebhookSignature };
1367
+ //# sourceMappingURL=index.js.map
1368
+ //# sourceMappingURL=index.js.map