@hraness/ghostget 0.17.6 → 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.
- package/CHANGELOG.md +301 -0
- package/README.md +16 -9
- package/dist/apple-photos-client.js +2 -1
- package/dist/beeper-client.js +2 -1
- package/dist/client.js +1 -0
- package/dist/imessage-direct-install-2gm3kge7.js +324 -0
- package/dist/index-01eeae9e.js +5 -0
- package/dist/index-2ymnp8xv.js +145 -0
- package/dist/index-en5hycxp.js +175 -0
- package/dist/{index-cytf8d9p.js → index-hfbygww8.js} +1 -1
- package/dist/index-n4szk3nw.js +50 -0
- package/dist/index-yq6maz71.js +1105 -0
- package/dist/index-z1w83f81.js +4 -0
- package/dist/index.js +4 -47
- package/dist/messaging-automation-api.js +21 -0
- package/dist/messaging-automation-j4274hvc.js +609 -0
- package/dist/messaging-native-install-8w1j36ah.js +16 -0
- package/dist/messaging.js +1 -0
- package/dist/omni-client.js +1 -0
- package/dist/whatsapp-automation-runtime-hgh1e9rm.js +845 -0
- package/dist/whatsapp-client.js +1 -0
- package/docs/control-panel.md +147 -0
- package/docs/imessage-direct-provider.md +31 -7
- package/docs/messaging-automation.md +103 -0
- package/package.json +66 -7
- package/skills/ghostget/SKILL.md +3 -1
- package/skills/ghostget/references/control-panel.md +43 -0
- package/skills/ghostget/references/install.md +5 -5
- package/skills/ghostget/references/linkedin-adapter.md +222 -12
- package/skills/ghostget/references/platform-patterns.md +1 -1
- package/src/args.ts +18 -5
- package/src/assets/adapters/imessage/wrench-web-adapter.json +126 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.json +1 -1
- package/src/assets/adapters/whatsapp/wrench-web-adapter.json +144 -0
- package/src/assets/messaging-runtime/NOTICE.txt +2580 -0
- package/src/assets/messaging-runtime/imsg-darwin-arm64.gz +0 -0
- package/src/assets/messaging-runtime/phone-number-info.plist.gz +0 -0
- package/src/assets/messaging-runtime/phone-number-metadata.json.gz +0 -0
- package/src/assets/messaging-runtime/phone-number-privacy.plist.gz +0 -0
- package/src/assets/messaging-runtime/wacli-darwin-arm64.gz +0 -0
- package/src/auth.ts +35 -1
- package/src/beeper-client-types.ts +1 -1
- package/src/cli.ts +18 -0
- package/src/confirmed-write-platform.ts +16 -1
- package/src/control/account-revision.ts +16 -0
- package/src/control/activity.ts +104 -0
- package/src/control/approval-broker.ts +59 -0
- package/src/control/approval-client.ts +49 -0
- package/src/control/bundled-interfaces.ts +20 -0
- package/src/control/cli.ts +20 -0
- package/src/control/connections.ts +87 -0
- package/src/control/credential-helper.ts +152 -0
- package/src/control/helper.ts +79 -0
- package/src/control/interface-cli.ts +22 -0
- package/src/control/interface-json.ts +94 -0
- package/src/control/interface-schema.ts +120 -0
- package/src/control/interfaces.ts +438 -0
- package/src/control/protocol.ts +182 -0
- package/src/control/service.ts +104 -0
- package/src/control/validation.ts +103 -0
- package/src/control/vault.ts +105 -0
- package/src/control/web-gateway.ts +62 -0
- package/src/control/web-policy.ts +56 -0
- package/src/ghostget.ts +18 -4
- package/src/messaging-automation-api.ts +12 -0
- package/src/messaging-automation-descriptors.ts +29 -0
- package/src/messaging-automation-factory.ts +148 -0
- package/src/messaging-automation-server.ts +183 -0
- package/src/messaging-automation-types.ts +155 -0
- package/src/messaging-automation-validation.ts +110 -0
- package/src/messaging-automation.ts +356 -0
- package/src/messaging-runtime.ts +3 -0
- package/src/oauth-google.ts +11 -5
- package/src/omni-runtime.ts +18 -3
- package/src/operation-permission-store.ts +92 -0
- package/src/operation-permission.ts +308 -0
- package/src/pinned-https.ts +5 -0
- package/src/plugins/imessage-direct/plugin.ts +12 -1
- package/src/plugins/imessage-direct/vendor/0003-feat-rpc-add-no-fetch-rich-cards.patch +276 -0
- package/src/plugins/imessage-direct/vendor/provenance.json +84 -10
- package/src/plugins/linkedin-web/plugin.ts +1 -1
- package/src/plugins/whatsapp-linked-device/plugin.ts +7 -2
- package/src/plugins/whatsapp-linked-device/vendor/0001-ghostget-private-messaging.patch +1093 -0
- package/src/plugins/whatsapp-linked-device/vendor/README.md +39 -0
- package/src/plugins/whatsapp-linked-device/vendor/provenance.json +45 -0
- package/src/provider-http.ts +11 -3
- package/src/provider-plugin-contract-identity.ts +2 -2
- package/src/provider-plugin-import-analysis.ts +52 -0
- package/src/provider-plugin-module-analysis.ts +21 -1
- package/src/provider-plugin-registry.ts +4 -8
- package/src/provider-plugin.ts +18 -8
- package/src/providers/imessage-automation.ts +250 -0
- package/src/providers/imessage-direct-install.ts +7 -0
- package/src/providers/imessage-direct-runtime.ts +32 -2
- package/src/providers/imessage-direct.ts +13 -3
- package/src/providers/linkedin-contact-failure.ts +21 -0
- package/src/providers/linkedin-contact-platform.ts +8 -1
- package/src/providers/linkedin-contact-program.ts +34 -5
- package/src/providers/linkedin-web-contact.ts +1223 -35
- package/src/providers/linkedin-web-profile-browser.ts +659 -45
- package/src/providers/linkedin-web-runtime.ts +1 -0
- package/src/providers/linkedin-web.ts +46 -2
- package/src/providers/messaging-native-artifacts.ts +38 -0
- package/src/providers/messaging-native-install.ts +75 -0
- package/src/providers/whatsapp-automation-runtime.ts +240 -0
- package/src/providers/whatsapp-automation.ts +153 -0
- package/src/read-client.ts +12 -2
- package/src/runtime.ts +81 -9
- package/src/state-helper.ts +2 -0
- package/src/storage.ts +71 -1
- package/src/usage.ts +9 -1
- package/src/version.ts +1 -1
- package/src/web-session-contract-definitions.ts +1 -1
- 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
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
"LinkedIn stats browser
|
|
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
|
-
|
|
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
|
-
"
|
|
486
|
-
"LinkedIn stats browser
|
|
896
|
+
"page-binding",
|
|
897
|
+
"LinkedIn stats browser omitted its Contact-info modal",
|
|
487
898
|
);
|
|
488
899
|
}
|
|
489
|
-
if (
|
|
900
|
+
if (matchesLinkedInBrowserEvalError(error, "LinkedIn stats browser Contact-info modal changed shape")) {
|
|
490
901
|
throw new LinkedInProfileBrowserFailure(
|
|
491
|
-
"
|
|
492
|
-
"LinkedIn stats browser
|
|
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
|
-
:
|
|
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: "
|
|
642
|
-
maxBytes:
|
|
643
|
-
path:
|
|
644
|
-
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
659
|
-
"LinkedIn stats browser
|
|
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") {
|