@openmdm/core 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +205 -4
- package/dist/index.js +339 -35
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +203 -2
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/dashboard.ts +40 -0
- package/src/device-identity.ts +338 -0
- package/src/index.ts +222 -27
- package/src/logger.ts +98 -0
- package/src/plugin-storage.ts +25 -5
- package/src/types.ts +216 -1
- package/src/webhooks.ts +15 -4
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomUUID, createHmac, timingSafeEqual } from 'crypto';
|
|
1
|
+
import { randomUUID, createHmac, createPublicKey, verify, timingSafeEqual } from 'crypto';
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
|
|
@@ -72,14 +72,69 @@ var ValidationError = class extends MDMError {
|
|
|
72
72
|
super(message, "VALIDATION_ERROR", 400, details);
|
|
73
73
|
}
|
|
74
74
|
};
|
|
75
|
+
|
|
76
|
+
// src/logger.ts
|
|
77
|
+
function normalize(...args) {
|
|
78
|
+
if (args.length === 1) {
|
|
79
|
+
return { context: void 0, message: args[0] };
|
|
80
|
+
}
|
|
81
|
+
return { context: args[0], message: args[1] };
|
|
82
|
+
}
|
|
83
|
+
function createConsoleLogger(scope = []) {
|
|
84
|
+
const prefix = scope.length > 0 ? `[openmdm:${scope.join(":")}]` : "[openmdm]";
|
|
85
|
+
const render = (context) => {
|
|
86
|
+
if (!context || Object.keys(context).length === 0) return "";
|
|
87
|
+
try {
|
|
88
|
+
return " " + JSON.stringify(context);
|
|
89
|
+
} catch {
|
|
90
|
+
return " " + String(context);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
debug: (...args) => {
|
|
95
|
+
const { context, message } = normalize(...args);
|
|
96
|
+
if (!process.env.DEBUG) return;
|
|
97
|
+
console.debug(`${prefix} ${message}${render(context)}`);
|
|
98
|
+
},
|
|
99
|
+
info: (...args) => {
|
|
100
|
+
const { context, message } = normalize(...args);
|
|
101
|
+
console.log(`${prefix} ${message}${render(context)}`);
|
|
102
|
+
},
|
|
103
|
+
warn: (...args) => {
|
|
104
|
+
const { context, message } = normalize(...args);
|
|
105
|
+
console.warn(`${prefix} ${message}${render(context)}`);
|
|
106
|
+
},
|
|
107
|
+
error: (...args) => {
|
|
108
|
+
const { context, message } = normalize(...args);
|
|
109
|
+
console.error(`${prefix} ${message}${render(context)}`);
|
|
110
|
+
},
|
|
111
|
+
child: (bindings) => {
|
|
112
|
+
const componentPart = typeof bindings.component === "string" ? [bindings.component] : [];
|
|
113
|
+
return createConsoleLogger([...scope, ...componentPart]);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function createSilentLogger() {
|
|
118
|
+
const silent = {
|
|
119
|
+
debug: () => void 0,
|
|
120
|
+
info: () => void 0,
|
|
121
|
+
warn: () => void 0,
|
|
122
|
+
error: () => void 0,
|
|
123
|
+
child: () => silent
|
|
124
|
+
};
|
|
125
|
+
return silent;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/webhooks.ts
|
|
75
129
|
var DEFAULT_RETRY_CONFIG = {
|
|
76
130
|
maxRetries: 3,
|
|
77
131
|
initialDelay: 1e3,
|
|
78
132
|
maxDelay: 3e4
|
|
79
133
|
};
|
|
80
|
-
function createWebhookManager(config) {
|
|
134
|
+
function createWebhookManager(config, logger = createSilentLogger()) {
|
|
81
135
|
const endpoints = /* @__PURE__ */ new Map();
|
|
82
136
|
const retryConfig = { ...DEFAULT_RETRY_CONFIG, ...config.retry };
|
|
137
|
+
const log = logger.child({ component: "webhooks" });
|
|
83
138
|
if (config.endpoints) {
|
|
84
139
|
for (const endpoint of config.endpoints) {
|
|
85
140
|
endpoints.set(endpoint.id, endpoint);
|
|
@@ -181,9 +236,14 @@ function createWebhookManager(config) {
|
|
|
181
236
|
const results = await Promise.all(deliveryPromises);
|
|
182
237
|
for (const result of results) {
|
|
183
238
|
if (!result.success) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
239
|
+
log.error(
|
|
240
|
+
{
|
|
241
|
+
endpointId: result.endpointId,
|
|
242
|
+
statusCode: result.statusCode,
|
|
243
|
+
retryCount: result.retryCount,
|
|
244
|
+
err: result.error
|
|
245
|
+
},
|
|
246
|
+
"Webhook delivery failed"
|
|
187
247
|
);
|
|
188
248
|
}
|
|
189
249
|
}
|
|
@@ -1033,12 +1093,20 @@ function createMessageQueueManager(db) {
|
|
|
1033
1093
|
}
|
|
1034
1094
|
|
|
1035
1095
|
// src/dashboard.ts
|
|
1096
|
+
function assertNoTenantScopeRequested(tenantId, method) {
|
|
1097
|
+
if (tenantId) {
|
|
1098
|
+
throw new Error(
|
|
1099
|
+
`DashboardManager.${method} was called with a tenantId but the database adapter does not implement tenant-scoped dashboard queries. Implement the matching DatabaseAdapter method, or omit tenantId to accept global stats. See docs/proposals/tenant-rbac-audit.md for context.`
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1036
1103
|
function createDashboardManager(db) {
|
|
1037
1104
|
return {
|
|
1038
1105
|
async getStats(_tenantId) {
|
|
1039
1106
|
if (db.getDashboardStats) {
|
|
1040
1107
|
return db.getDashboardStats(_tenantId);
|
|
1041
1108
|
}
|
|
1109
|
+
assertNoTenantScopeRequested(_tenantId, "getStats");
|
|
1042
1110
|
const devices = await db.listDevices({
|
|
1043
1111
|
limit: 1e4
|
|
1044
1112
|
// Get all for counting
|
|
@@ -1096,6 +1164,7 @@ function createDashboardManager(db) {
|
|
|
1096
1164
|
if (db.getDeviceStatusBreakdown) {
|
|
1097
1165
|
return db.getDeviceStatusBreakdown(_tenantId);
|
|
1098
1166
|
}
|
|
1167
|
+
assertNoTenantScopeRequested(_tenantId, "getDeviceStatusBreakdown");
|
|
1099
1168
|
const devices = await db.listDevices({
|
|
1100
1169
|
limit: 1e4
|
|
1101
1170
|
});
|
|
@@ -1128,6 +1197,7 @@ function createDashboardManager(db) {
|
|
|
1128
1197
|
if (db.getEnrollmentTrend) {
|
|
1129
1198
|
return db.getEnrollmentTrend(days, _tenantId);
|
|
1130
1199
|
}
|
|
1200
|
+
assertNoTenantScopeRequested(_tenantId, "getEnrollmentTrend");
|
|
1131
1201
|
const now = /* @__PURE__ */ new Date();
|
|
1132
1202
|
const startDate = new Date(now.getTime() - days * 24 * 60 * 60 * 1e3);
|
|
1133
1203
|
const events = await db.listEvents({
|
|
@@ -1184,6 +1254,7 @@ function createDashboardManager(db) {
|
|
|
1184
1254
|
if (db.getCommandSuccessRates) {
|
|
1185
1255
|
return db.getCommandSuccessRates(_tenantId);
|
|
1186
1256
|
}
|
|
1257
|
+
assertNoTenantScopeRequested(_tenantId, "getCommandSuccessRates");
|
|
1187
1258
|
const now = /* @__PURE__ */ new Date();
|
|
1188
1259
|
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1e3);
|
|
1189
1260
|
const commands = await db.listCommands({ limit: 1e4 });
|
|
@@ -1232,6 +1303,7 @@ function createDashboardManager(db) {
|
|
|
1232
1303
|
if (db.getAppInstallationSummary) {
|
|
1233
1304
|
return db.getAppInstallationSummary(_tenantId);
|
|
1234
1305
|
}
|
|
1306
|
+
assertNoTenantScopeRequested(_tenantId, "getAppInstallationSummary");
|
|
1235
1307
|
const apps = await db.listApplications();
|
|
1236
1308
|
const appMap = new Map(apps.map((a) => [a.packageName, a]));
|
|
1237
1309
|
const byStatus = {
|
|
@@ -1277,7 +1349,6 @@ function createPluginStorageAdapter(db) {
|
|
|
1277
1349
|
const value = await db.getPluginValue(pluginName, key);
|
|
1278
1350
|
return value;
|
|
1279
1351
|
}
|
|
1280
|
-
console.warn("Plugin storage not supported by database adapter");
|
|
1281
1352
|
return null;
|
|
1282
1353
|
},
|
|
1283
1354
|
async set(pluginName, key, value) {
|
|
@@ -1285,20 +1356,17 @@ function createPluginStorageAdapter(db) {
|
|
|
1285
1356
|
await db.setPluginValue(pluginName, key, value);
|
|
1286
1357
|
return;
|
|
1287
1358
|
}
|
|
1288
|
-
console.warn("Plugin storage not supported by database adapter");
|
|
1289
1359
|
},
|
|
1290
1360
|
async delete(pluginName, key) {
|
|
1291
1361
|
if (db.deletePluginValue) {
|
|
1292
1362
|
await db.deletePluginValue(pluginName, key);
|
|
1293
1363
|
return;
|
|
1294
1364
|
}
|
|
1295
|
-
console.warn("Plugin storage not supported by database adapter");
|
|
1296
1365
|
},
|
|
1297
1366
|
async list(pluginName, prefix) {
|
|
1298
1367
|
if (db.listPluginKeys) {
|
|
1299
1368
|
return db.listPluginKeys(pluginName, prefix);
|
|
1300
1369
|
}
|
|
1301
|
-
console.warn("Plugin storage not supported by database adapter");
|
|
1302
1370
|
return [];
|
|
1303
1371
|
},
|
|
1304
1372
|
async clear(pluginName) {
|
|
@@ -1306,7 +1374,6 @@ function createPluginStorageAdapter(db) {
|
|
|
1306
1374
|
await db.clearPluginData(pluginName);
|
|
1307
1375
|
return;
|
|
1308
1376
|
}
|
|
1309
|
-
console.warn("Plugin storage not supported by database adapter");
|
|
1310
1377
|
}
|
|
1311
1378
|
};
|
|
1312
1379
|
}
|
|
@@ -1352,6 +1419,135 @@ function parsePluginKey(key) {
|
|
|
1352
1419
|
const [namespace, ...parts] = key.split(":");
|
|
1353
1420
|
return { namespace, parts };
|
|
1354
1421
|
}
|
|
1422
|
+
function importPublicKeyFromSpki(spkiBase64) {
|
|
1423
|
+
let buffer;
|
|
1424
|
+
try {
|
|
1425
|
+
buffer = Buffer.from(spkiBase64, "base64");
|
|
1426
|
+
} catch (err) {
|
|
1427
|
+
throw new InvalidPublicKeyError(
|
|
1428
|
+
"Public key is not valid base64",
|
|
1429
|
+
err instanceof Error ? err : void 0
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
if (buffer.length === 0) {
|
|
1433
|
+
throw new InvalidPublicKeyError("Public key is empty");
|
|
1434
|
+
}
|
|
1435
|
+
try {
|
|
1436
|
+
const key = createPublicKey({
|
|
1437
|
+
key: buffer,
|
|
1438
|
+
format: "der",
|
|
1439
|
+
type: "spki"
|
|
1440
|
+
});
|
|
1441
|
+
const asymmetricKeyType = key.asymmetricKeyType;
|
|
1442
|
+
if (asymmetricKeyType !== "ec") {
|
|
1443
|
+
throw new InvalidPublicKeyError(
|
|
1444
|
+
`Expected EC key, got ${asymmetricKeyType ?? "unknown"}`
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
const curve = key.asymmetricKeyDetails?.namedCurve;
|
|
1448
|
+
if (curve && curve !== "prime256v1" && curve !== "P-256") {
|
|
1449
|
+
throw new InvalidPublicKeyError(
|
|
1450
|
+
`Unsupported EC curve: ${curve}. Only P-256 is accepted.`
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
return key;
|
|
1454
|
+
} catch (err) {
|
|
1455
|
+
if (err instanceof InvalidPublicKeyError) throw err;
|
|
1456
|
+
throw new InvalidPublicKeyError(
|
|
1457
|
+
"Failed to parse SPKI public key",
|
|
1458
|
+
err instanceof Error ? err : void 0
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
function verifyEcdsaSignature(publicKey, message, signatureBase64) {
|
|
1463
|
+
const key = typeof publicKey === "string" ? importPublicKeyFromSpki(publicKey) : publicKey;
|
|
1464
|
+
let signatureBuffer;
|
|
1465
|
+
try {
|
|
1466
|
+
signatureBuffer = Buffer.from(signatureBase64, "base64");
|
|
1467
|
+
} catch {
|
|
1468
|
+
return false;
|
|
1469
|
+
}
|
|
1470
|
+
if (signatureBuffer.length === 0) return false;
|
|
1471
|
+
try {
|
|
1472
|
+
return verify("sha256", Buffer.from(message, "utf8"), key, signatureBuffer);
|
|
1473
|
+
} catch {
|
|
1474
|
+
return false;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
function canonicalEnrollmentMessage(parts) {
|
|
1478
|
+
return [
|
|
1479
|
+
parts.publicKey,
|
|
1480
|
+
parts.model,
|
|
1481
|
+
parts.manufacturer,
|
|
1482
|
+
parts.osVersion,
|
|
1483
|
+
parts.serialNumber ?? "",
|
|
1484
|
+
parts.imei ?? "",
|
|
1485
|
+
parts.macAddress ?? "",
|
|
1486
|
+
parts.androidId ?? "",
|
|
1487
|
+
parts.method,
|
|
1488
|
+
parts.timestamp,
|
|
1489
|
+
parts.challenge
|
|
1490
|
+
].join("|");
|
|
1491
|
+
}
|
|
1492
|
+
function canonicalDeviceRequestMessage(parts) {
|
|
1493
|
+
return [parts.deviceId, parts.timestamp, parts.body, parts.nonce ?? ""].join("|");
|
|
1494
|
+
}
|
|
1495
|
+
async function verifyDeviceRequest(opts) {
|
|
1496
|
+
const device = await opts.mdm.devices.get(opts.deviceId);
|
|
1497
|
+
if (!device) {
|
|
1498
|
+
return { ok: false, reason: "not-found" };
|
|
1499
|
+
}
|
|
1500
|
+
if (!device.publicKey) {
|
|
1501
|
+
return { ok: false, reason: "no-pinned-key", device };
|
|
1502
|
+
}
|
|
1503
|
+
let verified;
|
|
1504
|
+
try {
|
|
1505
|
+
verified = verifyEcdsaSignature(
|
|
1506
|
+
device.publicKey,
|
|
1507
|
+
opts.canonicalMessage,
|
|
1508
|
+
opts.signatureBase64
|
|
1509
|
+
);
|
|
1510
|
+
} catch (err) {
|
|
1511
|
+
opts.mdm.logger.child({ component: "device-identity" }).error(
|
|
1512
|
+
{
|
|
1513
|
+
deviceId: opts.deviceId,
|
|
1514
|
+
err: err instanceof Error ? err.message : String(err)
|
|
1515
|
+
},
|
|
1516
|
+
"Pinned public key failed to parse"
|
|
1517
|
+
);
|
|
1518
|
+
return { ok: false, reason: "signature-invalid", device };
|
|
1519
|
+
}
|
|
1520
|
+
if (!verified) {
|
|
1521
|
+
return { ok: false, reason: "signature-invalid", device };
|
|
1522
|
+
}
|
|
1523
|
+
return { ok: true, device };
|
|
1524
|
+
}
|
|
1525
|
+
var InvalidPublicKeyError = class extends Error {
|
|
1526
|
+
constructor(message, cause) {
|
|
1527
|
+
super(message);
|
|
1528
|
+
this.cause = cause;
|
|
1529
|
+
this.name = "InvalidPublicKeyError";
|
|
1530
|
+
}
|
|
1531
|
+
code = "INVALID_PUBLIC_KEY";
|
|
1532
|
+
};
|
|
1533
|
+
var PublicKeyMismatchError = class extends Error {
|
|
1534
|
+
constructor(deviceId) {
|
|
1535
|
+
super(
|
|
1536
|
+
`Device ${deviceId} is already enrolled with a different pinned public key`
|
|
1537
|
+
);
|
|
1538
|
+
this.deviceId = deviceId;
|
|
1539
|
+
this.name = "PublicKeyMismatchError";
|
|
1540
|
+
}
|
|
1541
|
+
code = "PUBLIC_KEY_MISMATCH";
|
|
1542
|
+
};
|
|
1543
|
+
var ChallengeInvalidError = class extends Error {
|
|
1544
|
+
constructor(message, challenge) {
|
|
1545
|
+
super(message);
|
|
1546
|
+
this.challenge = challenge;
|
|
1547
|
+
this.name = "ChallengeInvalidError";
|
|
1548
|
+
}
|
|
1549
|
+
code = "CHALLENGE_INVALID";
|
|
1550
|
+
};
|
|
1355
1551
|
|
|
1356
1552
|
// src/schema.ts
|
|
1357
1553
|
var mdmSchema = {
|
|
@@ -2042,9 +2238,19 @@ function wantsAgentProtocolV2(headerValue) {
|
|
|
2042
2238
|
// src/index.ts
|
|
2043
2239
|
function createMDM(config) {
|
|
2044
2240
|
const { database, push, enrollment, webhooks: webhooksConfig, plugins = [] } = config;
|
|
2241
|
+
const logger = config.logger ?? createConsoleLogger();
|
|
2242
|
+
const errorMessage = (err) => {
|
|
2243
|
+
if (err instanceof Error) return err.message;
|
|
2244
|
+
if (typeof err === "string") return err;
|
|
2245
|
+
try {
|
|
2246
|
+
return JSON.stringify(err);
|
|
2247
|
+
} catch {
|
|
2248
|
+
return String(err);
|
|
2249
|
+
}
|
|
2250
|
+
};
|
|
2045
2251
|
const eventHandlers = /* @__PURE__ */ new Map();
|
|
2046
|
-
const pushAdapter = push ? createPushAdapter(push, database) : createStubPushAdapter();
|
|
2047
|
-
const webhookManager = webhooksConfig ? createWebhookManager(webhooksConfig) : void 0;
|
|
2252
|
+
const pushAdapter = push ? createPushAdapter(push, database, logger) : createStubPushAdapter(logger);
|
|
2253
|
+
const webhookManager = webhooksConfig ? createWebhookManager(webhooksConfig, logger) : void 0;
|
|
2048
2254
|
const tenantManager = config.multiTenancy?.enabled ? createTenantManager(database) : void 0;
|
|
2049
2255
|
const authorizationManager = config.authorization?.enabled ? createAuthorizationManager(database) : void 0;
|
|
2050
2256
|
const auditManager = config.audit?.enabled ? createAuditManager(database) : void 0;
|
|
@@ -2078,11 +2284,14 @@ function createMDM(config) {
|
|
|
2078
2284
|
payload: eventRecord.payload
|
|
2079
2285
|
});
|
|
2080
2286
|
} catch (error) {
|
|
2081
|
-
|
|
2287
|
+
logger.error({ err: errorMessage(error), event }, "Failed to persist event");
|
|
2082
2288
|
}
|
|
2083
2289
|
if (webhookManager) {
|
|
2084
2290
|
webhookManager.deliver(eventRecord).catch((error) => {
|
|
2085
|
-
|
|
2291
|
+
logger.error(
|
|
2292
|
+
{ err: errorMessage(error), event },
|
|
2293
|
+
"Webhook delivery error"
|
|
2294
|
+
);
|
|
2086
2295
|
});
|
|
2087
2296
|
}
|
|
2088
2297
|
if (handlers) {
|
|
@@ -2090,7 +2299,10 @@ function createMDM(config) {
|
|
|
2090
2299
|
try {
|
|
2091
2300
|
await handler(eventRecord);
|
|
2092
2301
|
} catch (error) {
|
|
2093
|
-
|
|
2302
|
+
logger.error(
|
|
2303
|
+
{ err: errorMessage(error), event },
|
|
2304
|
+
"Event handler threw"
|
|
2305
|
+
);
|
|
2094
2306
|
}
|
|
2095
2307
|
}
|
|
2096
2308
|
}
|
|
@@ -2098,7 +2310,7 @@ function createMDM(config) {
|
|
|
2098
2310
|
try {
|
|
2099
2311
|
await config.onEvent(eventRecord);
|
|
2100
2312
|
} catch (error) {
|
|
2101
|
-
|
|
2313
|
+
logger.error({ err: errorMessage(error) }, "onEvent hook threw");
|
|
2102
2314
|
}
|
|
2103
2315
|
}
|
|
2104
2316
|
};
|
|
@@ -2579,7 +2791,13 @@ function createMDM(config) {
|
|
|
2579
2791
|
`Enrollment method '${request.method}' is not allowed`
|
|
2580
2792
|
);
|
|
2581
2793
|
}
|
|
2582
|
-
|
|
2794
|
+
const isPinnedKeyPath = Boolean(request.publicKey);
|
|
2795
|
+
if (!isPinnedKeyPath && enrollment?.pinnedKey?.required) {
|
|
2796
|
+
throw new EnrollmentError(
|
|
2797
|
+
"Pinned-key enrollment is required but the request carried no publicKey. The agent must generate a Keystore keypair and submit the SPKI public key alongside an ECDSA signature over the canonical enrollment message."
|
|
2798
|
+
);
|
|
2799
|
+
}
|
|
2800
|
+
if (!isPinnedKeyPath && enrollment?.deviceSecret) {
|
|
2583
2801
|
const isValid = verifyEnrollmentSignature(
|
|
2584
2802
|
request,
|
|
2585
2803
|
enrollment.deviceSecret
|
|
@@ -2588,6 +2806,65 @@ function createMDM(config) {
|
|
|
2588
2806
|
throw new EnrollmentError("Invalid enrollment signature");
|
|
2589
2807
|
}
|
|
2590
2808
|
}
|
|
2809
|
+
let challengeRecord = null;
|
|
2810
|
+
let importedPublicKey = null;
|
|
2811
|
+
if (isPinnedKeyPath) {
|
|
2812
|
+
if (!request.attestationChallenge) {
|
|
2813
|
+
throw new EnrollmentError(
|
|
2814
|
+
"Pinned-key enrollment requires attestationChallenge. Fetch a fresh challenge from /agent/enroll/challenge first."
|
|
2815
|
+
);
|
|
2816
|
+
}
|
|
2817
|
+
if (!database.consumeEnrollmentChallenge) {
|
|
2818
|
+
throw new EnrollmentError(
|
|
2819
|
+
"Pinned-key enrollment requires an adapter that implements enrollment challenge storage. Upgrade to a database adapter that supports it, or submit an HMAC-signed enrollment instead."
|
|
2820
|
+
);
|
|
2821
|
+
}
|
|
2822
|
+
try {
|
|
2823
|
+
importedPublicKey = importPublicKeyFromSpki(request.publicKey);
|
|
2824
|
+
} catch (err) {
|
|
2825
|
+
throw new EnrollmentError(
|
|
2826
|
+
err instanceof Error ? `Invalid enrollment public key: ${err.message}` : "Invalid enrollment public key"
|
|
2827
|
+
);
|
|
2828
|
+
}
|
|
2829
|
+
challengeRecord = await database.consumeEnrollmentChallenge(
|
|
2830
|
+
request.attestationChallenge
|
|
2831
|
+
);
|
|
2832
|
+
if (!challengeRecord) {
|
|
2833
|
+
throw new ChallengeInvalidError(
|
|
2834
|
+
"Enrollment challenge is missing, expired, or already consumed",
|
|
2835
|
+
request.attestationChallenge
|
|
2836
|
+
);
|
|
2837
|
+
}
|
|
2838
|
+
if (challengeRecord.expiresAt.getTime() < Date.now()) {
|
|
2839
|
+
throw new ChallengeInvalidError(
|
|
2840
|
+
"Enrollment challenge has expired",
|
|
2841
|
+
request.attestationChallenge
|
|
2842
|
+
);
|
|
2843
|
+
}
|
|
2844
|
+
const canonical = canonicalEnrollmentMessage({
|
|
2845
|
+
publicKey: request.publicKey,
|
|
2846
|
+
model: request.model,
|
|
2847
|
+
manufacturer: request.manufacturer,
|
|
2848
|
+
osVersion: request.osVersion,
|
|
2849
|
+
serialNumber: request.serialNumber,
|
|
2850
|
+
imei: request.imei,
|
|
2851
|
+
macAddress: request.macAddress,
|
|
2852
|
+
androidId: request.androidId,
|
|
2853
|
+
method: request.method,
|
|
2854
|
+
timestamp: request.timestamp,
|
|
2855
|
+
challenge: request.attestationChallenge
|
|
2856
|
+
});
|
|
2857
|
+
const verified = verifyEcdsaSignature(
|
|
2858
|
+
importedPublicKey,
|
|
2859
|
+
canonical,
|
|
2860
|
+
request.signature
|
|
2861
|
+
);
|
|
2862
|
+
if (!verified) {
|
|
2863
|
+
throw new EnrollmentError(
|
|
2864
|
+
"Invalid enrollment signature (device-pinned-key path)"
|
|
2865
|
+
);
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2591
2868
|
if (enrollment?.validate) {
|
|
2592
2869
|
const isValid = await enrollment.validate(request);
|
|
2593
2870
|
if (!isValid) {
|
|
@@ -2602,13 +2879,23 @@ function createMDM(config) {
|
|
|
2602
2879
|
}
|
|
2603
2880
|
let device = await database.findDeviceByEnrollmentId(enrollmentId);
|
|
2604
2881
|
if (device) {
|
|
2605
|
-
|
|
2882
|
+
if (isPinnedKeyPath && device.publicKey) {
|
|
2883
|
+
if (device.publicKey !== request.publicKey) {
|
|
2884
|
+
throw new PublicKeyMismatchError(device.id);
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
const updateInput = {
|
|
2606
2888
|
status: "enrolled",
|
|
2607
2889
|
model: request.model,
|
|
2608
2890
|
manufacturer: request.manufacturer,
|
|
2609
2891
|
osVersion: request.osVersion,
|
|
2610
2892
|
lastSync: /* @__PURE__ */ new Date()
|
|
2611
|
-
}
|
|
2893
|
+
};
|
|
2894
|
+
if (isPinnedKeyPath && !device.publicKey) {
|
|
2895
|
+
updateInput.publicKey = request.publicKey;
|
|
2896
|
+
updateInput.enrollmentMethod = "pinned-key";
|
|
2897
|
+
}
|
|
2898
|
+
device = await database.updateDevice(device.id, updateInput);
|
|
2612
2899
|
} else if (enrollment?.autoEnroll) {
|
|
2613
2900
|
device = await database.createDevice({
|
|
2614
2901
|
enrollmentId,
|
|
@@ -2621,6 +2908,12 @@ function createMDM(config) {
|
|
|
2621
2908
|
androidId: request.androidId,
|
|
2622
2909
|
policyId: request.policyId || enrollment.defaultPolicyId
|
|
2623
2910
|
});
|
|
2911
|
+
if (isPinnedKeyPath) {
|
|
2912
|
+
device = await database.updateDevice(device.id, {
|
|
2913
|
+
publicKey: request.publicKey,
|
|
2914
|
+
enrollmentMethod: "pinned-key"
|
|
2915
|
+
});
|
|
2916
|
+
}
|
|
2624
2917
|
if (enrollment.defaultGroupId) {
|
|
2625
2918
|
await database.addDeviceToGroup(device.id, enrollment.defaultGroupId);
|
|
2626
2919
|
}
|
|
@@ -2635,6 +2928,12 @@ function createMDM(config) {
|
|
|
2635
2928
|
macAddress: request.macAddress,
|
|
2636
2929
|
androidId: request.androidId
|
|
2637
2930
|
});
|
|
2931
|
+
if (isPinnedKeyPath) {
|
|
2932
|
+
device = await database.updateDevice(device.id, {
|
|
2933
|
+
publicKey: request.publicKey,
|
|
2934
|
+
enrollmentMethod: "pinned-key"
|
|
2935
|
+
});
|
|
2936
|
+
}
|
|
2638
2937
|
} else {
|
|
2639
2938
|
throw new EnrollmentError(
|
|
2640
2939
|
"Device not registered and auto-enroll is disabled"
|
|
@@ -2775,6 +3074,7 @@ function createMDM(config) {
|
|
|
2775
3074
|
push: pushAdapter,
|
|
2776
3075
|
webhooks: webhookManager,
|
|
2777
3076
|
db: database,
|
|
3077
|
+
logger,
|
|
2778
3078
|
config,
|
|
2779
3079
|
on,
|
|
2780
3080
|
emit,
|
|
@@ -2797,11 +3097,11 @@ function createMDM(config) {
|
|
|
2797
3097
|
if (plugin.onInit) {
|
|
2798
3098
|
try {
|
|
2799
3099
|
await plugin.onInit(instance);
|
|
2800
|
-
|
|
3100
|
+
logger.info({ plugin: plugin.name }, "Plugin initialized");
|
|
2801
3101
|
} catch (error) {
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
3102
|
+
logger.error(
|
|
3103
|
+
{ plugin: plugin.name, err: errorMessage(error) },
|
|
3104
|
+
"Failed to initialize plugin"
|
|
2805
3105
|
);
|
|
2806
3106
|
}
|
|
2807
3107
|
}
|
|
@@ -2809,21 +3109,23 @@ function createMDM(config) {
|
|
|
2809
3109
|
})();
|
|
2810
3110
|
return instance;
|
|
2811
3111
|
}
|
|
2812
|
-
function createPushAdapter(config, database) {
|
|
3112
|
+
function createPushAdapter(config, database, logger) {
|
|
2813
3113
|
if (!config) {
|
|
2814
|
-
return createStubPushAdapter();
|
|
3114
|
+
return createStubPushAdapter(logger);
|
|
2815
3115
|
}
|
|
3116
|
+
const pushLogger = logger.child({ component: "push" });
|
|
2816
3117
|
return {
|
|
2817
3118
|
async send(deviceId, message) {
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
3119
|
+
pushLogger.debug(
|
|
3120
|
+
{ deviceId, type: message.type, payload: message.payload },
|
|
3121
|
+
"send"
|
|
2821
3122
|
);
|
|
2822
3123
|
return { success: true, messageId: randomUUID() };
|
|
2823
3124
|
},
|
|
2824
3125
|
async sendBatch(deviceIds, message) {
|
|
2825
|
-
|
|
2826
|
-
|
|
3126
|
+
pushLogger.debug(
|
|
3127
|
+
{ count: deviceIds.length, type: message.type },
|
|
3128
|
+
"sendBatch"
|
|
2827
3129
|
);
|
|
2828
3130
|
const results = deviceIds.map((deviceId) => ({
|
|
2829
3131
|
deviceId,
|
|
@@ -2853,15 +3155,17 @@ function createPushAdapter(config, database) {
|
|
|
2853
3155
|
}
|
|
2854
3156
|
};
|
|
2855
3157
|
}
|
|
2856
|
-
function createStubPushAdapter() {
|
|
3158
|
+
function createStubPushAdapter(logger) {
|
|
3159
|
+
const stubLogger = logger.child({ component: "push-stub" });
|
|
2857
3160
|
return {
|
|
2858
3161
|
async send(deviceId, message) {
|
|
2859
|
-
|
|
3162
|
+
stubLogger.debug({ deviceId, type: message.type }, "send (stub)");
|
|
2860
3163
|
return { success: true, messageId: "stub" };
|
|
2861
3164
|
},
|
|
2862
3165
|
async sendBatch(deviceIds, message) {
|
|
2863
|
-
|
|
2864
|
-
|
|
3166
|
+
stubLogger.debug(
|
|
3167
|
+
{ count: deviceIds.length, type: message.type },
|
|
3168
|
+
"sendBatch (stub)"
|
|
2865
3169
|
);
|
|
2866
3170
|
return {
|
|
2867
3171
|
successCount: deviceIds.length,
|
|
@@ -2917,6 +3221,6 @@ function generateDeviceToken(deviceId, secret, expirationSeconds) {
|
|
|
2917
3221
|
return `${header}.${payload}.${signature}`;
|
|
2918
3222
|
}
|
|
2919
3223
|
|
|
2920
|
-
export { AGENT_PROTOCOL_HEADER, AGENT_PROTOCOL_V2, ApplicationNotFoundError, AuthenticationError, AuthorizationError, CommandNotFoundError, DeviceNotFoundError, EnrollmentError, GroupNotFoundError, MDMError, PolicyNotFoundError, RoleNotFoundError, TenantNotFoundError, UserNotFoundError, ValidationError, agentFail, agentOk, camelToSnake, createAuditManager, createAuthorizationManager, createDashboardManager, createMDM, createMemoryPluginStorageAdapter, createMessageQueueManager, createPluginKey, createPluginStorageAdapter, createScheduleManager, createTenantManager, createWebhookManager, getColumnNames, getPrimaryKey, getTableNames, mdmSchema, parsePluginKey, snakeToCamel, transformToCamelCase, transformToSnakeCase, verifyEnrollmentSignature, verifyWebhookSignature, wantsAgentProtocolV2 };
|
|
3224
|
+
export { AGENT_PROTOCOL_HEADER, AGENT_PROTOCOL_V2, ApplicationNotFoundError, AuthenticationError, AuthorizationError, ChallengeInvalidError, CommandNotFoundError, DeviceNotFoundError, EnrollmentError, GroupNotFoundError, InvalidPublicKeyError, MDMError, PolicyNotFoundError, PublicKeyMismatchError, RoleNotFoundError, TenantNotFoundError, UserNotFoundError, ValidationError, agentFail, agentOk, camelToSnake, canonicalDeviceRequestMessage, canonicalEnrollmentMessage, createAuditManager, createAuthorizationManager, createConsoleLogger, createDashboardManager, createMDM, createMemoryPluginStorageAdapter, createMessageQueueManager, createPluginKey, createPluginStorageAdapter, createScheduleManager, createSilentLogger, createTenantManager, createWebhookManager, getColumnNames, getPrimaryKey, getTableNames, importPublicKeyFromSpki, mdmSchema, parsePluginKey, snakeToCamel, transformToCamelCase, transformToSnakeCase, verifyDeviceRequest, verifyEcdsaSignature, verifyEnrollmentSignature, verifyWebhookSignature, wantsAgentProtocolV2 };
|
|
2921
3225
|
//# sourceMappingURL=index.js.map
|
|
2922
3226
|
//# sourceMappingURL=index.js.map
|