@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 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.2.2
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 delegate
44
- through a separately reviewed `ghostget` executable on `PATH` (or the absolute
45
- `PEOPLEBLADE_GHOSTGET_EXECUTABLE` path). PeopleBlade bundles only the pinned Ghostget
46
- 0.17.1 client validators needed to fence requests and receipts; it does not bundle
47
- or silently download Ghostget's execution runtime.
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.1 before invoking a provider operation.
52
+ bridge still requires exact Ghostget 0.17.3 before invoking a provider operation.
52
53
 
53
- PeopleBlade 0.2.2 contains the reviewed Ghostget-backed Beeper, Google, and WhatsApp
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.1 package. Beeper support is bounded to account-aware
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.1. The command appends reviewed metadata for the same source
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
@@ -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.1 (commit
5
- `b74ae2ac594bcf07c3def71dc4bb2e5a98c826dd`) and message-bundle contract code
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.1";
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 (
@@ -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.1, Beeper adapter 2.4.0, CLI 0.6.2; body-free relationships require macOS arm64" },
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; no authenticated browser collection" },
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: "manual-public-research", priority: 2, status: "supported", commands: ["research prepare", "research apply"], paid: false, boundary: "Agent or human supplies cited public evidence; PeopleBlade performs no provider calls" },
4327
- { id: "cloud-professional-research", priority: 3, 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" },
4328
- { id: "cloud-email-fallback", priority: 4, status: "optional", commands: ["cloud enrich --person-id ID"], paid: true, boundary: "Hunter is a separately configured narrow fallback, not a substitute for identity evidence" }
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-471pfpgn.js
13701
- var GHOSTGET_VERSION = "0.17.1";
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.1",
18687
- commit: "b74ae2ac594bcf07c3def71dc4bb2e5a98c826dd"
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 === currentMetadata)
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 PROVIDER11 = "whatsapp";
25375
+ var PROVIDER12 = "whatsapp";
25017
25376
  var STREAM2 = "messages";
