opc-agent 0.9.0 → 1.1.0

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/src/schema/oad.ts CHANGED
@@ -48,6 +48,11 @@ export const HITLSchema = z.object({
48
48
  defaultAction: z.enum(['approve', 'deny']).default('deny'),
49
49
  });
50
50
 
51
+ export const PluginRefSchema = z.object({
52
+ name: z.string(),
53
+ config: z.record(z.unknown()).optional(),
54
+ });
55
+
51
56
  export const AuthSchema = z.object({
52
57
  enabled: z.boolean().default(false),
53
58
  apiKeys: z.array(z.string()).default([]),
@@ -131,6 +136,7 @@ export const SpecSchema = z.object({
131
136
  webhook: WebhookSchema.optional(),
132
137
  hitl: HITLSchema.optional(),
133
138
  auth: AuthSchema.optional(),
139
+ plugins: z.array(PluginRefSchema).optional(),
134
140
  });
135
141
 
136
142
  export const OADSchema = z.object({
@@ -0,0 +1,83 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { OPCError, ProviderError, ValidationError, ConfigError, ChannelError, PluginError, RateLimitError, SecurityError, TimeoutError, wrapError, formatErrorForUser } from '../src/core/errors';
3
+
4
+ describe('Error Hierarchy', () => {
5
+ it('OPCError has code, hint, timestamp', () => {
6
+ const err = new OPCError('boom', { code: 'TEST', hint: 'try again' });
7
+ expect(err.message).toBe('boom');
8
+ expect(err.code).toBe('TEST');
9
+ expect(err.hint).toBe('try again');
10
+ expect(err.timestamp).toBeGreaterThan(0);
11
+ expect(err.toUserMessage()).toContain('try again');
12
+ });
13
+
14
+ it('toJSON serializes correctly', () => {
15
+ const err = new OPCError('test', { code: 'T1' });
16
+ const json = err.toJSON();
17
+ expect(json.name).toBe('OPCError');
18
+ expect(json.code).toBe('T1');
19
+ });
20
+
21
+ it('ProviderError includes provider', () => {
22
+ const err = new ProviderError('openai', 'API key invalid', { statusCode: 401 });
23
+ expect(err.provider).toBe('openai');
24
+ expect(err.statusCode).toBe(401);
25
+ expect(err.code).toBe('OPC_PROVIDER_ERROR');
26
+ expect(err instanceof OPCError).toBe(true);
27
+ });
28
+
29
+ it('ValidationError includes errors array', () => {
30
+ const err = new ValidationError('Invalid config', ['missing name', 'bad version'], 'metadata');
31
+ expect(err.errors).toEqual(['missing name', 'bad version']);
32
+ expect(err.field).toBe('metadata');
33
+ });
34
+
35
+ it('ConfigError', () => {
36
+ const err = new ConfigError('Missing oad.yaml');
37
+ expect(err.code).toBe('OPC_CONFIG_ERROR');
38
+ });
39
+
40
+ it('ChannelError', () => {
41
+ const err = new ChannelError('web', 'Port in use');
42
+ expect(err.channelType).toBe('web');
43
+ });
44
+
45
+ it('PluginError', () => {
46
+ const err = new PluginError('my-plugin', 'Init failed');
47
+ expect(err.pluginName).toBe('my-plugin');
48
+ });
49
+
50
+ it('RateLimitError', () => {
51
+ const err = new RateLimitError(undefined, 5000);
52
+ expect(err.retryAfterMs).toBe(5000);
53
+ expect(err.toUserMessage()).toContain('5 seconds');
54
+ });
55
+
56
+ it('SecurityError', () => {
57
+ const err = new SecurityError('Blocked');
58
+ expect(err.code).toBe('OPC_SECURITY_ERROR');
59
+ });
60
+
61
+ it('TimeoutError', () => {
62
+ const err = new TimeoutError('llm-call', 30000);
63
+ expect(err.message).toContain('30000ms');
64
+ });
65
+
66
+ it('wrapError wraps unknown errors', () => {
67
+ const wrapped = wrapError('string error');
68
+ expect(wrapped instanceof OPCError).toBe(true);
69
+ expect(wrapped.message).toBe('string error');
70
+
71
+ const native = wrapError(new Error('native'));
72
+ expect(native.message).toBe('native');
73
+
74
+ const existing = new ProviderError('x', 'y');
75
+ expect(wrapError(existing)).toBe(existing);
76
+ });
77
+
78
+ it('formatErrorForUser returns clean message', () => {
79
+ expect(formatErrorForUser(new RateLimitError())).toContain('Rate limit');
80
+ expect(formatErrorForUser(new Error('raw'))).toBe('raw');
81
+ expect(formatErrorForUser('string')).toBe('string');
82
+ });
83
+ });
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { sanitizeInput, detectInjection, APIKeyManager } from '../src/core/security';
3
+
4
+ describe('Security', () => {
5
+ describe('sanitizeInput', () => {
6
+ it('strips script tags', () => {
7
+ expect(sanitizeInput('<script>alert(1)</script>hello')).not.toContain('<script');
8
+ });
9
+ it('encodes HTML entities', () => {
10
+ const result = sanitizeInput('a < b & c > d');
11
+ expect(result).toContain('&lt;');
12
+ expect(result).toContain('&amp;');
13
+ expect(result).toContain('&gt;');
14
+ });
15
+ });
16
+
17
+ describe('detectInjection', () => {
18
+ it('detects XSS', () => {
19
+ const r = detectInjection('<script>alert(1)</script>');
20
+ expect(r.safe).toBe(false);
21
+ expect(r.threats).toContain('xss');
22
+ });
23
+ it('passes clean input', () => {
24
+ expect(detectInjection('Hello world')).toEqual({ safe: true, threats: [] });
25
+ });
26
+ });
27
+
28
+ describe('APIKeyManager', () => {
29
+ it('add, validate, revoke', () => {
30
+ const mgr = new APIKeyManager();
31
+ mgr.addKey('key1', { label: 'test' });
32
+ expect(mgr.isValid('key1')).toBe(true);
33
+ expect(mgr.isValid('key2')).toBe(false);
34
+ mgr.revokeKey('key1');
35
+ expect(mgr.isValid('key1')).toBe(false);
36
+ });
37
+
38
+ it('rotate key', () => {
39
+ const mgr = new APIKeyManager();
40
+ mgr.addKey('old');
41
+ expect(mgr.rotateKey('old', 'new')).toBe(true);
42
+ expect(mgr.isValid('old')).toBe(false);
43
+ expect(mgr.isValid('new')).toBe(true);
44
+ });
45
+
46
+ it('expires keys', () => {
47
+ const mgr = new APIKeyManager();
48
+ mgr.addKey('expired', { expiresAt: Date.now() - 1000 });
49
+ expect(mgr.isValid('expired')).toBe(false);
50
+ });
51
+
52
+ it('listActive filters', () => {
53
+ const mgr = new APIKeyManager();
54
+ mgr.addKey('a');
55
+ mgr.addKey('b');
56
+ mgr.revokeKey('b');
57
+ expect(mgr.listActive().length).toBe(1);
58
+ });
59
+ });
60
+ });