@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
package/src/index.ts CHANGED
@@ -11,6 +11,12 @@ export * from './types.js';
11
11
  export * from './logger.js';
12
12
  export * from './plugin-loader.js';
13
13
  export * from './enhanced-kernel.js';
14
+ export * from './api-registry.js';
15
+ export * from './api-registry-plugin.js';
16
+ export * as QA from './qa/index.js';
17
+
18
+ // Export security utilities
19
+ export * from './security/index.js';
14
20
 
15
21
  // Re-export contracts from @objectstack/spec for backward compatibility
16
22
  export type {
package/src/logger.ts CHANGED
@@ -21,6 +21,7 @@ export class ObjectLogger implements Logger {
21
21
  private isNode: boolean;
22
22
  private pinoLogger?: any; // Pino logger instance for Node.js
23
23
  private pinoInstance?: any; // Base Pino instance for creating child loggers
24
+ private require?: any; // CommonJS require function for Node.js
24
25
 
25
26
  constructor(config: Partial<LoggerConfig> = {}) {
26
27
  // Detect runtime environment
@@ -53,8 +54,13 @@ export class ObjectLogger implements Logger {
53
54
  if (!this.isNode) return;
54
55
 
55
56
  try {
56
- // Dynamic import for Pino (Node.js only)
57
- const pino = require('pino');
57
+ // Create require function dynamically for Node.js (avoids bundling issues in browser)
58
+ // @ts-ignore - dynamic import of Node.js module
59
+ const { createRequire } = eval('require("module")');
60
+ this.require = createRequire(import.meta.url);
61
+
62
+ // Synchronous import for Pino using createRequire (works in ESM)
63
+ const pino = this.require('pino');
58
64
 
59
65
  // Build Pino options
60
66
  const pinoOptions: any = {
@@ -75,15 +81,34 @@ export class ObjectLogger implements Logger {
75
81
 
76
82
  // Console transport
77
83
  if (this.config.format === 'pretty') {
78
- targets.push({
79
- target: 'pino-pretty',
80
- options: {
81
- colorize: true,
82
- translateTime: 'SYS:standard',
83
- ignore: 'pid,hostname'
84
- },
85
- level: this.config.level
86
- });
84
+ // Check if pino-pretty is available
85
+ let hasPretty = false;
86
+ try {
87
+ this.require.resolve('pino-pretty');
88
+ hasPretty = true;
89
+ } catch (e) {
90
+ // ignore
91
+ }
92
+
93
+ if (hasPretty) {
94
+ targets.push({
95
+ target: 'pino-pretty',
96
+ options: {
97
+ colorize: true,
98
+ translateTime: 'SYS:standard',
99
+ ignore: 'pid,hostname'
100
+ },
101
+ level: this.config.level
102
+ });
103
+ } else {
104
+ console.warn('[Logger] pino-pretty not found. Install it for pretty logging: pnpm add -D pino-pretty');
105
+ // Fallback to text/simple
106
+ targets.push({
107
+ target: 'pino/file',
108
+ options: { destination: 1 },
109
+ level: this.config.level
110
+ });
111
+ }
87
112
  } else if (this.config.format === 'json') {
88
113
  // JSON to stdout
89
114
  targets.push({
@@ -32,10 +32,11 @@ export interface ServiceRegistration {
32
32
  }
33
33
 
34
34
  /**
35
- * Plugin Configuration Validator
35
+ * Plugin Configuration Validator Interface
36
36
  * Uses Zod for runtime validation of plugin configurations
37
+ * @deprecated Use the PluginConfigValidator class from security module instead
37
38
  */
38
- export interface PluginConfigValidator {
39
+ export interface IPluginConfigValidator {
39
40
  schema: z.ZodSchema;
40
41
  validate(config: any): any;
41
42
  }
@@ -366,15 +367,20 @@ export class PluginLoader {
366
367
  return semverRegex.test(version);
367
368
  }
368
369
 
369
- private validatePluginConfig(plugin: PluginMetadata): void {
370
+ private validatePluginConfig(plugin: PluginMetadata, config?: any): void {
370
371
  if (!plugin.configSchema) {
371
372
  return;
372
373
  }
373
374
 
374
- // TODO: Configuration validation implementation
375
- // This requires plugin config to be passed during loading
376
- // For now, just validate that the schema exists
377
- this.logger.debug(`Plugin ${plugin.name} has configuration schema (validation not yet implemented)`);
375
+ if (!config) {
376
+ this.logger.debug(`Plugin ${plugin.name} has configuration schema but no config provided`);
377
+ return;
378
+ }
379
+
380
+ // Configuration validation is now implemented in PluginConfigValidator
381
+ // This is a placeholder that logs the validation would happen
382
+ // The actual validation should be done by the caller when config is available
383
+ this.logger.debug(`Plugin ${plugin.name} has configuration schema (use PluginConfigValidator for validation)`);
378
384
  }
379
385
 
380
386
  private async verifyPluginSignature(plugin: PluginMetadata): Promise<void> {
@@ -382,12 +388,10 @@ export class PluginLoader {
382
388
  return;
383
389
  }
384
390
 
385
- // TODO: Plugin signature verification implementation
386
- // In a real implementation:
387
- // 1. Extract public key from trusted source
388
- // 2. Verify signature against plugin code hash
389
- // 3. Throw error if verification fails
390
- this.logger.debug(`Plugin ${plugin.name} signature verification (not yet implemented)`);
391
+ // Plugin signature verification is now implemented in PluginSignatureVerifier
392
+ // This is a placeholder that logs the verification would happen
393
+ // The actual verification should be done by the caller with proper security config
394
+ this.logger.debug(`Plugin ${plugin.name} has signature (use PluginSignatureVerifier for verification)`);
391
395
  }
392
396
 
393
397
  private async getSingletonService<T>(registration: ServiceRegistration): Promise<T> {
@@ -0,0 +1,14 @@
1
+ import { QA } from '@objectstack/spec';
2
+
3
+ /**
4
+ * Interface for executing test actions against a target system.
5
+ * The target could be a local Kernel instance or a remote API.
6
+ */
7
+ export interface TestExecutionAdapter {
8
+ /**
9
+ * Execute a single test action.
10
+ * @param action The action to perform (create_record, api_call, etc.)
11
+ * @returns The result of the action (e.g. created record, API response)
12
+ */
13
+ execute(action: QA.TestAction, context: Record<string, unknown>): Promise<unknown>;
14
+ }
@@ -0,0 +1,114 @@
1
+ import { QA } from '@objectstack/spec';
2
+ import { TestExecutionAdapter } from './adapter.js';
3
+
4
+ export class HttpTestAdapter implements TestExecutionAdapter {
5
+ constructor(private baseUrl: string, private authToken?: string) {}
6
+
7
+ async execute(action: QA.TestAction, _context: Record<string, unknown>): Promise<unknown> {
8
+ const headers: Record<string, string> = {
9
+ 'Content-Type': 'application/json',
10
+ };
11
+ if (this.authToken) {
12
+ headers['Authorization'] = `Bearer ${this.authToken}`;
13
+ }
14
+ // If action.user is specified, maybe add a specific header for impersonation if supported?
15
+ if (action.user) {
16
+ headers['X-Run-As'] = action.user;
17
+ }
18
+
19
+ switch (action.type) {
20
+ case 'create_record':
21
+ return this.createRecord(action.target, action.payload || {}, headers);
22
+ case 'update_record':
23
+ return this.updateRecord(action.target, action.payload || {}, headers);
24
+ case 'delete_record':
25
+ return this.deleteRecord(action.target, action.payload || {}, headers);
26
+ case 'read_record':
27
+ return this.readRecord(action.target, action.payload || {}, headers);
28
+ case 'query_records':
29
+ return this.queryRecords(action.target, action.payload || {}, headers);
30
+ case 'api_call':
31
+ return this.rawApiCall(action.target, action.payload || {}, headers);
32
+ case 'wait':
33
+ const ms = Number(action.payload?.duration || 1000);
34
+ return new Promise(resolve => setTimeout(() => resolve({ waited: ms }), ms));
35
+ default:
36
+ throw new Error(`Unsupported action type in HttpAdapter: ${action.type}`);
37
+ }
38
+ }
39
+
40
+ private async createRecord(objectName: string, data: Record<string, unknown>, headers: Record<string, string>) {
41
+ const response = await fetch(`${this.baseUrl}/api/data/${objectName}`, {
42
+ method: 'POST',
43
+ headers,
44
+ body: JSON.stringify(data)
45
+ });
46
+ return this.handleResponse(response);
47
+ }
48
+
49
+ private async updateRecord(objectName: string, data: Record<string, unknown>, headers: Record<string, string>) {
50
+ const id = data._id || data.id;
51
+ if (!id) throw new Error('Update record requires _id or id in payload');
52
+ const response = await fetch(`${this.baseUrl}/api/data/${objectName}/${id}`, {
53
+ method: 'PUT',
54
+ headers,
55
+ body: JSON.stringify(data)
56
+ });
57
+ return this.handleResponse(response);
58
+ }
59
+
60
+ private async deleteRecord(objectName: string, data: Record<string, unknown>, headers: Record<string, string>) {
61
+ const id = data._id || data.id;
62
+ if (!id) throw new Error('Delete record requires _id or id in payload');
63
+ const response = await fetch(`${this.baseUrl}/api/data/${objectName}/${id}`, {
64
+ method: 'DELETE',
65
+ headers
66
+ });
67
+ return this.handleResponse(response);
68
+ }
69
+
70
+ private async readRecord(objectName: string, data: Record<string, unknown>, headers: Record<string, string>) {
71
+ const id = data._id || data.id;
72
+ if (!id) throw new Error('Read record requires _id or id in payload');
73
+ const response = await fetch(`${this.baseUrl}/api/data/${objectName}/${id}`, {
74
+ method: 'GET',
75
+ headers
76
+ });
77
+ return this.handleResponse(response);
78
+ }
79
+
80
+ private async queryRecords(objectName: string, data: Record<string, unknown>, headers: Record<string, string>) {
81
+ // Assuming query via POST or GraphQL-like endpoint
82
+ const response = await fetch(`${this.baseUrl}/api/data/${objectName}/query`, {
83
+ method: 'POST',
84
+ headers,
85
+ body: JSON.stringify(data)
86
+ });
87
+ return this.handleResponse(response);
88
+ }
89
+
90
+ private async rawApiCall(endpoint: string, data: Record<string, unknown>, headers: Record<string, string>) {
91
+ const method = (data.method as string) || 'GET';
92
+ const body = data.body ? JSON.stringify(data.body) : undefined;
93
+ const url = endpoint.startsWith('http') ? endpoint : `${this.baseUrl}${endpoint}`;
94
+
95
+ const response = await fetch(url, {
96
+ method,
97
+ headers,
98
+ body
99
+ });
100
+ return this.handleResponse(response);
101
+ }
102
+
103
+ private async handleResponse(response: Response) {
104
+ if (!response.ok) {
105
+ const text = await response.text();
106
+ throw new Error(`HTTP Error ${response.status}: ${text}`);
107
+ }
108
+ const contentType = response.headers.get('content-type');
109
+ if (contentType && contentType.includes('application/json')) {
110
+ return response.json();
111
+ }
112
+ return response.text();
113
+ }
114
+ }
@@ -0,0 +1,3 @@
1
+ export * from './adapter.js';
2
+ export * from './runner.js';
3
+ export * from './http-adapter.js';
@@ -0,0 +1,179 @@
1
+ import { QA } from '@objectstack/spec';
2
+ import { TestExecutionAdapter } from './adapter.js';
3
+
4
+ export interface TestResult {
5
+ scenarioId: string;
6
+ passed: boolean;
7
+ steps: StepResult[];
8
+ error?: unknown;
9
+ duration: number;
10
+ }
11
+
12
+ export interface StepResult {
13
+ stepName: string;
14
+ passed: boolean;
15
+ error?: unknown;
16
+ output?: unknown;
17
+ duration: number;
18
+ }
19
+
20
+ export class TestRunner {
21
+ constructor(private adapter: TestExecutionAdapter) {}
22
+
23
+ async runSuite(suite: QA.TestSuite): Promise<TestResult[]> {
24
+ const results: TestResult[] = [];
25
+ for (const scenario of suite.scenarios) {
26
+ results.push(await this.runScenario(scenario));
27
+ }
28
+ return results;
29
+ }
30
+
31
+ async runScenario(scenario: QA.TestScenario): Promise<TestResult> {
32
+ const startTime = Date.now();
33
+ const context: Record<string, unknown> = {}; // Variable context
34
+
35
+ // Initialize context from initial payload if needed? Currently schema doesn't have initial context prop on Scenario
36
+ // But we defined TestContextSchema separately.
37
+
38
+ // Setup
39
+ if (scenario.setup) {
40
+ for (const step of scenario.setup) {
41
+ try {
42
+ await this.runStep(step, context);
43
+ } catch (e) {
44
+ return {
45
+ scenarioId: scenario.id,
46
+ passed: false,
47
+ steps: [],
48
+ error: `Setup failed: ${e instanceof Error ? e.message : String(e)}`,
49
+ duration: Date.now() - startTime
50
+ };
51
+ }
52
+ }
53
+ }
54
+
55
+ const stepResults: StepResult[] = [];
56
+ let scenarioPassed = true;
57
+ let scenarioError: unknown = undefined;
58
+
59
+ // Main Steps
60
+ for (const step of scenario.steps) {
61
+ const stepStartTime = Date.now();
62
+ try {
63
+ const output = await this.runStep(step, context);
64
+ stepResults.push({
65
+ stepName: step.name,
66
+ passed: true,
67
+ output,
68
+ duration: Date.now() - stepStartTime
69
+ });
70
+ } catch (e) {
71
+ scenarioPassed = false;
72
+ scenarioError = e;
73
+ stepResults.push({
74
+ stepName: step.name,
75
+ passed: false,
76
+ error: e,
77
+ duration: Date.now() - stepStartTime
78
+ });
79
+ break; // Stop on first failure
80
+ }
81
+ }
82
+
83
+ // Teardown (run even if failed)
84
+ if (scenario.teardown) {
85
+ for (const step of scenario.teardown) {
86
+ try {
87
+ await this.runStep(step, context);
88
+ } catch (e) {
89
+ // Log teardown failure but don't override main failure if it exists
90
+ if (scenarioPassed) {
91
+ scenarioPassed = false;
92
+ scenarioError = `Teardown failed: ${e instanceof Error ? e.message : String(e)}`;
93
+ }
94
+ }
95
+ }
96
+ }
97
+
98
+ return {
99
+ scenarioId: scenario.id,
100
+ passed: scenarioPassed,
101
+ steps: stepResults,
102
+ error: scenarioError,
103
+ duration: Date.now() - startTime
104
+ };
105
+ }
106
+
107
+ private async runStep(step: QA.TestStep, context: Record<string, unknown>): Promise<unknown> {
108
+ // 1. Resolve Variables with Context (Simple interpolation or just pass context?)
109
+ // For now, assume adpater handles context resolution or we do basic replacement
110
+ const resolvedAction = this.resolveVariables(step.action, context);
111
+
112
+ // 2. Execute Action
113
+ const result = await this.adapter.execute(resolvedAction, context);
114
+
115
+ // 3. Capture Outputs
116
+ if (step.capture) {
117
+ for (const [varName, path] of Object.entries(step.capture)) {
118
+ context[varName] = this.getValueByPath(result, path);
119
+ }
120
+ }
121
+
122
+ // 4. Run Assertions
123
+ if (step.assertions) {
124
+ for (const assertion of step.assertions) {
125
+ this.assert(result, assertion, context);
126
+ }
127
+ }
128
+
129
+ return result;
130
+ }
131
+
132
+ private resolveVariables(action: QA.TestAction, _context: Record<string, unknown>): QA.TestAction {
133
+ // TODO: Implement JSON path variable substitution stringify/parse
134
+ // For now returning as is
135
+ return action;
136
+ }
137
+
138
+ private getValueByPath(obj: unknown, path: string): unknown {
139
+ if (!path) return obj;
140
+ const parts = path.split('.');
141
+ let current: any = obj;
142
+ for (const part of parts) {
143
+ if (current === null || current === undefined) return undefined;
144
+ current = current[part];
145
+ }
146
+ return current;
147
+ }
148
+
149
+ private assert(result: unknown, assertion: QA.TestAssertion, _context: Record<string, unknown>) {
150
+ const actual = this.getValueByPath(result, assertion.field);
151
+ // Resolve expected value if it's a variable ref?
152
+ const expected = assertion.expectedValue; // Simplify for now
153
+
154
+ switch (assertion.operator) {
155
+ case 'equals':
156
+ if (actual !== expected) throw new Error(`Assertion failed: ${assertion.field} expected ${expected}, got ${actual}`);
157
+ break;
158
+ case 'not_equals':
159
+ if (actual === expected) throw new Error(`Assertion failed: ${assertion.field} expected not ${expected}, got ${actual}`);
160
+ break;
161
+ case 'contains':
162
+ if (Array.isArray(actual)) {
163
+ if (!actual.includes(expected)) throw new Error(`Assertion failed: ${assertion.field} array does not contain ${expected}`);
164
+ } else if (typeof actual === 'string') {
165
+ if (!actual.includes(String(expected))) throw new Error(`Assertion failed: ${assertion.field} string does not contain ${expected}`);
166
+ }
167
+ break;
168
+ case 'not_null':
169
+ if (actual === null || actual === undefined) throw new Error(`Assertion failed: ${assertion.field} is null`);
170
+ break;
171
+ case 'is_null':
172
+ if (actual !== null && actual !== undefined) throw new Error(`Assertion failed: ${assertion.field} is not null`);
173
+ break;
174
+ // ... Add other operators
175
+ default:
176
+ throw new Error(`Unknown assertion operator: ${assertion.operator}`);
177
+ }
178
+ }
179
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Security Module
3
+ *
4
+ * Provides security features for the ObjectStack microkernel:
5
+ * - Plugin signature verification
6
+ * - Plugin configuration validation
7
+ * - Permission and capability enforcement
8
+ *
9
+ * @module @objectstack/core/security
10
+ */
11
+
12
+ export {
13
+ PluginSignatureVerifier,
14
+ type PluginSignatureConfig,
15
+ type SignatureVerificationResult,
16
+ } from './plugin-signature-verifier.js';
17
+
18
+ export {
19
+ PluginConfigValidator,
20
+ createPluginConfigValidator,
21
+ } from './plugin-config-validator.js';
22
+
23
+ export {
24
+ PluginPermissionEnforcer,
25
+ SecurePluginContext,
26
+ createPluginPermissionEnforcer,
27
+ type PluginPermissions,
28
+ type PermissionCheckResult,
29
+ } from './plugin-permission-enforcer.js';