@hraness/peopleblade 0.4.0 → 0.4.1

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.4.0
18
+ bun add --global @hraness/peopleblade@0.4.1
19
19
  peopleblade --version
20
20
  peopleblade init
21
21
  peopleblade stats --json
@@ -51,7 +51,7 @@ execution runtime.
51
51
  path. When both variables are set, they must name the same executable. The
52
52
  bridge still requires exact Ghostget 0.18.13 before invoking a provider operation.
53
53
 
54
- PeopleBlade 0.4.0 contains the reviewed Ghostget-backed Beeper, Google, and WhatsApp
54
+ PeopleBlade 0.4.1 contains the reviewed Ghostget-backed Beeper, Google, and WhatsApp
55
55
  live sync commands. Running them requires the separately installed public
56
56
  `@hraness/ghostget` 0.18.13 package. Beeper support is bounded to account-aware
57
57
  `contacts.list@3` reads, `contacts.search@1`, `messaging.search@2`, and the separate
@@ -155,20 +155,29 @@ The envelope binds this database and its current identity evidence. The strict
155
155
  legacy three-field input remains supported. JSON-mode errors are one structured
156
156
  `peopleblade.error.v1` object on stderr; successful results stay on stdout.
157
157
 
158
- ### Optional Hunter reverse enrichment
158
+ ### Optional Hunter enrichment
159
159
 
160
160
  Hunter customers can reverse-enrich contacts that have a business email but no
161
- LinkedIn profile. Each explicit invocation is capped at 100 contacts, skips terminal
162
- prior attempts, and prints aggregate outcomes only:
161
+ LinkedIn profile, verify stored email deliverability, or find a professional email
162
+ for contacts that have none. Each explicit invocation is capped at 100 contacts,
163
+ skips terminal prior attempts, and prints aggregate outcomes only:
163
164
 
164
165
  ```bash
165
166
  HUNTER_API_KEY=... peopleblade hunter reverse-enrich \
166
167
  --limit 100 --non-webmail --json
168
+ peopleblade hunter verify --limit 100 --key-file /private/path/hunter-key --json
169
+ peopleblade hunter find-emails --limit 100 --key-file /private/path/hunter-key --json
167
170
  ```
168
171
 
169
- The command promotes a LinkedIn profile only after exact returned-email,
170
- compatible-name, safe-URL, and local-ownership checks pass. It uses the caller's
171
- Hunter account directly and is separate from PeopleBlade cloud credits.
172
+ `hunter reverse-enrich` promotes a LinkedIn profile only after exact returned-email,
173
+ compatible-name, safe-URL, and local-ownership checks pass. `hunter verify` records
174
+ the verdict, score, flags, and sources on the existing address without replacing it;
175
+ a Hunter privacy rejection removes the address from identity matching.
176
+ `hunter find-emails` starts from a trusted LinkedIn profile or usable stored name and
177
+ organization, then adds a returned email only when its syntax, name compatibility,
178
+ and local ownership pass. The commands use the caller's Hunter account directly
179
+ and are separate from PeopleBlade cloud credits. `--key-file` requires a private
180
+ regular file owned by the current user; `HUNTER_API_KEY` is the alternative.
172
181
 
173
182
  ## Privacy boundary
174
183
 
@@ -34483,14 +34483,880 @@ async function hunterReverseEnrich(database, options) {
34483
34483
  };
34484
34484
  }
34485
34485
 
