@objectstack/core 0.8.2 → 0.9.1

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.
Files changed (73) hide show
  1. package/API_REGISTRY.md +392 -0
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +36 -0
  4. package/dist/api-registry-plugin.d.ts +54 -0
  5. package/dist/api-registry-plugin.d.ts.map +1 -0
  6. package/dist/api-registry-plugin.js +53 -0
  7. package/dist/api-registry-plugin.test.d.ts +2 -0
  8. package/dist/api-registry-plugin.test.d.ts.map +1 -0
  9. package/dist/api-registry-plugin.test.js +332 -0
  10. package/dist/api-registry.d.ts +259 -0
  11. package/dist/api-registry.d.ts.map +1 -0
  12. package/dist/api-registry.js +599 -0
  13. package/dist/api-registry.test.d.ts +2 -0
  14. package/dist/api-registry.test.d.ts.map +1 -0
  15. package/dist/api-registry.test.js +957 -0
  16. package/dist/index.d.ts +4 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +5 -0
  19. package/dist/logger.d.ts +1 -0
  20. package/dist/logger.d.ts.map +1 -1
  21. package/dist/logger.js +35 -11
  22. package/dist/plugin-loader.d.ts +3 -2
  23. package/dist/plugin-loader.d.ts.map +1 -1
  24. package/dist/plugin-loader.js +13 -11
  25. package/dist/qa/adapter.d.ts +14 -0
  26. package/dist/qa/adapter.d.ts.map +1 -0
  27. package/dist/qa/adapter.js +1 -0
  28. package/dist/qa/http-adapter.d.ts +16 -0
  29. package/dist/qa/http-adapter.d.ts.map +1 -0
  30. package/dist/qa/http-adapter.js +107 -0
  31. package/dist/qa/index.d.ts +4 -0
  32. package/dist/qa/index.d.ts.map +1 -0
  33. package/dist/qa/index.js +3 -0
  34. package/dist/qa/runner.d.ts +27 -0
  35. package/dist/qa/runner.d.ts.map +1 -0
  36. package/dist/qa/runner.js +157 -0
  37. package/dist/security/index.d.ts +14 -0
  38. package/dist/security/index.d.ts.map +1 -0
  39. package/dist/security/index.js +13 -0
  40. package/dist/security/plugin-config-validator.d.ts +79 -0
  41. package/dist/security/plugin-config-validator.d.ts.map +1 -0
  42. package/dist/security/plugin-config-validator.js +166 -0
  43. package/dist/security/plugin-config-validator.test.d.ts +2 -0
  44. package/dist/security/plugin-config-validator.test.d.ts.map +1 -0
  45. package/dist/security/plugin-config-validator.test.js +223 -0
  46. package/dist/security/plugin-permission-enforcer.d.ts +154 -0
  47. package/dist/security/plugin-permission-enforcer.d.ts.map +1 -0
  48. package/dist/security/plugin-permission-enforcer.js +323 -0
  49. package/dist/security/plugin-permission-enforcer.test.d.ts +2 -0
  50. package/dist/security/plugin-permission-enforcer.test.d.ts.map +1 -0
  51. package/dist/security/plugin-permission-enforcer.test.js +205 -0
  52. package/dist/security/plugin-signature-verifier.d.ts +96 -0
  53. package/dist/security/plugin-signature-verifier.d.ts.map +1 -0
  54. package/dist/security/plugin-signature-verifier.js +250 -0
  55. package/examples/api-registry-example.ts +557 -0
  56. package/package.json +2 -2
  57. package/src/api-registry-plugin.test.ts +391 -0
  58. package/src/api-registry-plugin.ts +86 -0
  59. package/src/api-registry.test.ts +1089 -0
  60. package/src/api-registry.ts +736 -0
  61. package/src/index.ts +6 -0
  62. package/src/logger.ts +36 -11
  63. package/src/plugin-loader.ts +17 -13
  64. package/src/qa/adapter.ts +14 -0
  65. package/src/qa/http-adapter.ts +114 -0
  66. package/src/qa/index.ts +3 -0
  67. package/src/qa/runner.ts +179 -0
  68. package/src/security/index.ts +29 -0
  69. package/src/security/plugin-config-validator.test.ts +276 -0
  70. package/src/security/plugin-config-validator.ts +191 -0
  71. package/src/security/plugin-permission-enforcer.test.ts +251 -0
  72. package/src/security/plugin-permission-enforcer.ts +408 -0
  73. package/src/security/plugin-signature-verifier.ts +359 -0
