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,794 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shadow Session Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Manages virtualized browser environments with persistent authentication.
|
|
5
|
+
* Each Shadow Session is isolated, fingerprinted, and maintains its own state.
|
|
6
|
+
*
|
|
7
|
+
* Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 5.5
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { randomBytes } from 'crypto';
|
|
11
|
+
import type {
|
|
12
|
+
ShadowSession,
|
|
13
|
+
DeviceFingerprint,
|
|
14
|
+
SessionConfig,
|
|
15
|
+
} from '../core/types.js';
|
|
16
|
+
import type { SessionVault, RawSessionData } from '../auth/session-vault.js';
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// Types
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Browser context state for isolation.
|
|
24
|
+
*/
|
|
25
|
+
export interface BrowserContextState {
|
|
26
|
+
/** Unique context ID */
|
|
27
|
+
contextId: string;
|
|
28
|
+
/** Cookies stored in this context */
|
|
29
|
+
cookies: CookieData[];
|
|
30
|
+
/** localStorage data */
|
|
31
|
+
localStorage: Record<string, string>;
|
|
32
|
+
/** sessionStorage data */
|
|
33
|
+
sessionStorage: Record<string, string>;
|
|
34
|
+
/** Headers to use for requests */
|
|
35
|
+
headers: Record<string, string>;
|
|
36
|
+
/** Last activity timestamp */
|
|
37
|
+
lastActivity: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Cookie data structure.
|
|
42
|
+
*/
|
|
43
|
+
export interface CookieData {
|
|
44
|
+
name: string;
|
|
45
|
+
value: string;
|
|
46
|
+
domain: string;
|
|
47
|
+
path: string;
|
|
48
|
+
expires?: number;
|
|
49
|
+
httpOnly?: boolean;
|
|
50
|
+
secure?: boolean;
|
|
51
|
+
sameSite?: 'Strict' | 'Lax' | 'None';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Shadow Session with internal state.
|
|
56
|
+
*/
|
|
57
|
+
export interface ShadowSessionInternal extends ShadowSession {
|
|
58
|
+
/** Browser context state */
|
|
59
|
+
contextState: BrowserContextState;
|
|
60
|
+
/** Agent ID this session belongs to */
|
|
61
|
+
agentId?: string;
|
|
62
|
+
/** Whether session is currently locked for use */
|
|
63
|
+
locked: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Session creation result.
|
|
68
|
+
*/
|
|
69
|
+
export interface SessionCreationResult {
|
|
70
|
+
success: boolean;
|
|
71
|
+
session?: ShadowSession;
|
|
72
|
+
error?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Session resumption result.
|
|
77
|
+
*/
|
|
78
|
+
export interface SessionResumptionResult {
|
|
79
|
+
success: boolean;
|
|
80
|
+
session?: ShadowSession;
|
|
81
|
+
error?: string;
|
|
82
|
+
reAuthRequired?: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Heartbeat result.
|
|
87
|
+
*/
|
|
88
|
+
export interface HeartbeatResult {
|
|
89
|
+
success: boolean;
|
|
90
|
+
sessionActive: boolean;
|
|
91
|
+
nextHeartbeatMs: number;
|
|
92
|
+
error?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// =============================================================================
|
|
96
|
+
// Fingerprint Generation
|
|
97
|
+
// =============================================================================
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Common user agents for realistic fingerprinting.
|
|
101
|
+
*/
|
|
102
|
+
const USER_AGENTS = [
|
|
103
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
104
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
105
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
|
|
106
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15',
|
|
107
|
+
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Common screen resolutions.
|
|
112
|
+
*/
|
|
113
|
+
const RESOLUTIONS = [
|
|
114
|
+
{ width: 1920, height: 1080 },
|
|
115
|
+
{ width: 1366, height: 768 },
|
|
116
|
+
{ width: 1536, height: 864 },
|
|
117
|
+
{ width: 1440, height: 900 },
|
|
118
|
+
{ width: 2560, height: 1440 },
|
|
119
|
+
{ width: 1280, height: 720 },
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Common fonts for fingerprinting.
|
|
124
|
+
*/
|
|
125
|
+
const FONT_SETS = [
|
|
126
|
+
['Arial', 'Helvetica', 'Times New Roman', 'Georgia', 'Verdana', 'Courier New'],
|
|
127
|
+
['Arial', 'Helvetica Neue', 'Segoe UI', 'Roboto', 'Open Sans', 'Lato'],
|
|
128
|
+
['San Francisco', 'Helvetica', 'Arial', 'Lucida Grande', 'Geneva'],
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Common timezones.
|
|
133
|
+
*/
|
|
134
|
+
const TIMEZONES = [
|
|
135
|
+
'America/New_York',
|
|
136
|
+
'America/Los_Angeles',
|
|
137
|
+
'America/Chicago',
|
|
138
|
+
'Europe/London',
|
|
139
|
+
'Europe/Paris',
|
|
140
|
+
'Asia/Tokyo',
|
|
141
|
+
'Asia/Shanghai',
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Common languages.
|
|
146
|
+
*/
|
|
147
|
+
const LANGUAGES = ['en-US', 'en-GB', 'en', 'es-ES', 'fr-FR', 'de-DE', 'ja-JP', 'zh-CN'];
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Generate a unique device fingerprint.
|
|
151
|
+
* Creates realistic browser fingerprints to avoid bot detection.
|
|
152
|
+
*
|
|
153
|
+
* Requirements: 4.1, 4.2
|
|
154
|
+
*/
|
|
155
|
+
export function generateFingerprint(seed?: string): DeviceFingerprint {
|
|
156
|
+
// Use seed for deterministic generation if provided
|
|
157
|
+
const random = seed
|
|
158
|
+
? createSeededRandom(seed)
|
|
159
|
+
: () => Math.random();
|
|
160
|
+
|
|
161
|
+
const resolution = RESOLUTIONS[Math.floor(random() * RESOLUTIONS.length)];
|
|
162
|
+
const userAgent = USER_AGENTS[Math.floor(random() * USER_AGENTS.length)];
|
|
163
|
+
const fonts = FONT_SETS[Math.floor(random() * FONT_SETS.length)];
|
|
164
|
+
const timezone = TIMEZONES[Math.floor(random() * TIMEZONES.length)];
|
|
165
|
+
const language = LANGUAGES[Math.floor(random() * LANGUAGES.length)];
|
|
166
|
+
|
|
167
|
+
// Generate a unique GPU signature
|
|
168
|
+
const gpuVendors = ['NVIDIA', 'AMD', 'Intel'];
|
|
169
|
+
const gpuModels = ['GeForce RTX 3080', 'Radeon RX 6800', 'UHD Graphics 630', 'GeForce GTX 1660'];
|
|
170
|
+
const gpuVendor = gpuVendors[Math.floor(random() * gpuVendors.length)];
|
|
171
|
+
const gpuModel = gpuModels[Math.floor(random() * gpuModels.length)];
|
|
172
|
+
const gpuSignature = `${gpuVendor} ${gpuModel}`;
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
resolution,
|
|
176
|
+
userAgent,
|
|
177
|
+
fonts,
|
|
178
|
+
gpuSignature,
|
|
179
|
+
timezone,
|
|
180
|
+
language,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Create a seeded random number generator for deterministic fingerprints.
|
|
186
|
+
*/
|
|
187
|
+
function createSeededRandom(seed: string): () => number {
|
|
188
|
+
let hash = 0;
|
|
189
|
+
for (let i = 0; i < seed.length; i++) {
|
|
190
|
+
const char = seed.charCodeAt(i);
|
|
191
|
+
hash = ((hash << 5) - hash) + char;
|
|
192
|
+
hash = hash & hash;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return () => {
|
|
196
|
+
hash = Math.sin(hash) * 10000;
|
|
197
|
+
return hash - Math.floor(hash);
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// =============================================================================
|
|
202
|
+
// Shadow Session Orchestrator
|
|
203
|
+
// =============================================================================
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Configuration for the Shadow Session Orchestrator.
|
|
207
|
+
*/
|
|
208
|
+
export interface ShadowSessionOrchestratorConfig {
|
|
209
|
+
/** Session Vault for credential storage */
|
|
210
|
+
vault?: SessionVault;
|
|
211
|
+
/** Heartbeat interval in milliseconds (default: 30000) */
|
|
212
|
+
heartbeatIntervalMs?: number;
|
|
213
|
+
/** Session timeout in milliseconds (default: 3600000 = 1 hour) */
|
|
214
|
+
sessionTimeoutMs?: number;
|
|
215
|
+
/** Maximum concurrent sessions (default: 100) */
|
|
216
|
+
maxConcurrentSessions?: number;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Shadow Session Orchestrator
|
|
221
|
+
*
|
|
222
|
+
* Manages virtualized browser environments with:
|
|
223
|
+
* - Unique device fingerprints per session
|
|
224
|
+
* - Per-agent isolation (no cross-contamination)
|
|
225
|
+
* - Persistent authentication via Session Vault
|
|
226
|
+
* - Automatic heartbeat and session maintenance
|
|
227
|
+
*
|
|
228
|
+
* Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 5.5
|
|
229
|
+
*/
|
|
230
|
+
export class ShadowSessionOrchestrator {
|
|
231
|
+
private readonly vault?: SessionVault;
|
|
232
|
+
private readonly heartbeatIntervalMs: number;
|
|
233
|
+
private readonly sessionTimeoutMs: number;
|
|
234
|
+
private readonly maxConcurrentSessions: number;
|
|
235
|
+
|
|
236
|
+
/** Active sessions by ID */
|
|
237
|
+
private sessions: Map<string, ShadowSessionInternal> = new Map();
|
|
238
|
+
|
|
239
|
+
/** Sessions by agent ID for isolation tracking */
|
|
240
|
+
private sessionsByAgent: Map<string, Set<string>> = new Map();
|
|
241
|
+
|
|
242
|
+
/** Heartbeat timers by session ID */
|
|
243
|
+
private heartbeatTimers: Map<string, NodeJS.Timeout> = new Map();
|
|
244
|
+
|
|
245
|
+
constructor(config: ShadowSessionOrchestratorConfig = {}) {
|
|
246
|
+
this.vault = config.vault;
|
|
247
|
+
this.heartbeatIntervalMs = config.heartbeatIntervalMs ?? 30000;
|
|
248
|
+
this.sessionTimeoutMs = config.sessionTimeoutMs ?? 3600000;
|
|
249
|
+
this.maxConcurrentSessions = config.maxConcurrentSessions ?? 100;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ===========================================================================
|
|
253
|
+
// Session Creation
|
|
254
|
+
// ===========================================================================
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Generate a unique session ID.
|
|
258
|
+
*/
|
|
259
|
+
private generateSessionId(): string {
|
|
260
|
+
return `shadow_${randomBytes(16).toString('hex')}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Generate a unique context ID for browser isolation.
|
|
265
|
+
*/
|
|
266
|
+
private generateContextId(): string {
|
|
267
|
+
return `ctx_${randomBytes(12).toString('hex')}`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Create a new Shadow Session with unique fingerprint.
|
|
272
|
+
*
|
|
273
|
+
* Requirements: 4.1, 4.2
|
|
274
|
+
*/
|
|
275
|
+
async create(config: SessionConfig, agentId?: string): Promise<SessionCreationResult> {
|
|
276
|
+
// Check concurrent session limit
|
|
277
|
+
if (this.sessions.size >= this.maxConcurrentSessions) {
|
|
278
|
+
return {
|
|
279
|
+
success: false,
|
|
280
|
+
error: `Maximum concurrent sessions (${this.maxConcurrentSessions}) reached`,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const sessionId = this.generateSessionId();
|
|
285
|
+
const contextId = this.generateContextId();
|
|
286
|
+
const now = Date.now();
|
|
287
|
+
|
|
288
|
+
// Use provided fingerprint or generate a new one
|
|
289
|
+
const fingerprint = config.fingerprint ?? generateFingerprint(sessionId);
|
|
290
|
+
|
|
291
|
+
// Create isolated browser context state
|
|
292
|
+
const contextState: BrowserContextState = {
|
|
293
|
+
contextId,
|
|
294
|
+
cookies: [],
|
|
295
|
+
localStorage: {},
|
|
296
|
+
sessionStorage: {},
|
|
297
|
+
headers: this.generateHeaders(fingerprint),
|
|
298
|
+
lastActivity: now,
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// Create the session
|
|
302
|
+
const session: ShadowSessionInternal = {
|
|
303
|
+
id: sessionId,
|
|
304
|
+
domain: config.targetDomain,
|
|
305
|
+
status: 'active',
|
|
306
|
+
createdAt: now,
|
|
307
|
+
lastHeartbeat: now,
|
|
308
|
+
fingerprint,
|
|
309
|
+
contextState,
|
|
310
|
+
agentId,
|
|
311
|
+
locked: false,
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// Store the session
|
|
315
|
+
this.sessions.set(sessionId, session);
|
|
316
|
+
|
|
317
|
+
// Track by agent ID for isolation
|
|
318
|
+
if (agentId) {
|
|
319
|
+
if (!this.sessionsByAgent.has(agentId)) {
|
|
320
|
+
this.sessionsByAgent.set(agentId, new Set());
|
|
321
|
+
}
|
|
322
|
+
this.sessionsByAgent.get(agentId)!.add(sessionId);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Start heartbeat timer
|
|
326
|
+
this.startHeartbeat(sessionId);
|
|
327
|
+
|
|
328
|
+
// Return public session (without internal state)
|
|
329
|
+
return {
|
|
330
|
+
success: true,
|
|
331
|
+
session: this.toPublicSession(session),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Generate realistic HTTP headers based on fingerprint.
|
|
337
|
+
*/
|
|
338
|
+
private generateHeaders(fingerprint: DeviceFingerprint): Record<string, string> {
|
|
339
|
+
return {
|
|
340
|
+
'User-Agent': fingerprint.userAgent,
|
|
341
|
+
'Accept-Language': `${fingerprint.language},en;q=0.9`,
|
|
342
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
|
343
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
344
|
+
'Connection': 'keep-alive',
|
|
345
|
+
'Upgrade-Insecure-Requests': '1',
|
|
346
|
+
'Sec-Fetch-Dest': 'document',
|
|
347
|
+
'Sec-Fetch-Mode': 'navigate',
|
|
348
|
+
'Sec-Fetch-Site': 'none',
|
|
349
|
+
'Sec-Fetch-User': '?1',
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Convert internal session to public session (hide internal state).
|
|
355
|
+
*/
|
|
356
|
+
private toPublicSession(internal: ShadowSessionInternal): ShadowSession {
|
|
357
|
+
return {
|
|
358
|
+
id: internal.id,
|
|
359
|
+
domain: internal.domain,
|
|
360
|
+
status: internal.status,
|
|
361
|
+
createdAt: internal.createdAt,
|
|
362
|
+
lastHeartbeat: internal.lastHeartbeat,
|
|
363
|
+
fingerprint: internal.fingerprint,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ===========================================================================
|
|
368
|
+
// Session Heartbeat and Persistence
|
|
369
|
+
// ===========================================================================
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Start heartbeat timer for a session.
|
|
373
|
+
*
|
|
374
|
+
* Requirements: 4.3
|
|
375
|
+
*/
|
|
376
|
+
private startHeartbeat(sessionId: string): void {
|
|
377
|
+
// Clear existing timer if any
|
|
378
|
+
this.stopHeartbeat(sessionId);
|
|
379
|
+
|
|
380
|
+
const timer = setInterval(() => {
|
|
381
|
+
this.performHeartbeat(sessionId);
|
|
382
|
+
}, this.heartbeatIntervalMs);
|
|
383
|
+
|
|
384
|
+
this.heartbeatTimers.set(sessionId, timer);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Stop heartbeat timer for a session.
|
|
389
|
+
*/
|
|
390
|
+
private stopHeartbeat(sessionId: string): void {
|
|
391
|
+
const timer = this.heartbeatTimers.get(sessionId);
|
|
392
|
+
if (timer) {
|
|
393
|
+
clearInterval(timer);
|
|
394
|
+
this.heartbeatTimers.delete(sessionId);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Perform heartbeat for a session.
|
|
400
|
+
* Refreshes cookies and rotates headers to maintain session.
|
|
401
|
+
*
|
|
402
|
+
* Requirements: 4.3
|
|
403
|
+
*/
|
|
404
|
+
private async performHeartbeat(sessionId: string): Promise<void> {
|
|
405
|
+
const session = this.sessions.get(sessionId);
|
|
406
|
+
if (!session) {
|
|
407
|
+
this.stopHeartbeat(sessionId);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const now = Date.now();
|
|
412
|
+
|
|
413
|
+
// Check if session has timed out
|
|
414
|
+
if (now - session.lastHeartbeat > this.sessionTimeoutMs) {
|
|
415
|
+
session.status = 'expired';
|
|
416
|
+
this.stopHeartbeat(sessionId);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Update heartbeat timestamp
|
|
421
|
+
session.lastHeartbeat = now;
|
|
422
|
+
session.contextState.lastActivity = now;
|
|
423
|
+
|
|
424
|
+
// Rotate some headers to appear more human-like
|
|
425
|
+
this.rotateHeaders(session);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Rotate headers to maintain realistic behavior.
|
|
430
|
+
*/
|
|
431
|
+
private rotateHeaders(session: ShadowSessionInternal): void {
|
|
432
|
+
// Slightly vary Accept-Language quality values
|
|
433
|
+
const lang = session.fingerprint.language;
|
|
434
|
+
const quality = (0.8 + Math.random() * 0.2).toFixed(1);
|
|
435
|
+
session.contextState.headers['Accept-Language'] = `${lang},en;q=${quality}`;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Manually trigger heartbeat for a session.
|
|
440
|
+
*
|
|
441
|
+
* Requirements: 4.3
|
|
442
|
+
*/
|
|
443
|
+
async heartbeat(sessionId: string): Promise<HeartbeatResult> {
|
|
444
|
+
const session = this.sessions.get(sessionId);
|
|
445
|
+
|
|
446
|
+
if (!session) {
|
|
447
|
+
return {
|
|
448
|
+
success: false,
|
|
449
|
+
sessionActive: false,
|
|
450
|
+
nextHeartbeatMs: 0,
|
|
451
|
+
error: 'Session not found',
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (session.status === 'expired') {
|
|
456
|
+
return {
|
|
457
|
+
success: false,
|
|
458
|
+
sessionActive: false,
|
|
459
|
+
nextHeartbeatMs: 0,
|
|
460
|
+
error: 'Session expired',
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
await this.performHeartbeat(sessionId);
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
success: true,
|
|
468
|
+
sessionActive: session.status === 'active',
|
|
469
|
+
nextHeartbeatMs: this.heartbeatIntervalMs,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ===========================================================================
|
|
474
|
+
// Session Isolation
|
|
475
|
+
// ===========================================================================
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Get a session by ID.
|
|
479
|
+
*/
|
|
480
|
+
getSession(sessionId: string): ShadowSession | null {
|
|
481
|
+
const session = this.sessions.get(sessionId);
|
|
482
|
+
return session ? this.toPublicSession(session) : null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Get internal session (for testing).
|
|
487
|
+
*/
|
|
488
|
+
getInternalSession(sessionId: string): ShadowSessionInternal | null {
|
|
489
|
+
return this.sessions.get(sessionId) ?? null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Get all sessions for an agent.
|
|
494
|
+
*
|
|
495
|
+
* Requirements: 4.4
|
|
496
|
+
*/
|
|
497
|
+
getSessionsForAgent(agentId: string): ShadowSession[] {
|
|
498
|
+
const sessionIds = this.sessionsByAgent.get(agentId);
|
|
499
|
+
if (!sessionIds) {
|
|
500
|
+
return [];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return Array.from(sessionIds)
|
|
504
|
+
.map((id) => this.sessions.get(id))
|
|
505
|
+
.filter((s): s is ShadowSessionInternal => s !== undefined)
|
|
506
|
+
.map((s) => this.toPublicSession(s));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Check if two sessions are isolated (no shared state).
|
|
511
|
+
*
|
|
512
|
+
* Requirements: 4.4
|
|
513
|
+
*/
|
|
514
|
+
areSessionsIsolated(sessionId1: string, sessionId2: string): boolean {
|
|
515
|
+
const session1 = this.sessions.get(sessionId1);
|
|
516
|
+
const session2 = this.sessions.get(sessionId2);
|
|
517
|
+
|
|
518
|
+
if (!session1 || !session2) {
|
|
519
|
+
return true; // Non-existent sessions are trivially isolated
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Sessions are isolated if they have different context IDs
|
|
523
|
+
return session1.contextState.contextId !== session2.contextState.contextId;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Modify session state (for testing isolation).
|
|
528
|
+
*/
|
|
529
|
+
modifySessionState(
|
|
530
|
+
sessionId: string,
|
|
531
|
+
key: string,
|
|
532
|
+
value: string,
|
|
533
|
+
storage: 'localStorage' | 'sessionStorage' = 'localStorage'
|
|
534
|
+
): boolean {
|
|
535
|
+
const session = this.sessions.get(sessionId);
|
|
536
|
+
if (!session) {
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (storage === 'localStorage') {
|
|
541
|
+
session.contextState.localStorage[key] = value;
|
|
542
|
+
} else {
|
|
543
|
+
session.contextState.sessionStorage[key] = value;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Get session state value (for testing isolation).
|
|
551
|
+
*/
|
|
552
|
+
getSessionStateValue(
|
|
553
|
+
sessionId: string,
|
|
554
|
+
key: string,
|
|
555
|
+
storage: 'localStorage' | 'sessionStorage' = 'localStorage'
|
|
556
|
+
): string | undefined {
|
|
557
|
+
const session = this.sessions.get(sessionId);
|
|
558
|
+
if (!session) {
|
|
559
|
+
return undefined;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (storage === 'localStorage') {
|
|
563
|
+
return session.contextState.localStorage[key];
|
|
564
|
+
} else {
|
|
565
|
+
return session.contextState.sessionStorage[key];
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ===========================================================================
|
|
570
|
+
// Session Resumption from Vault
|
|
571
|
+
// ===========================================================================
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Resume a session from the vault without re-authentication.
|
|
575
|
+
*
|
|
576
|
+
* Requirements: 5.5
|
|
577
|
+
*/
|
|
578
|
+
async resume(domain: string, agentId?: string): Promise<SessionResumptionResult> {
|
|
579
|
+
if (!this.vault) {
|
|
580
|
+
return {
|
|
581
|
+
success: false,
|
|
582
|
+
error: 'No vault configured for session resumption',
|
|
583
|
+
reAuthRequired: true,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Try to retrieve session from vault
|
|
588
|
+
const vaultedSession = await this.vault.retrieve(domain);
|
|
589
|
+
|
|
590
|
+
if (!vaultedSession) {
|
|
591
|
+
return {
|
|
592
|
+
success: false,
|
|
593
|
+
error: 'No vaulted session found for domain',
|
|
594
|
+
reAuthRequired: true,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Check if session is expired
|
|
599
|
+
if (vaultedSession.expiresAt < Date.now()) {
|
|
600
|
+
return {
|
|
601
|
+
success: false,
|
|
602
|
+
error: 'Vaulted session has expired',
|
|
603
|
+
reAuthRequired: true,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Decrypt session data
|
|
608
|
+
const rawData = await this.vault.retrieveDecrypted(domain);
|
|
609
|
+
if (!rawData) {
|
|
610
|
+
return {
|
|
611
|
+
success: false,
|
|
612
|
+
error: 'Failed to decrypt vaulted session',
|
|
613
|
+
reAuthRequired: true,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Create a new session with the vaulted state
|
|
618
|
+
const fingerprint = generateFingerprint();
|
|
619
|
+
const config: SessionConfig = {
|
|
620
|
+
targetDomain: domain,
|
|
621
|
+
fingerprint,
|
|
622
|
+
isolationLevel: 'strict',
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
const createResult = await this.create(config, agentId);
|
|
626
|
+
if (!createResult.success || !createResult.session) {
|
|
627
|
+
return {
|
|
628
|
+
success: false,
|
|
629
|
+
error: createResult.error ?? 'Failed to create session',
|
|
630
|
+
reAuthRequired: true,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Restore the vaulted state to the new session
|
|
635
|
+
const session = this.sessions.get(createResult.session.id);
|
|
636
|
+
if (session) {
|
|
637
|
+
// Parse and restore cookies
|
|
638
|
+
try {
|
|
639
|
+
session.contextState.cookies = JSON.parse(rawData.cookies);
|
|
640
|
+
} catch {
|
|
641
|
+
session.contextState.cookies = [];
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Parse and restore localStorage
|
|
645
|
+
try {
|
|
646
|
+
session.contextState.localStorage = JSON.parse(rawData.localStorage);
|
|
647
|
+
} catch {
|
|
648
|
+
session.contextState.localStorage = {};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Parse and restore sessionStorage
|
|
652
|
+
try {
|
|
653
|
+
session.contextState.sessionStorage = JSON.parse(rawData.sessionStorage);
|
|
654
|
+
} catch {
|
|
655
|
+
session.contextState.sessionStorage = {};
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return {
|
|
660
|
+
success: true,
|
|
661
|
+
session: createResult.session,
|
|
662
|
+
reAuthRequired: false,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Save current session state to vault.
|
|
668
|
+
*/
|
|
669
|
+
async saveToVault(sessionId: string): Promise<boolean> {
|
|
670
|
+
if (!this.vault) {
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const session = this.sessions.get(sessionId);
|
|
675
|
+
if (!session) {
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const rawData: RawSessionData = {
|
|
680
|
+
cookies: JSON.stringify(session.contextState.cookies),
|
|
681
|
+
localStorage: JSON.stringify(session.contextState.localStorage),
|
|
682
|
+
sessionStorage: JSON.stringify(session.contextState.sessionStorage),
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
// Store with 24-hour expiration
|
|
686
|
+
const expiresAt = Date.now() + 86400000;
|
|
687
|
+
await this.vault.store(session.domain, rawData, expiresAt);
|
|
688
|
+
|
|
689
|
+
return true;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Check if a vaulted session exists and is valid.
|
|
694
|
+
*
|
|
695
|
+
* Requirements: 5.5
|
|
696
|
+
*/
|
|
697
|
+
async hasValidVaultedSession(domain: string): Promise<boolean> {
|
|
698
|
+
if (!this.vault) {
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const session = await this.vault.retrieve(domain);
|
|
703
|
+
return session !== null && session.expiresAt > Date.now();
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// ===========================================================================
|
|
707
|
+
// Session Destruction
|
|
708
|
+
// ===========================================================================
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Destroy a session and clean up resources.
|
|
712
|
+
*/
|
|
713
|
+
async destroy(sessionId: string): Promise<void> {
|
|
714
|
+
const session = this.sessions.get(sessionId);
|
|
715
|
+
if (!session) {
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Stop heartbeat
|
|
720
|
+
this.stopHeartbeat(sessionId);
|
|
721
|
+
|
|
722
|
+
// Remove from agent tracking
|
|
723
|
+
if (session.agentId) {
|
|
724
|
+
const agentSessions = this.sessionsByAgent.get(session.agentId);
|
|
725
|
+
if (agentSessions) {
|
|
726
|
+
agentSessions.delete(sessionId);
|
|
727
|
+
if (agentSessions.size === 0) {
|
|
728
|
+
this.sessionsByAgent.delete(session.agentId);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Remove session
|
|
734
|
+
this.sessions.delete(sessionId);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Destroy all sessions for an agent.
|
|
739
|
+
*/
|
|
740
|
+
async destroyAllForAgent(agentId: string): Promise<number> {
|
|
741
|
+
const sessionIds = this.sessionsByAgent.get(agentId);
|
|
742
|
+
if (!sessionIds) {
|
|
743
|
+
return 0;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const count = sessionIds.size;
|
|
747
|
+
for (const sessionId of sessionIds) {
|
|
748
|
+
await this.destroy(sessionId);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return count;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Get count of active sessions.
|
|
756
|
+
*/
|
|
757
|
+
getActiveSessionCount(): number {
|
|
758
|
+
return Array.from(this.sessions.values()).filter((s) => s.status === 'active').length;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Get total session count.
|
|
763
|
+
*/
|
|
764
|
+
getTotalSessionCount(): number {
|
|
765
|
+
return this.sessions.size;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Clear all sessions (for testing).
|
|
770
|
+
*/
|
|
771
|
+
clear(): void {
|
|
772
|
+
// Stop all heartbeats
|
|
773
|
+
for (const sessionId of this.heartbeatTimers.keys()) {
|
|
774
|
+
this.stopHeartbeat(sessionId);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
this.sessions.clear();
|
|
778
|
+
this.sessionsByAgent.clear();
|
|
779
|
+
this.heartbeatTimers.clear();
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// =============================================================================
|
|
784
|
+
// Factory Function
|
|
785
|
+
// =============================================================================
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Create a new Shadow Session Orchestrator.
|
|
789
|
+
*/
|
|
790
|
+
export function createShadowSessionOrchestrator(
|
|
791
|
+
config?: ShadowSessionOrchestratorConfig
|
|
792
|
+
): ShadowSessionOrchestrator {
|
|
793
|
+
return new ShadowSessionOrchestrator(config);
|
|
794
|
+
}
|