34486
+ // src/local/providers/hunter-verify.ts
34487
+ import { randomUUID as randomUUID5 } from "crypto";
34488
+ var PROVIDER12 = "hunter";
34489
+ var ACCOUNT_KEY4 = "hunter.io";
34490
+ var MODE9 = "email-verification";
34491
+ function allRows8(database, sql, ...bindings) {
34492
+ return database.query(sql).all(...bindings);
34493
+ }
34494
+ function run9(database, sql, ...bindings) {
34495
+ return database.run(sql, bindings);
34496
+ }
34497
+ function insertedId10(value) {
34498
+ return Number(value.lastInsertRowid);
34499
+ }
34500
+ var TERMINAL_STATUSES = [
34501
+ "valid",
34502
+ "invalid",
34503
+ "accept_all",
34504
+ "webmail",
34505
+ "disposable",
34506
+ "unknown",
34507
+ "claimed",
34508
+ "invalid_email"
34509
+ ];
34510
+ var VERDICTS = new Set(["valid", "invalid", "accept_all", "webmail", "disposable", "unknown"]);
34511
+ function fetchCandidates2(database, limit) {
34512
+ const terminal = TERMINAL_STATUSES.map((status) => `'${status}'`).join(",");
34513
+ const rows = allRows8(database, `SELECT cm.id, cm.person_id, cm.value, cm.normalized_value
34514
+ FROM contact_methods cm
34515
+ WHERE cm.kind = 'email' AND cm.active = 1
34516
+ AND NOT EXISTS (
34517
+ SELECT 1
34518
+ FROM source_records sr
34519
+ JOIN source_runs source_run ON source_run.id = sr.run_id
34520
+ AND source_run.provider = 'hunter'
34521
+ AND source_run.mode = 'email-verification'
34522
+ JOIN contact_methods prior ON prior.person_id = sr.person_id
34523
+ AND prior.kind = 'email'
34524
+ AND prior.normalized_value = cm.normalized_value
34525
+ WHERE json_extract(sr.projection_json, '$.status') IN (${terminal})
34526
+ )
34527
+ ORDER BY (
34528
+ SELECT COALESCE(SUM(interaction_count), 0) FROM interaction_metrics im WHERE im.person_id = cm.person_id
34529
+ ) DESC, cm.person_id, cm.is_primary DESC,
34530
+ CASE cm.confidence WHEN 'exact' THEN 0 WHEN 'likely' THEN 1 WHEN 'possible' THEN 2 ELSE 3 END,
34531
+ cm.id`);
34532
+ const unique = new Map;
34533
+ for (const row of rows) {
34534
+ if (unique.has(row.normalized_value))
34535
+ continue;
34536
+ unique.set(row.normalized_value, {
34537
+ contact_method_id: row.id,
34538
+ person_id: row.person_id,
34539
+ email: row.value,
34540
+ normalized: row.normalized_value
34541
+ });
34542
+ }
34543
+ const values = [...unique.values()];
34544
+ return limit === undefined ? values : values.slice(0, limit);
34545
+ }
34546
+ var HUNTER_MAX_RESPONSE_BYTES2 = 2 * 1024 * 1024;
34547
+
34548
+ class HunterHttpError2 extends Error {
34549
+ status;
34550
+ constructor(status) {
34551
+ super(`HUNTER_HTTP_${status}`);
34552
+ this.status = status;
34553
+ }
34554
+ }
34555
+
34556
+ class HunterResponseError2 extends Error {
34557
+ constructor() {
34558
+ super("HUNTER_INVALID_RESPONSE");
34559
+ }
34560
+ }
34561
+
34562
+ class HunterPending extends Error {
34563
+ constructor() {
34564
+ super("HUNTER_PENDING");
34565
+ }
34566
+ }
34567
+ function optionalBoolean(value) {
34568
+ if (value === undefined || value === null)
34569
+ return null;
34570
+ if (typeof value !== "boolean")
34571
+ throw new HunterResponseError2;
34572
+ return value;
34573
+ }
34574
+ function optionalBoundedText(value, maximum) {
34575
+ if (value === undefined || value === null)
34576
+ return null;
34577
+ if (typeof value !== "string" || Buffer.byteLength(value, "utf8") > maximum)
34578
+ throw new HunterResponseError2;
34579
+ return value;
34580
+ }
34581
+ function parseVerifyResponse(value) {
34582
+ if (value === null || typeof value !== "object" || Array.isArray(value))
34583
+ throw new HunterResponseError2;
34584
+ const root = value;
34585
+ const data = root["data"];
34586
+ if (data === null || typeof data !== "object" || Array.isArray(data))
34587
+ throw new HunterResponseError2;
34588
+ const record9 = data;
34589
+ const status = optionalBoundedText(record9["status"], 32);
34590
+ const score = record9["score"];
34591
+ if (score !== undefined && score !== null && (typeof score !== "number" || !Number.isFinite(score) || score < 0 || score > 100)) {
34592
+ throw new HunterResponseError2;
34593
+ }
34594
+ const sources = record9["sources"];
34595
+ const sourceCount = Array.isArray(sources) ? Math.min(sources.length, 100) : 0;
34596
+ return {
34597
+ status,
34598
+ score: score === undefined || score === null ? null : score,
34599
+ regexp: optionalBoolean(record9["regexp"]),
34600
+ gibberish: optionalBoolean(record9["gibberish"]),
34601
+ disposable: optionalBoolean(record9["disposable"]),
34602
+ webmail: optionalBoolean(record9["webmail"]),
34603
+ mx_records: optionalBoolean(record9["mx_records"]),
34604
+ smtp_server: optionalBoolean(record9["smtp_server"]),
34605
+ smtp_check: optionalBoolean(record9["smtp_check"]),
34606
+ accept_all: optionalBoolean(record9["accept_all"]),
34607
+ block: optionalBoolean(record9["block"]),
34608
+ sources: sourceCount
34609
+ };
34610
+ }
34611
+ async function hunterVerifyRequest(apiKey, email3) {
34612
+ const url = `https://api.hunter.io/v2/email-verifier?email=${encodeURIComponent(email3)}&api_key=${encodeURIComponent(apiKey)}`;
34613
+ const response = await fetch(url, {
34614
+ method: "GET",
34615
+ headers: { Accept: "application/json" },
34616
+ redirect: "error",
34617
+ signal: AbortSignal.timeout(30000)
34618
+ });
34619
+ if (response.status === 202)
34620
+ throw new HunterPending;
34621
+ if (!response.ok)
34622
+ throw new HunterHttpError2(response.status);
34623
+ if (!(response.headers.get("content-type") ?? "").toLowerCase().includes("application/json"))
34624
+ throw new HunterResponseError2;
34625
+ const declaredLength = Number(response.headers.get("content-length") ?? "0");
34626
+ if (Number.isFinite(declaredLength) && declaredLength > HUNTER_MAX_RESPONSE_BYTES2)
34627
+ throw new HunterResponseError2;
34628
+ const bytes = new Uint8Array(await response.arrayBuffer());
34629
+ if (bytes.byteLength > HUNTER_MAX_RESPONSE_BYTES2)
34630
+ throw new HunterResponseError2;
34631
+ try {
34632
+ return parseVerifyResponse(JSON.parse(new TextDecoder().decode(bytes)));
34633
+ } catch (error) {
34634
+ if (error instanceof HunterResponseError2)
34635
+ throw error;
34636
+ throw new HunterResponseError2;
34637
+ }
34638
+ }
34639
+ async function hunterVerifyEmail(apiKey, email3) {
34640
+ for (let attempt = 0;attempt < 3; attempt += 1) {
34641
+ try {
34642
+ return await hunterVerifyRequest(apiKey, email3);
34643
+ } catch (error) {
34644
+ if (error instanceof HunterPending) {
34645
+ await new Promise((resolve8) => setTimeout(resolve8, 3000));
34646
+ continue;
34647
+ }
34648
+ throw error;
34649
+ }
34650
+ }
34651
+ throw new HunterPending;
34652
+ }
34653
+ function hunterErrorStatus2(error) {
34654
+ if (error.status === 400)
34655
+ return "invalid_email";
34656
+ if (error.status === 401 || error.status === 403)
34657
+ return "authentication_failed";
34658
+ if (error.status === 429)
34659
+ return "rate_limited";
34660
+ if (error.status === 451)
34661
+ return "claimed";
34662
+ if (error.status === 222)
34663
+ return "smtp_failed";
34664
+ return "provider_error";
34665
+ }
34666
+ function applyVerificationMetadata(database, candidate, runId, status, score) {
34667
+ run9(database, `UPDATE contact_methods
34668
+ SET metadata_json = json_patch(metadata_json, json(?))
34669
+ WHERE kind = 'email' AND normalized_value = ?`, JSON.stringify({ hunter_verification: { status, score, run_id: runId } }), candidate.normalized);
34670
+ if (status === "claimed") {
34671
+ run9(database, `UPDATE contact_methods SET identity_eligible = 0
34672
+ WHERE kind = 'email' AND normalized_value = ?`, candidate.normalized);
34673
+ }
34674
+ }
34675
+ function verificationDetail(verification, status) {
34676
+ return {
34677
+ status,
34678
+ score: verification.score,
34679
+ flags: {
34680
+ regexp: verification.regexp,
34681
+ gibberish: verification.gibberish,
34682
+ disposable: verification.disposable,
34683
+ webmail: verification.webmail,
34684
+ mx_records: verification.mx_records,
34685
+ smtp_server: verification.smtp_server,
34686
+ smtp_check: verification.smtp_check,
34687
+ accept_all: verification.accept_all,
34688
+ block: verification.block
34689
+ },
34690
+ sources: verification.sources
34691
+ };
34692
+ }
34693
+ function createSourceRun2(database, startedAt, completedAt, attempted, total, verified, outcomes) {
34694
+ const sha2 = sha256(`hunter-email-verification:${startedAt}:${randomUUID5()}`);
34695
+ const locatorSha = sha256("hunter-email-verification-local-command");
34696
+ const source = run9(database, `INSERT INTO source_runs(
34697
+ provider, account_key, mode, source_sha256, source_locator_sha256,
34698
+ completeness, result_json, started_at, completed_at
34699
+ ) VALUES (?,?,?,?,?,?,?,?,?)`, PROVIDER12, ACCOUNT_KEY4, MODE9, sha2, locatorSha, attempted === total ? "complete" : "partial", canonicalJson2({ attempted, total, verified, outcomes }), startedAt, completedAt);
34700
+ return insertedId10(source);
34701
+ }
34702
+ async function hunterVerifyEmails(database, options) {
34703
+ if (options.apiKey.trim().length < 8)
34704
+ throw new Error("HUNTER_NOT_CONFIGURED");
34705
+ if (options.limit !== undefined && (!Number.isSafeInteger(options.limit) || options.limit < 1 || options.limit > 100)) {
34706
+ throw new Error("HUNTER_LIMIT_INVALID");
34707
+ }
34708
+ const delayMs = options.delayMs ?? 500;
34709
+ const startedAt = new Date().toISOString();
34710
+ const candidates = fetchCandidates2(database, options.limit);
34711
+ const total = candidates.length;
34712
+ const outcomes = {
34713
+ valid: 0,
34714
+ invalid: 0,
34715
+ accept_all: 0,
34716
+ webmail: 0,
34717
+ disposable: 0,
34718
+ unknown: 0,
34719
+ claimed: 0,
34720
+ invalid_email: 0,
34721
+ pending: 0,
34722
+ smtp_failed: 0,
34723
+ rate_limited: 0,
34724
+ authentication_failed: 0,
34725
+ provider_error: 0,
34726
+ network_error: 0
34727
+ };
34728
+ const internalRecords = [];
34729
+ let attempted = 0;
34730
+ let verified = 0;
34731
+ for (const candidate of candidates) {
34732
+ attempted += 1;
34733
+ let status = "network_error";
34734
+ let score = null;
34735
+ let errorDetail = null;
34736
+ let verification;
34737
+ try {
34738
+ const data = await hunterVerifyEmail(options.apiKey, candidate.email);
34739
+ verification = data;
34740
+ score = data.score;
34741
+ if (data.status !== null && VERDICTS.has(data.status)) {
34742
+ status = data.status;
34743
+ verified += 1;
34744
+ } else {
34745
+ status = "provider_error";
34746
+ errorDetail = "HUNTER_UNKNOWN_STATUS";
34747
+ }
34748
+ } catch (error) {
34749
+ if (error instanceof HunterPending) {
34750
+ status = "pending";
34751
+ } else if (error instanceof HunterHttpError2) {
34752
+ status = hunterErrorStatus2(error);
34753
+ errorDetail = { httpStatus: error.status };
34754
+ } else if (error instanceof HunterResponseError2) {
34755
+ status = "provider_error";
34756
+ errorDetail = error.message;
34757
+ } else {
34758
+ status = "network_error";
34759
+ errorDetail = "HUNTER_NETWORK_ERROR";
34760
+ }
34761
+ }
34762
+ outcomes[status] += 1;
34763
+ internalRecords.push({
34764
+ record: {
34765
+ contact_method_id: candidate.contact_method_id,
34766
+ person_id: candidate.person_id,
34767
+ status,
34768
+ score,
34769
+ error: errorDetail
34770
+ },
34771
+ candidate,
34772
+ ...verification === undefined ? {} : { verification }
34773
+ });
34774
+ options.onProgress?.(attempted, total);
34775
+ if (status === "authentication_failed" || status === "rate_limited")
34776
+ break;
34777
+ if (attempted < total)
34778
+ await new Promise((resolve8) => setTimeout(resolve8, delayMs));
34779
+ }
34780
+ const completedAt = new Date().toISOString();
34781
+ let runId = 0;
34782
+ database.exec("BEGIN");
34783
+ try {
34784
+ runId = createSourceRun2(database, startedAt, completedAt, attempted, total, verified, outcomes);
34785
+ internalRecords.forEach(({ record: record9, candidate, verification }, index) => {
34786
+ let detail;
34787
+ if (verification !== undefined) {
34788
+ detail = verificationDetail(verification, record9.status);
34789
+ applyVerificationMetadata(database, candidate, runId, record9.status, verification.score);
34790
+ } else {
34791
+ detail = { status: record9.status, error: record9.error };
34792
+ if (record9.status === "claimed")
34793
+ applyVerificationMetadata(database, candidate, runId, record9.status, null);
34794
+ }
34795
+ const errorText = ["provider_error", "network_error", "rate_limited", "authentication_failed"].includes(record9.status) ? JSON.stringify(record9.error) : null;
34796
+ const resolution = VERDICTS.has(record9.status) ? "matched" : "skipped";
34797
+ run9(database, `INSERT INTO source_records(
34798
+ run_id, ordinal, record_kind, record_sha256, person_id,
34799
+ provider_resource_id, resolution, projection_json, error_text
34800
+ ) VALUES (?,?,?,?,?,?,?,?,?)`, runId, index + 1, MODE9, sha256(JSON.stringify({ ordinal: index + 1, run: runId, detail })), record9.person_id, null, resolution, JSON.stringify(detail), errorText);
34801
+ });
34802
+ database.exec("COMMIT");
34803
+ } catch (error) {
34804
+ database.exec("ROLLBACK");
34805
+ throw error;
34806
+ }
34807
+ return {
34808
+ attempted,
34809
+ verified,
34810
+ run_id: runId,
34811
+ outcomes,
34812
+ records: internalRecords.map(({ record: record9 }) => record9)
34813
+ };
34814
+ }
34815
+
34816
+ // src/local/providers/hunter-find-emails.ts
34817
+ import { randomUUID as randomUUID6 } from "crypto";
34818
+ var PROVIDER13 = "hunter";
34819
+ var LINKEDIN_PROVIDER2 = "linkedin";
34820
+ var ACCOUNT_KEY5 = "hunter.io";
34821
+ var MODE10 = "email-finder";
34822
+ function allRows9(database, sql, ...bindings) {
34823
+ return database.query(sql).all(...bindings);
34824
+ }
34825
+ function run10(database, sql, ...bindings) {
34826
+ return database.run(sql, bindings);
34827
+ }
34828
+ function insertedId11(value) {
34829
+ return Number(value.lastInsertRowid);
34830
+ }
34831
+ var TERMINAL_STATUSES2 = [
34832
+ "accepted",
34833
+ "already_known",
34834
+ "conflict",
34835
+ "name_rejected",
34836
+ "not_found",
34837
+ "claimed",
34838
+ "invalid_request"
34839
+ ];
34840
+ var JUNK_ORGANIZATIONS = new Set([
34841
+ "n/a",
34842
+ "na",
34843
+ "none",
34844
+ "self",
34845
+ "self-employed",
34846
+ "self employed",
34847
+ "freelance",
34848
+ "freelancer",
34849
+ "unemployed",
34850
+ "retired",
34851
+ "student",
34852
+ "home",
34853
+ "private",
34854
+ "me",
34855
+ "myself",
34856
+ "unknown",
34857
+ "various",
34858
+ "personal",
34859
+ "independent",
34860
+ "consultant",
34861
+ "consulting",
34862
+ "-",
34863
+ "--",
34864
+ "."
34865
+ ]);
34866
+ function usableOrganization(value) {
34867
+ if (value === null)
34868
+ return null;
34869
+ const trimmed = value.trim();
34870
+ if (trimmed.length < 3 || trimmed.length > 100)
34871
+ return null;
34872
+ if (!/[A-Za-z]/u.test(trimmed))
34873
+ return null;
34874
+ if (JUNK_ORGANIZATIONS.has(trimmed.toLowerCase()))
34875
+ return null;
34876
+ return trimmed;
34877
+ }
34878
+ function extractLinkedInHandle(username3, profileUrl2, resourceId) {
34879
+ const fromUsername = username3?.trim();
34880
+ if (fromUsername !== undefined && /^[A-Za-z0-9_-]{3,100}$/u.test(fromUsername))
34881
+ return fromUsername;
34882
+ const candidates = [profileUrl2, resourceId];
34883
+ for (const raw of candidates) {
34884
+ if (raw === null || raw === undefined)
34885
+ continue;
34886
+ const value = raw.trim();
34887
+ const urlText = value.startsWith("http://") || value.startsWith("https://") ? value : null;
34888
+ if (urlText === null) {
34889
+ if (/^[A-Za-z0-9_-]{3,100}$/u.test(value))
34890
+ return value;
34891
+ continue;
34892
+ }
34893
+ try {
34894
+ const url = new URL(urlText);
34895
+ if (url.protocol !== "https:" || !["linkedin.com", "www.linkedin.com"].includes(url.hostname.toLowerCase()))
34896
+ continue;
34897
+ const match = /^\/in\/([^/]+)\/?$/u.exec(url.pathname);
34898
+ if (match === null)
34899
+ continue;
34900
+ const handle = decodeURIComponent(match[1] ?? "");
34901
+ if (handle.length >= 1 && handle.length <= 128 && !/\s/u.test(handle))
34902
+ return handle;
34903
+ } catch {
34904
+ continue;
34905
+ }
34906
+ }
34907
+ return null;
34908
+ }
34909
+ function finderSuppression() {
34910
+ const terminal = TERMINAL_STATUSES2.map((status) => `'${status}'`).join(",");
34911
+ return `NOT EXISTS (
34912
+ SELECT 1
34913
+ FROM source_records sr
34914
+ JOIN source_runs source_run ON source_run.id = sr.run_id
34915
+ AND source_run.provider = 'hunter'
34916
+ AND source_run.mode = 'email-finder'
34917
+ WHERE sr.person_id = p.id
34918
+ AND json_extract(sr.projection_json, '$.status') IN (${terminal})
34919
+ )`;
34920
+ }
34921
+ function fetchCandidates3(database, limit) {
34922
+ const linkedinRows = allRows9(database, `SELECT p.id, p.display_name, p.given_name, p.family_name, p.organization,
34923
+ pr.username, pr.profile_url, pr.resource_id AS pr_resource_id
34924
+ FROM people p
34925
+ JOIN provider_resources pr ON pr.person_id = p.id AND pr.provider = 'linkedin' AND pr.active = 1
34926
+ WHERE p.do_not_contact = 0
34927
+ AND NOT EXISTS (
34928
+ SELECT 1 FROM contact_methods cm WHERE cm.person_id = p.id AND cm.kind = 'email' AND cm.active = 1
34929
+ )
34930
+ AND ${finderSuppression()}
34931
+ ORDER BY (
34932
+ SELECT COALESCE(SUM(interaction_count), 0) FROM interaction_metrics im WHERE im.person_id = p.id
34933
+ ) DESC, p.id, pr.id`);
34934
+ const companyRows = allRows9(database, `SELECT p.id, p.display_name, p.given_name, p.family_name, p.organization
34935
+ FROM people p
34936
+ WHERE p.do_not_contact = 0
34937
+ AND p.organization IS NOT NULL AND p.organization <> ''
34938
+ AND NOT EXISTS (
34939
+ SELECT 1 FROM contact_methods cm WHERE cm.person_id = p.id AND cm.kind = 'email' AND cm.active = 1
34940
+ )
34941
+ AND ${finderSuppression()}
34942
+ ORDER BY (
34943
+ SELECT COALESCE(SUM(interaction_count), 0) FROM interaction_metrics im WHERE im.person_id = p.id
34944
+ ) DESC, p.id`);
34945
+ const unique = new Map;
34946
+ for (const row of linkedinRows) {
34947
+ if (unique.has(row.id))
34948
+ continue;
34949
+ const handle = extractLinkedInHandle(row.username, row.profile_url, row.pr_resource_id);
34950
+ if (handle === null)
34951
+ continue;
34952
+ unique.set(row.id, {
34953
+ person_id: row.id,
34954
+ display_name: row.display_name,
34955
+ given_name: row.given_name,
34956
+ family_name: row.family_name,
34957
+ organization: row.organization,
34958
+ anchor: "linkedin",
34959
+ linkedin_handle: handle
34960
+ });
34961
+ }
34962
+ for (const row of companyRows) {
34963
+ if (unique.has(row.id))
34964
+ continue;
34965
+ if (usableOrganization(row.organization) === null)
34966
+ continue;
34967
+ const hasSplitName = (row.given_name?.trim() ?? "") !== "" && (row.family_name?.trim() ?? "") !== "";
34968
+ const hasFullName = (row.display_name?.trim() ?? "").split(/\s+/u).filter(Boolean).length >= 2;
34969
+ if (!hasSplitName && !hasFullName)
34970
+ continue;
34971
+ unique.set(row.id, {
34972
+ person_id: row.id,
34973
+ display_name: row.display_name,
34974
+ given_name: row.given_name,
34975
+ family_name: row.family_name,
34976
+ organization: row.organization,
34977
+ anchor: "company",
34978
+ linkedin_handle: null
34979
+ });
34980
+ }
34981
+ const values = [...unique.values()];
34982
+ return limit === undefined ? values : values.slice(0, limit);
34983
+ }
34984
+ var HUNTER_MAX_RESPONSE_BYTES3 = 2 * 1024 * 1024;
34985
+
34986
+ class HunterHttpError3 extends Error {
34987
+ status;
34988
+ constructor(status) {
34989
+ super(`HUNTER_HTTP_${status}`);
34990
+ this.status = status;
34991
+ }
34992
+ }
34993
+
34994
+ class HunterResponseError3 extends Error {
34995
+ constructor() {
34996
+ super("HUNTER_INVALID_RESPONSE");
34997
+ }
34998
+ }
34999
+ function optionalBoundedText2(value, maximum) {
35000
+ if (value === undefined || value === null)
35001
+ return null;
35002
+ if (typeof value !== "string" || Buffer.byteLength(value, "utf8") > maximum)
35003
+ throw new HunterResponseError3;
35004
+ return value;
35005
+ }
35006
+ function parseFinderResponse(value) {
35007
+ if (value === null || typeof value !== "object" || Array.isArray(value))
35008
+ throw new HunterResponseError3;
35009
+ const root = value;
35010
+ const data = root["data"];
35011
+ if (data === null || data === undefined) {
35012
+ return {
35013
+ email: null,
35014
+ score: null,
35015
+ domain: null,
35016
+ first_name: null,
35017
+ last_name: null,
35018
+ company: null,
35019
+ position: null,
35020
+ linkedin_url: null,
35021
+ verification_status: null,
35022
+ verification_date: null,
35023
+ sources: 0
35024
+ };
35025
+ }
35026
+ if (typeof data !== "object" || Array.isArray(data))
35027
+ throw new HunterResponseError3;
35028
+ const record9 = data;
35029
+ const score = record9["score"];
35030
+ if (score !== undefined && score !== null && (typeof score !== "number" || !Number.isFinite(score) || score < 0 || score > 100)) {
35031
+ throw new HunterResponseError3;
35032
+ }
35033
+ const verification = record9["verification"];
35034
+ let verificationStatus = null;
35035
+ let verificationDate = null;
35036
+ if (verification !== undefined && verification !== null) {
35037
+ if (typeof verification !== "object" || Array.isArray(verification))
35038
+ throw new HunterResponseError3;
35039
+ const verificationRecord = verification;
35040
+ verificationStatus = optionalBoundedText2(verificationRecord["status"], 32);
35041
+ verificationDate = optionalBoundedText2(verificationRecord["date"], 64);
35042
+ }
35043
+ const sources = record9["sources"];
35044
+ const sourceCount = Array.isArray(sources) ? Math.min(sources.length, 100) : 0;
35045
+ return {
35046
+ email: optionalBoundedText2(record9["email"], 320),
35047
+ score: score === undefined || score === null ? null : score,
35048
+ domain: optionalBoundedText2(record9["domain"], 255),
35049
+ first_name: optionalBoundedText2(record9["first_name"], 256),
35050
+ last_name: optionalBoundedText2(record9["last_name"], 256),
35051
+ company: optionalBoundedText2(record9["company"], 512),
35052
+ position: optionalBoundedText2(record9["position"], 512),
35053
+ linkedin_url: optionalBoundedText2(record9["linkedin_url"], 2048),
35054
+ verification_status: verificationStatus,
35055
+ verification_date: verificationDate,
35056
+ sources: sourceCount
35057
+ };
35058
+ }
35059
+ function finderUrl(apiKey, candidate) {
35060
+ const params = new URLSearchParams;
35061
+ if (candidate.anchor === "linkedin") {
35062
+ params.set("linkedin_handle", candidate.linkedin_handle ?? "");
35063
+ } else {
35064
+ params.set("company", usableOrganization(candidate.organization) ?? "");
35065
+ const given = candidate.given_name?.trim() ?? "";
35066
+ const family = candidate.family_name?.trim() ?? "";
35067
+ if (given !== "" && family !== "") {
35068
+ params.set("first_name", given);
35069
+ params.set("last_name", family);
35070
+ } else {
35071
+ params.set("full_name", candidate.display_name?.trim() ?? "");
35072
+ }
35073
+ }
35074
+ params.set("api_key", apiKey);
35075
+ return `https://api.hunter.io/v2/email-finder?${params.toString()}`;
35076
+ }
35077
+ async function hunterFindEmail(apiKey, candidate) {
35078
+ const response = await fetch(finderUrl(apiKey, candidate), {
35079
+ method: "GET",
35080
+ headers: { Accept: "application/json" },
35081
+ redirect: "error",
35082
+ signal: AbortSignal.timeout(45000)
35083
+ });
35084
+ if (!response.ok)
35085
+ throw new HunterHttpError3(response.status);
35086
+ if (!(response.headers.get("content-type") ?? "").toLowerCase().includes("application/json"))
35087
+ throw new HunterResponseError3;
35088
+ const declaredLength = Number(response.headers.get("content-length") ?? "0");
35089
+ if (Number.isFinite(declaredLength) && declaredLength > HUNTER_MAX_RESPONSE_BYTES3)
35090
+ throw new HunterResponseError3;
35091
+ const bytes = new Uint8Array(await response.arrayBuffer());
35092
+ if (bytes.byteLength > HUNTER_MAX_RESPONSE_BYTES3)
35093
+ throw new HunterResponseError3;
35094
+ try {
35095
+ return parseFinderResponse(JSON.parse(new TextDecoder().decode(bytes)));
35096
+ } catch (error) {
35097
+ if (error instanceof HunterResponseError3)
35098
+ throw error;
35099
+ throw new HunterResponseError3;
35100
+ }
35101
+ }
35102
+ function hunterErrorStatus3(error) {
35103
+ if (error.status === 400)
35104
+ return "invalid_request";
35105
+ if (error.status === 401 || error.status === 403)
35106
+ return "authentication_failed";
35107
+ if (error.status === 404)
35108
+ return "not_found";
35109
+ if (error.status === 429)
35110
+ return "rate_limited";
35111
+ if (error.status === 451)
35112
+ return "claimed";
35113
+ return "provider_error";
35114
+ }
35115
+ function normalizedNameTokens2(value) {
35116
+ return value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/gu, " ").trim().split(/\s+/u).filter(Boolean);
35117
+ }
35118
+ function returnedNameConflicts(candidate, result) {
35119
+ const returnedTokens = normalizedNameTokens2(`${result.first_name ?? ""} ${result.last_name ?? ""}`);
35120
+ if (returnedTokens.length < 2)
35121
+ return false;
35122
+ const localTokens = normalizedNameTokens2([candidate.given_name, candidate.family_name].filter((part) => (part?.trim() ?? "") !== "").join(" ") || (candidate.display_name ?? ""));
35123
+ if (localTokens.length < 2)
35124
+ return false;
35125
+ return !localTokens.some((token3) => token3.length >= 2 && returnedTokens.includes(token3));
35126
+ }
35127
+ function usableEmail(value) {
35128
+ if (value === null)
35129
+ return null;
35130
+ const trimmed = value.trim();
35131
+ if (trimmed.length > 320)
35132
+ return null;
35133
+ if (!/^[^@\s]{1,64}@[^@\s]{1,255}$/u.test(trimmed))
35134
+ return null;
35135
+ const domain = trimmed.split("@").pop() ?? "";
35136
+ if (!domain.includes("."))
35137
+ return null;
35138
+ return trimmed;
35139
+ }
35140
+ function emailOwner(database, normalized) {
35141
+ const row = database.query(`SELECT person_id FROM contact_methods WHERE kind='email' AND normalized_value=? AND active=1 LIMIT 1`).get(normalized);
35142
+ return row?.person_id ?? null;
35143
+ }
35144
+ function linkedInHandleFromUrl(rawUrl) {
35145
+ return extractLinkedInHandle(null, rawUrl, null);
35146
+ }
35147
+ function linkedInResourceConflicts2(database, personId, handle) {
35148
+ const profileUrl2 = `https://www.linkedin.com/in/${handle}/`;
35149
+ const profileUrlWithoutSlash = profileUrl2.slice(0, -1);
35150
+ const profileUrlWithoutWww = `https://linkedin.com/in/${handle}`;
35151
+ const rows = database.query(`SELECT DISTINCT person_id FROM provider_resources
35152
+ WHERE provider='linkedin' AND active=1
35153
+ AND (lower(resource_id) IN (?,?,?,?) OR lower(profile_url) IN (?,?,?))`).all(handle.toLowerCase(), profileUrl2.toLowerCase(), profileUrlWithoutSlash.toLowerCase(), profileUrlWithoutWww.toLowerCase(), profileUrl2.toLowerCase(), profileUrlWithoutSlash.toLowerCase(), profileUrlWithoutWww.toLowerCase());
35154
+ return rows.some((row) => row.person_id !== personId);
35155
+ }
35156
+ function insertFoundEmail(database, candidate, result, email3, runId) {
35157
+ const normalized = email3.toLowerCase();
35158
+ const confidence = result.verification_status === "valid" ? "likely" : "possible";
35159
+ const metadata3 = canonicalJson2({
35160
+ source: "hunter-email-finder",
35161
+ anchor: candidate.anchor,
35162
+ score: result.score,
35163
+ domain: result.domain,
35164
+ verification_status: result.verification_status,
35165
+ verification_date: result.verification_date
35166
+ });
35167
+ run10(database, `INSERT INTO contact_methods(
35168
+ person_id, kind, value, normalized_value, label, confidence,
35169
+ first_seen_run_id, last_seen_run_id, metadata_json, active, identity_eligible
35170
+ ) VALUES (?,'email',?,?, '', ?, ?, ?, ?, 1, 1)
35171
+ ON CONFLICT(person_id, kind, normalized_value, label) DO UPDATE SET
35172
+ active=1,
35173
+ confidence=excluded.confidence,
35174
+ last_seen_run_id=excluded.last_seen_run_id,
35175
+ metadata_json=excluded.metadata_json`, candidate.person_id, email3, normalized, confidence, runId, runId, metadata3);
35176
+ }
35177
+ function insertLinkedInResource2(database, personId, handle, result, runId) {
35178
+ const profileUrl2 = `https://www.linkedin.com/in/${handle}/`;
35179
+ const displayName2 = [result.first_name, result.last_name].filter((part) => part !== null && part !== "").join(" ") || null;
35180
+ run10(database, `INSERT INTO provider_resources(
35181
+ provider, account_key, resource_type, resource_id, person_id,
35182
+ username, profile_url, display_name, active,
35183
+ first_seen_run_id, last_seen_run_id, metadata_json,
35184
+ profile_url_identity_eligible, name_identity_eligible
35185
+ ) VALUES (?,?,?,?,?,?,?,?,1,?,?,?,1,1)
35186
+ ON CONFLICT(provider, account_key, resource_type, resource_id) DO UPDATE SET
35187
+ username=excluded.username,
35188
+ profile_url=excluded.profile_url,
35189
+ display_name=excluded.display_name,
35190
+ active=1,
35191
+ last_seen_run_id=excluded.last_seen_run_id,
35192
+ metadata_json=excluded.metadata_json,
35193
+ profile_url_identity_eligible=1,
35194
+ name_identity_eligible=1`, LINKEDIN_PROVIDER2, ACCOUNT_KEY5, "profile", handle, personId, handle, profileUrl2, displayName2, runId, runId, canonicalJson2({
35195
+ source: "hunter-email-finder",
35196
+ company: result.company,
35197
+ position: result.position,
35198
+ first_name: result.first_name,
35199
+ last_name: result.last_name
35200
+ }));
35201
+ const resource = database.query(`SELECT id FROM provider_resources
35202
+ WHERE provider=? AND account_key=? AND resource_type='profile' AND resource_id=?`).get(LINKEDIN_PROVIDER2, ACCOUNT_KEY5, handle);
35203
+ if (resource === null)
35204
+ throw new Error("HUNTER_LINKEDIN_RESOURCE_MISSING");
35205
+ return resource.id;
35206
+ }
35207
+ function createSourceRun3(database, startedAt, completedAt, attempted, total, found, outcomes) {
35208
+ const sha2 = sha256(`hunter-email-finder:${startedAt}:${randomUUID6()}`);
35209
+ const locatorSha = sha256("hunter-email-finder-local-command");
35210
+ const source = run10(database, `INSERT INTO source_runs(
35211
+ provider, account_key, mode, source_sha256, source_locator_sha256,
35212
+ completeness, result_json, started_at, completed_at
35213
+ ) VALUES (?,?,?,?,?,?,?,?,?)`, PROVIDER13, ACCOUNT_KEY5, MODE10, sha2, locatorSha, attempted === total ? "complete" : "partial", canonicalJson2({ attempted, total, emails_found: found, outcomes }), startedAt, completedAt);
35214
+ return insertedId11(source);
35215
+ }
35216
+ async function hunterFindEmails(database, options) {
35217
+ if (options.apiKey.trim().length < 8)
35218
+ throw new Error("HUNTER_NOT_CONFIGURED");
35219
+ if (options.limit !== undefined && (!Number.isSafeInteger(options.limit) || options.limit < 1 || options.limit > 100)) {
35220
+ throw new Error("HUNTER_LIMIT_INVALID");
35221
+ }
35222
+ const delayMs = options.delayMs ?? 500;
35223
+ const startedAt = new Date().toISOString();
35224
+ const candidates = fetchCandidates3(database, options.limit);
35225
+ const total = candidates.length;
35226
+ const outcomes = {
35227
+ accepted: 0,
35228
+ already_known: 0,
35229
+ conflict: 0,
35230
+ name_rejected: 0,
35231
+ not_found: 0,
35232
+ claimed: 0,
35233
+ invalid_request: 0,
35234
+ rate_limited: 0,
35235
+ authentication_failed: 0,
35236
+ provider_error: 0,
35237
+ network_error: 0
35238
+ };
35239
+ const internalRecords = [];
35240
+ let attempted = 0;
35241
+ let found = 0;
35242
+ const batchClaimed = new Map;
35243
+ for (const candidate of candidates) {
35244
+ attempted += 1;
35245
+ let status = "network_error";
35246
+ let errorDetail = null;
35247
+ let finderResult;
35248
+ let foundEmail;
35249
+ try {
35250
+ const data = await hunterFindEmail(options.apiKey, candidate);
35251
+ finderResult = data;
35252
+ const email3 = usableEmail(data.email);
35253
+ if (email3 === null) {
35254
+ status = "not_found";
35255
+ } else {
35256
+ const normalized = email3.toLowerCase();
35257
+ const owner = emailOwner(database, normalized) ?? batchClaimed.get(normalized) ?? null;
35258
+ if (owner === candidate.person_id) {
35259
+ status = "already_known";
35260
+ batchClaimed.set(normalized, candidate.person_id);
35261
+ found += 1;
35262
+ } else if (owner !== null) {
35263
+ status = "conflict";
35264
+ } else if (returnedNameConflicts(candidate, data)) {
35265
+ status = "name_rejected";
35266
+ } else {
35267
+ status = "accepted";
35268
+ foundEmail = email3;
35269
+ batchClaimed.set(normalized, candidate.person_id);
35270
+ found += 1;
35271
+ }
35272
+ }
35273
+ } catch (error) {
35274
+ if (error instanceof HunterHttpError3) {
35275
+ status = hunterErrorStatus3(error);
35276
+ errorDetail = { httpStatus: error.status };
35277
+ } else if (error instanceof HunterResponseError3) {
35278
+ status = "provider_error";
35279
+ errorDetail = error.message;
35280
+ } else {
35281
+ status = "network_error";
35282
+ errorDetail = "HUNTER_NETWORK_ERROR";
35283
+ }
35284
+ }
35285
+ outcomes[status] += 1;
35286
+ internalRecords.push({
35287
+ record: {
35288
+ person_id: candidate.person_id,
35289
+ status,
35290
+ anchor: candidate.anchor,
35291
+ error: errorDetail
35292
+ },
35293
+ candidate,
35294
+ ...finderResult === undefined ? {} : { result: finderResult },
35295
+ ...foundEmail === undefined ? {} : { email: foundEmail }
35296
+ });
35297
+ options.onProgress?.(attempted, total);
35298
+ if (status === "authentication_failed" || status === "rate_limited")
35299
+ break;
35300
+ if (attempted < total)
35301
+ await new Promise((resolve8) => setTimeout(resolve8, delayMs));
35302
+ }
35303
+ const completedAt = new Date().toISOString();
35304
+ let runId = 0;
35305
+ database.exec("BEGIN");
35306
+ try {
35307
+ runId = createSourceRun3(database, startedAt, completedAt, attempted, total, found, outcomes);
35308
+ internalRecords.forEach(({ record: record9, candidate, result, email: email3 }, index) => {
35309
+ let detail;
35310
+ let providerResourceId = null;
35311
+ if (record9.status === "accepted" && email3 !== undefined && result !== undefined) {
35312
+ insertFoundEmail(database, candidate, result, email3, runId);
35313
+ const bonusHandle = candidate.anchor === "company" ? linkedInHandleFromUrl(result.linkedin_url) : null;
35314
+ if (bonusHandle !== null && !linkedInResourceConflicts2(database, candidate.person_id, bonusHandle)) {
35315
+ providerResourceId = insertLinkedInResource2(database, candidate.person_id, bonusHandle, result, runId);
35316
+ }
35317
+ detail = {
35318
+ status: record9.status,
35319
+ anchor: candidate.anchor,
35320
+ score: result.score,
35321
+ domain: result.domain,
35322
+ verification_status: result.verification_status,
35323
+ sources: result.sources
35324
+ };
35325
+ } else if (record9.status === "claimed") {
35326
+ run10(database, `UPDATE people SET do_not_contact = 1 WHERE id = ?`, candidate.person_id);
35327
+ detail = { status: record9.status, anchor: candidate.anchor };
35328
+ } else {
35329
+ detail = { status: record9.status, anchor: candidate.anchor, error: record9.error };
35330
+ }
35331
+ const errorText = ["provider_error", "network_error", "rate_limited", "authentication_failed"].includes(record9.status) ? JSON.stringify(record9.error) : null;
35332
+ const resolution = record9.status === "accepted" ? "imported" : record9.status === "already_known" ? "matched" : record9.status === "conflict" ? "conflict" : "skipped";
35333
+ run10(database, `INSERT INTO source_records(
35334
+ run_id, ordinal, record_kind, record_sha256, person_id,
35335
+ provider_resource_id, resolution, projection_json, error_text
35336
+ ) VALUES (?,?,?,?,?,?,?,?,?)`, runId, index + 1, MODE10, sha256(JSON.stringify({ ordinal: index + 1, run: runId, detail })), record9.person_id, providerResourceId, resolution, JSON.stringify(detail), errorText);
35337
+ });
35338
+ database.exec("COMMIT");
35339
+ } catch (error) {
35340
+ database.exec("ROLLBACK");
35341
+ throw error;
35342
+ }
35343
+ return {
35344
+ attempted,
35345
+ emails_found: found,
35346
+ run_id: runId,
35347
+ outcomes,
35348
+ records: internalRecords.map(({ record: record9 }) => record9)
35349
+ };
35350
+ }
35351
+
34486
35352
  // src/local/providers/linkedin-contact-info.ts