25018
- function getRow9(database, sql, ...values) {
25377
+ function getRow10(database, sql, ...values) {
25019
25378
  return database.query(sql).get(...values);
25020
25379
  }
25021
- function run8(database, sql, ...values) {
25380
+ function run9(database, sql, ...values) {
25022
25381
  return database.run(sql, values);
25023
25382
  }
25024
- function insertedId9(value) {
25383
+ function insertedId10(value) {
25025
25384
  return Number(value.lastInsertRowid);
25026
25385
  }
25027
- function record6(value, label) {
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 exact4(value, keys, label) {
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 text6(value, label, maximum = 4096, empty = false) {
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 = text6(value, label, 64);
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 = record6(value, "canonical JSON");
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 timestamp6(value, label) {
25085
- const parsed = text6(value, label, 40);
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 = text6(value, label, 19);
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 authId2(value) {
25100
- const parsed = text6(value, "WhatsApp auth ID", 48);
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 = text6(value, "WhatsApp account subject", 64);
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 = text6(value, label, 96);
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 envelope(value, input, auth) {
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: record6(parsed.output, "Ghostget output"), execution: parsed.execution };
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 = record6(value, label);
25152
- exact4(parsed, CONTACT_KEYS, label);
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 : text6(parsed.phone, `${label}.phone`, 20);
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 : text6(parsed.displayName, `${label}.displayName`, 512);
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
- text6(parsed[key], `${label}.${key}`, 512);
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 = envelope(value, input, auth);
25537
+ const parsed = envelope2(value, input, auth);
25179
25538
  const output = parsed.output;
25180
- exact4(output, ["provider", "operation", "accountSubject", "projection", "contacts", "nextCursor", "localContactTablePageComplete", "remoteContactSetComplete", "contactSetIncompleteReasons", "statsScope", "statsCompleteness"], "contact output");
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 = record6(value, "projection generation");
25206
- exact4(parsed, ["messageStoreIdentity", "schemaFingerprint"], "projection generation");
25207
- const identity = record6(parsed.messageStoreIdentity, "message-store identity");
25208
- exact4(identity, ["dev", "ino"], "message-store identity");
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 = record6(value, label);
25217
- exact4(parsed, ["cursor", "anchor"], label);
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 = record6(value, label);
25226
- exact4(parsed, ["rowid", "chatJid", "messageId", "senderJid", "timestamp", "fromMe", "chatKind"], label);
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 = text6(parsed.messageId, `${label}.messageId`, 256);
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: timestamp6(parsed.timestamp, `${label}.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 = envelope(value, input, auth);
25614
+ const parsed = envelope2(value, input, auth);
25256
25615
  const output = parsed.output;
25257
- exact4(output, ["provider", "operation", "accountSubject", "contactCollection", "projection", "projectionGeneration", "interactions", "nextCursor", "localInsertPageComplete", "checkpoint", "remoteHistoryComplete", "incompleteReasons"], "interaction output");
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 = authId2(options.authId ?? DEFAULT_WHATSAPP_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 = getRow9(database, `SELECT id,person_id,source_realm_id FROM provider_resources
25381
- WHERE provider=? AND account_key=? AND resource_type='jid' AND resource_id=?`, PROVIDER11, account, jid);
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
- run8(database, `UPDATE provider_resources SET source_realm_id=?,display_name=coalesce(?,display_name),active=1,last_seen_run_id=?,
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
- run8(database, "UPDATE people SET display_name=coalesce(display_name,?) WHERE id=?", name, existing.person_id);
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 = insertedId9(run8(database, "INSERT INTO people(stable_key,display_name,metadata_json) VALUES (?,?,?)", `whatsapp:${sha2569(`${account}\x00${jid}`)}`, name, stableJson6({ createdBy: "exact-whatsapp-jid" })));
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
- run8(database, `INSERT INTO contact_methods(
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 = insertedId9(run8(database, `INSERT INTO provider_resources(
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,?,?,?)`, PROVIDER11, account, sourceRealmId, jid, personId, name, runId, runId, stableJson6({ jidKind: USER_JID.test(jid) ? "user" : "lid", displayNameBasis: basis })));
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 = record6(JSON.parse(value), "stored WhatsApp checkpoint metadata");
25406
- exact4(parsed, ["generation", "coverage"], "stored WhatsApp checkpoint metadata");
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 = getRow9(database, `SELECT checkpoint.cursor,checkpoint.cursor_anchor_sha256,checkpoint.metadata_json
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=?`, PROVIDER11, auth, STREAM2);
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
- run8(database, "DELETE FROM interaction_metrics WHERE provider=? AND account_key=?", PROVIDER11, account);
25420
- run8(database, `INSERT INTO interaction_metrics(
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`, PROVIDER11, account, runId, PROVIDER11, account);
25787
+ GROUP BY person_id`, PROVIDER12, account, runId, PROVIDER12, account);
25429
25788
  }
25430
25789
  function resultFromJson2(value) {
25431
- const parsed = record6(JSON.parse(value), "stored WhatsApp result");
25432
- exact4(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");
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 = getRow9(database, "SELECT account_key,auth_sha256 FROM provider_accounts WHERE provider=? AND auth_id=?", PROVIDER11, snapshot.authId);
25484
- const byAccount = getRow9(database, "SELECT auth_id,auth_sha256 FROM provider_accounts WHERE provider=? AND account_key=?", PROVIDER11, account);
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
- run8(database, `INSERT INTO provider_accounts(provider,auth_id,account_key,auth_sha256,first_seen_at,last_seen_at)
25490
- VALUES (?,?,?,?,?,?)`, PROVIDER11, snapshot.authId, account, firstExecution.authSha256, firstExecution.startedAt, lastExecution.finishedAt);
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
- run8(database, "UPDATE provider_accounts SET last_seen_at=? WHERE provider=? AND auth_id=?", lastExecution.finishedAt, PROVIDER11, snapshot.authId);
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: PROVIDER11,
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 = getRow9(database, `SELECT cursor,cursor_anchor_sha256,generation_sha256,metadata_json
25502
- FROM provider_checkpoints WHERE provider=? AND account_key=? AND stream_key=?`, PROVIDER11, account, STREAM2);
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 = getRow9(database, `SELECT result_json FROM source_runs
25513
- WHERE provider=? AND account_key=? AND mode='linked-device-sync' AND source_sha256=?`, PROVIDER11, account, sourceDigest);
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 = getRow9(database, `SELECT 1 FROM provider_resources
25531
- WHERE provider=? AND account_key=? AND resource_type='jid' AND resource_id=?`, PROVIDER11, account, contact.providerId) !== null;
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 = run8(database, `INSERT INTO source_runs(
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',?,?,?)`, PROVIDER11, account, sourceDigest, sha2569(snapshot.authId), stableJson6(expectedResult), firstExecution.startedAt, lastExecution.finishedAt);
25568
- const runId = insertedId9(runInsert);
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
- run8(database, `INSERT INTO source_records(run_id,ordinal,record_kind,record_sha256,resolution,projection_json,error_text)
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
- run8(database, `INSERT INTO source_records(
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
- run8(database, `INSERT INTO source_records(
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
- run8(database, `INSERT INTO interaction_events(
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',?,?,?)`, PROVIDER11, account, `row:${message.rowid}`, personId, message.timestamp, direction, conversationHash, resolution, runId, stableJson6({ conversationKind, reason, messageIdSha256: sha2569(message.messageId) }));
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 metadata3 = stableJson6({ generation: scanGeneration, coverage: "local-insert-rowid-scan" });
25664
- run8(database, `INSERT INTO provider_checkpoints(
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`, PROVIDER11, account, sourceRealmId, STREAM2, sha2569(snapshot.authId), CONTACTS_CONTRACT_SHA2562, generationSha256(scanGeneration), end2.cursor, end2.anchor, runId, metadata3);
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 = authId2(options.authId ?? DEFAULT_WHATSAPP_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: PROVIDER11,
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.2.2";
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 authId3 = valueAfter(options, "--auth");
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 = authId3 ?? DEFAULT_BEEPER_AUTH;
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 authId3 = valueAfter(options, "--auth");
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, { ...authId3 === undefined ? {} : { authId: authId3 } }), asJson);
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 authId3 = valueAfter(options, "--auth");
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
- ...authId3 === undefined ? {} : { authId: authId3 },
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 authId3 = valueAfter(options, "--auth");
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, { ...authId3 === undefined ? {} : { authId: authId3 }, pageSize }), asJson);
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 authId3 = valueAfter(options, "--auth");
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
- ...authId3 === undefined ? {} : { authId: authId3 },
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]}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hraness/peopleblade",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Local-first, source-aware contact intelligence for people and agents",
5
5
  "type": "module",
6
6
  "bin": {