@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/LICENSE +21 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.js +1368 -0
- package/dist/index.js.map +1 -0
- package/dist/schema.d.ts +78 -0
- package/dist/schema.js +415 -0
- package/dist/schema.js.map +1 -0
- package/dist/types.d.ts +899 -0
- package/dist/types.js +49 -0
- package/dist/types.js.map +1 -0
- package/package.json +67 -0
- package/src/index.ts +1145 -0
- package/src/schema.ts +533 -0
- package/src/types.ts +1161 -0
- package/src/webhooks.ts +314 -0
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
|