34487
35353
  var LINKEDIN_CONTACT_INFO_ADAPTER_ID = "linkedin-web";
34488
35354
  var LINKEDIN_CONTACT_INFO_OPERATION_ID = "contacts.read";
34489
35355
  var LINKEDIN_CONTACT_INFO_ADAPTER_VERSION = "1.23.0";
34490
35356
  var LINKEDIN_CONTACT_INFO_CONTRACT_SHA256 = "8f80c234ca2b7706a49a5000012a11da78a36d38f3023c21854a240e16a13f43";
34491
35357
  var DEFAULT_LINKEDIN_AUTH = "linkedin-main";
34492
- var PROVIDER12 = "linkedin";
34493
- var MODE9 = "ghostget-contact-info";
35358
+ var PROVIDER14 = "linkedin";
35359
+ var MODE11 = "ghostget-contact-info";
34494
35360
  var RECORD_KIND = "contact-info";
34495
35361
  var OBSERVATION_BASIS2 = "linkedin-contact-info";
34496
35362
  var OBSERVATION_PRIORITY2 = 720;
@@ -34507,13 +35373,13 @@ var AUTH_ID3 = /^[a-z0-9][a-z0-9._:-]{0,255}$/u;
34507
35373
  function getRow9(database, sql, ...bindings) {
34508
35374
  return database.query(sql).get(...bindings);
34509
35375
  }
