device-passport 1.1.0 → 1.2.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 (49) hide show
  1. package/CHANGELOG.md +10 -7
  2. package/README.md +41 -0
  3. package/assets/ego-profile-manager.png +0 -0
  4. package/dist/adapters/index.cjs +2 -2
  5. package/dist/adapters/index.d.cts +4 -94
  6. package/dist/adapters/index.d.mts +4 -94
  7. package/dist/adapters/index.d.ts +4 -94
  8. package/dist/adapters/index.mjs +2 -2
  9. package/dist/ego/index.cjs +22 -0
  10. package/dist/ego/index.d.cts +5 -0
  11. package/dist/ego/index.d.mts +5 -0
  12. package/dist/ego/index.d.ts +5 -0
  13. package/dist/ego/index.mjs +8 -0
  14. package/dist/evaluator/index.cjs +22 -584
  15. package/dist/evaluator/index.mjs +3 -564
  16. package/dist/geo/index.cjs +1 -1
  17. package/dist/geo/index.d.cts +4 -0
  18. package/dist/geo/index.d.mts +4 -0
  19. package/dist/geo/index.d.ts +4 -0
  20. package/dist/geo/index.mjs +2 -2
  21. package/dist/index.cjs +41 -441
  22. package/dist/index.d.cts +10 -198
  23. package/dist/index.d.mts +10 -198
  24. package/dist/index.d.ts +10 -198
  25. package/dist/index.mjs +7 -418
  26. package/dist/shared/device-passport.BGfRcQNh.mjs +565 -0
  27. package/dist/shared/{device-passport.BPd2-w8e.cjs → device-passport.BKtmLzPt.cjs} +4 -3
  28. package/dist/shared/{device-passport.BBf9OKap.mjs → device-passport.BPQj0qdT.mjs} +4 -3
  29. package/dist/shared/device-passport.CDS7xrj8.d.ts +469 -0
  30. package/dist/shared/device-passport.CfLes-lq.d.cts +469 -0
  31. package/dist/shared/device-passport.CsOWqPIr.d.cts +77 -0
  32. package/dist/shared/device-passport.D7pEnyp2.d.ts +77 -0
  33. package/dist/shared/{device-passport.CLaBZWUV.cjs → device-passport.D9xW6R0B.cjs} +4 -2
  34. package/dist/shared/device-passport.DirQmSd4.d.cts +47 -0
  35. package/dist/shared/device-passport.DirQmSd4.d.mts +47 -0
  36. package/dist/shared/device-passport.DirQmSd4.d.ts +47 -0
  37. package/dist/shared/device-passport.EK4RX7Xd.cjs +584 -0
  38. package/dist/shared/device-passport.ZBspe4ZW.d.mts +77 -0
  39. package/dist/shared/device-passport.dVEqzSEb.d.mts +469 -0
  40. package/dist/shared/{device-passport.NHw_f6HT.mjs → device-passport.gNo82fQ5.mjs} +5 -3
  41. package/dist/shared/device-passport.kekU28UM.mjs +705 -0
  42. package/dist/shared/device-passport.qL4tjwc0.cjs +718 -0
  43. package/dist/storage/index.d.cts +3 -34
  44. package/dist/storage/index.d.mts +3 -34
  45. package/dist/storage/index.d.ts +3 -34
  46. package/dist/types/index.d.cts +183 -1
  47. package/dist/types/index.d.mts +183 -1
  48. package/dist/types/index.d.ts +183 -1
  49. package/package.json +6 -1
