@hraness/ghostget 0.17.2 → 0.17.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 +11 -0
- package/README.md +6 -6
- package/dist/apple-photos-client.js +1 -1
- package/dist/beeper-client.js +1 -1
- package/dist/{index-axwm9b3v.js → index-rxvsqb3f.js} +1 -1
- package/package.json +1 -1
- package/skills/ghostget/references/install.md +5 -5
- package/skills/ghostget/references/linkedin-adapter.md +16 -3
- package/skills/ghostget/references/platform-patterns.md +1 -1
- package/src/assets/adapters/linkedin/wrench-web-adapter.json +1 -1
- package/src/beeper-client-types.ts +1 -1
- package/src/provider-plugin-contract-identity.ts +2 -2
- package/src/providers/linkedin-web-contact.ts +108 -20
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,17 @@ Historical entries retain their original delivery coordinates.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.17.3 - 2026-09-10
|
|
11
|
+
|
|
12
|
+
- Read LinkedIn `contacts.read@1` Contact info from current profile pages:
|
|
13
|
+
`window.__como_rehydration__` is accepted as either a JSON object or an RSC
|
|
14
|
+
flight array, flight rows that carry JSON objects or arrays are decoded, and
|
|
15
|
+
the first-degree relationship binds when `memberDistance`, `networkDistance`,
|
|
16
|
+
or `distance` of `DISTANCE_1`, `1`, or `"1"` joins the requested vanity or
|
|
17
|
+
profile URN. Self and non-first-degree profiles still fail closed, Contact
|
|
18
|
+
info still resolves by `queryName` when the page embeds no decorated
|
|
19
|
+
`queryId`, and no email is ever invented. Adapter bundle 1.23.0.
|
|
20
|
+
|
|
10
21
|
## 0.17.2 - 2026-09-10
|
|
11
22
|
|
|
12
23
|
- Publish `@hraness/ghostget` to npm as ordinary software by owner decision:
|
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ ghostget plugin list
|
|
|
44
44
|
|
|
45
45
|
## Built-in provider catalog
|
|
46
46
|
|
|
47
|
-
This v0.17.
|
|
47
|
+
This v0.17.3 source tree supports executable actions for 20 services: Beeper,
|
|
48
48
|
Bluesky, ClasificadosOnline, Facebook, Facebook Groups, Facebook Marketplace,
|
|
49
49
|
GitHub, Gmail, Hacker News, Instagram, iMessage, LinkedIn, Reddit, Substack,
|
|
50
50
|
Threads, TikTok, Twitch, WhatsApp, X, and YouTube.
|
|
@@ -109,7 +109,7 @@ owns the narrow capability boundary that can sit beneath them.
|
|
|
109
109
|
Wrench is now Ghostget. The package name is `@hraness/ghostget`, and the CLI
|
|
110
110
|
command is `ghostget`. Read the [migration guide](docs/ghostget-migration.md)
|
|
111
111
|
before updating an existing installation. After its optional npm mirror is
|
|
112
|
-
verified public, `@hraness/ghostget@0.17.
|
|
112
|
+
verified public, `@hraness/ghostget@0.17.3` is also available from the registry.
|
|
113
113
|
|
|
114
114
|
This README describes the package version in this source tree. Its versioned
|
|
115
115
|
GitHub archive and Agent Skill become a supported public release after the
|
|
@@ -120,9 +120,9 @@ The optional npm mirror can follow later without delaying canonical delivery.
|
|
|
120
120
|
Install the single Ghostget Agent Skill with either runner:
|
|
121
121
|
|
|
122
122
|
```sh
|
|
123
|
-
npx skills add hraness/ghostget#v0.17.
|
|
123
|
+
npx skills add hraness/ghostget#v0.17.3
|
|
124
124
|
# or
|
|
125
|
-
bunx skills add hraness/ghostget#v0.17.
|
|
125
|
+
bunx skills add hraness/ghostget#v0.17.3
|
|
126
126
|
```
|
|
127
127
|
|
|
128
128
|
The skill teaches Codex, Claude Code, Cursor, and other compatible coding
|
|
@@ -132,7 +132,7 @@ install the CLI if it is missing. Start a new agent session after installation.
|
|
|
132
132
|
After the matching immutable Release exists, install this exact canonical archive:
|
|
133
133
|
|
|
134
134
|
```sh
|
|
135
|
-
bun add --global https://github.com/hraness/ghostget/releases/download/v0.17.
|
|
135
|
+
bun add --global https://github.com/hraness/ghostget/releases/download/v0.17.3/hraness-ghostget-0.17.3.tgz
|
|
136
136
|
ghostget adapter sync-bundled --json
|
|
137
137
|
ghostget doctor
|
|
138
138
|
```
|
|
@@ -160,7 +160,7 @@ For that same released coordinate, install Ghostget in an agent or application
|
|
|
160
160
|
that owns its own model, planning, tool loop, approvals, and interface:
|
|
161
161
|
|
|
162
162
|
```sh
|
|
163
|
-
bun add https://github.com/hraness/ghostget/releases/download/v0.17.
|
|
163
|
+
bun add https://github.com/hraness/ghostget/releases/download/v0.17.3/hraness-ghostget-0.17.3.tgz
|
|
164
164
|
```
|
|
165
165
|
|
|
166
166
|
```ts
|
package/dist/beeper-client.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hraness/ghostget",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.3",
|
|
4
4
|
"description": "Open-source CLI and TypeScript SDK for precise web capabilities for AI agents: page capture, verified media archives, encrypted reads, and typed provider operations.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -17,34 +17,34 @@ If Bun is missing, stop and direct the user to the official
|
|
|
17
17
|
[Bun installation guide](https://bun.sh/docs/installation). Do not switch
|
|
18
18
|
package managers or pipe an unreviewed installer into a shell.
|
|
19
19
|
|
|
20
|
-
This reference is authored for the exact v0.17.
|
|
20
|
+
This reference is authored for the exact v0.17.3 release coordinate. Use it
|
|
21
21
|
only from the matching release-bound Agent Skill after its canonical archive and
|
|
22
22
|
immutable GitHub Release exist. If the coordinate is not public, stop instead
|
|
23
23
|
of substituting `main`, another tag, or a different package version. Install
|
|
24
24
|
that exact release and its reviewed bundled adapter manifests:
|
|
25
25
|
|
|
26
26
|
```sh
|
|
27
|
-
bun add --global https://github.com/hraness/ghostget/releases/download/v0.17.
|
|
27
|
+
bun add --global https://github.com/hraness/ghostget/releases/download/v0.17.3/hraness-ghostget-0.17.3.tgz
|
|
28
28
|
ghostget adapter sync-bundled --json
|
|
29
29
|
ghostget --help
|
|
30
30
|
ghostget doctor --json
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
The package is `@hraness/ghostget`; `@hraness/ghostget@0.17.
|
|
33
|
+
The package is `@hraness/ghostget`; `@hraness/ghostget@0.17.3` is an optional npm
|
|
34
34
|
mirror only after verified registry publication. Canonical installation does not
|
|
35
35
|
wait for registry publication.
|
|
36
36
|
|
|
37
37
|
When upgrading from Wrench, use `ghostget` for new commands and
|
|
38
38
|
`GHOSTGET_STATE_HOME` for an explicit state root. Existing state is selected in
|
|
39
39
|
place; do not rename, copy, or delete a state directory as part of the upgrade.
|
|
40
|
-
The [migration guide](https://github.com/hraness/ghostget/blob/v0.17.
|
|
40
|
+
The [migration guide](https://github.com/hraness/ghostget/blob/v0.17.3/docs/ghostget-migration.md)
|
|
41
41
|
explains the retained state aliases and durable protocol names.
|
|
42
42
|
|
|
43
43
|
Do not clone the repository merely to run the CLI. Importing the SDK is a
|
|
44
44
|
separate project dependency and does not install a global command:
|
|
45
45
|
|
|
46
46
|
```sh
|
|
47
|
-
bun add https://github.com/hraness/ghostget/releases/download/v0.17.
|
|
47
|
+
bun add https://github.com/hraness/ghostget/releases/download/v0.17.3/hraness-ghostget-0.17.3.tgz
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
`ghostget adapter sync-bundled` upgrades exact bundled baselines, including an
|
|
@@ -230,10 +230,14 @@ operation fixed and target-bound.
|
|
|
230
230
|
profile URL. It binds the signed-in viewer, then requires a 1st-degree
|
|
231
231
|
relationship on that vanity from the profile page. Current LinkedIn pages are
|
|
232
232
|
SDUI/RSC and often omit classic `bpr-guid-*` Profile embeds. The binder
|
|
233
|
-
therefore walks both those Voyager code payloads and `window.__como_rehydration__
|
|
234
|
-
|
|
233
|
+
therefore walks both those Voyager code payloads and `window.__como_rehydration__`.
|
|
234
|
+
Current pages assign that global as either a JSON object (`= { … }`) or an RSC
|
|
235
|
+
flight array (`= [ "1:I[…]\\n2:{…}" ]`). The binder accepts both, decodes
|
|
236
|
+
flight rows that carry JSON objects or arrays, and treats
|
|
237
|
+
`memberDistance`, `networkDistance`, or `distance` of `DISTANCE_1`,
|
|
235
238
|
`1`, or `"1"` as first-degree when that value is joined to the requested vanity
|
|
236
|
-
or its profile URN.
|
|
239
|
+
or its profile URN (`entityUrn`, `objectUrn`, `profileUrn`, or `vieweeMemberUrn`).
|
|
240
|
+
Empty, missing, or import-only bootstrap stays fail-closed.
|
|
237
241
|
|
|
238
242
|
When the same page already embeds Contact-info fields, including a labeled
|
|
239
243
|
Email row, the operation projects those fields and does not issue a second
|
|
@@ -260,6 +264,15 @@ Connected since. The same capture had Como rehydration with numeric
|
|
|
260
264
|
`profileView` returned 410. That drift is why the binder and fetch path
|
|
261
265
|
changed.
|
|
262
266
|
|
|
267
|
+
A 2026-09-09 signed-in capture of the same 1st-degree path still omitted
|
|
268
|
+
`bpr-guid-*` and `voyagerIdentityDashProfileContactInfo` query IDs. Como
|
|
269
|
+
rehydration had moved from an object assignment to an RSC flight array, so
|
|
270
|
+
object-only parsing treated the page as missing bootstrap. The live flight
|
|
271
|
+
string still carried vanity-joined `networkDistance: 1` next to
|
|
272
|
+
`vieweeMemberUrn` / PROFILE_VIEW breadcrumbs. Adapter 1.23.0 accepts that
|
|
273
|
+
array form and still uses `queryName` when the page embeds no decorated
|
|
274
|
+
queryId.
|
|
275
|
+
|
|
263
276
|
Self profiles fail closed with guidance to use `profiles.read`. Second-degree,
|
|
264
277
|
third-degree, and out-of-network profiles fail closed because LinkedIn hid
|
|
265
278
|
Contact info from that viewer. The operation never invents a hidden email.
|
|
@@ -20,7 +20,7 @@ Leave the state `capture-required` when current evidence is absent, ambiguous, e
|
|
|
20
20
|
## LinkedIn
|
|
21
21
|
|
|
22
22
|
- Use `linkedin-web` with a browser-session/cookie realm for consumer Home feed, a member's recent-activity posts, inbox folders, conversations, and native article-editor surfaces. The separate official `linkedin` OAuth adapter covers approved post, comment, reply, repost, and reaction scopes; it does not supply the consumer Home feed, recent-activity pager, or inbox.
|
|
23
|
-
- Current bundle 1.
|
|
23
|
+
- Current bundle 1.23.0 observes `linkedin-web profiles.read@1`, `organizations.read@1`, `contacts.read@1`, `feeds.read@2` for `feed=profile-activity`, `articles.draft.save@7`, and image-only `posts.publish@3`. The two profile-stat reads use a path-backed contained Chrome realm, bind the current member before projecting one exact self profile or requested organization Page, and keep the optional private connection read sequential with the self-profile read. `contacts.read@1` uses the same contained Chrome path, requires a 1st-degree relationship on the requested vanity from classic Profile embeds or Como rehydration (JSON object or RSC flight array), and projects Contact-info email plus any shown profile link, connected-since date, phones, websites, or birthday. The profile-activity page uses the same contained Chrome path, binds the signed-in viewer, resolves the live `voyagerFeedDashProfileUpdates` query, and projects one recent-activity page with engagement counts when present; `feed=home` remains capture-required. The fixed browser evaluation performs only bounded, exact-route first-party fetches; it does not click or inspect LinkedIn DOM. A cookie-only standalone client is not a fallback because current live evidence shows LinkedIn invalidates an exported `li_at` outside its browser/device context. The Article contract binds the numeric member subject to the normalized Article-author profile, creates or replaces one exact private draft, keeps a supplied cover in the Article banner rather than the body, and can preserve the independently read existing banner during an exact replacement without another cover upload. It supports paragraphs/H1/H2/native blockquotes, native HTTPS links, and 1–20 bounded inline JPEG/PNG/WebP images with required alt text and optional captions, and verifies the exact unpublished editor-response readback. Its fixed cover and inline-image single-upload registrations, byte transfers, autosaves, and readbacks run inside contained Chrome without DOM automation. The separate post contract stages a real-size optional PNG through bounded ordered commands, admits one exact image transfer and post create, durably retains the accepted share target, and independently verifies it. `linkedin-web media.publish@1` separately reserves an MP4 route but remains capture-required; the official `linkedin posts.publish` OAuth contract already observes MP4.
|
|
24
24
|
- Every other LinkedIn web operation remains capture-required. Explicit inert reservations cover inbox folders, one bounded page of recommended connections, one connection invitation, Article reads/publication, and other comment/message/repost surfaces. Their retained candidates do not confer executable internal requests.
|
|
25
25
|
- Exclude presence, messaging badges, delivery acknowledgements, seen/read receipts, and notification badge traffic from every R1 contract.
|
|
26
26
|
- Bind the current viewer's person/member identity to the auth realm. `organizations.read` views a Page and does not confer Page-actor authority. For organization actions, additionally bind the administered organization actor selected by the plan.
|
|
@@ -96,7 +96,7 @@ export type BeeperContactInteractionExportReceipt = Readonly<{
|
|
|
96
96
|
implementation: Readonly<{
|
|
97
97
|
producer: Readonly<{
|
|
98
98
|
package: "@hraness/ghostget";
|
|
99
|
-
version: "0.17.
|
|
99
|
+
version: "0.17.3";
|
|
100
100
|
}>;
|
|
101
101
|
officialCli: Readonly<{
|
|
102
102
|
implementation: "github.com/beeper/cli";
|
|
@@ -303,8 +303,9 @@ const identities = Object.freeze({
|
|
|
303
303
|
"linkedin-web": {
|
|
304
304
|
schemaVersion: 1,
|
|
305
305
|
pluginVersion: "1.6.0",
|
|
306
|
-
implementationSha256: "
|
|
306
|
+
implementationSha256: "11b52ca3dd9cbafe4829eed9f27d59bdc1c312db943ea29b93cbab6417ac7025",
|
|
307
307
|
legacyCurrentReadImplementationSha256: [
|
|
308
|
+
"e4614ab9c7733d7526b8157410e462f1bb48fdaa1c2319a6a4179c18b9e6839d",
|
|
308
309
|
"97194dc5a8ec3afc4f4a20152efe51dbdc2fa8a8dde0cd9484b54718c4767f45",
|
|
309
310
|
"64621493e09949fcb9973270140fec1d7398374cf48d3ef150a0c24b882fdeb8",
|
|
310
311
|
"da9d19f6a31d5308579bff7d9b4f77bc458ef69890f0f8c3239cbebeac4bb2a5",
|
|
@@ -316,7 +317,6 @@ const identities = Object.freeze({
|
|
|
316
317
|
"9acb34f9ef5f59dd46b249766af236a3876a4975e0797e330f2975b3fd4ed643",
|
|
317
318
|
"951102310bd87f93ce0863c1a444371d62f412934cc805f82c9981f82edfe50e",
|
|
318
319
|
"4fd6c293984be688a9273acb09bccea64a75134daca745b322fa8d33dfe9f97c",
|
|
319
|
-
"624e146839f4d20d361a949c3ddb726f0c6b741f4d094e873e82185cf3f03175",
|
|
320
320
|
],
|
|
321
321
|
legacyReadImplementationSha256: {
|
|
322
322
|
test: "00a99426ec31182f8d37d5cceb819947a09e630b468bb6e3ed8827f5b0fa4628",
|
|
@@ -39,8 +39,10 @@ const HTML_ENTITY =
|
|
|
39
39
|
/&(?:nbsp|quot|amp|lt|gt|apos|#(?:[xX][0-9A-Fa-f]{1,6}|[0-9]{1,7}));/gu;
|
|
40
40
|
const MAX_HTML_BYTES = 8 * 1024 * 1024;
|
|
41
41
|
const MAX_CODE_TAGS = 256;
|
|
42
|
-
const
|
|
42
|
+
const MAX_COMO_ASSIGNMENTS = 8;
|
|
43
|
+
const MAX_COMO_DECODED_ROOTS = 256;
|
|
43
44
|
const MAX_WALK_NODES = 500_000;
|
|
45
|
+
const RSC_FLIGHT_ROW = /(?:^|\n)(\d+):/u;
|
|
44
46
|
const MAX_PHONES = 8;
|
|
45
47
|
const MAX_WEBSITES = 8;
|
|
46
48
|
const CONTACT_TYPE_SUFFIX = "ProfileContactInfo";
|
|
@@ -259,8 +261,16 @@ function htmlAttribute(value: string, name: string): string | null {
|
|
|
259
261
|
return raw.replace(HTML_ENTITY, (entity) => decodeHtmlEntity(entity));
|
|
260
262
|
}
|
|
261
263
|
|
|
262
|
-
function
|
|
263
|
-
|
|
264
|
+
function skipWhitespace(source: string, start: number): number {
|
|
265
|
+
let index = start;
|
|
266
|
+
while (index < source.length && /\s/u.test(source[index] ?? "")) index += 1;
|
|
267
|
+
return index;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function extractBalancedJsonValue(source: string, start: number): string | undefined {
|
|
271
|
+
const opener = source[start];
|
|
272
|
+
if (opener !== "{" && opener !== "[") return undefined;
|
|
273
|
+
const closer = opener === "{" ? "}" : "]";
|
|
264
274
|
let depth = 0;
|
|
265
275
|
let inString = false;
|
|
266
276
|
let escaped = false;
|
|
@@ -284,8 +294,8 @@ function extractBalancedJsonObject(source: string, start: number): string | unde
|
|
|
284
294
|
inString = true;
|
|
285
295
|
continue;
|
|
286
296
|
}
|
|
287
|
-
if (character ===
|
|
288
|
-
else if (character ===
|
|
297
|
+
if (character === opener) depth += 1;
|
|
298
|
+
else if (character === closer) {
|
|
289
299
|
depth -= 1;
|
|
290
300
|
if (depth === 0) return source.slice(start, index + 1);
|
|
291
301
|
}
|
|
@@ -293,34 +303,94 @@ function extractBalancedJsonObject(source: string, start: number): string | unde
|
|
|
293
303
|
return undefined;
|
|
294
304
|
}
|
|
295
305
|
|
|
306
|
+
function parseJsonRoot(json: string): unknown {
|
|
307
|
+
try {
|
|
308
|
+
return JSON.parse(json) as unknown;
|
|
309
|
+
} catch {
|
|
310
|
+
throw new Error("LinkedIn contact-info bootstrap payload contained malformed JSON");
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function looksLikeRscFlight(value: string): boolean {
|
|
315
|
+
return RSC_FLIGHT_ROW.test(value);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function decodeRscFlightRecords(text: string): unknown[] {
|
|
319
|
+
const records: unknown[] = [];
|
|
320
|
+
let index = 0;
|
|
321
|
+
while (index < text.length && records.length < MAX_COMO_DECODED_ROOTS) {
|
|
322
|
+
index = skipWhitespace(text, index);
|
|
323
|
+
const row = /^(\d+):/u.exec(text.slice(index));
|
|
324
|
+
if (row === null) break;
|
|
325
|
+
index += row[0].length;
|
|
326
|
+
const first = text[index];
|
|
327
|
+
if (first === "{" || first === "[") {
|
|
328
|
+
const json = extractBalancedJsonValue(text, index);
|
|
329
|
+
if (json === undefined) {
|
|
330
|
+
index += 1;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
records.push(parseJsonRoot(json));
|
|
334
|
+
index += json.length;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
const nextRow = text.slice(index).search(/\n\d+:/u);
|
|
338
|
+
index = nextRow === -1 ? text.length : index + nextRow + 1;
|
|
339
|
+
}
|
|
340
|
+
return records;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function decodeComoRehydrationValue(value: unknown): unknown[] {
|
|
344
|
+
if (typeof value === "string") {
|
|
345
|
+
return looksLikeRscFlight(value) ? decodeRscFlightRecords(value) : [];
|
|
346
|
+
}
|
|
347
|
+
if (Array.isArray(value)) {
|
|
348
|
+
if (value.length > 20_000) {
|
|
349
|
+
throw new Error("LinkedIn contact-info bootstrap array exceeded its reviewed bound");
|
|
350
|
+
}
|
|
351
|
+
const decoded: unknown[] = [];
|
|
352
|
+
for (const item of value) {
|
|
353
|
+
if (decoded.length >= MAX_COMO_DECODED_ROOTS) break;
|
|
354
|
+
if (typeof item === "string") {
|
|
355
|
+
decoded.push(...decodeComoRehydrationValue(item));
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
decoded.push(item);
|
|
359
|
+
}
|
|
360
|
+
return decoded;
|
|
361
|
+
}
|
|
362
|
+
if (isRecord(value)) return [value];
|
|
363
|
+
return [];
|
|
364
|
+
}
|
|
365
|
+
|
|
296
366
|
function extractComoRehydrationRoots(html: string): unknown[] {
|
|
297
367
|
const roots: unknown[] = [];
|
|
298
368
|
const marker = "__como_rehydration__";
|
|
299
369
|
let searchFrom = 0;
|
|
300
|
-
|
|
370
|
+
let assignments = 0;
|
|
371
|
+
while (
|
|
372
|
+
searchFrom < html.length
|
|
373
|
+
&& assignments < MAX_COMO_ASSIGNMENTS
|
|
374
|
+
&& roots.length < MAX_COMO_DECODED_ROOTS
|
|
375
|
+
) {
|
|
301
376
|
const markerIndex = html.indexOf(marker, searchFrom);
|
|
302
377
|
if (markerIndex === -1) break;
|
|
303
|
-
let cursor = markerIndex + marker.length;
|
|
304
|
-
while (cursor < html.length && /\s/u.test(html[cursor] ?? "")) cursor += 1;
|
|
378
|
+
let cursor = skipWhitespace(html, markerIndex + marker.length);
|
|
305
379
|
if (html[cursor] !== "=") {
|
|
306
380
|
searchFrom = markerIndex + marker.length;
|
|
307
381
|
continue;
|
|
308
382
|
}
|
|
309
|
-
cursor
|
|
310
|
-
|
|
311
|
-
if (html[cursor] !== "{") {
|
|
312
|
-
searchFrom = cursor;
|
|
313
|
-
continue;
|
|
314
|
-
}
|
|
315
|
-
const json = extractBalancedJsonObject(html, cursor);
|
|
383
|
+
cursor = skipWhitespace(html, cursor + 1);
|
|
384
|
+
const json = extractBalancedJsonValue(html, cursor);
|
|
316
385
|
if (json === undefined) {
|
|
317
386
|
searchFrom = cursor + 1;
|
|
318
387
|
continue;
|
|
319
388
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
389
|
+
assignments += 1;
|
|
390
|
+
const decoded = decodeComoRehydrationValue(parseJsonRoot(json));
|
|
391
|
+
for (const root of decoded) {
|
|
392
|
+
if (roots.length >= MAX_COMO_DECODED_ROOTS) break;
|
|
393
|
+
roots.push(root);
|
|
324
394
|
}
|
|
325
395
|
searchFrom = cursor + json.length;
|
|
326
396
|
}
|
|
@@ -436,7 +506,7 @@ function vanityFromRecord(record: JsonRecord): string | null {
|
|
|
436
506
|
}
|
|
437
507
|
|
|
438
508
|
function profileUrnFromRecord(record: JsonRecord): string | null {
|
|
439
|
-
for (const key of ["entityUrn", "objectUrn", "profileUrn"] as const) {
|
|
509
|
+
for (const key of ["entityUrn", "objectUrn", "profileUrn", "vieweeMemberUrn"] as const) {
|
|
440
510
|
const value = record[key];
|
|
441
511
|
if (typeof value !== "string") continue;
|
|
442
512
|
try {
|
|
@@ -471,6 +541,24 @@ export function projectLinkedInProfileContactBinding(input: {
|
|
|
471
541
|
const urn = profileUrnFromRecord(record);
|
|
472
542
|
if (urn !== null) urns.add(urn);
|
|
473
543
|
}
|
|
544
|
+
if (urns.size < 1) {
|
|
545
|
+
const joined = new Set<string>();
|
|
546
|
+
let sawSelf = false;
|
|
547
|
+
for (const record of records) {
|
|
548
|
+
const urn = profileUrnFromRecord(record);
|
|
549
|
+
const distance = distanceValue(record);
|
|
550
|
+
if (urn === viewer || (distance !== null && isSelfDistance(distance))) {
|
|
551
|
+
sawSelf = true;
|
|
552
|
+
}
|
|
553
|
+
if (urn === null || urn === viewer || distance === null) continue;
|
|
554
|
+
joined.add(urn);
|
|
555
|
+
}
|
|
556
|
+
if (joined.size === 1) {
|
|
557
|
+
for (const urn of joined) urns.add(urn);
|
|
558
|
+
} else if (sawSelf && joined.size === 0) {
|
|
559
|
+
throw selfProfileError();
|
|
560
|
+
}
|
|
561
|
+
}
|
|
474
562
|
if (urns.size < 1) {
|
|
475
563
|
throw new Error("LinkedIn contact-info profile page omitted its target identity");
|
|
476
564
|
}
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Canonical immutable Ghostget package release identity. */
|
|
2
|
-
export const GHOSTGET_VERSION = "0.17.
|
|
2
|
+
export const GHOSTGET_VERSION = "0.17.3" as const;
|