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.
- package/README.md +261 -0
- package/package.json +95 -0
- package/src/agents/README.md +139 -0
- package/src/agents/adapters/anthropic.adapter.ts +166 -0
- package/src/agents/adapters/dalle.adapter.ts +145 -0
- package/src/agents/adapters/gemini.adapter.ts +134 -0
- package/src/agents/adapters/imagen.adapter.ts +106 -0
- package/src/agents/adapters/nano-banana.adapter.ts +129 -0
- package/src/agents/adapters/openai.adapter.ts +165 -0
- package/src/agents/adapters/veo.adapter.ts +130 -0
- package/src/agents/agent.schema.property.test.ts +379 -0
- package/src/agents/agent.schema.test.ts +148 -0
- package/src/agents/agent.schema.ts +263 -0
- package/src/agents/index.ts +60 -0
- package/src/agents/registered-agent.schema.ts +356 -0
- package/src/agents/registry.ts +97 -0
- package/src/agents/tournament-configs.property.test.ts +266 -0
- package/src/cli/README.md +145 -0
- package/src/cli/commands/define.ts +79 -0
- package/src/cli/commands/list.ts +46 -0
- package/src/cli/commands/logs.ts +83 -0
- package/src/cli/commands/run.ts +416 -0
- package/src/cli/commands/verify.ts +110 -0
- package/src/cli/index.ts +81 -0
- package/src/config/README.md +128 -0
- package/src/config/env.ts +262 -0
- package/src/config/index.ts +19 -0
- package/src/eval/README.md +318 -0
- package/src/eval/ai-judge.test.ts +435 -0
- package/src/eval/ai-judge.ts +368 -0
- package/src/eval/code-validators.ts +414 -0
- package/src/eval/evaluateOutcome.property.test.ts +1174 -0
- package/src/eval/evaluateOutcome.ts +591 -0
- package/src/eval/immigration-validators.ts +122 -0
- package/src/eval/index.ts +90 -0
- package/src/eval/judge-cache.ts +402 -0
- package/src/eval/tournament-validators.property.test.ts +439 -0
- package/src/eval/validators.property.test.ts +1118 -0
- package/src/eval/validators.ts +1199 -0
- package/src/eval/weighted-scorer.ts +285 -0
- package/src/index.ts +17 -0
- package/src/league/README.md +188 -0
- package/src/league/health-check.ts +353 -0
- package/src/league/index.ts +93 -0
- package/src/league/killAgent.ts +151 -0
- package/src/league/league.test.ts +1151 -0
- package/src/league/runLeague.ts +843 -0
- package/src/league/scoreAgent.ts +175 -0
- package/src/modules/omnibridge/__tests__/.gitkeep +1 -0
- package/src/modules/omnibridge/__tests__/auth-tunnel.property.test.ts +524 -0
- package/src/modules/omnibridge/__tests__/deterministic-logger.property.test.ts +965 -0
- package/src/modules/omnibridge/__tests__/ghost-api.property.test.ts +461 -0
- package/src/modules/omnibridge/__tests__/omnibridge-integration.test.ts +542 -0
- package/src/modules/omnibridge/__tests__/parallel-executor.property.test.ts +671 -0
- package/src/modules/omnibridge/__tests__/semantic-normalizer.property.test.ts +521 -0
- package/src/modules/omnibridge/__tests__/semantic-normalizer.test.ts +254 -0
- package/src/modules/omnibridge/__tests__/session-vault.property.test.ts +367 -0
- package/src/modules/omnibridge/__tests__/shadow-session.property.test.ts +523 -0
- package/src/modules/omnibridge/__tests__/triangulation-engine.property.test.ts +292 -0
- package/src/modules/omnibridge/__tests__/verification-engine.property.test.ts +769 -0
- package/src/modules/omnibridge/api/.gitkeep +1 -0
- package/src/modules/omnibridge/api/ghost-api.ts +1087 -0
- package/src/modules/omnibridge/auth/.gitkeep +1 -0
- package/src/modules/omnibridge/auth/auth-tunnel.ts +843 -0
- package/src/modules/omnibridge/auth/session-vault.ts +577 -0
- package/src/modules/omnibridge/core/.gitkeep +1 -0
- package/src/modules/omnibridge/core/semantic-normalizer.ts +702 -0
- package/src/modules/omnibridge/core/triangulation-engine.ts +530 -0
- package/src/modules/omnibridge/core/types.ts +610 -0
- package/src/modules/omnibridge/execution/.gitkeep +1 -0
- package/src/modules/omnibridge/execution/deterministic-logger.ts +629 -0
- package/src/modules/omnibridge/execution/parallel-executor.ts +542 -0
- package/src/modules/omnibridge/execution/shadow-session.ts +794 -0
- package/src/modules/omnibridge/index.ts +212 -0
- package/src/modules/omnibridge/omnibridge.ts +510 -0
- package/src/modules/omnibridge/verification/.gitkeep +1 -0
- package/src/modules/omnibridge/verification/verification-engine.ts +783 -0
- package/src/outcomes/README.md +75 -0
- package/src/outcomes/acquire-pilot-customer.ts +297 -0
- package/src/outcomes/code-delivery-outcomes.ts +89 -0
- package/src/outcomes/code-outcomes.ts +256 -0
- package/src/outcomes/code_review_battle.test.ts +135 -0
- package/src/outcomes/code_review_battle.ts +135 -0
- package/src/outcomes/cold_email_battle.ts +97 -0
- package/src/outcomes/content_creation_battle.ts +160 -0
- package/src/outcomes/f1_stem_opt_compliance.ts +61 -0
- package/src/outcomes/index.ts +107 -0
- package/src/outcomes/lead_gen_battle.test.ts +113 -0
- package/src/outcomes/lead_gen_battle.ts +99 -0
- package/src/outcomes/outcome.schema.property.test.ts +229 -0
- package/src/outcomes/outcome.schema.ts +187 -0
- package/src/outcomes/qualified_sales_interest.ts +118 -0
- package/src/outcomes/swarm_planner.property.test.ts +370 -0
- package/src/outcomes/swarm_planner.ts +96 -0
- package/src/outcomes/web_extraction.ts +234 -0
- package/src/runtime/README.md +220 -0
- package/src/runtime/agentRunner.test.ts +341 -0
- package/src/runtime/agentRunner.ts +746 -0
- package/src/runtime/claudeAdapter.ts +232 -0
- package/src/runtime/costTracker.ts +123 -0
- package/src/runtime/index.ts +34 -0
- package/src/runtime/modelAdapter.property.test.ts +305 -0
- package/src/runtime/modelAdapter.ts +144 -0
- package/src/runtime/openaiAdapter.ts +235 -0
- package/src/utils/README.md +122 -0
- package/src/utils/command-runner.ts +134 -0
- package/src/utils/cost-guard.ts +379 -0
- package/src/utils/errors.test.ts +290 -0
- package/src/utils/errors.ts +442 -0
- package/src/utils/index.ts +37 -0
- package/src/utils/logger.test.ts +361 -0
- package/src/utils/logger.ts +419 -0
- package/src/utils/output-parsers.ts +216 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Vault - Zero-Knowledge Credential Storage
|
|
3
|
+
*
|
|
4
|
+
* Implements secure, encrypted storage for session states.
|
|
5
|
+
* Credentials are NEVER exposed to LLM context - only capability tokens.
|
|
6
|
+
*
|
|
7
|
+
* Requirements: 6.1, 6.2, 6.3, 6.4, 6.5, 6.6, 6.7
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { randomBytes, createCipheriv, createDecipheriv } from 'crypto';
|
|
11
|
+
import type {
|
|
12
|
+
EncryptedBlob,
|
|
13
|
+
SerializedSession,
|
|
14
|
+
ActionScope,
|
|
15
|
+
CapabilityToken,
|
|
16
|
+
OmniBridgeError,
|
|
17
|
+
} from '../core/types.js';
|
|
18
|
+
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Types
|
|
21
|
+
// =============================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Raw session data before encryption.
|
|
25
|
+
*/
|
|
26
|
+
export interface RawSessionData {
|
|
27
|
+
cookies: string;
|
|
28
|
+
localStorage: string;
|
|
29
|
+
sessionStorage: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Credential access log entry (redacted).
|
|
34
|
+
*/
|
|
35
|
+
export interface CredentialAccessLog {
|
|
36
|
+
timestamp: number;
|
|
37
|
+
tokenId: string;
|
|
38
|
+
domain: string;
|
|
39
|
+
action: 'issued' | 'used' | 'revoked' | 'violation';
|
|
40
|
+
/** Redacted details - never contains actual credentials */
|
|
41
|
+
details: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Session Vault configuration.
|
|
46
|
+
*/
|
|
47
|
+
export interface SessionVaultConfig {
|
|
48
|
+
/** Encryption key (32 bytes for AES-256) */
|
|
49
|
+
encryptionKey: Buffer;
|
|
50
|
+
/** Default token expiration in milliseconds (default: 1 hour) */
|
|
51
|
+
defaultTokenExpirationMs?: number;
|
|
52
|
+
/** Default max executions per token (default: 100) */
|
|
53
|
+
defaultMaxExecutions?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Result of a scope check.
|
|
58
|
+
*/
|
|
59
|
+
export interface ScopeCheckResult {
|
|
60
|
+
allowed: boolean;
|
|
61
|
+
error?: OmniBridgeError;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// =============================================================================
|
|
65
|
+
// Session Vault Implementation
|
|
66
|
+
// =============================================================================
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Session Vault - Zero-Knowledge Credential Storage
|
|
70
|
+
*
|
|
71
|
+
* Key security properties:
|
|
72
|
+
* - All stored data is AES-256-GCM encrypted
|
|
73
|
+
* - Credentials never exposed to LLM context
|
|
74
|
+
* - Capability tokens contain only action permissions, not credentials
|
|
75
|
+
* - All access is logged (with redacted details)
|
|
76
|
+
*/
|
|
77
|
+
export class SessionVault {
|
|
78
|
+
private readonly encryptionKey: Buffer;
|
|
79
|
+
private readonly defaultTokenExpirationMs: number;
|
|
80
|
+
private readonly defaultMaxExecutions: number;
|
|
81
|
+
|
|
82
|
+
/** Encrypted session storage by domain */
|
|
83
|
+
private sessions: Map<string, SerializedSession> = new Map();
|
|
84
|
+
|
|
85
|
+
/** Active capability tokens by ID */
|
|
86
|
+
private tokens: Map<string, CapabilityToken> = new Map();
|
|
87
|
+
|
|
88
|
+
/** Token to session mapping */
|
|
89
|
+
private tokenToSession: Map<string, string> = new Map();
|
|
90
|
+
|
|
91
|
+
/** Access logs (redacted) */
|
|
92
|
+
private accessLogs: CredentialAccessLog[] = [];
|
|
93
|
+
|
|
94
|
+
/** Revoked token IDs */
|
|
95
|
+
private revokedTokens: Set<string> = new Set();
|
|
96
|
+
|
|
97
|
+
constructor(config: SessionVaultConfig) {
|
|
98
|
+
if (config.encryptionKey.length !== 32) {
|
|
99
|
+
throw new Error('Encryption key must be 32 bytes for AES-256');
|
|
100
|
+
}
|
|
101
|
+
this.encryptionKey = config.encryptionKey;
|
|
102
|
+
this.defaultTokenExpirationMs = config.defaultTokenExpirationMs ?? 3600000; // 1 hour
|
|
103
|
+
this.defaultMaxExecutions = config.defaultMaxExecutions ?? 100;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ===========================================================================
|
|
107
|
+
// Encryption Methods
|
|
108
|
+
// ===========================================================================
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Encrypt data using AES-256-GCM.
|
|
112
|
+
* Returns an EncryptedBlob with the encrypted data and IV.
|
|
113
|
+
*/
|
|
114
|
+
encrypt(data: string): EncryptedBlob {
|
|
115
|
+
const iv = randomBytes(16);
|
|
116
|
+
const cipher = createCipheriv('aes-256-gcm', this.encryptionKey, iv);
|
|
117
|
+
|
|
118
|
+
let encrypted = cipher.update(data, 'utf8', 'base64');
|
|
119
|
+
encrypted += cipher.final('base64');
|
|
120
|
+
|
|
121
|
+
// Append auth tag to encrypted data
|
|
122
|
+
const authTag = cipher.getAuthTag();
|
|
123
|
+
const encryptedWithTag = encrypted + ':' + authTag.toString('base64');
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
data: encryptedWithTag,
|
|
127
|
+
iv: iv.toString('base64'),
|
|
128
|
+
algorithm: 'AES-256-GCM',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Decrypt an EncryptedBlob back to plaintext.
|
|
134
|
+
* Validates the encrypted data format before attempting decryption.
|
|
135
|
+
*/
|
|
136
|
+
decrypt(blob: EncryptedBlob): string {
|
|
137
|
+
// Validate encrypted data format
|
|
138
|
+
if (!blob.data || !blob.data.includes(':')) {
|
|
139
|
+
throw new Error('Invalid encrypted data format: missing auth tag separator');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const iv = Buffer.from(blob.iv, 'base64');
|
|
143
|
+
const [encryptedData, authTagBase64] = blob.data.split(':');
|
|
144
|
+
|
|
145
|
+
if (!encryptedData || !authTagBase64) {
|
|
146
|
+
throw new Error('Invalid encrypted data format: malformed data or auth tag');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const authTag = Buffer.from(authTagBase64, 'base64');
|
|
150
|
+
|
|
151
|
+
const decipher = createDecipheriv('aes-256-gcm', this.encryptionKey, iv);
|
|
152
|
+
decipher.setAuthTag(authTag);
|
|
153
|
+
|
|
154
|
+
let decrypted = decipher.update(encryptedData, 'base64', 'utf8');
|
|
155
|
+
decrypted += decipher.final('utf8');
|
|
156
|
+
|
|
157
|
+
return decrypted;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ===========================================================================
|
|
161
|
+
// Session Storage Methods
|
|
162
|
+
// ===========================================================================
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Store a session with encrypted data.
|
|
166
|
+
* Raw credentials are encrypted immediately and never stored in plaintext.
|
|
167
|
+
*/
|
|
168
|
+
async store(domain: string, rawData: RawSessionData, expiresAt?: number): Promise<void> {
|
|
169
|
+
const session: SerializedSession = {
|
|
170
|
+
cookies: this.encrypt(rawData.cookies),
|
|
171
|
+
localStorage: this.encrypt(rawData.localStorage),
|
|
172
|
+
sessionStorage: this.encrypt(rawData.sessionStorage),
|
|
173
|
+
expiresAt: expiresAt ?? Date.now() + 86400000, // Default: 24 hours
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
this.sessions.set(domain, session);
|
|
177
|
+
|
|
178
|
+
this.logAccess({
|
|
179
|
+
timestamp: Date.now(),
|
|
180
|
+
tokenId: 'system',
|
|
181
|
+
domain,
|
|
182
|
+
action: 'issued',
|
|
183
|
+
details: `Session stored for domain (expires: ${new Date(session.expiresAt).toISOString()})`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Retrieve an encrypted session for a domain.
|
|
189
|
+
* Returns null if no session exists or if expired.
|
|
190
|
+
*/
|
|
191
|
+
async retrieve(domain: string): Promise<SerializedSession | null> {
|
|
192
|
+
const session = this.sessions.get(domain);
|
|
193
|
+
|
|
194
|
+
if (!session) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Check expiration
|
|
199
|
+
if (session.expiresAt < Date.now()) {
|
|
200
|
+
this.sessions.delete(domain);
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return session;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Decrypt and retrieve raw session data.
|
|
209
|
+
* This should only be used internally by Shadow Sessions.
|
|
210
|
+
*/
|
|
211
|
+
async retrieveDecrypted(domain: string): Promise<RawSessionData | null> {
|
|
212
|
+
const session = await this.retrieve(domain);
|
|
213
|
+
|
|
214
|
+
if (!session) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
cookies: this.decrypt(session.cookies),
|
|
220
|
+
localStorage: this.decrypt(session.localStorage),
|
|
221
|
+
sessionStorage: this.decrypt(session.sessionStorage),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ===========================================================================
|
|
226
|
+
// Capability Token Methods
|
|
227
|
+
// ===========================================================================
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Generate a unique token ID.
|
|
231
|
+
*/
|
|
232
|
+
private generateTokenId(): string {
|
|
233
|
+
return `cap_${randomBytes(16).toString('hex')}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Issue a Capability Token scoped to specific actions.
|
|
238
|
+
* The token contains ONLY capabilities, never raw credentials.
|
|
239
|
+
*
|
|
240
|
+
* Requirements: 6.2, 6.3
|
|
241
|
+
*/
|
|
242
|
+
async issueToken(domain: string, scope: Partial<ActionScope>): Promise<CapabilityToken> {
|
|
243
|
+
// Verify session exists
|
|
244
|
+
const session = await this.retrieve(domain);
|
|
245
|
+
if (!session) {
|
|
246
|
+
throw new Error(`No session found for domain: ${domain}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const tokenId = this.generateTokenId();
|
|
250
|
+
const now = Date.now();
|
|
251
|
+
|
|
252
|
+
const fullScope: ActionScope = {
|
|
253
|
+
allowedActions: scope.allowedActions ?? [],
|
|
254
|
+
blockedActions: scope.blockedActions ?? [],
|
|
255
|
+
maxExecutions: scope.maxExecutions ?? this.defaultMaxExecutions,
|
|
256
|
+
expiresAt: scope.expiresAt ?? now + this.defaultTokenExpirationMs,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const token: CapabilityToken = {
|
|
260
|
+
id: tokenId,
|
|
261
|
+
domain,
|
|
262
|
+
scope: fullScope,
|
|
263
|
+
issuedAt: now,
|
|
264
|
+
executionCount: 0,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
this.tokens.set(tokenId, token);
|
|
268
|
+
this.tokenToSession.set(tokenId, domain);
|
|
269
|
+
|
|
270
|
+
this.logAccess({
|
|
271
|
+
timestamp: now,
|
|
272
|
+
tokenId,
|
|
273
|
+
domain,
|
|
274
|
+
action: 'issued',
|
|
275
|
+
details: `Token issued with ${fullScope.allowedActions.length} allowed actions, ${fullScope.blockedActions.length} blocked actions`,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
return token;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Get a token by ID.
|
|
283
|
+
*/
|
|
284
|
+
getToken(tokenId: string): CapabilityToken | null {
|
|
285
|
+
if (this.revokedTokens.has(tokenId)) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
return this.tokens.get(tokenId) ?? null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Check if an action is allowed by a token's scope.
|
|
293
|
+
*
|
|
294
|
+
* Requirements: 6.2, 6.3
|
|
295
|
+
*/
|
|
296
|
+
checkScope(tokenId: string, action: string): ScopeCheckResult {
|
|
297
|
+
const token = this.getToken(tokenId);
|
|
298
|
+
|
|
299
|
+
if (!token) {
|
|
300
|
+
return {
|
|
301
|
+
allowed: false,
|
|
302
|
+
error: {
|
|
303
|
+
type: 'scope_violation',
|
|
304
|
+
attemptedAction: action,
|
|
305
|
+
allowedActions: [],
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Check if token is expired
|
|
311
|
+
if (token.scope.expiresAt < Date.now()) {
|
|
312
|
+
this.revokeToken(tokenId);
|
|
313
|
+
return {
|
|
314
|
+
allowed: false,
|
|
315
|
+
error: {
|
|
316
|
+
type: 'scope_violation',
|
|
317
|
+
attemptedAction: action,
|
|
318
|
+
allowedActions: token.scope.allowedActions,
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Check if max executions exceeded
|
|
324
|
+
if (token.executionCount >= token.scope.maxExecutions) {
|
|
325
|
+
return {
|
|
326
|
+
allowed: false,
|
|
327
|
+
error: {
|
|
328
|
+
type: 'scope_violation',
|
|
329
|
+
attemptedAction: action,
|
|
330
|
+
allowedActions: token.scope.allowedActions,
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Check if action is explicitly blocked
|
|
336
|
+
if (token.scope.blockedActions.includes(action)) {
|
|
337
|
+
return {
|
|
338
|
+
allowed: false,
|
|
339
|
+
error: {
|
|
340
|
+
type: 'scope_violation',
|
|
341
|
+
attemptedAction: action,
|
|
342
|
+
allowedActions: token.scope.allowedActions,
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Check if action is in allowed list (if allowedActions is non-empty)
|
|
348
|
+
if (token.scope.allowedActions.length > 0 && !token.scope.allowedActions.includes(action)) {
|
|
349
|
+
return {
|
|
350
|
+
allowed: false,
|
|
351
|
+
error: {
|
|
352
|
+
type: 'scope_violation',
|
|
353
|
+
attemptedAction: action,
|
|
354
|
+
allowedActions: token.scope.allowedActions,
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return { allowed: true };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Use a token for an action.
|
|
364
|
+
* Increments execution count and logs access.
|
|
365
|
+
*/
|
|
366
|
+
async useToken(tokenId: string, action: string): Promise<ScopeCheckResult> {
|
|
367
|
+
const scopeCheck = this.checkScope(tokenId, action);
|
|
368
|
+
|
|
369
|
+
if (!scopeCheck.allowed) {
|
|
370
|
+
// Log the violation
|
|
371
|
+
const token = this.tokens.get(tokenId);
|
|
372
|
+
this.logAccess({
|
|
373
|
+
timestamp: Date.now(),
|
|
374
|
+
tokenId,
|
|
375
|
+
domain: token?.domain ?? 'unknown',
|
|
376
|
+
action: 'violation',
|
|
377
|
+
details: `Attempted blocked action: ${action}`,
|
|
378
|
+
});
|
|
379
|
+
return scopeCheck;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Increment execution count
|
|
383
|
+
const token = this.tokens.get(tokenId)!;
|
|
384
|
+
token.executionCount++;
|
|
385
|
+
|
|
386
|
+
this.logAccess({
|
|
387
|
+
timestamp: Date.now(),
|
|
388
|
+
tokenId,
|
|
389
|
+
domain: token.domain,
|
|
390
|
+
action: 'used',
|
|
391
|
+
details: `Action executed: ${action} (${token.executionCount}/${token.scope.maxExecutions})`,
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
return { allowed: true };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Revoke a Capability Token immediately.
|
|
399
|
+
*
|
|
400
|
+
* Requirements: 6.5
|
|
401
|
+
*/
|
|
402
|
+
revokeToken(tokenId: string): void {
|
|
403
|
+
const token = this.tokens.get(tokenId);
|
|
404
|
+
|
|
405
|
+
if (token) {
|
|
406
|
+
this.revokedTokens.add(tokenId);
|
|
407
|
+
this.tokens.delete(tokenId);
|
|
408
|
+
this.tokenToSession.delete(tokenId);
|
|
409
|
+
|
|
410
|
+
this.logAccess({
|
|
411
|
+
timestamp: Date.now(),
|
|
412
|
+
tokenId,
|
|
413
|
+
domain: token.domain,
|
|
414
|
+
action: 'revoked',
|
|
415
|
+
details: 'Token revoked',
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Revoke all tokens for a domain.
|
|
422
|
+
* Used when a bounty completes.
|
|
423
|
+
*
|
|
424
|
+
* Requirements: 6.5
|
|
425
|
+
*/
|
|
426
|
+
revokeAllTokensForDomain(domain: string): number {
|
|
427
|
+
let revokedCount = 0;
|
|
428
|
+
|
|
429
|
+
for (const [tokenId, sessionDomain] of this.tokenToSession.entries()) {
|
|
430
|
+
if (sessionDomain === domain) {
|
|
431
|
+
this.revokeToken(tokenId);
|
|
432
|
+
revokedCount++;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return revokedCount;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Check if a token is revoked.
|
|
441
|
+
*/
|
|
442
|
+
isTokenRevoked(tokenId: string): boolean {
|
|
443
|
+
return this.revokedTokens.has(tokenId);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ===========================================================================
|
|
447
|
+
// Scope Violation Handling
|
|
448
|
+
// ===========================================================================
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Handle a scope violation.
|
|
452
|
+
* Terminates the session and revokes the token immediately.
|
|
453
|
+
*
|
|
454
|
+
* Requirements: 6.7
|
|
455
|
+
*/
|
|
456
|
+
async handleScopeViolation(
|
|
457
|
+
tokenId: string,
|
|
458
|
+
attemptedAction: string
|
|
459
|
+
): Promise<{ terminated: boolean; error: OmniBridgeError }> {
|
|
460
|
+
const token = this.tokens.get(tokenId);
|
|
461
|
+
const domain = token?.domain ?? 'unknown';
|
|
462
|
+
|
|
463
|
+
// Revoke the token immediately
|
|
464
|
+
this.revokeToken(tokenId);
|
|
465
|
+
|
|
466
|
+
// Log the violation
|
|
467
|
+
this.logAccess({
|
|
468
|
+
timestamp: Date.now(),
|
|
469
|
+
tokenId,
|
|
470
|
+
domain,
|
|
471
|
+
action: 'violation',
|
|
472
|
+
details: `SECURITY: Scope violation - attempted action "${attemptedAction}" - token revoked and session terminated`,
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
terminated: true,
|
|
477
|
+
error: {
|
|
478
|
+
type: 'scope_violation',
|
|
479
|
+
attemptedAction,
|
|
480
|
+
allowedActions: token?.scope.allowedActions ?? [],
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ===========================================================================
|
|
486
|
+
// Logging Methods
|
|
487
|
+
// ===========================================================================
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Log a credential access event (redacted).
|
|
491
|
+
*
|
|
492
|
+
* Requirements: 6.6
|
|
493
|
+
*/
|
|
494
|
+
private logAccess(entry: CredentialAccessLog): void {
|
|
495
|
+
this.accessLogs.push(entry);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Get access logs (redacted - no sensitive data).
|
|
500
|
+
*/
|
|
501
|
+
getAccessLogs(domain?: string): CredentialAccessLog[] {
|
|
502
|
+
if (domain) {
|
|
503
|
+
return this.accessLogs.filter((log) => log.domain === domain);
|
|
504
|
+
}
|
|
505
|
+
return [...this.accessLogs];
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ===========================================================================
|
|
509
|
+
// Utility Methods
|
|
510
|
+
// ===========================================================================
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Check if a token contains raw credentials.
|
|
514
|
+
* Used for property testing - should always return false.
|
|
515
|
+
*
|
|
516
|
+
* Requirements: 6.1
|
|
517
|
+
*/
|
|
518
|
+
tokenContainsCredentials(token: CapabilityToken): boolean {
|
|
519
|
+
// Check if any field contains credential-like patterns
|
|
520
|
+
const tokenStr = JSON.stringify(token);
|
|
521
|
+
|
|
522
|
+
// Patterns that would indicate raw credentials
|
|
523
|
+
const credentialPatterns = [
|
|
524
|
+
/password["\s]*:/i,
|
|
525
|
+
/api[_-]?key["\s]*:/i,
|
|
526
|
+
/secret["\s]*:/i,
|
|
527
|
+
/token["\s]*:["\s]*[a-zA-Z0-9]{20,}/i, // Long token values (not our token ID format)
|
|
528
|
+
/cookie["\s]*:/i,
|
|
529
|
+
/session["\s]*:/i,
|
|
530
|
+
/Bearer\s+[a-zA-Z0-9]/i,
|
|
531
|
+
/Basic\s+[a-zA-Z0-9]/i,
|
|
532
|
+
];
|
|
533
|
+
|
|
534
|
+
for (const pattern of credentialPatterns) {
|
|
535
|
+
if (pattern.test(tokenStr)) {
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Clear all data (for testing).
|
|
545
|
+
*/
|
|
546
|
+
clear(): void {
|
|
547
|
+
this.sessions.clear();
|
|
548
|
+
this.tokens.clear();
|
|
549
|
+
this.tokenToSession.clear();
|
|
550
|
+
this.revokedTokens.clear();
|
|
551
|
+
this.accessLogs = [];
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// =============================================================================
|
|
556
|
+
// Factory Function
|
|
557
|
+
// =============================================================================
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Create a new Session Vault instance.
|
|
561
|
+
*/
|
|
562
|
+
export function createSessionVault(config?: Partial<SessionVaultConfig>): SessionVault {
|
|
563
|
+
const encryptionKey = config?.encryptionKey ?? randomBytes(32);
|
|
564
|
+
|
|
565
|
+
return new SessionVault({
|
|
566
|
+
encryptionKey,
|
|
567
|
+
defaultTokenExpirationMs: config?.defaultTokenExpirationMs,
|
|
568
|
+
defaultMaxExecutions: config?.defaultMaxExecutions,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Generate a secure encryption key for the vault.
|
|
574
|
+
*/
|
|
575
|
+
export function generateEncryptionKey(): Buffer {
|
|
576
|
+
return randomBytes(32);
|
|
577
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Core module - Semantic Normalizer, Triangulation Engine, Intent Types
|