outcome-cli 1.0.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.
Files changed (113) hide show
  1. package/README.md +261 -0
  2. package/package.json +95 -0
  3. package/src/agents/README.md +139 -0
  4. package/src/agents/adapters/anthropic.adapter.ts +166 -0
  5. package/src/agents/adapters/dalle.adapter.ts +145 -0
  6. package/src/agents/adapters/gemini.adapter.ts +134 -0
  7. package/src/agents/adapters/imagen.adapter.ts +106 -0
  8. package/src/agents/adapters/nano-banana.adapter.ts +129 -0
  9. package/src/agents/adapters/openai.adapter.ts +165 -0
  10. package/src/agents/adapters/veo.adapter.ts +130 -0
  11. package/src/agents/agent.schema.property.test.ts +379 -0
  12. package/src/agents/agent.schema.test.ts +148 -0
  13. package/src/agents/agent.schema.ts +263 -0
  14. package/src/agents/index.ts +60 -0
  15. package/src/agents/registered-agent.schema.ts +356 -0
  16. package/src/agents/registry.ts +97 -0
  17. package/src/agents/tournament-configs.property.test.ts +266 -0
  18. package/src/cli/README.md +145 -0
  19. package/src/cli/commands/define.ts +79 -0
  20. package/src/cli/commands/list.ts +46 -0
  21. package/src/cli/commands/logs.ts +83 -0
  22. package/src/cli/commands/run.ts +416 -0
  23. package/src/cli/commands/verify.ts +110 -0
  24. package/src/cli/index.ts +81 -0
  25. package/src/config/README.md +128 -0
  26. package/src/config/env.ts +262 -0
  27. package/src/config/index.ts +19 -0
  28. package/src/eval/README.md +318 -0
  29. package/src/eval/ai-judge.test.ts +435 -0
  30. package/src/eval/ai-judge.ts +368 -0
  31. package/src/eval/code-validators.ts +414 -0
  32. package/src/eval/evaluateOutcome.property.test.ts +1174 -0
  33. package/src/eval/evaluateOutcome.ts +591 -0
  34. package/src/eval/immigration-validators.ts +122 -0
  35. package/src/eval/index.ts +90 -0
  36. package/src/eval/judge-cache.ts +402 -0
  37. package/src/eval/tournament-validators.property.test.ts +439 -0
  38. package/src/eval/validators.property.test.ts +1118 -0
  39. package/src/eval/validators.ts +1199 -0
  40. package/src/eval/weighted-scorer.ts +285 -0
  41. package/src/index.ts +17 -0
  42. package/src/league/README.md +188 -0
  43. package/src/league/health-check.ts +353 -0
  44. package/src/league/index.ts +93 -0
  45. package/src/league/killAgent.ts +151 -0
  46. package/src/league/league.test.ts +1151 -0
  47. package/src/league/runLeague.ts +843 -0
  48. package/src/league/scoreAgent.ts +175 -0
  49. package/src/modules/omnibridge/__tests__/.gitkeep +1 -0
  50. package/src/modules/omnibridge/__tests__/auth-tunnel.property.test.ts +524 -0
  51. package/src/modules/omnibridge/__tests__/deterministic-logger.property.test.ts +965 -0
  52. package/src/modules/omnibridge/__tests__/ghost-api.property.test.ts +461 -0
  53. package/src/modules/omnibridge/__tests__/omnibridge-integration.test.ts +542 -0
  54. package/src/modules/omnibridge/__tests__/parallel-executor.property.test.ts +671 -0
  55. package/src/modules/omnibridge/__tests__/semantic-normalizer.property.test.ts +521 -0
  56. package/src/modules/omnibridge/__tests__/semantic-normalizer.test.ts +254 -0
  57. package/src/modules/omnibridge/__tests__/session-vault.property.test.ts +367 -0
  58. package/src/modules/omnibridge/__tests__/shadow-session.property.test.ts +523 -0
  59. package/src/modules/omnibridge/__tests__/triangulation-engine.property.test.ts +292 -0
  60. package/src/modules/omnibridge/__tests__/verification-engine.property.test.ts +769 -0
  61. package/src/modules/omnibridge/api/.gitkeep +1 -0
  62. package/src/modules/omnibridge/api/ghost-api.ts +1087 -0
  63. package/src/modules/omnibridge/auth/.gitkeep +1 -0
  64. package/src/modules/omnibridge/auth/auth-tunnel.ts +843 -0
  65. package/src/modules/omnibridge/auth/session-vault.ts +577 -0
  66. package/src/modules/omnibridge/core/.gitkeep +1 -0
  67. package/src/modules/omnibridge/core/semantic-normalizer.ts +702 -0
  68. package/src/modules/omnibridge/core/triangulation-engine.ts +530 -0
  69. package/src/modules/omnibridge/core/types.ts +610 -0
  70. package/src/modules/omnibridge/execution/.gitkeep +1 -0
  71. package/src/modules/omnibridge/execution/deterministic-logger.ts +629 -0
  72. package/src/modules/omnibridge/execution/parallel-executor.ts +542 -0
  73. package/src/modules/omnibridge/execution/shadow-session.ts +794 -0
  74. package/src/modules/omnibridge/index.ts +212 -0
  75. package/src/modules/omnibridge/omnibridge.ts +510 -0
  76. package/src/modules/omnibridge/verification/.gitkeep +1 -0
  77. package/src/modules/omnibridge/verification/verification-engine.ts +783 -0
  78. package/src/outcomes/README.md +75 -0
  79. package/src/outcomes/acquire-pilot-customer.ts +297 -0
  80. package/src/outcomes/code-delivery-outcomes.ts +89 -0
  81. package/src/outcomes/code-outcomes.ts +256 -0
  82. package/src/outcomes/code_review_battle.test.ts +135 -0
  83. package/src/outcomes/code_review_battle.ts +135 -0
  84. package/src/outcomes/cold_email_battle.ts +97 -0
  85. package/src/outcomes/content_creation_battle.ts +160 -0
  86. package/src/outcomes/f1_stem_opt_compliance.ts +61 -0
  87. package/src/outcomes/index.ts +107 -0
  88. package/src/outcomes/lead_gen_battle.test.ts +113 -0
  89. package/src/outcomes/lead_gen_battle.ts +99 -0
  90. package/src/outcomes/outcome.schema.property.test.ts +229 -0
  91. package/src/outcomes/outcome.schema.ts +187 -0
  92. package/src/outcomes/qualified_sales_interest.ts +118 -0
  93. package/src/outcomes/swarm_planner.property.test.ts +370 -0
  94. package/src/outcomes/swarm_planner.ts +96 -0
  95. package/src/outcomes/web_extraction.ts +234 -0
  96. package/src/runtime/README.md +220 -0
  97. package/src/runtime/agentRunner.test.ts +341 -0
  98. package/src/runtime/agentRunner.ts +746 -0
  99. package/src/runtime/claudeAdapter.ts +232 -0
  100. package/src/runtime/costTracker.ts +123 -0
  101. package/src/runtime/index.ts +34 -0
  102. package/src/runtime/modelAdapter.property.test.ts +305 -0
  103. package/src/runtime/modelAdapter.ts +144 -0
  104. package/src/runtime/openaiAdapter.ts +235 -0
  105. package/src/utils/README.md +122 -0
  106. package/src/utils/command-runner.ts +134 -0
  107. package/src/utils/cost-guard.ts +379 -0
  108. package/src/utils/errors.test.ts +290 -0
  109. package/src/utils/errors.ts +442 -0
  110. package/src/utils/index.ts +37 -0
  111. package/src/utils/logger.test.ts +361 -0
  112. package/src/utils/logger.ts +419 -0
  113. package/src/utils/output-parsers.ts +216 -0
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Unit Tests for Semantic Normalizer
3
+ *
4
+ * Tests for HTML parsing, noise stripping, Intent_ID assignment, and form extraction.
5
+ * Requirements: 1.1-1.6
6
+ */
7
+
8
+ import { describe, test, expect } from 'vitest';
9
+ import { SemanticNormalizer } from '../core/semantic-normalizer.js';
10
+
11
+ const normalizer = new SemanticNormalizer();
12
+
13
+ describe('SemanticNormalizer', () => {
14
+ describe('stripNoise', () => {
15
+ test('removes script tags', () => {
16
+ const html = '<html><body><script>alert("test")</script><p>Content</p></body></html>';
17
+ const result = normalizer.stripNoise(html);
18
+ expect(result).not.toContain('<script>');
19
+ expect(result).toContain('<p>Content</p>');
20
+ });
21
+
22
+ test('removes style tags', () => {
23
+ const html = '<html><head><style>.test { color: red; }</style></head><body><p>Content</p></body></html>';
24
+ const result = normalizer.stripNoise(html);
25
+ expect(result).not.toContain('<style>');
26
+ expect(result).toContain('<p>Content</p>');
27
+ });
28
+
29
+ test('removes inline styles', () => {
30
+ const html = '<html><body><div style="color: red; margin: 10px;">Content</div></body></html>';
31
+ const result = normalizer.stripNoise(html);
32
+ expect(result).not.toContain('style=');
33
+ });
34
+
35
+ test('removes tracking attributes', () => {
36
+ const html = '<html><body><div data-tracking="click" data-analytics="view">Content</div></body></html>';
37
+ const result = normalizer.stripNoise(html);
38
+ expect(result).not.toContain('data-tracking');
39
+ expect(result).not.toContain('data-analytics');
40
+ });
41
+
42
+ test('removes meta tags', () => {
43
+ const html = '<html><head><meta name="description" content="test"><meta name="keywords" content="test"></head><body><p>Content</p></body></html>';
44
+ const result = normalizer.stripNoise(html);
45
+ expect(result).not.toContain('<meta');
46
+ });
47
+
48
+ test('removes stylesheet links', () => {
49
+ const html = '<html><head><link rel="stylesheet" href="styles.css"></head><body><p>Content</p></body></html>';
50
+ const result = normalizer.stripNoise(html);
51
+ expect(result).not.toContain('stylesheet');
52
+ });
53
+ });
54
+
55
+ describe('assignIntentId', () => {
56
+ test('assigns ACTION category for button elements', () => {
57
+ const result = normalizer.assignIntentId('button', undefined, {}, 'Submit', []);
58
+ expect(result.category).toBe('ACTION');
59
+ });
60
+
61
+ test('assigns NAV category for anchor elements', () => {
62
+ const result = normalizer.assignIntentId('a', undefined, { href: '/home' }, 'Home', []);
63
+ expect(result.category).toBe('NAV');
64
+ });
65
+
66
+ test('assigns INPUT category for input elements', () => {
67
+ const result = normalizer.assignIntentId('input', undefined, { type: 'text' }, '', []);
68
+ expect(result.category).toBe('INPUT');
69
+ });
70
+
71
+ test('prioritizes ARIA role over tag name', () => {
72
+ // div with role="button" should be ACTION, not DISPLAY
73
+ const result = normalizer.assignIntentId('div', 'button', {}, 'Click me', []);
74
+ expect(result.category).toBe('ACTION');
75
+ expect(result.confidence).toBe(0.95);
76
+ });
77
+
78
+ test('infers LOGIN purpose from text content', () => {
79
+ const result = normalizer.assignIntentId('button', undefined, {}, 'Login', []);
80
+ expect(result.purpose).toBe('LOGIN');
81
+ });
82
+
83
+ test('infers SEARCH purpose from placeholder', () => {
84
+ const result = normalizer.assignIntentId('input', undefined, { placeholder: 'Search...' }, '', []);
85
+ expect(result.purpose).toBe('SEARCH');
86
+ });
87
+
88
+ test('infers EMAIL_INPUT purpose from type', () => {
89
+ const result = normalizer.assignIntentId('input', undefined, { type: 'email' }, '', []);
90
+ expect(result.purpose).toBe('EMAIL_INPUT');
91
+ });
92
+
93
+ test('infers PASSWORD_INPUT purpose from type', () => {
94
+ const result = normalizer.assignIntentId('input', undefined, { type: 'password' }, '', []);
95
+ expect(result.purpose).toBe('PASSWORD_INPUT');
96
+ });
97
+
98
+ test('increases confidence with more signals', () => {
99
+ const lowConfidence = normalizer.assignIntentId('div', undefined, {}, '', []);
100
+ const highConfidence = normalizer.assignIntentId('div', undefined, { id: 'main' }, 'Content', ['context']);
101
+ expect(highConfidence.confidence).toBeGreaterThan(lowConfidence.confidence);
102
+ });
103
+ });
104
+
105
+
106
+ describe('normalize', () => {
107
+ test('extracts semantic elements from HTML', () => {
108
+ const html = `
109
+ <html>
110
+ <body>
111
+ <button id="submit-btn">Submit</button>
112
+ <a href="/home">Home</a>
113
+ <input type="text" name="username" placeholder="Username">
114
+ </body>
115
+ </html>
116
+ `;
117
+ const result = normalizer.normalize(html, 'https://test.com');
118
+
119
+ expect(result.elements.length).toBeGreaterThan(0);
120
+ expect(result.sourceUrl).toBe('https://test.com');
121
+ expect(result.createdAt).toBeDefined();
122
+ });
123
+
124
+ test('extracts forms with fields', () => {
125
+ const html = `
126
+ <html>
127
+ <body>
128
+ <form action="/login" method="POST">
129
+ <label for="email">Email</label>
130
+ <input type="email" id="email" name="email" required>
131
+ <label for="password">Password</label>
132
+ <input type="password" id="password" name="password" required minlength="8">
133
+ <button type="submit">Login</button>
134
+ </form>
135
+ </body>
136
+ </html>
137
+ `;
138
+ const result = normalizer.normalize(html, 'https://test.com');
139
+
140
+ expect(result.forms.length).toBe(1);
141
+ const form = result.forms[0];
142
+ expect(form.action).toBe('/login');
143
+ expect(form.method).toBe('POST');
144
+ expect(form.fields.length).toBe(2);
145
+
146
+ const emailField = form.fields.find((f) => f.name === 'email');
147
+ expect(emailField).toBeDefined();
148
+ expect(emailField?.required).toBe(true);
149
+ expect(emailField?.type).toBe('email');
150
+
151
+ const passwordField = form.fields.find((f) => f.name === 'password');
152
+ expect(passwordField).toBeDefined();
153
+ expect(passwordField?.required).toBe(true);
154
+ expect(passwordField?.validationRules).toContain('minlength:8');
155
+ });
156
+
157
+ test('extracts navigation links', () => {
158
+ const html = `
159
+ <html>
160
+ <body>
161
+ <nav>
162
+ <a href="/home">Home</a>
163
+ <a href="/about">About</a>
164
+ <a href="/contact">Contact</a>
165
+ </nav>
166
+ </body>
167
+ </html>
168
+ `;
169
+ const result = normalizer.normalize(html, 'https://test.com');
170
+
171
+ expect(result.navigation.primaryLinks.length).toBe(3);
172
+ expect(result.navigation.primaryLinks.map((l) => l.label)).toContain('Home');
173
+ expect(result.navigation.primaryLinks.map((l) => l.label)).toContain('About');
174
+ expect(result.navigation.primaryLinks.map((l) => l.label)).toContain('Contact');
175
+ });
176
+
177
+ test('calculates build time', () => {
178
+ const html = '<html><body><p>Test</p></body></html>';
179
+ const result = normalizer.normalize(html, 'https://test.com');
180
+
181
+ expect(result.buildTimeMs).toBeGreaterThanOrEqual(0);
182
+ });
183
+
184
+ test('calculates token reduction', () => {
185
+ const html = `
186
+ <html>
187
+ <head>
188
+ <style>.test { color: red; background: blue; margin: 10px; padding: 20px; }</style>
189
+ <script>console.log("tracking"); analytics.track("pageview");</script>
190
+ <meta name="description" content="A very long description that takes up tokens">
191
+ </head>
192
+ <body>
193
+ <div style="color: red; margin: 10px; padding: 20px; background: white;">
194
+ <p>Simple content</p>
195
+ </div>
196
+ </body>
197
+ </html>
198
+ `;
199
+ const result = normalizer.normalize(html, 'https://test.com');
200
+
201
+ // Token reduction should be calculated
202
+ expect(typeof result.tokenReduction).toBe('number');
203
+ });
204
+
205
+ test('handles elements with ARIA roles', () => {
206
+ const html = `
207
+ <html>
208
+ <body>
209
+ <div role="button" id="custom-btn">Click me</div>
210
+ <div role="navigation">
211
+ <span role="link">Nav item</span>
212
+ </div>
213
+ </body>
214
+ </html>
215
+ `;
216
+ const result = normalizer.normalize(html, 'https://test.com');
217
+
218
+ const buttonElement = result.elements.find((e) => e.ariaRole === 'button');
219
+ expect(buttonElement).toBeDefined();
220
+ expect(buttonElement?.intentId).toContain('ACTION');
221
+
222
+ const navElement = result.elements.find((e) => e.ariaRole === 'navigation');
223
+ expect(navElement).toBeDefined();
224
+ expect(navElement?.intentId).toContain('NAV');
225
+ });
226
+
227
+ test('skips hidden elements', () => {
228
+ const html = `
229
+ <html>
230
+ <body>
231
+ <button hidden>Hidden button</button>
232
+ <button aria-hidden="true">Also hidden</button>
233
+ <button>Visible button</button>
234
+ </body>
235
+ </html>
236
+ `;
237
+ const result = normalizer.normalize(html, 'https://test.com');
238
+
239
+ const buttons = result.elements.filter((e) => e.tagName === 'button');
240
+ expect(buttons.length).toBe(1);
241
+ expect(buttons[0].label).toBe('Visible button');
242
+ });
243
+ });
244
+
245
+ describe('countTokens', () => {
246
+ test('approximates token count', () => {
247
+ const text = 'Hello world this is a test';
248
+ const tokens = normalizer.countTokens(text);
249
+ // ~4 chars per token, 26 chars = ~7 tokens
250
+ expect(tokens).toBeGreaterThan(0);
251
+ expect(tokens).toBeLessThan(text.length);
252
+ });
253
+ });
254
+ });
@@ -0,0 +1,367 @@
1
+ /**
2
+ * Property-Based Tests for Session Vault
3
+ *
4
+ * These tests validate the correctness properties defined in the design document.
5
+ * Each property test runs minimum 100 iterations with randomly generated inputs.
6
+ */
7
+
8
+ import { describe, test, expect, beforeEach } from 'vitest';
9
+ import * as fc from 'fast-check';
10
+ import {
11
+ SessionVault,
12
+ createSessionVault,
13
+ generateEncryptionKey,
14
+ type RawSessionData,
15
+ } from '../auth/session-vault.js';
16
+ import type { ActionScope, CapabilityToken } from '../core/types.js';
17
+
18
+ // =============================================================================
19
+ // Test Setup
20
+ // =============================================================================
21
+
22
+ // Note: Each property test creates its own vault instance to ensure isolation
23
+ // The beforeEach vault is only used for tests that explicitly need shared state
24
+
25
+ let vault: SessionVault;
26
+
27
+ beforeEach(() => {
28
+ vault = createSessionVault({
29
+ encryptionKey: generateEncryptionKey(),
30
+ defaultTokenExpirationMs: 3600000, // 1 hour
31
+ defaultMaxExecutions: 100,
32
+ });
33
+ });
34
+
35
+ // =============================================================================
36
+ // Arbitraries (Test Data Generators)
37
+ // =============================================================================
38
+
39
+ /**
40
+ * Generate a valid domain name.
41
+ */
42
+ const domainArb = fc.stringMatching(/^[a-z][a-z0-9-]{2,20}\.(com|org|net|io)$/);
43
+
44
+ /**
45
+ * Generate raw session data (simulating cookies, localStorage, sessionStorage).
46
+ */
47
+ const rawSessionDataArb: fc.Arbitrary<RawSessionData> = fc.record({
48
+ cookies: fc.string({ minLength: 10, maxLength: 500 }),
49
+ localStorage: fc.string({ minLength: 10, maxLength: 500 }),
50
+ sessionStorage: fc.string({ minLength: 10, maxLength: 500 }),
51
+ });
52
+
53
+ /**
54
+ * Generate action names.
55
+ */
56
+ const actionNameArb = fc.stringMatching(/^[a-z][a-z_]{2,20}$/);
57
+
58
+ /**
59
+ * Fixed base timestamp for deterministic test behavior.
60
+ */
61
+ const BASE_TIME = 1700000000000;
62
+
63
+ /**
64
+ * Generate an action scope.
65
+ * Uses fixed timestamps relative to BASE_TIME for deterministic tests.
66
+ */
67
+ const actionScopeArb: fc.Arbitrary<Partial<ActionScope>> = fc.record({
68
+ allowedActions: fc.array(actionNameArb, { minLength: 1, maxLength: 10 }),
69
+ blockedActions: fc.array(actionNameArb, { minLength: 0, maxLength: 5 }),
70
+ maxExecutions: fc.integer({ min: 1, max: 1000 }),
71
+ expiresAt: fc.integer({ min: BASE_TIME + 60000, max: BASE_TIME + 86400000 }),
72
+ });
73
+
74
+ /**
75
+ * Generate credential-like strings that should NEVER appear in tokens.
76
+ */
77
+ const credentialPatternArb = fc.oneof(
78
+ fc.constant('password: "secret123"'),
79
+ fc.constant('api_key: "sk_live_abc123xyz"'),
80
+ fc.constant('secret: "my-secret-value"'),
81
+ fc.constant('Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'),
82
+ fc.constant('Basic dXNlcm5hbWU6cGFzc3dvcmQ='),
83
+ fc.constant('cookie: "session_id=abc123"'),
84
+ fc.constant('session: "authenticated_session_token"')
85
+ );
86
+
87
+ // =============================================================================
88
+ // Property Tests
89
+ // =============================================================================
90
+
91
+ describe('Session Vault Property Tests', () => {
92
+ /**
93
+ * **Feature: omnibridge, Property 12: Zero-Knowledge Credentials**
94
+ *
95
+ * *For any* Capability_Token issued to an agent, the token SHALL NOT contain
96
+ * raw credentials (passwords, API keys, session secrets)—only capability references.
97
+ *
98
+ * **Validates: Requirements 6.1**
99
+ */
100
+ test.each([{ numRuns: 100 }])(
101
+ 'Property 12: Zero-Knowledge Credentials - tokens never contain raw credentials',
102
+ async () => {
103
+ await fc.assert(
104
+ fc.asyncProperty(
105
+ domainArb,
106
+ rawSessionDataArb,
107
+ actionScopeArb,
108
+ async (domain, sessionData, scope) => {
109
+ // Store session with potentially sensitive data
110
+ await vault.store(domain, sessionData);
111
+
112
+ // Issue a capability token
113
+ const token = await vault.issueToken(domain, scope);
114
+
115
+ // PROPERTY: Token must NOT contain any raw credentials
116
+ const containsCredentials = vault.tokenContainsCredentials(token);
117
+ expect(containsCredentials).toBe(false);
118
+
119
+ // Additional checks: token should only contain capability info
120
+ expect(token.id).toMatch(/^cap_[a-f0-9]{32}$/);
121
+ expect(token.domain).toBe(domain);
122
+ expect(token.scope).toBeDefined();
123
+ expect(token.issuedAt).toBeGreaterThan(0);
124
+ expect(token.executionCount).toBe(0);
125
+
126
+ // Token should NOT contain the raw session data
127
+ const tokenStr = JSON.stringify(token);
128
+ expect(tokenStr).not.toContain(sessionData.cookies);
129
+ expect(tokenStr).not.toContain(sessionData.localStorage);
130
+ expect(tokenStr).not.toContain(sessionData.sessionStorage);
131
+
132
+ // Clean up for next iteration
133
+ vault.clear();
134
+
135
+ return true;
136
+ }
137
+ ),
138
+ { numRuns: 100 }
139
+ );
140
+ }
141
+ );
142
+
143
+ /**
144
+ * **Feature: omnibridge, Property 13: Capability Token Scoping**
145
+ *
146
+ * *For any* Capability_Token, attempting an action in `blockedActions` SHALL fail
147
+ * with a scope_violation error, while actions in `allowedActions` SHALL succeed.
148
+ *
149
+ * **Validates: Requirements 6.2, 6.3**
150
+ */
151
+ test.each([{ numRuns: 100 }])(
152
+ 'Property 13: Capability Token Scoping - blocked actions fail, allowed actions succeed',
153
+ async () => {
154
+ await fc.assert(
155
+ fc.asyncProperty(
156
+ domainArb,
157
+ rawSessionDataArb,
158
+ fc.array(actionNameArb, { minLength: 2, maxLength: 10 }),
159
+ fc.array(actionNameArb, { minLength: 1, maxLength: 5 }),
160
+ async (domain, sessionData, allowedActions, blockedActions) => {
161
+ // Ensure no overlap between allowed and blocked
162
+ const uniqueAllowed = allowedActions.filter((a) => !blockedActions.includes(a));
163
+ if (uniqueAllowed.length === 0) {
164
+ return true; // Skip if no unique allowed actions
165
+ }
166
+
167
+ await vault.store(domain, sessionData);
168
+
169
+ const token = await vault.issueToken(domain, {
170
+ allowedActions: uniqueAllowed,
171
+ blockedActions,
172
+ maxExecutions: 1000,
173
+ expiresAt: Date.now() + 3600000,
174
+ });
175
+
176
+ // PROPERTY: Allowed actions should succeed
177
+ for (const action of uniqueAllowed) {
178
+ const result = vault.checkScope(token.id, action);
179
+ expect(result.allowed).toBe(true);
180
+ expect(result.error).toBeUndefined();
181
+ }
182
+
183
+ // PROPERTY: Blocked actions should fail with scope_violation
184
+ for (const action of blockedActions) {
185
+ const result = vault.checkScope(token.id, action);
186
+ expect(result.allowed).toBe(false);
187
+ expect(result.error).toBeDefined();
188
+ expect(result.error?.type).toBe('scope_violation');
189
+ }
190
+
191
+ vault.clear();
192
+ return true;
193
+ }
194
+ ),
195
+ { numRuns: 100 }
196
+ );
197
+ }
198
+ );
199
+
200
+ /**
201
+ * **Feature: omnibridge, Property 14: Scope Violation Termination**
202
+ *
203
+ * *For any* scope violation attempt, the Session_Vault SHALL immediately revoke
204
+ * the Capability_Token AND terminate the associated Shadow_Session.
205
+ *
206
+ * **Validates: Requirements 6.7**
207
+ */
208
+ test.each([{ numRuns: 100 }])(
209
+ 'Property 14: Scope Violation Termination - violations revoke token immediately',
210
+ async () => {
211
+ await fc.assert(
212
+ fc.asyncProperty(
213
+ domainArb,
214
+ rawSessionDataArb,
215
+ fc.array(actionNameArb, { minLength: 1, maxLength: 5 }),
216
+ actionNameArb,
217
+ async (domain, sessionData, allowedActions, blockedAction) => {
218
+ // Ensure blocked action is not in allowed list
219
+ if (allowedActions.includes(blockedAction)) {
220
+ return true; // Skip this case
221
+ }
222
+
223
+ await vault.store(domain, sessionData);
224
+
225
+ const token = await vault.issueToken(domain, {
226
+ allowedActions,
227
+ blockedActions: [blockedAction],
228
+ maxExecutions: 100,
229
+ expiresAt: Date.now() + 3600000,
230
+ });
231
+
232
+ // Token should be valid initially
233
+ expect(vault.getToken(token.id)).not.toBeNull();
234
+ expect(vault.isTokenRevoked(token.id)).toBe(false);
235
+
236
+ // Attempt a scope violation
237
+ const violationResult = await vault.handleScopeViolation(token.id, blockedAction);
238
+
239
+ // PROPERTY: Token must be revoked immediately
240
+ expect(violationResult.terminated).toBe(true);
241
+ expect(violationResult.error.type).toBe('scope_violation');
242
+ // Type guard for scope_violation error
243
+ if (violationResult.error.type === 'scope_violation') {
244
+ expect(violationResult.error.attemptedAction).toBe(blockedAction);
245
+ }
246
+
247
+ // PROPERTY: Token should no longer be retrievable
248
+ expect(vault.getToken(token.id)).toBeNull();
249
+ expect(vault.isTokenRevoked(token.id)).toBe(true);
250
+
251
+ // PROPERTY: Further operations with this token should fail
252
+ const checkResult = vault.checkScope(token.id, allowedActions[0]);
253
+ expect(checkResult.allowed).toBe(false);
254
+
255
+ vault.clear();
256
+ return true;
257
+ }
258
+ ),
259
+ { numRuns: 100 }
260
+ );
261
+ }
262
+ );
263
+ });
264
+
265
+ // =============================================================================
266
+ // Additional Unit Tests for Edge Cases
267
+ // =============================================================================
268
+
269
+ describe('Session Vault Unit Tests', () => {
270
+ test('encryption round-trip preserves data', async () => {
271
+ const testData = 'sensitive_cookie_data=abc123; session_id=xyz789';
272
+
273
+ const encrypted = vault.encrypt(testData);
274
+ const decrypted = vault.decrypt(encrypted);
275
+
276
+ expect(decrypted).toBe(testData);
277
+ expect(encrypted.algorithm).toBe('AES-256-GCM');
278
+ expect(encrypted.iv).toBeDefined();
279
+ expect(encrypted.data).not.toBe(testData);
280
+ });
281
+
282
+ test('session storage and retrieval works correctly', async () => {
283
+ const domain = 'example.com';
284
+ const sessionData: RawSessionData = {
285
+ cookies: 'session_id=abc123',
286
+ localStorage: '{"user": "test"}',
287
+ sessionStorage: '{"temp": "data"}',
288
+ };
289
+
290
+ await vault.store(domain, sessionData);
291
+
292
+ const retrieved = await vault.retrieve(domain);
293
+ expect(retrieved).not.toBeNull();
294
+ expect(retrieved?.expiresAt).toBeGreaterThan(Date.now());
295
+
296
+ const decrypted = await vault.retrieveDecrypted(domain);
297
+ expect(decrypted).toEqual(sessionData);
298
+ });
299
+
300
+ test('expired sessions are not retrieved', async () => {
301
+ const domain = 'expired.com';
302
+ const sessionData: RawSessionData = {
303
+ cookies: 'test',
304
+ localStorage: 'test',
305
+ sessionStorage: 'test',
306
+ };
307
+
308
+ // Store with past expiration
309
+ await vault.store(domain, sessionData, Date.now() - 1000);
310
+
311
+ const retrieved = await vault.retrieve(domain);
312
+ expect(retrieved).toBeNull();
313
+ });
314
+
315
+ test('token revocation on bounty completion', async () => {
316
+ const domain = 'bounty.com';
317
+ const sessionData: RawSessionData = {
318
+ cookies: 'test',
319
+ localStorage: 'test',
320
+ sessionStorage: 'test',
321
+ };
322
+
323
+ await vault.store(domain, sessionData);
324
+
325
+ // Issue multiple tokens
326
+ const token1 = await vault.issueToken(domain, { allowedActions: ['read'] });
327
+ const token2 = await vault.issueToken(domain, { allowedActions: ['write'] });
328
+
329
+ // Both tokens should be valid
330
+ expect(vault.getToken(token1.id)).not.toBeNull();
331
+ expect(vault.getToken(token2.id)).not.toBeNull();
332
+
333
+ // Revoke all tokens for domain (bounty completion)
334
+ const revokedCount = vault.revokeAllTokensForDomain(domain);
335
+
336
+ expect(revokedCount).toBe(2);
337
+ expect(vault.getToken(token1.id)).toBeNull();
338
+ expect(vault.getToken(token2.id)).toBeNull();
339
+ });
340
+
341
+ test('access logs are recorded correctly', async () => {
342
+ const domain = 'logged.com';
343
+ const sessionData: RawSessionData = {
344
+ cookies: 'test',
345
+ localStorage: 'test',
346
+ sessionStorage: 'test',
347
+ };
348
+
349
+ await vault.store(domain, sessionData);
350
+ const token = await vault.issueToken(domain, { allowedActions: ['read'] });
351
+ await vault.useToken(token.id, 'read');
352
+ vault.revokeToken(token.id);
353
+
354
+ const logs = vault.getAccessLogs(domain);
355
+
356
+ // Should have: store, issue, use, revoke
357
+ expect(logs.length).toBeGreaterThanOrEqual(4);
358
+ expect(logs.some((l) => l.action === 'issued')).toBe(true);
359
+ expect(logs.some((l) => l.action === 'used')).toBe(true);
360
+ expect(logs.some((l) => l.action === 'revoked')).toBe(true);
361
+
362
+ // Logs should not contain actual credentials
363
+ for (const log of logs) {
364
+ expect(log.details).not.toContain(sessionData.cookies);
365
+ }
366
+ });
367
+ });