@openmdm/plugin-geofence 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,519 @@
1
+ // src/index.ts
2
+ var EARTH_RADIUS_METERS = 6371e3;
3
+ function haversineDistance(lat1, lon1, lat2, lon2) {
4
+ const toRad = (deg) => deg * Math.PI / 180;
5
+ const dLat = toRad(lat2 - lat1);
6
+ const dLon = toRad(lon2 - lon1);
7
+ const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
8
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
9
+ return EARTH_RADIUS_METERS * c;
10
+ }
11
+ function isInsideCircle(point, center, radiusMeters) {
12
+ const distance = haversineDistance(
13
+ point.latitude,
14
+ point.longitude,
15
+ center.latitude,
16
+ center.longitude
17
+ );
18
+ return distance <= radiusMeters;
19
+ }
20
+ function isInsidePolygon(point, vertices) {
21
+ if (vertices.length < 3) return false;
22
+ let inside = false;
23
+ const n = vertices.length;
24
+ for (let i = 0, j = n - 1; i < n; j = i++) {
25
+ const xi = vertices[i].longitude;
26
+ const yi = vertices[i].latitude;
27
+ const xj = vertices[j].longitude;
28
+ const yj = vertices[j].latitude;
29
+ const intersect = yi > point.latitude !== yj > point.latitude && point.longitude < (xj - xi) * (point.latitude - yi) / (yj - yi) + xi;
30
+ if (intersect) inside = !inside;
31
+ }
32
+ return inside;
33
+ }
34
+ function isZoneScheduleActive(schedule) {
35
+ if (!schedule) return true;
36
+ const now = /* @__PURE__ */ new Date();
37
+ const tz = schedule.timezone || "UTC";
38
+ const formatter = new Intl.DateTimeFormat("en-US", {
39
+ timeZone: tz,
40
+ hour: "2-digit",
41
+ minute: "2-digit",
42
+ hour12: false,
43
+ weekday: "short"
44
+ });
45
+ const parts = formatter.formatToParts(now);
46
+ const currentHour = parseInt(parts.find((p) => p.type === "hour")?.value || "0");
47
+ const currentMinute = parseInt(parts.find((p) => p.type === "minute")?.value || "0");
48
+ const weekdayStr = parts.find((p) => p.type === "weekday")?.value || "";
49
+ const weekdayMap = {
50
+ Sun: 0,
51
+ Mon: 1,
52
+ Tue: 2,
53
+ Wed: 3,
54
+ Thu: 4,
55
+ Fri: 5,
56
+ Sat: 6
57
+ };
58
+ const currentDay = weekdayMap[weekdayStr] ?? now.getDay();
59
+ if (schedule.daysOfWeek && !schedule.daysOfWeek.includes(currentDay)) {
60
+ return false;
61
+ }
62
+ if (schedule.startTime && schedule.endTime) {
63
+ const [startHour, startMin] = schedule.startTime.split(":").map(Number);
64
+ const [endHour, endMin] = schedule.endTime.split(":").map(Number);
65
+ const currentMins = currentHour * 60 + currentMinute;
66
+ const startMins = startHour * 60 + startMin;
67
+ const endMins = endHour * 60 + endMin;
68
+ if (startMins <= endMins) {
69
+ if (currentMins < startMins || currentMins > endMins) {
70
+ return false;
71
+ }
72
+ } else {
73
+ if (currentMins < startMins && currentMins > endMins) {
74
+ return false;
75
+ }
76
+ }
77
+ }
78
+ return true;
79
+ }
80
+ function geofencePlugin(options = {}) {
81
+ const {
82
+ onEnter,
83
+ onExit,
84
+ onInside,
85
+ defaultDwellTime = 0,
86
+ trackHistory = false,
87
+ maxHistoryEntries = 1e3
88
+ } = options;
89
+ let mdm;
90
+ const zones = /* @__PURE__ */ new Map();
91
+ const deviceZoneStates = /* @__PURE__ */ new Map();
92
+ const locationHistory = /* @__PURE__ */ new Map();
93
+ let zoneIdCounter = 1;
94
+ function generateZoneId() {
95
+ return `zone_${Date.now()}_${zoneIdCounter++}`;
96
+ }
97
+ function isInsideZone(location, zone) {
98
+ if (!zone.enabled) return false;
99
+ if (!isZoneScheduleActive(zone.schedule)) return false;
100
+ if (zone.type === "circle" && zone.center && zone.radius) {
101
+ return isInsideCircle(location, zone.center, zone.radius);
102
+ }
103
+ if (zone.type === "polygon" && zone.vertices) {
104
+ return isInsidePolygon(location, zone.vertices);
105
+ }
106
+ return false;
107
+ }
108
+ async function processLocation(device, location) {
109
+ if (!deviceZoneStates.has(device.id)) {
110
+ deviceZoneStates.set(device.id, /* @__PURE__ */ new Map());
111
+ }
112
+ const deviceStates = deviceZoneStates.get(device.id);
113
+ const currentZones = [];
114
+ for (const [zoneId, zone] of zones) {
115
+ const wasInside = deviceStates.get(zoneId)?.inside ?? false;
116
+ const isInside = isInsideZone(location, zone);
117
+ if (isInside) {
118
+ currentZones.push(zoneId);
119
+ }
120
+ if (isInside && !wasInside) {
121
+ const enteredAt = /* @__PURE__ */ new Date();
122
+ deviceStates.set(zoneId, {
123
+ deviceId: device.id,
124
+ zoneId,
125
+ inside: true,
126
+ enteredAt
127
+ });
128
+ const dwellTime = zone.metadata?.dwellTime ?? defaultDwellTime;
129
+ if (dwellTime > 0) {
130
+ setTimeout(async () => {
131
+ const state = deviceStates.get(zoneId);
132
+ if (state?.inside && state.enteredAt === enteredAt) {
133
+ await triggerEnter(device, zone);
134
+ }
135
+ }, dwellTime);
136
+ } else {
137
+ await triggerEnter(device, zone);
138
+ }
139
+ } else if (!isInside && wasInside) {
140
+ const state = deviceStates.get(zoneId);
141
+ deviceStates.set(zoneId, {
142
+ deviceId: device.id,
143
+ zoneId,
144
+ inside: false,
145
+ enteredAt: state?.enteredAt,
146
+ exitedAt: /* @__PURE__ */ new Date(),
147
+ dwellTime: state?.enteredAt ? Date.now() - state.enteredAt.getTime() : void 0
148
+ });
149
+ await triggerExit(device, zone);
150
+ } else if (isInside) {
151
+ await onInside?.(device, zone);
152
+ }
153
+ }
154
+ if (trackHistory) {
155
+ if (!locationHistory.has(device.id)) {
156
+ locationHistory.set(device.id, []);
157
+ }
158
+ const history = locationHistory.get(device.id);
159
+ history.push({
160
+ deviceId: device.id,
161
+ location,
162
+ zones: currentZones,
163
+ timestamp: /* @__PURE__ */ new Date()
164
+ });
165
+ if (history.length > maxHistoryEntries) {
166
+ history.splice(0, history.length - maxHistoryEntries);
167
+ }
168
+ }
169
+ }
170
+ async function triggerEnter(device, zone) {
171
+ console.log(`[OpenMDM Geofence] Device ${device.id} entered zone ${zone.name}`);
172
+ await onEnter?.(device, zone);
173
+ await mdm.emit("custom", {
174
+ type: "geofence.enter",
175
+ deviceId: device.id,
176
+ zoneId: zone.id,
177
+ zoneName: zone.name,
178
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
179
+ });
180
+ if (zone.onEnter) {
181
+ await executeAction(device, zone.onEnter, zone, "enter");
182
+ }
183
+ if (zone.policyOverride) {
184
+ await mdm.devices.assignPolicy(device.id, zone.policyOverride);
185
+ }
186
+ }
187
+ async function triggerExit(device, zone) {
188
+ console.log(`[OpenMDM Geofence] Device ${device.id} exited zone ${zone.name}`);
189
+ await onExit?.(device, zone);
190
+ const state = deviceZoneStates.get(device.id)?.get(zone.id);
191
+ await mdm.emit("custom", {
192
+ type: "geofence.exit",
193
+ deviceId: device.id,
194
+ zoneId: zone.id,
195
+ zoneName: zone.name,
196
+ dwellTime: state?.dwellTime,
197
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
198
+ });
199
+ if (zone.onExit) {
200
+ await executeAction(device, zone.onExit, zone, "exit");
201
+ }
202
+ if (zone.policyOverride && device.policyId !== zone.policyOverride) {
203
+ const stillInOverrideZone = Array.from(zones.values()).some(
204
+ (z) => z.id !== zone.id && z.policyOverride === zone.policyOverride && deviceZoneStates.get(device.id)?.get(z.id)?.inside
205
+ );
206
+ if (!stillInOverrideZone) {
207
+ console.log(
208
+ `[OpenMDM Geofence] Policy override ended for device ${device.id}`
209
+ );
210
+ }
211
+ }
212
+ }
213
+ async function executeAction(device, action, zone, trigger) {
214
+ switch (action.type) {
215
+ case "notify":
216
+ if (action.notification) {
217
+ await mdm.commands.send({
218
+ deviceId: device.id,
219
+ type: "sendNotification",
220
+ payload: {
221
+ title: action.notification.title,
222
+ body: action.notification.body
223
+ }
224
+ });
225
+ }
226
+ break;
227
+ case "command":
228
+ if (action.command) {
229
+ await mdm.commands.send({
230
+ deviceId: device.id,
231
+ type: action.command.type,
232
+ payload: action.command.payload
233
+ });
234
+ }
235
+ break;
236
+ case "policy":
237
+ if (action.policyId) {
238
+ await mdm.devices.assignPolicy(device.id, action.policyId);
239
+ }
240
+ break;
241
+ case "webhook":
242
+ if (action.webhook) {
243
+ try {
244
+ await fetch(action.webhook.url, {
245
+ method: action.webhook.method || "POST",
246
+ headers: {
247
+ "Content-Type": "application/json",
248
+ ...action.webhook.headers
249
+ },
250
+ body: JSON.stringify({
251
+ event: `geofence.${trigger}`,
252
+ device: {
253
+ id: device.id,
254
+ enrollmentId: device.enrollmentId
255
+ },
256
+ zone: {
257
+ id: zone.id,
258
+ name: zone.name
259
+ },
260
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
261
+ ...action.webhook.body
262
+ })
263
+ });
264
+ } catch (error) {
265
+ console.error(
266
+ `[OpenMDM Geofence] Webhook failed for zone ${zone.id}:`,
267
+ error
268
+ );
269
+ }
270
+ }
271
+ break;
272
+ }
273
+ }
274
+ const routes = [
275
+ // List all zones
276
+ {
277
+ method: "GET",
278
+ path: "/geofence/zones",
279
+ auth: true,
280
+ admin: true,
281
+ handler: async (context) => {
282
+ const zoneList = Array.from(zones.values()).map((zone) => ({
283
+ ...zone,
284
+ createdAt: zone.createdAt.toISOString(),
285
+ updatedAt: zone.updatedAt.toISOString()
286
+ }));
287
+ return context.json({ zones: zoneList });
288
+ }
289
+ },
290
+ // Get zone by ID
291
+ {
292
+ method: "GET",
293
+ path: "/geofence/zones/:zoneId",
294
+ auth: true,
295
+ admin: true,
296
+ handler: async (context) => {
297
+ const { zoneId } = context.req.param();
298
+ const zone = zones.get(zoneId);
299
+ if (!zone) {
300
+ return context.json({ error: "Zone not found" }, 404);
301
+ }
302
+ return context.json({
303
+ ...zone,
304
+ createdAt: zone.createdAt.toISOString(),
305
+ updatedAt: zone.updatedAt.toISOString()
306
+ });
307
+ }
308
+ },
309
+ // Create zone
310
+ {
311
+ method: "POST",
312
+ path: "/geofence/zones",
313
+ auth: true,
314
+ admin: true,
315
+ handler: async (context) => {
316
+ const body = await context.req.json();
317
+ if (body.type === "circle") {
318
+ if (!body.center || !body.radius) {
319
+ return context.json(
320
+ { error: "Circle zone requires center and radius" },
321
+ 400
322
+ );
323
+ }
324
+ } else if (body.type === "polygon") {
325
+ if (!body.vertices || body.vertices.length < 3) {
326
+ return context.json(
327
+ { error: "Polygon zone requires at least 3 vertices" },
328
+ 400
329
+ );
330
+ }
331
+ }
332
+ const zone = {
333
+ id: generateZoneId(),
334
+ name: body.name,
335
+ description: body.description,
336
+ type: body.type,
337
+ enabled: body.enabled ?? true,
338
+ center: body.center,
339
+ radius: body.radius,
340
+ vertices: body.vertices,
341
+ onEnter: body.onEnter,
342
+ onExit: body.onExit,
343
+ policyOverride: body.policyOverride,
344
+ schedule: body.schedule,
345
+ metadata: body.metadata,
346
+ createdAt: /* @__PURE__ */ new Date(),
347
+ updatedAt: /* @__PURE__ */ new Date()
348
+ };
349
+ zones.set(zone.id, zone);
350
+ return context.json(
351
+ {
352
+ ...zone,
353
+ createdAt: zone.createdAt.toISOString(),
354
+ updatedAt: zone.updatedAt.toISOString()
355
+ },
356
+ 201
357
+ );
358
+ }
359
+ },
360
+ // Update zone
361
+ {
362
+ method: "PUT",
363
+ path: "/geofence/zones/:zoneId",
364
+ auth: true,
365
+ admin: true,
366
+ handler: async (context) => {
367
+ const { zoneId } = context.req.param();
368
+ const body = await context.req.json();
369
+ const existing = zones.get(zoneId);
370
+ if (!existing) {
371
+ return context.json({ error: "Zone not found" }, 404);
372
+ }
373
+ const updated = {
374
+ ...existing,
375
+ ...body,
376
+ id: zoneId,
377
+ type: existing.type,
378
+ // Type cannot be changed
379
+ createdAt: existing.createdAt,
380
+ updatedAt: /* @__PURE__ */ new Date()
381
+ };
382
+ zones.set(zoneId, updated);
383
+ return context.json({
384
+ ...updated,
385
+ createdAt: updated.createdAt.toISOString(),
386
+ updatedAt: updated.updatedAt.toISOString()
387
+ });
388
+ }
389
+ },
390
+ // Delete zone
391
+ {
392
+ method: "DELETE",
393
+ path: "/geofence/zones/:zoneId",
394
+ auth: true,
395
+ admin: true,
396
+ handler: async (context) => {
397
+ const { zoneId } = context.req.param();
398
+ if (!zones.has(zoneId)) {
399
+ return context.json({ error: "Zone not found" }, 404);
400
+ }
401
+ zones.delete(zoneId);
402
+ for (const states of deviceZoneStates.values()) {
403
+ states.delete(zoneId);
404
+ }
405
+ return context.json({ success: true });
406
+ }
407
+ },
408
+ // Get devices in zone
409
+ {
410
+ method: "GET",
411
+ path: "/geofence/zones/:zoneId/devices",
412
+ auth: true,
413
+ admin: true,
414
+ handler: async (context) => {
415
+ const { zoneId } = context.req.param();
416
+ if (!zones.has(zoneId)) {
417
+ return context.json({ error: "Zone not found" }, 404);
418
+ }
419
+ const devicesInZone = [];
420
+ for (const [deviceId, states] of deviceZoneStates) {
421
+ if (states.get(zoneId)?.inside) {
422
+ devicesInZone.push(deviceId);
423
+ }
424
+ }
425
+ return context.json({ deviceIds: devicesInZone });
426
+ }
427
+ },
428
+ // Get device zone status
429
+ {
430
+ method: "GET",
431
+ path: "/geofence/devices/:deviceId/zones",
432
+ auth: true,
433
+ admin: true,
434
+ handler: async (context) => {
435
+ const { deviceId } = context.req.param();
436
+ const states = deviceZoneStates.get(deviceId);
437
+ if (!states) {
438
+ return context.json({ zones: [] });
439
+ }
440
+ const zoneStates = Array.from(states.values()).map((state) => ({
441
+ ...state,
442
+ enteredAt: state.enteredAt?.toISOString(),
443
+ exitedAt: state.exitedAt?.toISOString(),
444
+ zoneName: zones.get(state.zoneId)?.name
445
+ }));
446
+ return context.json({ zones: zoneStates });
447
+ }
448
+ },
449
+ // Get device location history
450
+ {
451
+ method: "GET",
452
+ path: "/geofence/devices/:deviceId/history",
453
+ auth: true,
454
+ admin: true,
455
+ handler: async (context) => {
456
+ const { deviceId } = context.req.param();
457
+ const history = locationHistory.get(deviceId) || [];
458
+ return context.json({
459
+ history: history.map((entry) => ({
460
+ ...entry,
461
+ timestamp: entry.timestamp.toISOString()
462
+ }))
463
+ });
464
+ }
465
+ },
466
+ // Check if point is in any zone
467
+ {
468
+ method: "POST",
469
+ path: "/geofence/check",
470
+ auth: true,
471
+ handler: async (context) => {
472
+ const body = await context.req.json();
473
+ const { latitude, longitude } = body;
474
+ const matchingZones = [];
475
+ const location = { latitude, longitude, timestamp: /* @__PURE__ */ new Date() };
476
+ for (const [zoneId, zone] of zones) {
477
+ if (isInsideZone(location, zone)) {
478
+ matchingZones.push(zoneId);
479
+ }
480
+ }
481
+ return context.json({
482
+ inside: matchingZones.length > 0,
483
+ zones: matchingZones.map((id) => ({
484
+ id,
485
+ name: zones.get(id)?.name
486
+ }))
487
+ });
488
+ }
489
+ }
490
+ ];
491
+ return {
492
+ name: "geofence",
493
+ version: "1.0.0",
494
+ async onInit(instance) {
495
+ mdm = instance;
496
+ console.log("[OpenMDM Geofence] Plugin initialized");
497
+ },
498
+ async onDestroy() {
499
+ zones.clear();
500
+ deviceZoneStates.clear();
501
+ locationHistory.clear();
502
+ console.log("[OpenMDM Geofence] Plugin destroyed");
503
+ },
504
+ routes,
505
+ async onHeartbeat(device, heartbeat) {
506
+ if (heartbeat.location) {
507
+ await processLocation(device, heartbeat.location);
508
+ }
509
+ }
510
+ };
511
+ }
512
+ export {
513
+ geofencePlugin,
514
+ haversineDistance,
515
+ isInsideCircle,
516
+ isInsidePolygon,
517
+ isZoneScheduleActive
518
+ };
519
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * OpenMDM Geofencing Plugin\n *\n * Provides location-based policy enforcement and monitoring.\n * Supports circular and polygon geofence zones with enter/exit actions.\n *\n * @example\n * ```typescript\n * import { createMDM } from '@openmdm/core';\n * import { geofencePlugin } from '@openmdm/plugin-geofence';\n *\n * const mdm = createMDM({\n * database: drizzleAdapter(db),\n * plugins: [\n * geofencePlugin({\n * onEnter: async (device, zone) => {\n * console.log(`Device ${device.id} entered ${zone.name}`);\n * },\n * onExit: async (device, zone) => {\n * console.log(`Device ${device.id} left ${zone.name}`);\n * },\n * }),\n * ],\n * });\n * ```\n */\n\nimport type {\n MDMPlugin,\n MDMInstance,\n Device,\n DeviceLocation,\n Heartbeat,\n PluginRoute,\n} from '@openmdm/core';\n\n// ============================================\n// Geofence Types\n// ============================================\n\nexport interface GeofencePluginOptions {\n /**\n * Callback when device enters a geofence zone\n */\n onEnter?: (device: Device, zone: GeofenceZone) => Promise<void>;\n\n /**\n * Callback when device exits a geofence zone\n */\n onExit?: (device: Device, zone: GeofenceZone) => Promise<void>;\n\n /**\n * Callback when device is inside a zone during heartbeat\n */\n onInside?: (device: Device, zone: GeofenceZone) => Promise<void>;\n\n /**\n * Default dwell time (ms) before triggering enter event (default: 0)\n */\n defaultDwellTime?: number;\n\n /**\n * Enable location history tracking (default: false)\n */\n trackHistory?: boolean;\n\n /**\n * Maximum history entries per device (default: 1000)\n */\n maxHistoryEntries?: number;\n}\n\nexport interface GeofenceZone {\n id: string;\n name: string;\n description?: string;\n type: 'circle' | 'polygon';\n enabled: boolean;\n\n // Circle zone\n center?: {\n latitude: number;\n longitude: number;\n };\n radius?: number; // meters\n\n // Polygon zone\n vertices?: Array<{\n latitude: number;\n longitude: number;\n }>;\n\n // Actions\n onEnter?: GeofenceAction;\n onExit?: GeofenceAction;\n\n // Policy override when inside zone\n policyOverride?: string;\n\n // Scheduling\n schedule?: GeofenceSchedule;\n\n // Metadata\n metadata?: Record<string, unknown>;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface GeofenceAction {\n /** Action type */\n type: 'notify' | 'command' | 'policy' | 'webhook' | 'none';\n\n /** Notification message */\n notification?: {\n title: string;\n body: string;\n };\n\n /** Command to execute */\n command?: {\n type: string;\n payload?: Record<string, unknown>;\n };\n\n /** Policy to apply */\n policyId?: string;\n\n /** Webhook to call */\n webhook?: {\n url: string;\n method?: 'GET' | 'POST';\n headers?: Record<string, string>;\n body?: Record<string, unknown>;\n };\n}\n\nexport interface GeofenceSchedule {\n /** Days of week (0=Sunday, 6=Saturday) */\n daysOfWeek?: number[];\n\n /** Start time (HH:mm) */\n startTime?: string;\n\n /** End time (HH:mm) */\n endTime?: string;\n\n /** Timezone (default: UTC) */\n timezone?: string;\n}\n\nexport interface DeviceZoneState {\n deviceId: string;\n zoneId: string;\n inside: boolean;\n enteredAt?: Date;\n exitedAt?: Date;\n dwellTime?: number;\n}\n\nexport interface LocationHistoryEntry {\n deviceId: string;\n location: DeviceLocation;\n zones: string[]; // Zone IDs device was inside\n timestamp: Date;\n}\n\nexport interface CreateGeofenceZoneInput {\n name: string;\n description?: string;\n type: 'circle' | 'polygon';\n enabled?: boolean;\n center?: { latitude: number; longitude: number };\n radius?: number;\n vertices?: Array<{ latitude: number; longitude: number }>;\n onEnter?: GeofenceAction;\n onExit?: GeofenceAction;\n policyOverride?: string;\n schedule?: GeofenceSchedule;\n metadata?: Record<string, unknown>;\n}\n\nexport interface UpdateGeofenceZoneInput {\n name?: string;\n description?: string;\n enabled?: boolean;\n center?: { latitude: number; longitude: number };\n radius?: number;\n vertices?: Array<{ latitude: number; longitude: number }>;\n onEnter?: GeofenceAction;\n onExit?: GeofenceAction;\n policyOverride?: string;\n schedule?: GeofenceSchedule;\n metadata?: Record<string, unknown>;\n}\n\n// ============================================\n// Geo Utilities\n// ============================================\n\nconst EARTH_RADIUS_METERS = 6371000;\n\n/**\n * Calculate distance between two points using Haversine formula\n */\nfunction haversineDistance(\n lat1: number,\n lon1: number,\n lat2: number,\n lon2: number\n): number {\n const toRad = (deg: number) => (deg * Math.PI) / 180;\n\n const dLat = toRad(lat2 - lat1);\n const dLon = toRad(lon2 - lon1);\n\n const a =\n Math.sin(dLat / 2) * Math.sin(dLat / 2) +\n Math.cos(toRad(lat1)) *\n Math.cos(toRad(lat2)) *\n Math.sin(dLon / 2) *\n Math.sin(dLon / 2);\n\n const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));\n\n return EARTH_RADIUS_METERS * c;\n}\n\n/**\n * Check if point is inside circular zone\n */\nfunction isInsideCircle(\n point: { latitude: number; longitude: number },\n center: { latitude: number; longitude: number },\n radiusMeters: number\n): boolean {\n const distance = haversineDistance(\n point.latitude,\n point.longitude,\n center.latitude,\n center.longitude\n );\n return distance <= radiusMeters;\n}\n\n/**\n * Check if point is inside polygon using ray casting algorithm\n */\nfunction isInsidePolygon(\n point: { latitude: number; longitude: number },\n vertices: Array<{ latitude: number; longitude: number }>\n): boolean {\n if (vertices.length < 3) return false;\n\n let inside = false;\n const n = vertices.length;\n\n for (let i = 0, j = n - 1; i < n; j = i++) {\n const xi = vertices[i].longitude;\n const yi = vertices[i].latitude;\n const xj = vertices[j].longitude;\n const yj = vertices[j].latitude;\n\n const intersect =\n yi > point.latitude !== yj > point.latitude &&\n point.longitude < ((xj - xi) * (point.latitude - yi)) / (yj - yi) + xi;\n\n if (intersect) inside = !inside;\n }\n\n return inside;\n}\n\n/**\n * Check if zone is active according to schedule\n */\nfunction isZoneScheduleActive(schedule: GeofenceSchedule | undefined): boolean {\n if (!schedule) return true;\n\n const now = new Date();\n const tz = schedule.timezone || 'UTC';\n\n // Get current time in zone's timezone\n const formatter = new Intl.DateTimeFormat('en-US', {\n timeZone: tz,\n hour: '2-digit',\n minute: '2-digit',\n hour12: false,\n weekday: 'short',\n });\n\n const parts = formatter.formatToParts(now);\n const currentHour = parseInt(parts.find((p) => p.type === 'hour')?.value || '0');\n const currentMinute = parseInt(parts.find((p) => p.type === 'minute')?.value || '0');\n const weekdayStr = parts.find((p) => p.type === 'weekday')?.value || '';\n\n const weekdayMap: Record<string, number> = {\n Sun: 0,\n Mon: 1,\n Tue: 2,\n Wed: 3,\n Thu: 4,\n Fri: 5,\n Sat: 6,\n };\n const currentDay = weekdayMap[weekdayStr] ?? now.getDay();\n\n // Check day of week\n if (schedule.daysOfWeek && !schedule.daysOfWeek.includes(currentDay)) {\n return false;\n }\n\n // Check time window\n if (schedule.startTime && schedule.endTime) {\n const [startHour, startMin] = schedule.startTime.split(':').map(Number);\n const [endHour, endMin] = schedule.endTime.split(':').map(Number);\n\n const currentMins = currentHour * 60 + currentMinute;\n const startMins = startHour * 60 + startMin;\n const endMins = endHour * 60 + endMin;\n\n if (startMins <= endMins) {\n // Normal case: e.g., 09:00-17:00\n if (currentMins < startMins || currentMins > endMins) {\n return false;\n }\n } else {\n // Overnight case: e.g., 22:00-06:00\n if (currentMins < startMins && currentMins > endMins) {\n return false;\n }\n }\n }\n\n return true;\n}\n\n// ============================================\n// Geofence Plugin Implementation\n// ============================================\n\n/**\n * Create geofencing plugin\n */\nexport function geofencePlugin(options: GeofencePluginOptions = {}): MDMPlugin {\n const {\n onEnter,\n onExit,\n onInside,\n defaultDwellTime = 0,\n trackHistory = false,\n maxHistoryEntries = 1000,\n } = options;\n\n let mdm: MDMInstance;\n\n // In-memory storage (should be moved to DB adapter in production)\n const zones = new Map<string, GeofenceZone>();\n const deviceZoneStates = new Map<string, Map<string, DeviceZoneState>>();\n const locationHistory = new Map<string, LocationHistoryEntry[]>();\n\n let zoneIdCounter = 1;\n\n /**\n * Generate zone ID\n */\n function generateZoneId(): string {\n return `zone_${Date.now()}_${zoneIdCounter++}`;\n }\n\n /**\n * Check if location is inside zone\n */\n function isInsideZone(\n location: DeviceLocation,\n zone: GeofenceZone\n ): boolean {\n if (!zone.enabled) return false;\n if (!isZoneScheduleActive(zone.schedule)) return false;\n\n if (zone.type === 'circle' && zone.center && zone.radius) {\n return isInsideCircle(location, zone.center, zone.radius);\n }\n\n if (zone.type === 'polygon' && zone.vertices) {\n return isInsidePolygon(location, zone.vertices);\n }\n\n return false;\n }\n\n /**\n * Process location update for a device\n */\n async function processLocation(\n device: Device,\n location: DeviceLocation\n ): Promise<void> {\n if (!deviceZoneStates.has(device.id)) {\n deviceZoneStates.set(device.id, new Map());\n }\n const deviceStates = deviceZoneStates.get(device.id)!;\n\n const currentZones: string[] = [];\n\n for (const [zoneId, zone] of zones) {\n const wasInside = deviceStates.get(zoneId)?.inside ?? false;\n const isInside = isInsideZone(location, zone);\n\n if (isInside) {\n currentZones.push(zoneId);\n }\n\n if (isInside && !wasInside) {\n // Device entered zone\n const enteredAt = new Date();\n deviceStates.set(zoneId, {\n deviceId: device.id,\n zoneId,\n inside: true,\n enteredAt,\n });\n\n // Check dwell time\n const dwellTime = (zone.metadata?.dwellTime as number) ?? defaultDwellTime;\n\n if (dwellTime > 0) {\n setTimeout(async () => {\n const state = deviceStates.get(zoneId);\n if (state?.inside && state.enteredAt === enteredAt) {\n await triggerEnter(device, zone);\n }\n }, dwellTime);\n } else {\n await triggerEnter(device, zone);\n }\n } else if (!isInside && wasInside) {\n // Device exited zone\n const state = deviceStates.get(zoneId);\n deviceStates.set(zoneId, {\n deviceId: device.id,\n zoneId,\n inside: false,\n enteredAt: state?.enteredAt,\n exitedAt: new Date(),\n dwellTime: state?.enteredAt\n ? Date.now() - state.enteredAt.getTime()\n : undefined,\n });\n\n await triggerExit(device, zone);\n } else if (isInside) {\n // Device still inside\n await onInside?.(device, zone);\n }\n }\n\n // Track history\n if (trackHistory) {\n if (!locationHistory.has(device.id)) {\n locationHistory.set(device.id, []);\n }\n const history = locationHistory.get(device.id)!;\n\n history.push({\n deviceId: device.id,\n location,\n zones: currentZones,\n timestamp: new Date(),\n });\n\n // Trim history\n if (history.length > maxHistoryEntries) {\n history.splice(0, history.length - maxHistoryEntries);\n }\n }\n }\n\n /**\n * Trigger enter event\n */\n async function triggerEnter(device: Device, zone: GeofenceZone): Promise<void> {\n console.log(`[OpenMDM Geofence] Device ${device.id} entered zone ${zone.name}`);\n\n await onEnter?.(device, zone);\n\n // Emit event\n await mdm.emit('custom', {\n type: 'geofence.enter',\n deviceId: device.id,\n zoneId: zone.id,\n zoneName: zone.name,\n timestamp: new Date().toISOString(),\n });\n\n // Execute enter action\n if (zone.onEnter) {\n await executeAction(device, zone.onEnter, zone, 'enter');\n }\n\n // Apply policy override\n if (zone.policyOverride) {\n await mdm.devices.assignPolicy(device.id, zone.policyOverride);\n }\n }\n\n /**\n * Trigger exit event\n */\n async function triggerExit(device: Device, zone: GeofenceZone): Promise<void> {\n console.log(`[OpenMDM Geofence] Device ${device.id} exited zone ${zone.name}`);\n\n await onExit?.(device, zone);\n\n // Emit event\n const state = deviceZoneStates.get(device.id)?.get(zone.id);\n await mdm.emit('custom', {\n type: 'geofence.exit',\n deviceId: device.id,\n zoneId: zone.id,\n zoneName: zone.name,\n dwellTime: state?.dwellTime,\n timestamp: new Date().toISOString(),\n });\n\n // Execute exit action\n if (zone.onExit) {\n await executeAction(device, zone.onExit, zone, 'exit');\n }\n\n // Revert policy override (restore original policy)\n if (zone.policyOverride && device.policyId !== zone.policyOverride) {\n // Only if not still in another zone with same override\n const stillInOverrideZone = Array.from(zones.values()).some(\n (z) =>\n z.id !== zone.id &&\n z.policyOverride === zone.policyOverride &&\n deviceZoneStates.get(device.id)?.get(z.id)?.inside\n );\n\n if (!stillInOverrideZone) {\n // Revert to previous policy (would need to track original policy)\n console.log(\n `[OpenMDM Geofence] Policy override ended for device ${device.id}`\n );\n }\n }\n }\n\n /**\n * Execute geofence action\n */\n async function executeAction(\n device: Device,\n action: GeofenceAction,\n zone: GeofenceZone,\n trigger: 'enter' | 'exit'\n ): Promise<void> {\n switch (action.type) {\n case 'notify':\n if (action.notification) {\n await mdm.commands.send({\n deviceId: device.id,\n type: 'sendNotification',\n payload: {\n title: action.notification.title,\n body: action.notification.body,\n },\n });\n }\n break;\n\n case 'command':\n if (action.command) {\n await mdm.commands.send({\n deviceId: device.id,\n type: action.command.type as any,\n payload: action.command.payload,\n });\n }\n break;\n\n case 'policy':\n if (action.policyId) {\n await mdm.devices.assignPolicy(device.id, action.policyId);\n }\n break;\n\n case 'webhook':\n if (action.webhook) {\n try {\n await fetch(action.webhook.url, {\n method: action.webhook.method || 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...action.webhook.headers,\n },\n body: JSON.stringify({\n event: `geofence.${trigger}`,\n device: {\n id: device.id,\n enrollmentId: device.enrollmentId,\n },\n zone: {\n id: zone.id,\n name: zone.name,\n },\n timestamp: new Date().toISOString(),\n ...action.webhook.body,\n }),\n });\n } catch (error) {\n console.error(\n `[OpenMDM Geofence] Webhook failed for zone ${zone.id}:`,\n error\n );\n }\n }\n break;\n }\n }\n\n // Define plugin routes\n const routes: PluginRoute[] = [\n // List all zones\n {\n method: 'GET',\n path: '/geofence/zones',\n auth: true,\n admin: true,\n handler: async (context: any) => {\n const zoneList = Array.from(zones.values()).map((zone) => ({\n ...zone,\n createdAt: zone.createdAt.toISOString(),\n updatedAt: zone.updatedAt.toISOString(),\n }));\n return context.json({ zones: zoneList });\n },\n },\n\n // Get zone by ID\n {\n method: 'GET',\n path: '/geofence/zones/:zoneId',\n auth: true,\n admin: true,\n handler: async (context: any) => {\n const { zoneId } = context.req.param();\n const zone = zones.get(zoneId);\n\n if (!zone) {\n return context.json({ error: 'Zone not found' }, 404);\n }\n\n return context.json({\n ...zone,\n createdAt: zone.createdAt.toISOString(),\n updatedAt: zone.updatedAt.toISOString(),\n });\n },\n },\n\n // Create zone\n {\n method: 'POST',\n path: '/geofence/zones',\n auth: true,\n admin: true,\n handler: async (context: any) => {\n const body = (await context.req.json()) as CreateGeofenceZoneInput;\n\n // Validate\n if (body.type === 'circle') {\n if (!body.center || !body.radius) {\n return context.json(\n { error: 'Circle zone requires center and radius' },\n 400\n );\n }\n } else if (body.type === 'polygon') {\n if (!body.vertices || body.vertices.length < 3) {\n return context.json(\n { error: 'Polygon zone requires at least 3 vertices' },\n 400\n );\n }\n }\n\n const zone: GeofenceZone = {\n id: generateZoneId(),\n name: body.name,\n description: body.description,\n type: body.type,\n enabled: body.enabled ?? true,\n center: body.center,\n radius: body.radius,\n vertices: body.vertices,\n onEnter: body.onEnter,\n onExit: body.onExit,\n policyOverride: body.policyOverride,\n schedule: body.schedule,\n metadata: body.metadata,\n createdAt: new Date(),\n updatedAt: new Date(),\n };\n\n zones.set(zone.id, zone);\n\n return context.json(\n {\n ...zone,\n createdAt: zone.createdAt.toISOString(),\n updatedAt: zone.updatedAt.toISOString(),\n },\n 201\n );\n },\n },\n\n // Update zone\n {\n method: 'PUT',\n path: '/geofence/zones/:zoneId',\n auth: true,\n admin: true,\n handler: async (context: any) => {\n const { zoneId } = context.req.param();\n const body = (await context.req.json()) as UpdateGeofenceZoneInput;\n\n const existing = zones.get(zoneId);\n if (!existing) {\n return context.json({ error: 'Zone not found' }, 404);\n }\n\n const updated: GeofenceZone = {\n ...existing,\n ...body,\n id: zoneId,\n type: existing.type, // Type cannot be changed\n createdAt: existing.createdAt,\n updatedAt: new Date(),\n };\n\n zones.set(zoneId, updated);\n\n return context.json({\n ...updated,\n createdAt: updated.createdAt.toISOString(),\n updatedAt: updated.updatedAt.toISOString(),\n });\n },\n },\n\n // Delete zone\n {\n method: 'DELETE',\n path: '/geofence/zones/:zoneId',\n auth: true,\n admin: true,\n handler: async (context: any) => {\n const { zoneId } = context.req.param();\n\n if (!zones.has(zoneId)) {\n return context.json({ error: 'Zone not found' }, 404);\n }\n\n zones.delete(zoneId);\n\n // Clean up device states for this zone\n for (const states of deviceZoneStates.values()) {\n states.delete(zoneId);\n }\n\n return context.json({ success: true });\n },\n },\n\n // Get devices in zone\n {\n method: 'GET',\n path: '/geofence/zones/:zoneId/devices',\n auth: true,\n admin: true,\n handler: async (context: any) => {\n const { zoneId } = context.req.param();\n\n if (!zones.has(zoneId)) {\n return context.json({ error: 'Zone not found' }, 404);\n }\n\n const devicesInZone: string[] = [];\n for (const [deviceId, states] of deviceZoneStates) {\n if (states.get(zoneId)?.inside) {\n devicesInZone.push(deviceId);\n }\n }\n\n return context.json({ deviceIds: devicesInZone });\n },\n },\n\n // Get device zone status\n {\n method: 'GET',\n path: '/geofence/devices/:deviceId/zones',\n auth: true,\n admin: true,\n handler: async (context: any) => {\n const { deviceId } = context.req.param();\n const states = deviceZoneStates.get(deviceId);\n\n if (!states) {\n return context.json({ zones: [] });\n }\n\n const zoneStates = Array.from(states.values()).map((state) => ({\n ...state,\n enteredAt: state.enteredAt?.toISOString(),\n exitedAt: state.exitedAt?.toISOString(),\n zoneName: zones.get(state.zoneId)?.name,\n }));\n\n return context.json({ zones: zoneStates });\n },\n },\n\n // Get device location history\n {\n method: 'GET',\n path: '/geofence/devices/:deviceId/history',\n auth: true,\n admin: true,\n handler: async (context: any) => {\n const { deviceId } = context.req.param();\n const history = locationHistory.get(deviceId) || [];\n\n return context.json({\n history: history.map((entry) => ({\n ...entry,\n timestamp: entry.timestamp.toISOString(),\n })),\n });\n },\n },\n\n // Check if point is in any zone\n {\n method: 'POST',\n path: '/geofence/check',\n auth: true,\n handler: async (context: any) => {\n const body = await context.req.json();\n const { latitude, longitude } = body;\n\n const matchingZones: string[] = [];\n const location = { latitude, longitude, timestamp: new Date() };\n\n for (const [zoneId, zone] of zones) {\n if (isInsideZone(location, zone)) {\n matchingZones.push(zoneId);\n }\n }\n\n return context.json({\n inside: matchingZones.length > 0,\n zones: matchingZones.map((id) => ({\n id,\n name: zones.get(id)?.name,\n })),\n });\n },\n },\n ];\n\n return {\n name: 'geofence',\n version: '1.0.0',\n\n async onInit(instance: MDMInstance): Promise<void> {\n mdm = instance;\n console.log('[OpenMDM Geofence] Plugin initialized');\n },\n\n async onDestroy(): Promise<void> {\n zones.clear();\n deviceZoneStates.clear();\n locationHistory.clear();\n console.log('[OpenMDM Geofence] Plugin destroyed');\n },\n\n routes,\n\n async onHeartbeat(device: Device, heartbeat: Heartbeat): Promise<void> {\n if (heartbeat.location) {\n await processLocation(device, heartbeat.location);\n }\n },\n };\n}\n\n// ============================================\n// Exports\n// ============================================\n\nexport {\n haversineDistance,\n isInsideCircle,\n isInsidePolygon,\n isZoneScheduleActive,\n};\n\nexport type { MDMPlugin };\n"],"mappings":";AAuMA,IAAM,sBAAsB;AAK5B,SAAS,kBACP,MACA,MACA,MACA,MACQ;AACR,QAAM,QAAQ,CAAC,QAAiB,MAAM,KAAK,KAAM;AAEjD,QAAM,OAAO,MAAM,OAAO,IAAI;AAC9B,QAAM,OAAO,MAAM,OAAO,IAAI;AAE9B,QAAM,IACJ,KAAK,IAAI,OAAO,CAAC,IAAI,KAAK,IAAI,OAAO,CAAC,IACtC,KAAK,IAAI,MAAM,IAAI,CAAC,IAClB,KAAK,IAAI,MAAM,IAAI,CAAC,IACpB,KAAK,IAAI,OAAO,CAAC,IACjB,KAAK,IAAI,OAAO,CAAC;AAErB,QAAM,IAAI,IAAI,KAAK,MAAM,KAAK,KAAK,CAAC,GAAG,KAAK,KAAK,IAAI,CAAC,CAAC;AAEvD,SAAO,sBAAsB;AAC/B;AAKA,SAAS,eACP,OACA,QACA,cACS;AACT,QAAM,WAAW;AAAA,IACf,MAAM;AAAA,IACN,MAAM;AAAA,IACN,OAAO;AAAA,IACP,OAAO;AAAA,EACT;AACA,SAAO,YAAY;AACrB;AAKA,SAAS,gBACP,OACA,UACS;AACT,MAAI,SAAS,SAAS,EAAG,QAAO;AAEhC,MAAI,SAAS;AACb,QAAM,IAAI,SAAS;AAEnB,WAAS,IAAI,GAAG,IAAI,IAAI,GAAG,IAAI,GAAG,IAAI,KAAK;AACzC,UAAM,KAAK,SAAS,CAAC,EAAE;AACvB,UAAM,KAAK,SAAS,CAAC,EAAE;AACvB,UAAM,KAAK,SAAS,CAAC,EAAE;AACvB,UAAM,KAAK,SAAS,CAAC,EAAE;AAEvB,UAAM,YACJ,KAAK,MAAM,aAAa,KAAK,MAAM,YACnC,MAAM,aAAc,KAAK,OAAO,MAAM,WAAW,OAAQ,KAAK,MAAM;AAEtE,QAAI,UAAW,UAAS,CAAC;AAAA,EAC3B;AAEA,SAAO;AACT;AAKA,SAAS,qBAAqB,UAAiD;AAC7E,MAAI,CAAC,SAAU,QAAO;AAEtB,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,KAAK,SAAS,YAAY;AAGhC,QAAM,YAAY,IAAI,KAAK,eAAe,SAAS;AAAA,IACjD,UAAU;AAAA,IACV,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,SAAS;AAAA,EACX,CAAC;AAED,QAAM,QAAQ,UAAU,cAAc,GAAG;AACzC,QAAM,cAAc,SAAS,MAAM,KAAK,CAAC,MAAM,EAAE,SAAS,MAAM,GAAG,SAAS,GAAG;AAC/E,QAAM,gBAAgB,SAAS,MAAM,KAAK,CAAC,MAAM,EAAE,SAAS,QAAQ,GAAG,SAAS,GAAG;AACnF,QAAM,aAAa,MAAM,KAAK,CAAC,MAAM,EAAE,SAAS,SAAS,GAAG,SAAS;AAErE,QAAM,aAAqC;AAAA,IACzC,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,EACP;AACA,QAAM,aAAa,WAAW,UAAU,KAAK,IAAI,OAAO;AAGxD,MAAI,SAAS,cAAc,CAAC,SAAS,WAAW,SAAS,UAAU,GAAG;AACpE,WAAO;AAAA,EACT;AAGA,MAAI,SAAS,aAAa,SAAS,SAAS;AAC1C,UAAM,CAAC,WAAW,QAAQ,IAAI,SAAS,UAAU,MAAM,GAAG,EAAE,IAAI,MAAM;AACtE,UAAM,CAAC,SAAS,MAAM,IAAI,SAAS,QAAQ,MAAM,GAAG,EAAE,IAAI,MAAM;AAEhE,UAAM,cAAc,cAAc,KAAK;AACvC,UAAM,YAAY,YAAY,KAAK;AACnC,UAAM,UAAU,UAAU,KAAK;AAE/B,QAAI,aAAa,SAAS;AAExB,UAAI,cAAc,aAAa,cAAc,SAAS;AACpD,eAAO;AAAA,MACT;AAAA,IACF,OAAO;AAEL,UAAI,cAAc,aAAa,cAAc,SAAS;AACpD,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AASO,SAAS,eAAe,UAAiC,CAAC,GAAc;AAC7E,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA,mBAAmB;AAAA,IACnB,eAAe;AAAA,IACf,oBAAoB;AAAA,EACtB,IAAI;AAEJ,MAAI;AAGJ,QAAM,QAAQ,oBAAI,IAA0B;AAC5C,QAAM,mBAAmB,oBAAI,IAA0C;AACvE,QAAM,kBAAkB,oBAAI,IAAoC;AAEhE,MAAI,gBAAgB;AAKpB,WAAS,iBAAyB;AAChC,WAAO,QAAQ,KAAK,IAAI,CAAC,IAAI,eAAe;AAAA,EAC9C;AAKA,WAAS,aACP,UACA,MACS;AACT,QAAI,CAAC,KAAK,QAAS,QAAO;AAC1B,QAAI,CAAC,qBAAqB,KAAK,QAAQ,EAAG,QAAO;AAEjD,QAAI,KAAK,SAAS,YAAY,KAAK,UAAU,KAAK,QAAQ;AACxD,aAAO,eAAe,UAAU,KAAK,QAAQ,KAAK,MAAM;AAAA,IAC1D;AAEA,QAAI,KAAK,SAAS,aAAa,KAAK,UAAU;AAC5C,aAAO,gBAAgB,UAAU,KAAK,QAAQ;AAAA,IAChD;AAEA,WAAO;AAAA,EACT;AAKA,iBAAe,gBACb,QACA,UACe;AACf,QAAI,CAAC,iBAAiB,IAAI,OAAO,EAAE,GAAG;AACpC,uBAAiB,IAAI,OAAO,IAAI,oBAAI,IAAI,CAAC;AAAA,IAC3C;AACA,UAAM,eAAe,iBAAiB,IAAI,OAAO,EAAE;AAEnD,UAAM,eAAyB,CAAC;AAEhC,eAAW,CAAC,QAAQ,IAAI,KAAK,OAAO;AAClC,YAAM,YAAY,aAAa,IAAI,MAAM,GAAG,UAAU;AACtD,YAAM,WAAW,aAAa,UAAU,IAAI;AAE5C,UAAI,UAAU;AACZ,qBAAa,KAAK,MAAM;AAAA,MAC1B;AAEA,UAAI,YAAY,CAAC,WAAW;AAE1B,cAAM,YAAY,oBAAI,KAAK;AAC3B,qBAAa,IAAI,QAAQ;AAAA,UACvB,UAAU,OAAO;AAAA,UACjB;AAAA,UACA,QAAQ;AAAA,UACR;AAAA,QACF,CAAC;AAGD,cAAM,YAAa,KAAK,UAAU,aAAwB;AAE1D,YAAI,YAAY,GAAG;AACjB,qBAAW,YAAY;AACrB,kBAAM,QAAQ,aAAa,IAAI,MAAM;AACrC,gBAAI,OAAO,UAAU,MAAM,cAAc,WAAW;AAClD,oBAAM,aAAa,QAAQ,IAAI;AAAA,YACjC;AAAA,UACF,GAAG,SAAS;AAAA,QACd,OAAO;AACL,gBAAM,aAAa,QAAQ,IAAI;AAAA,QACjC;AAAA,MACF,WAAW,CAAC,YAAY,WAAW;AAEjC,cAAM,QAAQ,aAAa,IAAI,MAAM;AACrC,qBAAa,IAAI,QAAQ;AAAA,UACvB,UAAU,OAAO;AAAA,UACjB;AAAA,UACA,QAAQ;AAAA,UACR,WAAW,OAAO;AAAA,UAClB,UAAU,oBAAI,KAAK;AAAA,UACnB,WAAW,OAAO,YACd,KAAK,IAAI,IAAI,MAAM,UAAU,QAAQ,IACrC;AAAA,QACN,CAAC;AAED,cAAM,YAAY,QAAQ,IAAI;AAAA,MAChC,WAAW,UAAU;AAEnB,cAAM,WAAW,QAAQ,IAAI;AAAA,MAC/B;AAAA,IACF;AAGA,QAAI,cAAc;AAChB,UAAI,CAAC,gBAAgB,IAAI,OAAO,EAAE,GAAG;AACnC,wBAAgB,IAAI,OAAO,IAAI,CAAC,CAAC;AAAA,MACnC;AACA,YAAM,UAAU,gBAAgB,IAAI,OAAO,EAAE;AAE7C,cAAQ,KAAK;AAAA,QACX,UAAU,OAAO;AAAA,QACjB;AAAA,QACA,OAAO;AAAA,QACP,WAAW,oBAAI,KAAK;AAAA,MACtB,CAAC;AAGD,UAAI,QAAQ,SAAS,mBAAmB;AACtC,gBAAQ,OAAO,GAAG,QAAQ,SAAS,iBAAiB;AAAA,MACtD;AAAA,IACF;AAAA,EACF;AAKA,iBAAe,aAAa,QAAgB,MAAmC;AAC7E,YAAQ,IAAI,6BAA6B,OAAO,EAAE,iBAAiB,KAAK,IAAI,EAAE;AAE9E,UAAM,UAAU,QAAQ,IAAI;AAG5B,UAAM,IAAI,KAAK,UAAU;AAAA,MACvB,MAAM;AAAA,MACN,UAAU,OAAO;AAAA,MACjB,QAAQ,KAAK;AAAA,MACb,UAAU,KAAK;AAAA,MACf,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC,CAAC;AAGD,QAAI,KAAK,SAAS;AAChB,YAAM,cAAc,QAAQ,KAAK,SAAS,MAAM,OAAO;AAAA,IACzD;AAGA,QAAI,KAAK,gBAAgB;AACvB,YAAM,IAAI,QAAQ,aAAa,OAAO,IAAI,KAAK,cAAc;AAAA,IAC/D;AAAA,EACF;AAKA,iBAAe,YAAY,QAAgB,MAAmC;AAC5E,YAAQ,IAAI,6BAA6B,OAAO,EAAE,gBAAgB,KAAK,IAAI,EAAE;AAE7E,UAAM,SAAS,QAAQ,IAAI;AAG3B,UAAM,QAAQ,iBAAiB,IAAI,OAAO,EAAE,GAAG,IAAI,KAAK,EAAE;AAC1D,UAAM,IAAI,KAAK,UAAU;AAAA,MACvB,MAAM;AAAA,MACN,UAAU,OAAO;AAAA,MACjB,QAAQ,KAAK;AAAA,MACb,UAAU,KAAK;AAAA,MACf,WAAW,OAAO;AAAA,MAClB,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC,CAAC;AAGD,QAAI,KAAK,QAAQ;AACf,YAAM,cAAc,QAAQ,KAAK,QAAQ,MAAM,MAAM;AAAA,IACvD;AAGA,QAAI,KAAK,kBAAkB,OAAO,aAAa,KAAK,gBAAgB;AAElE,YAAM,sBAAsB,MAAM,KAAK,MAAM,OAAO,CAAC,EAAE;AAAA,QACrD,CAAC,MACC,EAAE,OAAO,KAAK,MACd,EAAE,mBAAmB,KAAK,kBAC1B,iBAAiB,IAAI,OAAO,EAAE,GAAG,IAAI,EAAE,EAAE,GAAG;AAAA,MAChD;AAEA,UAAI,CAAC,qBAAqB;AAExB,gBAAQ;AAAA,UACN,uDAAuD,OAAO,EAAE;AAAA,QAClE;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAKA,iBAAe,cACb,QACA,QACA,MACA,SACe;AACf,YAAQ,OAAO,MAAM;AAAA,MACnB,KAAK;AACH,YAAI,OAAO,cAAc;AACvB,gBAAM,IAAI,SAAS,KAAK;AAAA,YACtB,UAAU,OAAO;AAAA,YACjB,MAAM;AAAA,YACN,SAAS;AAAA,cACP,OAAO,OAAO,aAAa;AAAA,cAC3B,MAAM,OAAO,aAAa;AAAA,YAC5B;AAAA,UACF,CAAC;AAAA,QACH;AACA;AAAA,MAEF,KAAK;AACH,YAAI,OAAO,SAAS;AAClB,gBAAM,IAAI,SAAS,KAAK;AAAA,YACtB,UAAU,OAAO;AAAA,YACjB,MAAM,OAAO,QAAQ;AAAA,YACrB,SAAS,OAAO,QAAQ;AAAA,UAC1B,CAAC;AAAA,QACH;AACA;AAAA,MAEF,KAAK;AACH,YAAI,OAAO,UAAU;AACnB,gBAAM,IAAI,QAAQ,aAAa,OAAO,IAAI,OAAO,QAAQ;AAAA,QAC3D;AACA;AAAA,MAEF,KAAK;AACH,YAAI,OAAO,SAAS;AAClB,cAAI;AACF,kBAAM,MAAM,OAAO,QAAQ,KAAK;AAAA,cAC9B,QAAQ,OAAO,QAAQ,UAAU;AAAA,cACjC,SAAS;AAAA,gBACP,gBAAgB;AAAA,gBAChB,GAAG,OAAO,QAAQ;AAAA,cACpB;AAAA,cACA,MAAM,KAAK,UAAU;AAAA,gBACnB,OAAO,YAAY,OAAO;AAAA,gBAC1B,QAAQ;AAAA,kBACN,IAAI,OAAO;AAAA,kBACX,cAAc,OAAO;AAAA,gBACvB;AAAA,gBACA,MAAM;AAAA,kBACJ,IAAI,KAAK;AAAA,kBACT,MAAM,KAAK;AAAA,gBACb;AAAA,gBACA,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,gBAClC,GAAG,OAAO,QAAQ;AAAA,cACpB,CAAC;AAAA,YACH,CAAC;AAAA,UACH,SAAS,OAAO;AACd,oBAAQ;AAAA,cACN,8CAA8C,KAAK,EAAE;AAAA,cACrD;AAAA,YACF;AAAA,UACF;AAAA,QACF;AACA;AAAA,IACJ;AAAA,EACF;AAGA,QAAM,SAAwB;AAAA;AAAA,IAE5B;AAAA,MACE,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,MAAM;AAAA,MACN,OAAO;AAAA,MACP,SAAS,OAAO,YAAiB;AAC/B,cAAM,WAAW,MAAM,KAAK,MAAM,OAAO,CAAC,EAAE,IAAI,CAAC,UAAU;AAAA,UACzD,GAAG;AAAA,UACH,WAAW,KAAK,UAAU,YAAY;AAAA,UACtC,WAAW,KAAK,UAAU,YAAY;AAAA,QACxC,EAAE;AACF,eAAO,QAAQ,KAAK,EAAE,OAAO,SAAS,CAAC;AAAA,MACzC;AAAA,IACF;AAAA;AAAA,IAGA;AAAA,MACE,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,MAAM;AAAA,MACN,OAAO;AAAA,MACP,SAAS,OAAO,YAAiB;AAC/B,cAAM,EAAE,OAAO,IAAI,QAAQ,IAAI,MAAM;AACrC,cAAM,OAAO,MAAM,IAAI,MAAM;AAE7B,YAAI,CAAC,MAAM;AACT,iBAAO,QAAQ,KAAK,EAAE,OAAO,iBAAiB,GAAG,GAAG;AAAA,QACtD;AAEA,eAAO,QAAQ,KAAK;AAAA,UAClB,GAAG;AAAA,UACH,WAAW,KAAK,UAAU,YAAY;AAAA,UACtC,WAAW,KAAK,UAAU,YAAY;AAAA,QACxC,CAAC;AAAA,MACH;AAAA,IACF;AAAA;AAAA,IAGA;AAAA,MACE,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,MAAM;AAAA,MACN,OAAO;AAAA,MACP,SAAS,OAAO,YAAiB;AAC/B,cAAM,OAAQ,MAAM,QAAQ,IAAI,KAAK;AAGrC,YAAI,KAAK,SAAS,UAAU;AAC1B,cAAI,CAAC,KAAK,UAAU,CAAC,KAAK,QAAQ;AAChC,mBAAO,QAAQ;AAAA,cACb,EAAE,OAAO,yCAAyC;AAAA,cAClD;AAAA,YACF;AAAA,UACF;AAAA,QACF,WAAW,KAAK,SAAS,WAAW;AAClC,cAAI,CAAC,KAAK,YAAY,KAAK,SAAS,SAAS,GAAG;AAC9C,mBAAO,QAAQ;AAAA,cACb,EAAE,OAAO,4CAA4C;AAAA,cACrD;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAEA,cAAM,OAAqB;AAAA,UACzB,IAAI,eAAe;AAAA,UACnB,MAAM,KAAK;AAAA,UACX,aAAa,KAAK;AAAA,UAClB,MAAM,KAAK;AAAA,UACX,SAAS,KAAK,WAAW;AAAA,UACzB,QAAQ,KAAK;AAAA,UACb,QAAQ,KAAK;AAAA,UACb,UAAU,KAAK;AAAA,UACf,SAAS,KAAK;AAAA,UACd,QAAQ,KAAK;AAAA,UACb,gBAAgB,KAAK;AAAA,UACrB,UAAU,KAAK;AAAA,UACf,UAAU,KAAK;AAAA,UACf,WAAW,oBAAI,KAAK;AAAA,UACpB,WAAW,oBAAI,KAAK;AAAA,QACtB;AAEA,cAAM,IAAI,KAAK,IAAI,IAAI;AAEvB,eAAO,QAAQ;AAAA,UACb;AAAA,YACE,GAAG;AAAA,YACH,WAAW,KAAK,UAAU,YAAY;AAAA,YACtC,WAAW,KAAK,UAAU,YAAY;AAAA,UACxC;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA;AAAA,IAGA;AAAA,MACE,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,MAAM;AAAA,MACN,OAAO;AAAA,MACP,SAAS,OAAO,YAAiB;AAC/B,cAAM,EAAE,OAAO,IAAI,QAAQ,IAAI,MAAM;AACrC,cAAM,OAAQ,MAAM,QAAQ,IAAI,KAAK;AAErC,cAAM,WAAW,MAAM,IAAI,MAAM;AACjC,YAAI,CAAC,UAAU;AACb,iBAAO,QAAQ,KAAK,EAAE,OAAO,iBAAiB,GAAG,GAAG;AAAA,QACtD;AAEA,cAAM,UAAwB;AAAA,UAC5B,GAAG;AAAA,UACH,GAAG;AAAA,UACH,IAAI;AAAA,UACJ,MAAM,SAAS;AAAA;AAAA,UACf,WAAW,SAAS;AAAA,UACpB,WAAW,oBAAI,KAAK;AAAA,QACtB;AAEA,cAAM,IAAI,QAAQ,OAAO;AAEzB,eAAO,QAAQ,KAAK;AAAA,UAClB,GAAG;AAAA,UACH,WAAW,QAAQ,UAAU,YAAY;AAAA,UACzC,WAAW,QAAQ,UAAU,YAAY;AAAA,QAC3C,CAAC;AAAA,MACH;AAAA,IACF;AAAA;AAAA,IAGA;AAAA,MACE,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,MAAM;AAAA,MACN,OAAO;AAAA,MACP,SAAS,OAAO,YAAiB;AAC/B,cAAM,EAAE,OAAO,IAAI,QAAQ,IAAI,MAAM;AAErC,YAAI,CAAC,MAAM,IAAI,MAAM,GAAG;AACtB,iBAAO,QAAQ,KAAK,EAAE,OAAO,iBAAiB,GAAG,GAAG;AAAA,QACtD;AAEA,cAAM,OAAO,MAAM;AAGnB,mBAAW,UAAU,iBAAiB,OAAO,GAAG;AAC9C,iBAAO,OAAO,MAAM;AAAA,QACtB;AAEA,eAAO,QAAQ,KAAK,EAAE,SAAS,KAAK,CAAC;AAAA,MACvC;AAAA,IACF;AAAA;AAAA,IAGA;AAAA,MACE,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,MAAM;AAAA,MACN,OAAO;AAAA,MACP,SAAS,OAAO,YAAiB;AAC/B,cAAM,EAAE,OAAO,IAAI,QAAQ,IAAI,MAAM;AAErC,YAAI,CAAC,MAAM,IAAI,MAAM,GAAG;AACtB,iBAAO,QAAQ,KAAK,EAAE,OAAO,iBAAiB,GAAG,GAAG;AAAA,QACtD;AAEA,cAAM,gBAA0B,CAAC;AACjC,mBAAW,CAAC,UAAU,MAAM,KAAK,kBAAkB;AACjD,cAAI,OAAO,IAAI,MAAM,GAAG,QAAQ;AAC9B,0BAAc,KAAK,QAAQ;AAAA,UAC7B;AAAA,QACF;AAEA,eAAO,QAAQ,KAAK,EAAE,WAAW,cAAc,CAAC;AAAA,MAClD;AAAA,IACF;AAAA;AAAA,IAGA;AAAA,MACE,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,MAAM;AAAA,MACN,OAAO;AAAA,MACP,SAAS,OAAO,YAAiB;AAC/B,cAAM,EAAE,SAAS,IAAI,QAAQ,IAAI,MAAM;AACvC,cAAM,SAAS,iBAAiB,IAAI,QAAQ;AAE5C,YAAI,CAAC,QAAQ;AACX,iBAAO,QAAQ,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC;AAAA,QACnC;AAEA,cAAM,aAAa,MAAM,KAAK,OAAO,OAAO,CAAC,EAAE,IAAI,CAAC,WAAW;AAAA,UAC7D,GAAG;AAAA,UACH,WAAW,MAAM,WAAW,YAAY;AAAA,UACxC,UAAU,MAAM,UAAU,YAAY;AAAA,UACtC,UAAU,MAAM,IAAI,MAAM,MAAM,GAAG;AAAA,QACrC,EAAE;AAEF,eAAO,QAAQ,KAAK,EAAE,OAAO,WAAW,CAAC;AAAA,MAC3C;AAAA,IACF;AAAA;AAAA,IAGA;AAAA,MACE,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,MAAM;AAAA,MACN,OAAO;AAAA,MACP,SAAS,OAAO,YAAiB;AAC/B,cAAM,EAAE,SAAS,IAAI,QAAQ,IAAI,MAAM;AACvC,cAAM,UAAU,gBAAgB,IAAI,QAAQ,KAAK,CAAC;AAElD,eAAO,QAAQ,KAAK;AAAA,UAClB,SAAS,QAAQ,IAAI,CAAC,WAAW;AAAA,YAC/B,GAAG;AAAA,YACH,WAAW,MAAM,UAAU,YAAY;AAAA,UACzC,EAAE;AAAA,QACJ,CAAC;AAAA,MACH;AAAA,IACF;AAAA;AAAA,IAGA;AAAA,MACE,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,MAAM;AAAA,MACN,SAAS,OAAO,YAAiB;AAC/B,cAAM,OAAO,MAAM,QAAQ,IAAI,KAAK;AACpC,cAAM,EAAE,UAAU,UAAU,IAAI;AAEhC,cAAM,gBAA0B,CAAC;AACjC,cAAM,WAAW,EAAE,UAAU,WAAW,WAAW,oBAAI,KAAK,EAAE;AAE9D,mBAAW,CAAC,QAAQ,IAAI,KAAK,OAAO;AAClC,cAAI,aAAa,UAAU,IAAI,GAAG;AAChC,0BAAc,KAAK,MAAM;AAAA,UAC3B;AAAA,QACF;AAEA,eAAO,QAAQ,KAAK;AAAA,UAClB,QAAQ,cAAc,SAAS;AAAA,UAC/B,OAAO,cAAc,IAAI,CAAC,QAAQ;AAAA,YAChC;AAAA,YACA,MAAM,MAAM,IAAI,EAAE,GAAG;AAAA,UACvB,EAAE;AAAA,QACJ,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IAET,MAAM,OAAO,UAAsC;AACjD,YAAM;AACN,cAAQ,IAAI,uCAAuC;AAAA,IACrD;AAAA,IAEA,MAAM,YAA2B;AAC/B,YAAM,MAAM;AACZ,uBAAiB,MAAM;AACvB,sBAAgB,MAAM;AACtB,cAAQ,IAAI,qCAAqC;AAAA,IACnD;AAAA,IAEA;AAAA,IAEA,MAAM,YAAY,QAAgB,WAAqC;AACrE,UAAI,UAAU,UAAU;AACtB,cAAM,gBAAgB,QAAQ,UAAU,QAAQ;AAAA,MAClD;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@openmdm/plugin-geofence",
3
+ "version": "0.2.0",
4
+ "description": "Geofencing plugin for OpenMDM - location-based policy enforcement for Android devices",
5
+ "author": "OpenMDM Contributors",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "src"
20
+ ],
21
+ "dependencies": {
22
+ "@openmdm/core": "0.2.0"
23
+ },
24
+ "devDependencies": {
25
+ "tsup": "^8.0.0",
26
+ "typescript": "^5.5.0"
27
+ },
28
+ "keywords": [
29
+ "openmdm",
30
+ "geofence",
31
+ "location",
32
+ "gps",
33
+ "mdm"
34
+ ],
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/azoila/openmdm.git",
39
+ "directory": "packages/plugins/geofence"
40
+ },
41
+ "homepage": "https://openmdm.dev",
42
+ "bugs": {
43
+ "url": "https://github.com/azoila/openmdm/issues"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "scripts": {
49
+ "build": "tsup",
50
+ "dev": "tsup --watch",
51
+ "typecheck": "tsc --noEmit",
52
+ "clean": "rm -rf dist"
53
+ }
54
+ }