34510
- function allRows8(database, sql, ...bindings) {
35376
+ function allRows10(database, sql, ...bindings) {
34511
35377
  return database.query(sql).all(...bindings);
34512
35378
  }
34513
- function run9(database, sql, ...bindings) {
35379
+ function run11(database, sql, ...bindings) {
34514
35380
  return database.run(sql, bindings);
34515
35381
  }
34516
- function insertedId10(value) {
35382
+ function insertedId12(value) {
34517
35383
  return Number(value.lastInsertRowid);
34518
35384
  }
34519
35385
  function record9(value, label) {
@@ -34604,7 +35470,7 @@ function parseFields(value, label) {
34604
35470
  function projectLinkedInContactInfo(value, expectedProfileUrl) {
34605
35471
  const output = record9(value, "Ghostget LinkedIn contact output");
34606
35472
  exact4(output, ["schemaVersion", "provider", "profile", "viewer", "observedAt", "completeness", "contact"], "contact output");
34607
- if (output.schemaVersion !== 1 || output.provider !== PROVIDER12)
35473
+ if (output.schemaVersion !== 1 || output.provider !== PROVIDER14)
34608
35474
  throw new Error("Ghostget LinkedIn contact semantics drifted");
34609
35475
  const profile = record9(output.profile, "contact output.profile");
34610
35476
  exact4(profile, ["vanity", "profileUrn", "url", "relationship"], "contact output.profile");
@@ -34692,9 +35558,9 @@ function readLinkedInContactInfo(database, options) {
34692
35558
  const auth = authId2(options.authId);
34693
35559
  if (!Number.isSafeInteger(options.personId) || options.personId < 1)
34694
35560
  throw new Error("person ID is invalid");
34695
- const resources = allRows8(database, `SELECT id,person_id,profile_url,resource_id,metadata_json FROM provider_resources
35561
+ const resources = allRows10(database, `SELECT id,person_id,profile_url,resource_id,metadata_json FROM provider_resources
34696
35562
  WHERE provider=? AND account_key=? AND resource_type='profile' AND active=1 AND person_id=?
34697
- ORDER BY id`, PROVIDER12, account, options.personId);
35563
+ ORDER BY id`, PROVIDER14, account, options.personId);
34698
35564
  if (resources.length === 0) {
34699
35565
  throw new Error("This person has no active LinkedIn profile in that account; import the official LinkedIn export first");
34700
35566
  }
@@ -34705,7 +35571,7 @@ function readLinkedInContactInfo(database, options) {
34705
35571
  const profileUrl2 = canonicalLinkedInProfileUrl(resource.profile_url ?? resource.resource_id, "stored LinkedIn profile URL");
34706
35572
  if (profileUrl2 === null)
34707
35573
  throw new Error("stored LinkedIn profile resource lacks an exact profile URL");
34708
- const owner = getRow9(database, "SELECT owner_key FROM archive_accounts WHERE provider=? AND account_key=?", PROVIDER12, account);
35574
+ const owner = getRow9(database, "SELECT owner_key FROM archive_accounts WHERE provider=? AND account_key=?", PROVIDER14, account);
34709
35575
  if (owner !== null && owner.owner_key === profileUrl2) {
34710
35576
  throw new Error("Contact info is read for connections, not the account owner's own profile");
34711
35577
  }
@@ -34740,19 +35606,19 @@ function readLinkedInContactInfo(database, options) {
34740
35606
  };
34741
35607
  database.exec("BEGIN IMMEDIATE");
34742
35608
  try {
34743
- const cached = getRow9(database, "SELECT id,result_json FROM source_runs WHERE provider=? AND account_key=? AND mode=? AND source_sha256=?", PROVIDER12, account, MODE9, sourceSha256);
35609
+ const cached = getRow9(database, "SELECT id,result_json FROM source_runs WHERE provider=? AND account_key=? AND mode=? AND source_sha256=?", PROVIDER14, account, MODE11, sourceSha256);
34744
35610
  if (cached !== null) {
34745
35611
  database.exec("COMMIT");
34746
35612
  return { ...storedResultFromJson(cached.result_json), run_id: cached.id, replayed: true };
34747
35613
  }
34748
- const runId = insertedId10(run9(database, `INSERT INTO source_runs(
35614
+ const runId = insertedId12(run11(database, `INSERT INTO source_runs(
34749
35615
  provider,account_key,mode,source_sha256,source_locator_sha256,completeness,result_json,started_at,completed_at
34750
- ) VALUES (?,?,?,?,?,?,?,?,?)`, PROVIDER12, account, MODE9, sourceSha256, sha256(profileUrl2), projection2.completeness, canonicalJson2(stored), execution.startedAt, execution.finishedAt));
35616
+ ) VALUES (?,?,?,?,?,?,?,?,?)`, PROVIDER14, account, MODE11, sourceSha256, sha256(profileUrl2), projection2.completeness, canonicalJson2(stored), execution.startedAt, execution.finishedAt));
34751
35617
  const methodLabel = `${OBSERVATION_BASIS2}:${sha256(profileUrl2).slice(0, 16)}`;
34752
35618
  const methodMetadata = canonicalJson2({ observedIn: OBSERVATION_BASIS2, observedAt: projection2.observedAt });
34753
- run9(database, "UPDATE contact_methods SET active=0 WHERE provider_resource_id=? AND label=?", resourceId, methodLabel);
35619
+ run11(database, "UPDATE contact_methods SET active=0 WHERE provider_resource_id=? AND label=?", resourceId, methodLabel);
34754
35620
  if (projection2.contact.email !== null) {
34755
- run9(database, `INSERT INTO contact_methods(
35621
+ run11(database, `INSERT INTO contact_methods(
34756
35622
  person_id,kind,value,normalized_value,label,confidence,first_seen_run_id,last_seen_run_id,
34757
35623
  metadata_json,provider_resource_id,active,identity_eligible
34758
35624
  ) VALUES (?,'email',?,?,?,'exact',?,?,?,?,1,1)
@@ -34763,7 +35629,7 @@ function readLinkedInContactInfo(database, options) {
34763
35629
  for (const { phone, normalized } of resolvedPhones) {
34764
35630
  if (normalized === null)
34765
35631
  continue;
34766
- run9(database, `INSERT INTO contact_methods(
35632
+ run11(database, `INSERT INTO contact_methods(
34767
35633
  person_id,kind,value,normalized_value,label,confidence,first_seen_run_id,last_seen_run_id,
34768
35634
  metadata_json,provider_resource_id,active,identity_eligible
34769
35635
  ) VALUES (?,'phone',?,?,?,'exact',?,?,?,?,1,1)
@@ -34772,7 +35638,7 @@ function readLinkedInContactInfo(database, options) {
34772
35638
  provider_resource_id=excluded.provider_resource_id,active=1,identity_eligible=1`, personId, phone, normalized, methodLabel, runId, runId, methodMetadata, resourceId);
34773
35639
  }
34774
35640
  for (const site of projection2.contact.websites) {
34775
- run9(database, `INSERT INTO contact_methods(
35641
+ run11(database, `INSERT INTO contact_methods(
34776
35642
  person_id,kind,value,normalized_value,label,confidence,first_seen_run_id,last_seen_run_id,
34777
35643
  metadata_json,provider_resource_id,active,identity_eligible
34778
35644
  ) VALUES (?,'url',?,?,?,'exact',?,?,?,?,1,0)
@@ -34780,14 +35646,14 @@ function readLinkedInContactInfo(database, options) {
34780
35646
  value=excluded.value,last_seen_run_id=excluded.last_seen_run_id,metadata_json=excluded.metadata_json,
34781
35647
  provider_resource_id=excluded.provider_resource_id,active=1,identity_eligible=0`, personId, site, normalizedWebsite(site), methodLabel, runId, runId, methodMetadata, resourceId);
34782
35648
  }
34783
- run9(database, "UPDATE person_field_observations SET active=0 WHERE provider_resource_id=? AND basis=?", resourceId, OBSERVATION_BASIS2);
35649
+ run11(database, "UPDATE person_field_observations SET active=0 WHERE provider_resource_id=? AND basis=?", resourceId, OBSERVATION_BASIS2);
34784
35650
  if (projection2.contact.birthday !== null) {
34785
- run9(database, `INSERT INTO person_field_observations(
35651
+ run11(database, `INSERT INTO person_field_observations(
34786
35652
  provider_resource_id,field,value,basis,priority,active,first_seen_run_id,last_seen_run_id,metadata_json
34787
35653
  ) VALUES (?,'birthday',?,?,?,1,?,?,'{}')
34788
35654
  ON CONFLICT(provider_resource_id,field,value,basis) DO UPDATE SET
34789
35655
  priority=excluded.priority,active=1,last_seen_run_id=excluded.last_seen_run_id`, resourceId, projection2.contact.birthday, OBSERVATION_BASIS2, OBSERVATION_PRIORITY2, runId, runId);
34790
- run9(database, "UPDATE people SET birthday=coalesce(birthday,?) WHERE id=?", projection2.contact.birthday, personId);
35656
+ run11(database, "UPDATE people SET birthday=coalesce(birthday,?) WHERE id=?", projection2.contact.birthday, personId);
34791
35657
  }
34792
35658
  const resourceMetadata = {
34793
35659
  ...metadata3(resource.metadata_json),
@@ -34799,12 +35665,12 @@ function readLinkedInContactInfo(database, options) {
34799
35665
  runId
34800
35666
  }
34801
35667
  };
34802
- run9(database, "UPDATE provider_resources SET last_seen_run_id=?,metadata_json=? WHERE id=?", runId, canonicalJson2(resourceMetadata), resourceId);
34803
- run9(database, `INSERT INTO source_records(
35668
+ run11(database, "UPDATE provider_resources SET last_seen_run_id=?,metadata_json=? WHERE id=?", runId, canonicalJson2(resourceMetadata), resourceId);
35669
+ run11(database, `INSERT INTO source_records(
34804
35670
  run_id,ordinal,record_kind,record_sha256,person_id,provider_resource_id,resolution,projection_json,error_text
34805
35671
  ) VALUES (?,1,?,?,?,?,'matched',?,NULL)`, runId, RECORD_KIND, sourceSha256, personId, resourceId, canonicalJson2(projection2));
34806
35672
  ledgerPeopleBladeGhostgetInvocation(database, {
34807
- provider: PROVIDER12,
35673
+ provider: PROVIDER14,
34808
35674
  accountKey: account,
34809
35675
  capability: "contact_info",
34810
35676
  execution,
@@ -34839,15 +35705,15 @@ var DEVICE_LID_JID = /^([0-9]{5,32}):[0-9]{1,5}@lid$/u;
34839
35705
  var GROUP_JID = /^[0-9]{5,32}(?:-[0-9]{5,20})?@g\.us$/u;
34840
35706
  var OTHER_JID = /^(?:0@s\.whatsapp\.net|[0-9]{5,32}@newsletter|(?:status|[0-9]{5,32})@broadcast)$/u;
34841
35707
  var MESSAGE_ID = /^[A-Za-z0-9._~:-]{1,256}$/u;
34842
- var PROVIDER13 = "whatsapp";
35708
+ var PROVIDER15 = "whatsapp";
34843
35709
  var STREAM2 = "messages";
34844
35710
  function getRow10(database, sql, ...values) {
34845
35711
  return database.query(sql).get(...values);
34846
35712
  }
34847
- function run10(database, sql, ...values) {
35713
+ function run12(database, sql, ...values) {
34848
35714
  return database.run(sql, values);
34849
35715
  }
34850
- function insertedId11(value) {
35716
+ function insertedId13(value) {
34851
35717
  return Number(value.lastInsertRowid);
34852
35718
  }
34853
35719
  function record10(value, label) {
@@ -35204,27 +36070,27 @@ function generationSha256(value) {
35204
36070
  }
35205
36071
  function ensureResource2(database, account, sourceRealmId, jid, name, basis, runId) {
35206
36072
  const existing = getRow10(database, `SELECT id,person_id,source_realm_id FROM provider_resources
35207
- WHERE provider=? AND account_key=? AND resource_type='jid' AND resource_id=?`, PROVIDER13, account, jid);
36073
+ WHERE provider=? AND account_key=? AND resource_type='jid' AND resource_id=?`, PROVIDER15, account, jid);
35208
36074
  if (existing !== null) {
35209
36075
  if (existing.source_realm_id !== null && existing.source_realm_id !== sourceRealmId) {
35210
36076
  throw new Error("WhatsApp contact resource crossed its bound account realm");
35211
36077
  }
35212
- run10(database, `UPDATE provider_resources SET source_realm_id=?,display_name=coalesce(?,display_name),active=1,last_seen_run_id=?,
36078
+ run12(database, `UPDATE provider_resources SET source_realm_id=?,display_name=coalesce(?,display_name),active=1,last_seen_run_id=?,
35213
36079
  metadata_json=? WHERE id=?`, sourceRealmId, name, runId, stableJson6({ jidKind: USER_JID.test(jid) ? "user" : "lid", displayNameBasis: basis }), existing.id);
35214
36080
  if (name !== null)
35215
- run10(database, "UPDATE people SET display_name=coalesce(display_name,?) WHERE id=?", name, existing.person_id);
36081
+ run12(database, "UPDATE people SET display_name=coalesce(display_name,?) WHERE id=?", name, existing.person_id);
35216
36082
  return { personId: existing.person_id, resourceId: existing.id, imported: false };
35217
36083
  }
35218
- const personId = insertedId11(run10(database, "INSERT INTO people(stable_key,display_name,metadata_json) VALUES (?,?,?)", `whatsapp:${sha2569(`${account}\x00${jid}`)}`, name, stableJson6({ createdBy: "exact-whatsapp-jid" })));
36084
+ const personId = insertedId13(run12(database, "INSERT INTO people(stable_key,display_name,metadata_json) VALUES (?,?,?)", `whatsapp:${sha2569(`${account}\x00${jid}`)}`, name, stableJson6({ createdBy: "exact-whatsapp-jid" })));
35219
36085
  const phone = phoneForJid(jid);
35220
36086
  if (phone !== null)
35221
- run10(database, `INSERT INTO contact_methods(
36087
+ run12(database, `INSERT INTO contact_methods(
35222
36088
  person_id,kind,value,normalized_value,label,confidence,first_seen_run_id,last_seen_run_id,metadata_json
35223
36089
  ) VALUES (?,'phone',?,?,'whatsapp','exact',?,?,?)`, personId, phone, phone, runId, runId, stableJson6({ derivedFromJid: true }));
35224
- const resourceId = insertedId11(run10(database, `INSERT INTO provider_resources(
36090
+ const resourceId = insertedId13(run12(database, `INSERT INTO provider_resources(
35225
36091
  provider,account_key,source_realm_id,resource_type,resource_id,person_id,display_name,active,
35226
36092
  first_seen_run_id,last_seen_run_id,metadata_json
35227
- ) VALUES (?,?,?,'jid',?,?,?,1,?,?,?)`, PROVIDER13, account, sourceRealmId, jid, personId, name, runId, runId, stableJson6({ jidKind: USER_JID.test(jid) ? "user" : "lid", displayNameBasis: basis })));
36093
+ ) VALUES (?,?,?,'jid',?,?,?,1,?,?,?)`, PROVIDER15, account, sourceRealmId, jid, personId, name, runId, runId, stableJson6({ jidKind: USER_JID.test(jid) ? "user" : "lid", displayNameBasis: basis })));
35228
36094
  return { personId, resourceId, imported: true };
35229
36095
  }
35230
36096
  function parseStoredGeneration(value) {
@@ -35238,12 +36104,12 @@ function databaseCheckpoint(database, auth) {
35238
36104
  const row = getRow10(database, `SELECT checkpoint.cursor,checkpoint.cursor_anchor_sha256,checkpoint.metadata_json
35239
36105
  FROM provider_accounts account JOIN provider_checkpoints checkpoint
35240
36106
  ON checkpoint.provider=account.provider AND checkpoint.account_key=account.account_key
35241
- WHERE account.provider=? AND account.auth_id=? AND checkpoint.stream_key=?`, PROVIDER13, auth, STREAM2);
36107
+ WHERE account.provider=? AND account.auth_id=? AND checkpoint.stream_key=?`, PROVIDER15, auth, STREAM2);
35242
36108
  return row === null ? { cursor: "0", anchor: null } : { cursor: row.cursor, anchor: row.cursor_anchor_sha256, generation: parseStoredGeneration(row.metadata_json) };
35243
36109
  }
35244
36110
  function refreshMetrics2(database, account, runId) {
35245
- run10(database, "DELETE FROM interaction_metrics WHERE provider=? AND account_key=?", PROVIDER13, account);
35246
- run10(database, `INSERT INTO interaction_metrics(
36111
+ run12(database, "DELETE FROM interaction_metrics WHERE provider=? AND account_key=?", PROVIDER15, account);
36112
+ run12(database, `INSERT INTO interaction_metrics(
35247
36113
  provider,account_key,person_id,sent_count,received_count,interaction_count,conversation_count,
35248
36114
  first_interaction_at,last_interaction_at,reciprocal,completeness,last_seen_run_id,metadata_json
35249
36115
  ) SELECT ?,?,person_id,sum(direction='outgoing'),sum(direction='incoming'),count(*),
@@ -35251,7 +36117,7 @@ function refreshMetrics2(database, account, runId) {
35251
36117
  CASE WHEN sum(direction='outgoing')>0 AND sum(direction='incoming')>0 THEN 1 ELSE 0 END,
35252
36118
  'lower-bound',?,'{"coverage":"local-insert-rowid-scan"}'
35253
36119
  FROM interaction_events WHERE provider=? AND account_key=? AND resolution='matched' AND person_id IS NOT NULL
35254
- GROUP BY person_id`, PROVIDER13, account, runId, PROVIDER13, account);
36120
+ GROUP BY person_id`, PROVIDER15, account, runId, PROVIDER15, account);
35255
36121
  }
35256
36122
  function resultFromJson2(value) {
35257
36123
  const parsed = record10(JSON.parse(value), "stored WhatsApp result");
@@ -35306,18 +36172,18 @@ function importWhatsAppSnapshot(database, snapshot) {
35306
36172
  }));
35307
36173
  database.exec("BEGIN IMMEDIATE");
35308
36174
  try {
35309
- const byAuth = getRow10(database, "SELECT account_key,auth_sha256 FROM provider_accounts WHERE provider=? AND auth_id=?", PROVIDER13, snapshot.authId);
35310
- const byAccount = getRow10(database, "SELECT auth_id,auth_sha256 FROM provider_accounts WHERE provider=? AND account_key=?", PROVIDER13, account);
36175
+ const byAuth = getRow10(database, "SELECT account_key,auth_sha256 FROM provider_accounts WHERE provider=? AND auth_id=?", PROVIDER15, snapshot.authId);
36176
+ const byAccount = getRow10(database, "SELECT auth_id,auth_sha256 FROM provider_accounts WHERE provider=? AND account_key=?", PROVIDER15, account);
35311
36177
  if (byAuth !== null && (byAuth.account_key !== account || byAuth.auth_sha256 !== firstExecution.authSha256) || byAccount !== null && (byAccount.auth_id !== snapshot.authId || byAccount.auth_sha256 !== firstExecution.authSha256)) {
35312
36178
  throw new Error("WhatsApp durable auth-to-account binding drifted");
35313
36179
  }
35314
36180
  if (byAuth === null)
35315
- run10(database, `INSERT INTO provider_accounts(provider,auth_id,account_key,auth_sha256,first_seen_at,last_seen_at)
35316
- VALUES (?,?,?,?,?,?)`, PROVIDER13, snapshot.authId, account, firstExecution.authSha256, firstExecution.startedAt, lastExecution.finishedAt);
36181
+ run12(database, `INSERT INTO provider_accounts(provider,auth_id,account_key,auth_sha256,first_seen_at,last_seen_at)
36182
+ VALUES (?,?,?,?,?,?)`, PROVIDER15, snapshot.authId, account, firstExecution.authSha256, firstExecution.startedAt, lastExecution.finishedAt);
35317
36183
  else
35318
- run10(database, "UPDATE provider_accounts SET last_seen_at=? WHERE provider=? AND auth_id=?", lastExecution.finishedAt, PROVIDER13, snapshot.authId);
36184
+ run12(database, "UPDATE provider_accounts SET last_seen_at=? WHERE provider=? AND auth_id=?", lastExecution.finishedAt, PROVIDER15, snapshot.authId);
35319
36185
  const sourceRealmId = bindProviderAccountRealm(database, {
35320
- provider: PROVIDER13,
36186
+ provider: PROVIDER15,
35321
36187
  authId: snapshot.authId,
35322
36188
  accountKey: account,
35323
36189
  authSha256: firstExecution.authSha256,
@@ -35325,7 +36191,7 @@ function importWhatsAppSnapshot(database, snapshot) {
35325
36191
  lastSeenAt: lastExecution.finishedAt
35326
36192
  }).id;
35327
36193
  const storedCheckpoint = getRow10(database, `SELECT cursor,cursor_anchor_sha256,generation_sha256,metadata_json
35328
- FROM provider_checkpoints WHERE provider=? AND account_key=? AND stream_key=?`, PROVIDER13, account, STREAM2);
36194
+ FROM provider_checkpoints WHERE provider=? AND account_key=? AND stream_key=?`, PROVIDER15, account, STREAM2);
35329
36195
  if (storedCheckpoint === null) {
35330
36196
  if (snapshot.startCheckpoint.cursor !== "0" || snapshot.startCheckpoint.anchor !== null || snapshot.startCheckpoint.generation !== undefined)
35331
36197
  throw new Error("WhatsApp initial scan must start at zero");
@@ -35336,7 +36202,7 @@ function importWhatsAppSnapshot(database, snapshot) {
35336
36202
  }
35337
36203
  }
35338
36204
  const existingRun = getRow10(database, `SELECT result_json FROM source_runs
35339
- WHERE provider=? AND account_key=? AND mode='linked-device-sync' AND source_sha256=?`, PROVIDER13, account, sourceDigest);
36205
+ WHERE provider=? AND account_key=? AND mode='linked-device-sync' AND source_sha256=?`, PROVIDER15, account, sourceDigest);
35340
36206
  if (existingRun !== null) {
35341
36207
  database.exec("COMMIT");
35342
36208
  return resultFromJson2(existingRun.result_json);
@@ -35354,7 +36220,7 @@ function importWhatsAppSnapshot(database, snapshot) {
35354
36220
  continue;
35355
36221
  }
35356
36222
  const exists = getRow10(database, `SELECT 1 FROM provider_resources
35357
- WHERE provider=? AND account_key=? AND resource_type='jid' AND resource_id=?`, PROVIDER13, account, contact.providerId) !== null;
36223
+ WHERE provider=? AND account_key=? AND resource_type='jid' AND resource_id=?`, PROVIDER15, account, contact.providerId) !== null;
35358
36224
  if (exists)
35359
36225
  expectedContactsMatched += 1;
35360
36226
  else
@@ -35387,11 +36253,11 @@ function importWhatsAppSnapshot(database, snapshot) {
35387
36253
  counts_complete: false,
35388
36254
  counts_lower_bound: true
35389
36255
  };
35390
- const runInsert = run10(database, `INSERT INTO source_runs(
36256
+ const runInsert = run12(database, `INSERT INTO source_runs(
35391
36257
  provider,account_key,mode,source_sha256,source_locator_sha256,completeness,
35392
36258
  result_json,started_at,completed_at
35393
- ) VALUES (?,?,'linked-device-sync',?,?,'lower-bound',?,?,?)`, PROVIDER13, account, sourceDigest, sha2569(snapshot.authId), stableJson6(expectedResult), firstExecution.startedAt, lastExecution.finishedAt);
35394
- const runId = insertedId11(runInsert);
36259
+ ) VALUES (?,?,'linked-device-sync',?,?,'lower-bound',?,?,?)`, PROVIDER15, account, sourceDigest, sha2569(snapshot.authId), stableJson6(expectedResult), firstExecution.startedAt, lastExecution.finishedAt);
36260
+ const runId = insertedId13(runInsert);
35395
36261
  let ordinal = 0;
35396
36262
  let contactsSeen = 0;
35397
36263
  let contactsImported = 0;
@@ -35404,7 +36270,7 @@ function importWhatsAppSnapshot(database, snapshot) {
35404
36270
  if (contact.providerId === owner) {
35405
36271
  contactsSkippedSelf += 1;
35406
36272
  const item2 = stableJson6({ type: "contact", ...contact, resolution: "skipped-self" });
35407
- run10(database, `INSERT INTO source_records(run_id,ordinal,record_kind,record_sha256,resolution,projection_json,error_text)
36273
+ run12(database, `INSERT INTO source_records(run_id,ordinal,record_kind,record_sha256,resolution,projection_json,error_text)
35408
36274
  VALUES (?,?,'contact',?,'skipped',?,'owner-self-contact')`, runId, ordinal, sha2569(item2), item2);
35409
36275
  continue;
35410
36276
  }
@@ -35414,7 +36280,7 @@ function importWhatsAppSnapshot(database, snapshot) {
35414
36280
  else
35415
36281
  contactsMatched += 1;
35416
36282
  const item = stableJson6({ type: "contact", ...contact });
35417
- run10(database, `INSERT INTO source_records(
36283
+ run12(database, `INSERT INTO source_records(
35418
36284
  run_id,ordinal,record_kind,record_sha256,person_id,provider_resource_id,resolution,projection_json
35419
36285
  ) VALUES (?,?,'contact',?,?,?,?,?)`, runId, ordinal, sha2569(item), identity.personId, identity.resourceId, identity.imported ? "imported" : "matched", item);
35420
36286
  }
@@ -35458,13 +36324,13 @@ function importWhatsAppSnapshot(database, snapshot) {
35458
36324
  reason
35459
36325
  };
35460
36326
  const evidenceJson = stableJson6(evidence);
35461
- run10(database, `INSERT INTO source_records(
36327
+ run12(database, `INSERT INTO source_records(
35462
36328
  run_id,ordinal,record_kind,record_sha256,person_id,provider_resource_id,resolution,projection_json,error_text
35463
36329
  ) VALUES (?,?,'message',?,?,?,?,?,?)`, runId, ordinal, sha2569(evidenceJson), personId, resourceId, resolution, evidenceJson, resolution === "matched" ? null : reason);
35464
- run10(database, `INSERT INTO interaction_events(
36330
+ run12(database, `INSERT INTO interaction_events(
35465
36331
  provider,account_key,external_id,person_id,occurred_at,direction,conversation_sha256,
35466
36332
  event_kind,resolution,run_id,metadata_json
35467
- ) VALUES (?,?,?,?,?,?,?,'message',?,?,?)`, PROVIDER13, account, `row:${message.rowid}`, personId, message.timestamp, direction, conversationHash, resolution, runId, stableJson6({ conversationKind, reason, messageIdSha256: sha2569(message.messageId) }));
36333
+ ) VALUES (?,?,?,?,?,?,?,'message',?,?,?)`, PROVIDER15, account, `row:${message.rowid}`, personId, message.timestamp, direction, conversationHash, resolution, runId, stableJson6({ conversationKind, reason, messageIdSha256: sha2569(message.messageId) }));
35468
36334
  messagesInserted += 1;
35469
36335
  }
35470
36336
  refreshMetrics2(database, account, runId);
@@ -35487,7 +36353,7 @@ function importWhatsAppSnapshot(database, snapshot) {
35487
36353
  if (stableJson6(result) !== stableJson6(expectedResult))
35488
36354
  throw new Error("WhatsApp applied counts drifted from the reviewed snapshot");
35489
36355
  const metadata4 = stableJson6({ generation: scanGeneration, coverage: "local-insert-rowid-scan" });
35490
- run10(database, `INSERT INTO provider_checkpoints(
36356
+ run12(database, `INSERT INTO provider_checkpoints(
35491
36357
  provider,account_key,source_realm_id,stream_key,source_locator_sha256,schema_sha256,generation_sha256,
35492
36358
  cursor,cursor_anchor_sha256,last_run_id,metadata_json
35493
36359
  ) VALUES (?,?,?,?,?,?,?,?,?,?,?)
@@ -35495,7 +36361,7 @@ function importWhatsAppSnapshot(database, snapshot) {
35495
36361
  source_realm_id=excluded.source_realm_id,source_locator_sha256=excluded.source_locator_sha256,schema_sha256=excluded.schema_sha256,
35496
36362
  generation_sha256=excluded.generation_sha256,cursor=excluded.cursor,
35497
36363
  cursor_anchor_sha256=excluded.cursor_anchor_sha256,last_run_id=excluded.last_run_id,
35498
- metadata_json=excluded.metadata_json,updated_at=CURRENT_TIMESTAMP`, PROVIDER13, account, sourceRealmId, STREAM2, sha2569(snapshot.authId), CONTACTS_CONTRACT_SHA2562, generationSha256(scanGeneration), end2.cursor, end2.anchor, runId, metadata4);
36364
+ metadata_json=excluded.metadata_json,updated_at=CURRENT_TIMESTAMP`, PROVIDER15, account, sourceRealmId, STREAM2, sha2569(snapshot.authId), CONTACTS_CONTRACT_SHA2562, generationSha256(scanGeneration), end2.cursor, end2.anchor, runId, metadata4);
35499
36365
  database.exec("COMMIT");
35500
36366
  return result;
35501
36367
  } catch (error) {
@@ -35510,7 +36376,7 @@ function syncWhatsAppRelationships(database, options = {}) {
35510
36376
  const snapshot = ghostgetWhatsAppSnapshot({ ...options, authId: auth, checkpoint: databaseCheckpoint(database, auth) });
35511
36377
  for (const page of [...snapshot.contactPages, ...snapshot.interactionPages]) {
35512
36378
  ledgerPeopleBladeGhostgetInvocation(database, {
35513
- provider: PROVIDER13,
36379
+ provider: PROVIDER15,
35514
36380
  accountKey: snapshot.accountSubject,
35515
36381
  capability: "contact_interactions",
35516
36382
  execution: page.execution,
@@ -35524,7 +36390,7 @@ function syncWhatsAppRelationships(database, options = {}) {
35524
36390
  }
35525
36391
 
35526
36392
  // src/cli/version.ts
35527
- var peoplebladeVersion = "0.4.0";
36393
+ var peoplebladeVersion = "0.4.1";
35528
36394
 
35529
36395
  // src/cli/intro.ts
35530
36396
  function terminalIntro(terminal) {
@@ -35609,6 +36475,10 @@ Enrichment (local):
35609
36475
  hunter reverse-enrich [--limit N] [--non-webmail]
35610
36476
  Reverse-enrich emails to LinkedIn profiles via Hunter
35611
36477
  [--key-file PATH] Read HUNTER_API_KEY from file instead of env
36478
+ hunter verify [--limit N] Verify email deliverability via Hunter
36479
+ [--key-file PATH] Read HUNTER_API_KEY from file instead of env
36480
+ hunter find-emails [--limit N] Find professional emails via Hunter
36481
+ [--key-file PATH] Read HUNTER_API_KEY from file instead of env
35612
36482
 
35613
36483
  Cloud (optional):
35614
36484
  cloud signin Register/sign in through your browser
@@ -36830,6 +37700,52 @@ ${verification}`) });
36830
37700
  print({ attempted: result.attempted, linkedin_found: result.linkedin_found, run_id: result.run_id, outcomes: result.outcomes }, asJson);
36831
37701
  return;
36832
37702
  }
37703
+ if (command === "hunter" && subcommand === "verify") {
37704
+ const options = [...rest];
37705
+ const limitValue = valueAfter(options, "--limit");
37706
+ const keyFile = valueAfter(options, "--key-file");
37707
+ if (options.length)
37708
+ fail3(`Unknown argument: ${options[0]}`);
37709
+ let apiKey = process.env.HUNTER_API_KEY;
37710
+ if (apiKey === undefined && keyFile !== undefined)
37711
+ apiKey = readHunterApiKeyFile(keyFile);
37712
+ if (apiKey === undefined || apiKey.trim().length < 8)
37713
+ fail3("Set a valid HUNTER_API_KEY or use --key-file PATH.");
37714
+ const limit = positive(limitValue, "--limit", 100, 100);
37715
+ const result = await hunterVerifyEmails(database, {
37716
+ apiKey: apiKey.trim(),
37717
+ limit,
37718
+ onProgress: (done, total) => {
37719
+ if (!asJson)
37720
+ console.log(`[${done}/${total}]`);
37721
+ }
37722
+ });
37723
+ print({ attempted: result.attempted, verified: result.verified, run_id: result.run_id, outcomes: result.outcomes }, asJson);
37724
+ return;
37725
+ }
37726
+ if (command === "hunter" && subcommand === "find-emails") {
37727
+ const options = [...rest];
37728
+ const limitValue = valueAfter(options, "--limit");
37729
+ const keyFile = valueAfter(options, "--key-file");
37730
+ if (options.length)
37731
+ fail3(`Unknown argument: ${options[0]}`);
37732
+ let apiKey = process.env.HUNTER_API_KEY;
37733
+ if (apiKey === undefined && keyFile !== undefined)
37734
+ apiKey = readHunterApiKeyFile(keyFile);
37735
+ if (apiKey === undefined || apiKey.trim().length < 8)
37736
+ fail3("Set a valid HUNTER_API_KEY or use --key-file PATH.");
37737
+ const limit = positive(limitValue, "--limit", 100, 100);
37738
+ const result = await hunterFindEmails(database, {
37739
+ apiKey: apiKey.trim(),
37740
+ limit,
37741
+ onProgress: (done, total) => {
37742
+ if (!asJson)
37743
+ console.log(`[${done}/${total}]`);
37744
+ }
37745
+ });
37746
+ print({ attempted: result.attempted, emails_found: result.emails_found, run_id: result.run_id, outcomes: result.outcomes }, asJson);
37747
+ return;
37748
+ }
36833
37749
  if (command === "x" && subcommand === "import") {
36834
37750
  const options = [...rest];
36835
37751
  const archive = options.shift();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hraness/peopleblade",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Local-first, source-aware contact intelligence for people and agents",
5
5
  "type": "module",
6
6
  "bin": {