Skip to content
Merged
656 changes: 656 additions & 0 deletions MICROKERNEL_ASSESSMENT.md

Large diffs are not rendered by default.

533 changes: 533 additions & 0 deletions PLUGIN_SECURITY_GUIDE.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ export * from './api-registry.js';
export * from './api-registry-plugin.js';
export * as QA from './qa/index.js';

// Export security utilities
export * from './security/index.js';

// Re-export contracts from @objectstack/spec for backward compatibility
export type {
Logger,
Expand Down
30 changes: 17 additions & 13 deletions packages/core/src/plugin-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ export interface ServiceRegistration {
}

/**
* Plugin Configuration Validator
* Plugin Configuration Validator Interface
* Uses Zod for runtime validation of plugin configurations
* @deprecated Use the PluginConfigValidator class from security module instead
*/
export interface PluginConfigValidator {
export interface IPluginConfigValidator {
schema: z.ZodSchema;
validate(config: any): any;
}
Expand Down Expand Up @@ -366,28 +367,31 @@ export class PluginLoader {
return semverRegex.test(version);
}

private validatePluginConfig(plugin: PluginMetadata): void {
private validatePluginConfig(plugin: PluginMetadata, config?: any): void {
if (!plugin.configSchema) {
return;
}

// TODO: Configuration validation implementation
// This requires plugin config to be passed during loading
// For now, just validate that the schema exists
this.logger.debug(`Plugin ${plugin.name} has configuration schema (validation not yet implemented)`);
if (!config) {
this.logger.debug(`Plugin ${plugin.name} has configuration schema but no config provided`);
return;
}

// Configuration validation is now implemented in PluginConfigValidator
// This is a placeholder that logs the validation would happen
// The actual validation should be done by the caller when config is available
this.logger.debug(`Plugin ${plugin.name} has configuration schema (use PluginConfigValidator for validation)`);
}

private async verifyPluginSignature(plugin: PluginMetadata): Promise<void> {
if (!plugin.signature) {
return;
}

// TODO: Plugin signature verification implementation
// In a real implementation:
// 1. Extract public key from trusted source
// 2. Verify signature against plugin code hash
// 3. Throw error if verification fails
this.logger.debug(`Plugin ${plugin.name} signature verification (not yet implemented)`);
// Plugin signature verification is now implemented in PluginSignatureVerifier
// This is a placeholder that logs the verification would happen
// The actual verification should be done by the caller with proper security config
this.logger.debug(`Plugin ${plugin.name} has signature (use PluginSignatureVerifier for verification)`);
}

private async getSingletonService<T>(registration: ServiceRegistration): Promise<T> {
Expand Down
29 changes: 29 additions & 0 deletions packages/core/src/security/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Security Module
*
* Provides security features for the ObjectStack microkernel:
* - Plugin signature verification
* - Plugin configuration validation
* - Permission and capability enforcement
*
* @module @objectstack/core/security
*/

export {
PluginSignatureVerifier,
type PluginSignatureConfig,
type SignatureVerificationResult,
} from './plugin-signature-verifier.js';

export {
PluginConfigValidator,
createPluginConfigValidator,
} from './plugin-config-validator.js';

export {
PluginPermissionEnforcer,
SecurePluginContext,
createPluginPermissionEnforcer,
type PluginPermissions,
type PermissionCheckResult,
} from './plugin-permission-enforcer.js';
276 changes: 276 additions & 0 deletions packages/core/src/security/plugin-config-validator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { z } from 'zod';
import { PluginConfigValidator } from './plugin-config-validator.js';
import { createLogger } from '../logger.js';
import type { PluginMetadata } from '../plugin-loader.js';

describe('PluginConfigValidator', () => {
let validator: PluginConfigValidator;
let logger: ReturnType<typeof createLogger>;

beforeEach(() => {
logger = createLogger({ level: 'error' });
validator = new PluginConfigValidator(logger);
});

describe('validatePluginConfig', () => {
it('should validate valid configuration', () => {
const configSchema = z.object({
port: z.number().min(1000).max(65535),
host: z.string(),
debug: z.boolean().default(false),
});

const plugin: PluginMetadata = {
name: 'com.test.plugin',
version: '1.0.0',
configSchema,
init: async () => {},
};

const config = {
port: 3000,
host: 'localhost',
debug: true,
};

const validatedConfig = validator.validatePluginConfig(plugin, config);

expect(validatedConfig).toEqual(config);
});

it('should apply defaults for missing optional fields', () => {
const configSchema = z.object({
port: z.number().default(3000),
host: z.string().default('localhost'),
debug: z.boolean().default(false),
});

const plugin: PluginMetadata = {
name: 'com.test.plugin',
version: '1.0.0',
configSchema,
init: async () => {},
};

const config = {
port: 8080,
};

const validatedConfig = validator.validatePluginConfig(plugin, config);

expect(validatedConfig).toEqual({
port: 8080,
host: 'localhost',
debug: false,
});
});

it('should throw error for invalid configuration', () => {
const configSchema = z.object({
port: z.number().min(1000).max(65535),
host: z.string(),
});

const plugin: PluginMetadata = {
name: 'com.test.plugin',
version: '1.0.0',
configSchema,
init: async () => {},
};

const config = {
port: 100, // Invalid: < 1000
host: 'localhost',
};

expect(() => validator.validatePluginConfig(plugin, config)).toThrow();
});

it('should provide detailed error messages', () => {
const configSchema = z.object({
port: z.number().min(1000),
host: z.string().min(1),
});

const plugin: PluginMetadata = {
name: 'com.test.plugin',
version: '1.0.0',
configSchema,
init: async () => {},
};

const config = {
port: 100,
host: '',
};

try {
validator.validatePluginConfig(plugin, config);
expect.fail('Should have thrown validation error');
} catch (error) {
const errorMessage = (error as Error).message;
expect(errorMessage).toContain('com.test.plugin');
expect(errorMessage).toContain('port');
expect(errorMessage).toContain('host');
}
});

it('should skip validation when no schema is provided', () => {
const plugin: PluginMetadata = {
name: 'com.test.plugin',
version: '1.0.0',
init: async () => {},
};

const config = { anything: 'goes' };

const validatedConfig = validator.validatePluginConfig(plugin, config);

expect(validatedConfig).toEqual(config);
});
});

describe('validatePartialConfig', () => {
it('should validate partial configuration', () => {
const configSchema = z.object({
port: z.number().min(1000),
host: z.string(),
debug: z.boolean(),
});

const plugin: PluginMetadata = {
name: 'com.test.plugin',
version: '1.0.0',
configSchema,
init: async () => {},
};

const partialConfig = {
port: 8080,
};

const validatedConfig = validator.validatePartialConfig(plugin, partialConfig);

expect(validatedConfig).toEqual({ port: 8080 });
});
});

describe('getDefaultConfig', () => {
it('should extract default configuration', () => {
const configSchema = z.object({
port: z.number().default(3000),
host: z.string().default('localhost'),
debug: z.boolean().default(false),
});

const plugin: PluginMetadata = {
name: 'com.test.plugin',
version: '1.0.0',
configSchema,
init: async () => {},
};

const defaults = validator.getDefaultConfig(plugin);

expect(defaults).toEqual({
port: 3000,
host: 'localhost',
debug: false,
});
});

it('should return undefined when schema requires fields', () => {
const configSchema = z.object({
port: z.number(),
host: z.string(),
});

const plugin: PluginMetadata = {
name: 'com.test.plugin',
version: '1.0.0',
configSchema,
init: async () => {},
};

const defaults = validator.getDefaultConfig(plugin);

expect(defaults).toBeUndefined();
});
});

describe('isConfigValid', () => {
it('should return true for valid config', () => {
const configSchema = z.object({
port: z.number(),
});

const plugin: PluginMetadata = {
name: 'com.test.plugin',
version: '1.0.0',
configSchema,
init: async () => {},
};

const isValid = validator.isConfigValid(plugin, { port: 3000 });

expect(isValid).toBe(true);
});

it('should return false for invalid config', () => {
const configSchema = z.object({
port: z.number(),
});

const plugin: PluginMetadata = {
name: 'com.test.plugin',
version: '1.0.0',
configSchema,
init: async () => {},
};

const isValid = validator.isConfigValid(plugin, { port: 'invalid' });

expect(isValid).toBe(false);
});
});

describe('getConfigErrors', () => {
it('should return errors for invalid config', () => {
const configSchema = z.object({
port: z.number().min(1000),
host: z.string().min(1),
});

const plugin: PluginMetadata = {
name: 'com.test.plugin',
version: '1.0.0',
configSchema,
init: async () => {},
};

const errors = validator.getConfigErrors(plugin, { port: 100, host: '' });

expect(errors).toHaveLength(2);
expect(errors[0].path).toBe('port');
expect(errors[1].path).toBe('host');
});

it('should return empty array for valid config', () => {
const configSchema = z.object({
port: z.number(),
});

const plugin: PluginMetadata = {
name: 'com.test.plugin',
version: '1.0.0',
configSchema,
init: async () => {},
};

const errors = validator.getConfigErrors(plugin, { port: 3000 });

expect(errors).toEqual([]);
});
});
});
Loading
Loading