@@ -0,0 +1,718 @@
1
+ 'use strict';
2
+
3
+ const cdp = require('./device-passport.D9xW6R0B.cjs');
4
+ const entropy = require('./device-passport.EK4RX7Xd.cjs');
5
+ const memory = require('./device-passport.Cjxlv8xI.cjs');
6
+
7
+ class DevicePassportInstance {
8
+ profile;
9
+ constructor(profile) {
10
+ if (!profile) {
11
+ throw new Error("[DevicePassportInstance] Profile cannot be null or undefined");
12
+ }
13
+ this.profile = profile;
14
+ }
15
+ /**
16
+ * Factory method to create an instance from a plain DevicePassport object.
17
+ */
18
+ static from(profile) {
19
+ return new DevicePassportInstance(profile);
20
+ }
21
+ // ==========================================
22
+ // Getters for core profile facets
23
+ // ==========================================
24
+ get id() {
25
+ return this.profile.id;
26
+ }
27
+ get seed() {
28
+ return this.profile.seed;
29
+ }
30
+ get createdAt() {
31
+ return this.profile.createdAt;
32
+ }
33
+ get browser() {
34
+ return this.profile.browser;
35
+ }
36
+ get os() {
37
+ return this.profile.os;
38
+ }
39
+ get hardware() {
40
+ return this.profile.hardware;
41
+ }
42
+ get locale() {
43
+ return this.profile.locale;
44
+ }
45
+ get headers() {
46
+ return this.profile.headers;
47
+ }
48
+ get clientHints() {
49
+ return this.profile.clientHints;
50
+ }
51
+ get context() {
52
+ return this.profile.context;
53
+ }
54
+ get userAgent() {
55
+ return this.profile.headers["user-agent"] || this.profile.browser.userAgent;
56
+ }
57
+ get timezone() {
58
+ return this.profile.locale.timezone;
59
+ }
60
+ get playwrightContext() {
61
+ return this.getPlaywrightContext();
62
+ }
63
+ get puppeteerArgs() {
64
+ return this.getPuppeteerArgs();
65
+ }
66
+ get evaluation() {
67
+ return this.profile.evaluation;
68
+ }
69
+ // ==========================================
70
+ // Lightweight HTTP Mode Adapters
71
+ // ==========================================
72
+ /**
73
+ * Returns HTTP headers formatted in strict canonical browser sequence.
74
+ * Essential for anti-bot evasion against Cloudflare, Akamai, and DataDome.
75
+ */
76
+ getHeaders(overrides) {
77
+ return cdp.formatCanonicalHeaders(this.profile.headers, overrides);
78
+ }
79
+ /**
80
+ * Returns a higher-order fetch function wrapping standard fetch with this passport's canonical persona.
81
+ */
82
+ wrapFetch(customFetch) {
83
+ return cdp.withPassport(this, customFetch);
84
+ }
85
+ /**
86
+ * Alias for wrapFetch.
87
+ */
88
+ withPassport(customFetch) {
89
+ return cdp.withPassport(this, customFetch);
90
+ }
91
+ /**
92
+ * Generates a copy-pasteable cURL command with correct browser headers and User-Agent.
93
+ */
94
+ toCurl(url, options) {
95
+ return cdp.toCurl(this, url, options);
96
+ }
97
+ // ==========================================
98
+ // Heavyweight Browser Automation Adapters
99
+ // ==========================================
100
+ /**
101
+ * Generates Playwright BrowserContext options.
102
+ * Usage:
103
+ * ```ts
104
+ * const context = await browser.newContext(passport.getPlaywrightContext());
105
+ * ```
106
+ */
107
+ getPlaywrightContext() {
108
+ return cdp.getPlaywrightContext(this.profile);
109
+ }
110
+ /**
111
+ * Returns Puppeteer launch args and page emulation parameters.
112
+ */
113
+ getPuppeteerArgs() {
114
+ return cdp.getPuppeteerArgs(this.profile);
115
+ }
116
+ /**
117
+ * Generates an in-page stealth script to patch runtime JavaScript APIs
118
+ * (navigator.webdriver, hardwareConcurrency, deviceMemory, WebGL, Client Hints).
119
+ */
120
+ getStealthScript() {
121
+ return cdp.getStealthScript(this.profile);
122
+ }
123
+ /**
124
+ * Injects stealth scripts and extra HTTP headers into a Playwright BrowserContext.
125
+ */
126
+ async applyToPlaywright(context) {
127
+ await cdp.applyToPlaywrightContext(context, this.profile);
128
+ }
129
+ /**
130
+ * Injects stealth scripts and emulation parameters into a Puppeteer Page.
131
+ */
132
+ async applyToPuppeteer(page) {
133
+ await cdp.applyToPuppeteerPage(page, this.profile);
134
+ }
135
+ // ==========================================
136
+ // Consistency & Diagnostics
137
+ // ==========================================
138
+ /**
139
+ * Evaluates cross-layer consistency and Shannon entropy.
140
+ * Caches the result on `this.profile.evaluation`.
141
+ */
142
+ evaluate(forceRecompute = false) {
143
+ if (this.profile.evaluation && !forceRecompute) {
144
+ return this.profile.evaluation;
145
+ }
146
+ try {
147
+ const contradictions = entropy.detectAllContradictions(this.profile);
148
+ let estimatedBits = 48;
149
+ let collisionRisk = "very_low";
150
+ try {
151
+ const entropy$1 = entropy.analyzeHeaderEntropy(this.profile.headers);
152
+ if (entropy$1) {
153
+ estimatedBits = entropy$1.estimatedBits;
154
+ collisionRisk = entropy$1.collisionRisk;
155
+ }
156
+ } catch {
157
+ }
158
+ const totalPenalty = contradictions.reduce((sum, c) => sum + c.impactPenalty, 0);
159
+ const overallScore = Math.max(0, Math.min(100, Math.round(100 - totalPenalty)));
160
+ const status = overallScore >= 90 ? "PASS" : overallScore >= 70 ? "WARN" : "FAIL";
161
+ const grade = overallScore >= 95 ? "A+" : overallScore >= 85 ? "A" : overallScore >= 70 ? "B" : overallScore >= 50 ? "C" : "F";
162
+ const checks = [
163
+ {
164
+ id: "chk_os_ua_alignment",
165
+ name: "OS and User-Agent Alignment",
166
+ category: "os_ua_alignment",
167
+ passed: !contradictions.some((c) => c.category === "os_browser"),
168
+ score: contradictions.some((c) => c.category === "os_browser") ? 0 : 100,
169
+ weight: 1.5,
170
+ message: contradictions.find((c) => c.category === "os_browser")?.description ?? "User-Agent and Operating System are fully coherent"
171
+ },
172
+ {
173
+ id: "chk_hardware_plausibility",
174
+ name: "Hardware and GPU Plausibility",
175
+ category: "hardware_plausibility",
176
+ passed: !contradictions.some((c) => c.category === "hardware_system"),
177
+ score: contradictions.some((c) => c.category === "hardware_system") ? 0 : 100,
178
+ weight: 2,
179
+ message: contradictions.find((c) => c.category === "hardware_system")?.description ?? "GPU renderer, CPU architecture and RAM are physically plausible"
180
+ },
181
+ {
182
+ id: "chk_geo_locale_coherence",
183
+ name: "Geo and Locale Coherence",
184
+ category: "geo_locale_coherence",
185
+ passed: !contradictions.some((c) => c.category === "geo_chrono"),
186
+ score: contradictions.some((c) => c.category === "geo_chrono") ? 0 : 100,
187
+ weight: 1.5,
188
+ message: contradictions.find((c) => c.category === "geo_chrono")?.description ?? "Timezone, country, and Accept-Language are geographically locked"
189
+ },
190
+ {
191
+ id: "chk_client_hints_entropy",
192
+ name: "Client Hints RFC 8942 Compliance",
193
+ category: "client_hints_entropy",
194
+ passed: !contradictions.some((c) => c.category === "client_hints"),
195
+ score: contradictions.some((c) => c.category === "client_hints") ? 0 : 100,
196
+ weight: 1.5,
197
+ message: contradictions.find((c) => c.category === "client_hints")?.description ?? "User-Agent Client Hints headers strictly match W3C and Chromium freeze specifications"
198
+ }
199
+ ];
200
+ const evaluation = {
201
+ overallScore,
202
+ status,
203
+ grade,
204
+ checks,
205
+ entropyAnalysis: {
206
+ estimatedBits,
207
+ collisionRisk
208
+ },
209
+ evaluatedAt: (/* @__PURE__ */ new Date()).toISOString()
210
+ };
211
+ this.profile.evaluation = evaluation;
212
+ return evaluation;
213
+ } catch {
214
+ if (this.profile.evaluation) {
215
+ return this.profile.evaluation;
216
+ }
217
+ const baseline = {
218
+ overallScore: 100,
219
+ status: "PASS",
220
+ grade: "A+",
221
+ checks: [],
222
+ evaluatedAt: (/* @__PURE__ */ new Date()).toISOString()
223
+ };
224
+ this.profile.evaluation = baseline;
225
+ return baseline;
226
+ }
227
+ }
228
+ // ==========================================
229
+ // Serialization
230
+ // ==========================================
231
+ /**
232
+ * Returns the underlying raw DevicePassport data object.
233
+ */
234
+ toJSON() {
235
+ return this.profile;
236
+ }
237
+ toString() {
238
+ const { id, browser, os, locale } = this.profile;
239
+ return `[DevicePassport ${id} | ${browser?.name ?? "Browser"} ${browser?.version ?? ""} on ${os?.platform ?? "OS"} | ${locale?.language ?? "Locale"}]`;
240
+ }
241
+ }
242
+ function createPassport(seedOrOptionsOrProfile, extraOptions) {
243
+ if (seedOrOptionsOrProfile instanceof DevicePassportInstance) {
244
+ return seedOrOptionsOrProfile;
245
+ }
246
+ if (seedOrOptionsOrProfile && typeof seedOrOptionsOrProfile === "object" && "id" in seedOrOptionsOrProfile && "clientHints" in seedOrOptionsOrProfile && "headers" in seedOrOptionsOrProfile) {
247
+ return new DevicePassportInstance(seedOrOptionsOrProfile);
248
+ }
249
+ let options = {};
250
+ if (typeof seedOrOptionsOrProfile === "string") {
251
+ options = { ...extraOptions, seed: seedOrOptionsOrProfile };
252
+ } else if (seedOrOptionsOrProfile && typeof seedOrOptionsOrProfile === "object") {
253
+ options = { ...seedOrOptionsOrProfile };
254
+ } else if (extraOptions) {
255
+ options = { ...extraOptions };
256
+ }
257
+ if (options.store && options.seed) {
258
+ const storage = typeof options.store === "string" ? new memory.JSONFileStorage({ filePath: options.store }) : options.store;
259
+ const seedKey = String(options.seed);
260
+ if (storage.hasSync?.(seedKey) && storage.getSync) {
261
+ const existing = storage.getSync(seedKey);
262
+ if (existing) return new DevicePassportInstance(existing);
263
+ }
264
+ const generated2 = cdp.generatePassport(options);
265
+ if (storage.setSync) {
266
+ storage.setSync(seedKey, generated2);
267
+ } else {
268
+ storage.set(seedKey, generated2).catch(() => {
269
+ });
270
+ }
271
+ return new DevicePassportInstance(generated2);
272
+ }
273
+ const generated = cdp.generatePassport(options);
274
+ return new DevicePassportInstance(generated);
275
+ }
276
+
277
+ class PassportManager {
278
+ storage;
279
+ defaultGeo;
280
+ defaultOS;
281
+ defaultBrowser;
282
+ generator;
283
+ constructor(options) {
284
+ if (typeof options?.store === "string") {
285
+ this.storage = new memory.JSONFileStorage(options.store);
286
+ } else if (options?.store && typeof options.store.get === "function") {
287
+ this.storage = options.store;
288
+ } else {
289
+ this.storage = new memory.MemoryStorage();
290
+ }
291
+ this.defaultGeo = options?.defaultGeo ?? "US";
292
+ this.defaultOS = options?.defaultOS;
293
+ this.defaultBrowser = options?.defaultBrowser;
294
+ this.generator = options?.generator ?? cdp.generatePassport;
295
+ }
296
+ /**
297
+ * Retrieves an existing sticky passport by seed or deterministically generates
298
+ * and persists a new one if it does not already exist.
299
+ *
300
+ * @param seed Deterministic seed (user ID, account handle, proxy IP, worker slot)
301
+ * @param options Generator options to use if persona needs to be created
302
+ */
303
+ async getPassport(seed, options) {
304
+ const seedStr = String(seed);
305
+ const existing = await this.storage.get(seedStr);
306
+ if (existing) {
307
+ return new DevicePassportInstance(existing);
308
+ }
309
+ const mergedOptions = {
310
+ seed: seedStr,
311
+ country: options?.country ?? this.defaultGeo,
312
+ os: options?.os ?? this.defaultOS,
313
+ browser: options?.browser ?? this.defaultBrowser,
314
+ ...options
315
+ };
316
+ const profile = this.generator(mergedOptions);
317
+ await this.storage.set(seedStr, profile);
318
+ return new DevicePassportInstance(profile);
319
+ }
320
+ /**
321
+ * Synchronous passport retrieval for environments backed by MemoryStorage or JSONFileStorage.
322
+ */
323
+ getPassportSync(seed, options) {
324
+ const seedStr = String(seed);
325
+ const syncStore = this.storage;
326
+ if (typeof syncStore.getSync === "function") {
327
+ const existing = syncStore.getSync(seedStr);
328
+ if (existing) {
329
+ return new DevicePassportInstance(existing);
330
+ }
331
+ const mergedOptions = {
332
+ seed: seedStr,
333
+ country: options?.country ?? this.defaultGeo,
334
+ os: options?.os ?? this.defaultOS,
335
+ browser: options?.browser ?? this.defaultBrowser,
336
+ ...options
337
+ };
338
+ const profile = this.generator(mergedOptions);
339
+ if (typeof syncStore.setSync === "function") {
340
+ syncStore.setSync(seedStr, profile);
341
+ }
342
+ return new DevicePassportInstance(profile);
343
+ }
344
+ throw new Error(
345
+ "[PassportManager] Current storage adapter does not support synchronous operations. Use await manager.getPassport(seed) instead."
346
+ );
347
+ }
348
+ /**
349
+ * Forces re-generation and overwrite of a passport for the given seed.
350
+ */
351
+ async renewPassport(seed, options) {
352
+ const seedStr = String(seed);
353
+ const mergedOptions = {
354
+ seed: seedStr,
355
+ country: options?.country ?? this.defaultGeo,
356
+ os: options?.os ?? this.defaultOS,
357
+ browser: options?.browser ?? this.defaultBrowser,
358
+ ...options
359
+ };
360
+ const profile = this.generator(mergedOptions);
361
+ await this.storage.set(seedStr, profile);
362
+ return new DevicePassportInstance(profile);
363
+ }
364
+ /**
365
+ * Check if a passport exists in storage for the given seed.
366
+ */
367
+ async hasPassport(seed) {
368
+ return this.storage.has(String(seed));
369
+ }
370
+ /**
371
+ * Delete a passport from storage.
372
+ */
373
+ async deletePassport(seed) {
374
+ return this.storage.delete(String(seed));
375
+ }
376
+ /**
377
+ * Import an existing passport into storage.
378
+ */
379
+ async importPassport(passport) {
380
+ const raw = passport instanceof DevicePassportInstance ? passport.toJSON() : passport;
381
+ await this.storage.set(raw.seed, raw);
382
+ return new DevicePassportInstance(raw);
383
+ }
384
+ /**
385
+ * List all stored seeds.
386
+ */
387
+ async listSeeds() {
388
+ return this.storage.keys();
389
+ }
390
+ /**
391
+ * List all stored passports as DevicePassportInstance objects.
392
+ */
393
+ async listPassports() {
394
+ const keys = await this.storage.keys();
395
+ const instances = [];
396
+ for (const key of keys) {
397
+ const item = await this.storage.get(key);
398
+ if (item) {
399
+ instances.push(new DevicePassportInstance(item));
400
+ }
401
+ }
402
+ return instances;
403
+ }
404
+ /**
405
+ * Clear all stored passports.
406
+ */
407
+ async clear() {
408
+ await this.storage.clear();
409
+ }
410
+ /**
411
+ * Return number of stored personas.
412
+ */
413
+ async count() {
414
+ const keys = await this.storage.keys();
415
+ return keys.length;
416
+ }
417
+ }
418
+
419
+ const PROXY_RESOLUTION_NOTE = "ego-lite cannot configure a proxy at the browser layer: CDP has no Browser.setProxySettings and ego-lite exposes no proxy API. Bind this proxy at the system level (OS proxy / TUN / local forwarder in front of the ego-lite process) before launching the session.";
420
+ const PROXY_MISSING_NOTE = "No proxy is declared for this account, so the session will egress directly. ego-lite cannot add a proxy later at the browser layer \u2014 either declare one on the account profile or route the whole ego-lite process through a system-level proxy.";
421
+ function getProfileReadiness(profile) {
422
+ if (!profile.profileId) {
423
+ return "missing-profile";
424
+ }
425
+ if (!profile.proxy) {
426
+ return "missing-proxy";
427
+ }
428
+ return "ready";
429
+ }
430
+ function buildProxyDeclaration(profile) {
431
+ if (profile.proxy) {
432
+ return {
433
+ bound: true,
434
+ binding: profile.proxy,
435
+ resolution: "system-level",
436
+ note: PROXY_RESOLUTION_NOTE
437
+ };
438
+ }
439
+ return {
440
+ bound: false,
441
+ binding: null,
442
+ resolution: "system-level",
443
+ note: PROXY_MISSING_NOTE
444
+ };
445
+ }
446
+ function buildEgoSessionPlan(passport, profile, options = {}) {
447
+ const cdpPlan = cdp.getCdpPlan(passport, options.sessionId);
448
+ const readiness = getProfileReadiness(profile);
449
+ const proxy = buildProxyDeclaration(profile);
450
+ return {
451
+ seed: profile.seed,
452
+ profileId: profile.profileId,
453
+ taskSpaceName: options.taskSpaceName ?? defaultTaskSpaceName(profile.seed),
454
+ ...cdpPlan,
455
+ proxy,
456
+ readiness,
457
+ launchable: readiness === "ready"
458
+ };
459
+ }
460
+ function defaultTaskSpaceName(seed) {
461
+ return `dp-${seed}`;
462
+ }
463
+
464
+ class AccountProfileManager {
465
+ /** Persistent store of account profile records. */
466
+ storage;
467
+ /** The passport manager holding the sticky identities. */
468
+ passportManager;
469
+ /** The ego-lite bridge, when one was injected. */
470
+ bridge;
471
+ constructor(options = {}) {
472
+ if (typeof options.store === "string") {
473
+ this.storage = new memory.JSONFileStorage({ filePath: options.store });
474
+ } else if (options.store && typeof options.store.get === "function") {
475
+ this.storage = options.store;
476
+ } else {
477
+ this.storage = new memory.MemoryStorage();
478
+ }
479
+ const passportStore = options.passportStore ?? options.passportOptions?.store;
480
+ this.passportManager = options.passportManager ?? new PassportManager({
481
+ ...options.passportOptions,
482
+ ...passportStore !== void 0 ? { store: passportStore } : {}
483
+ });
484
+ this.bridge = options.bridge;
485
+ }
486
+ /**
487
+ * Lists the ego-lite profiles available for binding.
488
+ *
489
+ * @throws When no bridge was injected — ego-lite has no "create profile" API,
490
+ * so a profile must already exist and be listed before it can be bound.
491
+ */
492
+ async listEgoProfiles() {
493
+ if (!this.bridge) {
494
+ throw new Error(
495
+ "[ego] no ego-lite bridge available. Inject `bridge` to list profiles; ego-lite has no API for creating profiles."
496
+ );
497
+ }
498
+ return this.bridge.listProfiles();
499
+ }
500
+ /**
501
+ * Returns the stored account profile record for a seed.
502
+ */
503
+ async getProfile(seed) {
504
+ return this.storage.get(String(seed));
505
+ }
506
+ /**
507
+ * Returns the readiness of an account profile.
508
+ *
509
+ * `ready` requires both a bound ego-lite profile and a declared proxy.
510
+ */
511
+ async getReadiness(seed) {
512
+ const profile = await this.getProfile(seed);
513
+ if (!profile) {
514
+ return "missing-profile";
515
+ }
516
+ return getProfileReadiness(profile);
517
+ }
518
+ /**
519
+ * Binds an account to an existing ego-lite profile, and optionally declares its proxy.
520
+ *
521
+ * The proxy is only *recorded* here. ego-lite cannot apply it at the browser layer,
522
+ * so the operator must bind it at the system level before launching a session.
523
+ *
524
+ * @param seed The account seed. Doubles as the sticky passport seed.
525
+ * @param options The profile id to bind and the optional proxy declaration.
526
+ * @param passportOptions Generator options used when the passport is first created.
527
+ */
528
+ async bindProfile(seed, options, passportOptions) {
529
+ const seedStr = String(seed);
530
+ if (!options.profileId) {
531
+ throw new Error("[ego] profileId is required to bind an account profile");
532
+ }
533
+ if (this.bridge) {
534
+ const profiles = await this.bridge.listProfiles();
535
+ if (!profiles.some((p) => p.id === options.profileId)) {
536
+ throw new Error(
537
+ `[ego] profile "${options.profileId}" does not exist in ego-lite. Available profiles: ${profiles.map((p) => p.id).join(", ") || "(none)"}`
538
+ );
539
+ }
540
+ }
541
+ const existing = await this.storage.get(seedStr);
542
+ const effectiveProxy = options.proxy !== void 0 ? options.proxy : existing?.proxy ?? null;
543
+ const passport = await this.passportManager.getPassport(seedStr, {
544
+ ...passportOptions,
545
+ ...effectiveProxy?.country ? { geo: effectiveProxy.country } : {}
546
+ });
547
+ const proxy = options.proxy !== void 0 ? options.proxy : existing?.proxy ?? null;
548
+ const record = {
549
+ seed: seedStr,
550
+ passportId: passport.id,
551
+ profileId: options.profileId,
552
+ proxy: proxy ?? null,
553
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
554
+ };
555
+ await this.storage.set(seedStr, record);
556
+ return record;
557
+ }
558
+ /**
559
+ * Declares (or clears) the proxy binding of an account.
560
+ *
561
+ * Passing `null` removes the declaration; the account then reports `missing-proxy`.
562
+ */
563
+ async setProxy(seed, proxy) {
564
+ const seedStr = String(seed);
565
+ const existing = await this.storage.get(seedStr);
566
+ if (!existing) {
567
+ throw new Error(
568
+ `[ego] no account profile for seed ${seedStr}. Bind a profile before declaring a proxy.`
569
+ );
570
+ }
571
+ const record = {
572
+ ...existing,
573
+ proxy: proxy ?? null,
574
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
575
+ };
576
+ await this.storage.set(seedStr, record);
577
+ return record;
578
+ }
579
+ /**
580
+ * Returns the sticky passport instance for an account.
581
+ */
582
+ async getPassport(seed, options) {
583
+ return this.passportManager.getPassport(String(seed), options);
584
+ }
585
+ /**
586
+ * Builds the complete launch plan for one account session.
587
+ *
588
+ * The plan carries the CDP command sequence, init scripts, headers, the ego-lite
589
+ * profile id, and an explicit proxy declaration whose `bound` flag reflects whether
590
+ * a proxy is actually declared for the account.
591
+ *
592
+ * @throws When the account has no bound profile.
593
+ */
594
+ async createSessionPlan(seed, options = {}) {
595
+ const seedStr = String(seed);
596
+ const profile = await this.storage.get(seedStr);
597
+ if (!profile) {
598
+ throw new Error(`[ego] no account profile for seed ${seedStr}`);
599
+ }
600
+ const passport = await this.passportManager.getPassport(seedStr, {
601
+ // The exit IP's country must match the persona's geo context, otherwise the
602
+ // browser claims one country while the traffic leaves from another — the
603
+ // classic "geo divergence" that anti-bot classifiers score on.
604
+ ...profile.proxy?.country ? { geo: profile.proxy.country } : {}
605
+ });
606
+ return buildEgoSessionPlan(passport.profile, profile, {
607
+ taskSpaceName: options.taskSpaceName,
608
+ sessionId: options.sessionId
609
+ });
610
+ }
611
+ /**
612
+ * Opens an ego-lite task space bound to the account's profile.
613
+ *
614
+ * The task space inherits the profile's cookies and login state; a different
615
+ * profile is fully isolated from it.
616
+ *
617
+ * @returns The task space identifier.
618
+ * @throws When the account has no bound profile or no bridge was injected.
619
+ */
620
+ async openTaskSpace(seed, taskSpaceName) {
621
+ if (!this.bridge) {
622
+ throw new Error(
623
+ "[ego] no ego-lite bridge available. Inject `bridge` to open task spaces."
624
+ );
625
+ }
626
+ const seedStr = String(seed);
627
+ const profile = await this.storage.get(seedStr);
628
+ if (!profile) {
629
+ throw new Error(`[ego] no account profile for seed ${seedStr}`);
630
+ }
631
+ const plan = await this.createSessionPlan(seedStr, { taskSpaceName });
632
+ return this.bridge.createTaskSpace(plan.taskSpaceName, profile.profileId);
633
+ }
634
+ /**
635
+ * Lists every stored account seed.
636
+ */
637
+ async listSeeds() {
638
+ return this.storage.keys();
639
+ }
640
+ /**
641
+ * Removes an account profile record.
642
+ */
643
+ async removeProfile(seed) {
644
+ return this.storage.delete(String(seed));
645
+ }
646
+ }
647
+
648
+ function getEgoNativeBridge(scope = globalThis) {
649
+ if (!scope || typeof scope !== "object") {
650
+ return null;
651
+ }
652
+ const candidate = scope.ego;
653
+ if (!candidate || typeof candidate !== "object") {
654
+ return null;
655
+ }
656
+ return candidate;
657
+ }
658
+ function createEgoBridge(native = getEgoNativeBridge()) {
659
+ if (!native) {
660
+ throw new Error(
661
+ "[ego] ego-lite native bridge not found. Run inside the ego-browser CLI nodejs heredoc mode."
662
+ );
663
+ }
664
+ return {
665
+ async listProfiles() {
666
+ if (typeof native.listProfiles !== "function") {
667
+ throw new Error("[ego] ego.listProfiles is not available");
668
+ }
669
+ const result = await native.listProfiles();
670
+ const profiles = Array.isArray(result) ? result : result?.profiles;
671
+ if (!Array.isArray(profiles)) {
672
+ throw new Error("[ego] ego.listProfiles returned no profiles array");
673
+ }
674
+ return profiles;
675
+ },
676
+ async createTaskSpace(name, profileId) {
677
+ if (typeof native.createTaskSpace !== "function") {
678
+ throw new Error("[ego] ego.createTaskSpace is not available");
679
+ }
680
+ const result = await native.createTaskSpace(name, profileId);
681
+ return normalizeTaskSpaceId(result, name);
682
+ }
683
+ };
684
+ }
685
+ function normalizeTaskSpaceId(result, name) {
686
+ if (typeof result === "string" && result.length > 0) {
687
+ return result;
688
+ }
689
+ if (typeof result === "number" && Number.isFinite(result)) {
690
+ return String(result);
691
+ }
692
+ if (result && typeof result === "object") {
693
+ const record = result;
694
+ for (const key of ["taskId", "id", "name"]) {
695
+ const value = record[key];
696
+ if (typeof value === "string" && value.length > 0) {
697
+ return value;
698
+ }
699
+ if (typeof value === "number" && Number.isFinite(value)) {
700
+ return String(value);
701
+ }
702
+ }
703
+ }
704
+ throw new Error(`[ego] could not resolve task space id for ${name}`);
705
+ }
706
+
707
+ exports.AccountProfileManager = AccountProfileManager;
708
+ exports.DevicePassportInstance = DevicePassportInstance;
709
+ exports.PROXY_MISSING_NOTE = PROXY_MISSING_NOTE;
710
+ exports.PROXY_RESOLUTION_NOTE = PROXY_RESOLUTION_NOTE;
711
+ exports.PassportManager = PassportManager;
712
+ exports.buildEgoSessionPlan = buildEgoSessionPlan;
713
+ exports.buildProxyDeclaration = buildProxyDeclaration;
714
+ exports.createEgoBridge = createEgoBridge;
715
+ exports.createPassport = createPassport;
716
+ exports.defaultTaskSpaceName = defaultTaskSpaceName;
717
+ exports.getEgoNativeBridge = getEgoNativeBridge;
718
+ exports.getProfileReadiness = getProfileReadiness;