@hraness/ghostget 0.18.0 → 0.18.3
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 +315 -0
- package/README.md +7 -7
- 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-9wca02er.js → index-cnbz0gws.js} +1 -1
- package/dist/index-en5hycxp.js +175 -0
- 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 +2 -2
- package/docs/imessage-direct-provider.md +31 -7
- package/docs/messaging-automation.md +103 -0
- package/package.json +22 -2
- package/skills/ghostget/references/control-panel.md +1 -1
- package/skills/ghostget/references/install.md +5 -5
- package/skills/ghostget/references/linkedin-adapter.md +215 -13
- 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/beeper-client-types.ts +1 -1
- package/src/ghostget.ts +16 -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/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-plugin.ts +14 -0
- 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 +31 -6
- package/src/providers/linkedin-web-contact.ts +1072 -51
- 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/usage.ts +3 -1
- package/src/version.ts +1 -1
- package/src/web-session-contract-definitions.ts +1 -1
- package/tsconfig.json +3 -0
|
@@ -10,8 +10,51 @@ export const LINKEDIN_PROFILE_CONTACT_INFO_QUERY_NAME =
|
|
|
10
10
|
"voyagerIdentityDashProfileContactInfo";
|
|
11
11
|
export const LINKEDIN_CONTACT_DETAILS_OVERLAY_SCREEN_ID =
|
|
12
12
|
"com.linkedin.sdui.flagshipnav.profile.ProfileContactDetailsOverlay";
|
|
13
|
+
export const LINKEDIN_CONTACT_DETAILS_OVERLAY_PATH =
|
|
14
|
+
"/flagship-web/rsc-action/actions/navigation";
|
|
15
|
+
export const LINKEDIN_CONTACT_NAVIGATION_SDUIID_MAX_LENGTH = 256;
|
|
16
|
+
export const LINKEDIN_CONTACT_DETAILS_PAGE_KEY = "profile_view_base_contact_details";
|
|
17
|
+
export const LINKEDIN_CONTACT_NAVIGATION_CLIENT_ARGUMENT_KEYS = Object.freeze([
|
|
18
|
+
"$type",
|
|
19
|
+
"requestedStateKeys",
|
|
20
|
+
"payload",
|
|
21
|
+
"requestMetadata",
|
|
22
|
+
] as const);
|
|
23
|
+
export const LINKEDIN_CONTACT_NAVIGATION_PAYLOAD_KEYS = Object.freeze([
|
|
24
|
+
"vanityName",
|
|
25
|
+
"givenName",
|
|
26
|
+
"familyName",
|
|
27
|
+
"isVanityNameResolved",
|
|
28
|
+
] as const);
|
|
29
|
+
export const LINKEDIN_CONTACT_NAVIGATION_REQUESTED_ARGUMENTS_TYPE =
|
|
30
|
+
"proto.sdui.actions.requests.RequestedArguments";
|
|
31
|
+
export const LINKEDIN_CONTACT_NAVIGATION_REQUEST_METADATA_TYPE =
|
|
32
|
+
"proto.sdui.common.RequestMetadata";
|
|
33
|
+
export const LINKEDIN_CONTACT_NAVIGATION_REQUEST_METADATA_KEYS = Object.freeze([
|
|
34
|
+
"$type",
|
|
35
|
+
"states",
|
|
36
|
+
"screenId",
|
|
37
|
+
"knownTemplates",
|
|
38
|
+
] as const);
|
|
39
|
+
const CONTACT_NAVIGATION_CLIENT_ARGUMENT_KEY_SET = new Set<string>(
|
|
40
|
+
LINKEDIN_CONTACT_NAVIGATION_CLIENT_ARGUMENT_KEYS,
|
|
41
|
+
);
|
|
42
|
+
const CONTACT_NAVIGATION_PAYLOAD_KEY_SET = new Set<string>(
|
|
43
|
+
LINKEDIN_CONTACT_NAVIGATION_PAYLOAD_KEYS,
|
|
44
|
+
);
|
|
45
|
+
const CONTACT_NAVIGATION_REQUEST_METADATA_KEY_SET = new Set<string>(
|
|
46
|
+
LINKEDIN_CONTACT_NAVIGATION_REQUEST_METADATA_KEYS,
|
|
47
|
+
);
|
|
48
|
+
const LINKEDIN_SDUI_TYPE = /^com\.linkedin(?:\.[A-Za-z][A-Za-z0-9]*)+$/u;
|
|
49
|
+
const PROTO_SDUI_TYPE = /^proto\.sdui(?:\.[A-Za-z][A-Za-z0-9]*)+$/u;
|
|
50
|
+
const CONTACT_NAVIGATION_HREF =
|
|
51
|
+
/(?:https:\/\/www\.linkedin\.com)?(\/flagship-web\/rsc-action\/actions\/navigation\?[^\s"'<>]+)/gu;
|
|
13
52
|
const CONTACT_QUERY_ID =
|
|
14
53
|
/^voyagerIdentityDashProfileContactInfo\.[0-9a-f]{32}$/u;
|
|
54
|
+
const LINKEDIN_CONTACT_INFO_OVERLAY_PATH =
|
|
55
|
+
/^\/in\/([A-Za-z0-9][A-Za-z0-9_-]{1,99})\/overlay\/contact-info\/$/u;
|
|
56
|
+
const LINKEDIN_CONTACT_INFO_FIELD_LABEL_ERROR =
|
|
57
|
+
"LinkedIn contact-info field label must be a bounded string";
|
|
15
58
|
|
|
16
59
|
const LINKEDIN_ORIGIN = "https://www.linkedin.com";
|
|
17
60
|
const PROFILE_URN = /^urn:li:fsd_profile:[A-Za-z0-9_-]{1,256}$/u;
|
|
@@ -20,20 +63,31 @@ const EMAIL = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,24}$/u;
|
|
|
20
63
|
const PHONE = /^\+?[0-9][0-9 .\-()]{6,30}[0-9]$/u;
|
|
21
64
|
const BIRTHDAY = /^(?:[0-9]{4}-)?(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])$/u;
|
|
22
65
|
const CONNECTED_DISPLAY =
|
|
23
|
-
/^(Jan|Feb|Mar|Apr|
|
|
66
|
+
/^(January|February|March|April|May|June|July|August|September|October|November|December|Jan|Feb|Mar|Apr|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ([1-9]|[12][0-9]|3[01]), ([0-9]{4})$/u;
|
|
24
67
|
const MONTHS = Object.freeze({
|
|
25
68
|
Jan: "01",
|
|
69
|
+
January: "01",
|
|
26
70
|
Feb: "02",
|
|
71
|
+
February: "02",
|
|
27
72
|
Mar: "03",
|
|
73
|
+
March: "03",
|
|
28
74
|
Apr: "04",
|
|
75
|
+
April: "04",
|
|
29
76
|
May: "05",
|
|
30
77
|
Jun: "06",
|
|
78
|
+
June: "06",
|
|
31
79
|
Jul: "07",
|
|
80
|
+
July: "07",
|
|
32
81
|
Aug: "08",
|
|
82
|
+
August: "08",
|
|
33
83
|
Sep: "09",
|
|
84
|
+
September: "09",
|
|
34
85
|
Oct: "10",
|
|
86
|
+
October: "10",
|
|
35
87
|
Nov: "11",
|
|
88
|
+
November: "11",
|
|
36
89
|
Dec: "12",
|
|
90
|
+
December: "12",
|
|
37
91
|
});
|
|
38
92
|
const HTML_ENTITY =
|
|
39
93
|
/&(?:nbsp|quot|amp|lt|gt|apos|#(?:[xX][0-9A-Fa-f]{1,6}|[0-9]{1,7}));/gu;
|
|
@@ -46,11 +100,16 @@ const MAX_WALK_DEPTH = 128;
|
|
|
46
100
|
const RSC_FLIGHT_ROW = /(?:^|\n)(\d+):/u;
|
|
47
101
|
const PROFILE_ID = /^[A-Za-z0-9_-]{1,256}$/u;
|
|
48
102
|
const DISTANCE_IN_TEXT =
|
|
49
|
-
/(?:^|["'\\{,])(?:networkDistance|memberDistance|distance)"?\s*:\s
|
|
103
|
+
/(?:^|["'\\{,])(?:networkDistance|memberDistance|distance)\\*"?\s*:\s*\\*"?(DISTANCE_[A-Z0-9]+|OUT_OF_NETWORK|SELF|[0-9]+)\\*"?/gu;
|
|
50
104
|
const PROFILE_URN_IN_TEXT = /urn:li:fsd_profile:[A-Za-z0-9_-]{1,256}/gu;
|
|
51
|
-
const VIEWEE_PROFILE_ID_IN_TEXT =
|
|
105
|
+
const VIEWEE_PROFILE_ID_IN_TEXT =
|
|
106
|
+
/vieweeProfileId\\*"?\s*:\s*\\*"?([A-Za-z0-9_-]{1,256})/gu;
|
|
107
|
+
const VIEWEE_MEMBER_URN_IN_TEXT =
|
|
108
|
+
/vieweeMemberUrn\\*"?\s*:\s*\\*"?(urn:li:(?:fsd_profile|member):[A-Za-z0-9_-]{1,256}|[0-9]{1,32})/gu;
|
|
109
|
+
const MEMBER_URN = /^urn:li:member:[0-9]{1,32}$/u;
|
|
52
110
|
const VANITY_IN_TEXT =
|
|
53
|
-
/(?:vanityName|publicIdentifier)"
|
|
111
|
+
/(?:vanityName|publicIdentifier)\\*"?\s*:\s*\\*"?([A-Za-z0-9][A-Za-z0-9_-]{1,99})/gu;
|
|
112
|
+
const MAX_JSON_STRING_PEELS = 4;
|
|
54
113
|
const KEYED_IDENTITY_KEYS = new Set([
|
|
55
114
|
"distance",
|
|
56
115
|
"entityUrn",
|
|
@@ -104,6 +163,49 @@ export type LinkedInContactInfoJsonInput = {
|
|
|
104
163
|
readonly queryId?: string;
|
|
105
164
|
};
|
|
106
165
|
|
|
166
|
+
export type LinkedInContactNavigationRequestMetadata = {
|
|
167
|
+
readonly $type?: string;
|
|
168
|
+
readonly states?: readonly string[];
|
|
169
|
+
readonly screenId?: string;
|
|
170
|
+
readonly knownTemplates?: readonly string[];
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export type LinkedInContactNavigationPayload = {
|
|
174
|
+
readonly vanityName?: string;
|
|
175
|
+
readonly givenName?: string;
|
|
176
|
+
readonly familyName?: string;
|
|
177
|
+
readonly isVanityNameResolved?: boolean;
|
|
178
|
+
readonly requestMetadata?: LinkedInContactNavigationRequestMetadata;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export type LinkedInContactNavigationClientArguments = {
|
|
182
|
+
readonly $type?: string;
|
|
183
|
+
readonly requestedStateKeys?: readonly string[];
|
|
184
|
+
readonly payload: LinkedInContactNavigationPayload;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export type LinkedInContactNavigationAction = {
|
|
188
|
+
readonly sduiid: string;
|
|
189
|
+
readonly clientArguments: LinkedInContactNavigationClientArguments;
|
|
190
|
+
readonly isModal: true;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export type LinkedInContactNavigationInput = {
|
|
194
|
+
readonly profileUrl: string;
|
|
195
|
+
readonly profileUrn: string;
|
|
196
|
+
readonly sduiid: string;
|
|
197
|
+
readonly clientArguments: LinkedInContactNavigationClientArguments;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export type LinkedInContactNavigationActionResult =
|
|
201
|
+
| { readonly kind: "action"; readonly action: LinkedInContactNavigationAction }
|
|
202
|
+
| { readonly kind: "absent" }
|
|
203
|
+
| { readonly kind: "omitted-sduiid" }
|
|
204
|
+
| { readonly kind: "unreviewed-sduiid" }
|
|
205
|
+
| { readonly kind: "ambiguous-sduiid" }
|
|
206
|
+
| { readonly kind: "unreviewed-client-arguments"; readonly keys: readonly string[] }
|
|
207
|
+
| { readonly kind: "unbound-client-arguments" };
|
|
208
|
+
|
|
107
209
|
type JsonRecord = Readonly<Record<string, unknown>>;
|
|
108
210
|
|
|
109
211
|
function isRecord(value: unknown): value is JsonRecord {
|
|
@@ -174,6 +276,82 @@ export function linkedInProfileContactInfoGraphqlUrl(input: {
|
|
|
174
276
|
return new URL(buildLinkedInProfileContactInfoGraphqlPath(input), LINKEDIN_ORIGIN);
|
|
175
277
|
}
|
|
176
278
|
|
|
279
|
+
export function buildLinkedInProfileContactDetailsOverlayPath(input: {
|
|
280
|
+
readonly profileUrn: unknown;
|
|
281
|
+
}): string {
|
|
282
|
+
const urn = profileUrn(input.profileUrn);
|
|
283
|
+
return `${LINKEDIN_CONTACT_DETAILS_OVERLAY_PATH}?screenId=${encodeURIComponent(LINKEDIN_CONTACT_DETAILS_OVERLAY_SCREEN_ID)}&profileUrn=${encodeURIComponent(urn)}`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function linkedInProfileContactDetailsOverlayUrl(input: {
|
|
287
|
+
readonly profileUrn: unknown;
|
|
288
|
+
}): URL {
|
|
289
|
+
return new URL(buildLinkedInProfileContactDetailsOverlayPath(input), LINKEDIN_ORIGIN);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function parseLinkedInContactNavigationSduiid(raw: unknown): string {
|
|
293
|
+
const sduiid = boundedText(
|
|
294
|
+
raw,
|
|
295
|
+
"LinkedIn contact-info navigation sduiid",
|
|
296
|
+
LINKEDIN_CONTACT_NAVIGATION_SDUIID_MAX_LENGTH,
|
|
297
|
+
);
|
|
298
|
+
if (
|
|
299
|
+
/[\s"'<>\\]/u.test(sduiid)
|
|
300
|
+
|| sduiid !== LINKEDIN_CONTACT_DETAILS_OVERLAY_SCREEN_ID
|
|
301
|
+
) {
|
|
302
|
+
throw new Error("LinkedIn contact-info navigation sduiid must equal ProfileContactDetailsOverlay");
|
|
303
|
+
}
|
|
304
|
+
return sduiid;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function buildLinkedInProfileContactDetailsNavigationPostPath(
|
|
308
|
+
input: { readonly sduiid?: unknown } = {},
|
|
309
|
+
): string {
|
|
310
|
+
const sduiid = input.sduiid === undefined
|
|
311
|
+
? LINKEDIN_CONTACT_DETAILS_OVERLAY_SCREEN_ID
|
|
312
|
+
: parseLinkedInContactNavigationSduiid(input.sduiid);
|
|
313
|
+
// Live LinkedIn rsc-action routes use `sduiid`, not `sduid`. The headed 200
|
|
314
|
+
// Contact-info POST and public SDUI server-request/pagination URLs keep that
|
|
315
|
+
// spelling; `sduid=` notes are transcription typos.
|
|
316
|
+
return `${LINKEDIN_CONTACT_DETAILS_OVERLAY_PATH}?screenId=${encodeURIComponent(LINKEDIN_CONTACT_DETAILS_OVERLAY_SCREEN_ID)}&sduiid=${encodeURIComponent(sduiid)}`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function linkedInProfileContactDetailsNavigationPostUrl(input: {
|
|
320
|
+
readonly sduiid: unknown;
|
|
321
|
+
}): URL {
|
|
322
|
+
return new URL(buildLinkedInProfileContactDetailsNavigationPostPath(input), LINKEDIN_ORIGIN);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function buildLinkedInContactNavigationBody(
|
|
326
|
+
input: Pick<LinkedInContactNavigationAction, "clientArguments">,
|
|
327
|
+
): string {
|
|
328
|
+
return JSON.stringify({
|
|
329
|
+
clientArguments: canonicalContactNavigationClientArguments(input.clientArguments),
|
|
330
|
+
isModal: true,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function buildLinkedInProfileContactInfoOverlayPath(input: {
|
|
335
|
+
readonly profileUrl: unknown;
|
|
336
|
+
}): string {
|
|
337
|
+
const target = linkedInPersonalProfileTarget(input.profileUrl);
|
|
338
|
+
return `/in/${target.slug}/overlay/contact-info/`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function linkedInProfileContactInfoOverlayUrl(input: {
|
|
342
|
+
readonly profileUrl: unknown;
|
|
343
|
+
}): URL {
|
|
344
|
+
return new URL(buildLinkedInProfileContactInfoOverlayPath(input), LINKEDIN_ORIGIN);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function assertLinkedInContactInfoOverlayUrl(url: URL): void {
|
|
348
|
+
const match = LINKEDIN_CONTACT_INFO_OVERLAY_PATH.exec(url.pathname);
|
|
349
|
+
if (url.search !== "" || match?.[1] === undefined) {
|
|
350
|
+
throw new Error("LinkedIn contact-info request escaped its exact reviewed route");
|
|
351
|
+
}
|
|
352
|
+
linkedInPersonalProfilePublicIdentifier(match[1]);
|
|
353
|
+
}
|
|
354
|
+
|
|
177
355
|
export function resolveLinkedInProfileContactInfoQueryId(html: unknown): string | undefined {
|
|
178
356
|
if (typeof html !== "string" || html.length < 1 || html.length > MAX_HTML_BYTES) {
|
|
179
357
|
return undefined;
|
|
@@ -188,6 +366,51 @@ export function resolveLinkedInProfileContactInfoQueryId(html: unknown): string
|
|
|
188
366
|
return unique.values().next().value;
|
|
189
367
|
}
|
|
190
368
|
|
|
369
|
+
function assertLinkedInContactInfoNavigationPostUrl(url: URL): void {
|
|
370
|
+
const queryNames = [...url.searchParams.keys()];
|
|
371
|
+
if (
|
|
372
|
+
url.pathname !== LINKEDIN_CONTACT_DETAILS_OVERLAY_PATH
|
|
373
|
+
|| queryNames.length !== 2
|
|
374
|
+
|| queryNames[0] !== "screenId"
|
|
375
|
+
|| queryNames[1] !== "sduiid"
|
|
376
|
+
|| url.searchParams.get("screenId") !== LINKEDIN_CONTACT_DETAILS_OVERLAY_SCREEN_ID
|
|
377
|
+
|| url.searchParams.getAll("screenId").length !== 1
|
|
378
|
+
|| url.searchParams.getAll("sduiid").length !== 1
|
|
379
|
+
) throw new Error("LinkedIn contact-info request escaped its exact reviewed route");
|
|
380
|
+
parseLinkedInContactNavigationSduiid(url.searchParams.get("sduiid"));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function assertLinkedInContactInfoNavigationBody(body: unknown): void {
|
|
384
|
+
if (typeof body === "string") {
|
|
385
|
+
if (body.length < 1 || body.length > 4_096) {
|
|
386
|
+
throw new Error("LinkedIn contact-info navigation body exceeded its reviewed bound");
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
body = JSON.parse(body) as unknown;
|
|
390
|
+
} catch {
|
|
391
|
+
throw new Error("LinkedIn contact-info navigation body must be JSON");
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (!isRecord(body)) throw new Error("LinkedIn contact-info navigation body must be an object");
|
|
395
|
+
const keys = Object.keys(body);
|
|
396
|
+
if (
|
|
397
|
+
keys.length !== 2
|
|
398
|
+
|| keys[0] !== "clientArguments"
|
|
399
|
+
|| keys[1] !== "isModal"
|
|
400
|
+
|| body.isModal !== true
|
|
401
|
+
|| !isRecord(body.clientArguments)
|
|
402
|
+
) throw new Error("LinkedIn contact-info navigation body escaped its reviewed shape");
|
|
403
|
+
const reviewed = reviewedContactNavigationClientArguments(body.clientArguments);
|
|
404
|
+
if (reviewed.kind === "unreviewed") {
|
|
405
|
+
throw new Error(
|
|
406
|
+
`LinkedIn contact-info navigation clientArguments used unreviewed keys: ${reviewed.keys.join(", ")}`,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
if (reviewed.kind !== "args") {
|
|
410
|
+
throw new Error("LinkedIn contact-info navigation body escaped its reviewed shape");
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
191
414
|
function assertLinkedInContactInfoGraphqlUrl(url: URL): void {
|
|
192
415
|
const queryNames = [...url.searchParams.keys()];
|
|
193
416
|
const queryKey = queryNames[1];
|
|
@@ -220,7 +443,9 @@ export function assertLinkedInContactInfoRequest(requestValue: unknown): void {
|
|
|
220
443
|
}
|
|
221
444
|
const method = boundedText(requestValue.method, "LinkedIn contact-info request method", 16)
|
|
222
445
|
.toUpperCase();
|
|
223
|
-
if (method !== "GET"
|
|
446
|
+
if (method !== "GET" && method !== "POST") {
|
|
447
|
+
throw new Error("LinkedIn contact-info reads require GET or the reviewed navigation POST");
|
|
448
|
+
}
|
|
224
449
|
const rawUrl = requestValue.url;
|
|
225
450
|
if (!(rawUrl instanceof URL) && typeof rawUrl !== "string") {
|
|
226
451
|
throw new Error("LinkedIn contact-info request URL is invalid");
|
|
@@ -232,6 +457,13 @@ export function assertLinkedInContactInfoRequest(requestValue: unknown): void {
|
|
|
232
457
|
&& url.password === ""
|
|
233
458
|
&& url.hash === ""
|
|
234
459
|
) {
|
|
460
|
+
if (method === "POST") {
|
|
461
|
+
assertLinkedInContactInfoNavigationPostUrl(url);
|
|
462
|
+
if (requestValue.body !== undefined) {
|
|
463
|
+
assertLinkedInContactInfoNavigationBody(requestValue.body);
|
|
464
|
+
}
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
235
467
|
if (url.search === "" && url.pathname === "/voyager/api/me") return;
|
|
236
468
|
if (url.search === "" && /^\/in\/[A-Za-z0-9][A-Za-z0-9_-]{1,99}\/$/u.test(url.pathname)) {
|
|
237
469
|
linkedInPersonalProfileTarget(url.href);
|
|
@@ -241,6 +473,10 @@ export function assertLinkedInContactInfoRequest(requestValue: unknown): void {
|
|
|
241
473
|
assertLinkedInContactInfoGraphqlUrl(url);
|
|
242
474
|
return;
|
|
243
475
|
}
|
|
476
|
+
if (LINKEDIN_CONTACT_INFO_OVERLAY_PATH.test(url.pathname)) {
|
|
477
|
+
assertLinkedInContactInfoOverlayUrl(url);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
244
480
|
}
|
|
245
481
|
throw new Error("LinkedIn contact-info request escaped its exact reviewed route");
|
|
246
482
|
}
|
|
@@ -356,15 +592,74 @@ function extractJsonString(source: string, start: number): string | undefined {
|
|
|
356
592
|
return undefined;
|
|
357
593
|
}
|
|
358
594
|
|
|
359
|
-
function
|
|
360
|
-
|
|
595
|
+
function uniqueTextCapture(values: readonly string[]): string | null {
|
|
596
|
+
const unique = [...new Set(values)];
|
|
597
|
+
return unique.length === 1 ? unique[0]! : null;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function peelOneJsonStringLayer(value: string): string {
|
|
601
|
+
if (!value.includes("\\")) return value;
|
|
602
|
+
let peeled = "";
|
|
603
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
604
|
+
const character = value[index];
|
|
605
|
+
if (character !== "\\") {
|
|
606
|
+
peeled += character;
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
const escaped = value[index + 1];
|
|
610
|
+
if (escaped === undefined) {
|
|
611
|
+
peeled += character;
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
if (escaped === "\"" || escaped === "\\" || escaped === "/") {
|
|
615
|
+
peeled += escaped;
|
|
616
|
+
index += 1;
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
if (escaped === "n" || escaped === "r" || escaped === "t") {
|
|
620
|
+
peeled += escaped === "n" ? "\n" : escaped === "r" ? "\r" : "\t";
|
|
621
|
+
index += 1;
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
if (escaped === "u") {
|
|
625
|
+
const hex = value.slice(index + 2, index + 6);
|
|
626
|
+
if (/^[0-9A-Fa-f]{4}$/u.test(hex)) {
|
|
627
|
+
const codePoint = Number.parseInt(hex, 16);
|
|
628
|
+
if (codePoint < 0xD800 || codePoint > 0xDFFF) {
|
|
629
|
+
peeled += String.fromCharCode(codePoint);
|
|
630
|
+
index += 5;
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
peeled += character;
|
|
636
|
+
}
|
|
637
|
+
return peeled;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function peelJsonStringEscapes(value: string): string {
|
|
641
|
+
let current = value;
|
|
642
|
+
for (let peel = 0; peel < MAX_JSON_STRING_PEELS; peel += 1) {
|
|
643
|
+
const next = peelOneJsonStringLayer(current);
|
|
644
|
+
if (next === current) break;
|
|
645
|
+
current = next;
|
|
646
|
+
}
|
|
647
|
+
return current;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function captureBreadcrumbRecord(value: string): JsonRecord | null {
|
|
361
651
|
const distances = [...value.matchAll(DISTANCE_IN_TEXT)]
|
|
362
652
|
.map((match) => match[1])
|
|
363
653
|
.filter((item): item is string => item !== undefined);
|
|
364
|
-
const urns = [...value.matchAll(PROFILE_URN_IN_TEXT)]
|
|
654
|
+
const urns = [...value.matchAll(PROFILE_URN_IN_TEXT)]
|
|
655
|
+
.map((match) => match[0])
|
|
656
|
+
.filter((item): item is string => item !== undefined);
|
|
365
657
|
const vieweeIds = [...value.matchAll(VIEWEE_PROFILE_ID_IN_TEXT)]
|
|
366
658
|
.map((match) => match[1])
|
|
367
659
|
.filter((item): item is string => item !== undefined);
|
|
660
|
+
const vieweeMembers = [...value.matchAll(VIEWEE_MEMBER_URN_IN_TEXT)]
|
|
661
|
+
.map((match) => match[1])
|
|
662
|
+
.filter((item): item is string => item !== undefined);
|
|
368
663
|
const vanities = [...value.matchAll(VANITY_IN_TEXT)]
|
|
369
664
|
.map((match) => match[1])
|
|
370
665
|
.filter((item): item is string => item !== undefined);
|
|
@@ -372,25 +667,33 @@ function breadcrumbRecordFromText(value: string): JsonRecord | null {
|
|
|
372
667
|
distances.length === 0
|
|
373
668
|
&& urns.length === 0
|
|
374
669
|
&& vieweeIds.length === 0
|
|
670
|
+
&& vieweeMembers.length === 0
|
|
375
671
|
&& vanities.length === 0
|
|
376
672
|
) return null;
|
|
377
673
|
const record: Record<string, unknown> = {};
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
record.networkDistance = /^[0-9]+$/u.test(
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
674
|
+
const distance = uniqueTextCapture(distances);
|
|
675
|
+
if (distance !== null) {
|
|
676
|
+
record.networkDistance = /^[0-9]+$/u.test(distance) ? Number(distance) : distance;
|
|
677
|
+
}
|
|
678
|
+
const vieweeMemberUrn = uniqueTextCapture(vieweeMembers);
|
|
679
|
+
if (vieweeMemberUrn !== null) record.vieweeMemberUrn = vieweeMemberUrn;
|
|
680
|
+
else {
|
|
681
|
+
const incidental = uniqueTextCapture(urns);
|
|
682
|
+
if (incidental !== null) record.profileUrn = incidental;
|
|
387
683
|
}
|
|
388
|
-
if (urns.length === 1) record.profileUrn = urns[0]![0];
|
|
389
684
|
if (vieweeIds.length === 1) record.vieweeProfileId = vieweeIds[0];
|
|
390
685
|
if (vanities.length === 1) record.vanityName = vanities[0];
|
|
391
686
|
return Object.keys(record).length > 0 ? Object.freeze(record) : null;
|
|
392
687
|
}
|
|
393
688
|
|
|
689
|
+
function breadcrumbRecordFromText(value: string): JsonRecord | null {
|
|
690
|
+
if (value.length < 8 || value.length > 1024 * 1024) return null;
|
|
691
|
+
const peeled = peelJsonStringEscapes(value);
|
|
692
|
+
return captureBreadcrumbRecord(peeled) ?? (
|
|
693
|
+
peeled === value ? null : captureBreadcrumbRecord(value)
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
|
|
394
697
|
function stringLooksLikeBootstrap(value: string): boolean {
|
|
395
698
|
return value.includes("networkDistance")
|
|
396
699
|
|| value.includes("memberDistance")
|
|
@@ -404,15 +707,20 @@ function stringLooksLikeBootstrap(value: string): boolean {
|
|
|
404
707
|
function decodeStringBootstrap(value: string): unknown[] {
|
|
405
708
|
const trimmed = value.trim();
|
|
406
709
|
if (trimmed.length < 8 || trimmed.length > 1024 * 1024) return [];
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
710
|
+
const peeled = peelJsonStringEscapes(trimmed);
|
|
711
|
+
for (const candidate of peeled === trimmed ? [trimmed] : [trimmed, peeled]) {
|
|
712
|
+
if (candidate.startsWith("{") || candidate.startsWith("[")) {
|
|
713
|
+
try {
|
|
714
|
+
return [JSON.parse(candidate) as unknown];
|
|
715
|
+
} catch {
|
|
716
|
+
// Fall through to breadcrumb extraction from partial RSC string rows.
|
|
717
|
+
}
|
|
412
718
|
}
|
|
413
719
|
}
|
|
414
|
-
if (!stringLooksLikeBootstrap(trimmed))
|
|
415
|
-
|
|
720
|
+
if (!stringLooksLikeBootstrap(trimmed) && !stringLooksLikeBootstrap(peeled)) {
|
|
721
|
+
return [];
|
|
722
|
+
}
|
|
723
|
+
const breadcrumb = breadcrumbRecordFromText(peeled);
|
|
416
724
|
return breadcrumb === null ? [] : [breadcrumb];
|
|
417
725
|
}
|
|
418
726
|
|
|
@@ -477,7 +785,9 @@ function decodeRscFlightRecords(text: string): unknown[] {
|
|
|
477
785
|
|
|
478
786
|
function decodeComoRehydrationValue(value: unknown): unknown[] {
|
|
479
787
|
if (typeof value === "string") {
|
|
480
|
-
return looksLikeRscFlight(value)
|
|
788
|
+
return looksLikeRscFlight(value)
|
|
789
|
+
? decodeRscFlightRecords(value)
|
|
790
|
+
: decodeStringBootstrap(value);
|
|
481
791
|
}
|
|
482
792
|
if (Array.isArray(value)) {
|
|
483
793
|
if (value.length > 20_000) {
|
|
@@ -673,6 +983,32 @@ function profileUrnFromRecord(record: JsonRecord): string | null {
|
|
|
673
983
|
return profileUrnFromIdentity(record.vieweeProfileId);
|
|
674
984
|
}
|
|
675
985
|
|
|
986
|
+
function durableProfileUrnFromRecord(record: JsonRecord): string | null {
|
|
987
|
+
for (const key of ["entityUrn", "objectUrn", "profileUrn"] as const) {
|
|
988
|
+
const urn = profileUrnFromIdentity(record[key]);
|
|
989
|
+
if (urn !== null) return urn;
|
|
990
|
+
}
|
|
991
|
+
const viewee = profileUrnFromIdentity(record.vieweeMemberUrn);
|
|
992
|
+
if (viewee !== null && !VIEWER_SUBJECT.test(viewee)) return viewee;
|
|
993
|
+
return profileUrnFromIdentity(record.vieweeProfileId);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function classicProfileUrnFromRecord(record: JsonRecord): string | null {
|
|
997
|
+
for (const key of ["entityUrn", "objectUrn", "profileUrn"] as const) {
|
|
998
|
+
const urn = profileUrnFromIdentity(record[key]);
|
|
999
|
+
if (urn !== null) return urn;
|
|
1000
|
+
}
|
|
1001
|
+
return null;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function vieweeMemberIdentity(record: JsonRecord): string | null {
|
|
1005
|
+
const value = record.vieweeMemberUrn;
|
|
1006
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 512) return null;
|
|
1007
|
+
return profileUrnFromIdentity(value)
|
|
1008
|
+
?? (MEMBER_URN.test(value) ? value : null)
|
|
1009
|
+
?? (PROFILE_ID.test(value) ? value : null);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
676
1012
|
function recordIsSelfView(record: JsonRecord): boolean {
|
|
677
1013
|
return record.isSelfView === true;
|
|
678
1014
|
}
|
|
@@ -710,8 +1046,26 @@ function recordContradictsTarget(
|
|
|
710
1046
|
urn: string,
|
|
711
1047
|
): boolean {
|
|
712
1048
|
const vanity = vanityFromRecord(record);
|
|
713
|
-
const
|
|
714
|
-
return (vanity !== null && vanity !== slug)
|
|
1049
|
+
const classicUrn = classicProfileUrnFromRecord(record);
|
|
1050
|
+
return (vanity !== null && vanity !== slug)
|
|
1051
|
+
|| (classicUrn !== null && classicUrn !== urn);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function recordIsViewerViewee(
|
|
1055
|
+
record: JsonRecord,
|
|
1056
|
+
viewer: string,
|
|
1057
|
+
): boolean {
|
|
1058
|
+
const member = vieweeMemberIdentity(record);
|
|
1059
|
+
return member === viewer || profileUrnFromIdentity(member) === viewer;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function uniqueNonSelfDistance(
|
|
1063
|
+
values: readonly (string | number)[],
|
|
1064
|
+
): string | number | undefined {
|
|
1065
|
+
const other = values.filter((value) => !isSelfDistance(value));
|
|
1066
|
+
const unique = uniqueDistances(other);
|
|
1067
|
+
if (unique.length > 1) throw omittedDistanceError();
|
|
1068
|
+
return unique[0];
|
|
715
1069
|
}
|
|
716
1070
|
|
|
717
1071
|
function relationshipDistance(
|
|
@@ -724,22 +1078,35 @@ function relationshipDistance(
|
|
|
724
1078
|
.filter((record) => recordMatchesTarget(record, slug, urn))
|
|
725
1079
|
.map(distanceValue)
|
|
726
1080
|
.filter((value): value is string | number => value !== null);
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
if (boundUnique.length === 1 && boundUnique[0] !== undefined) return boundUnique[0];
|
|
730
|
-
if (boundUnique.length > 1) throw omittedDistanceError();
|
|
1081
|
+
const boundDistance = uniqueNonSelfDistance(bound);
|
|
1082
|
+
if (boundDistance !== undefined) return boundDistance;
|
|
731
1083
|
if (bound.some(isSelfDistance) && urn === viewer) throw selfProfileError();
|
|
732
1084
|
|
|
733
|
-
const
|
|
734
|
-
.filter((record) =>
|
|
1085
|
+
const vieweeJoined = records
|
|
1086
|
+
.filter((record) => (
|
|
1087
|
+
!recordContradictsTarget(record, slug, urn)
|
|
1088
|
+
&& !recordIsViewerViewee(record, viewer)
|
|
1089
|
+
&& vieweeMemberIdentity(record) !== null
|
|
1090
|
+
))
|
|
735
1091
|
.map(distanceValue)
|
|
736
1092
|
.filter((value): value is string | number => value !== null);
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
if (
|
|
740
|
-
|
|
1093
|
+
const vieweeDistance = uniqueNonSelfDistance(vieweeJoined);
|
|
1094
|
+
if (vieweeDistance !== undefined) return vieweeDistance;
|
|
1095
|
+
if (vieweeJoined.some(isSelfDistance) && (urn === viewer || !vieweeJoined.some((value) => !isSelfDistance(value)))) {
|
|
1096
|
+
throw selfProfileError();
|
|
741
1097
|
}
|
|
742
|
-
|
|
1098
|
+
|
|
1099
|
+
const breadcrumbs = records
|
|
1100
|
+
.filter((record) => (
|
|
1101
|
+
!recordContradictsTarget(record, slug, urn)
|
|
1102
|
+
&& !recordMatchesTarget(record, slug, urn)
|
|
1103
|
+
&& vieweeMemberIdentity(record) === null
|
|
1104
|
+
))
|
|
1105
|
+
.map(distanceValue)
|
|
1106
|
+
.filter((value): value is string | number => value !== null);
|
|
1107
|
+
const breadcrumbDistance = uniqueNonSelfDistance(breadcrumbs);
|
|
1108
|
+
if (breadcrumbDistance !== undefined) return breadcrumbDistance;
|
|
1109
|
+
if (breadcrumbs.some(isSelfDistance) && (urn === viewer || !breadcrumbs.some((value) => !isSelfDistance(value)))) {
|
|
743
1110
|
throw selfProfileError();
|
|
744
1111
|
}
|
|
745
1112
|
throw omittedDistanceError();
|
|
@@ -760,14 +1127,14 @@ export function projectLinkedInProfileContactBinding(input: {
|
|
|
760
1127
|
if (vanityRecords.some(recordIsSelfView)) throw selfProfileError();
|
|
761
1128
|
const urns = new Set<string>();
|
|
762
1129
|
for (const record of vanityRecords) {
|
|
763
|
-
const urn =
|
|
1130
|
+
const urn = durableProfileUrnFromRecord(record);
|
|
764
1131
|
if (urn !== null) urns.add(urn);
|
|
765
1132
|
}
|
|
766
1133
|
if (urns.size < 1) {
|
|
767
1134
|
const joined = new Set<string>();
|
|
768
1135
|
let sawSelf = false;
|
|
769
1136
|
for (const record of records) {
|
|
770
|
-
const urn =
|
|
1137
|
+
const urn = durableProfileUrnFromRecord(record);
|
|
771
1138
|
const distance = distanceValue(record);
|
|
772
1139
|
if (
|
|
773
1140
|
recordIsSelfView(record)
|
|
@@ -808,14 +1175,652 @@ export function projectLinkedInProfileContactBinding(input: {
|
|
|
808
1175
|
});
|
|
809
1176
|
}
|
|
810
1177
|
|
|
1178
|
+
function isOverlayContactScreen(value: unknown): boolean {
|
|
1179
|
+
return value === LINKEDIN_CONTACT_DETAILS_OVERLAY_SCREEN_ID
|
|
1180
|
+
|| value === "ProfileContactDetailsOverlay";
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function isContactDetailsPageKey(value: unknown): boolean {
|
|
1184
|
+
return value === LINKEDIN_CONTACT_DETAILS_PAGE_KEY;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function reviewedSduiType(value: unknown): string | null {
|
|
1188
|
+
if (typeof value !== "string" || value.length < 12 || value.length > 256) return null;
|
|
1189
|
+
return LINKEDIN_SDUI_TYPE.test(value) || PROTO_SDUI_TYPE.test(value) ? value : null;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function reviewedStateKeyList(value: unknown): readonly string[] | null {
|
|
1193
|
+
if (!Array.isArray(value) || value.length > 32) return null;
|
|
1194
|
+
const keys: string[] = [];
|
|
1195
|
+
for (const item of value) {
|
|
1196
|
+
if (typeof item !== "string" || item.length < 1 || item.length > 256 || /[\s"'<>\\]/u.test(item)) {
|
|
1197
|
+
return null;
|
|
1198
|
+
}
|
|
1199
|
+
keys.push(item);
|
|
1200
|
+
}
|
|
1201
|
+
return Object.freeze(keys);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function optionalPersonName(value: unknown): string | null {
|
|
1205
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 128) return null;
|
|
1206
|
+
if (/[@:/\\<>]/u.test(value)) return null;
|
|
1207
|
+
return value;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
type OptionalSduiid =
|
|
1211
|
+
| { readonly kind: "absent" }
|
|
1212
|
+
| { readonly kind: "reviewed"; readonly sduiid: string }
|
|
1213
|
+
| { readonly kind: "unreviewed" };
|
|
1214
|
+
|
|
1215
|
+
function optionalNavigationSduiid(value: unknown): OptionalSduiid {
|
|
1216
|
+
if (value === undefined || value === null || value === "") return { kind: "absent" };
|
|
1217
|
+
if (typeof value !== "string") return { kind: "unreviewed" };
|
|
1218
|
+
try {
|
|
1219
|
+
return { kind: "reviewed", sduiid: parseLinkedInContactNavigationSduiid(value) };
|
|
1220
|
+
} catch {
|
|
1221
|
+
return { kind: "unreviewed" };
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function parseContactNavigationHref(value: unknown): {
|
|
1226
|
+
readonly screenId: string | null;
|
|
1227
|
+
readonly sduiid: OptionalSduiid;
|
|
1228
|
+
} | null {
|
|
1229
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 2_048) return null;
|
|
1230
|
+
let url: URL;
|
|
1231
|
+
try {
|
|
1232
|
+
url = new URL(
|
|
1233
|
+
value.startsWith("http")
|
|
1234
|
+
? value
|
|
1235
|
+
: `https://www.linkedin.com${value.startsWith("/") ? value : `/${value}`}`,
|
|
1236
|
+
);
|
|
1237
|
+
} catch {
|
|
1238
|
+
return null;
|
|
1239
|
+
}
|
|
1240
|
+
if (
|
|
1241
|
+
(url.hostname !== "www.linkedin.com" && url.hostname !== "linkedin.com")
|
|
1242
|
+
|| url.pathname !== LINKEDIN_CONTACT_DETAILS_OVERLAY_PATH
|
|
1243
|
+
) return null;
|
|
1244
|
+
const screenId = url.searchParams.get("screenId");
|
|
1245
|
+
return Object.freeze({
|
|
1246
|
+
screenId,
|
|
1247
|
+
sduiid: optionalNavigationSduiid(url.searchParams.get("sduiid")),
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function peelContactNavigationClientArguments(value: unknown): JsonRecord | null {
|
|
1252
|
+
if (!isRecord(value)) return null;
|
|
1253
|
+
if (isRecord(value.clientArguments)) return value.clientArguments;
|
|
1254
|
+
const requested = isRecord(value.requestedArguments) ? value.requestedArguments : null;
|
|
1255
|
+
const nested = isRecord(value.arguments) ? value.arguments : null;
|
|
1256
|
+
for (const candidate of [requested, nested]) {
|
|
1257
|
+
if (candidate === null) continue;
|
|
1258
|
+
if (isRecord(candidate.clientArguments)) return candidate.clientArguments;
|
|
1259
|
+
if (isRecord(candidate.payload) || candidate.$type !== undefined) return candidate;
|
|
1260
|
+
return Object.freeze({ payload: candidate });
|
|
1261
|
+
}
|
|
1262
|
+
if (isRecord(value.payload) || value.requestMetadata !== undefined) {
|
|
1263
|
+
const root: Record<string, unknown> = {};
|
|
1264
|
+
if (value.$type !== undefined) root.$type = value.$type;
|
|
1265
|
+
if (value.requestedStateKeys !== undefined) root.requestedStateKeys = value.requestedStateKeys;
|
|
1266
|
+
if (isRecord(value.payload)) root.payload = value.payload;
|
|
1267
|
+
if (value.requestMetadata !== undefined) root.requestMetadata = value.requestMetadata;
|
|
1268
|
+
return Object.freeze(root);
|
|
1269
|
+
}
|
|
1270
|
+
return null;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function requestMetadataEquals(
|
|
1274
|
+
left: LinkedInContactNavigationRequestMetadata,
|
|
1275
|
+
right: LinkedInContactNavigationRequestMetadata,
|
|
1276
|
+
): boolean {
|
|
1277
|
+
return left.$type === right.$type
|
|
1278
|
+
&& left.screenId === right.screenId
|
|
1279
|
+
&& JSON.stringify(left.states ?? null) === JSON.stringify(right.states ?? null)
|
|
1280
|
+
&& JSON.stringify(left.knownTemplates ?? null) === JSON.stringify(right.knownTemplates ?? null);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function reviewedRequestMetadata(
|
|
1284
|
+
raw: unknown,
|
|
1285
|
+
):
|
|
1286
|
+
| { readonly kind: "meta"; readonly meta: LinkedInContactNavigationRequestMetadata }
|
|
1287
|
+
| { readonly kind: "unreviewed"; readonly keys: readonly string[] }
|
|
1288
|
+
| { readonly kind: "empty" }
|
|
1289
|
+
{
|
|
1290
|
+
if (raw === undefined) return { kind: "empty" };
|
|
1291
|
+
if (!isRecord(raw)) return { kind: "unreviewed", keys: Object.freeze(["requestMetadata"]) };
|
|
1292
|
+
const extra = Object.keys(raw).filter((key) => !CONTACT_NAVIGATION_REQUEST_METADATA_KEY_SET.has(key));
|
|
1293
|
+
if (extra.length > 0) return { kind: "unreviewed", keys: Object.freeze(extra.map((key) => `requestMetadata.${key}`)) };
|
|
1294
|
+
const meta: {
|
|
1295
|
+
$type?: string;
|
|
1296
|
+
states?: readonly string[];
|
|
1297
|
+
screenId?: string;
|
|
1298
|
+
knownTemplates?: readonly string[];
|
|
1299
|
+
} = {};
|
|
1300
|
+
if (raw.$type !== undefined) {
|
|
1301
|
+
const type = reviewedSduiType(raw.$type);
|
|
1302
|
+
if (type === null) return { kind: "unreviewed", keys: Object.freeze(["requestMetadata.$type"]) };
|
|
1303
|
+
meta.$type = type;
|
|
1304
|
+
}
|
|
1305
|
+
if (raw.states !== undefined) {
|
|
1306
|
+
const states = reviewedStateKeyList(raw.states);
|
|
1307
|
+
if (states === null) return { kind: "unreviewed", keys: Object.freeze(["requestMetadata.states"]) };
|
|
1308
|
+
meta.states = states;
|
|
1309
|
+
}
|
|
1310
|
+
if (raw.screenId !== undefined) {
|
|
1311
|
+
if (!isOverlayContactScreen(raw.screenId)) {
|
|
1312
|
+
return { kind: "unreviewed", keys: Object.freeze(["requestMetadata.screenId"]) };
|
|
1313
|
+
}
|
|
1314
|
+
meta.screenId = LINKEDIN_CONTACT_DETAILS_OVERLAY_SCREEN_ID;
|
|
1315
|
+
}
|
|
1316
|
+
if (raw.knownTemplates !== undefined) {
|
|
1317
|
+
const templates = reviewedStateKeyList(raw.knownTemplates);
|
|
1318
|
+
if (templates === null) return { kind: "unreviewed", keys: Object.freeze(["requestMetadata.knownTemplates"]) };
|
|
1319
|
+
meta.knownTemplates = templates;
|
|
1320
|
+
}
|
|
1321
|
+
return Object.keys(meta).length === 0
|
|
1322
|
+
? { kind: "empty" }
|
|
1323
|
+
: { kind: "meta", meta: Object.freeze(meta) };
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function headedContactRequestMetadata(
|
|
1327
|
+
meta?: LinkedInContactNavigationRequestMetadata,
|
|
1328
|
+
): LinkedInContactNavigationRequestMetadata {
|
|
1329
|
+
return Object.freeze({
|
|
1330
|
+
$type: meta?.$type ?? LINKEDIN_CONTACT_NAVIGATION_REQUEST_METADATA_TYPE,
|
|
1331
|
+
states: meta?.states ?? Object.freeze([]),
|
|
1332
|
+
screenId: LINKEDIN_CONTACT_DETAILS_OVERLAY_SCREEN_ID,
|
|
1333
|
+
knownTemplates: meta?.knownTemplates ?? Object.freeze([]),
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
function canonicalContactNavigationPayload(
|
|
1338
|
+
payload: LinkedInContactNavigationPayload,
|
|
1339
|
+
): LinkedInContactNavigationPayload {
|
|
1340
|
+
const next: {
|
|
1341
|
+
vanityName?: string;
|
|
1342
|
+
givenName?: string;
|
|
1343
|
+
familyName?: string;
|
|
1344
|
+
isVanityNameResolved?: boolean;
|
|
1345
|
+
requestMetadata: LinkedInContactNavigationRequestMetadata;
|
|
1346
|
+
} = {
|
|
1347
|
+
requestMetadata: headedContactRequestMetadata(payload.requestMetadata),
|
|
1348
|
+
};
|
|
1349
|
+
if (payload.vanityName !== undefined) next.vanityName = payload.vanityName;
|
|
1350
|
+
if (payload.givenName !== undefined) next.givenName = payload.givenName;
|
|
1351
|
+
if (payload.familyName !== undefined) next.familyName = payload.familyName;
|
|
1352
|
+
if (payload.isVanityNameResolved !== undefined) next.isVanityNameResolved = payload.isVanityNameResolved;
|
|
1353
|
+
return Object.freeze({
|
|
1354
|
+
...(next.vanityName === undefined ? {} : { vanityName: next.vanityName }),
|
|
1355
|
+
...(next.givenName === undefined ? {} : { givenName: next.givenName }),
|
|
1356
|
+
...(next.familyName === undefined ? {} : { familyName: next.familyName }),
|
|
1357
|
+
...(next.isVanityNameResolved === undefined ? {} : { isVanityNameResolved: next.isVanityNameResolved }),
|
|
1358
|
+
requestMetadata: next.requestMetadata,
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function canonicalContactNavigationClientArguments(
|
|
1363
|
+
args: LinkedInContactNavigationClientArguments,
|
|
1364
|
+
): LinkedInContactNavigationClientArguments {
|
|
1365
|
+
return Object.freeze({
|
|
1366
|
+
$type: args.$type ?? LINKEDIN_CONTACT_NAVIGATION_REQUESTED_ARGUMENTS_TYPE,
|
|
1367
|
+
requestedStateKeys: args.requestedStateKeys ?? Object.freeze([]),
|
|
1368
|
+
payload: canonicalContactNavigationPayload(args.payload),
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
function reviewedContactNavigationClientArguments(
|
|
1373
|
+
raw: JsonRecord | null,
|
|
1374
|
+
):
|
|
1375
|
+
| { readonly kind: "args"; readonly args: LinkedInContactNavigationClientArguments }
|
|
1376
|
+
| { readonly kind: "unreviewed"; readonly keys: readonly string[] }
|
|
1377
|
+
| { readonly kind: "empty" }
|
|
1378
|
+
{
|
|
1379
|
+
if (raw === null) return { kind: "empty" };
|
|
1380
|
+
if (
|
|
1381
|
+
isRecord(raw.payload)
|
|
1382
|
+
|| raw.$type !== undefined
|
|
1383
|
+
|| raw.requestedStateKeys !== undefined
|
|
1384
|
+
|| raw.requestMetadata !== undefined
|
|
1385
|
+
) {
|
|
1386
|
+
const extra = Object.keys(raw).filter((key) => !CONTACT_NAVIGATION_CLIENT_ARGUMENT_KEY_SET.has(key));
|
|
1387
|
+
if (extra.length > 0) return { kind: "unreviewed", keys: Object.freeze(extra) };
|
|
1388
|
+
const args: {
|
|
1389
|
+
$type?: string;
|
|
1390
|
+
requestedStateKeys?: readonly string[];
|
|
1391
|
+
payload: LinkedInContactNavigationPayload;
|
|
1392
|
+
} = { payload: Object.freeze({}) };
|
|
1393
|
+
if (raw.$type !== undefined) {
|
|
1394
|
+
const type = reviewedSduiType(raw.$type);
|
|
1395
|
+
if (type === null) return { kind: "unreviewed", keys: Object.freeze(["$type"]) };
|
|
1396
|
+
args.$type = type;
|
|
1397
|
+
}
|
|
1398
|
+
if (raw.requestedStateKeys !== undefined) {
|
|
1399
|
+
const keys = reviewedStateKeyList(raw.requestedStateKeys);
|
|
1400
|
+
if (keys === null) return { kind: "unreviewed", keys: Object.freeze(["requestedStateKeys"]) };
|
|
1401
|
+
args.requestedStateKeys = keys;
|
|
1402
|
+
}
|
|
1403
|
+
const siblingMetadata = reviewedRequestMetadata(raw.requestMetadata);
|
|
1404
|
+
if (siblingMetadata.kind === "unreviewed") return siblingMetadata;
|
|
1405
|
+
let nestedMetadata:
|
|
1406
|
+
| { readonly kind: "meta"; readonly meta: LinkedInContactNavigationRequestMetadata }
|
|
1407
|
+
| { readonly kind: "unreviewed"; readonly keys: readonly string[] }
|
|
1408
|
+
| { readonly kind: "empty" } = { kind: "empty" };
|
|
1409
|
+
const payload: {
|
|
1410
|
+
vanityName?: string;
|
|
1411
|
+
givenName?: string;
|
|
1412
|
+
familyName?: string;
|
|
1413
|
+
isVanityNameResolved?: boolean;
|
|
1414
|
+
requestMetadata?: LinkedInContactNavigationRequestMetadata;
|
|
1415
|
+
} = {};
|
|
1416
|
+
if (raw.payload !== undefined) {
|
|
1417
|
+
if (!isRecord(raw.payload)) return { kind: "unreviewed", keys: Object.freeze(["payload"]) };
|
|
1418
|
+
const extraPayload = Object.keys(raw.payload)
|
|
1419
|
+
.filter((key) => key !== "requestMetadata" && !CONTACT_NAVIGATION_PAYLOAD_KEY_SET.has(key));
|
|
1420
|
+
if (extraPayload.length > 0) {
|
|
1421
|
+
return { kind: "unreviewed", keys: Object.freeze(extraPayload.map((key) => `payload.${key}`)) };
|
|
1422
|
+
}
|
|
1423
|
+
if (raw.payload.vanityName !== undefined) {
|
|
1424
|
+
const vanity = optionalPublicIdentifier(raw.payload.vanityName);
|
|
1425
|
+
if (vanity === null) return { kind: "unreviewed", keys: Object.freeze(["payload.vanityName"]) };
|
|
1426
|
+
payload.vanityName = vanity;
|
|
1427
|
+
}
|
|
1428
|
+
if (raw.payload.givenName !== undefined) {
|
|
1429
|
+
const givenName = optionalPersonName(raw.payload.givenName);
|
|
1430
|
+
if (givenName === null) return { kind: "unreviewed", keys: Object.freeze(["payload.givenName"]) };
|
|
1431
|
+
payload.givenName = givenName;
|
|
1432
|
+
}
|
|
1433
|
+
if (raw.payload.familyName !== undefined) {
|
|
1434
|
+
const familyName = optionalPersonName(raw.payload.familyName);
|
|
1435
|
+
if (familyName === null) return { kind: "unreviewed", keys: Object.freeze(["payload.familyName"]) };
|
|
1436
|
+
payload.familyName = familyName;
|
|
1437
|
+
}
|
|
1438
|
+
if (raw.payload.isVanityNameResolved !== undefined) {
|
|
1439
|
+
if (typeof raw.payload.isVanityNameResolved !== "boolean") {
|
|
1440
|
+
return { kind: "unreviewed", keys: Object.freeze(["payload.isVanityNameResolved"]) };
|
|
1441
|
+
}
|
|
1442
|
+
payload.isVanityNameResolved = raw.payload.isVanityNameResolved;
|
|
1443
|
+
}
|
|
1444
|
+
nestedMetadata = reviewedRequestMetadata(raw.payload.requestMetadata);
|
|
1445
|
+
if (nestedMetadata.kind === "unreviewed") {
|
|
1446
|
+
return {
|
|
1447
|
+
kind: "unreviewed",
|
|
1448
|
+
keys: Object.freeze(nestedMetadata.keys.map((key) => (
|
|
1449
|
+
key.startsWith("requestMetadata") ? `payload.${key}` : key
|
|
1450
|
+
))),
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
if (siblingMetadata.kind === "meta" && nestedMetadata.kind === "meta") {
|
|
1455
|
+
if (!requestMetadataEquals(siblingMetadata.meta, nestedMetadata.meta)) {
|
|
1456
|
+
return { kind: "unreviewed", keys: Object.freeze(["requestMetadata"]) };
|
|
1457
|
+
}
|
|
1458
|
+
payload.requestMetadata = headedContactRequestMetadata(siblingMetadata.meta);
|
|
1459
|
+
} else if (siblingMetadata.kind === "meta") {
|
|
1460
|
+
payload.requestMetadata = headedContactRequestMetadata(siblingMetadata.meta);
|
|
1461
|
+
} else if (nestedMetadata.kind === "meta") {
|
|
1462
|
+
payload.requestMetadata = headedContactRequestMetadata(nestedMetadata.meta);
|
|
1463
|
+
}
|
|
1464
|
+
args.payload = Object.freeze(payload);
|
|
1465
|
+
return { kind: "args", args: canonicalContactNavigationClientArguments(Object.freeze(args)) };
|
|
1466
|
+
}
|
|
1467
|
+
const extra = Object.keys(raw).filter((key) => (
|
|
1468
|
+
key !== "vanityName"
|
|
1469
|
+
&& key !== "publicIdentifier"
|
|
1470
|
+
&& key !== "vanity"
|
|
1471
|
+
));
|
|
1472
|
+
if (extra.length > 0) return { kind: "unreviewed", keys: Object.freeze(extra) };
|
|
1473
|
+
const vanity = optionalPublicIdentifier(raw.vanityName)
|
|
1474
|
+
?? optionalPublicIdentifier(raw.publicIdentifier)
|
|
1475
|
+
?? optionalPublicIdentifier(raw.vanity);
|
|
1476
|
+
return vanity === null
|
|
1477
|
+
? { kind: "empty" }
|
|
1478
|
+
: { kind: "args", args: Object.freeze({ payload: Object.freeze({ vanityName: vanity }) }) };
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
function clientArgumentsBindTarget(
|
|
1482
|
+
args: LinkedInContactNavigationClientArguments,
|
|
1483
|
+
publicIdentifier: string,
|
|
1484
|
+
): boolean {
|
|
1485
|
+
const vanity = optionalPublicIdentifier(args.payload.vanityName);
|
|
1486
|
+
if (vanity !== null && vanity !== publicIdentifier) return false;
|
|
1487
|
+
return vanity === publicIdentifier || vanity === null;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
function actionNameLooksLikeNavigateToScreen(value: unknown): boolean {
|
|
1491
|
+
return typeof value === "string" && /NavigateToScreen/iu.test(value);
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
function collectContactNavigationCandidates(
|
|
1495
|
+
records: readonly JsonRecord[],
|
|
1496
|
+
html: string,
|
|
1497
|
+
): readonly {
|
|
1498
|
+
readonly sduiid: OptionalSduiid;
|
|
1499
|
+
readonly clientArguments: JsonRecord | null;
|
|
1500
|
+
}[] {
|
|
1501
|
+
const candidates: {
|
|
1502
|
+
sduiid: OptionalSduiid;
|
|
1503
|
+
clientArguments: JsonRecord | null;
|
|
1504
|
+
}[] = [];
|
|
1505
|
+
const push = (sduiid: OptionalSduiid, clientArguments: JsonRecord | null) => {
|
|
1506
|
+
candidates.push({ sduiid, clientArguments });
|
|
1507
|
+
};
|
|
1508
|
+
for (const record of records) {
|
|
1509
|
+
const href = parseContactNavigationHref(
|
|
1510
|
+
record.href ?? record.url ?? record.navigationUrl ?? record.canonicalUrl,
|
|
1511
|
+
);
|
|
1512
|
+
const screen = record.screenId
|
|
1513
|
+
?? record.destinationScreen
|
|
1514
|
+
?? record.screen
|
|
1515
|
+
?? record.destination;
|
|
1516
|
+
const pageKey = record.pageKey ?? record.page_key;
|
|
1517
|
+
const overlay = isOverlayContactScreen(screen)
|
|
1518
|
+
|| isContactDetailsPageKey(pageKey)
|
|
1519
|
+
|| (href !== null && isOverlayContactScreen(href.screenId));
|
|
1520
|
+
const navigate = actionNameLooksLikeNavigateToScreen(
|
|
1521
|
+
record.actionName ?? record.action ?? record.$type ?? record.type,
|
|
1522
|
+
);
|
|
1523
|
+
const sduiid = optionalNavigationSduiid(record.sduiid ?? record.sduiId ?? record.sduiID);
|
|
1524
|
+
const hrefSduiid = href?.sduiid ?? { kind: "absent" as const };
|
|
1525
|
+
const args = peelContactNavigationClientArguments(record);
|
|
1526
|
+
if (!overlay) continue;
|
|
1527
|
+
if (
|
|
1528
|
+
!navigate
|
|
1529
|
+
&& sduiid.kind === "absent"
|
|
1530
|
+
&& hrefSduiid.kind === "absent"
|
|
1531
|
+
&& args === null
|
|
1532
|
+
&& href === null
|
|
1533
|
+
&& !isContactDetailsPageKey(pageKey)
|
|
1534
|
+
) continue;
|
|
1535
|
+
push(
|
|
1536
|
+
sduiid.kind !== "absent" ? sduiid : hrefSduiid,
|
|
1537
|
+
args,
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
for (const match of html.matchAll(CONTACT_NAVIGATION_HREF)) {
|
|
1541
|
+
const href = parseContactNavigationHref(match[1] ?? match[0]);
|
|
1542
|
+
if (href === null || !isOverlayContactScreen(href.screenId)) continue;
|
|
1543
|
+
push(href.sduiid, null);
|
|
1544
|
+
}
|
|
1545
|
+
return Object.freeze(candidates);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
export function linkedInContactNavigationActionError(
|
|
1549
|
+
result: Exclude<LinkedInContactNavigationActionResult, { kind: "action" | "absent" }>,
|
|
1550
|
+
): Error {
|
|
1551
|
+
if (result.kind === "omitted-sduiid") {
|
|
1552
|
+
return new Error("LinkedIn contact-info profile page omitted its Contact-info navigation sduiid");
|
|
1553
|
+
}
|
|
1554
|
+
if (result.kind === "unreviewed-sduiid") {
|
|
1555
|
+
return new Error(
|
|
1556
|
+
"LinkedIn contact-info navigation sduiid must equal ProfileContactDetailsOverlay",
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
if (result.kind === "ambiguous-sduiid") {
|
|
1560
|
+
return new Error(
|
|
1561
|
+
"LinkedIn contact-info profile page exposed ambiguous Contact-info navigation sduiids",
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
if (result.kind === "unreviewed-client-arguments") {
|
|
1565
|
+
return new Error(
|
|
1566
|
+
`LinkedIn contact-info navigation clientArguments used unreviewed keys: ${result.keys.join(", ")}`,
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
return new Error(
|
|
1570
|
+
"LinkedIn contact-info navigation clientArguments did not bind the requested profile",
|
|
1571
|
+
);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
export function extractLinkedInProfileContactNames(input: {
|
|
1575
|
+
readonly profileHtml: unknown;
|
|
1576
|
+
readonly publicIdentifier: string;
|
|
1577
|
+
}): {
|
|
1578
|
+
readonly givenName?: string;
|
|
1579
|
+
readonly familyName?: string;
|
|
1580
|
+
} {
|
|
1581
|
+
const publicIdentifier = linkedInPersonalProfilePublicIdentifier(input.publicIdentifier);
|
|
1582
|
+
const given = new Set<string>();
|
|
1583
|
+
const family = new Set<string>();
|
|
1584
|
+
for (const record of embeddedRecords(input.profileHtml)) {
|
|
1585
|
+
if (vanityFromRecord(record) !== publicIdentifier) continue;
|
|
1586
|
+
const givenName = optionalPersonName(record.givenName ?? record.firstName);
|
|
1587
|
+
const familyName = optionalPersonName(record.familyName ?? record.lastName);
|
|
1588
|
+
if (givenName !== null) given.add(givenName);
|
|
1589
|
+
if (familyName !== null) family.add(familyName);
|
|
1590
|
+
}
|
|
1591
|
+
const uniqueGiven = given.size === 1 ? [...given][0] : undefined;
|
|
1592
|
+
const uniqueFamily = family.size === 1 ? [...family][0] : undefined;
|
|
1593
|
+
return Object.freeze({
|
|
1594
|
+
...(uniqueGiven === undefined ? {} : { givenName: uniqueGiven }),
|
|
1595
|
+
...(uniqueFamily === undefined ? {} : { familyName: uniqueFamily }),
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
export function buildLinkedInAbsentContactNavigationAction(input: {
|
|
1600
|
+
readonly profileHtml: unknown;
|
|
1601
|
+
readonly publicIdentifier: string;
|
|
1602
|
+
readonly profileUrn: string;
|
|
1603
|
+
}): LinkedInContactNavigationAction {
|
|
1604
|
+
const publicIdentifier = linkedInPersonalProfilePublicIdentifier(input.publicIdentifier);
|
|
1605
|
+
profileUrn(input.profileUrn);
|
|
1606
|
+
const names = extractLinkedInProfileContactNames({
|
|
1607
|
+
profileHtml: input.profileHtml,
|
|
1608
|
+
publicIdentifier,
|
|
1609
|
+
});
|
|
1610
|
+
return Object.freeze({
|
|
1611
|
+
sduiid: LINKEDIN_CONTACT_DETAILS_OVERLAY_SCREEN_ID,
|
|
1612
|
+
clientArguments: canonicalContactNavigationClientArguments({
|
|
1613
|
+
payload: {
|
|
1614
|
+
vanityName: publicIdentifier,
|
|
1615
|
+
...names,
|
|
1616
|
+
isVanityNameResolved: true,
|
|
1617
|
+
},
|
|
1618
|
+
}),
|
|
1619
|
+
isModal: true,
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
export function extractLinkedInContactNavigationAction(input: {
|
|
1624
|
+
readonly profileHtml: unknown;
|
|
1625
|
+
readonly publicIdentifier: string;
|
|
1626
|
+
readonly profileUrn: string;
|
|
1627
|
+
}): LinkedInContactNavigationActionResult {
|
|
1628
|
+
const publicIdentifier = linkedInPersonalProfilePublicIdentifier(input.publicIdentifier);
|
|
1629
|
+
profileUrn(input.profileUrn);
|
|
1630
|
+
const html = typeof input.profileHtml === "string" ? input.profileHtml : "";
|
|
1631
|
+
const records = embeddedRecords(input.profileHtml);
|
|
1632
|
+
const candidates = collectContactNavigationCandidates(records, html);
|
|
1633
|
+
if (candidates.length < 1) return { kind: "absent" };
|
|
1634
|
+
if (candidates.some((candidate) => candidate.sduiid.kind === "unreviewed")) {
|
|
1635
|
+
return { kind: "unreviewed-sduiid" };
|
|
1636
|
+
}
|
|
1637
|
+
const sduiids = [...new Set(
|
|
1638
|
+
candidates
|
|
1639
|
+
.map((candidate) => candidate.sduiid.kind === "reviewed" ? candidate.sduiid.sduiid : undefined)
|
|
1640
|
+
.filter((value): value is string => value !== undefined),
|
|
1641
|
+
)];
|
|
1642
|
+
if (sduiids.length > 1) return { kind: "ambiguous-sduiid" };
|
|
1643
|
+
let reviewed:
|
|
1644
|
+
| { readonly kind: "args"; readonly args: LinkedInContactNavigationClientArguments }
|
|
1645
|
+
| { readonly kind: "empty" }
|
|
1646
|
+
| undefined;
|
|
1647
|
+
for (const candidate of candidates) {
|
|
1648
|
+
const peeled = reviewedContactNavigationClientArguments(candidate.clientArguments);
|
|
1649
|
+
if (peeled.kind === "unreviewed") {
|
|
1650
|
+
return { kind: "unreviewed-client-arguments", keys: peeled.keys };
|
|
1651
|
+
}
|
|
1652
|
+
if (peeled.kind === "args") {
|
|
1653
|
+
if (!clientArgumentsBindTarget(peeled.args, publicIdentifier)) {
|
|
1654
|
+
return { kind: "unbound-client-arguments" };
|
|
1655
|
+
}
|
|
1656
|
+
reviewed = peeled;
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
const clientArguments = reviewed?.kind === "args"
|
|
1660
|
+
? canonicalContactNavigationClientArguments({
|
|
1661
|
+
...reviewed.args,
|
|
1662
|
+
payload: {
|
|
1663
|
+
...reviewed.args.payload,
|
|
1664
|
+
vanityName: reviewed.args.payload.vanityName ?? publicIdentifier,
|
|
1665
|
+
},
|
|
1666
|
+
})
|
|
1667
|
+
: canonicalContactNavigationClientArguments({
|
|
1668
|
+
payload: { vanityName: publicIdentifier },
|
|
1669
|
+
});
|
|
1670
|
+
if (!clientArgumentsBindTarget(clientArguments, publicIdentifier)) {
|
|
1671
|
+
return { kind: "unbound-client-arguments" };
|
|
1672
|
+
}
|
|
1673
|
+
return {
|
|
1674
|
+
kind: "action",
|
|
1675
|
+
action: Object.freeze({
|
|
1676
|
+
sduiid: LINKEDIN_CONTACT_DETAILS_OVERLAY_SCREEN_ID,
|
|
1677
|
+
clientArguments,
|
|
1678
|
+
isModal: true,
|
|
1679
|
+
}),
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
|
|
811
1683
|
export function projectLinkedInEmbeddedContactFields(
|
|
812
1684
|
html: unknown,
|
|
813
1685
|
vanity: string,
|
|
814
1686
|
): LinkedInContactFields | undefined {
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
1687
|
+
try {
|
|
1688
|
+
const records = embeddedRecords(html);
|
|
1689
|
+
const entities = collectRecords(records).filter(contactEntity);
|
|
1690
|
+
if (entities.length < 1) return undefined;
|
|
1691
|
+
return projectFields(records, vanity);
|
|
1692
|
+
} catch (error) {
|
|
1693
|
+
if (error instanceof Error && error.message === LINKEDIN_CONTACT_INFO_FIELD_LABEL_ERROR) {
|
|
1694
|
+
return undefined;
|
|
1695
|
+
}
|
|
1696
|
+
throw error;
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
function overlayMailtoFields(value: string): JsonRecord | null {
|
|
1701
|
+
const emails = [...value.matchAll(/mailto:([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,24})/giu)]
|
|
1702
|
+
.map((match) => match[1]?.toLowerCase())
|
|
1703
|
+
.filter((item): item is string => item !== undefined);
|
|
1704
|
+
const unique = [...new Set(emails)];
|
|
1705
|
+
if (unique.length !== 1) return null;
|
|
1706
|
+
return Object.freeze({ emailAddress: unique[0] });
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
function overlayTextPairFields(value: readonly unknown[]): JsonRecord | null {
|
|
1710
|
+
const texts: string[] = [];
|
|
1711
|
+
for (const item of value) {
|
|
1712
|
+
if (typeof item === "string") {
|
|
1713
|
+
const text = item.trim();
|
|
1714
|
+
if (text.length > 0 && text.length <= 2_048) texts.push(text);
|
|
1715
|
+
continue;
|
|
1716
|
+
}
|
|
1717
|
+
if (!isRecord(item)) continue;
|
|
1718
|
+
const text = item.text ?? item.value ?? item.title ?? item.label;
|
|
1719
|
+
if (typeof text === "string") {
|
|
1720
|
+
const trimmed = text.trim();
|
|
1721
|
+
if (trimmed.length > 0 && trimmed.length <= 2_048) texts.push(trimmed);
|
|
1722
|
+
}
|
|
1723
|
+
const href = item.href ?? item.url;
|
|
1724
|
+
if (typeof href === "string" && href.toLowerCase().startsWith("mailto:")) {
|
|
1725
|
+
texts.push(href);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
if (texts.length < 2) return null;
|
|
1729
|
+
const collected: {
|
|
1730
|
+
email?: string;
|
|
1731
|
+
connectedSince?: string;
|
|
1732
|
+
phones: string[];
|
|
1733
|
+
websites: string[];
|
|
1734
|
+
birthday?: string;
|
|
1735
|
+
} = { phones: [], websites: [] };
|
|
1736
|
+
for (let index = 0; index < texts.length - 1; index += 1) {
|
|
1737
|
+
const label = texts[index];
|
|
1738
|
+
const raw = texts[index + 1];
|
|
1739
|
+
if (label === undefined || raw === undefined) continue;
|
|
1740
|
+
const key = optionalLabelKey(label);
|
|
1741
|
+
if (key === undefined) continue;
|
|
1742
|
+
if (key === "email") {
|
|
1743
|
+
const email = optionalEmail(
|
|
1744
|
+
raw.toLowerCase().startsWith("mailto:") ? raw.slice("mailto:".length) : raw,
|
|
1745
|
+
"LinkedIn contact-info Email",
|
|
1746
|
+
);
|
|
1747
|
+
if (email !== null) collected.email = email;
|
|
1748
|
+
continue;
|
|
1749
|
+
}
|
|
1750
|
+
if (key === "connected since") {
|
|
1751
|
+
const connected = connectedSinceValue(raw);
|
|
1752
|
+
if (connected !== null) collected.connectedSince = connected;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
if (collected.email === undefined && collected.connectedSince === undefined) return null;
|
|
1756
|
+
return Object.freeze({
|
|
1757
|
+
emailAddress: collected.email ?? null,
|
|
1758
|
+
connectedAt: collected.connectedSince ?? null,
|
|
1759
|
+
phoneNumbers: collected.phones,
|
|
1760
|
+
websites: collected.websites,
|
|
1761
|
+
birthDateOn: collected.birthday ?? null,
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
function normalizeOverlayPayload(value: unknown): unknown {
|
|
1766
|
+
if (typeof value !== "string") return value;
|
|
1767
|
+
if (value.length < 1 || value.length > MAX_HTML_BYTES) {
|
|
1768
|
+
throw new Error("LinkedIn contact-info overlay payload exceeded its reviewed bound");
|
|
1769
|
+
}
|
|
1770
|
+
const trimmed = value.trim();
|
|
1771
|
+
if (looksLikeRscFlight(trimmed)) return decodeRscFlightRecords(trimmed);
|
|
1772
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) return parseJsonRoot(trimmed);
|
|
1773
|
+
const mailto = overlayMailtoFields(trimmed);
|
|
1774
|
+
return mailto === null ? trimmed : [mailto];
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
const LINKEDIN_CONTACT_OVERLAY_OMITTED_FIELDS =
|
|
1778
|
+
"LinkedIn contact-info overlay omitted its contact fields";
|
|
1779
|
+
|
|
1780
|
+
function overlayHasProjectedField(fields: LinkedInContactFields): boolean {
|
|
1781
|
+
return fields.email !== null
|
|
1782
|
+
|| fields.connectedSince !== null
|
|
1783
|
+
|| fields.phones.length > 0
|
|
1784
|
+
|| fields.websites.length > 0
|
|
1785
|
+
|| fields.birthday !== null;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
function leftoverOverlayMailtoEmails(value: string): string[] {
|
|
1789
|
+
return [...new Set(
|
|
1790
|
+
[...value.matchAll(/mailto:([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,24})/giu)]
|
|
1791
|
+
.map((match) => match[1]?.toLowerCase())
|
|
1792
|
+
.filter((item): item is string => item !== undefined),
|
|
1793
|
+
)];
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
function projectOverlayContactFields(
|
|
1797
|
+
overlay: unknown,
|
|
1798
|
+
vanity: string,
|
|
1799
|
+
): LinkedInContactFields {
|
|
1800
|
+
const normalized = normalizeOverlayPayload(overlay);
|
|
1801
|
+
if (typeof normalized !== "string") return projectFields(normalized, vanity);
|
|
1802
|
+
if (leftoverOverlayMailtoEmails(normalized).length > 1) {
|
|
1803
|
+
throw new Error("LinkedIn contact-info email was ambiguous");
|
|
1804
|
+
}
|
|
1805
|
+
try {
|
|
1806
|
+
const embedded = projectLinkedInEmbeddedContactFields(normalized, vanity);
|
|
1807
|
+
if (embedded !== undefined && overlayHasProjectedField(embedded)) return embedded;
|
|
1808
|
+
} catch (error) {
|
|
1809
|
+
if (
|
|
1810
|
+
!(error instanceof Error)
|
|
1811
|
+
|| !/bootstrap|HTML bound|too many code payloads/u.test(error.message)
|
|
1812
|
+
) {
|
|
1813
|
+
throw error;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
throw new Error(LINKEDIN_CONTACT_OVERLAY_OMITTED_FIELDS);
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
export function projectLinkedInOverlayContactFields(
|
|
1820
|
+
overlay: unknown,
|
|
1821
|
+
vanity: string,
|
|
1822
|
+
): LinkedInContactFields {
|
|
1823
|
+
return projectOverlayContactFields(overlay, vanity);
|
|
819
1824
|
}
|
|
820
1825
|
|
|
821
1826
|
function oneUnique<T>(
|
|
@@ -965,8 +1970,13 @@ function websitesFromUnknown(value: unknown): readonly string[] {
|
|
|
965
1970
|
}));
|
|
966
1971
|
}
|
|
967
1972
|
|
|
968
|
-
function
|
|
969
|
-
|
|
1973
|
+
function optionalLabelKey(value: unknown): string | undefined {
|
|
1974
|
+
if (typeof value !== "string") return undefined;
|
|
1975
|
+
const trimmed = value.trim();
|
|
1976
|
+
if (trimmed.length < 1 || trimmed.length > 64 || /[\0\r]/u.test(trimmed)) {
|
|
1977
|
+
return undefined;
|
|
1978
|
+
}
|
|
1979
|
+
return trimmed.toLowerCase();
|
|
970
1980
|
}
|
|
971
1981
|
|
|
972
1982
|
function labeledFields(value: unknown): JsonRecord | null {
|
|
@@ -984,7 +1994,8 @@ function labeledFields(value: unknown): JsonRecord | null {
|
|
|
984
1994
|
const label = item.label ?? item.title ?? item.heading;
|
|
985
1995
|
const raw = item.value ?? item.text ?? item.href;
|
|
986
1996
|
if (typeof label !== "string") continue;
|
|
987
|
-
const key =
|
|
1997
|
+
const key = optionalLabelKey(label);
|
|
1998
|
+
if (key === undefined) continue;
|
|
988
1999
|
if (key === "email") {
|
|
989
2000
|
const email = optionalEmail(raw, "LinkedIn contact-info Email");
|
|
990
2001
|
if (email !== null) collected.email = email;
|
|
@@ -1049,15 +2060,25 @@ function collectRecords(value: unknown): readonly JsonRecord[] {
|
|
|
1049
2060
|
if (next.value.length > 20_000) {
|
|
1050
2061
|
throw new Error("LinkedIn contact-info payload array exceeded its reviewed bound");
|
|
1051
2062
|
}
|
|
1052
|
-
const labeled = labeledFields(next.value);
|
|
2063
|
+
const labeled = labeledFields(next.value) ?? overlayTextPairFields(next.value);
|
|
1053
2064
|
if (labeled !== null) records.push(labeled);
|
|
1054
2065
|
for (const item of next.value) stack.push({ value: item, depth: next.depth + 1 });
|
|
1055
2066
|
continue;
|
|
1056
2067
|
}
|
|
1057
2068
|
if (!isRecord(next.value)) continue;
|
|
1058
2069
|
records.push(next.value);
|
|
1059
|
-
const nestedLabels = labeledFields(next.value.fields ?? next.value.items ?? next.value.rows)
|
|
2070
|
+
const nestedLabels = labeledFields(next.value.fields ?? next.value.items ?? next.value.rows)
|
|
2071
|
+
?? overlayTextPairFields(
|
|
2072
|
+
Array.isArray(next.value.fields) ? next.value.fields
|
|
2073
|
+
: Array.isArray(next.value.items) ? next.value.items
|
|
2074
|
+
: Array.isArray(next.value.rows) ? next.value.rows
|
|
2075
|
+
: [],
|
|
2076
|
+
);
|
|
1060
2077
|
if (nestedLabels !== null) records.push(nestedLabels);
|
|
2078
|
+
if (typeof next.value.href === "string" || typeof next.value.url === "string") {
|
|
2079
|
+
const mailto = overlayMailtoFields(String(next.value.href ?? next.value.url));
|
|
2080
|
+
if (mailto !== null) records.push(mailto);
|
|
2081
|
+
}
|
|
1061
2082
|
for (const item of Object.values(next.value)) {
|
|
1062
2083
|
stack.push({ value: item, depth: next.depth + 1 });
|
|
1063
2084
|
}
|
|
@@ -1116,7 +2137,7 @@ export function projectLinkedInContactInfo(input: {
|
|
|
1116
2137
|
profileUrl: input.profileUrl,
|
|
1117
2138
|
expectedViewerSubject: input.expectedViewerSubject,
|
|
1118
2139
|
});
|
|
1119
|
-
const contact =
|
|
2140
|
+
const contact = projectOverlayContactFields(input.contactPayload, binding.vanity);
|
|
1120
2141
|
const hasAny = contact.email !== null
|
|
1121
2142
|
|| contact.connectedSince !== null
|
|
1122
2143
|
|| contact.phones.length > 0
|