@hraness/peopleblade 0.2.2 → 0.3.0
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/README.md +39 -10
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/dist/cli.ts +1 -1
- package/dist/peopleblade.js +483 -102
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Installation includes the pinned runtime dependencies Zod 4.4.3 and Effect 3.22.
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
bun add --global @hraness/peopleblade@0.
|
|
18
|
+
bun add --global @hraness/peopleblade@0.3.0
|
|
19
19
|
peopleblade --version
|
|
20
20
|
peopleblade init
|
|
21
21
|
peopleblade stats --json
|
|
@@ -40,19 +40,20 @@ token remains available.
|
|
|
40
40
|
### Optional Ghostget-backed sources
|
|
41
41
|
|
|
42
42
|
The package does not install Ghostget, browser automation, KB, native model runtimes,
|
|
43
|
-
or provider credentials. Beeper, Google, and WhatsApp live sync commands
|
|
44
|
-
through a separately reviewed `ghostget`
|
|
45
|
-
`PEOPLEBLADE_GHOSTGET_EXECUTABLE` path).
|
|
46
|
-
0.17.
|
|
47
|
-
or silently download Ghostget's
|
|
43
|
+
or provider credentials. Beeper, Google, and WhatsApp live sync commands and the
|
|
44
|
+
LinkedIn Contact-info read delegate through a separately reviewed `ghostget`
|
|
45
|
+
executable on `PATH` (or the absolute `PEOPLEBLADE_GHOSTGET_EXECUTABLE` path).
|
|
46
|
+
PeopleBlade bundles only the pinned Ghostget 0.17.3 client validators needed to
|
|
47
|
+
fence requests and receipts; it does not bundle or silently download Ghostget's
|
|
48
|
+
execution runtime.
|
|
48
49
|
|
|
49
50
|
`PEOPLEBLADE_WRENCH_EXECUTABLE` remains a compatibility alias for the explicit
|
|
50
51
|
path. When both variables are set, they must name the same executable. The
|
|
51
|
-
bridge still requires exact Ghostget 0.17.
|
|
52
|
+
bridge still requires exact Ghostget 0.17.3 before invoking a provider operation.
|
|
52
53
|
|
|
53
|
-
PeopleBlade 0.
|
|
54
|
+
PeopleBlade 0.3.0 contains the reviewed Ghostget-backed Beeper, Google, and WhatsApp
|
|
54
55
|
live sync commands. Running them requires the separately installed public
|
|
55
|
-
`@hraness/ghostget` 0.17.
|
|
56
|
+
`@hraness/ghostget` 0.17.3 package. Beeper support is bounded to account-aware
|
|
56
57
|
`contacts.list@3` reads, `contacts.search@1`, `messaging.search@2`, and the separate
|
|
57
58
|
body-free interaction exporter. `contacts.search@1` executes the official Beeper
|
|
58
59
|
CLI 0.6.2 executable pinned by Ghostget; `contacts.list@3` and `messaging.search@2`
|
|
@@ -62,13 +63,41 @@ only. See
|
|
|
62
63
|
[peopleblade.com/sources](https://peopleblade.com/sources) for the complete provider
|
|
63
64
|
guide. A database previously synced by PeopleBlade 0.1.1 through Wrench 0.16.7
|
|
64
65
|
must run `peopleblade beeper rebind --auth beeper-main --confirm --json` once after
|
|
65
|
-
installing Ghostget 0.17.
|
|
66
|
+
installing Ghostget 0.17.3. The command appends reviewed metadata for the same source
|
|
66
67
|
identity only after both complete account inventories match. Do not force-bind
|
|
67
68
|
Ghostget unless its authenticated Desktop target changed. PeopleBlade calls no
|
|
68
69
|
Beeper interface directly and exposes none of Ghostget's Beeper send or action
|
|
69
70
|
operations. Archive imports, Apple Contacts, iMessage, notes, identity review,
|
|
70
71
|
backups, and optional PeopleBlade cloud commands do not require Ghostget.
|
|
71
72
|
|
|
73
|
+
### LinkedIn Contact info through Ghostget
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
peopleblade linkedin contact-info --person-id ID [--account KEY] [--auth ID]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`linkedin contact-info` reads the Contact info panel (email, phone numbers,
|
|
80
|
+
websites, birthday, and connected-since date) of one first-degree LinkedIn
|
|
81
|
+
connection that `linkedin import` already imported from the official LinkedIn
|
|
82
|
+
data export. The command records the read as exact, provider-attributed evidence
|
|
83
|
+
for that person; repeating an identical read changes nothing.
|
|
84
|
+
|
|
85
|
+
The read runs through Ghostget, Wrench's renamed successor, published as the
|
|
86
|
+
`@hraness/ghostget` package. The package does not install Ghostget. The command
|
|
87
|
+
delegates to a separately installed `ghostget` executable on `PATH` (or the
|
|
88
|
+
absolute `PEOPLEBLADE_GHOSTGET_EXECUTABLE` path) and refuses any executable
|
|
89
|
+
that does not report exactly version 0.17.3. It needs a Ghostget path-backed
|
|
90
|
+
auth binding for a signed-in LinkedIn browser profile; the default binding id
|
|
91
|
+
is `linkedin-main`, and `--auth ID` selects another. Beeper, Google, and
|
|
92
|
+
WhatsApp delegate through the same `ghostget` executable.
|
|
93
|
+
|
|
94
|
+
Each command reads one bounded profile. It never lists connections, reads
|
|
95
|
+
messages or feeds, or sends invitations, and it fails without writing anything
|
|
96
|
+
when the profile belongs to the account owner or is not a first-degree
|
|
97
|
+
connection. PeopleBlade bundles only the pinned Ghostget 0.17.3 client
|
|
98
|
+
validators that fence the request and receipt; it does not bundle or download
|
|
99
|
+
Ghostget's execution runtime or browser automation.
|
|
100
|
+
|
|
72
101
|
## Browse and edit locally
|
|
73
102
|
|
|
74
103
|
```bash
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Third-party notices
|
|
2
2
|
|
|
3
3
|
The PeopleBlade CLI bundle includes reviewed client-boundary code from
|
|
4
|
-
`@hraness/ghostget` 0.17.
|
|
5
|
-
`
|
|
4
|
+
`@hraness/ghostget` 0.17.3 (commit
|
|
5
|
+
`67b53cc1917bb09a48b22042cc988c3afbc0aaf8`) and message-bundle contract code
|
|
6
6
|
from `@hraness/message-like-me` 0.7.0. Those portions are provided under the
|
|
7
7
|
following license. The PeopleBlade CLI package has its own MIT grant in
|
|
8
8
|
`LICENSE`; this notice preserves the attribution for the bundled portions.
|
package/dist/cli.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { accessSync, constants, realpathSync } from "node:fs";
|
|
4
4
|
import { isAbsolute, resolve } from "node:path";
|
|
5
5
|
|
|
6
|
-
const reviewedGhostgetVersion = "0.17.
|
|
6
|
+
const reviewedGhostgetVersion = "0.17.3";
|
|
7
7
|
const canonicalConfigured = process.env.PEOPLEBLADE_GHOSTGET_EXECUTABLE;
|
|
8
8
|
const legacyConfigured = process.env.PEOPLEBLADE_WRENCH_EXECUTABLE;
|
|
9
9
|
if (
|
package/dist/peopleblade.js
CHANGED
|
@@ -4313,9 +4313,9 @@ var peoplebladeCapabilities = Object.freeze({
|
|
|
4313
4313
|
{ id: "apple-contacts", command: "contacts sync", status: "supported", transport: "local-database", requirement: "macOS Contacts access" },
|
|
4314
4314
|
{ id: "imessage", command: "imessage sync", status: "supported", transport: "local-metadata", requirement: "macOS Messages access; no message bodies" },
|
|
4315
4315
|
{ id: "google-contacts", command: "google sync", status: "supported", transport: "ghostget", requirement: "Reviewed Gmail/People contacts.list and contact scopes" },
|
|
4316
|
-
{ id: "beeper", command: "beeper sync", status: "supported", transport: "ghostget", requirement: "Ghostget 0.17.
|
|
4316
|
+
{ id: "beeper", command: "beeper sync", status: "supported", transport: "ghostget", requirement: "Ghostget 0.17.3, Beeper adapter 2.4.0, CLI 0.6.2; body-free relationships require macOS arm64" },
|
|
4317
4317
|
{ id: "whatsapp", command: "whatsapp sync", status: "supported", transport: "ghostget", requirement: "Reviewed linked-device contacts.list v2" },
|
|
4318
|
-
{ id: "linkedin", command: "linkedin import", status: "supported", transport: "official-archive", requirement: "Owner-selected official ZIP;
|
|
4318
|
+
{ id: "linkedin", command: "linkedin import", status: "supported", transport: "official-archive", requirement: "Owner-selected official ZIP; connection lists are not collected from a signed-in browser" },
|
|
4319
4319
|
{ id: "instagram", command: "instagram import", status: "supported", transport: "official-archive", requirement: "Owner-selected JSON ZIP and exact owner username" },
|
|
4320
4320
|
{ id: "x-archive", command: "x import", status: "bounded", transport: "official-archive", requirement: "Owner-selected ZIP; unresolved IDs remain inert" },
|
|
4321
4321
|
{ id: "x-live", command: "x mutuals sync", status: "blocked", transport: "unavailable", requirement: "A reviewed bounded Ghostget mutuals capability is not yet available" },
|
|
@@ -4323,9 +4323,10 @@ var peoplebladeCapabilities = Object.freeze({
|
|
|
4323
4323
|
],
|
|
4324
4324
|
enrichment: [
|
|
4325
4325
|
{ id: "local-anchors", priority: 1, status: "supported", commands: ["identity audit", "identity suggest", "identity decide"], paid: false, boundary: "Exact source evidence first; joins require reversible evidence-bound decisions" },
|
|
4326
|
-
{ id: "
|
|
4327
|
-
{ id: "
|
|
4328
|
-
{ id: "cloud-
|
|
4326
|
+
{ id: "linkedin-contact-info", priority: 2, status: "supported", commands: ["linkedin contact-info --person-id ID"], paid: false, boundary: "Reviewed Ghostget linkedin-web contacts.read for one exact first-degree connection already imported from the official export; signed-in browser profile, one bounded read, no connect, message, or feed access" },
|
|
4327
|
+
{ id: "manual-public-research", priority: 3, status: "supported", commands: ["research prepare", "research apply"], paid: false, boundary: "Agent or human supplies cited public evidence; PeopleBlade performs no provider calls" },
|
|
4328
|
+
{ id: "cloud-professional-research", priority: 4, status: "optional", commands: ["cloud enrich --person-id ID", "cloud enrich --confirm TOKEN"], paid: true, boundary: "Exa exact profiles or bounded search, then cited model extraction; device-bound preview approval required" },
|
|
4329
|
+
{ id: "cloud-email-fallback", priority: 5, status: "optional", commands: ["cloud enrich --person-id ID"], paid: true, boundary: "Hunter is a separately configured narrow fallback, not a substitute for identity evidence" }
|
|
4329
4330
|
],
|
|
4330
4331
|
interfaces: {
|
|
4331
4332
|
workspace: { command: "ui [--port N]", availability: "included-in-this-build", binding: "127.0.0.1", defaultPort: "random", authentication: "per-process capability file", remoteAssets: false, noteUpload: false },
|
|
@@ -13697,8 +13698,8 @@ function invokeCapabilitySync(request, options = {}) {
|
|
|
13697
13698
|
return parsed.live;
|
|
13698
13699
|
}
|
|
13699
13700
|
|
|
13700
|
-
// node_modules/@hraness/ghostget/dist/index-
|
|
13701
|
-
var GHOSTGET_VERSION = "0.17.
|
|
13701
|
+
// node_modules/@hraness/ghostget/dist/index-rxvsqb3f.js
|
|
13702
|
+
var GHOSTGET_VERSION = "0.17.3";
|
|
13702
13703
|
|
|
13703
13704
|
// node_modules/@hraness/ghostget/dist/beeper-client.js
|
|
13704
13705
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
@@ -18683,8 +18684,8 @@ function projectBeeperContactInteractions(summary2, coordinates) {
|
|
|
18683
18684
|
// src/local/providers/beeper-compatibility.ts
|
|
18684
18685
|
var BEEPER_GHOSTGET_COMPATIBILITY = Object.freeze({
|
|
18685
18686
|
ghostget: Object.freeze({
|
|
18686
|
-
version: "0.17.
|
|
18687
|
-
commit: "
|
|
18687
|
+
version: "0.17.3",
|
|
18688
|
+
commit: "67b53cc1917bb09a48b22042cc988c3afbc0aaf8"
|
|
18688
18689
|
}),
|
|
18689
18690
|
adapter: Object.freeze({
|
|
18690
18691
|
id: "beeper-local",
|
|
@@ -18721,6 +18722,22 @@ var BEEPER_GHOSTGET_COMPATIBILITY = Object.freeze({
|
|
|
18721
18722
|
commit: "b9c1714410139c2139b597338cd002d785653e85"
|
|
18722
18723
|
})
|
|
18723
18724
|
});
|
|
18725
|
+
var PREVIOUS_GHOSTGET_0171_BEEPER_SOURCE_BINDING_METADATA = Object.freeze({
|
|
18726
|
+
transport: "local-cli",
|
|
18727
|
+
wrenchVersion: "0.17.1",
|
|
18728
|
+
wrenchCommit: "b74ae2ac594bcf07c3def71dc4bb2e5a98c826dd",
|
|
18729
|
+
adapterId: "beeper-local",
|
|
18730
|
+
adapterVersion: "2.4.0",
|
|
18731
|
+
adapterSha256: "2714af7e821b799c1d0b57106e8e193cb9787e5a6da37e1269f4aae0710a4ddc",
|
|
18732
|
+
cliId: "beeper-cli",
|
|
18733
|
+
cliVersion: "0.6.2",
|
|
18734
|
+
cliCommit: "a416af06023449a87312dc11e54643fd9dc94b8c",
|
|
18735
|
+
contracts: Object.freeze({
|
|
18736
|
+
contactsList: "8048565547c2a09078528d0a35f7a0c3a2e8850b17b075dc572e8b59115fcb57",
|
|
18737
|
+
contactsSearch: "e57cdcc9012ed5fb8f956e16a2368e95adf0a6b8bb404f903e64df85061b1ca1",
|
|
18738
|
+
messagingSearch: "2fc54d139a54d19c31a82237e4b417994e12679db3da5fd71ec9265e3622b0bf"
|
|
18739
|
+
})
|
|
18740
|
+
});
|
|
18724
18741
|
var PREVIOUS_WRENCH_0168_BEEPER_SOURCE_BINDING_METADATA = Object.freeze({
|
|
18725
18742
|
transport: "local-cli",
|
|
18726
18743
|
wrenchVersion: "0.16.8",
|
|
@@ -19292,8 +19309,11 @@ function classifyReviewedBeeperPredecessor(value, currentMetadata) {
|
|
|
19292
19309
|
if (encoded === canonicalJson(PREVIOUS_WRENCH_0168_BEEPER_SOURCE_BINDING_METADATA)) {
|
|
19293
19310
|
return "wrench-0.16.8-beeper-2.4";
|
|
19294
19311
|
}
|
|
19295
|
-
if (encoded ===
|
|
19312
|
+
if (encoded === canonicalJson(PREVIOUS_GHOSTGET_0171_BEEPER_SOURCE_BINDING_METADATA)) {
|
|
19296
19313
|
return "ghostget-0.17.1-beeper-2.4";
|
|
19314
|
+
}
|
|
19315
|
+
if (encoded === currentMetadata)
|
|
19316
|
+
return "ghostget-0.17.3-beeper-2.4";
|
|
19297
19317
|
return null;
|
|
19298
19318
|
}
|
|
19299
19319
|
function assertStoredRealmInventory(database, sourceBindingId, accounts, storedSha256) {
|
|
@@ -19372,7 +19392,7 @@ function rebindBeeperSource(database, options = {}) {
|
|
|
19372
19392
|
if (predecessor === null) {
|
|
19373
19393
|
throw new Error("Beeper rebind source coordinates are not a reviewed predecessor.");
|
|
19374
19394
|
}
|
|
19375
|
-
const metadataOnly = sameIdentity && (predecessor === "peopleblade-0.1.0-wrench-0.15-beeper-2.0" || predecessor === "wrench-0.16.7-beeper-2.4" || predecessor === "wrench-0.16.8-beeper-2.4");
|
|
19395
|
+
const metadataOnly = sameIdentity && (predecessor === "peopleblade-0.1.0-wrench-0.15-beeper-2.0" || predecessor === "wrench-0.16.7-beeper-2.4" || predecessor === "wrench-0.16.8-beeper-2.4" || predecessor === "ghostget-0.17.1-beeper-2.4");
|
|
19376
19396
|
if (sameIdentity && !metadataOnly) {
|
|
19377
19397
|
throw new Error("Beeper metadata-only rebind requires an exact reviewed same-identity predecessor.");
|
|
19378
19398
|
}
|
|
@@ -24589,6 +24609,9 @@ function linkedInAccountKey(value) {
|
|
|
24589
24609
|
}
|
|
24590
24610
|
return key;
|
|
24591
24611
|
}
|
|
24612
|
+
function canonicalLinkedInProfileUrl(value, label) {
|
|
24613
|
+
return profileUrl(value, label);
|
|
24614
|
+
}
|
|
24592
24615
|
function ownerProfile(messages, supplied) {
|
|
24593
24616
|
const explicit = supplied === undefined ? null : profileUrl(supplied, "LinkedIn archive owner profile URL");
|
|
24594
24617
|
if (supplied !== undefined && explicit === null)
|
|
@@ -24993,6 +25016,342 @@ function linkedInStatsAll(database) {
|
|
|
24993
25016
|
WHERE account.provider='linkedin' ORDER BY account.account_key`, MODE7).map((row) => ({ ...row, remote_set_complete: false }));
|
|
24994
25017
|
}
|
|
24995
25018
|
|
|
25019
|
+
// src/local/providers/linkedin-contact-info.ts
|
|
25020
|
+
var LINKEDIN_CONTACT_INFO_ADAPTER_ID = "linkedin-web";
|
|
25021
|
+
var LINKEDIN_CONTACT_INFO_OPERATION_ID = "contacts.read";
|
|
25022
|
+
var LINKEDIN_CONTACT_INFO_ADAPTER_VERSION = "1.23.0";
|
|
25023
|
+
var LINKEDIN_CONTACT_INFO_CONTRACT_SHA256 = "8f80c234ca2b7706a49a5000012a11da78a36d38f3023c21854a240e16a13f43";
|
|
25024
|
+
var DEFAULT_LINKEDIN_AUTH = "linkedin-main";
|
|
25025
|
+
var PROVIDER11 = "linkedin";
|
|
25026
|
+
var MODE8 = "ghostget-contact-info";
|
|
25027
|
+
var RECORD_KIND = "contact-info";
|
|
25028
|
+
var OBSERVATION_BASIS2 = "linkedin-contact-info";
|
|
25029
|
+
var OBSERVATION_PRIORITY2 = 720;
|
|
25030
|
+
var MAX_PHONES = 8;
|
|
25031
|
+
var MAX_WEBSITES = 8;
|
|
25032
|
+
var EMAIL5 = /^(?=[\x21-\x7E]+$)[^@\s]+@[^@\s]+\.[^@\s]+$/u;
|
|
25033
|
+
var PHONE = /^\+?[0-9][0-9 .\-()]{6,30}[0-9]$/u;
|
|
25034
|
+
var VIEWER_SUBJECT = /^urn:li:fsd_profile:[0-9]{1,32}$/u;
|
|
25035
|
+
var PROFILE_URN = /^urn:li:fsd_profile:[A-Za-z0-9_-]{1,256}$/u;
|
|
25036
|
+
var VANITY = /^[A-Za-z0-9%._-]{1,1024}$/u;
|
|
25037
|
+
var DATE = /^[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])$/u;
|
|
25038
|
+
var BIRTHDAY = /^(?:[0-9]{4}-)?(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])$/u;
|
|
25039
|
+
var AUTH_ID3 = /^[a-z0-9][a-z0-9._:-]{0,255}$/u;
|
|
25040
|
+
function getRow9(database, sql, ...bindings) {
|
|
25041
|
+
return database.query(sql).get(...bindings);
|
|
25042
|
+
}
|
|
25043
|
+
function allRows7(database, sql, ...bindings) {
|
|
25044
|
+
return database.query(sql).all(...bindings);
|
|
25045
|
+
}
|
|
25046
|
+
function run8(database, sql, ...bindings) {
|
|
25047
|
+
return database.run(sql, bindings);
|
|
25048
|
+
}
|
|
25049
|
+
function insertedId9(value) {
|
|
25050
|
+
return Number(value.lastInsertRowid);
|
|
25051
|
+
}
|
|
25052
|
+
function record6(value, label) {
|
|
25053
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
25054
|
+
throw new Error(`${label} must be an object`);
|
|
25055
|
+
return value;
|
|
25056
|
+
}
|
|
25057
|
+
function exact4(value, keys, label) {
|
|
25058
|
+
const actual = Object.keys(value).sort();
|
|
25059
|
+
const expected = [...keys].sort();
|
|
25060
|
+
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
|
|
25061
|
+
throw new Error(`${label} changed its reviewed shape`);
|
|
25062
|
+
}
|
|
25063
|
+
}
|
|
25064
|
+
function text6(value, label, maximum) {
|
|
25065
|
+
if (typeof value !== "string")
|
|
25066
|
+
throw new Error(`${label} must be a string`);
|
|
25067
|
+
const normalized = value.normalize("NFC");
|
|
25068
|
+
if (normalized.length < 1 || Buffer.byteLength(normalized, "utf8") > maximum || /[\u0000-\u001F\u007F-\u009F]/u.test(normalized))
|
|
25069
|
+
throw new Error(`${label} is invalid`);
|
|
25070
|
+
return normalized;
|
|
25071
|
+
}
|
|
25072
|
+
function optionalText3(value, label, maximum) {
|
|
25073
|
+
return value === null ? null : text6(value, label, maximum);
|
|
25074
|
+
}
|
|
25075
|
+
function timestamp6(value, label) {
|
|
25076
|
+
const parsed = text6(value, label, 64);
|
|
25077
|
+
const milliseconds2 = Date.parse(parsed);
|
|
25078
|
+
if (!Number.isFinite(milliseconds2) || new Date(milliseconds2).toISOString() !== parsed) {
|
|
25079
|
+
throw new Error(`${label} must be a canonical UTC timestamp`);
|
|
25080
|
+
}
|
|
25081
|
+
return parsed;
|
|
25082
|
+
}
|
|
25083
|
+
function boundedList(value, label, maximum) {
|
|
25084
|
+
if (!Array.isArray(value) || value.length > maximum)
|
|
25085
|
+
throw new Error(`${label} exceeded its reviewed bound`);
|
|
25086
|
+
const items = value.map((item, index) => text6(item, `${label}[${index}]`, 2048));
|
|
25087
|
+
if (new Set(items).size !== items.length)
|
|
25088
|
+
throw new Error(`${label} contains duplicates`);
|
|
25089
|
+
return items;
|
|
25090
|
+
}
|
|
25091
|
+
function website(value, label) {
|
|
25092
|
+
let parsed;
|
|
25093
|
+
try {
|
|
25094
|
+
parsed = new URL(value);
|
|
25095
|
+
} catch (error) {
|
|
25096
|
+
throw new Error(`${label} is not an absolute URL`, { cause: error });
|
|
25097
|
+
}
|
|
25098
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:" || parsed.username !== "" || parsed.password !== "")
|
|
25099
|
+
throw new Error(`${label} must be a plain web URL`);
|
|
25100
|
+
return parsed.href;
|
|
25101
|
+
}
|
|
25102
|
+
function normalizedWebsite(value) {
|
|
25103
|
+
const parsed = new URL(value);
|
|
25104
|
+
parsed.hash = "";
|
|
25105
|
+
const path = parsed.pathname.length > 1 && parsed.pathname.endsWith("/") ? parsed.pathname.slice(0, -1) : parsed.pathname;
|
|
25106
|
+
return `${parsed.protocol}//${parsed.host.toLowerCase()}${path}${parsed.search}`;
|
|
25107
|
+
}
|
|
25108
|
+
function normalizedEmail3(value, label) {
|
|
25109
|
+
if (!EMAIL5.test(value) || Buffer.byteLength(value, "utf8") > 320)
|
|
25110
|
+
throw new Error(`${label} is not an email address`);
|
|
25111
|
+
return value.toLowerCase();
|
|
25112
|
+
}
|
|
25113
|
+
function parseFields(value, label) {
|
|
25114
|
+
const parsed = record6(value, label);
|
|
25115
|
+
exact4(parsed, ["email", "profileUrl", "connectedSince", "phones", "websites", "birthday"], label);
|
|
25116
|
+
const email2 = optionalText3(parsed.email, `${label}.email`, 320);
|
|
25117
|
+
if (email2 !== null)
|
|
25118
|
+
normalizedEmail3(email2, `${label}.email`);
|
|
25119
|
+
const profileUrl2 = optionalText3(parsed.profileUrl, `${label}.profileUrl`, 4096);
|
|
25120
|
+
if (profileUrl2 !== null && canonicalLinkedInProfileUrl(profileUrl2, `${label}.profileUrl`) === null) {
|
|
25121
|
+
throw new Error(`${label}.profileUrl is invalid`);
|
|
25122
|
+
}
|
|
25123
|
+
const connectedSince = optionalText3(parsed.connectedSince, `${label}.connectedSince`, 10);
|
|
25124
|
+
if (connectedSince !== null && !DATE.test(connectedSince))
|
|
25125
|
+
throw new Error(`${label}.connectedSince is invalid`);
|
|
25126
|
+
const phones = boundedList(parsed.phones, `${label}.phones`, MAX_PHONES);
|
|
25127
|
+
for (const [index, phone] of phones.entries()) {
|
|
25128
|
+
if (!PHONE.test(phone))
|
|
25129
|
+
throw new Error(`${label}.phones[${index}] is invalid`);
|
|
25130
|
+
}
|
|
25131
|
+
const websites = boundedList(parsed.websites, `${label}.websites`, MAX_WEBSITES).map((item, index) => website(item, `${label}.websites[${index}]`));
|
|
25132
|
+
const birthday2 = optionalText3(parsed.birthday, `${label}.birthday`, 10);
|
|
25133
|
+
if (birthday2 !== null && !BIRTHDAY.test(birthday2))
|
|
25134
|
+
throw new Error(`${label}.birthday is invalid`);
|
|
25135
|
+
return Object.freeze({ email: email2, profileUrl: profileUrl2, connectedSince, phones, websites, birthday: birthday2 });
|
|
25136
|
+
}
|
|
25137
|
+
function projectLinkedInContactInfo(value, expectedProfileUrl) {
|
|
25138
|
+
const output = record6(value, "Ghostget LinkedIn contact output");
|
|
25139
|
+
exact4(output, ["schemaVersion", "provider", "profile", "viewer", "observedAt", "completeness", "contact"], "contact output");
|
|
25140
|
+
if (output.schemaVersion !== 1 || output.provider !== PROVIDER11)
|
|
25141
|
+
throw new Error("Ghostget LinkedIn contact semantics drifted");
|
|
25142
|
+
const profile = record6(output.profile, "contact output.profile");
|
|
25143
|
+
exact4(profile, ["vanity", "profileUrn", "url", "relationship"], "contact output.profile");
|
|
25144
|
+
const vanity = text6(profile.vanity, "contact output.profile.vanity", 1024);
|
|
25145
|
+
if (!VANITY.test(vanity))
|
|
25146
|
+
throw new Error("Ghostget LinkedIn profile vanity is invalid");
|
|
25147
|
+
const profileUrn = text6(profile.profileUrn, "contact output.profile.profileUrn", 300);
|
|
25148
|
+
if (!PROFILE_URN.test(profileUrn))
|
|
25149
|
+
throw new Error("Ghostget LinkedIn profile URN is invalid");
|
|
25150
|
+
const url = canonicalLinkedInProfileUrl(text6(profile.url, "contact output.profile.url", 4096), "contact output.profile.url");
|
|
25151
|
+
if (url === null || url !== expectedProfileUrl) {
|
|
25152
|
+
throw new Error("Ghostget LinkedIn contact output does not bind the requested profile");
|
|
25153
|
+
}
|
|
25154
|
+
if (profile.relationship !== "first-degree")
|
|
25155
|
+
throw new Error("Ghostget LinkedIn contact output is not a first-degree read");
|
|
25156
|
+
const viewer = record6(output.viewer, "contact output.viewer");
|
|
25157
|
+
exact4(viewer, ["subject"], "contact output.viewer");
|
|
25158
|
+
const viewerSubject = text6(viewer.subject, "contact output.viewer.subject", 64);
|
|
25159
|
+
if (!VIEWER_SUBJECT.test(viewerSubject))
|
|
25160
|
+
throw new Error("Ghostget LinkedIn viewer subject is invalid");
|
|
25161
|
+
const observedAt = timestamp6(output.observedAt, "contact output.observedAt");
|
|
25162
|
+
if (output.completeness !== "complete" && output.completeness !== "partial") {
|
|
25163
|
+
throw new Error("Ghostget LinkedIn contact completeness is invalid");
|
|
25164
|
+
}
|
|
25165
|
+
const contact = parseFields(output.contact, "contact output.contact");
|
|
25166
|
+
const hasAny = contact.email !== null || contact.connectedSince !== null || contact.phones.length > 0 || contact.websites.length > 0 || contact.birthday !== null;
|
|
25167
|
+
if (hasAny !== (output.completeness === "complete")) {
|
|
25168
|
+
throw new Error("Ghostget LinkedIn contact completeness disagrees with its fields");
|
|
25169
|
+
}
|
|
25170
|
+
return Object.freeze({
|
|
25171
|
+
profileUrl: url,
|
|
25172
|
+
vanity,
|
|
25173
|
+
profileUrn,
|
|
25174
|
+
relationship: "first-degree",
|
|
25175
|
+
viewerSubject,
|
|
25176
|
+
observedAt,
|
|
25177
|
+
completeness: output.completeness,
|
|
25178
|
+
contact
|
|
25179
|
+
});
|
|
25180
|
+
}
|
|
25181
|
+
function authId2(value) {
|
|
25182
|
+
const parsed = (value ?? DEFAULT_LINKEDIN_AUTH).trim();
|
|
25183
|
+
if (!AUTH_ID3.test(parsed))
|
|
25184
|
+
throw new Error("Ghostget auth ID is invalid");
|
|
25185
|
+
return parsed;
|
|
25186
|
+
}
|
|
25187
|
+
function envelope(value, input, auth) {
|
|
25188
|
+
return validatePeopleBladeGhostgetInvocation(value, {
|
|
25189
|
+
adapterId: LINKEDIN_CONTACT_INFO_ADAPTER_ID,
|
|
25190
|
+
operationId: LINKEDIN_CONTACT_INFO_OPERATION_ID,
|
|
25191
|
+
authId: auth,
|
|
25192
|
+
input
|
|
25193
|
+
}, {
|
|
25194
|
+
adapterVersion: LINKEDIN_CONTACT_INFO_ADAPTER_VERSION,
|
|
25195
|
+
authKind: "browser-profile",
|
|
25196
|
+
transport: "web-session-api",
|
|
25197
|
+
contractSha256: LINKEDIN_CONTACT_INFO_CONTRACT_SHA256,
|
|
25198
|
+
finalOrigin: "https://www.linkedin.com"
|
|
25199
|
+
});
|
|
25200
|
+
}
|
|
25201
|
+
function storedResultFromJson(value) {
|
|
25202
|
+
const parsed = record6(JSON.parse(value), "stored LinkedIn contact-info result");
|
|
25203
|
+
exact4(parsed, [
|
|
25204
|
+
"account_key",
|
|
25205
|
+
"person_id",
|
|
25206
|
+
"provider_resource_id",
|
|
25207
|
+
"profile_url",
|
|
25208
|
+
"vanity",
|
|
25209
|
+
"observed_at",
|
|
25210
|
+
"completeness",
|
|
25211
|
+
"connected_since",
|
|
25212
|
+
"email_recorded",
|
|
25213
|
+
"phones_recorded",
|
|
25214
|
+
"phones_unresolved",
|
|
25215
|
+
"websites_recorded",
|
|
25216
|
+
"birthday_recorded"
|
|
25217
|
+
], "stored LinkedIn contact-info result");
|
|
25218
|
+
return parsed;
|
|
25219
|
+
}
|
|
25220
|
+
function metadata3(value) {
|
|
25221
|
+
return record6(JSON.parse(value), "LinkedIn resource metadata");
|
|
25222
|
+
}
|
|
25223
|
+
function readLinkedInContactInfo(database, options) {
|
|
25224
|
+
const account = linkedInAccountKey(options.accountKey);
|
|
25225
|
+
const auth = authId2(options.authId);
|
|
25226
|
+
if (!Number.isSafeInteger(options.personId) || options.personId < 1)
|
|
25227
|
+
throw new Error("person ID is invalid");
|
|
25228
|
+
const resources = allRows7(database, `SELECT id,person_id,profile_url,resource_id,metadata_json FROM provider_resources
|
|
25229
|
+
WHERE provider=? AND account_key=? AND resource_type='profile' AND active=1 AND person_id=?
|
|
25230
|
+
ORDER BY id`, PROVIDER11, account, options.personId);
|
|
25231
|
+
if (resources.length === 0) {
|
|
25232
|
+
throw new Error("This person has no active LinkedIn profile in that account; import the official LinkedIn export first");
|
|
25233
|
+
}
|
|
25234
|
+
if (resources.length > 1) {
|
|
25235
|
+
throw new Error("This person has more than one active LinkedIn profile in that account; review its identity decisions first");
|
|
25236
|
+
}
|
|
25237
|
+
const resource = resources[0];
|
|
25238
|
+
const profileUrl2 = canonicalLinkedInProfileUrl(resource.profile_url ?? resource.resource_id, "stored LinkedIn profile URL");
|
|
25239
|
+
if (profileUrl2 === null)
|
|
25240
|
+
throw new Error("stored LinkedIn profile resource lacks an exact profile URL");
|
|
25241
|
+
const owner = getRow9(database, "SELECT owner_key FROM archive_accounts WHERE provider=? AND account_key=?", PROVIDER11, account);
|
|
25242
|
+
if (owner !== null && owner.owner_key === profileUrl2) {
|
|
25243
|
+
throw new Error("Contact info is read for connections, not the account owner's own profile");
|
|
25244
|
+
}
|
|
25245
|
+
const input = Object.freeze({ profile_url: profileUrl2 });
|
|
25246
|
+
const invocation = callPeopleBladeGhostget({
|
|
25247
|
+
adapterId: LINKEDIN_CONTACT_INFO_ADAPTER_ID,
|
|
25248
|
+
operationId: LINKEDIN_CONTACT_INFO_OPERATION_ID,
|
|
25249
|
+
authId: auth,
|
|
25250
|
+
input
|
|
25251
|
+
}, options.invoke);
|
|
25252
|
+
const parsed = envelope(invocation, input, auth);
|
|
25253
|
+
const projection2 = projectLinkedInContactInfo(parsed.output, profileUrl2);
|
|
25254
|
+
const sourceSha256 = sha256(canonicalJson(projection2));
|
|
25255
|
+
const execution = parsed.execution;
|
|
25256
|
+
const personId = resource.person_id;
|
|
25257
|
+
const resourceId = resource.id;
|
|
25258
|
+
const resolvedPhones = projection2.contact.phones.map((phone) => ({ phone, normalized: resolvePhoneIdentity(phone, null) }));
|
|
25259
|
+
const stored = {
|
|
25260
|
+
account_key: account,
|
|
25261
|
+
person_id: personId,
|
|
25262
|
+
provider_resource_id: resourceId,
|
|
25263
|
+
profile_url: profileUrl2,
|
|
25264
|
+
vanity: projection2.vanity,
|
|
25265
|
+
observed_at: projection2.observedAt,
|
|
25266
|
+
completeness: projection2.completeness,
|
|
25267
|
+
connected_since: projection2.contact.connectedSince,
|
|
25268
|
+
email_recorded: projection2.contact.email !== null,
|
|
25269
|
+
phones_recorded: resolvedPhones.filter((entry) => entry.normalized !== null).length,
|
|
25270
|
+
phones_unresolved: resolvedPhones.filter((entry) => entry.normalized === null).length,
|
|
25271
|
+
websites_recorded: projection2.contact.websites.length,
|
|
25272
|
+
birthday_recorded: projection2.contact.birthday !== null
|
|
25273
|
+
};
|
|
25274
|
+
database.exec("BEGIN IMMEDIATE");
|
|
25275
|
+
try {
|
|
25276
|
+
const cached = getRow9(database, "SELECT id,result_json FROM source_runs WHERE provider=? AND account_key=? AND mode=? AND source_sha256=?", PROVIDER11, account, MODE8, sourceSha256);
|
|
25277
|
+
if (cached !== null) {
|
|
25278
|
+
database.exec("COMMIT");
|
|
25279
|
+
return { ...storedResultFromJson(cached.result_json), run_id: cached.id, replayed: true };
|
|
25280
|
+
}
|
|
25281
|
+
const runId = insertedId9(run8(database, `INSERT INTO source_runs(
|
|
25282
|
+
provider,account_key,mode,source_sha256,source_locator_sha256,completeness,result_json,started_at,completed_at
|
|
25283
|
+
) VALUES (?,?,?,?,?,?,?,?,?)`, PROVIDER11, account, MODE8, sourceSha256, sha256(profileUrl2), projection2.completeness, canonicalJson(stored), execution.startedAt, execution.finishedAt));
|
|
25284
|
+
const methodLabel = `${OBSERVATION_BASIS2}:${sha256(profileUrl2).slice(0, 16)}`;
|
|
25285
|
+
const methodMetadata = canonicalJson({ observedIn: OBSERVATION_BASIS2, observedAt: projection2.observedAt });
|
|
25286
|
+
run8(database, "UPDATE contact_methods SET active=0 WHERE provider_resource_id=? AND label=?", resourceId, methodLabel);
|
|
25287
|
+
if (projection2.contact.email !== null) {
|
|
25288
|
+
run8(database, `INSERT INTO contact_methods(
|
|
25289
|
+
person_id,kind,value,normalized_value,label,confidence,first_seen_run_id,last_seen_run_id,
|
|
25290
|
+
metadata_json,provider_resource_id,active,identity_eligible
|
|
25291
|
+
) VALUES (?,'email',?,?,?,'exact',?,?,?,?,1,1)
|
|
25292
|
+
ON CONFLICT(person_id,kind,normalized_value,label) DO UPDATE SET
|
|
25293
|
+
value=excluded.value,last_seen_run_id=excluded.last_seen_run_id,metadata_json=excluded.metadata_json,
|
|
25294
|
+
provider_resource_id=excluded.provider_resource_id,active=1,identity_eligible=1`, personId, projection2.contact.email, normalizedEmail3(projection2.contact.email, "contact email"), methodLabel, runId, runId, methodMetadata, resourceId);
|
|
25295
|
+
}
|
|
25296
|
+
for (const { phone, normalized } of resolvedPhones) {
|
|
25297
|
+
if (normalized === null)
|
|
25298
|
+
continue;
|
|
25299
|
+
run8(database, `INSERT INTO contact_methods(
|
|
25300
|
+
person_id,kind,value,normalized_value,label,confidence,first_seen_run_id,last_seen_run_id,
|
|
25301
|
+
metadata_json,provider_resource_id,active,identity_eligible
|
|
25302
|
+
) VALUES (?,'phone',?,?,?,'exact',?,?,?,?,1,1)
|
|
25303
|
+
ON CONFLICT(person_id,kind,normalized_value,label) DO UPDATE SET
|
|
25304
|
+
value=excluded.value,last_seen_run_id=excluded.last_seen_run_id,metadata_json=excluded.metadata_json,
|
|
25305
|
+
provider_resource_id=excluded.provider_resource_id,active=1,identity_eligible=1`, personId, phone, normalized, methodLabel, runId, runId, methodMetadata, resourceId);
|
|
25306
|
+
}
|
|
25307
|
+
for (const site of projection2.contact.websites) {
|
|
25308
|
+
run8(database, `INSERT INTO contact_methods(
|
|
25309
|
+
person_id,kind,value,normalized_value,label,confidence,first_seen_run_id,last_seen_run_id,
|
|
25310
|
+
metadata_json,provider_resource_id,active,identity_eligible
|
|
25311
|
+
) VALUES (?,'url',?,?,?,'exact',?,?,?,?,1,0)
|
|
25312
|
+
ON CONFLICT(person_id,kind,normalized_value,label) DO UPDATE SET
|
|
25313
|
+
value=excluded.value,last_seen_run_id=excluded.last_seen_run_id,metadata_json=excluded.metadata_json,
|
|
25314
|
+
provider_resource_id=excluded.provider_resource_id,active=1,identity_eligible=0`, personId, site, normalizedWebsite(site), methodLabel, runId, runId, methodMetadata, resourceId);
|
|
25315
|
+
}
|
|
25316
|
+
run8(database, "UPDATE person_field_observations SET active=0 WHERE provider_resource_id=? AND basis=?", resourceId, OBSERVATION_BASIS2);
|
|
25317
|
+
if (projection2.contact.birthday !== null) {
|
|
25318
|
+
run8(database, `INSERT INTO person_field_observations(
|
|
25319
|
+
provider_resource_id,field,value,basis,priority,active,first_seen_run_id,last_seen_run_id,metadata_json
|
|
25320
|
+
) VALUES (?,'birthday',?,?,?,1,?,?,'{}')
|
|
25321
|
+
ON CONFLICT(provider_resource_id,field,value,basis) DO UPDATE SET
|
|
25322
|
+
priority=excluded.priority,active=1,last_seen_run_id=excluded.last_seen_run_id`, resourceId, projection2.contact.birthday, OBSERVATION_BASIS2, OBSERVATION_PRIORITY2, runId, runId);
|
|
25323
|
+
run8(database, "UPDATE people SET birthday=coalesce(birthday,?) WHERE id=?", projection2.contact.birthday, personId);
|
|
25324
|
+
}
|
|
25325
|
+
const resourceMetadata = {
|
|
25326
|
+
...metadata3(resource.metadata_json),
|
|
25327
|
+
linkedInContactInfo: {
|
|
25328
|
+
observedAt: projection2.observedAt,
|
|
25329
|
+
completeness: projection2.completeness,
|
|
25330
|
+
connectedSince: projection2.contact.connectedSince,
|
|
25331
|
+
viewerSubject: projection2.viewerSubject,
|
|
25332
|
+
runId
|
|
25333
|
+
}
|
|
25334
|
+
};
|
|
25335
|
+
run8(database, "UPDATE provider_resources SET last_seen_run_id=?,metadata_json=? WHERE id=?", runId, canonicalJson(resourceMetadata), resourceId);
|
|
25336
|
+
run8(database, `INSERT INTO source_records(
|
|
25337
|
+
run_id,ordinal,record_kind,record_sha256,person_id,provider_resource_id,resolution,projection_json,error_text
|
|
25338
|
+
) VALUES (?,1,?,?,?,?,'matched',?,NULL)`, runId, RECORD_KIND, sourceSha256, personId, resourceId, canonicalJson(projection2));
|
|
25339
|
+
ledgerPeopleBladeGhostgetInvocation(database, {
|
|
25340
|
+
provider: PROVIDER11,
|
|
25341
|
+
accountKey: account,
|
|
25342
|
+
capability: "contact_info",
|
|
25343
|
+
execution,
|
|
25344
|
+
costBasis: "local-browser-read",
|
|
25345
|
+
metadata: { profileUrlSha256: sha256(profileUrl2), completeness: projection2.completeness }
|
|
25346
|
+
});
|
|
25347
|
+
database.exec("COMMIT");
|
|
25348
|
+
return { ...stored, run_id: runId, replayed: false };
|
|
25349
|
+
} catch (error) {
|
|
25350
|
+
database.exec("ROLLBACK");
|
|
25351
|
+
throw error;
|
|
25352
|
+
}
|
|
25353
|
+
}
|
|
25354
|
+
|
|
24996
25355
|
// src/local/providers/whatsapp.ts
|
|
24997
25356
|
import { createHash as createHash9 } from "crypto";
|
|
24998
25357
|
var CONTACTS_CONTRACT_SHA2562 = "0bd3c994f16608d5886f1192dd58d41736e4bd01add66a5a3e029efca8b14a38";
|
|
@@ -25013,18 +25372,18 @@ var DEVICE_LID_JID = /^([0-9]{5,32}):[0-9]{1,5}@lid$/u;
|
|
|
25013
25372
|
var GROUP_JID = /^[0-9]{5,32}(?:-[0-9]{5,20})?@g\.us$/u;
|
|
25014
25373
|
var OTHER_JID = /^(?:0@s\.whatsapp\.net|[0-9]{5,32}@newsletter|(?:status|[0-9]{5,32})@broadcast)$/u;
|
|
25015
25374
|
var MESSAGE_ID = /^[A-Za-z0-9._~:-]{1,256}$/u;
|
|
25016
|
-
var
|
|
25375
|
+
var PROVIDER12 = "whatsapp";
|
|
25017
25376
|
var STREAM2 = "messages";
|
|
25018
|
-
function
|
|
25377
|
+
function getRow10(database, sql, ...values) {
|
|
25019
25378
|
return database.query(sql).get(...values);
|
|
25020
25379
|
}
|
|
25021
|
-
function
|
|
25380
|
+
function run9(database, sql, ...values) {
|
|
25022
25381
|
return database.run(sql, values);
|
|
25023
25382
|
}
|
|
25024
|
-
function
|
|
25383
|
+
function insertedId10(value) {
|
|
25025
25384
|
return Number(value.lastInsertRowid);
|
|
25026
25385
|
}
|
|
25027
|
-
function
|
|
25386
|
+
function record7(value, label) {
|
|
25028
25387
|
if (typeof value !== "object" || value === null || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
|
|
25029
25388
|
throw new Error(`${label} must be a plain object`);
|
|
25030
25389
|
}
|
|
@@ -25036,14 +25395,14 @@ function record6(value, label) {
|
|
|
25036
25395
|
throw new Error(`${label} has symbol keys`);
|
|
25037
25396
|
return value;
|
|
25038
25397
|
}
|
|
25039
|
-
function
|
|
25398
|
+
function exact5(value, keys, label) {
|
|
25040
25399
|
const actual = Object.keys(value).sort();
|
|
25041
25400
|
const expected = [...keys].sort();
|
|
25042
25401
|
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
|
|
25043
25402
|
throw new Error(`${label} contains unsupported or missing fields`);
|
|
25044
25403
|
}
|
|
25045
25404
|
}
|
|
25046
|
-
function
|
|
25405
|
+
function text7(value, label, maximum = 4096, empty = false) {
|
|
25047
25406
|
if (typeof value !== "string" || !empty && value.length === 0 || Buffer.byteLength(value, "utf8") > maximum || /[\u0000\r]/u.test(value)) {
|
|
25048
25407
|
throw new Error(`${label} must be bounded text`);
|
|
25049
25408
|
}
|
|
@@ -25060,7 +25419,7 @@ function bool3(value, label) {
|
|
|
25060
25419
|
return value;
|
|
25061
25420
|
}
|
|
25062
25421
|
function digest5(value, label) {
|
|
25063
|
-
const parsed =
|
|
25422
|
+
const parsed = text7(value, label, 64);
|
|
25064
25423
|
if (!SHA2563.test(parsed))
|
|
25065
25424
|
throw new Error(`${label} is invalid`);
|
|
25066
25425
|
return parsed;
|
|
@@ -25078,11 +25437,11 @@ function stableJson6(value) {
|
|
|
25078
25437
|
}
|
|
25079
25438
|
if (Array.isArray(value))
|
|
25080
25439
|
return `[${value.map(stableJson6).join(",")}]`;
|
|
25081
|
-
const parsed =
|
|
25440
|
+
const parsed = record7(value, "canonical JSON");
|
|
25082
25441
|
return `{${Object.keys(parsed).sort().map((key) => `${JSON.stringify(key)}:${stableJson6(parsed[key])}`).join(",")}}`;
|
|
25083
25442
|
}
|
|
25084
|
-
function
|
|
25085
|
-
const parsed =
|
|
25443
|
+
function timestamp7(value, label) {
|
|
25444
|
+
const parsed = text7(value, label, 40);
|
|
25086
25445
|
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/u.test(parsed))
|
|
25087
25446
|
throw new Error(`${label} is invalid`);
|
|
25088
25447
|
const milliseconds2 = Date.parse(parsed);
|
|
@@ -25091,25 +25450,25 @@ function timestamp6(value, label) {
|
|
|
25091
25450
|
return new Date(milliseconds2).toISOString();
|
|
25092
25451
|
}
|
|
25093
25452
|
function decimal(value, label) {
|
|
25094
|
-
const parsed =
|
|
25453
|
+
const parsed = text7(value, label, 19);
|
|
25095
25454
|
if (!DECIMAL.test(parsed) || BigInt(parsed) > BigInt("9223372036854775807"))
|
|
25096
25455
|
throw new Error(`${label} is invalid`);
|
|
25097
25456
|
return parsed;
|
|
25098
25457
|
}
|
|
25099
|
-
function
|
|
25100
|
-
const parsed =
|
|
25458
|
+
function authId3(value) {
|
|
25459
|
+
const parsed = text7(value, "WhatsApp auth ID", 48);
|
|
25101
25460
|
if (!/^[a-z][a-z0-9-]{0,47}$/u.test(parsed))
|
|
25102
25461
|
throw new Error("WhatsApp auth ID is invalid");
|
|
25103
25462
|
return parsed;
|
|
25104
25463
|
}
|
|
25105
25464
|
function subject(value) {
|
|
25106
|
-
const parsed =
|
|
25465
|
+
const parsed = text7(value, "WhatsApp account subject", 64);
|
|
25107
25466
|
if (!SUBJECT2.test(parsed))
|
|
25108
25467
|
throw new Error("WhatsApp account subject is invalid");
|
|
25109
25468
|
return parsed;
|
|
25110
25469
|
}
|
|
25111
25470
|
function canonicalJid(value, label, device = false) {
|
|
25112
|
-
const parsed =
|
|
25471
|
+
const parsed = text7(value, label, 96);
|
|
25113
25472
|
const user = USER_JID.exec(parsed) ?? (device ? DEVICE_USER_JID.exec(parsed) : null);
|
|
25114
25473
|
if (user?.[1] !== undefined)
|
|
25115
25474
|
return { jid: `${user[1]}@s.whatsapp.net`, kind: "user" };
|
|
@@ -25131,7 +25490,7 @@ function directJid(value, label) {
|
|
|
25131
25490
|
function ownerJid(accountSubject) {
|
|
25132
25491
|
return accountSubject.startsWith("whatsapp:pn:") ? `${accountSubject.slice(12)}@s.whatsapp.net` : `${accountSubject.slice(13)}@lid`;
|
|
25133
25492
|
}
|
|
25134
|
-
function
|
|
25493
|
+
function envelope2(value, input, auth) {
|
|
25135
25494
|
const parsed = validatePeopleBladeGhostgetInvocation(value, {
|
|
25136
25495
|
adapterId: "whatsapp-web",
|
|
25137
25496
|
operationId: "contacts.list",
|
|
@@ -25144,25 +25503,25 @@ function envelope(value, input, auth) {
|
|
|
25144
25503
|
contractSha256: CONTACTS_CONTRACT_SHA2562,
|
|
25145
25504
|
finalOrigin: "https://web.whatsapp.com"
|
|
25146
25505
|
});
|
|
25147
|
-
return { output:
|
|
25506
|
+
return { output: record7(parsed.output, "Ghostget output"), execution: parsed.execution };
|
|
25148
25507
|
}
|
|
25149
25508
|
var CONTACT_KEYS = ["providerId", "jidKind", "phone", "redactedPhone", "firstName", "fullName", "pushName", "businessName", "displayName", "displayNameBasis", "alias", "tags", "updatedAt", "localProjectionStatsComplete", "sentCount", "sentCountComplete", "sentCountLowerBound", "sentCountTruncated", "receivedCount", "receivedCountComplete", "receivedCountLowerBound", "receivedCountTruncated", "lastSentAt", "lastSentAtComplete", "lastSentAtBasis", "sentStatsIncompleteReasons", "lastReceivedAt", "lastReceivedAtComplete", "lastReceivedAtBasis", "receivedStatsIncompleteReasons"];
|
|
25150
25509
|
function parseContact4(value, label) {
|
|
25151
|
-
const parsed =
|
|
25152
|
-
|
|
25510
|
+
const parsed = record7(value, label);
|
|
25511
|
+
exact5(parsed, CONTACT_KEYS, label);
|
|
25153
25512
|
const identity = directJid(parsed.providerId, `${label}.providerId`);
|
|
25154
25513
|
if (parsed.jidKind !== identity.kind)
|
|
25155
25514
|
throw new Error(`${label}.jidKind drifted`);
|
|
25156
|
-
const phone = parsed.phone === null ? null :
|
|
25515
|
+
const phone = parsed.phone === null ? null : text7(parsed.phone, `${label}.phone`, 20);
|
|
25157
25516
|
if (phone !== (USER_JID.exec(identity.jid)?.[1] ?? null))
|
|
25158
25517
|
throw new Error(`${label}.phone drifted`);
|
|
25159
|
-
const displayName2 = parsed.displayName === null ? null :
|
|
25518
|
+
const displayName2 = parsed.displayName === null ? null : text7(parsed.displayName, `${label}.displayName`, 512);
|
|
25160
25519
|
const bases = ["full-name", "push-name", "business-name", "first-name", "redacted-phone", "phone", "unavailable"];
|
|
25161
25520
|
if (!bases.includes(parsed.displayNameBasis))
|
|
25162
25521
|
throw new Error(`${label}.displayNameBasis is invalid`);
|
|
25163
25522
|
for (const key of ["redactedPhone", "firstName", "fullName", "pushName", "businessName"])
|
|
25164
25523
|
if (parsed[key] !== null)
|
|
25165
|
-
|
|
25524
|
+
text7(parsed[key], `${label}.${key}`, 512);
|
|
25166
25525
|
if (parsed.alias !== null || parsed.updatedAt !== null || !Array.isArray(parsed.tags) || parsed.tags.length !== 0 || parsed.localProjectionStatsComplete !== false || parsed.sentCount !== null || parsed.sentCountComplete !== false || parsed.sentCountLowerBound !== false || parsed.sentCountTruncated !== false || parsed.receivedCount !== null || parsed.receivedCountComplete !== false || parsed.receivedCountLowerBound !== false || parsed.receivedCountTruncated !== false || parsed.lastSentAt !== null || parsed.lastSentAtComplete !== false || parsed.lastSentAtBasis !== "unavailable" || parsed.lastReceivedAt !== null || parsed.lastReceivedAtComplete !== false || parsed.lastReceivedAtBasis !== "unavailable" || stableJson6(parsed.sentStatsIncompleteReasons) !== stableJson6(["whatsapp-message-store-account-owner-unavailable"]) || stableJson6(parsed.receivedStatsIncompleteReasons) !== stableJson6(["whatsapp-message-store-account-owner-unavailable"])) {
|
|
25167
25526
|
throw new Error(`${label} changed bounded contact semantics`);
|
|
25168
25527
|
}
|
|
@@ -25175,9 +25534,9 @@ function parseContact4(value, label) {
|
|
|
25175
25534
|
};
|
|
25176
25535
|
}
|
|
25177
25536
|
function parseContactPage(value, input, auth) {
|
|
25178
|
-
const parsed =
|
|
25537
|
+
const parsed = envelope2(value, input, auth);
|
|
25179
25538
|
const output = parsed.output;
|
|
25180
|
-
|
|
25539
|
+
exact5(output, ["provider", "operation", "accountSubject", "projection", "contacts", "nextCursor", "localContactTablePageComplete", "remoteContactSetComplete", "contactSetIncompleteReasons", "statsScope", "statsCompleteness"], "contact output");
|
|
25181
25540
|
if (output.provider !== "whatsapp" || output.operation !== "contacts.list" || output.projection !== "quiescent-account-bound-session-store" || output.remoteContactSetComplete !== false || output.statsScope !== "unavailable" || output.statsCompleteness !== "unavailable" || stableJson6(output.contactSetIncompleteReasons) !== stableJson6(["linked-device-contact-sync-coverage-unknown"]) || input.collection !== "contacts") {
|
|
25182
25541
|
throw new Error("Ghostget contact semantics drifted");
|
|
25183
25542
|
}
|
|
@@ -25202,10 +25561,10 @@ function parseContactPage(value, input, auth) {
|
|
|
25202
25561
|
return { input, accountSubject, contacts, nextCursor, localPageComplete: complete, safeOutputSha256, execution: parsed.execution };
|
|
25203
25562
|
}
|
|
25204
25563
|
function generation(value) {
|
|
25205
|
-
const parsed =
|
|
25206
|
-
|
|
25207
|
-
const identity =
|
|
25208
|
-
|
|
25564
|
+
const parsed = record7(value, "projection generation");
|
|
25565
|
+
exact5(parsed, ["messageStoreIdentity", "schemaFingerprint"], "projection generation");
|
|
25566
|
+
const identity = record7(parsed.messageStoreIdentity, "message-store identity");
|
|
25567
|
+
exact5(identity, ["dev", "ino"], "message-store identity");
|
|
25209
25568
|
const dev = decimal(identity.dev, "message-store dev");
|
|
25210
25569
|
const ino = decimal(identity.ino, "message-store ino");
|
|
25211
25570
|
if (ino === "0" || parsed.schemaFingerprint !== PROJECTION_SCHEMA_FINGERPRINT)
|
|
@@ -25213,8 +25572,8 @@ function generation(value) {
|
|
|
25213
25572
|
return { messageStoreIdentity: { dev, ino }, schemaFingerprint: PROJECTION_SCHEMA_FINGERPRINT };
|
|
25214
25573
|
}
|
|
25215
25574
|
function checkpoint(value, label) {
|
|
25216
|
-
const parsed =
|
|
25217
|
-
|
|
25575
|
+
const parsed = record7(value, label);
|
|
25576
|
+
exact5(parsed, ["cursor", "anchor"], label);
|
|
25218
25577
|
const cursor = decimal(parsed.cursor, `${label}.cursor`);
|
|
25219
25578
|
const anchor = parsed.anchor === null ? null : digest5(parsed.anchor, `${label}.anchor`);
|
|
25220
25579
|
if (cursor === "0" !== (anchor === null))
|
|
@@ -25222,14 +25581,14 @@ function checkpoint(value, label) {
|
|
|
25222
25581
|
return { cursor, anchor };
|
|
25223
25582
|
}
|
|
25224
25583
|
function parseInteraction(value, label) {
|
|
25225
|
-
const parsed =
|
|
25226
|
-
|
|
25584
|
+
const parsed = record7(value, label);
|
|
25585
|
+
exact5(parsed, ["rowid", "chatJid", "messageId", "senderJid", "timestamp", "fromMe", "chatKind"], label);
|
|
25227
25586
|
const rowid = decimal(parsed.rowid, `${label}.rowid`);
|
|
25228
25587
|
if (rowid === "0")
|
|
25229
25588
|
throw new Error(`${label}.rowid is invalid`);
|
|
25230
25589
|
const chat = canonicalJid(parsed.chatJid, `${label}.chatJid`);
|
|
25231
25590
|
const sender = parsed.senderJid === null ? null : canonicalJid(parsed.senderJid, `${label}.senderJid`, true).jid;
|
|
25232
|
-
const messageId =
|
|
25591
|
+
const messageId = text7(parsed.messageId, `${label}.messageId`, 256);
|
|
25233
25592
|
if (!MESSAGE_ID.test(messageId))
|
|
25234
25593
|
throw new Error(`${label}.messageId is invalid`);
|
|
25235
25594
|
const kinds = ["dm", "group", "broadcast", "newsletter", "unknown"];
|
|
@@ -25243,7 +25602,7 @@ function parseInteraction(value, label) {
|
|
|
25243
25602
|
chatJid: chat.jid,
|
|
25244
25603
|
messageId,
|
|
25245
25604
|
senderJid: sender,
|
|
25246
|
-
timestamp:
|
|
25605
|
+
timestamp: timestamp7(parsed.timestamp, `${label}.timestamp`),
|
|
25247
25606
|
fromMe: bool3(parsed.fromMe, `${label}.fromMe`),
|
|
25248
25607
|
chatKind: parsed.chatKind
|
|
25249
25608
|
};
|
|
@@ -25252,9 +25611,9 @@ function interactionAnchor(item) {
|
|
|
25252
25611
|
return createHash9("sha256").update(item.rowid).update("\x00").update(item.chatJid).update("\x00").update(item.messageId).digest("hex");
|
|
25253
25612
|
}
|
|
25254
25613
|
function parseInteractionPage(value, input, auth) {
|
|
25255
|
-
const parsed =
|
|
25614
|
+
const parsed = envelope2(value, input, auth);
|
|
25256
25615
|
const output = parsed.output;
|
|
25257
|
-
|
|
25616
|
+
exact5(output, ["provider", "operation", "accountSubject", "contactCollection", "projection", "projectionGeneration", "interactions", "nextCursor", "localInsertPageComplete", "checkpoint", "remoteHistoryComplete", "incompleteReasons"], "interaction output");
|
|
25258
25617
|
if (output.provider !== "whatsapp" || output.operation !== "contacts.list" || output.contactCollection !== "interactions" || output.projection !== "quiescent-account-bound-local-message-inserts" || output.remoteHistoryComplete !== false || stableJson6(output.incompleteReasons) !== stableJson6(["linked-device-history-coverage-unknown", "rowid-cursor-discovers-inserts-only"]) || input.collection !== "interactions") {
|
|
25259
25618
|
throw new Error("interaction projection semantics drifted");
|
|
25260
25619
|
}
|
|
@@ -25291,7 +25650,7 @@ function parseInteractionPage(value, input, auth) {
|
|
|
25291
25650
|
};
|
|
25292
25651
|
}
|
|
25293
25652
|
function ghostgetWhatsAppSnapshot(options = {}) {
|
|
25294
|
-
const auth =
|
|
25653
|
+
const auth = authId3(options.authId ?? DEFAULT_WHATSAPP_AUTH);
|
|
25295
25654
|
const contactPageSize = options.contactPageSize === undefined ? DEFAULT_WHATSAPP_CONTACT_PAGE_SIZE : integer6(options.contactPageSize, "contact page size", 1, 100);
|
|
25296
25655
|
const interactionPageSize = options.interactionPageSize === undefined ? DEFAULT_WHATSAPP_INTERACTION_PAGE_SIZE : integer6(options.interactionPageSize, "interaction page size", 1, 1000);
|
|
25297
25656
|
const supplied = options.checkpoint;
|
|
@@ -25377,47 +25736,47 @@ function generationSha256(value) {
|
|
|
25377
25736
|
return sha2569(stableJson6(value));
|
|
25378
25737
|
}
|
|
25379
25738
|
function ensureResource2(database, account, sourceRealmId, jid, name, basis, runId) {
|
|
25380
|
-
const existing =
|
|
25381
|
-
WHERE provider=? AND account_key=? AND resource_type='jid' AND resource_id=?`,
|
|
25739
|
+
const existing = getRow10(database, `SELECT id,person_id,source_realm_id FROM provider_resources
|
|
25740
|
+
WHERE provider=? AND account_key=? AND resource_type='jid' AND resource_id=?`, PROVIDER12, account, jid);
|
|
25382
25741
|
if (existing !== null) {
|
|
25383
25742
|
if (existing.source_realm_id !== null && existing.source_realm_id !== sourceRealmId) {
|
|
25384
25743
|
throw new Error("WhatsApp contact resource crossed its bound account realm");
|
|
25385
25744
|
}
|
|
25386
|
-
|
|
25745
|
+
run9(database, `UPDATE provider_resources SET source_realm_id=?,display_name=coalesce(?,display_name),active=1,last_seen_run_id=?,
|
|
25387
25746
|
metadata_json=? WHERE id=?`, sourceRealmId, name, runId, stableJson6({ jidKind: USER_JID.test(jid) ? "user" : "lid", displayNameBasis: basis }), existing.id);
|
|
25388
25747
|
if (name !== null)
|
|
25389
|
-
|
|
25748
|
+
run9(database, "UPDATE people SET display_name=coalesce(display_name,?) WHERE id=?", name, existing.person_id);
|
|
25390
25749
|
return { personId: existing.person_id, resourceId: existing.id, imported: false };
|
|
25391
25750
|
}
|
|
25392
|
-
const personId =
|
|
25751
|
+
const personId = insertedId10(run9(database, "INSERT INTO people(stable_key,display_name,metadata_json) VALUES (?,?,?)", `whatsapp:${sha2569(`${account}\x00${jid}`)}`, name, stableJson6({ createdBy: "exact-whatsapp-jid" })));
|
|
25393
25752
|
const phone = phoneForJid(jid);
|
|
25394
25753
|
if (phone !== null)
|
|
25395
|
-
|
|
25754
|
+
run9(database, `INSERT INTO contact_methods(
|
|
25396
25755
|
person_id,kind,value,normalized_value,label,confidence,first_seen_run_id,last_seen_run_id,metadata_json
|
|
25397
25756
|
) VALUES (?,'phone',?,?,'whatsapp','exact',?,?,?)`, personId, phone, phone, runId, runId, stableJson6({ derivedFromJid: true }));
|
|
25398
|
-
const resourceId =
|
|
25757
|
+
const resourceId = insertedId10(run9(database, `INSERT INTO provider_resources(
|
|
25399
25758
|
provider,account_key,source_realm_id,resource_type,resource_id,person_id,display_name,active,
|
|
25400
25759
|
first_seen_run_id,last_seen_run_id,metadata_json
|
|
25401
|
-
) VALUES (?,?,?,'jid',?,?,?,1,?,?,?)`,
|
|
25760
|
+
) VALUES (?,?,?,'jid',?,?,?,1,?,?,?)`, PROVIDER12, account, sourceRealmId, jid, personId, name, runId, runId, stableJson6({ jidKind: USER_JID.test(jid) ? "user" : "lid", displayNameBasis: basis })));
|
|
25402
25761
|
return { personId, resourceId, imported: true };
|
|
25403
25762
|
}
|
|
25404
25763
|
function parseStoredGeneration(value) {
|
|
25405
|
-
const parsed =
|
|
25406
|
-
|
|
25764
|
+
const parsed = record7(JSON.parse(value), "stored WhatsApp checkpoint metadata");
|
|
25765
|
+
exact5(parsed, ["generation", "coverage"], "stored WhatsApp checkpoint metadata");
|
|
25407
25766
|
if (parsed.coverage !== "local-insert-rowid-scan")
|
|
25408
25767
|
throw new Error("stored WhatsApp coverage drifted");
|
|
25409
25768
|
return generation(parsed.generation);
|
|
25410
25769
|
}
|
|
25411
25770
|
function databaseCheckpoint(database, auth) {
|
|
25412
|
-
const row =
|
|
25771
|
+
const row = getRow10(database, `SELECT checkpoint.cursor,checkpoint.cursor_anchor_sha256,checkpoint.metadata_json
|
|
25413
25772
|
FROM provider_accounts account JOIN provider_checkpoints checkpoint
|
|
25414
25773
|
ON checkpoint.provider=account.provider AND checkpoint.account_key=account.account_key
|
|
25415
|
-
WHERE account.provider=? AND account.auth_id=? AND checkpoint.stream_key=?`,
|
|
25774
|
+
WHERE account.provider=? AND account.auth_id=? AND checkpoint.stream_key=?`, PROVIDER12, auth, STREAM2);
|
|
25416
25775
|
return row === null ? { cursor: "0", anchor: null } : { cursor: row.cursor, anchor: row.cursor_anchor_sha256, generation: parseStoredGeneration(row.metadata_json) };
|
|
25417
25776
|
}
|
|
25418
25777
|
function refreshMetrics2(database, account, runId) {
|
|
25419
|
-
|
|
25420
|
-
|
|
25778
|
+
run9(database, "DELETE FROM interaction_metrics WHERE provider=? AND account_key=?", PROVIDER12, account);
|
|
25779
|
+
run9(database, `INSERT INTO interaction_metrics(
|
|
25421
25780
|
provider,account_key,person_id,sent_count,received_count,interaction_count,conversation_count,
|
|
25422
25781
|
first_interaction_at,last_interaction_at,reciprocal,completeness,last_seen_run_id,metadata_json
|
|
25423
25782
|
) SELECT ?,?,person_id,sum(direction='outgoing'),sum(direction='incoming'),count(*),
|
|
@@ -25425,11 +25784,11 @@ function refreshMetrics2(database, account, runId) {
|
|
|
25425
25784
|
CASE WHEN sum(direction='outgoing')>0 AND sum(direction='incoming')>0 THEN 1 ELSE 0 END,
|
|
25426
25785
|
'lower-bound',?,'{"coverage":"local-insert-rowid-scan"}'
|
|
25427
25786
|
FROM interaction_events WHERE provider=? AND account_key=? AND resolution='matched' AND person_id IS NOT NULL
|
|
25428
|
-
GROUP BY person_id`,
|
|
25787
|
+
GROUP BY person_id`, PROVIDER12, account, runId, PROVIDER12, account);
|
|
25429
25788
|
}
|
|
25430
25789
|
function resultFromJson2(value) {
|
|
25431
|
-
const parsed =
|
|
25432
|
-
|
|
25790
|
+
const parsed = record7(JSON.parse(value), "stored WhatsApp result");
|
|
25791
|
+
exact5(parsed, ["account_subject", "contacts_seen", "contacts_imported", "contacts_matched", "contacts_skipped_self", "messages_seen", "messages_inserted", "matched_messages", "skipped_messages", "checkpoint_cursor", "local_insert_scan_complete", "remote_history_complete", "counts_complete", "counts_lower_bound"], "stored WhatsApp result");
|
|
25433
25792
|
const numeric = ["contacts_seen", "contacts_imported", "contacts_matched", "contacts_skipped_self", "messages_seen", "messages_inserted", "matched_messages", "skipped_messages"];
|
|
25434
25793
|
for (const key of numeric)
|
|
25435
25794
|
integer6(parsed[key], `stored WhatsApp result.${key}`, 0, Number.MAX_SAFE_INTEGER);
|
|
@@ -25480,26 +25839,26 @@ function importWhatsAppSnapshot(database, snapshot) {
|
|
|
25480
25839
|
}));
|
|
25481
25840
|
database.exec("BEGIN IMMEDIATE");
|
|
25482
25841
|
try {
|
|
25483
|
-
const byAuth =
|
|
25484
|
-
const byAccount =
|
|
25842
|
+
const byAuth = getRow10(database, "SELECT account_key,auth_sha256 FROM provider_accounts WHERE provider=? AND auth_id=?", PROVIDER12, snapshot.authId);
|
|
25843
|
+
const byAccount = getRow10(database, "SELECT auth_id,auth_sha256 FROM provider_accounts WHERE provider=? AND account_key=?", PROVIDER12, account);
|
|
25485
25844
|
if (byAuth !== null && (byAuth.account_key !== account || byAuth.auth_sha256 !== firstExecution.authSha256) || byAccount !== null && (byAccount.auth_id !== snapshot.authId || byAccount.auth_sha256 !== firstExecution.authSha256)) {
|
|
25486
25845
|
throw new Error("WhatsApp durable auth-to-account binding drifted");
|
|
25487
25846
|
}
|
|
25488
25847
|
if (byAuth === null)
|
|
25489
|
-
|
|
25490
|
-
VALUES (?,?,?,?,?,?)`,
|
|
25848
|
+
run9(database, `INSERT INTO provider_accounts(provider,auth_id,account_key,auth_sha256,first_seen_at,last_seen_at)
|
|
25849
|
+
VALUES (?,?,?,?,?,?)`, PROVIDER12, snapshot.authId, account, firstExecution.authSha256, firstExecution.startedAt, lastExecution.finishedAt);
|
|
25491
25850
|
else
|
|
25492
|
-
|
|
25851
|
+
run9(database, "UPDATE provider_accounts SET last_seen_at=? WHERE provider=? AND auth_id=?", lastExecution.finishedAt, PROVIDER12, snapshot.authId);
|
|
25493
25852
|
const sourceRealmId = bindProviderAccountRealm(database, {
|
|
25494
|
-
provider:
|
|
25853
|
+
provider: PROVIDER12,
|
|
25495
25854
|
authId: snapshot.authId,
|
|
25496
25855
|
accountKey: account,
|
|
25497
25856
|
authSha256: firstExecution.authSha256,
|
|
25498
25857
|
firstSeenAt: firstExecution.startedAt,
|
|
25499
25858
|
lastSeenAt: lastExecution.finishedAt
|
|
25500
25859
|
}).id;
|
|
25501
|
-
const storedCheckpoint =
|
|
25502
|
-
FROM provider_checkpoints WHERE provider=? AND account_key=? AND stream_key=?`,
|
|
25860
|
+
const storedCheckpoint = getRow10(database, `SELECT cursor,cursor_anchor_sha256,generation_sha256,metadata_json
|
|
25861
|
+
FROM provider_checkpoints WHERE provider=? AND account_key=? AND stream_key=?`, PROVIDER12, account, STREAM2);
|
|
25503
25862
|
if (storedCheckpoint === null) {
|
|
25504
25863
|
if (snapshot.startCheckpoint.cursor !== "0" || snapshot.startCheckpoint.anchor !== null || snapshot.startCheckpoint.generation !== undefined)
|
|
25505
25864
|
throw new Error("WhatsApp initial scan must start at zero");
|
|
@@ -25509,8 +25868,8 @@ function importWhatsAppSnapshot(database, snapshot) {
|
|
|
25509
25868
|
throw new Error("WhatsApp durable checkpoint or message-store generation drifted");
|
|
25510
25869
|
}
|
|
25511
25870
|
}
|
|
25512
|
-
const existingRun =
|
|
25513
|
-
WHERE provider=? AND account_key=? AND mode='linked-device-sync' AND source_sha256=?`,
|
|
25871
|
+
const existingRun = getRow10(database, `SELECT result_json FROM source_runs
|
|
25872
|
+
WHERE provider=? AND account_key=? AND mode='linked-device-sync' AND source_sha256=?`, PROVIDER12, account, sourceDigest);
|
|
25514
25873
|
if (existingRun !== null) {
|
|
25515
25874
|
database.exec("COMMIT");
|
|
25516
25875
|
return resultFromJson2(existingRun.result_json);
|
|
@@ -25527,8 +25886,8 @@ function importWhatsAppSnapshot(database, snapshot) {
|
|
|
25527
25886
|
expectedContactsSkippedSelf += 1;
|
|
25528
25887
|
continue;
|
|
25529
25888
|
}
|
|
25530
|
-
const exists =
|
|
25531
|
-
WHERE provider=? AND account_key=? AND resource_type='jid' AND resource_id=?`,
|
|
25889
|
+
const exists = getRow10(database, `SELECT 1 FROM provider_resources
|
|
25890
|
+
WHERE provider=? AND account_key=? AND resource_type='jid' AND resource_id=?`, PROVIDER12, account, contact.providerId) !== null;
|
|
25532
25891
|
if (exists)
|
|
25533
25892
|
expectedContactsMatched += 1;
|
|
25534
25893
|
else
|
|
@@ -25561,11 +25920,11 @@ function importWhatsAppSnapshot(database, snapshot) {
|
|
|
25561
25920
|
counts_complete: false,
|
|
25562
25921
|
counts_lower_bound: true
|
|
25563
25922
|
};
|
|
25564
|
-
const runInsert =
|
|
25923
|
+
const runInsert = run9(database, `INSERT INTO source_runs(
|
|
25565
25924
|
provider,account_key,mode,source_sha256,source_locator_sha256,completeness,
|
|
25566
25925
|
result_json,started_at,completed_at
|
|
25567
|
-
) VALUES (?,?,'linked-device-sync',?,?,'lower-bound',?,?,?)`,
|
|
25568
|
-
const runId =
|
|
25926
|
+
) VALUES (?,?,'linked-device-sync',?,?,'lower-bound',?,?,?)`, PROVIDER12, account, sourceDigest, sha2569(snapshot.authId), stableJson6(expectedResult), firstExecution.startedAt, lastExecution.finishedAt);
|
|
25927
|
+
const runId = insertedId10(runInsert);
|
|
25569
25928
|
let ordinal = 0;
|
|
25570
25929
|
let contactsSeen = 0;
|
|
25571
25930
|
let contactsImported = 0;
|
|
@@ -25578,7 +25937,7 @@ function importWhatsAppSnapshot(database, snapshot) {
|
|
|
25578
25937
|
if (contact.providerId === owner) {
|
|
25579
25938
|
contactsSkippedSelf += 1;
|
|
25580
25939
|
const item2 = stableJson6({ type: "contact", ...contact, resolution: "skipped-self" });
|
|
25581
|
-
|
|
25940
|
+
run9(database, `INSERT INTO source_records(run_id,ordinal,record_kind,record_sha256,resolution,projection_json,error_text)
|
|
25582
25941
|
VALUES (?,?,'contact',?,'skipped',?,'owner-self-contact')`, runId, ordinal, sha2569(item2), item2);
|
|
25583
25942
|
continue;
|
|
25584
25943
|
}
|
|
@@ -25588,7 +25947,7 @@ function importWhatsAppSnapshot(database, snapshot) {
|
|
|
25588
25947
|
else
|
|
25589
25948
|
contactsMatched += 1;
|
|
25590
25949
|
const item = stableJson6({ type: "contact", ...contact });
|
|
25591
|
-
|
|
25950
|
+
run9(database, `INSERT INTO source_records(
|
|
25592
25951
|
run_id,ordinal,record_kind,record_sha256,person_id,provider_resource_id,resolution,projection_json
|
|
25593
25952
|
) VALUES (?,?,'contact',?,?,?,?,?)`, runId, ordinal, sha2569(item), identity.personId, identity.resourceId, identity.imported ? "imported" : "matched", item);
|
|
25594
25953
|
}
|
|
@@ -25632,13 +25991,13 @@ function importWhatsAppSnapshot(database, snapshot) {
|
|
|
25632
25991
|
reason
|
|
25633
25992
|
};
|
|
25634
25993
|
const evidenceJson = stableJson6(evidence);
|
|
25635
|
-
|
|
25994
|
+
run9(database, `INSERT INTO source_records(
|
|
25636
25995
|
run_id,ordinal,record_kind,record_sha256,person_id,provider_resource_id,resolution,projection_json,error_text
|
|
25637
25996
|
) VALUES (?,?,'message',?,?,?,?,?,?)`, runId, ordinal, sha2569(evidenceJson), personId, resourceId, resolution, evidenceJson, resolution === "matched" ? null : reason);
|
|
25638
|
-
|
|
25997
|
+
run9(database, `INSERT INTO interaction_events(
|
|
25639
25998
|
provider,account_key,external_id,person_id,occurred_at,direction,conversation_sha256,
|
|
25640
25999
|
event_kind,resolution,run_id,metadata_json
|
|
25641
|
-
) VALUES (?,?,?,?,?,?,?,'message',?,?,?)`,
|
|
26000
|
+
) VALUES (?,?,?,?,?,?,?,'message',?,?,?)`, PROVIDER12, account, `row:${message.rowid}`, personId, message.timestamp, direction, conversationHash, resolution, runId, stableJson6({ conversationKind, reason, messageIdSha256: sha2569(message.messageId) }));
|
|
25642
26001
|
messagesInserted += 1;
|
|
25643
26002
|
}
|
|
25644
26003
|
refreshMetrics2(database, account, runId);
|
|
@@ -25660,8 +26019,8 @@ function importWhatsAppSnapshot(database, snapshot) {
|
|
|
25660
26019
|
};
|
|
25661
26020
|
if (stableJson6(result) !== stableJson6(expectedResult))
|
|
25662
26021
|
throw new Error("WhatsApp applied counts drifted from the reviewed snapshot");
|
|
25663
|
-
const
|
|
25664
|
-
|
|
26022
|
+
const metadata4 = stableJson6({ generation: scanGeneration, coverage: "local-insert-rowid-scan" });
|
|
26023
|
+
run9(database, `INSERT INTO provider_checkpoints(
|
|
25665
26024
|
provider,account_key,source_realm_id,stream_key,source_locator_sha256,schema_sha256,generation_sha256,
|
|
25666
26025
|
cursor,cursor_anchor_sha256,last_run_id,metadata_json
|
|
25667
26026
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
|
@@ -25669,7 +26028,7 @@ function importWhatsAppSnapshot(database, snapshot) {
|
|
|
25669
26028
|
source_realm_id=excluded.source_realm_id,source_locator_sha256=excluded.source_locator_sha256,schema_sha256=excluded.schema_sha256,
|
|
25670
26029
|
generation_sha256=excluded.generation_sha256,cursor=excluded.cursor,
|
|
25671
26030
|
cursor_anchor_sha256=excluded.cursor_anchor_sha256,last_run_id=excluded.last_run_id,
|
|
25672
|
-
metadata_json=excluded.metadata_json,updated_at=CURRENT_TIMESTAMP`,
|
|
26031
|
+
metadata_json=excluded.metadata_json,updated_at=CURRENT_TIMESTAMP`, PROVIDER12, account, sourceRealmId, STREAM2, sha2569(snapshot.authId), CONTACTS_CONTRACT_SHA2562, generationSha256(scanGeneration), end2.cursor, end2.anchor, runId, metadata4);
|
|
25673
26032
|
database.exec("COMMIT");
|
|
25674
26033
|
return result;
|
|
25675
26034
|
} catch (error) {
|
|
@@ -25678,13 +26037,13 @@ function importWhatsAppSnapshot(database, snapshot) {
|
|
|
25678
26037
|
}
|
|
25679
26038
|
}
|
|
25680
26039
|
function syncWhatsAppRelationships(database, options = {}) {
|
|
25681
|
-
const auth =
|
|
26040
|
+
const auth = authId3(options.authId ?? DEFAULT_WHATSAPP_AUTH);
|
|
25682
26041
|
if (options.checkpoint !== undefined)
|
|
25683
26042
|
throw new Error("sync checkpoint is database-owned");
|
|
25684
26043
|
const snapshot = ghostgetWhatsAppSnapshot({ ...options, authId: auth, checkpoint: databaseCheckpoint(database, auth) });
|
|
25685
26044
|
for (const page of [...snapshot.contactPages, ...snapshot.interactionPages]) {
|
|
25686
26045
|
ledgerPeopleBladeGhostgetInvocation(database, {
|
|
25687
|
-
provider:
|
|
26046
|
+
provider: PROVIDER12,
|
|
25688
26047
|
accountKey: snapshot.accountSubject,
|
|
25689
26048
|
capability: "contact_interactions",
|
|
25690
26049
|
execution: page.execution,
|
|
@@ -25698,7 +26057,7 @@ function syncWhatsAppRelationships(database, options = {}) {
|
|
|
25698
26057
|
}
|
|
25699
26058
|
|
|
25700
26059
|
// src/cli/version.ts
|
|
25701
|
-
var peoplebladeVersion = "0.
|
|
26060
|
+
var peoplebladeVersion = "0.3.0";
|
|
25702
26061
|
|
|
25703
26062
|
// src/cli/main.ts
|
|
25704
26063
|
var usage = `PeopleBlade \u2014 local-first contact intelligence
|
|
@@ -25752,6 +26111,9 @@ Sources (append --auto-accept to run identity auto-accept after a successful imp
|
|
|
25752
26111
|
x mutuals sync Sync X mutuals through Ghostget (requires reviewed Ghostget capability)
|
|
25753
26112
|
whatsapp sync [--auth ID] Sync WhatsApp through Ghostget's linked-device projection
|
|
25754
26113
|
linkedin import ZIP Import an official LinkedIn data-export ZIP
|
|
26114
|
+
linkedin contact-info --person-id ID
|
|
26115
|
+
Read one first-degree connection's Contact info through Ghostget
|
|
26116
|
+
[--account KEY] [--auth ID]
|
|
25755
26117
|
linkedin stats Show aggregate LinkedIn import statistics
|
|
25756
26118
|
instagram import ZIP Import an official Instagram JSON archive
|
|
25757
26119
|
instagram stats Show aggregate Instagram import statistics
|
|
@@ -26481,14 +26843,14 @@ ${verification}`) });
|
|
|
26481
26843
|
}
|
|
26482
26844
|
if (command === "beeper" && subcommand === "sync") {
|
|
26483
26845
|
const options = [...rest];
|
|
26484
|
-
const
|
|
26846
|
+
const authId4 = valueAfter(options, "--auth");
|
|
26485
26847
|
const contactLimit = positive(valueAfter(options, "--contact-limit"), "--contact-limit", 200, 200);
|
|
26486
26848
|
const withRelationships = options.includes("--with-relationships");
|
|
26487
26849
|
if (withRelationships)
|
|
26488
26850
|
options.splice(options.indexOf("--with-relationships"), 1);
|
|
26489
26851
|
if (options.length)
|
|
26490
26852
|
fail3(`Unknown argument: ${options[0]}`);
|
|
26491
|
-
const effectiveAuthId =
|
|
26853
|
+
const effectiveAuthId = authId4 ?? DEFAULT_BEEPER_AUTH;
|
|
26492
26854
|
if (withRelationships)
|
|
26493
26855
|
console.error("Beeper relationships \xB7 deriving a body-free interaction summary through Ghostget");
|
|
26494
26856
|
console.error("Beeper contacts \xB7 discovering connected accounts through Ghostget");
|
|
@@ -26510,7 +26872,7 @@ ${verification}`) });
|
|
|
26510
26872
|
}
|
|
26511
26873
|
if (command === "beeper" && subcommand === "rebind") {
|
|
26512
26874
|
const options = [...rest];
|
|
26513
|
-
const
|
|
26875
|
+
const authId4 = valueAfter(options, "--auth");
|
|
26514
26876
|
const confirm = options.includes("--confirm");
|
|
26515
26877
|
if (confirm)
|
|
26516
26878
|
options.splice(options.indexOf("--confirm"), 1);
|
|
@@ -26519,14 +26881,14 @@ ${verification}`) });
|
|
|
26519
26881
|
if (!confirm)
|
|
26520
26882
|
fail3("beeper rebind requires --confirm after reviewing the current Ghostget binding, release, and connected-account inventory.");
|
|
26521
26883
|
console.error("Beeper binding \xB7 verifying the complete connected-account inventory before an append-only transition");
|
|
26522
|
-
print(rebindBeeperSource(database, { ...
|
|
26884
|
+
print(rebindBeeperSource(database, { ...authId4 === undefined ? {} : { authId: authId4 } }), asJson);
|
|
26523
26885
|
return;
|
|
26524
26886
|
}
|
|
26525
26887
|
if (command === "beeper" && subcommand === "search") {
|
|
26526
26888
|
const options = [...rest];
|
|
26527
26889
|
const queries = valuesAfter(options, "--query");
|
|
26528
26890
|
const services = valuesAfter(options, "--service");
|
|
26529
|
-
const
|
|
26891
|
+
const authId4 = valueAfter(options, "--auth");
|
|
26530
26892
|
const limit = positive(valueAfter(options, "--limit"), "--limit", 20, 20);
|
|
26531
26893
|
if (queries.length === 0)
|
|
26532
26894
|
fail3("beeper search requires one or more --query values.");
|
|
@@ -26535,18 +26897,18 @@ ${verification}`) });
|
|
|
26535
26897
|
print(backfillBeeperSearch(database, {
|
|
26536
26898
|
queries,
|
|
26537
26899
|
...services.length === 0 ? {} : { services },
|
|
26538
|
-
...
|
|
26900
|
+
...authId4 === undefined ? {} : { authId: authId4 },
|
|
26539
26901
|
limit
|
|
26540
26902
|
}), asJson);
|
|
26541
26903
|
return;
|
|
26542
26904
|
}
|
|
26543
26905
|
if (command === "google" && subcommand === "sync") {
|
|
26544
26906
|
const options = [...rest];
|
|
26545
|
-
const
|
|
26907
|
+
const authId4 = valueAfter(options, "--auth");
|
|
26546
26908
|
const pageSize = positive(valueAfter(options, "--page-size"), "--page-size", 100, 100);
|
|
26547
26909
|
if (options.length)
|
|
26548
26910
|
fail3(`Unknown argument: ${options[0]}`);
|
|
26549
|
-
printImport(syncGoogleContacts(database, { ...
|
|
26911
|
+
printImport(syncGoogleContacts(database, { ...authId4 === undefined ? {} : { authId: authId4 }, pageSize }), asJson);
|
|
26550
26912
|
return;
|
|
26551
26913
|
}
|
|
26552
26914
|
if (command === "google" && subcommand === "stats") {
|
|
@@ -26571,13 +26933,13 @@ ${verification}`) });
|
|
|
26571
26933
|
}
|
|
26572
26934
|
if (command === "whatsapp" && subcommand === "sync") {
|
|
26573
26935
|
const options = [...rest];
|
|
26574
|
-
const
|
|
26936
|
+
const authId4 = valueAfter(options, "--auth");
|
|
26575
26937
|
const contactPageSize = positive(valueAfter(options, "--contact-page-size"), "--contact-page-size", 100, 100);
|
|
26576
26938
|
const interactionPageSize = positive(valueAfter(options, "--interaction-page-size"), "--interaction-page-size", 1000, 1000);
|
|
26577
26939
|
if (options.length)
|
|
26578
26940
|
fail3(`Unknown argument: ${options[0]}`);
|
|
26579
26941
|
printImport(syncWhatsAppRelationships(database, {
|
|
26580
|
-
...
|
|
26942
|
+
...authId4 === undefined ? {} : { authId: authId4 },
|
|
26581
26943
|
contactPageSize,
|
|
26582
26944
|
interactionPageSize
|
|
26583
26945
|
}), asJson);
|
|
@@ -26598,6 +26960,25 @@ ${verification}`) });
|
|
|
26598
26960
|
}), asJson);
|
|
26599
26961
|
return;
|
|
26600
26962
|
}
|
|
26963
|
+
if (command === "linkedin" && subcommand === "contact-info") {
|
|
26964
|
+
const options = [...rest];
|
|
26965
|
+
const personText = valueAfter(options, "--person-id");
|
|
26966
|
+
if (personText === undefined)
|
|
26967
|
+
fail3("linkedin contact-info requires --person-id ID.");
|
|
26968
|
+
const personId = Number(personText);
|
|
26969
|
+
if (!Number.isSafeInteger(personId) || personId < 1)
|
|
26970
|
+
fail3("--person-id must be a positive integer.");
|
|
26971
|
+
const accountKey5 = valueAfter(options, "--account");
|
|
26972
|
+
const authId4 = valueAfter(options, "--auth");
|
|
26973
|
+
if (options.length)
|
|
26974
|
+
fail3(`Unknown argument: ${options[0]}`);
|
|
26975
|
+
print(readLinkedInContactInfo(database, {
|
|
26976
|
+
personId,
|
|
26977
|
+
...accountKey5 === undefined ? {} : { accountKey: accountKey5 },
|
|
26978
|
+
...authId4 === undefined ? {} : { authId: authId4 }
|
|
26979
|
+
}), asJson);
|
|
26980
|
+
return;
|
|
26981
|
+
}
|
|
26601
26982
|
if (command === "linkedin" && subcommand === "stats") {
|
|
26602
26983
|
if (rest.length)
|
|
26603
26984
|
fail3(`Unknown argument: ${rest[0]}`);
|