@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.
- package/API_REGISTRY.md +392 -0
- package/CHANGELOG.md +8 -0
- package/README.md +36 -0
- package/dist/api-registry-plugin.d.ts +54 -0
- package/dist/api-registry-plugin.d.ts.map +1 -0
- package/dist/api-registry-plugin.js +53 -0
- package/dist/api-registry-plugin.test.d.ts +2 -0
- package/dist/api-registry-plugin.test.d.ts.map +1 -0
- package/dist/api-registry-plugin.test.js +332 -0
- package/dist/api-registry.d.ts +259 -0
- package/dist/api-registry.d.ts.map +1 -0
- package/dist/api-registry.js +599 -0
- package/dist/api-registry.test.d.ts +2 -0
- package/dist/api-registry.test.d.ts.map +1 -0
- package/dist/api-registry.test.js +957 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/logger.d.ts +1 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +35 -11
- package/dist/plugin-loader.d.ts +3 -2
- package/dist/plugin-loader.d.ts.map +1 -1
- package/dist/plugin-loader.js +13 -11
- package/dist/qa/adapter.d.ts +14 -0
- package/dist/qa/adapter.d.ts.map +1 -0
- package/dist/qa/adapter.js +1 -0
- package/dist/qa/http-adapter.d.ts +16 -0
- package/dist/qa/http-adapter.d.ts.map +1 -0
- package/dist/qa/http-adapter.js +107 -0
- package/dist/qa/index.d.ts +4 -0
- package/dist/qa/index.d.ts.map +1 -0
- package/dist/qa/index.js +3 -0
- package/dist/qa/runner.d.ts +27 -0
- package/dist/qa/runner.d.ts.map +1 -0
- package/dist/qa/runner.js +157 -0
- package/dist/security/index.d.ts +14 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +13 -0
- package/dist/security/plugin-config-validator.d.ts +79 -0
- package/dist/security/plugin-config-validator.d.ts.map +1 -0
- package/dist/security/plugin-config-validator.js +166 -0
- package/dist/security/plugin-config-validator.test.d.ts +2 -0
- package/dist/security/plugin-config-validator.test.d.ts.map +1 -0
- package/dist/security/plugin-config-validator.test.js +223 -0
- package/dist/security/plugin-permission-enforcer.d.ts +154 -0
- package/dist/security/plugin-permission-enforcer.d.ts.map +1 -0
- package/dist/security/plugin-permission-enforcer.js +323 -0
- package/dist/security/plugin-permission-enforcer.test.d.ts +2 -0
- package/dist/security/plugin-permission-enforcer.test.d.ts.map +1 -0
- package/dist/security/plugin-permission-enforcer.test.js +205 -0
- package/dist/security/plugin-signature-verifier.d.ts +96 -0
- package/dist/security/plugin-signature-verifier.d.ts.map +1 -0
- package/dist/security/plugin-signature-verifier.js +250 -0
- package/examples/api-registry-example.ts +557 -0
- package/package.json +2 -2
- package/src/api-registry-plugin.test.ts +391 -0
- package/src/api-registry-plugin.ts +86 -0
- package/src/api-registry.test.ts +1089 -0
- package/src/api-registry.ts +736 -0
- package/src/index.ts +6 -0
- package/src/logger.ts +36 -11
- package/src/plugin-loader.ts +17 -13
- package/src/qa/adapter.ts +14 -0
- package/src/qa/http-adapter.ts +114 -0
- package/src/qa/index.ts +3 -0
- package/src/qa/runner.ts +179 -0
- package/src/security/index.ts +29 -0
- package/src/security/plugin-config-validator.test.ts +276 -0
- package/src/security/plugin-config-validator.ts +191 -0
- package/src/security/plugin-permission-enforcer.test.ts +251 -0
- package/src/security/plugin-permission-enforcer.ts +408 -0
- 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
|
-
//
|
|
57
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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({
|
package/src/plugin-loader.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
//
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
|
|
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
|
+
}
|
package/src/qa/index.ts
ADDED
package/src/qa/runner.ts
ADDED
|
@@ -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';
|