@@ -0,0 +1,408 @@
1
+ import type { Logger } from '@objectstack/spec/contracts';
2
+ import type { PluginCapability } from '@objectstack/spec/system';
3
+ import type { PluginContext } from '../types.js';
4
+
5
+ /**
6
+ * Plugin Permissions
7
+ * Defines what actions a plugin is allowed to perform
8
+ */
9
+ export interface PluginPermissions {
10
+ canAccessService(serviceName: string): boolean;
11
+ canTriggerHook(hookName: string): boolean;
12
+ canReadFile(path: string): boolean;
13
+ canWriteFile(path: string): boolean;
14
+ canNetworkRequest(url: string): boolean;
15
+ }
16
+
17
+ /**
18
+ * Permission Check Result
19
+ */
20
+ export interface PermissionCheckResult {
21
+ allowed: boolean;
22
+ reason?: string;
23
+ capability?: string;
24
+ }
25
+
26
+ /**
27
+ * Plugin Permission Enforcer
28
+ *
29
+ * Implements capability-based security model to enforce:
30
+ * 1. Service access control - which services a plugin can use
31
+ * 2. Hook restrictions - which hooks a plugin can trigger
32
+ * 3. File system permissions - what files a plugin can read/write
33
+ * 4. Network permissions - what URLs a plugin can access
34
+ *
35
+ * Architecture:
36
+ * - Uses capability declarations from plugin manifest
37
+ * - Checks permissions before allowing operations
38
+ * - Logs all permission denials for security audit
39
+ * - Supports allowlist and denylist patterns
40
+ *
41
+ * Security Model:
42
+ * - Principle of least privilege - plugins get minimal permissions
43
+ * - Explicit declaration - all capabilities must be declared
44
+ * - Runtime enforcement - checks happen at operation time
45
+ * - Audit trail - all denials are logged
46
+ *
47
+ * Usage:
48
+ * ```typescript
49
+ * const enforcer = new PluginPermissionEnforcer(logger);
50
+ * enforcer.registerPluginPermissions(pluginName, capabilities);
51
+ * enforcer.enforceServiceAccess(pluginName, 'database');
52
+ * ```
53
+ */
54
+ export class PluginPermissionEnforcer {
55
+ private logger: Logger;
56
+ private permissionRegistry: Map<string, PluginPermissions> = new Map();
57
+ private capabilityRegistry: Map<string, PluginCapability[]> = new Map();
58
+
59
+ constructor(logger: Logger) {
60
+ this.logger = logger;
61
+ }
62
+
63
+ /**
64
+ * Register plugin capabilities and build permission set
65
+ *
66
+ * @param pluginName - Plugin identifier
67
+ * @param capabilities - Array of capability declarations
68
+ */
69
+ registerPluginPermissions(pluginName: string, capabilities: PluginCapability[]): void {
70
+ this.capabilityRegistry.set(pluginName, capabilities);
71
+
72
+ const permissions: PluginPermissions = {
73
+ canAccessService: (service) => this.checkServiceAccess(capabilities, service),
74
+ canTriggerHook: (hook) => this.checkHookAccess(capabilities, hook),
75
+ canReadFile: (path) => this.checkFileRead(capabilities, path),
76
+ canWriteFile: (path) => this.checkFileWrite(capabilities, path),
77
+ canNetworkRequest: (url) => this.checkNetworkAccess(capabilities, url),
78
+ };
79
+
80
+ this.permissionRegistry.set(pluginName, permissions);
81
+
82
+ this.logger.info(`Permissions registered for plugin: ${pluginName}`, {
83
+ plugin: pluginName,
84
+ capabilityCount: capabilities.length,
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Enforce service access permission
90
+ *
91
+ * @param pluginName - Plugin requesting access
92
+ * @param serviceName - Service to access
93
+ * @throws Error if permission denied
94
+ */
95
+ enforceServiceAccess(pluginName: string, serviceName: string): void {
96
+ const result = this.checkPermission(pluginName, (perms) => perms.canAccessService(serviceName));
97
+
98
+ if (!result.allowed) {
99
+ const error = `Permission denied: Plugin ${pluginName} cannot access service ${serviceName}`;
100
+ this.logger.warn(error, {
101
+ plugin: pluginName,
102
+ service: serviceName,
103
+ reason: result.reason,
104
+ });
105
+ throw new Error(error);
106
+ }
107
+
108
+ this.logger.debug(`Service access granted: ${pluginName} -> ${serviceName}`);
109
+ }
110
+
111
+ /**
112
+ * Enforce hook trigger permission
113
+ *
114
+ * @param pluginName - Plugin requesting access
115
+ * @param hookName - Hook to trigger
116
+ * @throws Error if permission denied
117
+ */
118
+ enforceHookTrigger(pluginName: string, hookName: string): void {
119
+ const result = this.checkPermission(pluginName, (perms) => perms.canTriggerHook(hookName));
120
+
121
+ if (!result.allowed) {
122
+ const error = `Permission denied: Plugin ${pluginName} cannot trigger hook ${hookName}`;
123
+ this.logger.warn(error, {
124
+ plugin: pluginName,
125
+ hook: hookName,
126
+ reason: result.reason,
127
+ });
128
+ throw new Error(error);
129
+ }
130
+
131
+ this.logger.debug(`Hook trigger granted: ${pluginName} -> ${hookName}`);
132
+ }
133
+
134
+ /**
135
+ * Enforce file read permission
136
+ *
137
+ * @param pluginName - Plugin requesting access
138
+ * @param path - File path to read
139
+ * @throws Error if permission denied
140
+ */
141
+ enforceFileRead(pluginName: string, path: string): void {
142
+ const result = this.checkPermission(pluginName, (perms) => perms.canReadFile(path));
143
+
144
+ if (!result.allowed) {
145
+ const error = `Permission denied: Plugin ${pluginName} cannot read file ${path}`;
146
+ this.logger.warn(error, {
147
+ plugin: pluginName,
148
+ path,
149
+ reason: result.reason,
150
+ });
151
+ throw new Error(error);
152
+ }
153
+
154
+ this.logger.debug(`File read granted: ${pluginName} -> ${path}`);
155
+ }
156
+
157
+ /**
158
+ * Enforce file write permission
159
+ *
160
+ * @param pluginName - Plugin requesting access
161
+ * @param path - File path to write
162
+ * @throws Error if permission denied
163
+ */
164
+ enforceFileWrite(pluginName: string, path: string): void {
165
+ const result = this.checkPermission(pluginName, (perms) => perms.canWriteFile(path));
166
+
167
+ if (!result.allowed) {
168
+ const error = `Permission denied: Plugin ${pluginName} cannot write file ${path}`;
169
+ this.logger.warn(error, {
170
+ plugin: pluginName,
171
+ path,
172
+ reason: result.reason,
173
+ });
174
+ throw new Error(error);
175
+ }
176
+
177
+ this.logger.debug(`File write granted: ${pluginName} -> ${path}`);
178
+ }
179
+
180
+ /**
181
+ * Enforce network request permission
182
+ *
183
+ * @param pluginName - Plugin requesting access
184
+ * @param url - URL to access
185
+ * @throws Error if permission denied
186
+ */
187
+ enforceNetworkRequest(pluginName: string, url: string): void {
188
+ const result = this.checkPermission(pluginName, (perms) => perms.canNetworkRequest(url));
189
+
190
+ if (!result.allowed) {
191
+ const error = `Permission denied: Plugin ${pluginName} cannot access URL ${url}`;
192
+ this.logger.warn(error, {
193
+ plugin: pluginName,
194
+ url,
195
+ reason: result.reason,
196
+ });
197
+ throw new Error(error);
198
+ }
199
+
200
+ this.logger.debug(`Network request granted: ${pluginName} -> ${url}`);
201
+ }
202
+
203
+ /**
204
+ * Get plugin capabilities
205
+ *
206
+ * @param pluginName - Plugin identifier
207
+ * @returns Array of capabilities or undefined
208
+ */
209
+ getPluginCapabilities(pluginName: string): PluginCapability[] | undefined {
210
+ return this.capabilityRegistry.get(pluginName);
211
+ }
212
+
213
+ /**
214
+ * Get plugin permissions
215
+ *
216
+ * @param pluginName - Plugin identifier
217
+ * @returns Permissions object or undefined
218
+ */
219
+ getPluginPermissions(pluginName: string): PluginPermissions | undefined {
220
+ return this.permissionRegistry.get(pluginName);
221
+ }
222
+
223
+ /**
224
+ * Revoke all permissions for a plugin
225
+ *
226
+ * @param pluginName - Plugin identifier
227
+ */
228
+ revokePermissions(pluginName: string): void {
229
+ this.permissionRegistry.delete(pluginName);
230
+ this.capabilityRegistry.delete(pluginName);
231
+ this.logger.warn(`Permissions revoked for plugin: ${pluginName}`);
232
+ }
233
+
234
+ // Private methods
235
+
236
+ private checkPermission(
237
+ pluginName: string,
238
+ check: (perms: PluginPermissions) => boolean
239
+ ): PermissionCheckResult {
240
+ const permissions = this.permissionRegistry.get(pluginName);
241
+
242
+ if (!permissions) {
243
+ return {
244
+ allowed: false,
245
+ reason: 'Plugin permissions not registered',
246
+ };
247
+ }
248
+
249
+ const allowed = check(permissions);
250
+
251
+ return {
252
+ allowed,
253
+ reason: allowed ? undefined : 'No matching capability found',
254
+ };
255
+ }
256
+
257
+ private checkServiceAccess(capabilities: PluginCapability[], serviceName: string): boolean {
258
+ // Check if plugin has capability to access this service
259
+ return capabilities.some(cap => {
260
+ const protocolId = cap.protocol.id;
261
+
262
+ // Check for wildcard service access
263
+ if (protocolId.includes('protocol.service.all')) {
264
+ return true;
265
+ }
266
+
267
+ // Check for specific service protocol
268
+ if (protocolId.includes(`protocol.service.${serviceName}`)) {
269
+ return true;
270
+ }
271
+
272
+ // Check for service category match
273
+ const serviceCategory = serviceName.split('.')[0];
274
+ if (protocolId.includes(`protocol.service.${serviceCategory}`)) {
275
+ return true;
276
+ }
277
+
278
+ return false;
279
+ });
280
+ }
281
+
282
+ private checkHookAccess(capabilities: PluginCapability[], hookName: string): boolean {
283
+ // Check if plugin has capability to trigger this hook
284
+ return capabilities.some(cap => {
285
+ const protocolId = cap.protocol.id;
286
+
287
+ // Check for wildcard hook access
288
+ if (protocolId.includes('protocol.hook.all')) {
289
+ return true;
290
+ }
291
+
292
+ // Check for specific hook protocol
293
+ if (protocolId.includes(`protocol.hook.${hookName}`)) {
294
+ return true;
295
+ }
296
+
297
+ // Check for hook category match
298
+ const hookCategory = hookName.split(':')[0];
299
+ if (protocolId.includes(`protocol.hook.${hookCategory}`)) {
300
+ return true;
301
+ }
302
+
303
+ return false;
304
+ });
305
+ }
306
+
307
+ private checkFileRead(capabilities: PluginCapability[], _path: string): boolean {
308
+ // Check if plugin has capability to read this file
309
+ return capabilities.some(cap => {
310
+ const protocolId = cap.protocol.id;
311
+
312
+ // Check for file read capability
313
+ if (protocolId.includes('protocol.filesystem.read')) {
314
+ // TODO: Add path pattern matching
315
+ return true;
316
+ }
317
+
318
+ return false;
319
+ });
320
+ }
321
+
322
+ private checkFileWrite(capabilities: PluginCapability[], _path: string): boolean {
323
+ // Check if plugin has capability to write this file
324
+ return capabilities.some(cap => {
325
+ const protocolId = cap.protocol.id;
326
+
327
+ // Check for file write capability
328
+ if (protocolId.includes('protocol.filesystem.write')) {
329
+ // TODO: Add path pattern matching
330
+ return true;
331
+ }
332
+
333
+ return false;
334
+ });
335
+ }
336
+
337
+ private checkNetworkAccess(capabilities: PluginCapability[], _url: string): boolean {
338
+ // Check if plugin has capability to access this URL
339
+ return capabilities.some(cap => {
340
+ const protocolId = cap.protocol.id;
341
+
342
+ // Check for network capability
343
+ if (protocolId.includes('protocol.network')) {
344
+ // TODO: Add URL pattern matching
345
+ return true;
346
+ }
347
+
348
+ return false;
349
+ });
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Secure Plugin Context
355
+ * Wraps PluginContext with permission checks
356
+ */
357
+ export class SecurePluginContext implements PluginContext {
358
+ constructor(
359
+ private pluginName: string,
360
+ private permissionEnforcer: PluginPermissionEnforcer,
361
+ private baseContext: PluginContext
362
+ ) {}
363
+
364
+ registerService(name: string, service: any): void {
365
+ // No permission check for service registration (handled during init)
366
+ this.baseContext.registerService(name, service);
367
+ }
368
+
369
+ getService<T>(name: string): T {
370
+ // Check permission before accessing service
371
+ this.permissionEnforcer.enforceServiceAccess(this.pluginName, name);
372
+ return this.baseContext.getService<T>(name);
373
+ }
374
+
375
+ getServices(): Map<string, any> {
376
+ // Return all services (no permission check for listing)
377
+ return this.baseContext.getServices();
378
+ }
379
+
380
+ hook(name: string, handler: (...args: any[]) => void | Promise<void>): void {
381
+ // No permission check for registering hooks (handled during init)
382
+ this.baseContext.hook(name, handler);
383
+ }
384
+
385
+ async trigger(name: string, ...args: any[]): Promise<void> {
386
+ // Check permission before triggering hook
387
+ this.permissionEnforcer.enforceHookTrigger(this.pluginName, name);
388
+ await this.baseContext.trigger(name, ...args);
389
+ }
390
+
391
+ get logger() {
392
+ return this.baseContext.logger;
393
+ }
394
+
395
+ getKernel() {
396
+ return this.baseContext.getKernel();
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Create a plugin permission enforcer
402
+ *
403
+ * @param logger - Logger instance
404
+ * @returns Plugin permission enforcer
405
+ */
406
+ export function createPluginPermissionEnforcer(logger: Logger): PluginPermissionEnforcer {
407
+ return new PluginPermissionEnforcer(logger);
408
+ }