@hraness/ghostget 0.18.0 → 0.18.2

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 (76) hide show
  1. package/CHANGELOG.md +283 -0
  2. package/README.md +7 -7
  3. package/dist/apple-photos-client.js +2 -1
  4. package/dist/beeper-client.js +2 -1
  5. package/dist/client.js +1 -0
  6. package/dist/imessage-direct-install-2gm3kge7.js +324 -0
  7. package/dist/index-01eeae9e.js +5 -0
  8. package/dist/index-2ymnp8xv.js +145 -0
  9. package/dist/index-en5hycxp.js +175 -0
  10. package/dist/{index-9wca02er.js → index-hfbygww8.js} +1 -1
  11. package/dist/index-n4szk3nw.js +50 -0
  12. package/dist/index-yq6maz71.js +1105 -0
  13. package/dist/index-z1w83f81.js +4 -0
  14. package/dist/index.js +4 -47
  15. package/dist/messaging-automation-api.js +21 -0
  16. package/dist/messaging-automation-j4274hvc.js +609 -0
  17. package/dist/messaging-native-install-8w1j36ah.js +16 -0
  18. package/dist/messaging.js +1 -0
  19. package/dist/omni-client.js +1 -0
  20. package/dist/whatsapp-automation-runtime-hgh1e9rm.js +845 -0
  21. package/dist/whatsapp-client.js +1 -0
  22. package/docs/control-panel.md +2 -2
  23. package/docs/imessage-direct-provider.md +31 -7
  24. package/docs/messaging-automation.md +103 -0
  25. package/package.json +22 -2
  26. package/skills/ghostget/references/control-panel.md +1 -1
  27. package/skills/ghostget/references/install.md +5 -5
  28. package/skills/ghostget/references/linkedin-adapter.md +209 -13
  29. package/skills/ghostget/references/platform-patterns.md +1 -1
  30. package/src/args.ts +18 -5
  31. package/src/assets/adapters/imessage/wrench-web-adapter.json +126 -0
  32. package/src/assets/adapters/linkedin/wrench-web-adapter.json +1 -1
  33. package/src/assets/adapters/whatsapp/wrench-web-adapter.json +144 -0
  34. package/src/assets/messaging-runtime/NOTICE.txt +2580 -0
  35. package/src/assets/messaging-runtime/imsg-darwin-arm64.gz +0 -0
  36. package/src/assets/messaging-runtime/phone-number-info.plist.gz +0 -0
  37. package/src/assets/messaging-runtime/phone-number-metadata.json.gz +0 -0
  38. package/src/assets/messaging-runtime/phone-number-privacy.plist.gz +0 -0
  39. package/src/assets/messaging-runtime/wacli-darwin-arm64.gz +0 -0
  40. package/src/beeper-client-types.ts +1 -1
  41. package/src/ghostget.ts +16 -4
  42. package/src/messaging-automation-api.ts +12 -0
  43. package/src/messaging-automation-descriptors.ts +29 -0
  44. package/src/messaging-automation-factory.ts +148 -0
  45. package/src/messaging-automation-server.ts +183 -0
  46. package/src/messaging-automation-types.ts +155 -0
  47. package/src/messaging-automation-validation.ts +110 -0
  48. package/src/messaging-automation.ts +356 -0
  49. package/src/plugins/imessage-direct/plugin.ts +12 -1
  50. package/src/plugins/imessage-direct/vendor/0003-feat-rpc-add-no-fetch-rich-cards.patch +276 -0
  51. package/src/plugins/imessage-direct/vendor/provenance.json +84 -10
  52. package/src/plugins/linkedin-web/plugin.ts +1 -1
  53. package/src/plugins/whatsapp-linked-device/plugin.ts +7 -2
  54. package/src/plugins/whatsapp-linked-device/vendor/0001-ghostget-private-messaging.patch +1093 -0
  55. package/src/plugins/whatsapp-linked-device/vendor/README.md +39 -0
  56. package/src/plugins/whatsapp-linked-device/vendor/provenance.json +45 -0
  57. package/src/provider-plugin.ts +14 -0
  58. package/src/providers/imessage-automation.ts +250 -0
  59. package/src/providers/imessage-direct-install.ts +7 -0
  60. package/src/providers/imessage-direct-runtime.ts +32 -2
  61. package/src/providers/imessage-direct.ts +13 -3
  62. package/src/providers/linkedin-contact-failure.ts +21 -0
  63. package/src/providers/linkedin-contact-platform.ts +8 -1
  64. package/src/providers/linkedin-contact-program.ts +34 -5
  65. package/src/providers/linkedin-web-contact.ts +1023 -51
  66. package/src/providers/linkedin-web-profile-browser.ts +659 -45
  67. package/src/providers/linkedin-web-runtime.ts +1 -0
  68. package/src/providers/linkedin-web.ts +46 -2
  69. package/src/providers/messaging-native-artifacts.ts +38 -0
  70. package/src/providers/messaging-native-install.ts +75 -0
  71. package/src/providers/whatsapp-automation-runtime.ts +240 -0
  72. package/src/providers/whatsapp-automation.ts +153 -0
  73. package/src/usage.ts +3 -1
  74. package/src/version.ts +1 -1
  75. package/src/web-session-contract-definitions.ts +1 -1
  76. package/tsconfig.json +3 -0
@@ -12,8 +12,12 @@ import {
12
12
  } from "../browser";
13
13
  import type { GhostgetManifest } from "../model";
14
14
  import {
15
+ assertLinkedInContactInfoRequest,
16
+ buildLinkedInProfileContactDetailsNavigationPostPath,
15
17
  buildLinkedInProfileContactInfoGraphqlPath,
18
+ buildLinkedInProfileContactInfoOverlayPath,
16
19
  type LinkedInContactInfoJsonInput,
20
+ type LinkedInContactNavigationInput,
17
21
  } from "./linkedin-web-contact";
