@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 +17 -8
- package/dist/peopleblade.js +973 -57
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Installation includes the pinned runtime dependencies Zod 4.4.3 and Effect 3.22.
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
bun add --global @hraness/peopleblade@0.4.
|
|
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.
|
|
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
|
|
158
|
+
### Optional Hunter enrichment
|
|
159
159
|
|
|
160
160
|
Hunter customers can reverse-enrich contacts that have a business email but no
|
|
161
|
-
LinkedIn profile
|
|
162
|
-
|
|
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
|
-
|
|
170
|
-
compatible-name, safe-URL, and local-ownership checks pass.
|
|
171
|
-
|
|
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
|
|
package/dist/peopleblade.js
CHANGED
|
@@ -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
|
|
34493
|
-
var
|
|
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
|
|
35376
|
+
function allRows10(database, sql, ...bindings) {
|
|
34511
35377
|
return database.query(sql).all(...bindings);
|
|
34512
35378
|
}
|
|
34513
|
-
function
|
|
35379
|
+
function run11(database, sql, ...bindings) {
|
|
34514
35380
|
return database.run(sql, bindings);
|
|
34515
35381
|
}
|
|
34516
|
-
function
|
|
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 !==
|
|
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 =
|
|
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`,
|
|
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=?",
|
|
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=?",
|
|
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 =
|
|
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 (?,?,?,?,?,?,?,?,?)`,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34803
|
-
|
|
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:
|
|
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
|
|
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
|
|
35713
|
+
function run12(database, sql, ...values) {
|
|
34848
35714
|
return database.run(sql, values);
|
|
34849
35715
|
}
|
|
34850
|
-
function
|
|
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=?`,
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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,?,?,?)`,
|
|
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=?`,
|
|
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
|
-
|
|
35246
|
-
|
|
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`,
|
|
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=?",
|
|
35310
|
-
const byAccount = getRow10(database, "SELECT auth_id,auth_sha256 FROM provider_accounts WHERE provider=? AND account_key=?",
|
|
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
|
-
|
|
35316
|
-
VALUES (?,?,?,?,?,?)`,
|
|
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
|
-
|
|
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:
|
|
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=?`,
|
|
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=?`,
|
|
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=?`,
|
|
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 =
|
|
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',?,?,?)`,
|
|
35394
|
-
const runId =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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',?,?,?)`,
|
|
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
|
-
|
|
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`,
|
|
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:
|
|
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.
|
|
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();
|