@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,276 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { z } from 'zod';
3
+ import { PluginConfigValidator } from './plugin-config-validator.js';
4
+ import { createLogger } from '../logger.js';
5
+ import type { PluginMetadata } from '../plugin-loader.js';
6
+
7
+ describe('PluginConfigValidator', () => {
8
+ let validator: PluginConfigValidator;
9
+ let logger: ReturnType<typeof createLogger>;
10
+
11
+ beforeEach(() => {
12
+ logger = createLogger({ level: 'error' });
13
+ validator = new PluginConfigValidator(logger);
14
+ });
15
+
16
+ describe('validatePluginConfig', () => {
17
+ it('should validate valid configuration', () => {
18
+ const configSchema = z.object({
19
+ port: z.number().min(1000).max(65535),
20
+ host: z.string(),
21
+ debug: z.boolean().default(false),
22
+ });
23
+
24
+ const plugin: PluginMetadata = {
25
+ name: 'com.test.plugin',
26
+ version: '1.0.0',
27
+ configSchema,
28
+ init: async () => {},
29
+ };
30
+
31
+ const config = {
32
+ port: 3000,
33
+ host: 'localhost',
34
+ debug: true,
35
+ };
36
+
37
+ const validatedConfig = validator.validatePluginConfig(plugin, config);
38
+
39
+ expect(validatedConfig).toEqual(config);
40
+ });
41
+
42
+ it('should apply defaults for missing optional fields', () => {
43
+ const configSchema = z.object({
44
+ port: z.number().default(3000),
45
+ host: z.string().default('localhost'),
46
+ debug: z.boolean().default(false),
47
+ });
48
+
49
+ const plugin: PluginMetadata = {
50
+ name: 'com.test.plugin',
51
+ version: '1.0.0',
52
+ configSchema,
53
+ init: async () => {},
54
+ };
55
+
56
+ const config = {
57
+ port: 8080,
58
+ };
59
+
60
+ const validatedConfig = validator.validatePluginConfig(plugin, config);
61
+
62
+ expect(validatedConfig).toEqual({
63
+ port: 8080,
64
+ host: 'localhost',
65
+ debug: false,
66
+ });
67
+ });
68
+
69
+ it('should throw error for invalid configuration', () => {
70
+ const configSchema = z.object({
71
+ port: z.number().min(1000).max(65535),
72
+ host: z.string(),
73
+ });
74
+
75
+ const plugin: PluginMetadata = {
76
+ name: 'com.test.plugin',
77
+ version: '1.0.0',
78
+ configSchema,
79
+ init: async () => {},
80
+ };
81
+
82
+ const config = {
83
+ port: 100, // Invalid: < 1000
84
+ host: 'localhost',
85
+ };
86
+
87
+ expect(() => validator.validatePluginConfig(plugin, config)).toThrow();
88
+ });
89
+
90
+ it('should provide detailed error messages', () => {
91
+ const configSchema = z.object({
92
+ port: z.number().min(1000),
93
+ host: z.string().min(1),
94
+ });
95
+
96
+ const plugin: PluginMetadata = {
97
+ name: 'com.test.plugin',
98
+ version: '1.0.0',
99
+ configSchema,
100
+ init: async () => {},
101
+ };
102
+
103
+ const config = {
104
+ port: 100,
105
+ host: '',
106
+ };
107
+
108
+ try {
109
+ validator.validatePluginConfig(plugin, config);
110
+ expect.fail('Should have thrown validation error');
111
+ } catch (error) {
112
+ const errorMessage = (error as Error).message;
113
+ expect(errorMessage).toContain('com.test.plugin');
114
+ expect(errorMessage).toContain('port');
115
+ expect(errorMessage).toContain('host');
116
+ }
117
+ });
118
+
119
+ it('should skip validation when no schema is provided', () => {
120
+ const plugin: PluginMetadata = {
121
+ name: 'com.test.plugin',
122
+ version: '1.0.0',
123
+ init: async () => {},
124
+ };
125
+
126
+ const config = { anything: 'goes' };
127
+
128
+ const validatedConfig = validator.validatePluginConfig(plugin, config);
129
+
130
+ expect(validatedConfig).toEqual(config);
131
+ });
132
+ });
133
+
134
+ describe('validatePartialConfig', () => {
135
+ it('should validate partial configuration', () => {
136
+ const configSchema = z.object({
137
+ port: z.number().min(1000),
138
+ host: z.string(),
139
+ debug: z.boolean(),
140
+ });
141
+
142
+ const plugin: PluginMetadata = {
143
+ name: 'com.test.plugin',
144
+ version: '1.0.0',
145
+ configSchema,
146
+ init: async () => {},
147
+ };
148
+
149
+ const partialConfig = {
150
+ port: 8080,
151
+ };
152
+
153
+ const validatedConfig = validator.validatePartialConfig(plugin, partialConfig);
154
+
155
+ expect(validatedConfig).toEqual({ port: 8080 });
156
+ });
157
+ });
158
+
159
+ describe('getDefaultConfig', () => {
160
+ it('should extract default configuration', () => {
161
+ const configSchema = z.object({
162
+ port: z.number().default(3000),
163
+ host: z.string().default('localhost'),
164
+ debug: z.boolean().default(false),
165
+ });
166
+
167
+ const plugin: PluginMetadata = {
168
+ name: 'com.test.plugin',
169
+ version: '1.0.0',
170
+ configSchema,
171
+ init: async () => {},
172
+ };
173
+
174
+ const defaults = validator.getDefaultConfig(plugin);
175
+
176
+ expect(defaults).toEqual({
177
+ port: 3000,
178
+ host: 'localhost',
179
+ debug: false,
180
+ });
181
+ });
182
+
183
+ it('should return undefined when schema requires fields', () => {
184
+ const configSchema = z.object({
185
+ port: z.number(),
186
+ host: z.string(),
187
+ });
188
+
189
+ const plugin: PluginMetadata = {
190
+ name: 'com.test.plugin',
191
+ version: '1.0.0',
192
+ configSchema,
193
+ init: async () => {},
194
+ };
195
+
196
+ const defaults = validator.getDefaultConfig(plugin);
197
+
198
+ expect(defaults).toBeUndefined();
199
+ });
200
+ });
201
+
202
+ describe('isConfigValid', () => {
203
+ it('should return true for valid config', () => {
204
+ const configSchema = z.object({
205
+ port: z.number(),
206
+ });
207
+
208
+ const plugin: PluginMetadata = {
209
+ name: 'com.test.plugin',
210
+ version: '1.0.0',
211
+ configSchema,
212
+ init: async () => {},
213
+ };
214
+
215
+ const isValid = validator.isConfigValid(plugin, { port: 3000 });
216
+
217
+ expect(isValid).toBe(true);
218
+ });
219
+
220
+ it('should return false for invalid config', () => {
221
+ const configSchema = z.object({
222
+ port: z.number(),
223
+ });
224
+
225
+ const plugin: PluginMetadata = {
226
+ name: 'com.test.plugin',
227
+ version: '1.0.0',
228
+ configSchema,
229
+ init: async () => {},
230
+ };
231
+
232
+ const isValid = validator.isConfigValid(plugin, { port: 'invalid' });
233
+
234
+ expect(isValid).toBe(false);
235
+ });
236
+ });
237
+
238
+ describe('getConfigErrors', () => {
239
+ it('should return errors for invalid config', () => {
240
+ const configSchema = z.object({
241
+ port: z.number().min(1000),
242
+ host: z.string().min(1),
243
+ });
244
+
245
+ const plugin: PluginMetadata = {
246
+ name: 'com.test.plugin',
247
+ version: '1.0.0',
248
+ configSchema,
249
+ init: async () => {},
250
+ };
251
+
252
+ const errors = validator.getConfigErrors(plugin, { port: 100, host: '' });
253
+
254
+ expect(errors).toHaveLength(2);
255
+ expect(errors[0].path).toBe('port');
256
+ expect(errors[1].path).toBe('host');
257
+ });
258
+
259
+ it('should return empty array for valid config', () => {
260
+ const configSchema = z.object({
261
+ port: z.number(),
262
+ });
263
+
264
+ const plugin: PluginMetadata = {
265
+ name: 'com.test.plugin',
266
+ version: '1.0.0',
267
+ configSchema,
268
+ init: async () => {},
269
+ };
270
+
271
+ const errors = validator.getConfigErrors(plugin, { port: 3000 });
272
+
273
+ expect(errors).toEqual([]);
274
+ });
275
+ });
276
+ });
@@ -0,0 +1,191 @@
1
+ import { z } from 'zod';
2
+ import type { Logger } from '@objectstack/spec/contracts';
3
+ import type { PluginMetadata } from '../plugin-loader.js';
4
+
5
+ /**
6
+ * Plugin Configuration Validator
7
+ *
8
+ * Validates plugin configurations against Zod schemas to ensure:
9
+ * 1. Type safety - all config values have correct types
10
+ * 2. Business rules - values meet constraints (min/max, regex, etc.)
11
+ * 3. Required fields - all mandatory configuration is provided
12
+ * 4. Default values - missing optional fields get defaults
13
+ *
14
+ * Architecture:
15
+ * - Uses Zod for runtime validation
16
+ * - Provides detailed error messages with field paths
17
+ * - Supports nested configuration objects
18
+ * - Allows partial validation for incremental updates
19
+ *
20
+ * Usage:
21
+ * ```typescript
22
+ * const validator = new PluginConfigValidator(logger);
23
+ * const validConfig = validator.validatePluginConfig(plugin, userConfig);
24
+ * ```
25
+ */
26
+ export class PluginConfigValidator {
27
+ private logger: Logger;
28
+
29
+ constructor(logger: Logger) {
30
+ this.logger = logger;
31
+ }
32
+
33
+ /**
34
+ * Validate plugin configuration against its Zod schema
35
+ *
36
+ * @param plugin - Plugin metadata with configSchema
37
+ * @param config - User-provided configuration
38
+ * @returns Validated and typed configuration
39
+ * @throws Error with detailed validation errors
40
+ */
41
+ validatePluginConfig<T = any>(plugin: PluginMetadata, config: any): T {
42
+ if (!plugin.configSchema) {
43
+ this.logger.debug(`Plugin ${plugin.name} has no config schema - skipping validation`);
44
+ return config as T;
45
+ }
46
+
47
+ try {
48
+ // Use Zod to parse and validate
49
+ const validatedConfig = plugin.configSchema.parse(config);
50
+
51
+ this.logger.debug(`✅ Plugin config validated: ${plugin.name}`, {
52
+ plugin: plugin.name,
53
+ configKeys: Object.keys(config || {}).length,
54
+ });
55
+
56
+ return validatedConfig as T;
57
+ } catch (error) {
58
+ if (error instanceof z.ZodError) {
59
+ const formattedErrors = this.formatZodErrors(error);
60
+ const errorMessage = [
61
+ `Plugin ${plugin.name} configuration validation failed:`,
62
+ ...formattedErrors.map(e => ` - ${e.path}: ${e.message}`),
63
+ ].join('\n');
64
+
65
+ this.logger.error(errorMessage, undefined, {
66
+ plugin: plugin.name,
67
+ errors: formattedErrors,
68
+ });
69
+
70
+ throw new Error(errorMessage);
71
+ }
72
+
73
+ // Re-throw other errors
74
+ throw error;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Validate partial configuration (for incremental updates)
80
+ *
81
+ * @param plugin - Plugin metadata
82
+ * @param partialConfig - Partial configuration to validate
83
+ * @returns Validated partial configuration
84
+ */
85
+ validatePartialConfig<T = any>(plugin: PluginMetadata, partialConfig: any): Partial<T> {
86
+ if (!plugin.configSchema) {
87
+ return partialConfig as Partial<T>;
88
+ }
89
+
90
+ try {
91
+ // Use Zod's partial() method for partial validation
92
+ // Cast to ZodObject to access partial() method
93
+ const partialSchema = (plugin.configSchema as any).partial();
94
+ const validatedConfig = partialSchema.parse(partialConfig);
95
+
96
+ this.logger.debug(`✅ Partial config validated: ${plugin.name}`);
97
+ return validatedConfig as Partial<T>;
98
+ } catch (error) {
99
+ if (error instanceof z.ZodError) {
100
+ const formattedErrors = this.formatZodErrors(error);
101
+ const errorMessage = [
102
+ `Plugin ${plugin.name} partial configuration validation failed:`,
103
+ ...formattedErrors.map(e => ` - ${e.path}: ${e.message}`),
104
+ ].join('\n');
105
+
106
+ throw new Error(errorMessage);
107
+ }
108
+
109
+ throw error;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Get default configuration from schema
115
+ *
116
+ * @param plugin - Plugin metadata
117
+ * @returns Default configuration object
118
+ */
119
+ getDefaultConfig<T = any>(plugin: PluginMetadata): T | undefined {
120
+ if (!plugin.configSchema) {
121
+ return undefined;
122
+ }
123
+
124
+ try {
125
+ // Parse empty object to get defaults
126
+ const defaults = plugin.configSchema.parse({});
127
+ this.logger.debug(`Default config extracted: ${plugin.name}`);
128
+ return defaults as T;
129
+ } catch (error) {
130
+ // Schema may require some fields - return undefined
131
+ this.logger.debug(`No default config available: ${plugin.name}`);
132
+ return undefined;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Check if configuration is valid without throwing
138
+ *
139
+ * @param plugin - Plugin metadata
140
+ * @param config - Configuration to check
141
+ * @returns True if valid, false otherwise
142
+ */
143
+ isConfigValid(plugin: PluginMetadata, config: any): boolean {
144
+ if (!plugin.configSchema) {
145
+ return true;
146
+ }
147
+
148
+ const result = plugin.configSchema.safeParse(config);
149
+ return result.success;
150
+ }
151
+
152
+ /**
153
+ * Get configuration errors without throwing
154
+ *
155
+ * @param plugin - Plugin metadata
156
+ * @param config - Configuration to check
157
+ * @returns Array of validation errors, or empty array if valid
158
+ */
159
+ getConfigErrors(plugin: PluginMetadata, config: any): Array<{path: string; message: string}> {
160
+ if (!plugin.configSchema) {
161
+ return [];
162
+ }
163
+
164
+ const result = plugin.configSchema.safeParse(config);
165
+
166
+ if (result.success) {
167
+ return [];
168
+ }
169
+
170
+ return this.formatZodErrors(result.error);
171
+ }
172
+
173
+ // Private methods
174
+
175
+ private formatZodErrors(error: z.ZodError<any>): Array<{path: string; message: string}> {
176
+ return error.issues.map((e: z.ZodIssue) => ({
177
+ path: e.path.join('.') || 'root',
178
+ message: e.message,
179
+ }));
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Create a plugin config validator
185
+ *
186
+ * @param logger - Logger instance
187
+ * @returns Plugin config validator
188
+ */
189
+ export function createPluginConfigValidator(logger: Logger): PluginConfigValidator {
190
+ return new PluginConfigValidator(logger);
191
+ }
@@ -0,0 +1,251 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { PluginPermissionEnforcer, SecurePluginContext } from './plugin-permission-enforcer.js';
3
+ import { createLogger } from '../logger.js';
4
+ import type { PluginCapability } from '@objectstack/spec/system';
5
+ import type { PluginContext } from '../types.js';
6
+
7
+ describe('PluginPermissionEnforcer', () => {
8
+ let enforcer: PluginPermissionEnforcer;
9
+ let logger: ReturnType<typeof createLogger>;
10
+
11
+ beforeEach(() => {
12
+ logger = createLogger({ level: 'error' });
13
+ enforcer = new PluginPermissionEnforcer(logger);
14
+ });
15
+
16
+ describe('registerPluginPermissions', () => {
17
+ it('should register plugin capabilities', () => {
18
+ const capabilities: PluginCapability[] = [
19
+ {
20
+ protocol: {
21
+ id: 'com.objectstack.protocol.service.database.v1',
22
+ label: 'Database Service',
23
+ version: { major: 1, minor: 0, patch: 0 },
24
+ },
25
+ conformance: 'full',
26
+ certified: false,
27
+ },
28
+ ];
29
+
30
+ enforcer.registerPluginPermissions('com.test.plugin', capabilities);
31
+
32
+ const registeredCapabilities = enforcer.getPluginCapabilities('com.test.plugin');
33
+ expect(registeredCapabilities).toEqual(capabilities);
34
+ });
35
+ });
36
+
37
+ describe('enforceServiceAccess', () => {
38
+ it('should allow access to declared services', () => {
39
+ const capabilities: PluginCapability[] = [
40
+ {
41
+ protocol: {
42
+ id: 'com.objectstack.protocol.service.database.v1',
43
+ label: 'Database Service',
44
+ version: { major: 1, minor: 0, patch: 0 },
45
+ },
46
+ conformance: 'full',
47
+ certified: false,
48
+ },
49
+ ];
50
+
51
+ enforcer.registerPluginPermissions('com.test.plugin', capabilities);
52
+
53
+ // Should not throw
54
+ expect(() => {
55
+ enforcer.enforceServiceAccess('com.test.plugin', 'database');
56
+ }).not.toThrow();
57
+ });
58
+
59
+ it('should deny access to undeclared services', () => {
60
+ const capabilities: PluginCapability[] = [
61
+ {
62
+ protocol: {
63
+ id: 'com.objectstack.protocol.service.database.v1',
64
+ label: 'Database Service',
65
+ version: { major: 1, minor: 0, patch: 0 },
66
+ },
67
+ conformance: 'full',
68
+ certified: false,
69
+ },
70
+ ];
71
+
72
+ enforcer.registerPluginPermissions('com.test.plugin', capabilities);
73
+
74
+ expect(() => {
75
+ enforcer.enforceServiceAccess('com.test.plugin', 'network');
76
+ }).toThrow(/Permission denied/);
77
+ });
78
+
79
+ it('should allow wildcard service access', () => {
80
+ const capabilities: PluginCapability[] = [
81
+ {
82
+ protocol: {
83
+ id: 'com.objectstack.protocol.service.all.v1',
84
+ label: 'All Services',
85
+ version: { major: 1, minor: 0, patch: 0 },
86
+ },
87
+ conformance: 'full',
88
+ certified: false,
89
+ },
90
+ ];
91
+
92
+ enforcer.registerPluginPermissions('com.test.plugin', capabilities);
93
+
94
+ // Should allow any service
95
+ expect(() => {
96
+ enforcer.enforceServiceAccess('com.test.plugin', 'database');
97
+ enforcer.enforceServiceAccess('com.test.plugin', 'network');
98
+ enforcer.enforceServiceAccess('com.test.plugin', 'filesystem');
99
+ }).not.toThrow();
100
+ });
101
+ });
102
+
103
+ describe('enforceHookTrigger', () => {
104
+ it('should allow triggering declared hooks', () => {
105
+ const capabilities: PluginCapability[] = [
106
+ {
107
+ protocol: {
108
+ id: 'com.objectstack.protocol.hook.data.v1',
109
+ label: 'Data Hooks',
110
+ version: { major: 1, minor: 0, patch: 0 },
111
+ },
112
+ conformance: 'full',
113
+ certified: false,
114
+ },
115
+ ];
116
+
117
+ enforcer.registerPluginPermissions('com.test.plugin', capabilities);
118
+
119
+ expect(() => {
120
+ enforcer.enforceHookTrigger('com.test.plugin', 'data:beforeCreate');
121
+ }).not.toThrow();
122
+ });
123
+
124
+ it('should deny triggering undeclared hooks', () => {
125
+ const capabilities: PluginCapability[] = [
126
+ {
127
+ protocol: {
128
+ id: 'com.objectstack.protocol.hook.data.v1',
129
+ label: 'Data Hooks',
130
+ version: { major: 1, minor: 0, patch: 0 },
131
+ },
132
+ conformance: 'full',
133
+ certified: false,
134
+ },
135
+ ];
136
+
137
+ enforcer.registerPluginPermissions('com.test.plugin', capabilities);
138
+
139
+ expect(() => {
140
+ enforcer.enforceHookTrigger('com.test.plugin', 'kernel:shutdown');
141
+ }).toThrow(/Permission denied/);
142
+ });
143
+ });
144
+
145
+ describe('revokePermissions', () => {
146
+ it('should revoke plugin permissions', () => {
147
+ const capabilities: PluginCapability[] = [
148
+ {
149
+ protocol: {
150
+ id: 'com.objectstack.protocol.service.database.v1',
151
+ label: 'Database Service',
152
+ version: { major: 1, minor: 0, patch: 0 },
153
+ },
154
+ conformance: 'full',
155
+ certified: false,
156
+ },
157
+ ];
158
+
159
+ enforcer.registerPluginPermissions('com.test.plugin', capabilities);
160
+ enforcer.revokePermissions('com.test.plugin');
161
+
162
+ expect(() => {
163
+ enforcer.enforceServiceAccess('com.test.plugin', 'database');
164
+ }).toThrow(/Permission denied/);
165
+ });
166
+ });
167
+ });
168
+
169
+ describe('SecurePluginContext', () => {
170
+ let enforcer: PluginPermissionEnforcer;
171
+ let logger: ReturnType<typeof createLogger>;
172
+ let mockBaseContext: PluginContext;
173
+
174
+ beforeEach(() => {
175
+ logger = createLogger({ level: 'error' });
176
+ enforcer = new PluginPermissionEnforcer(logger);
177
+
178
+ mockBaseContext = {
179
+ registerService: () => {},
180
+ getService: <T>(name: string): T => ({ name } as any),
181
+ getServices: () => new Map(),
182
+ hook: () => {},
183
+ trigger: async () => {},
184
+ logger,
185
+ getKernel: () => ({} as any),
186
+ };
187
+ });
188
+
189
+ describe('getService', () => {
190
+ it('should check permission before accessing service', () => {
191
+ const capabilities: PluginCapability[] = [
192
+ {
193
+ protocol: {
194
+ id: 'com.objectstack.protocol.service.database.v1',
195
+ label: 'Database Service',
196
+ version: { major: 1, minor: 0, patch: 0 },
197
+ },
198
+ conformance: 'full',
199
+ certified: false,
200
+ },
201
+ ];
202
+
203
+ enforcer.registerPluginPermissions('com.test.plugin', capabilities);
204
+
205
+ const secureContext = new SecurePluginContext(
206
+ 'com.test.plugin',
207
+ enforcer,
208
+ mockBaseContext
209
+ );
210
+
211
+ // Should succeed with permission
212
+ const service = secureContext.getService('database');
213
+ expect(service).toBeDefined();
214
+
215
+ // Should fail without permission
216
+ expect(() => {
217
+ secureContext.getService('network');
218
+ }).toThrow(/Permission denied/);
219
+ });
220
+ });
221
+
222
+ describe('trigger', () => {
223
+ it('should check permission before triggering hook', async () => {
224
+ const capabilities: PluginCapability[] = [
225
+ {
226
+ protocol: {
227
+ id: 'com.objectstack.protocol.hook.data.v1',
228
+ label: 'Data Hooks',
229
+ version: { major: 1, minor: 0, patch: 0 },
230
+ },
231
+ conformance: 'full',
232
+ certified: false,
233
+ },
234
+ ];
235
+
236
+ enforcer.registerPluginPermissions('com.test.plugin', capabilities);
237
+
238
+ const secureContext = new SecurePluginContext(
239
+ 'com.test.plugin',
240
+ enforcer,
241
+ mockBaseContext
242
+ );
243
+
244
+ // Should succeed with permission
245
+ await expect(secureContext.trigger('data:beforeCreate')).resolves.not.toThrow();
246
+
247
+ // Should fail without permission
248
+ await expect(secureContext.trigger('kernel:shutdown')).rejects.toThrow(/Permission denied/);
249
+ });
250
+ });
251
+ });