18
22
  import type {
19
23
  WebSessionCleanupResourcePublisher,
@@ -55,6 +59,8 @@ export type LinkedInProfileBrowserTransport = {
55
59
  readonly readProfileHtml: (profileUrl: string) => Promise<string>;
56
60
  readonly readConnectionsHtml: (profileUrl: string) => Promise<string>;
57
61
  readonly readContactInfoJson: (input: LinkedInContactInfoJsonInput) => Promise<unknown>;
62
+ readonly readContactOverlayText: (input: LinkedInContactInfoJsonInput) => Promise<string>;
63
+ readonly readContactNavigationText: (input: LinkedInContactNavigationInput) => Promise<string>;
58
64
  readonly readOrganizationHtml: (organizationUrl: string) => Promise<string>;
59
65
  readonly close: () => Promise<void>;
60
66
  };
@@ -66,14 +72,54 @@ export type LinkedInProfileBrowserDependencies = {
66
72
  };
67
73
 
68
74
  type BrowserReadBinding = {
69
- readonly kind: "html" | "json";
75
+ readonly kind: "html" | "json" | "rsc" | "rsc-action";
70
76
  readonly maxBytes: number;
71
77
  readonly path: string;
72
78
  readonly referrer: string;
79
+ readonly body?: string;
80
+ readonly documentUrl?: string;
81
+ readonly pageInstance?: string;
82
+ readonly track?: string;
83
+ readonly applicationVersion?: string;
84
+ readonly applicationInstance?: string;
85
+ readonly anchorPageKey?: string;
86
+ readonly rscStream?: string;
87
+ readonly pageInstanceTrackingId?: string;
88
+ readonly pageforestId?: string;
89
+ readonly traceparent?: string;
90
+ readonly tracestate?: string;
91
+ readonly layoutTree?: string;
73
92
  };
74
93
 
94
+ type LinkedInProfilePageBindings = {
95
+ readonly pageInstance: string;
96
+ readonly track?: string;
97
+ readonly applicationVersion?: string;
98
+ readonly applicationInstance?: string;
99
+ readonly anchorPageKey?: string;
100
+ readonly rscStream?: "true";
101
+ readonly pageInstanceTrackingId?: string;
102
+ readonly pageforestId?: string;
103
+ readonly traceparent?: string;
104
+ readonly tracestate?: string;
105
+ readonly layoutTree?: string;
106
+ };
107
+
108
+ type LinkedInProfileNetworkRoute = "rsc-action" | "voyager";
109
+
75
110
  type BrowserReadState = "ready" | "identity" | "profile" | "complete";
76
111
 
112
+ const PROFILE_PAGE_INSTANCE_PATTERN =
113
+ /^urn:li:page:d_flagship3_profile[A-Za-z0-9_:-]{0,128};[A-Za-z0-9+/=_-]{1,512}$/u;
114
+ const PROFILE_OBSERVED_ID_PATTERN = /^[A-Za-z0-9+/=._-]{1,512}$/u;
115
+ const PROFILE_TRACEPARENT_PATTERN =
116
+ /^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/u;
117
+ const PROFILE_TRACESTATE_PATTERN = /^[A-Za-z0-9=,_.*@/+-]{1,512}$/u;
118
+ const PROFILE_LAYOUT_TREE_PATTERN = /^[\x20-\x7E]{1,8192}$/u;
119
+ const PROFILE_PAGE_WAIT_MS = 5_000;
120
+ const PROFILE_PAGE_CONTEXT_EXTRACT_SOURCE =
121
+ `(async()=>{if(location.origin!=="${LINKEDIN_ORIGIN}")throw new Error("unexpected LinkedIn origin");if(/^\\/(?:authwall|checkpoint|login|uas\\/login(?:-submit)?)(?:\\/|$)/u.test(location.pathname))throw new Error("LinkedIn stats browser reached the signed-out authwall");const root=document.documentElement;if(root===null)throw new Error("LinkedIn profile document omitted its root");const html=root.outerHTML;if(typeof html!=="string"||html.length<1||html.length>${MAX_STATS_PAGE_BYTES})throw new Error("LinkedIn profile document changed shape");const matches=html.match(/urn:li:page:d_flagship3_profile[A-Za-z0-9_:-]{0,128};[A-Za-z0-9+/=_-]{1,512}/g)||[];const unique=[];for(const value of matches){if(!unique.includes(value))unique.push(value);if(unique.length>1)throw new Error("LinkedIn profile document page-instance binding is ambiguous")}return{href:location.href,pageInstance:unique[0]??null}})()`;
122
+
77
123
  const LINKEDIN_RESPONSE_MEDIA_TYPE =
78
124
  /^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/u;
79
125
 
@@ -86,6 +132,7 @@ export type LinkedInProfileBrowserFailureCategory =
86
132
  | "execution-context"
87
133
  | "identity-json"
88
134
  | "output-bound"
135
+ | "page-binding"
89
136
  | "provider-fetch"
90
137
  | "response-envelope"
91
138
  | "response-rejected"
@@ -163,9 +210,325 @@ function exactLinkedInUrl(
163
210
  return url;
164
211
  }
165
212
 
213
+ function boundedProfileHeader(value: unknown, label: string, maximum: number): string {
214
+ if (
215
+ typeof value !== "string"
216
+ || value.length < 1
217
+ || value.length > maximum
218
+ || /[\0\r\n]/u.test(value)
219
+ ) throw new LinkedInProfileBrowserFailure(
220
+ "page-binding",
221
+ `${label} changed its reviewed bound`,
222
+ );
223
+ return value;
224
+ }
225
+
226
+ function linkedInProfilePageInstance(value: unknown): string {
227
+ const pageInstance = boundedProfileHeader(
228
+ value,
229
+ "LinkedIn profile x-li-page-instance binding",
230
+ 768,
231
+ );
232
+ if (!PROFILE_PAGE_INSTANCE_PATTERN.test(pageInstance)) {
233
+ throw new LinkedInProfileBrowserFailure(
234
+ "page-binding",
235
+ "LinkedIn stats browser omitted its reviewed profile page-instance binding",
236
+ );
237
+ }
238
+ return pageInstance;
239
+ }
240
+
241
+ function linkedInProfileTrack(value: unknown): string {
242
+ const track = boundedProfileHeader(value, "LinkedIn profile x-li-track binding", 4_096);
243
+ let parsed: unknown;
244
+ try {
245
+ parsed = JSON.parse(track) as unknown;
246
+ } catch {
247
+ throw new LinkedInProfileBrowserFailure(
248
+ "page-binding",
249
+ "LinkedIn profile x-li-track binding changed shape",
250
+ );
251
+ }
252
+ if (
253
+ !isRecord(parsed)
254
+ || (parsed.mpName !== "voyager-web" && parsed.mpName !== "web")
255
+ ) {
256
+ throw new LinkedInProfileBrowserFailure(
257
+ "page-binding",
258
+ "LinkedIn profile x-li-track binding changed shape",
259
+ );
260
+ }
261
+ return track;
262
+ }
263
+
264
+ function observedHeader(
265
+ headers: Readonly<Record<string, unknown>>,
266
+ name: string,
267
+ ): unknown {
268
+ if (Object.hasOwn(headers, name)) return headers[name];
269
+ const wanted = name.toLowerCase();
270
+ for (const [key, value] of Object.entries(headers)) {
271
+ if (key.toLowerCase() === wanted) return value;
272
+ }
273
+ return undefined;
274
+ }
275
+
276
+ function linkedInProfileApplicationVersion(value: unknown): string {
277
+ const version = boundedProfileHeader(
278
+ value,
279
+ "LinkedIn profile x-li-application-version binding",
280
+ 64,
281
+ );
282
+ if (!/^[0-9]+(?:\.[0-9A-Za-z_-]+){1,8}$/u.test(version)) {
283
+ throw new LinkedInProfileBrowserFailure(
284
+ "page-binding",
285
+ "LinkedIn profile x-li-application-version binding changed shape",
286
+ );
287
+ }
288
+ return version;
289
+ }
290
+
291
+ function linkedInProfileApplicationInstance(value: unknown): string {
292
+ const instance = boundedProfileHeader(
293
+ value,
294
+ "LinkedIn profile x-li-application-instance binding",
295
+ 512,
296
+ );
297
+ if (!/^[A-Za-z0-9+/=_-]{1,512}$/u.test(instance)) {
298
+ throw new LinkedInProfileBrowserFailure(
299
+ "page-binding",
300
+ "LinkedIn profile x-li-application-instance binding changed shape",
301
+ );
302
+ }
303
+ return instance;
304
+ }
305
+
306
+ function linkedInProfileAnchorPageKey(value: unknown): string {
307
+ const key = boundedProfileHeader(
308
+ value,
309
+ "LinkedIn profile x-li-anchor-page-key binding",
310
+ 160,
311
+ );
312
+ if (!/^d_flagship3_profile[A-Za-z0-9_-]{0,128}$/u.test(key)) {
313
+ throw new LinkedInProfileBrowserFailure(
314
+ "page-binding",
315
+ "LinkedIn profile x-li-anchor-page-key binding changed shape",
316
+ );
317
+ }
318
+ return key;
319
+ }
320
+
321
+ function linkedInProfileObservedId(value: unknown, label: string): string {
322
+ const observed = boundedProfileHeader(value, label, 512);
323
+ if (!PROFILE_OBSERVED_ID_PATTERN.test(observed)) {
324
+ throw new LinkedInProfileBrowserFailure(
325
+ "page-binding",
326
+ `${label} changed shape`,
327
+ );
328
+ }
329
+ return observed;
330
+ }
331
+
332
+ function linkedInProfileTraceparent(value: unknown): string {
333
+ const traceparent = boundedProfileHeader(
334
+ value,
335
+ "LinkedIn profile x-li-traceparent binding",
336
+ 55,
337
+ );
338
+ if (!PROFILE_TRACEPARENT_PATTERN.test(traceparent)) {
339
+ throw new LinkedInProfileBrowserFailure(
340
+ "page-binding",
341
+ "LinkedIn profile x-li-traceparent binding changed shape",
342
+ );
343
+ }
344
+ return traceparent;
345
+ }
346
+
347
+ function linkedInProfileTracestate(value: unknown): string {
348
+ const tracestate = boundedProfileHeader(
349
+ value,
350
+ "LinkedIn profile x-li-tracestate binding",
351
+ 512,
352
+ );
353
+ if (!PROFILE_TRACESTATE_PATTERN.test(tracestate)) {
354
+ throw new LinkedInProfileBrowserFailure(
355
+ "page-binding",
356
+ "LinkedIn profile x-li-tracestate binding changed shape",
357
+ );
358
+ }
359
+ return tracestate;
360
+ }
361
+
362
+ function linkedInProfileLayoutTree(value: unknown): string {
363
+ const layoutTree = boundedProfileHeader(
364
+ value,
365
+ "LinkedIn profile x-li-layout-tree binding",
366
+ 8_192,
367
+ );
368
+ if (!PROFILE_LAYOUT_TREE_PATTERN.test(layoutTree)) {
369
+ throw new LinkedInProfileBrowserFailure(
370
+ "page-binding",
371
+ "LinkedIn profile x-li-layout-tree binding changed shape",
372
+ );
373
+ }
374
+ return layoutTree;
375
+ }
376
+
377
+ function linkedInProfileCopiedHeaders(
378
+ headers: Readonly<Record<string, unknown>>,
379
+ ): Omit<LinkedInProfilePageBindings, "pageInstance"> {
380
+ const trackValue = observedHeader(headers, "x-li-track");
381
+ const applicationVersion = observedHeader(headers, "x-li-application-version");
382
+ const applicationInstance = observedHeader(headers, "x-li-application-instance");
383
+ const anchorPageKey = observedHeader(headers, "x-li-anchor-page-key");
384
+ const rscStream = observedHeader(headers, "x-li-rsc-stream");
385
+ const pageInstanceTrackingId = observedHeader(
386
+ headers,
387
+ "x-li-page-instance-tracking-id",
388
+ );
389
+ const pageforestId = observedHeader(headers, "x-li-pageforestid");
390
+ const traceparent = observedHeader(headers, "x-li-traceparent");
391
+ const tracestate = observedHeader(headers, "x-li-tracestate");
392
+ const layoutTree = observedHeader(headers, "x-li-layout-tree");
393
+ return Object.freeze({
394
+ ...(typeof trackValue === "string" ? { track: linkedInProfileTrack(trackValue) } : {}),
395
+ ...(typeof applicationVersion === "string"
396
+ ? { applicationVersion: linkedInProfileApplicationVersion(applicationVersion) }
397
+ : {}),
398
+ ...(typeof applicationInstance === "string"
399
+ ? { applicationInstance: linkedInProfileApplicationInstance(applicationInstance) }
400
+ : {}),
401
+ ...(typeof anchorPageKey === "string"
402
+ ? { anchorPageKey: linkedInProfileAnchorPageKey(anchorPageKey) }
403
+ : {}),
404
+ ...(rscStream === "true" ? { rscStream: "true" as const } : {}),
405
+ ...(typeof pageInstanceTrackingId === "string"
406
+ ? {
407
+ pageInstanceTrackingId: linkedInProfileObservedId(
408
+ pageInstanceTrackingId,
409
+ "LinkedIn profile x-li-page-instance-tracking-id binding",
410
+ ),
411
+ }
412
+ : {}),
413
+ ...(typeof pageforestId === "string"
414
+ ? {
415
+ pageforestId: linkedInProfileObservedId(
416
+ pageforestId,
417
+ "LinkedIn profile x-li-pageforestid binding",
418
+ ),
419
+ }
420
+ : {}),
421
+ ...(typeof traceparent === "string"
422
+ ? { traceparent: linkedInProfileTraceparent(traceparent) }
423
+ : {}),
424
+ ...(typeof tracestate === "string"
425
+ ? { tracestate: linkedInProfileTracestate(tracestate) }
426
+ : {}),
427
+ ...(typeof layoutTree === "string"
428
+ ? { layoutTree: linkedInProfileLayoutTree(layoutTree) }
429
+ : {}),
430
+ });
431
+ }
432
+
433
+ function linkedInProfileNetworkBindings(
434
+ value: unknown,
435
+ route: LinkedInProfileNetworkRoute,
436
+ ): LinkedInProfilePageBindings | null {
437
+ if (!isRecord(value) || !Array.isArray(value.requests) || value.requests.length > 10_000) {
438
+ throw new LinkedInProfileBrowserFailure(
439
+ "page-binding",
440
+ "LinkedIn profile network observation changed shape",
441
+ );
442
+ }
443
+ const pathPrefix = route === "rsc-action"
444
+ ? "/flagship-web/rsc-action/"
445
+ : "/voyager/api/";
446
+ let selected: LinkedInProfilePageBindings | null = null;
447
+ for (const item of value.requests) {
448
+ if (!isRecord(item) || !isRecord(item.headers)) continue;
449
+ if (
450
+ (route === "voyager" ? item.method !== "GET" : item.method !== "GET" && item.method !== "POST")
451
+ || item.status !== 200
452
+ || typeof item.url !== "string"
453
+ || item.url.length > 64 * 1_024
454
+ ) continue;
455
+ let url: URL;
456
+ try {
457
+ url = new URL(item.url);
458
+ } catch {
459
+ continue;
460
+ }
461
+ if (
462
+ url.origin !== LINKEDIN_ORIGIN
463
+ || url.username !== ""
464
+ || url.password !== ""
465
+ || !url.pathname.startsWith(pathPrefix)
466
+ ) continue;
467
+ const pageInstanceValue = observedHeader(item.headers, "x-li-page-instance");
468
+ if (
469
+ typeof pageInstanceValue !== "string"
470
+ || !PROFILE_PAGE_INSTANCE_PATTERN.test(pageInstanceValue)
471
+ ) continue;
472
+ selected = Object.freeze({
473
+ pageInstance: linkedInProfilePageInstance(pageInstanceValue),
474
+ ...linkedInProfileCopiedHeaders(item.headers),
475
+ });
476
+ }
477
+ return selected;
478
+ }
479
+
480
+ function linkedInProfileDocumentBindings(
481
+ value: unknown,
482
+ profileHref: string,
483
+ ): { readonly pageInstance: string | null } {
484
+ if (!isRecord(value) || typeof value.href !== "string") {
485
+ throw new LinkedInProfileBrowserFailure(
486
+ "page-binding",
487
+ "LinkedIn profile document omitted its page-instance binding",
488
+ );
489
+ }
490
+ let href: URL;
491
+ let expected: URL;
492
+ try {
493
+ href = new URL(value.href);
494
+ expected = new URL(profileHref);
495
+ } catch {
496
+ throw new LinkedInProfileBrowserFailure(
497
+ "page-binding",
498
+ "LinkedIn profile document omitted its page-instance binding",
499
+ );
500
+ }
501
+ const normalize = (path: string): string => path.endsWith("/") ? path : `${path}/`;
502
+ if (
503
+ href.origin !== expected.origin
504
+ || href.username !== ""
505
+ || href.password !== ""
506
+ || href.search !== ""
507
+ || href.hash !== ""
508
+ || normalize(href.pathname) !== normalize(expected.pathname)
509
+ ) {
510
+ throw new LinkedInProfileBrowserFailure(
511
+ "bootstrap",
512
+ "LinkedIn stats browser left its bound profile document",
513
+ );
514
+ }
515
+ if (value.pageInstance === null) return { pageInstance: null };
516
+ return { pageInstance: linkedInProfilePageInstance(value.pageInstance) };
517
+ }
518
+
519
+ const CONTACT_INFO_CONTROL_NAME = "Contact info";
520
+
521
+ function contactModalEvaluationSource(documentUrl: string): string {
522
+ const overlayPath = buildLinkedInProfileContactInfoOverlayPath({
523
+ profileUrl: documentUrl,
524
+ });
525
+ const bound = jsonScriptLiteral({ documentUrl, overlayPath });
526
+ return `(async()=>{const modalInput=${bound};if(location.origin!=="${LINKEDIN_ORIGIN}")throw new Error("unexpected LinkedIn origin");if(typeof modalInput.documentUrl!=="string"||typeof modalInput.overlayPath!=="string")throw new Error("LinkedIn stats browser left its bound profile document");const documentUrl=new URL(modalInput.documentUrl);const here=new URL(location.href);const normalize=(path)=>path.endsWith("/")?path:path+"/";const profilePath=normalize(documentUrl.pathname);const overlayPath=normalize(modalInput.overlayPath);if(overlayPath!==profilePath+"overlay/contact-info/")throw new Error("LinkedIn stats browser left its bound profile document");if(/^\\/(?:authwall|checkpoint|login|uas\\/login(?:-submit)?)(?:\\/|$)/u.test(here.pathname))throw new Error("LinkedIn stats browser reached the signed-out authwall");if(here.origin!==documentUrl.origin||here.username!==""||here.password!==""||normalize(here.pathname)!==profilePath)throw new Error("LinkedIn stats browser left its bound profile document");const nameOf=(el)=>{const labelled=el.getAttribute("aria-label");if(typeof labelled==="string"&&labelled.trim())return labelled.replace(/\\s+/g," ").trim();return((el.textContent||"").replace(/\\s+/g," ").trim())};const matches=[];for(const el of document.querySelectorAll('a,button,[role="button"],[role="link"]')){if(nameOf(el)==="${CONTACT_INFO_CONTROL_NAME}")matches.push(el)}if(matches.length===0)throw new Error("LinkedIn stats browser omitted its reviewed Contact-info control");if(matches.length!==1)throw new Error("LinkedIn stats browser Contact-info control was ambiguous");matches[0].click();const deadline=Date.now()+8000;let dialog=null;while(Date.now()<deadline){const after=new URL(location.href);if(/^\\/(?:authwall|checkpoint|login|uas\\/login(?:-submit)?)(?:\\/|$)/u.test(after.pathname))throw new Error("LinkedIn stats browser reached the signed-out authwall");if(after.origin!==documentUrl.origin||after.username!==""||after.password!==""||(normalize(after.pathname)!==profilePath&&normalize(after.pathname)!==overlayPath))throw new Error("LinkedIn stats browser left its bound profile document");const found=document.querySelectorAll('[role="dialog"],dialog,[aria-modal="true"]');if(found.length===1){dialog=found[0];break}if(found.length>1){const withEmail=[];for(const node of found){const text=node.innerText||"";const html=node.innerHTML||"";if(/Email/u.test(text)||/mailto:/iu.test(text)||/mailto:/iu.test(html))withEmail.push(node)}if(withEmail.length===1){dialog=withEmail[0];break}if(withEmail.length>1)throw new Error("LinkedIn stats browser Contact-info modal was ambiguous")}await new Promise((resolve)=>setTimeout(resolve,50))}if(dialog===null)throw new Error("LinkedIn stats browser omitted its Contact-info modal");const html=dialog.outerHTML;if(typeof html!=="string"||html.length<1||html.length>${MAX_STATS_PAGE_BYTES})throw new Error("LinkedIn stats browser Contact-info modal changed shape");const bytes=new TextEncoder().encode(html);const digest=Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256",bytes)),(value)=>value.toString(16).padStart(2,"0")).join("");let binary="";for(let offset=0;offset<bytes.length;offset+=32768)binary+=String.fromCharCode(...bytes.subarray(offset,Math.min(offset+32768,bytes.length)));return{authWall:false,bodyBase64:btoa(binary),bodyBytes:bytes.byteLength,bodySha256:digest,contentType:"text/html",status:200}})()`;
527
+ }
528
+
166
529
  function browserReadEvaluationSource(binding: BrowserReadBinding): string {
167
530
  const bound = jsonScriptLiteral(binding);
168
- return `(async()=>{const input=${bound};if(location.origin!=="${LINKEDIN_ORIGIN}")throw new Error("unexpected LinkedIn origin");if((input.kind!=="json"&&input.kind!=="html")||!Number.isSafeInteger(input.maxBytes)||input.maxBytes<1||input.maxBytes>${MAX_STATS_PAGE_BYTES})throw new Error("invalid LinkedIn stats browser request binding");const expected=new URL(input.path,"${LINKEDIN_ORIGIN}");if(expected.origin!=="${LINKEDIN_ORIGIN}"||expected.username!==""||expected.password!==""||expected.hash!==""||expected.href!=="${LINKEDIN_ORIGIN}"+input.path)throw new Error("invalid LinkedIn stats browser path binding");const headers=input.kind==="json"?{accept:"application/vnd.linkedin.normalized+json+2.1","x-li-lang":"en_US","x-requested-with":"XMLHttpRequest","x-restli-protocol-version":"2.0.0"}:{accept:"text/html"};if(input.kind==="json"){const raw=document.cookie.split("; ").find((part)=>part.startsWith("JSESSIONID="));if(typeof raw!=="string")throw new Error("missing LinkedIn browser CSRF cookie");const csrf=decodeURIComponent(raw.slice("JSESSIONID=".length)).replace(/^\"|\"$/g,"");if(!/^ajax:[A-Za-z0-9_-]{1,512}$/.test(csrf))throw new Error("invalid LinkedIn browser CSRF cookie");headers["csrf-token"]=csrf}const response=await fetch(input.path,{credentials:"include",headers,method:"GET",redirect:"error",referrer:input.referrer});const responseUrl=new URL(response.url);if(responseUrl.origin!=="${LINKEDIN_ORIGIN}"||responseUrl.username!==""||responseUrl.password!==""||responseUrl.hash!==""||responseUrl.href!==expected.href)throw new Error("LinkedIn stats browser response escaped its exact route");const contentType=(response.headers.get("content-type")||"").split(";",1)[0].trim().toLowerCase();const contentTypeAllowed=input.kind==="json"?(contentType==="application/vnd.linkedin.normalized+json+2.1"||contentType==="application/json"):contentType==="text/html";if(response.status!==200||!contentTypeAllowed){response.body?.cancel();return{authWall:false,bodyBase64:null,bodyBytes:0,bodySha256:null,contentType,status:response.status}}if(response.body===null)throw new Error("LinkedIn stats browser response omitted its body");const reader=response.body.getReader();const chunks=[];let bytes=0;while(true){const part=await reader.read();if(part.done)break;bytes+=part.value.byteLength;if(bytes>input.maxBytes){await reader.cancel();throw new Error("LinkedIn stats browser response exceeded its reviewed byte bound")}chunks.push(part.value)}const body=new Uint8Array(bytes);let cursor=0;for(const chunk of chunks){body.set(chunk,cursor);cursor+=chunk.byteLength}const text=new TextDecoder("utf-8",{fatal:true}).decode(body);const authWall=input.kind==="html"&&/(?:id|data-test-id)=[\"']authwall[\"']|name=[\"']loginCsrfParam[\"']|<form[^>]+(?:login|sign-in)/iu.test(text);if(authWall)return{authWall:true,bodyBase64:null,bodyBytes:0,bodySha256:null,contentType,status:response.status};const digest=Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256",body)),(value)=>value.toString(16).padStart(2,"0")).join("");let binary="";for(let offset=0;offset<body.length;offset+=32768)binary+=String.fromCharCode(...body.subarray(offset,Math.min(offset+32768,body.length)));return{authWall:false,bodyBase64:btoa(binary),bodyBytes:body.byteLength,bodySha256:digest,contentType,status:response.status}})()`;
531
+ return `(async()=>{const input=${bound};if(location.origin!=="${LINKEDIN_ORIGIN}")throw new Error("unexpected LinkedIn origin");if((input.kind!=="json"&&input.kind!=="html"&&input.kind!=="rsc"&&input.kind!=="rsc-action")||!Number.isSafeInteger(input.maxBytes)||input.maxBytes<1||input.maxBytes>${MAX_STATS_PAGE_BYTES}||(input.kind==="rsc-action"?typeof input.body!=="string"||input.body.length<1||input.body.length>4096:input.body!==undefined))throw new Error("invalid LinkedIn stats browser request binding");const expected=new URL(input.path,"${LINKEDIN_ORIGIN}");if(expected.origin!=="${LINKEDIN_ORIGIN}"||expected.username!==""||expected.password!==""||expected.hash!==""||expected.href!=="${LINKEDIN_ORIGIN}"+input.path)throw new Error("invalid LinkedIn stats browser path binding");const headers=input.kind==="json"?{accept:"application/vnd.linkedin.normalized+json+2.1","x-li-lang":"en_US","x-requested-with":"XMLHttpRequest","x-restli-protocol-version":"2.0.0"}:input.kind==="rsc"?{accept:"text/x-component","x-li-lang":"en_US","x-requested-with":"XMLHttpRequest",RSC:"1"}:input.kind==="rsc-action"?{accept:"*/*","content-type":"application/json","x-li-lang":"en_US"}:{accept:"text/html"};if(input.kind==="json"||input.kind==="rsc"||input.kind==="rsc-action"){const raw=document.cookie.split("; ").find((part)=>part.startsWith("JSESSIONID="));if(typeof raw!=="string")throw new Error("missing LinkedIn browser CSRF cookie");const csrf=decodeURIComponent(raw.slice("JSESSIONID=".length)).replace(/^\"|\"$/g,"");if(!/^ajax:[A-Za-z0-9_-]{1,512}$/.test(csrf))throw new Error("invalid LinkedIn browser CSRF cookie");headers["csrf-token"]=csrf}if(input.kind==="rsc-action"){if(typeof input.documentUrl!=="string")throw new Error("LinkedIn stats browser left its bound profile document");const documentUrl=new URL(input.documentUrl);const here=new URL(location.href);const normalize=(path)=>path.endsWith("/")?path:path+"/";if(/^\\/(?:authwall|checkpoint|login|uas\\/login(?:-submit)?)(?:\\/|$)/u.test(here.pathname))throw new Error("LinkedIn stats browser reached the signed-out authwall");if(here.origin!==documentUrl.origin||here.username!==""||here.password!==""||here.search!==""||here.hash!==""||normalize(here.pathname)!==normalize(documentUrl.pathname))throw new Error("LinkedIn stats browser left its bound profile document");if(typeof input.pageInstance!=="string"||!/^urn:li:page:d_flagship3_profile[A-Za-z0-9_:-]{0,128};[A-Za-z0-9+/=_-]{1,512}$/.test(input.pageInstance))throw new Error("missing LinkedIn browser page instance");headers["x-li-page-instance"]=input.pageInstance;if(input.track!==undefined){if(typeof input.track!=="string"||input.track.length<1||input.track.length>4096||/[\\0\\r\\n]/.test(input.track))throw new Error("invalid LinkedIn browser track binding");headers["x-li-track"]=input.track}if(input.applicationVersion!==undefined){if(typeof input.applicationVersion!=="string"||!/^[0-9]+(?:[.][0-9A-Za-z_-]+){1,8}$/.test(input.applicationVersion)||input.applicationVersion.length>64)throw new Error("invalid LinkedIn browser application version");headers["x-li-application-version"]=input.applicationVersion}if(input.applicationInstance!==undefined){if(typeof input.applicationInstance!=="string"||!/^[A-Za-z0-9+/=_-]{1,512}$/.test(input.applicationInstance))throw new Error("invalid LinkedIn browser application instance");headers["x-li-application-instance"]=input.applicationInstance}if(input.anchorPageKey!==undefined){if(typeof input.anchorPageKey!=="string"||!/^d_flagship3_profile[A-Za-z0-9_-]{0,128}$/.test(input.anchorPageKey))throw new Error("invalid LinkedIn browser anchor page key");headers["x-li-anchor-page-key"]=input.anchorPageKey}if(input.rscStream!==undefined){if(input.rscStream!=="true")throw new Error("invalid LinkedIn browser rsc stream");headers["x-li-rsc-stream"]="true"}if(input.pageInstanceTrackingId!==undefined){if(typeof input.pageInstanceTrackingId!=="string"||!/^[A-Za-z0-9+/=._-]{1,512}$/.test(input.pageInstanceTrackingId))throw new Error("invalid LinkedIn browser page-instance tracking id");headers["x-li-page-instance-tracking-id"]=input.pageInstanceTrackingId}if(input.pageforestId!==undefined){if(typeof input.pageforestId!=="string"||!/^[A-Za-z0-9+/=._-]{1,512}$/.test(input.pageforestId))throw new Error("invalid LinkedIn browser pageforest id");headers["x-li-pageforestid"]=input.pageforestId}if(input.traceparent!==undefined){if(typeof input.traceparent!=="string"||!/^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/.test(input.traceparent))throw new Error("invalid LinkedIn browser traceparent");headers["x-li-traceparent"]=input.traceparent}if(input.tracestate!==undefined){if(typeof input.tracestate!=="string"||!/^[A-Za-z0-9=,_.*@/+-]{1,512}$/.test(input.tracestate))throw new Error("invalid LinkedIn browser tracestate");headers["x-li-tracestate"]=input.tracestate}if(input.layoutTree!==undefined){if(typeof input.layoutTree!=="string"||input.layoutTree.length<1||input.layoutTree.length>8192||/[\\0\\r\\n]/.test(input.layoutTree)||!/^[\\x20-\\x7E]+$/.test(input.layoutTree))throw new Error("invalid LinkedIn browser layout tree");headers["x-li-layout-tree"]=input.layoutTree}}const response=await fetch(input.path,{credentials:"include",headers,method:input.kind==="rsc-action"?"POST":"GET",redirect:"error",referrer:input.referrer,...(input.kind==="rsc-action"?{body:input.body}:{})});const responseUrl=new URL(response.url);if(responseUrl.origin!=="${LINKEDIN_ORIGIN}"||responseUrl.username!==""||responseUrl.password!==""||responseUrl.hash!==""||responseUrl.href!==expected.href)throw new Error("LinkedIn stats browser response escaped its exact route");const contentType=(response.headers.get("content-type")||"").split(";",1)[0].trim().toLowerCase();const contentTypeAllowed=input.kind==="json"?(contentType==="application/vnd.linkedin.normalized+json+2.1"||contentType==="application/json"):input.kind==="rsc"?(contentType==="text/x-component"||contentType==="text/html"||contentType==="text/plain"):input.kind==="rsc-action"?(contentType==="application/octet-stream"||contentType==="text/x-component"||contentType==="text/html"||contentType==="text/plain"):contentType==="text/html";if(response.status!==200||!contentTypeAllowed){response.body?.cancel();return{authWall:false,bodyBase64:null,bodyBytes:0,bodySha256:null,contentType,status:response.status}}if(response.body===null)throw new Error("LinkedIn stats browser response omitted its body");const reader=response.body.getReader();const chunks=[];let bytes=0;while(true){const part=await reader.read();if(part.done)break;bytes+=part.value.byteLength;if(bytes>input.maxBytes){await reader.cancel();throw new Error("LinkedIn stats browser response exceeded its reviewed byte bound")}chunks.push(part.value)}const body=new Uint8Array(bytes);let cursor=0;for(const chunk of chunks){body.set(chunk,cursor);cursor+=chunk.byteLength}const text=new TextDecoder("utf-8",{fatal:true}).decode(body);const authWall=(input.kind==="html"||input.kind==="rsc"||input.kind==="rsc-action")&&/(?:id|data-test-id)=[\"']authwall[\"']|name=[\"']loginCsrfParam[\"']|<form[^>]+(?:login|sign-in)/iu.test(text);if(authWall)return{authWall:true,bodyBase64:null,bodyBytes:0,bodySha256:null,contentType,status:response.status};const digest=Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256",body)),(value)=>value.toString(16).padStart(2,"0")).join("");let binary="";for(let offset=0;offset<body.length;offset+=32768)binary+=String.fromCharCode(...body.subarray(offset,Math.min(offset+32768,body.length)));return{authWall:false,bodyBase64:btoa(binary),bodyBytes:body.byteLength,bodySha256:digest,contentType,status:response.status}})()`;
169
532
  }
170
533
 
171
534
  function encodedBodyBound(bytes: number): number {
@@ -253,12 +616,21 @@ function hasNoDefaultExecutionContext(error: unknown): boolean {
253
616
  }
254
617
 
255
618
  function hasUnexpectedLinkedInOrigin(error: unknown): boolean {
619
+ return matchesLinkedInBrowserEvalError(error, "unexpected LinkedIn origin");
620
+ }
621
+
622
+ function escapeLinkedInBrowserEvalPhrase(phrase: string): string {
623
+ return phrase.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
624
+ }
625
+
626
+ function matchesLinkedInBrowserEvalError(error: unknown, phrase: string): boolean {
627
+ const pattern = new RegExp(
628
+ `(?:^|: )${escapeLinkedInBrowserEvalPhrase(phrase)}(?:$|[\\r\\n]| +at )`,
629
+ "u",
630
+ );
256
631
  let current: unknown = error;
257
632
  for (let depth = 0; depth < 8 && current !== undefined; depth += 1) {
258
- if (
259
- current instanceof Error
260
- && /(?:^|: )unexpected LinkedIn origin(?:$|[\r\n])/u.test(current.message)
261
- ) return true;
633
+ if (current instanceof Error && pattern.test(current.message)) return true;
262
634
  current = current instanceof Error ? current.cause : undefined;
263
635
  }
264
636
  return false;
@@ -424,6 +796,7 @@ export async function createLinkedInProfileBrowserTransport(
424
796
  + BROWSER_ENVELOPE_BYTES;
425
797
  const sessionOptions: CreateBrowserSessionOptions = {
426
798
  allowCodeOwnedEvaluation: true,
799
+ allowCodeOwnedNetworkObservation: true,
427
800
  headed: true,
428
801
  maxOutputBytes: browserOutputBytes,
429
802
  timeoutMs: options.timeoutMs,
@@ -459,41 +832,156 @@ export async function createLinkedInProfileBrowserTransport(
459
832
 
460
833
  const remainingTimeMs = (): number =>
461
834
  options.operationDeadline?.remainingTimeMs() ?? options.timeoutMs;
462
- const run = async (
463
- binding: BrowserReadBinding,
464
- ): Promise<Readonly<Record<string, unknown>>> => {
465
- if (closed) throw new Error("LinkedIn stats browser transport is closed");
466
- const source = browserReadEvaluationSource(binding);
467
- let records: readonly Readonly<Record<string, unknown>>[];
468
- try {
469
- records = await session.runBatch(
470
- [["eval", source]],
471
- remainingTimeMs(),
472
- Math.min(
473
- encodedBodyBound(binding.maxBytes) + BROWSER_ENVELOPE_BYTES,
474
- browserOutputBytes,
475
- ),
835
+ const classifySessionError = (error: unknown): never => {
836
+ if (error instanceof PreservedBrowserArtifactsError) throw error;
837
+ if (error instanceof LinkedInProfileBrowserFailure) throw error;
838
+ options.operationDeadline?.throwIfUnavailable(
839
+ "LinkedIn stats browser operation",
840
+ );
841
+ if (hasNoDefaultExecutionContext(error)) {
842
+ throw new LinkedInProfileBrowserFailure(
843
+ "execution-context",
844
+ "LinkedIn stats browser lost its reviewed execution context",
476
845
  );
477
- } catch (error) {
478
- if (error instanceof PreservedBrowserArtifactsError) throw error;
479
- if (error instanceof LinkedInProfileBrowserFailure) throw error;
480
- options.operationDeadline?.throwIfUnavailable(
481
- "LinkedIn stats browser operation",
846
+ }
847
+ if (hasUnexpectedLinkedInOrigin(error)) {
848
+ throw new LinkedInProfileBrowserFailure(
849
+ "bootstrap",
850
+ "LinkedIn stats browser was not on its reviewed signed-in origin",
851
+ );
852
+ }
853
+ if (matchesLinkedInBrowserEvalError(error, "LinkedIn stats browser left its bound profile document")) {
854
+ throw new LinkedInProfileBrowserFailure(
855
+ "bootstrap",
856
+ "LinkedIn stats browser left its bound profile document",
482
857
  );
483
- if (hasNoDefaultExecutionContext(error)) {
858
+ }
859
+ if (matchesLinkedInBrowserEvalError(error, "LinkedIn stats browser reached the signed-out authwall")) {
860
+ throw new LinkedInProfileBrowserFailure(
861
+ "authwall",
862
+ "LinkedIn stats browser reached the signed-out authwall",
863
+ );
864
+ }
865
+ if (
866
+ error instanceof Error
867
+ && /(?:^|: )(?:missing LinkedIn browser page instance|invalid LinkedIn browser (?:track binding|application version|application instance|anchor page key|rsc stream|page-instance tracking id|pageforest id|traceparent|tracestate|layout tree)|LinkedIn profile document page-instance binding is ambiguous)(?:$|[\r\n]| +at )/u
868
+ .test(error.message)
869
+ ) {
870
+ throw new LinkedInProfileBrowserFailure(
871
+ "page-binding",
872
+ "LinkedIn stats browser omitted its reviewed profile page-instance binding",
873
+ );
874
+ }
875
+ if (error instanceof Error) {
876
+ if (matchesLinkedInBrowserEvalError(error, "LinkedIn stats browser Contact-info control was ambiguous")) {
877
+ throw new LinkedInProfileBrowserFailure(
878
+ "page-binding",
879
+ "LinkedIn stats browser Contact-info control was ambiguous",
880
+ );
881
+ }
882
+ if (matchesLinkedInBrowserEvalError(error, "LinkedIn stats browser omitted its reviewed Contact-info control")) {
883
+ throw new LinkedInProfileBrowserFailure(
884
+ "page-binding",
885
+ "LinkedIn stats browser omitted its reviewed Contact-info control",
886
+ );
887
+ }
888
+ if (matchesLinkedInBrowserEvalError(error, "LinkedIn stats browser Contact-info modal was ambiguous")) {
889
+ throw new LinkedInProfileBrowserFailure(
890
+ "page-binding",
891
+ "LinkedIn stats browser Contact-info modal was ambiguous",
892
+ );
893
+ }
894
+ if (matchesLinkedInBrowserEvalError(error, "LinkedIn stats browser omitted its Contact-info modal")) {
484
895
  throw new LinkedInProfileBrowserFailure(
485
- "execution-context",
486
- "LinkedIn stats browser lost its reviewed execution context",
896
+ "page-binding",
897
+ "LinkedIn stats browser omitted its Contact-info modal",
487
898
  );
488
899
  }
489
- if (hasUnexpectedLinkedInOrigin(error)) {
900
+ if (matchesLinkedInBrowserEvalError(error, "LinkedIn stats browser Contact-info modal changed shape")) {
490
901
  throw new LinkedInProfileBrowserFailure(
491
- "bootstrap",
492
- "LinkedIn stats browser was not on its reviewed signed-in origin",
902
+ "page-binding",
903
+ "LinkedIn stats browser Contact-info modal changed shape",
493
904
  );
494
905
  }
495
- throw classifiedBrowserCommandFailure(error);
496
906
  }
907
+ throw classifiedBrowserCommandFailure(error);
908
+ };
909
+ const runCommands = async (
910
+ commands: readonly (readonly string[])[],
911
+ maxOutputBytes = browserOutputBytes,
912
+ timeoutMs = remainingTimeMs(),
913
+ ): Promise<readonly Readonly<Record<string, unknown>>[]> => {
914
+ if (closed) throw new Error("LinkedIn stats browser transport is closed");
915
+ try {
916
+ return await session.runBatch(commands, timeoutMs, maxOutputBytes);
917
+ } catch (error) {
918
+ throw classifySessionError(error);
919
+ }
920
+ };
921
+ const bindProfilePageContext = async (
922
+ profileHref: string,
923
+ ): Promise<LinkedInProfilePageBindings> => {
924
+ await runCommands([["open", profileHref]]);
925
+ await runCommands(
926
+ [["wait", String(PROFILE_PAGE_WAIT_MS)]],
927
+ browserOutputBytes,
928
+ Math.min(remainingTimeMs(), 20_000),
929
+ );
930
+ const observeRoute = async (
931
+ filter: string,
932
+ route: LinkedInProfileNetworkRoute,
933
+ ): Promise<LinkedInProfilePageBindings | null> => {
934
+ const observed = await runCommands(
935
+ [["network", "requests", "--filter", filter]],
936
+ browserOutputBytes,
937
+ Math.min(remainingTimeMs(), 30_000),
938
+ );
939
+ const firstObserved = observed[0];
940
+ if (firstObserved === undefined) {
941
+ throw new LinkedInProfileBrowserFailure(
942
+ "page-binding",
943
+ "LinkedIn profile page-binding observation omitted its response",
944
+ );
945
+ }
946
+ return linkedInProfileNetworkBindings(browserResultData(firstObserved), route);
947
+ };
948
+ const sdui = await observeRoute("/flagship-web/rsc-action/", "rsc-action");
949
+ if (sdui !== null) return sdui;
950
+ const voyager = await observeRoute("/voyager/api/", "voyager");
951
+ if (voyager !== null) return voyager;
952
+ const extracted = await runCommands(
953
+ [["eval", PROFILE_PAGE_CONTEXT_EXTRACT_SOURCE]],
954
+ BROWSER_ENVELOPE_BYTES,
955
+ );
956
+ const firstExtracted = extracted[0];
957
+ if (firstExtracted === undefined) {
958
+ throw new LinkedInProfileBrowserFailure(
959
+ "page-binding",
960
+ "LinkedIn profile document omitted its page-instance binding",
961
+ );
962
+ }
963
+ const document = linkedInProfileDocumentBindings(
964
+ browserEvaluationResult(firstExtracted),
965
+ profileHref,
966
+ );
967
+ if (document.pageInstance === null) {
968
+ throw new LinkedInProfileBrowserFailure(
969
+ "page-binding",
970
+ "LinkedIn stats browser omitted its reviewed profile page-instance binding",
971
+ );
972
+ }
973
+ return Object.freeze({ pageInstance: document.pageInstance });
974
+ };
975
+ const run = async (
976
+ binding: BrowserReadBinding,
977
+ ): Promise<Readonly<Record<string, unknown>>> => {
978
+ const records = await runCommands(
979
+ [["eval", browserReadEvaluationSource(binding)]],
980
+ Math.min(
981
+ encodedBodyBound(binding.maxBytes) + BROWSER_ENVELOPE_BYTES,
982
+ browserOutputBytes,
983
+ ),
984
+ );
497
985
  const first = records[0];
498
986
  if (first === undefined) {
499
987
  throw new LinkedInProfileBrowserFailure(
@@ -523,7 +1011,11 @@ export async function createLinkedInProfileBrowserTransport(
523
1011
  }
524
1012
  const expectedContentTypes = binding.kind === "json"
525
1013
  ? ["application/vnd.linkedin.normalized+json+2.1", "application/json"]
526
- : ["text/html"];
1014
+ : binding.kind === "rsc"
1015
+ ? ["text/x-component", "text/html", "text/plain"]
1016
+ : binding.kind === "rsc-action"
1017
+ ? ["application/octet-stream", "text/x-component", "text/html", "text/plain"]
1018
+ : ["text/html"];
527
1019
  if (
528
1020
  typeof result.status !== "number"
529
1021
  || !Number.isSafeInteger(result.status)
@@ -538,7 +1030,7 @@ export async function createLinkedInProfileBrowserTransport(
538
1030
  );
539
1031
  if (result.authWall === true) {
540
1032
  if (
541
- binding.kind !== "html"
1033
+ (binding.kind !== "html" && binding.kind !== "rsc" && binding.kind !== "rsc-action")
542
1034
  || result.status !== 200
543
1035
  || result.contentType !== "text/html"
544
1036
  || result.bodyBase64 !== null
@@ -637,28 +1129,150 @@ export async function createLinkedInProfileBrowserTransport(
637
1129
  throw new Error("LinkedIn stats browser Contact-info read is out of order");
638
1130
  }
639
1131
  const profile = exactLinkedInUrl(input.profileUrl, "profile");
1132
+ try {
1133
+ const result = await run({
1134
+ kind: "json",
1135
+ maxBytes: Math.min(MAX_IDENTITY_BYTES, options.maxOutputBytes),
1136
+ path: buildLinkedInProfileContactInfoGraphqlPath({
1137
+ profileUrn: input.profileUrn,
1138
+ queryId: input.queryId,
1139
+ }),
1140
+ referrer: profile.href,
1141
+ });
1142
+ state = "complete";
1143
+ const text = decodedBody(
1144
+ result,
1145
+ Math.min(MAX_IDENTITY_BYTES, options.maxOutputBytes),
1146
+ );
1147
+ try {
1148
+ return JSON.parse(text) as unknown;
1149
+ } catch {
1150
+ throw new LinkedInProfileBrowserFailure(
1151
+ "identity-json",
1152
+ "LinkedIn stats browser Contact-info response was not valid JSON",
1153
+ );
1154
+ }
1155
+ } catch (error) {
1156
+ if (error instanceof LinkedInProfileBrowserResponseRejectedError) throw error;
1157
+ state = "complete";
1158
+ throw error;
1159
+ }
1160
+ },
1161
+ readContactOverlayText: async (input: LinkedInContactInfoJsonInput) => {
1162
+ if (state !== "profile") {
1163
+ throw new Error("LinkedIn stats browser Contact-info overlay read is out of order");
1164
+ }
1165
+ const profile = exactLinkedInUrl(input.profileUrl, "profile");
640
1166
  const result = await run({
641
- kind: "json",
642
- maxBytes: Math.min(MAX_IDENTITY_BYTES, options.maxOutputBytes),
643
- path: buildLinkedInProfileContactInfoGraphqlPath({
644
- profileUrn: input.profileUrn,
645
- queryId: input.queryId,
1167
+ kind: "rsc",
1168
+ maxBytes: options.maxOutputBytes,
1169
+ path: buildLinkedInProfileContactInfoOverlayPath({
1170
+ profileUrl: input.profileUrl,
646
1171
  }),
647
1172
  referrer: profile.href,
648
1173
  });
649
1174
  state = "complete";
650
- const text = decodedBody(
651
- result,
652
- Math.min(MAX_IDENTITY_BYTES, options.maxOutputBytes),
1175
+ return decodedBody(result, options.maxOutputBytes);
1176
+ },
1177
+ readContactNavigationText: async (input: LinkedInContactNavigationInput) => {
1178
+ if (state !== "profile") {
1179
+ throw new Error("LinkedIn stats browser Contact-info navigation read is out of order");
1180
+ }
1181
+ const profile = exactLinkedInUrl(input.profileUrl, "profile");
1182
+ assertLinkedInContactInfoRequest({
1183
+ method: "POST",
1184
+ url: new URL(
1185
+ buildLinkedInProfileContactDetailsNavigationPostPath({
1186
+ sduiid: input.sduiid,
1187
+ }),
1188
+ LINKEDIN_ORIGIN,
1189
+ ),
1190
+ });
1191
+ await bindProfilePageContext(profile.href);
1192
+ const records = await runCommands(
1193
+ [["eval", contactModalEvaluationSource(profile.href)]],
1194
+ Math.min(
1195
+ encodedBodyBound(options.maxOutputBytes) + BROWSER_ENVELOPE_BYTES,
1196
+ browserOutputBytes,
1197
+ ),
653
1198
  );
1199
+ const first = records[0];
1200
+ if (first === undefined) {
1201
+ throw new LinkedInProfileBrowserFailure(
1202
+ "response-envelope",
1203
+ "LinkedIn stats browser omitted its response",
1204
+ );
1205
+ }
1206
+ const result = browserEvaluationResult(first);
654
1207
  try {
655
- return JSON.parse(text) as unknown;
1208
+ exactKeys(
1209
+ result,
1210
+ [
1211
+ "authWall",
1212
+ "bodyBase64",
1213
+ "bodyBytes",
1214
+ "bodySha256",
1215
+ "contentType",
1216
+ "status",
1217
+ ],
1218
+ "LinkedIn stats browser request",
1219
+ );
656
1220
  } catch {
657
1221
  throw new LinkedInProfileBrowserFailure(
658
- "identity-json",
659
- "LinkedIn stats browser Contact-info response was not valid JSON",
1222
+ "response-envelope",
1223
+ "LinkedIn stats browser request returned an unexpected result shape",
1224
+ );
1225
+ }
1226
+ if (
1227
+ typeof result.status !== "number"
1228
+ || !Number.isSafeInteger(result.status)
1229
+ || result.status < 100
1230
+ || result.status > 599
1231
+ || typeof result.contentType !== "string"
1232
+ || result.contentType.length > 128
1233
+ || (result.contentType !== "" && !LINKEDIN_RESPONSE_MEDIA_TYPE.test(result.contentType))
1234
+ ) throw new LinkedInProfileBrowserFailure(
1235
+ "response-envelope",
1236
+ "LinkedIn stats browser returned a malformed response category",
1237
+ );
1238
+ if (result.authWall === true) {
1239
+ if (
1240
+ result.status !== 200
1241
+ || result.contentType !== "text/html"
1242
+ || result.bodyBase64 !== null
1243
+ || result.bodyBytes !== 0
1244
+ || result.bodySha256 !== null
1245
+ ) throw new LinkedInProfileBrowserFailure(
1246
+ "response-envelope",
1247
+ "LinkedIn stats browser returned a malformed authwall envelope",
1248
+ );
1249
+ throw new LinkedInProfileBrowserFailure(
1250
+ "authwall",
1251
+ "LinkedIn stats browser reached the signed-out authwall",
1252
+ );
1253
+ }
1254
+ if (result.authWall !== false) {
1255
+ throw new LinkedInProfileBrowserFailure(
1256
+ "response-envelope",
1257
+ "LinkedIn stats browser returned a malformed authwall envelope",
660
1258
  );
661
1259
  }
1260
+ if (result.status !== 200 || result.contentType !== "text/html") {
1261
+ if (
1262
+ result.bodyBase64 !== null
1263
+ || result.bodyBytes !== 0
1264
+ || result.bodySha256 !== null
1265
+ ) throw new LinkedInProfileBrowserFailure(
1266
+ "response-envelope",
1267
+ "LinkedIn stats browser returned a malformed rejection envelope",
1268
+ );
1269
+ throw new LinkedInProfileBrowserResponseRejectedError(
1270
+ result.status,
1271
+ result.contentType,
1272
+ );
1273
+ }
1274
+ state = "complete";
1275
+ return decodedBody(result, options.maxOutputBytes);
662
1276
  },
663
1277
  readOrganizationHtml: async (organizationUrl: string) => {
664
1278
  if (state !== "identity") {