@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
|
@@ -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;
|
|
@@ -42,7 +96,33 @@ const MAX_CODE_TAGS = 256;
|
|
|
42
96
|
const MAX_COMO_ASSIGNMENTS = 8;
|
|
43
97
|
const MAX_COMO_DECODED_ROOTS = 256;
|
|
44
98
|
const MAX_WALK_NODES = 500_000;
|
|
99
|
+
const MAX_WALK_DEPTH = 128;
|
|
45
100
|
const RSC_FLIGHT_ROW = /(?:^|\n)(\d+):/u;
|
|
101
|
+
const PROFILE_ID = /^[A-Za-z0-9_-]{1,256}$/u;
|
|
102
|
+
const DISTANCE_IN_TEXT =
|
|
103
|
+
/(?:^|["'\\{,])(?:networkDistance|memberDistance|distance)\\*"?\s*:\s*\\*"?(DISTANCE_[A-Z0-9]+|OUT_OF_NETWORK|SELF|[0-9]+)\\*"?/gu;
|
|
104
|
+
const PROFILE_URN_IN_TEXT = /urn:li:fsd_profile:[A-Za-z0-9_-]{1,256}/gu;
|
|
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;
|
|
110
|
+
const VANITY_IN_TEXT =
|
|
111
|
+
/(?:vanityName|publicIdentifier)\\*"?\s*:\s*\\*"?([A-Za-z0-9][A-Za-z0-9_-]{1,99})/gu;
|
|
112
|
+
const MAX_JSON_STRING_PEELS = 4;
|
|
113
|
+
const KEYED_IDENTITY_KEYS = new Set([
|
|
114
|
+
"distance",
|
|
115
|
+
"entityUrn",
|
|
116
|
+
"isSelfView",
|
|
117
|
+
"memberDistance",
|
|
118
|
+
"networkDistance",
|
|
119
|
+
"objectUrn",
|
|
120
|
+
"profileUrn",
|
|
121
|
+
"publicIdentifier",
|
|
122
|
+
"vanityName",
|
|
123
|
+
"vieweeMemberUrn",
|
|
124
|
+
"vieweeProfileId",
|
|
125
|
+
]);
|
|
46
126
|
const MAX_PHONES = 8;
|
|
47
127
|
const MAX_WEBSITES = 8;
|
|
48
128
|
const CONTACT_TYPE_SUFFIX = "ProfileContactInfo";
|
|
@@ -83,6 +163,49 @@ export type LinkedInContactInfoJsonInput = {
|
|
|
83
163
|
readonly queryId?: string;
|
|
84
164
|
};
|
|
85
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
|
+
|
|
86
209
|
type JsonRecord = Readonly<Record<string, unknown>>;
|
|
87
210
|
|
|
88
211
|
function isRecord(value: unknown): value is JsonRecord {
|
|
@@ -153,6 +276,82 @@ export function linkedInProfileContactInfoGraphqlUrl(input: {
|
|
|
153
276
|
return new URL(buildLinkedInProfileContactInfoGraphqlPath(input), LINKEDIN_ORIGIN);
|
|
154
277
|
}
|
|
155
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
|
+
|
|
156
355
|
export function resolveLinkedInProfileContactInfoQueryId(html: unknown): string | undefined {
|
|
157
356
|
if (typeof html !== "string" || html.length < 1 || html.length > MAX_HTML_BYTES) {
|
|
158
357
|
return undefined;
|
|
@@ -167,6 +366,51 @@ export function resolveLinkedInProfileContactInfoQueryId(html: unknown): string
|
|
|
167
366
|
return unique.values().next().value;
|
|
168
367
|
}
|
|
169
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
|
+
|
|
170
414
|
function assertLinkedInContactInfoGraphqlUrl(url: URL): void {
|
|
171
415
|
const queryNames = [...url.searchParams.keys()];
|
|
172
416
|
const queryKey = queryNames[1];
|
|
@@ -199,7 +443,9 @@ export function assertLinkedInContactInfoRequest(requestValue: unknown): void {
|
|
|
199
443
|
}
|
|
200
444
|
const method = boundedText(requestValue.method, "LinkedIn contact-info request method", 16)
|
|
201
445
|
.toUpperCase();
|
|
202
|
-
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
|
+
}
|
|
203
449
|
const rawUrl = requestValue.url;
|
|
204
450
|
if (!(rawUrl instanceof URL) && typeof rawUrl !== "string") {
|
|
205
451
|
throw new Error("LinkedIn contact-info request URL is invalid");
|
|
@@ -211,6 +457,13 @@ export function assertLinkedInContactInfoRequest(requestValue: unknown): void {
|
|
|
211
457
|
&& url.password === ""
|
|
212
458
|
&& url.hash === ""
|
|
213
459
|
) {
|
|
460
|
+
if (method === "POST") {
|
|
461
|
+
assertLinkedInContactInfoNavigationPostUrl(url);
|
|
462
|
+
if (requestValue.body !== undefined) {
|
|
463
|
+
assertLinkedInContactInfoNavigationBody(requestValue.body);
|
|
464
|
+
}
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
214
467
|
if (url.search === "" && url.pathname === "/voyager/api/me") return;
|
|
215
468
|
if (url.search === "" && /^\/in\/[A-Za-z0-9][A-Za-z0-9_-]{1,99}\/$/u.test(url.pathname)) {
|
|
216
469
|
linkedInPersonalProfileTarget(url.href);
|
|
@@ -220,6 +473,10 @@ export function assertLinkedInContactInfoRequest(requestValue: unknown): void {
|
|
|
220
473
|
assertLinkedInContactInfoGraphqlUrl(url);
|
|
221
474
|
return;
|
|
222
475
|
}
|
|
476
|
+
if (LINKEDIN_CONTACT_INFO_OVERLAY_PATH.test(url.pathname)) {
|
|
477
|
+
assertLinkedInContactInfoOverlayUrl(url);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
223
480
|
}
|
|
224
481
|
throw new Error("LinkedIn contact-info request escaped its exact reviewed route");
|
|
225
482
|
}
|
|
@@ -315,6 +572,171 @@ function looksLikeRscFlight(value: string): boolean {
|
|
|
315
572
|
return RSC_FLIGHT_ROW.test(value);
|
|
316
573
|
}
|
|
317
574
|
|
|
575
|
+
function extractJsonString(source: string, start: number): string | undefined {
|
|
576
|
+
if (source[start] !== "\"") return undefined;
|
|
577
|
+
let escaped = false;
|
|
578
|
+
const limit = Math.min(source.length, start + MAX_HTML_BYTES);
|
|
579
|
+
for (let index = start + 1; index < limit; index += 1) {
|
|
580
|
+
const character = source[index];
|
|
581
|
+
if (character === undefined) break;
|
|
582
|
+
if (escaped) {
|
|
583
|
+
escaped = false;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
if (character === "\\") {
|
|
587
|
+
escaped = true;
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
if (character === "\"") return source.slice(start, index + 1);
|
|
591
|
+
}
|
|
592
|
+
return undefined;
|
|
593
|
+
}
|
|
594
|
+
|
|
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 {
|
|
651
|
+
const distances = [...value.matchAll(DISTANCE_IN_TEXT)]
|
|
652
|
+
.map((match) => match[1])
|
|
653
|
+
.filter((item): item is string => item !== undefined);
|
|
654
|
+
const urns = [...value.matchAll(PROFILE_URN_IN_TEXT)]
|
|
655
|
+
.map((match) => match[0])
|
|
656
|
+
.filter((item): item is string => item !== undefined);
|
|
657
|
+
const vieweeIds = [...value.matchAll(VIEWEE_PROFILE_ID_IN_TEXT)]
|
|
658
|
+
.map((match) => match[1])
|
|
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);
|
|
663
|
+
const vanities = [...value.matchAll(VANITY_IN_TEXT)]
|
|
664
|
+
.map((match) => match[1])
|
|
665
|
+
.filter((item): item is string => item !== undefined);
|
|
666
|
+
if (
|
|
667
|
+
distances.length === 0
|
|
668
|
+
&& urns.length === 0
|
|
669
|
+
&& vieweeIds.length === 0
|
|
670
|
+
&& vieweeMembers.length === 0
|
|
671
|
+
&& vanities.length === 0
|
|
672
|
+
) return null;
|
|
673
|
+
const record: Record<string, unknown> = {};
|
|
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;
|
|
683
|
+
}
|
|
684
|
+
if (vieweeIds.length === 1) record.vieweeProfileId = vieweeIds[0];
|
|
685
|
+
if (vanities.length === 1) record.vanityName = vanities[0];
|
|
686
|
+
return Object.keys(record).length > 0 ? Object.freeze(record) : null;
|
|
687
|
+
}
|
|
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
|
+
|
|
697
|
+
function stringLooksLikeBootstrap(value: string): boolean {
|
|
698
|
+
return value.includes("networkDistance")
|
|
699
|
+
|| value.includes("memberDistance")
|
|
700
|
+
|| value.includes("vieweeProfileId")
|
|
701
|
+
|| value.includes("vieweeMemberUrn")
|
|
702
|
+
|| value.includes("vanityName")
|
|
703
|
+
|| value.includes("publicIdentifier")
|
|
704
|
+
|| /"distance"\s*:/u.test(value);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function decodeStringBootstrap(value: string): unknown[] {
|
|
708
|
+
const trimmed = value.trim();
|
|
709
|
+
if (trimmed.length < 8 || trimmed.length > 1024 * 1024) return [];
|
|
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
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (!stringLooksLikeBootstrap(trimmed) && !stringLooksLikeBootstrap(peeled)) {
|
|
721
|
+
return [];
|
|
722
|
+
}
|
|
723
|
+
const breadcrumb = breadcrumbRecordFromText(peeled);
|
|
724
|
+
return breadcrumb === null ? [] : [breadcrumb];
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function recordFromKeyedArray(value: readonly unknown[]): JsonRecord | null {
|
|
728
|
+
if (value.length < 2 || value.length % 2 !== 0 || value.length > 64) return null;
|
|
729
|
+
const record: Record<string, unknown> = {};
|
|
730
|
+
let recognized = 0;
|
|
731
|
+
for (let index = 0; index < value.length; index += 2) {
|
|
732
|
+
const key = value[index];
|
|
733
|
+
if (typeof key !== "string" || key.length < 1 || key.length > 128) return null;
|
|
734
|
+
record[key] = value[index + 1];
|
|
735
|
+
if (KEYED_IDENTITY_KEYS.has(key)) recognized += 1;
|
|
736
|
+
}
|
|
737
|
+
return recognized > 0 ? Object.freeze(record) : null;
|
|
738
|
+
}
|
|
739
|
+
|
|
318
740
|
function decodeRscFlightRecords(text: string): unknown[] {
|
|
319
741
|
const records: unknown[] = [];
|
|
320
742
|
let index = 0;
|
|
@@ -334,7 +756,28 @@ function decodeRscFlightRecords(text: string): unknown[] {
|
|
|
334
756
|
index += json.length;
|
|
335
757
|
continue;
|
|
336
758
|
}
|
|
337
|
-
|
|
759
|
+
if (first === "\"") {
|
|
760
|
+
const json = extractJsonString(text, index);
|
|
761
|
+
if (json === undefined) {
|
|
762
|
+
index += 1;
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
try {
|
|
766
|
+
const decoded = JSON.parse(json) as unknown;
|
|
767
|
+
if (typeof decoded === "string") records.push(...decodeStringBootstrap(decoded));
|
|
768
|
+
else records.push(decoded);
|
|
769
|
+
} catch {
|
|
770
|
+
const breadcrumb = breadcrumbRecordFromText(json);
|
|
771
|
+
if (breadcrumb !== null) records.push(breadcrumb);
|
|
772
|
+
}
|
|
773
|
+
index += json.length;
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
const remainder = text.slice(index);
|
|
777
|
+
const nextRow = remainder.search(/\n\d+:/u);
|
|
778
|
+
const rowText = nextRow === -1 ? remainder : remainder.slice(0, nextRow);
|
|
779
|
+
const breadcrumb = breadcrumbRecordFromText(rowText);
|
|
780
|
+
if (breadcrumb !== null) records.push(breadcrumb);
|
|
338
781
|
index = nextRow === -1 ? text.length : index + nextRow + 1;
|
|
339
782
|
}
|
|
340
783
|
return records;
|
|
@@ -342,7 +785,9 @@ function decodeRscFlightRecords(text: string): unknown[] {
|
|
|
342
785
|
|
|
343
786
|
function decodeComoRehydrationValue(value: unknown): unknown[] {
|
|
344
787
|
if (typeof value === "string") {
|
|
345
|
-
return looksLikeRscFlight(value)
|
|
788
|
+
return looksLikeRscFlight(value)
|
|
789
|
+
? decodeRscFlightRecords(value)
|
|
790
|
+
: decodeStringBootstrap(value);
|
|
346
791
|
}
|
|
347
792
|
if (Array.isArray(value)) {
|
|
348
793
|
if (value.length > 20_000) {
|
|
@@ -433,13 +878,21 @@ function embeddedRecords(html: unknown): readonly JsonRecord[] {
|
|
|
433
878
|
while (stack.length > 0) {
|
|
434
879
|
const next = stack.pop()!;
|
|
435
880
|
nodes += 1;
|
|
436
|
-
if (nodes > MAX_WALK_NODES || next.depth >
|
|
881
|
+
if (nodes > MAX_WALK_NODES || next.depth > MAX_WALK_DEPTH) {
|
|
437
882
|
throw new Error("LinkedIn contact-info bootstrap exceeded its traversal bound");
|
|
438
883
|
}
|
|
884
|
+
if (typeof next.value === "string") {
|
|
885
|
+
for (const value of decodeStringBootstrap(next.value)) {
|
|
886
|
+
stack.push({ value, depth: next.depth + 1 });
|
|
887
|
+
}
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
439
890
|
if (Array.isArray(next.value)) {
|
|
440
891
|
if (next.value.length > 20_000) {
|
|
441
892
|
throw new Error("LinkedIn contact-info bootstrap array exceeded its reviewed bound");
|
|
442
893
|
}
|
|
894
|
+
const keyed = recordFromKeyedArray(next.value);
|
|
895
|
+
if (keyed !== null) records.push(keyed);
|
|
443
896
|
for (const value of next.value) stack.push({ value, depth: next.depth + 1 });
|
|
444
897
|
continue;
|
|
445
898
|
}
|
|
@@ -505,25 +958,160 @@ function vanityFromRecord(record: JsonRecord): string | null {
|
|
|
505
958
|
?? vanityFromHref(record.navigationUrl);
|
|
506
959
|
}
|
|
507
960
|
|
|
508
|
-
function
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
if (typeof value !== "string") continue;
|
|
961
|
+
function profileUrnFromIdentity(value: unknown): string | null {
|
|
962
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 512) return null;
|
|
963
|
+
if (value.startsWith("urn:li:fsd_profile:")) {
|
|
512
964
|
try {
|
|
513
965
|
return profileUrn(value);
|
|
514
966
|
} catch {
|
|
515
|
-
|
|
967
|
+
return null;
|
|
516
968
|
}
|
|
517
969
|
}
|
|
970
|
+
if (!PROFILE_ID.test(value)) return null;
|
|
971
|
+
try {
|
|
972
|
+
return profileUrn(`urn:li:fsd_profile:${value}`);
|
|
973
|
+
} catch {
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function profileUrnFromRecord(record: JsonRecord): string | null {
|
|
979
|
+
for (const key of ["entityUrn", "objectUrn", "profileUrn", "vieweeMemberUrn"] as const) {
|
|
980
|
+
const urn = profileUrnFromIdentity(record[key]);
|
|
981
|
+
if (urn !== null) return urn;
|
|
982
|
+
}
|
|
983
|
+
return profileUrnFromIdentity(record.vieweeProfileId);
|
|
984
|
+
}
|
|
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
|
+
}
|
|
518
1001
|
return null;
|
|
519
1002
|
}
|
|
520
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
|
+
|
|
1012
|
+
function recordIsSelfView(record: JsonRecord): boolean {
|
|
1013
|
+
return record.isSelfView === true;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
521
1016
|
function selfProfileError(): Error {
|
|
522
1017
|
return new Error(
|
|
523
1018
|
"LinkedIn contacts.read reads one 1st-degree connection; use profiles.read for the signed-in self profile",
|
|
524
1019
|
);
|
|
525
1020
|
}
|
|
526
1021
|
|
|
1022
|
+
function omittedDistanceError(): Error {
|
|
1023
|
+
return new Error(
|
|
1024
|
+
"LinkedIn contact-info profile page omitted or contradicted its relationship distance",
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function uniqueDistances(values: readonly (string | number)[]): (string | number)[] {
|
|
1029
|
+
const unique = [...new Set(values.map((value) => String(value)))];
|
|
1030
|
+
return unique.map((value) => values.find((candidate) => String(candidate) === value)!);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function recordMatchesTarget(
|
|
1034
|
+
record: JsonRecord,
|
|
1035
|
+
slug: string,
|
|
1036
|
+
urn: string,
|
|
1037
|
+
): boolean {
|
|
1038
|
+
const vanity = vanityFromRecord(record);
|
|
1039
|
+
const recordUrn = profileUrnFromRecord(record);
|
|
1040
|
+
return vanity === slug || recordUrn === urn;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function recordContradictsTarget(
|
|
1044
|
+
record: JsonRecord,
|
|
1045
|
+
slug: string,
|
|
1046
|
+
urn: string,
|
|
1047
|
+
): boolean {
|
|
1048
|
+
const vanity = vanityFromRecord(record);
|
|
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];
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function relationshipDistance(
|
|
1072
|
+
records: readonly JsonRecord[],
|
|
1073
|
+
slug: string,
|
|
1074
|
+
urn: string,
|
|
1075
|
+
viewer: string,
|
|
1076
|
+
): string | number {
|
|
1077
|
+
const bound = records
|
|
1078
|
+
.filter((record) => recordMatchesTarget(record, slug, urn))
|
|
1079
|
+
.map(distanceValue)
|
|
1080
|
+
.filter((value): value is string | number => value !== null);
|
|
1081
|
+
const boundDistance = uniqueNonSelfDistance(bound);
|
|
1082
|
+
if (boundDistance !== undefined) return boundDistance;
|
|
1083
|
+
if (bound.some(isSelfDistance) && urn === viewer) throw selfProfileError();
|
|
1084
|
+
|
|
1085
|
+
const vieweeJoined = records
|
|
1086
|
+
.filter((record) => (
|
|
1087
|
+
!recordContradictsTarget(record, slug, urn)
|
|
1088
|
+
&& !recordIsViewerViewee(record, viewer)
|
|
1089
|
+
&& vieweeMemberIdentity(record) !== null
|
|
1090
|
+
))
|
|
1091
|
+
.map(distanceValue)
|
|
1092
|
+
.filter((value): value is string | number => value !== null);
|
|
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();
|
|
1097
|
+
}
|
|
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)))) {
|
|
1110
|
+
throw selfProfileError();
|
|
1111
|
+
}
|
|
1112
|
+
throw omittedDistanceError();
|
|
1113
|
+
}
|
|
1114
|
+
|
|
527
1115
|
export function projectLinkedInProfileContactBinding(input: {
|
|
528
1116
|
readonly profileHtml: unknown;
|
|
529
1117
|
readonly profileUrl: unknown;
|
|
@@ -536,18 +1124,23 @@ export function projectLinkedInProfileContactBinding(input: {
|
|
|
536
1124
|
if (vanityRecords.length < 1) {
|
|
537
1125
|
throw new Error("LinkedIn contact-info profile page did not bind the requested vanity");
|
|
538
1126
|
}
|
|
1127
|
+
if (vanityRecords.some(recordIsSelfView)) throw selfProfileError();
|
|
539
1128
|
const urns = new Set<string>();
|
|
540
1129
|
for (const record of vanityRecords) {
|
|
541
|
-
const urn =
|
|
1130
|
+
const urn = durableProfileUrnFromRecord(record);
|
|
542
1131
|
if (urn !== null) urns.add(urn);
|
|
543
1132
|
}
|
|
544
1133
|
if (urns.size < 1) {
|
|
545
1134
|
const joined = new Set<string>();
|
|
546
1135
|
let sawSelf = false;
|
|
547
1136
|
for (const record of records) {
|
|
548
|
-
const urn =
|
|
1137
|
+
const urn = durableProfileUrnFromRecord(record);
|
|
549
1138
|
const distance = distanceValue(record);
|
|
550
|
-
if (
|
|
1139
|
+
if (
|
|
1140
|
+
recordIsSelfView(record)
|
|
1141
|
+
|| urn === viewer
|
|
1142
|
+
|| (distance !== null && isSelfDistance(distance))
|
|
1143
|
+
) {
|
|
551
1144
|
sawSelf = true;
|
|
552
1145
|
}
|
|
553
1146
|
if (urn === null || urn === viewer || distance === null) continue;
|
|
@@ -567,17 +1160,7 @@ export function projectLinkedInProfileContactBinding(input: {
|
|
|
567
1160
|
}
|
|
568
1161
|
const urn = urns.values().next().value!;
|
|
569
1162
|
if (urn === viewer) throw selfProfileError();
|
|
570
|
-
const
|
|
571
|
-
vanityFromRecord(record) === target.slug || profileUrnFromRecord(record) === urn
|
|
572
|
-
);
|
|
573
|
-
const distances = related
|
|
574
|
-
.map(distanceValue)
|
|
575
|
-
.filter((value): value is string | number => value !== null);
|
|
576
|
-
const unique = [...new Set(distances.map((value) => String(value)))];
|
|
577
|
-
if (unique.length !== 1 || distances[0] === undefined) {
|
|
578
|
-
throw new Error("LinkedIn contact-info profile page omitted or contradicted its relationship distance");
|
|
579
|
-
}
|
|
580
|
-
const distance = distances[0];
|
|
1163
|
+
const distance = relationshipDistance(records, target.slug, urn, viewer);
|
|
581
1164
|
if (isSelfDistance(distance)) throw selfProfileError();
|
|
582
1165
|
if (!isFirstDegree(distance)) {
|
|
583
1166
|
throw new Error(
|
|
@@ -592,14 +1175,603 @@ export function projectLinkedInProfileContactBinding(input: {
|
|
|
592
1175
|
});
|
|
593
1176
|
}
|
|
594
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 extractLinkedInContactNavigationAction(input: {
|
|
1575
|
+
readonly profileHtml: unknown;
|
|
1576
|
+
readonly publicIdentifier: string;
|
|
1577
|
+
readonly profileUrn: string;
|
|
1578
|
+
}): LinkedInContactNavigationActionResult {
|
|
1579
|
+
const publicIdentifier = linkedInPersonalProfilePublicIdentifier(input.publicIdentifier);
|
|
1580
|
+
profileUrn(input.profileUrn);
|
|
1581
|
+
const html = typeof input.profileHtml === "string" ? input.profileHtml : "";
|
|
1582
|
+
const records = embeddedRecords(input.profileHtml);
|
|
1583
|
+
const candidates = collectContactNavigationCandidates(records, html);
|
|
1584
|
+
if (candidates.length < 1) return { kind: "absent" };
|
|
1585
|
+
if (candidates.some((candidate) => candidate.sduiid.kind === "unreviewed")) {
|
|
1586
|
+
return { kind: "unreviewed-sduiid" };
|
|
1587
|
+
}
|
|
1588
|
+
const sduiids = [...new Set(
|
|
1589
|
+
candidates
|
|
1590
|
+
.map((candidate) => candidate.sduiid.kind === "reviewed" ? candidate.sduiid.sduiid : undefined)
|
|
1591
|
+
.filter((value): value is string => value !== undefined),
|
|
1592
|
+
)];
|
|
1593
|
+
if (sduiids.length > 1) return { kind: "ambiguous-sduiid" };
|
|
1594
|
+
let reviewed:
|
|
1595
|
+
| { readonly kind: "args"; readonly args: LinkedInContactNavigationClientArguments }
|
|
1596
|
+
| { readonly kind: "empty" }
|
|
1597
|
+
| undefined;
|
|
1598
|
+
for (const candidate of candidates) {
|
|
1599
|
+
const peeled = reviewedContactNavigationClientArguments(candidate.clientArguments);
|
|
1600
|
+
if (peeled.kind === "unreviewed") {
|
|
1601
|
+
return { kind: "unreviewed-client-arguments", keys: peeled.keys };
|
|
1602
|
+
}
|
|
1603
|
+
if (peeled.kind === "args") {
|
|
1604
|
+
if (!clientArgumentsBindTarget(peeled.args, publicIdentifier)) {
|
|
1605
|
+
return { kind: "unbound-client-arguments" };
|
|
1606
|
+
}
|
|
1607
|
+
reviewed = peeled;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
const clientArguments = reviewed?.kind === "args"
|
|
1611
|
+
? canonicalContactNavigationClientArguments({
|
|
1612
|
+
...reviewed.args,
|
|
1613
|
+
payload: {
|
|
1614
|
+
...reviewed.args.payload,
|
|
1615
|
+
vanityName: reviewed.args.payload.vanityName ?? publicIdentifier,
|
|
1616
|
+
},
|
|
1617
|
+
})
|
|
1618
|
+
: canonicalContactNavigationClientArguments({
|
|
1619
|
+
payload: { vanityName: publicIdentifier },
|
|
1620
|
+
});
|
|
1621
|
+
if (!clientArgumentsBindTarget(clientArguments, publicIdentifier)) {
|
|
1622
|
+
return { kind: "unbound-client-arguments" };
|
|
1623
|
+
}
|
|
1624
|
+
return {
|
|
1625
|
+
kind: "action",
|
|
1626
|
+
action: Object.freeze({
|
|
1627
|
+
sduiid: LINKEDIN_CONTACT_DETAILS_OVERLAY_SCREEN_ID,
|
|
1628
|
+
clientArguments,
|
|
1629
|
+
isModal: true,
|
|
1630
|
+
}),
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
|
|
595
1634
|
export function projectLinkedInEmbeddedContactFields(
|
|
596
1635
|
html: unknown,
|
|
597
1636
|
vanity: string,
|
|
598
1637
|
): LinkedInContactFields | undefined {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
1638
|
+
try {
|
|
1639
|
+
const records = embeddedRecords(html);
|
|
1640
|
+
const entities = collectRecords(records).filter(contactEntity);
|
|
1641
|
+
if (entities.length < 1) return undefined;
|
|
1642
|
+
return projectFields(records, vanity);
|
|
1643
|
+
} catch (error) {
|
|
1644
|
+
if (error instanceof Error && error.message === LINKEDIN_CONTACT_INFO_FIELD_LABEL_ERROR) {
|
|
1645
|
+
return undefined;
|
|
1646
|
+
}
|
|
1647
|
+
throw error;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
function overlayMailtoFields(value: string): JsonRecord | null {
|
|
1652
|
+
const emails = [...value.matchAll(/mailto:([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,24})/giu)]
|
|
1653
|
+
.map((match) => match[1]?.toLowerCase())
|
|
1654
|
+
.filter((item): item is string => item !== undefined);
|
|
1655
|
+
const unique = [...new Set(emails)];
|
|
1656
|
+
if (unique.length !== 1) return null;
|
|
1657
|
+
return Object.freeze({ emailAddress: unique[0] });
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
function overlayTextPairFields(value: readonly unknown[]): JsonRecord | null {
|
|
1661
|
+
const texts: string[] = [];
|
|
1662
|
+
for (const item of value) {
|
|
1663
|
+
if (typeof item === "string") {
|
|
1664
|
+
const text = item.trim();
|
|
1665
|
+
if (text.length > 0 && text.length <= 2_048) texts.push(text);
|
|
1666
|
+
continue;
|
|
1667
|
+
}
|
|
1668
|
+
if (!isRecord(item)) continue;
|
|
1669
|
+
const text = item.text ?? item.value ?? item.title ?? item.label;
|
|
1670
|
+
if (typeof text === "string") {
|
|
1671
|
+
const trimmed = text.trim();
|
|
1672
|
+
if (trimmed.length > 0 && trimmed.length <= 2_048) texts.push(trimmed);
|
|
1673
|
+
}
|
|
1674
|
+
const href = item.href ?? item.url;
|
|
1675
|
+
if (typeof href === "string" && href.toLowerCase().startsWith("mailto:")) {
|
|
1676
|
+
texts.push(href);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
if (texts.length < 2) return null;
|
|
1680
|
+
const collected: {
|
|
1681
|
+
email?: string;
|
|
1682
|
+
connectedSince?: string;
|
|
1683
|
+
phones: string[];
|
|
1684
|
+
websites: string[];
|
|
1685
|
+
birthday?: string;
|
|
1686
|
+
} = { phones: [], websites: [] };
|
|
1687
|
+
for (let index = 0; index < texts.length - 1; index += 1) {
|
|
1688
|
+
const label = texts[index];
|
|
1689
|
+
const raw = texts[index + 1];
|
|
1690
|
+
if (label === undefined || raw === undefined) continue;
|
|
1691
|
+
const key = optionalLabelKey(label);
|
|
1692
|
+
if (key === undefined) continue;
|
|
1693
|
+
if (key === "email") {
|
|
1694
|
+
const email = optionalEmail(
|
|
1695
|
+
raw.toLowerCase().startsWith("mailto:") ? raw.slice("mailto:".length) : raw,
|
|
1696
|
+
"LinkedIn contact-info Email",
|
|
1697
|
+
);
|
|
1698
|
+
if (email !== null) collected.email = email;
|
|
1699
|
+
continue;
|
|
1700
|
+
}
|
|
1701
|
+
if (key === "connected since") {
|
|
1702
|
+
const connected = connectedSinceValue(raw);
|
|
1703
|
+
if (connected !== null) collected.connectedSince = connected;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
if (collected.email === undefined && collected.connectedSince === undefined) return null;
|
|
1707
|
+
return Object.freeze({
|
|
1708
|
+
emailAddress: collected.email ?? null,
|
|
1709
|
+
connectedAt: collected.connectedSince ?? null,
|
|
1710
|
+
phoneNumbers: collected.phones,
|
|
1711
|
+
websites: collected.websites,
|
|
1712
|
+
birthDateOn: collected.birthday ?? null,
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
function normalizeOverlayPayload(value: unknown): unknown {
|
|
1717
|
+
if (typeof value !== "string") return value;
|
|
1718
|
+
if (value.length < 1 || value.length > MAX_HTML_BYTES) {
|
|
1719
|
+
throw new Error("LinkedIn contact-info overlay payload exceeded its reviewed bound");
|
|
1720
|
+
}
|
|
1721
|
+
const trimmed = value.trim();
|
|
1722
|
+
if (looksLikeRscFlight(trimmed)) return decodeRscFlightRecords(trimmed);
|
|
1723
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) return parseJsonRoot(trimmed);
|
|
1724
|
+
const mailto = overlayMailtoFields(trimmed);
|
|
1725
|
+
return mailto === null ? trimmed : [mailto];
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
const LINKEDIN_CONTACT_OVERLAY_OMITTED_FIELDS =
|
|
1729
|
+
"LinkedIn contact-info overlay omitted its contact fields";
|
|
1730
|
+
|
|
1731
|
+
function overlayHasProjectedField(fields: LinkedInContactFields): boolean {
|
|
1732
|
+
return fields.email !== null
|
|
1733
|
+
|| fields.connectedSince !== null
|
|
1734
|
+
|| fields.phones.length > 0
|
|
1735
|
+
|| fields.websites.length > 0
|
|
1736
|
+
|| fields.birthday !== null;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
function leftoverOverlayMailtoEmails(value: string): string[] {
|
|
1740
|
+
return [...new Set(
|
|
1741
|
+
[...value.matchAll(/mailto:([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,24})/giu)]
|
|
1742
|
+
.map((match) => match[1]?.toLowerCase())
|
|
1743
|
+
.filter((item): item is string => item !== undefined),
|
|
1744
|
+
)];
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
function projectOverlayContactFields(
|
|
1748
|
+
overlay: unknown,
|
|
1749
|
+
vanity: string,
|
|
1750
|
+
): LinkedInContactFields {
|
|
1751
|
+
const normalized = normalizeOverlayPayload(overlay);
|
|
1752
|
+
if (typeof normalized !== "string") return projectFields(normalized, vanity);
|
|
1753
|
+
if (leftoverOverlayMailtoEmails(normalized).length > 1) {
|
|
1754
|
+
throw new Error("LinkedIn contact-info email was ambiguous");
|
|
1755
|
+
}
|
|
1756
|
+
try {
|
|
1757
|
+
const embedded = projectLinkedInEmbeddedContactFields(normalized, vanity);
|
|
1758
|
+
if (embedded !== undefined && overlayHasProjectedField(embedded)) return embedded;
|
|
1759
|
+
} catch (error) {
|
|
1760
|
+
if (
|
|
1761
|
+
!(error instanceof Error)
|
|
1762
|
+
|| !/bootstrap|HTML bound|too many code payloads/u.test(error.message)
|
|
1763
|
+
) {
|
|
1764
|
+
throw error;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
throw new Error(LINKEDIN_CONTACT_OVERLAY_OMITTED_FIELDS);
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
export function projectLinkedInOverlayContactFields(
|
|
1771
|
+
overlay: unknown,
|
|
1772
|
+
vanity: string,
|
|
1773
|
+
): LinkedInContactFields {
|
|
1774
|
+
return projectOverlayContactFields(overlay, vanity);
|
|
603
1775
|
}
|
|
604
1776
|
|
|
605
1777
|
function oneUnique<T>(
|
|
@@ -749,8 +1921,13 @@ function websitesFromUnknown(value: unknown): readonly string[] {
|
|
|
749
1921
|
}));
|
|
750
1922
|
}
|
|
751
1923
|
|
|
752
|
-
function
|
|
753
|
-
|
|
1924
|
+
function optionalLabelKey(value: unknown): string | undefined {
|
|
1925
|
+
if (typeof value !== "string") return undefined;
|
|
1926
|
+
const trimmed = value.trim();
|
|
1927
|
+
if (trimmed.length < 1 || trimmed.length > 64 || /[\0\r]/u.test(trimmed)) {
|
|
1928
|
+
return undefined;
|
|
1929
|
+
}
|
|
1930
|
+
return trimmed.toLowerCase();
|
|
754
1931
|
}
|
|
755
1932
|
|
|
756
1933
|
function labeledFields(value: unknown): JsonRecord | null {
|
|
@@ -768,7 +1945,8 @@ function labeledFields(value: unknown): JsonRecord | null {
|
|
|
768
1945
|
const label = item.label ?? item.title ?? item.heading;
|
|
769
1946
|
const raw = item.value ?? item.text ?? item.href;
|
|
770
1947
|
if (typeof label !== "string") continue;
|
|
771
|
-
const key =
|
|
1948
|
+
const key = optionalLabelKey(label);
|
|
1949
|
+
if (key === undefined) continue;
|
|
772
1950
|
if (key === "email") {
|
|
773
1951
|
const email = optionalEmail(raw, "LinkedIn contact-info Email");
|
|
774
1952
|
if (email !== null) collected.email = email;
|
|
@@ -826,22 +2004,32 @@ function collectRecords(value: unknown): readonly JsonRecord[] {
|
|
|
826
2004
|
while (stack.length > 0) {
|
|
827
2005
|
const next = stack.pop()!;
|
|
828
2006
|
nodes += 1;
|
|
829
|
-
if (nodes > MAX_WALK_NODES || next.depth >
|
|
2007
|
+
if (nodes > MAX_WALK_NODES || next.depth > MAX_WALK_DEPTH) {
|
|
830
2008
|
throw new Error("LinkedIn contact-info payload exceeded its traversal bound");
|
|
831
2009
|
}
|
|
832
2010
|
if (Array.isArray(next.value)) {
|
|
833
2011
|
if (next.value.length > 20_000) {
|
|
834
2012
|
throw new Error("LinkedIn contact-info payload array exceeded its reviewed bound");
|
|
835
2013
|
}
|
|
836
|
-
const labeled = labeledFields(next.value);
|
|
2014
|
+
const labeled = labeledFields(next.value) ?? overlayTextPairFields(next.value);
|
|
837
2015
|
if (labeled !== null) records.push(labeled);
|
|
838
2016
|
for (const item of next.value) stack.push({ value: item, depth: next.depth + 1 });
|
|
839
2017
|
continue;
|
|
840
2018
|
}
|
|
841
2019
|
if (!isRecord(next.value)) continue;
|
|
842
2020
|
records.push(next.value);
|
|
843
|
-
const nestedLabels = labeledFields(next.value.fields ?? next.value.items ?? next.value.rows)
|
|
2021
|
+
const nestedLabels = labeledFields(next.value.fields ?? next.value.items ?? next.value.rows)
|
|
2022
|
+
?? overlayTextPairFields(
|
|
2023
|
+
Array.isArray(next.value.fields) ? next.value.fields
|
|
2024
|
+
: Array.isArray(next.value.items) ? next.value.items
|
|
2025
|
+
: Array.isArray(next.value.rows) ? next.value.rows
|
|
2026
|
+
: [],
|
|
2027
|
+
);
|
|
844
2028
|
if (nestedLabels !== null) records.push(nestedLabels);
|
|
2029
|
+
if (typeof next.value.href === "string" || typeof next.value.url === "string") {
|
|
2030
|
+
const mailto = overlayMailtoFields(String(next.value.href ?? next.value.url));
|
|
2031
|
+
if (mailto !== null) records.push(mailto);
|
|
2032
|
+
}
|
|
845
2033
|
for (const item of Object.values(next.value)) {
|
|
846
2034
|
stack.push({ value: item, depth: next.depth + 1 });
|
|
847
2035
|
}
|
|
@@ -900,7 +2088,7 @@ export function projectLinkedInContactInfo(input: {
|
|
|
900
2088
|
profileUrl: input.profileUrl,
|
|
901
2089
|
expectedViewerSubject: input.expectedViewerSubject,
|
|
902
2090
|
});
|
|
903
|
-
const contact =
|
|
2091
|
+
const contact = projectOverlayContactFields(input.contactPayload, binding.vanity);
|
|
904
2092
|
const hasAny = contact.email !== null
|
|
905
2093
|
|| contact.connectedSince !== null
|
|
906
2094
|
|| contact.phones.length > 0
|