@openmdm/core 0.2.0 → 0.3.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.
@@ -0,0 +1,327 @@
1
+ /**
2
+ * OpenMDM Dashboard Manager
3
+ *
4
+ * Provides analytics and statistics for the MDM dashboard.
5
+ * Aggregates data from devices, commands, and applications.
6
+ */
7
+
8
+ import type {
9
+ DashboardManager,
10
+ DashboardStats,
11
+ DeviceStatusBreakdown,
12
+ EnrollmentTrendPoint,
13
+ CommandSuccessRates,
14
+ AppInstallationSummary,
15
+ DatabaseAdapter,
16
+ DeviceStatus,
17
+ } from './types';
18
+
19
+ /**
20
+ * Create a DashboardManager instance
21
+ */
22
+ export function createDashboardManager(db: DatabaseAdapter): DashboardManager {
23
+ return {
24
+ async getStats(_tenantId?: string): Promise<DashboardStats> {
25
+ // Use database method if available
26
+ if (db.getDashboardStats) {
27
+ return db.getDashboardStats(_tenantId);
28
+ }
29
+
30
+ // Fallback: compute from individual queries
31
+ const devices = await db.listDevices({
32
+ limit: 10000, // Get all for counting
33
+ });
34
+
35
+ const deviceStats = {
36
+ total: devices.total,
37
+ enrolled: devices.devices.filter((d) => d.status === 'enrolled').length,
38
+ active: devices.devices.filter((d) => d.status === 'enrolled').length, // 'active' = 'enrolled' for dashboard
39
+ blocked: devices.devices.filter((d) => d.status === 'blocked').length,
40
+ pending: devices.devices.filter((d) => d.status === 'pending').length,
41
+ };
42
+
43
+ const allPolicies = await db.listPolicies();
44
+ const policyStats = {
45
+ total: allPolicies.length,
46
+ deployed: allPolicies.filter((p) => p.isDefault).length,
47
+ };
48
+
49
+ const allApps = await db.listApplications();
50
+ const appStats = {
51
+ total: allApps.length,
52
+ deployed: allApps.length, // All apps in db are considered deployed
53
+ };
54
+
55
+ // Command stats - get recent commands
56
+ const now = new Date();
57
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
58
+ const allCommands = await db.listCommands({ limit: 10000 });
59
+
60
+ const pendingCommands = allCommands.filter((c) => c.status === 'pending');
61
+ const last24hCommands = allCommands.filter(
62
+ (c) => new Date(c.createdAt) >= yesterday
63
+ );
64
+
65
+ const commandStats = {
66
+ pendingCount: pendingCommands.length,
67
+ last24hTotal: last24hCommands.length,
68
+ last24hSuccess: last24hCommands.filter((c) => c.status === 'completed').length,
69
+ last24hFailed: last24hCommands.filter((c) => c.status === 'failed').length,
70
+ };
71
+
72
+ // Group stats
73
+ const allGroups = await db.listGroups();
74
+ let groupsWithDevices = 0;
75
+ for (const group of allGroups) {
76
+ const groupDevices = await db.listDevicesInGroup(group.id);
77
+ if (groupDevices.length > 0) groupsWithDevices++;
78
+ }
79
+
80
+ return {
81
+ devices: deviceStats,
82
+ policies: policyStats,
83
+ applications: appStats,
84
+ commands: commandStats,
85
+ groups: {
86
+ total: allGroups.length,
87
+ withDevices: groupsWithDevices,
88
+ },
89
+ };
90
+ },
91
+
92
+ async getDeviceStatusBreakdown(_tenantId?: string): Promise<DeviceStatusBreakdown> {
93
+ if (db.getDeviceStatusBreakdown) {
94
+ return db.getDeviceStatusBreakdown(_tenantId);
95
+ }
96
+
97
+ const devices = await db.listDevices({
98
+ limit: 10000,
99
+ });
100
+
101
+ const byStatus: Record<DeviceStatus, number> = {
102
+ pending: 0,
103
+ enrolled: 0,
104
+ blocked: 0,
105
+ unenrolled: 0,
106
+ };
107
+
108
+ const byOs: Record<string, number> = {};
109
+ const byManufacturer: Record<string, number> = {};
110
+ const byModel: Record<string, number> = {};
111
+
112
+ for (const device of devices.devices) {
113
+ // By status
114
+ byStatus[device.status]++;
115
+
116
+ // By OS version
117
+ const osKey = device.osVersion || 'Unknown';
118
+ byOs[osKey] = (byOs[osKey] || 0) + 1;
119
+
120
+ // By manufacturer
121
+ const mfr = device.manufacturer || 'Unknown';
122
+ byManufacturer[mfr] = (byManufacturer[mfr] || 0) + 1;
123
+
124
+ // By model
125
+ const model = device.model || 'Unknown';
126
+ byModel[model] = (byModel[model] || 0) + 1;
127
+ }
128
+
129
+ return {
130
+ byStatus,
131
+ byOs,
132
+ byManufacturer,
133
+ byModel,
134
+ };
135
+ },
136
+
137
+ async getEnrollmentTrend(days: number, _tenantId?: string): Promise<EnrollmentTrendPoint[]> {
138
+ if (db.getEnrollmentTrend) {
139
+ return db.getEnrollmentTrend(days, _tenantId);
140
+ }
141
+
142
+ // Generate trend data from event history
143
+ const now = new Date();
144
+ const startDate = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
145
+
146
+ // Get enrollment events
147
+ const events = await db.listEvents({
148
+ type: 'device.enrolled',
149
+ startDate,
150
+ limit: 10000,
151
+ });
152
+
153
+ const unenrollEvents = await db.listEvents({
154
+ type: 'device.unenrolled',
155
+ startDate,
156
+ limit: 10000,
157
+ });
158
+
159
+ // Group by date
160
+ const trendByDate = new Map<string, { enrolled: number; unenrolled: number }>();
161
+
162
+ // Initialize all dates
163
+ for (let i = 0; i < days; i++) {
164
+ const date = new Date(startDate.getTime() + i * 24 * 60 * 60 * 1000);
165
+ const dateKey = date.toISOString().split('T')[0];
166
+ trendByDate.set(dateKey, { enrolled: 0, unenrolled: 0 });
167
+ }
168
+
169
+ // Count events
170
+ for (const event of events) {
171
+ const dateKey = new Date(event.createdAt).toISOString().split('T')[0];
172
+ const entry = trendByDate.get(dateKey);
173
+ if (entry) {
174
+ entry.enrolled++;
175
+ }
176
+ }
177
+
178
+ for (const event of unenrollEvents) {
179
+ const dateKey = new Date(event.createdAt).toISOString().split('T')[0];
180
+ const entry = trendByDate.get(dateKey);
181
+ if (entry) {
182
+ entry.unenrolled++;
183
+ }
184
+ }
185
+
186
+ // Get initial device count
187
+ const initialDevices = await db.listDevices({
188
+ limit: 10000,
189
+ });
190
+ let runningTotal = initialDevices.total;
191
+
192
+ // Build trend points
193
+ const result: EnrollmentTrendPoint[] = [];
194
+ const sortedDates = Array.from(trendByDate.keys()).sort();
195
+
196
+ for (const dateKey of sortedDates) {
197
+ const entry = trendByDate.get(dateKey)!;
198
+ const netChange = entry.enrolled - entry.unenrolled;
199
+ runningTotal += netChange;
200
+
201
+ result.push({
202
+ date: new Date(dateKey),
203
+ enrolled: entry.enrolled,
204
+ unenrolled: entry.unenrolled,
205
+ netChange,
206
+ totalDevices: runningTotal,
207
+ });
208
+ }
209
+
210
+ return result;
211
+ },
212
+
213
+ async getCommandSuccessRates(_tenantId?: string): Promise<CommandSuccessRates> {
214
+ if (db.getCommandSuccessRates) {
215
+ return db.getCommandSuccessRates(_tenantId);
216
+ }
217
+
218
+ const now = new Date();
219
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
220
+
221
+ const commands = await db.listCommands({ limit: 10000 });
222
+
223
+ // Overall stats
224
+ const completed = commands.filter((c) => c.status === 'completed').length;
225
+ const failed = commands.filter((c) => c.status === 'failed').length;
226
+ const total = commands.length;
227
+
228
+ // By type
229
+ const byType: CommandSuccessRates['byType'] = {};
230
+ for (const cmd of commands) {
231
+ if (!byType[cmd.type]) {
232
+ byType[cmd.type] = {
233
+ total: 0,
234
+ completed: 0,
235
+ failed: 0,
236
+ successRate: 0,
237
+ };
238
+ }
239
+ byType[cmd.type].total++;
240
+ if (cmd.status === 'completed') byType[cmd.type].completed++;
241
+ if (cmd.status === 'failed') byType[cmd.type].failed++;
242
+ }
243
+
244
+ // Calculate success rates
245
+ for (const type of Object.keys(byType)) {
246
+ const stats = byType[type];
247
+ const finishedCount = stats.completed + stats.failed;
248
+ stats.successRate = finishedCount > 0 ? (stats.completed / finishedCount) * 100 : 0;
249
+ }
250
+
251
+ // Last 24h
252
+ const last24hCommands = commands.filter(
253
+ (c) => new Date(c.createdAt) >= yesterday
254
+ );
255
+
256
+ return {
257
+ overall: {
258
+ total,
259
+ completed,
260
+ failed,
261
+ successRate:
262
+ completed + failed > 0 ? (completed / (completed + failed)) * 100 : 0,
263
+ },
264
+ byType,
265
+ last24h: {
266
+ total: last24hCommands.length,
267
+ completed: last24hCommands.filter((c) => c.status === 'completed').length,
268
+ failed: last24hCommands.filter((c) => c.status === 'failed').length,
269
+ pending: last24hCommands.filter((c) => c.status === 'pending').length,
270
+ },
271
+ };
272
+ },
273
+
274
+ async getAppInstallationSummary(_tenantId?: string): Promise<AppInstallationSummary> {
275
+ if (db.getAppInstallationSummary) {
276
+ return db.getAppInstallationSummary(_tenantId);
277
+ }
278
+
279
+ // Get all apps
280
+ const apps = await db.listApplications();
281
+ const appMap = new Map(apps.map((a) => [a.packageName, a]));
282
+
283
+ // Get installation statuses if available
284
+ const byStatus: Record<string, number> = {
285
+ installed: 0,
286
+ installing: 0,
287
+ failed: 0,
288
+ pending: 0,
289
+ };
290
+
291
+ // Count installed apps per device
292
+ const installCounts: Record<string, number> = {};
293
+
294
+ // Get devices to count installations
295
+ const devices = await db.listDevices({
296
+ limit: 10000,
297
+ });
298
+
299
+ for (const device of devices.devices) {
300
+ if (device.installedApps) {
301
+ for (const app of device.installedApps) {
302
+ const key = app.packageName;
303
+ installCounts[key] = (installCounts[key] || 0) + 1;
304
+ byStatus['installed']++;
305
+ }
306
+ }
307
+ }
308
+
309
+ // Top installed apps
310
+ const topInstalled = Object.entries(installCounts)
311
+ .sort(([, a], [, b]) => b - a)
312
+ .slice(0, 10)
313
+ .map(([packageName, count]) => ({
314
+ packageName,
315
+ name: appMap.get(packageName)?.name || packageName,
316
+ installedCount: count,
317
+ }));
318
+
319
+ return {
320
+ total: Object.values(byStatus).reduce((a, b) => a + b, 0),
321
+ byStatus,
322
+ recentFailures: [], // Would need installation status tracking
323
+ topInstalled,
324
+ };
325
+ },
326
+ };
327
+ }
package/src/index.ts CHANGED
@@ -64,6 +64,15 @@ import type {
64
64
  CommandResult,
65
65
  InstalledApp,
66
66
  WebhookManager,
67
+ TenantManager,
68
+ AuthorizationManager,
69
+ AuditManager,
70
+ ScheduleManager,
71
+ MessageQueueManager,
72
+ DashboardManager,
73
+ PluginStorageAdapter,
74
+ GroupTreeNode,
75
+ GroupHierarchyStats,
67
76
  } from './types';
68
77
  import {
69
78
  DeviceNotFoundError,
@@ -71,6 +80,13 @@ import {
71
80
  EnrollmentError,
72
81
  } from './types';
73
82
  import { createWebhookManager } from './webhooks';
83
+ import { createTenantManager } from './tenant';
84
+ import { createAuthorizationManager } from './authorization';
85
+ import { createAuditManager } from './audit';
86
+ import { createScheduleManager } from './schedule';
87
+ import { createMessageQueueManager } from './queue';
88
+ import { createDashboardManager } from './dashboard';
89
+ import { createPluginStorageAdapter, createMemoryPluginStorageAdapter } from './plugin-storage';
74
90
 
75
91
  // Re-export all types
76
92
  export * from './types';
@@ -78,6 +94,15 @@ export * from './schema';
78
94
  export { createWebhookManager, verifyWebhookSignature } from './webhooks';
79
95
  export type { WebhookPayload } from './webhooks';
80
96
 
97
+ // Re-export enterprise manager factories
98
+ export { createTenantManager } from './tenant';
99
+ export { createAuthorizationManager } from './authorization';
100
+ export { createAuditManager } from './audit';
101
+ export { createScheduleManager } from './schedule';
102
+ export { createMessageQueueManager } from './queue';
103
+ export { createDashboardManager } from './dashboard';
104
+ export { createPluginStorageAdapter, createMemoryPluginStorageAdapter, createPluginKey, parsePluginKey } from './plugin-storage';
105
+
81
106
  /**
82
107
  * Create an MDM instance with the given configuration.
83
108
  */
@@ -97,6 +122,46 @@ export function createMDM(config: MDMConfig): MDMInstance {
97
122
  ? createWebhookManager(webhooksConfig)
98
123
  : undefined;
99
124
 
125
+ // ============================================
126
+ // Enterprise Managers (optional)
127
+ // ============================================
128
+
129
+ // Create tenant manager if multi-tenancy is enabled
130
+ const tenantManager: TenantManager | undefined = config.multiTenancy?.enabled
131
+ ? createTenantManager(database)
132
+ : undefined;
133
+
134
+ // Create authorization manager if authorization is enabled
135
+ const authorizationManager: AuthorizationManager | undefined = config.authorization?.enabled
136
+ ? createAuthorizationManager(database)
137
+ : undefined;
138
+
139
+ // Create audit manager if audit logging is enabled
140
+ const auditManager: AuditManager | undefined = config.audit?.enabled
141
+ ? createAuditManager(database)
142
+ : undefined;
143
+
144
+ // Create schedule manager if scheduling is enabled
145
+ const scheduleManager: ScheduleManager | undefined = config.scheduling?.enabled
146
+ ? createScheduleManager(database)
147
+ : undefined;
148
+
149
+ // Create message queue manager if the database supports it
150
+ const messageQueueManager: MessageQueueManager | undefined = database.enqueueMessage
151
+ ? createMessageQueueManager(database)
152
+ : undefined;
153
+
154
+ // Create dashboard manager (always available, uses database fallbacks)
155
+ const dashboardManager: DashboardManager = createDashboardManager(database);
156
+
157
+ // Create plugin storage adapter
158
+ const pluginStorageAdapter: PluginStorageAdapter | undefined =
159
+ config.pluginStorage?.adapter === 'database'
160
+ ? createPluginStorageAdapter(database)
161
+ : config.pluginStorage?.adapter === 'memory'
162
+ ? createMemoryPluginStorageAdapter()
163
+ : undefined;
164
+
100
165
  // Event subscription
101
166
  const on = <T extends EventType>(
102
167
  event: T,
@@ -656,6 +721,155 @@ export function createMDM(config: MDMConfig): MDMInstance {
656
721
  const allGroups = await database.listGroups();
657
722
  return allGroups.filter((g) => g.parentId === groupId);
658
723
  },
724
+
725
+ async getTree(rootId?: string): Promise<GroupTreeNode[]> {
726
+ // Use database implementation if available
727
+ if (database.getGroupTree) {
728
+ return database.getGroupTree(rootId);
729
+ }
730
+
731
+ // Fallback: Build tree from flat list
732
+ const allGroups = await database.listGroups();
733
+ const groupMap = new Map(allGroups.map((g) => [g.id, g]));
734
+
735
+ const buildNode = (group: Group, depth: number, path: string[]): GroupTreeNode => {
736
+ const children = allGroups
737
+ .filter((g) => g.parentId === group.id)
738
+ .map((child) => buildNode(child, depth + 1, [...path, group.id]));
739
+
740
+ return {
741
+ ...group,
742
+ children,
743
+ depth,
744
+ path,
745
+ effectivePolicyId: group.policyId,
746
+ };
747
+ };
748
+
749
+ // Find root groups (those with no parent or matching rootId)
750
+ const roots = allGroups.filter((g) =>
751
+ rootId ? g.id === rootId : !g.parentId
752
+ );
753
+
754
+ return roots.map((root) => buildNode(root, 0, []));
755
+ },
756
+
757
+ async getAncestors(groupId: string): Promise<Group[]> {
758
+ // Use database implementation if available
759
+ if (database.getGroupAncestors) {
760
+ return database.getGroupAncestors(groupId);
761
+ }
762
+
763
+ // Fallback: Traverse up the tree
764
+ const ancestors: Group[] = [];
765
+ const allGroups = await database.listGroups();
766
+ const groupMap = new Map(allGroups.map((g) => [g.id, g]));
767
+
768
+ let current = groupMap.get(groupId);
769
+ while (current?.parentId) {
770
+ const parent = groupMap.get(current.parentId);
771
+ if (parent) {
772
+ ancestors.push(parent);
773
+ current = parent;
774
+ } else {
775
+ break;
776
+ }
777
+ }
778
+
779
+ return ancestors;
780
+ },
781
+
782
+ async getDescendants(groupId: string): Promise<Group[]> {
783
+ // Use database implementation if available
784
+ if (database.getGroupDescendants) {
785
+ return database.getGroupDescendants(groupId);
786
+ }
787
+
788
+ // Fallback: Find all descendants recursively
789
+ const allGroups = await database.listGroups();
790
+ const descendants: Group[] = [];
791
+
792
+ const findDescendants = (parentId: string) => {
793
+ const children = allGroups.filter((g) => g.parentId === parentId);
794
+ for (const child of children) {
795
+ descendants.push(child);
796
+ findDescendants(child.id);
797
+ }
798
+ };
799
+
800
+ findDescendants(groupId);
801
+ return descendants;
802
+ },
803
+
804
+ async move(groupId: string, newParentId: string | null): Promise<Group> {
805
+ // Validate that we're not creating a cycle
806
+ if (newParentId) {
807
+ const ancestors = await this.getAncestors(newParentId);
808
+ if (ancestors.some((a) => a.id === groupId)) {
809
+ throw new Error('Cannot move group: would create circular reference');
810
+ }
811
+ }
812
+
813
+ return database.updateGroup(groupId, { parentId: newParentId });
814
+ },
815
+
816
+ async getEffectivePolicy(groupId: string): Promise<Policy | null> {
817
+ // Use database implementation if available
818
+ if (database.getGroupEffectivePolicy) {
819
+ return database.getGroupEffectivePolicy(groupId);
820
+ }
821
+
822
+ // Fallback: Walk up the tree to find first policy
823
+ const group = await database.findGroup(groupId);
824
+ if (!group) return null;
825
+
826
+ if (group.policyId) {
827
+ return database.findPolicy(group.policyId);
828
+ }
829
+
830
+ // Check ancestors
831
+ const ancestors = await this.getAncestors(groupId);
832
+ for (const ancestor of ancestors) {
833
+ if (ancestor.policyId) {
834
+ return database.findPolicy(ancestor.policyId);
835
+ }
836
+ }
837
+
838
+ return null;
839
+ },
840
+
841
+ async getHierarchyStats(): Promise<GroupHierarchyStats> {
842
+ // Use database implementation if available
843
+ if (database.getGroupHierarchyStats) {
844
+ return database.getGroupHierarchyStats();
845
+ }
846
+
847
+ // Fallback: Compute from flat list
848
+ const allGroups = await database.listGroups();
849
+ let maxDepth = 0;
850
+ let groupsWithDevices = 0;
851
+ let groupsWithPolicies = 0;
852
+
853
+ for (const group of allGroups) {
854
+ // Calculate depth
855
+ const ancestors = await this.getAncestors(group.id);
856
+ maxDepth = Math.max(maxDepth, ancestors.length);
857
+
858
+ // Check for devices
859
+ const devices = await database.listDevicesInGroup(group.id);
860
+ if (devices.length > 0) groupsWithDevices++;
861
+
862
+ // Check for policies
863
+ if (group.policyId) groupsWithPolicies++;
864
+ }
865
+
866
+ return {
867
+ totalGroups: allGroups.length,
868
+ maxDepth,
869
+ groupsWithDevices,
870
+ groupsWithPolicies,
871
+ };
872
+ },
659
873
  };
660
874
 
661
875
  // ============================================
@@ -969,6 +1183,14 @@ export function createMDM(config: MDMConfig): MDMInstance {
969
1183
  verifyDeviceToken,
970
1184
  getPlugins,
971
1185
  getPlugin,
1186
+ // Enterprise managers (optional)
1187
+ tenants: tenantManager,
1188
+ authorization: authorizationManager,
1189
+ audit: auditManager,
1190
+ schedules: scheduleManager,
1191
+ messageQueue: messageQueueManager,
1192
+ dashboard: dashboardManager,
1193
+ pluginStorage: pluginStorageAdapter,
972
1194
  };
973
1195
 
974
1196
  // Initialize plugins
@@ -0,0 +1,128 @@
1
+ /**
2
+ * OpenMDM Plugin Storage Manager
3
+ *
4
+ * Provides persistent storage for plugin state.
5
+ * Supports both database-backed and in-memory storage.
6
+ */
7
+
8
+ import type { PluginStorageAdapter, DatabaseAdapter } from './types';
9
+
10
+ /**
11
+ * Create a PluginStorageAdapter backed by the database
12
+ */
13
+ export function createPluginStorageAdapter(db: DatabaseAdapter): PluginStorageAdapter {
14
+ return {
15
+ async get<T>(pluginName: string, key: string): Promise<T | null> {
16
+ if (db.getPluginValue) {
17
+ const value = await db.getPluginValue(pluginName, key);
18
+ return value as T | null;
19
+ }
20
+
21
+ // Fallback: not supported
22
+ console.warn('Plugin storage not supported by database adapter');
23
+ return null;
24
+ },
25
+
26
+ async set<T>(pluginName: string, key: string, value: T): Promise<void> {
27
+ if (db.setPluginValue) {
28
+ await db.setPluginValue(pluginName, key, value);
29
+ return;
30
+ }
31
+
32
+ console.warn('Plugin storage not supported by database adapter');
33
+ },
34
+
35
+ async delete(pluginName: string, key: string): Promise<void> {
36
+ if (db.deletePluginValue) {
37
+ await db.deletePluginValue(pluginName, key);
38
+ return;
39
+ }
40
+
41
+ console.warn('Plugin storage not supported by database adapter');
42
+ },
43
+
44
+ async list(pluginName: string, prefix?: string): Promise<string[]> {
45
+ if (db.listPluginKeys) {
46
+ return db.listPluginKeys(pluginName, prefix);
47
+ }
48
+
49
+ console.warn('Plugin storage not supported by database adapter');
50
+ return [];
51
+ },
52
+
53
+ async clear(pluginName: string): Promise<void> {
54
+ if (db.clearPluginData) {
55
+ await db.clearPluginData(pluginName);
56
+ return;
57
+ }
58
+
59
+ console.warn('Plugin storage not supported by database adapter');
60
+ },
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Create an in-memory PluginStorageAdapter for testing
66
+ */
67
+ export function createMemoryPluginStorageAdapter(): PluginStorageAdapter {
68
+ const store = new Map<string, Map<string, unknown>>();
69
+
70
+ function getPluginStore(pluginName: string): Map<string, unknown> {
71
+ if (!store.has(pluginName)) {
72
+ store.set(pluginName, new Map());
73
+ }
74
+ return store.get(pluginName)!;
75
+ }
76
+
77
+ return {
78
+ async get<T>(pluginName: string, key: string): Promise<T | null> {
79
+ const pluginStore = getPluginStore(pluginName);
80
+ const value = pluginStore.get(key);
81
+ return value === undefined ? null : (value as T);
82
+ },
83
+
84
+ async set<T>(pluginName: string, key: string, value: T): Promise<void> {
85
+ const pluginStore = getPluginStore(pluginName);
86
+ pluginStore.set(key, value);
87
+ },
88
+
89
+ async delete(pluginName: string, key: string): Promise<void> {
90
+ const pluginStore = getPluginStore(pluginName);
91
+ pluginStore.delete(key);
92
+ },
93
+
94
+ async list(pluginName: string, prefix?: string): Promise<string[]> {
95
+ const pluginStore = getPluginStore(pluginName);
96
+ const keys = Array.from(pluginStore.keys());
97
+
98
+ if (prefix) {
99
+ return keys.filter((k) => k.startsWith(prefix));
100
+ }
101
+
102
+ return keys;
103
+ },
104
+
105
+ async clear(pluginName: string): Promise<void> {
106
+ store.delete(pluginName);
107
+ },
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Plugin storage utilities
113
+ */
114
+
115
+ /**
116
+ * Create a namespaced key for plugin storage
117
+ */
118
+ export function createPluginKey(namespace: string, ...parts: string[]): string {
119
+ return [namespace, ...parts].join(':');
120
+ }
121
+
122
+ /**
123
+ * Parse a namespaced key
124
+ */
125
+ export function parsePluginKey(key: string): { namespace: string; parts: string[] } {
126
+ const [namespace, ...parts] = key.split(':');
127
+ return { namespace, parts };
128
+ }