@hraness/peopleblade 0.2.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -10
- package/THIRD_PARTY_NOTICES.md +10 -2
- package/dist/cli.ts +1 -1
- package/dist/peopleblade.js +685 -106
- package/package.json +1 -1
package/dist/peopleblade.js
CHANGED
|
@@ -4313,9 +4313,9 @@ var peoplebladeCapabilities = Object.freeze({
|
|
|
4313
4313
|
{ id: "apple-contacts", command: "contacts sync", status: "supported", transport: "local-database", requirement: "macOS Contacts access" },
|
|
4314
4314
|
{ id: "imessage", command: "imessage sync", status: "supported", transport: "local-metadata", requirement: "macOS Messages access; no message bodies" },
|
|
4315
4315
|
{ id: "google-contacts", command: "google sync", status: "supported", transport: "ghostget", requirement: "Reviewed Gmail/People contacts.list and contact scopes" },
|
|
4316
|
-
{ id: "beeper", command: "beeper sync", status: "supported", transport: "ghostget", requirement: "Ghostget 0.17.
|
|
4316
|
+
{ id: "beeper", command: "beeper sync", status: "supported", transport: "ghostget", requirement: "Ghostget 0.17.3, Beeper adapter 2.4.0, CLI 0.6.2; body-free relationships require macOS arm64" },
|
|
4317
4317
|
{ id: "whatsapp", command: "whatsapp sync", status: "supported", transport: "ghostget", requirement: "Reviewed linked-device contacts.list v2" },
|
|
4318
|
-
{ id: "linkedin", command: "linkedin import", status: "supported", transport: "official-archive", requirement: "Owner-selected official ZIP;
|
|
4318
|
+
{ id: "linkedin", command: "linkedin import", status: "supported", transport: "official-archive", requirement: "Owner-selected official ZIP; connection lists are not collected from a signed-in browser" },
|
|
4319
4319
|
{ id: "instagram", command: "instagram import", status: "supported", transport: "official-archive", requirement: "Owner-selected JSON ZIP and exact owner username" },
|
|
4320
4320
|
{ id: "x-archive", command: "x import", status: "bounded", transport: "official-archive", requirement: "Owner-selected ZIP; unresolved IDs remain inert" },
|
|
4321
4321
|
{ id: "x-live", command: "x mutuals sync", status: "blocked", transport: "unavailable", requirement: "A reviewed bounded Ghostget mutuals capability is not yet available" },
|
|
@@ -4323,9 +4323,10 @@ var peoplebladeCapabilities = Object.freeze({
|
|
|
4323
4323
|
],
|
|
4324
4324
|
enrichment: [
|
|
4325
4325
|
{ id: "local-anchors", priority: 1, status: "supported", commands: ["identity audit", "identity suggest", "identity decide"], paid: false, boundary: "Exact source evidence first; joins require reversible evidence-bound decisions" },
|
|
4326
|
-
{ id: "
|
|
4327
|
-
{ id: "
|
|
4328
|
-
{ id: "cloud-
|
|
4326
|
+
{ id: "linkedin-contact-info", priority: 2, status: "supported", commands: ["linkedin contact-info --person-id ID"], paid: false, boundary: "Reviewed Ghostget linkedin-web contacts.read for one exact first-degree connection already imported from the official export; signed-in browser profile, one bounded read, no connect, message, or feed access" },
|
|
4327
|
+
{ id: "manual-public-research", priority: 3, status: "supported", commands: ["research prepare", "research apply"], paid: false, boundary: "Agent or human supplies cited public evidence; PeopleBlade performs no provider calls" },
|
|
4328
|
+
{ id: "cloud-professional-research", priority: 4, status: "optional", commands: ["cloud enrich --person-id ID", "cloud enrich --confirm TOKEN"], paid: true, boundary: "Exa exact profiles or bounded search, then cited model extraction; device-bound preview approval required" },
|
|
4329
|
+
{ id: "cloud-email-fallback", priority: 5, status: "optional", commands: ["cloud enrich --person-id ID"], paid: true, boundary: "Hunter is a separately configured narrow fallback, not a substitute for identity evidence" }
|
|
4329
4330
|
],
|
|
4330
4331
|
interfaces: {
|
|
4331
4332
|
workspace: { command: "ui [--port N]", availability: "included-in-this-build", binding: "127.0.0.1", defaultPort: "random", authentication: "per-process capability file", remoteAssets: false, noteUpload: false },
|
|
@@ -6419,9 +6420,208 @@ function parseGranolaImportJson(raw) {
|
|
|
6419
6420
|
}
|
|
6420
6421
|
}
|
|
6421
6422
|
|
|
6423
|
+
// styles/vendor/hraness-paper/paper-theme.css
|
|
6424
|
+
var paper_theme_default = `/* @hraness/design-kit paper-theme.css, portable contract 1.
|
|
6425
|
+
* Generated by scripts/generate-paper-theme.ts from src/palettes.ts.
|
|
6426
|
+
* Opt in with data-hraness-theme="paper". Import after existing theme rules.
|
|
6427
|
+
* No imports, asset requests, resets, component recipes, or runtime dependencies.
|
|
6428
|
+
* Appearance uses inherited color-scheme; set data-theme on the boundary for
|
|
6429
|
+
* an explicit island. A root without a mode follows the operating system.
|
|
6430
|
+
* A non-paper data-palette on the boundary retains its selected colors.
|
|
6431
|
+
* Requires CSS light-dark() (Chrome 123+, Firefox 120+, Safari 17.5+).
|
|
6432
|
+
*/
|
|
6433
|
+
|
|
6434
|
+
[data-hraness-theme="paper"] {
|
|
6435
|
+
--font-text: "Nebula Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
6436
|
+
--font-heading: var(--font-text);
|
|
6437
|
+
--font-sans: var(--font-text);
|
|
6438
|
+
--font-mono: ui-monospace, "SFMono-Regular", Menlo, Monaco, Consolas, monospace;
|
|
6439
|
+
--ui-font-sans: var(--font-text);
|
|
6440
|
+
--ui-font-heading: var(--font-heading);
|
|
6441
|
+
--ui-font-mono: var(--font-mono);
|
|
6442
|
+
--text-body: 1rem;
|
|
6443
|
+
--text-caption: .75rem;
|
|
6444
|
+
--text-label: .875rem;
|
|
6445
|
+
--font-weight-regular: 400;
|
|
6446
|
+
--font-weight-medium: 550;
|
|
6447
|
+
--font-weight-bold: 700;
|
|
6448
|
+
--radius-sm: .375rem;
|
|
6449
|
+
--radius-md: .625rem;
|
|
6450
|
+
--radius-lg: .875rem;
|
|
6451
|
+
--ui-radius: .875rem;
|
|
6452
|
+
--control-radius: .625rem;
|
|
6453
|
+
--hraness-paper-heading-size: clamp(1.75rem, 3vw, 2.375rem);
|
|
6454
|
+
--hraness-paper-section-heading-size: clamp(1.25rem, 2.2vw, 1.75rem);
|
|
6455
|
+
--hraness-paper-summary-size: 1rem;
|
|
6456
|
+
--hraness-paper-heading-leading: 1.15;
|
|
6457
|
+
--hraness-paper-heading-tracking: -.035em;
|
|
6458
|
+
--hraness-marketing-measure: 80rem;
|
|
6459
|
+
--hraness-marketing-copy-measure: 40rem;
|
|
6460
|
+
--hraness-marketing-prose-measure: 68ch;
|
|
6461
|
+
--hraness-marketing-gutter: clamp(1.25rem, 4vw, 3rem);
|
|
6462
|
+
--hraness-marketing-radius: .625rem;
|
|
6463
|
+
--hraness-marketing-frame-radius: .875rem;
|
|
6464
|
+
--hraness-marketing-heading-weight: 550;
|
|
6465
|
+
--hraness-marketing-heading-tracking: -.025em;
|
|
6466
|
+
--hraness-marketing-section-space: clamp(3rem, 7vw, 6rem);
|
|
6467
|
+
--hraness-marketing-action-height: 2.75rem;
|
|
6468
|
+
font-family: var(--font-text);
|
|
6469
|
+
}
|
|
6470
|
+
|
|
6471
|
+
/* Existing marketing roles declare defaults directly, so inherit this
|
|
6472
|
+
* opt-in rhythm at each role boundary without replacing component recipes. */
|
|
6473
|
+
[data-hraness-theme="paper"] :where(.hraness-marketing-page, .hraness-marketing-header, .hraness-marketing-hero, .hraness-marketing-pillars, .hraness-marketing-install, .hraness-marketing-section, .hraness-marketing-primitives, .hraness-marketing-stats, .hraness-marketing-interfaces, .hraness-marketing-trust, .hraness-marketing-quotes, .hraness-marketing-pricing, .hraness-marketing-questions, .hraness-marketing-maker, .hraness-marketing-cta, .hraness-marketing-proof-frame) {
|
|
6474
|
+
--hraness-marketing-measure: inherit;
|
|
6475
|
+
--hraness-marketing-copy-measure: inherit;
|
|
6476
|
+
--hraness-marketing-prose-measure: inherit;
|
|
6477
|
+
--hraness-marketing-gutter: inherit;
|
|
6478
|
+
--hraness-marketing-radius: inherit;
|
|
6479
|
+
--hraness-marketing-frame-radius: inherit;
|
|
6480
|
+
--hraness-marketing-heading-weight: inherit;
|
|
6481
|
+
--hraness-marketing-heading-tracking: inherit;
|
|
6482
|
+
--hraness-marketing-section-space: inherit;
|
|
6483
|
+
--hraness-marketing-action-height: inherit;
|
|
6484
|
+
}
|
|
6485
|
+
|
|
6486
|
+
:root[data-hraness-theme="paper"]:where(:not([data-theme]):not(.dark), [data-theme="system"]) { color-scheme: light dark; }
|
|
6487
|
+
[data-hraness-theme="paper"]:where([data-theme="dark"], .dark:not([data-theme="light"])) { color-scheme: dark; }
|
|
6488
|
+
[data-hraness-theme="paper"][data-theme="light"] { color-scheme: light; }
|
|
6489
|
+
|
|
6490
|
+
:root[data-hraness-theme="paper"]:where(:not([data-palette]), [data-palette="paper"]),
|
|
6491
|
+
[data-hraness-theme="paper"]:where(:not([data-palette]), [data-palette="paper"]) {
|
|
6492
|
+
--background: light-dark(#f8f7f4, #12100f);
|
|
6493
|
+
--foreground: light-dark(#1c1917, #f5f2ed);
|
|
6494
|
+
--muted: light-dark(#6c665f, #aaa29a);
|
|
6495
|
+
--faint: light-dark(#6c665f, #aaa29a);
|
|
6496
|
+
--grid: light-dark(#dfdcd6, #302b27);
|
|
6497
|
+
--line: light-dark(#b9b3ab, #514a44);
|
|
6498
|
+
--control-border: light-dark(#8a8177, #827a72);
|
|
6499
|
+
--surface: light-dark(#fffefa, #1d1a18);
|
|
6500
|
+
--surface-raised: light-dark(#fffefa, #211d1a);
|
|
6501
|
+
--surface-hover: light-dark(#ebe8e3, #292522);
|
|
6502
|
+
--card: light-dark(#fffefa, #1d1a18);
|
|
6503
|
+
--card-foreground: light-dark(#1c1917, #f5f2ed);
|
|
6504
|
+
--popover: light-dark(#fffefa, #211d1a);
|
|
6505
|
+
--popover-foreground: light-dark(#1c1917, #f5f2ed);
|
|
6506
|
+
--primary: light-dark(#1e5ae1, #8fb0ff);
|
|
6507
|
+
--primary-foreground: light-dark(#f8f7f4, #12100f);
|
|
6508
|
+
--primary-soft: light-dark(#dee5f3, #21232c);
|
|
6509
|
+
--secondary: light-dark(#ebe8e3, #292522);
|
|
6510
|
+
--secondary-foreground: light-dark(#1c1917, #f5f2ed);
|
|
6511
|
+
--accent: light-dark(#dee5f3, #21232c);
|
|
6512
|
+
--accent-foreground: light-dark(#1c1917, #f5f2ed);
|
|
6513
|
+
--focus: light-dark(#1e5ae1, #8fb0ff);
|
|
6514
|
+
--scrim: light-dark(#00000070, #000000b8);
|
|
6515
|
+
--disabled: light-dark(#fffefa, #211d1a);
|
|
6516
|
+
--disabled-foreground: light-dark(#6c665f, #aaa29a);
|
|
6517
|
+
--inverse: light-dark(#1c1917, #f5f2ed);
|
|
6518
|
+
--inverse-foreground: light-dark(#f8f7f4, #12100f);
|
|
6519
|
+
--danger: light-dark(#b23c34, #ef8c82);
|
|
6520
|
+
--danger-foreground: light-dark(#f8f7f4, #12100f);
|
|
6521
|
+
--danger-soft: light-dark(#f0e1dd, #2d1f1d);
|
|
6522
|
+
--warning: light-dark(#975600, #efae55);
|
|
6523
|
+
--warning-foreground: light-dark(#f8f7f4, #12100f);
|
|
6524
|
+
--warning-soft: light-dark(#eee5d7, #2d2317);
|
|
6525
|
+
--success: light-dark(#227445, #5fc98b);
|
|
6526
|
+
--success-foreground: light-dark(#f8f7f4, #12100f);
|
|
6527
|
+
--success-soft: light-dark(#dfe9e0, #1b261e);
|
|
6528
|
+
--info: light-dark(#1e5ae1, #8fb0ff);
|
|
6529
|
+
--info-foreground: light-dark(#f8f7f4, #12100f);
|
|
6530
|
+
--info-soft: light-dark(#dee5f3, #21232c);
|
|
6531
|
+
--chart1: light-dark(#a24a64, #db91aa);
|
|
6532
|
+
--chart2: light-dark(#227445, #5fc98b);
|
|
6533
|
+
--chart3: light-dark(#1e5ae1, #8fb0ff);
|
|
6534
|
+
--chart4: light-dark(#975600, #efae55);
|
|
6535
|
+
--chart5: light-dark(#7858a3, #b7a0dc);
|
|
6536
|
+
--ui-background: var(--background);
|
|
6537
|
+
--ui-foreground: var(--foreground);
|
|
6538
|
+
--ui-card: var(--card);
|
|
6539
|
+
--ui-card-foreground: var(--card-foreground);
|
|
6540
|
+
--ui-popover: var(--popover);
|
|
6541
|
+
--ui-popover-foreground: var(--popover-foreground);
|
|
6542
|
+
--ui-primary: var(--primary);
|
|
6543
|
+
--ui-primary-foreground: var(--primary-foreground);
|
|
6544
|
+
--ui-secondary: var(--secondary);
|
|
6545
|
+
--ui-secondary-foreground: var(--secondary-foreground);
|
|
6546
|
+
--ui-muted: var(--secondary);
|
|
6547
|
+
--ui-muted-foreground: var(--muted);
|
|
6548
|
+
--ui-accent: var(--accent);
|
|
6549
|
+
--ui-accent-foreground: var(--accent-foreground);
|
|
6550
|
+
--ui-destructive: var(--danger);
|
|
6551
|
+
--ui-destructive-foreground: var(--danger-foreground);
|
|
6552
|
+
--ui-destructive-soft: var(--danger-soft);
|
|
6553
|
+
--ui-border: var(--line);
|
|
6554
|
+
--ui-input: var(--control-border);
|
|
6555
|
+
--ui-ring: var(--focus);
|
|
6556
|
+
--ui-success: var(--success);
|
|
6557
|
+
--ui-success-foreground: var(--success-foreground);
|
|
6558
|
+
--ui-success-soft: var(--success-soft);
|
|
6559
|
+
--ui-warning: var(--warning);
|
|
6560
|
+
--ui-warning-foreground: var(--warning-foreground);
|
|
6561
|
+
--ui-warning-soft: var(--warning-soft);
|
|
6562
|
+
--ui-info: var(--info);
|
|
6563
|
+
--ui-info-foreground: var(--info-foreground);
|
|
6564
|
+
--ui-info-soft: var(--info-soft);
|
|
6565
|
+
--ui-chart-1: var(--chart1);
|
|
6566
|
+
--ui-chart-2: var(--chart2);
|
|
6567
|
+
--ui-chart-3: var(--chart3);
|
|
6568
|
+
--ui-chart-4: var(--chart4);
|
|
6569
|
+
--ui-chart-5: var(--chart5);
|
|
6570
|
+
--hraness-site-accent: var(--primary);
|
|
6571
|
+
--hraness-site-accent-ink: var(--primary-foreground);
|
|
6572
|
+
--plain-background: var(--background);
|
|
6573
|
+
--plain-foreground: var(--foreground);
|
|
6574
|
+
--plain-muted: var(--muted);
|
|
6575
|
+
--plain-line: var(--grid);
|
|
6576
|
+
--plain-surface: var(--secondary);
|
|
6577
|
+
--plain-link: var(--primary);
|
|
6578
|
+
--plain-syntax-keyword: var(--warning);
|
|
6579
|
+
--plain-syntax-string: var(--success);
|
|
6580
|
+
--plain-syntax-number: var(--danger);
|
|
6581
|
+
--plain-syntax-name: var(--info);
|
|
6582
|
+
--elevation-low: 0 1px 2px light-dark(rgb(33 25 18 / 8%), rgb(0 0 0 / 24%));
|
|
6583
|
+
--elevation-raised: 0 8px 24px -12px light-dark(rgb(33 25 18 / 22%), rgb(0 0 0 / 48%));
|
|
6584
|
+
--elevation-overlay: 0 18px 54px light-dark(rgb(33 25 18 / 20%), rgb(0 0 0 / 58%));
|
|
6585
|
+
}
|
|
6586
|
+
|
|
6587
|
+
@media (forced-colors: active) {
|
|
6588
|
+
:root[data-hraness-theme="paper"]:where(:not([data-palette]), [data-palette="paper"]),
|
|
6589
|
+
[data-hraness-theme="paper"]:where(:not([data-palette]), [data-palette="paper"]) {
|
|
6590
|
+
--background: Canvas;
|
|
6591
|
+
--foreground: CanvasText;
|
|
6592
|
+
--muted: CanvasText;
|
|
6593
|
+
--faint: CanvasText;
|
|
6594
|
+
--surface: Canvas;
|
|
6595
|
+
--surface-raised: Canvas;
|
|
6596
|
+
--surface-hover: Canvas;
|
|
6597
|
+
--card: Canvas;
|
|
6598
|
+
--card-foreground: CanvasText;
|
|
6599
|
+
--popover: Canvas;
|
|
6600
|
+
--popover-foreground: CanvasText;
|
|
6601
|
+
--secondary: Canvas;
|
|
6602
|
+
--secondary-foreground: CanvasText;
|
|
6603
|
+
--grid: CanvasText;
|
|
6604
|
+
--line: CanvasText;
|
|
6605
|
+
--control-border: ButtonText;
|
|
6606
|
+
--primary: Highlight;
|
|
6607
|
+
--primary-foreground: HighlightText;
|
|
6608
|
+
--accent: Highlight;
|
|
6609
|
+
--accent-foreground: HighlightText;
|
|
6610
|
+
--focus: Highlight;
|
|
6611
|
+
--inverse: CanvasText;
|
|
6612
|
+
--inverse-foreground: Canvas;
|
|
6613
|
+
--disabled: Canvas;
|
|
6614
|
+
--disabled-foreground: GrayText;
|
|
6615
|
+
--elevation-low: none;
|
|
6616
|
+
--elevation-raised: none;
|
|
6617
|
+
--elevation-overlay: none;
|
|
6618
|
+
}
|
|
6619
|
+
}
|
|
6620
|
+
`;
|
|
6621
|
+
|
|
6422
6622
|
// src/local/workspace-assets.ts
|
|
6423
6623
|
var workspaceHtml = `<!doctype html>
|
|
6424
|
-
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6624
|
+
<html lang="en" data-hraness-theme="paper"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6425
6625
|
<title>PeopleBlade \xB7 Local workspace</title><link rel="stylesheet" href="/workspace.css"><script src="/workspace.js" defer></script></head>
|
|
6426
6626
|
<body><a class="skip" href="#workspace">Skip to contacts</a><header><a class="brand" href="/">PeopleBlade<span>LOCAL WORKSPACE</span></a><p>Your people. Your machine.</p><span class="privacy">No cloud sync \xB7 no enrichment calls</span></header>
|
|
6427
6627
|
<main id="workspace"><section id="unlock" class="unlock"><span class="eyebrow">PRIVATE BY DEFAULT</span><h1>Open your local contact book.</h1><p>Select the access file printed by <code>peopleblade ui</code>. It connects this tab to that running process only. The file is not uploaded to a remote service.</p><label class="file-label">Workspace access file<input id="access-file" type="file" accept="application/json,.json"></label><p class="muted">Keep the terminal running. Closing it locks the workspace. Notes never leave this machine.</p></section>
|
|
@@ -6430,9 +6630,8 @@ var workspaceHtml = `<!doctype html>
|
|
|
6430
6630
|
<div class="workspace-grid"><section class="records" aria-label="Contacts"><div class="table-toolbar"><span id="range" aria-live="polite"></span><div><label>Sort <select id="sort"><option value="interactions">Interactions</option><option value="name">Name</option><option value="organization">Company</option><option value="last-contact">Last interaction</option></select></label><button id="direction" type="button" aria-label="Toggle sort direction">Descending \u2193</button></div></div><div class="table-scroll" tabindex="0" aria-label="Scrollable contacts"><table><caption class="sr-only">Local contacts; open a person to inspect details and notes.</caption><thead><tr><th scope="col">Person</th><th scope="col">Company</th><th scope="col">Interactions</th></tr></thead><tbody id="rows"></tbody></table></div><div id="empty" class="empty" hidden>No matches. Try another search or import a source with the CLI.</div><nav class="pagination" aria-label="Contact pages"><button id="previous" type="button">\u2190 Previous</button><span id="page"></span><button id="next" type="button">Next \u2192</button></nav><p class="live-note">Live view: imports and identity reviews can move rows between pages. Refresh after changes.</p></section>
|
|
6431
6631
|
<aside id="detail" class="detail" aria-label="Contact details"><div class="empty"><h2>A little context goes a long way.</h2><p>Open a person to see their contact methods, sources and private notes.</p></div></aside></div></section>
|
|
6432
6632
|
<p id="status" role="status" aria-live="polite"></p></main><footer>PeopleBlade \xB7 Local authority, reviewable identity, private notes.</footer></body></html>`;
|
|
6433
|
-
var workspaceCss = `
|
|
6434
|
-
:root{
|
|
6435
|
-
@media(prefers-color-scheme:dark){:root{--bg:#151a17;--panel:#1c231e;--text:#e1e7dc;--muted:#a3b19e;--line:#354136;--accent:#a9c4a3;--soft:#28342b;--danger:#ffb4a9}.primary{color:#182019}}
|
|
6633
|
+
var workspaceCss = paper_theme_default + `
|
|
6634
|
+
:root{--bg:var(--background);--panel:var(--surface);--text:var(--foreground);--soft:var(--secondary)}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.55 var(--font-text)}button,input,select,textarea{font:inherit;color:inherit}button,input,select,textarea{border:1px solid var(--line);border-radius:7px;background:var(--panel);padding:9px 12px}button{cursor:pointer;font-weight:550}button:hover{border-color:var(--primary);background:var(--soft)}button:disabled{opacity:.45;cursor:default}a{color:var(--primary)}:focus-visible{outline:3px solid var(--primary);outline-offset:3px}header{display:flex;align-items:center;gap:30px;padding:22px 32px;border-bottom:1px solid var(--line);background:var(--panel)}header p{color:var(--muted);margin:0}.brand{color:var(--text);font-size:22px;line-height:1.1;text-decoration:none;font-weight:650;letter-spacing:-.7px}.brand span{display:block;font-size:9px;letter-spacing:1.8px;color:var(--muted);margin-top:8px}.privacy{margin-left:auto;border:1px solid var(--line);border-radius:99px;padding:6px 12px;font-size:11px;color:var(--muted)}main{max-width:1600px;margin:auto;padding:36px 32px}h1{font-size:34px;line-height:1.2;letter-spacing:-1px;font-weight:550;margin:8px 0 24px}h2{font-size:21px;line-height:1.3;letter-spacing:-.5px;margin:0 0 12px}.eyebrow{font-size:10px;letter-spacing:2px;color:var(--muted);font-weight:650}.unlock{max-width:600px;margin:7vh auto;padding:36px;background:var(--panel);border:1px solid var(--line);border-radius:12px}.unlock p{color:var(--muted)}.file-label{display:grid;gap:8px;margin:25px 0}.page-heading{display:flex;align-items:center;justify-content:space-between}.page-heading button{margin-bottom:20px}.filters{display:flex;gap:10px;align-items:end;margin-bottom:20px;flex-wrap:wrap}.filters label{display:grid;gap:5px;font-size:11px;color:var(--muted)}.filters input,.filters select{font-size:13px;color:var(--text);min-height:42px}.filters .search{flex:1;min-width:220px}.workspace-grid{display:grid;grid-template-columns:minmax(420px,1.4fr) minmax(320px,1fr);gap:20px;align-items:start}.records,.detail{border:1px solid var(--line);background:var(--panel);border-radius:10px;overflow:hidden}.table-toolbar{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:13px 16px;border-bottom:1px solid var(--line);color:var(--muted);font-size:12px;flex-wrap:wrap}.table-toolbar>div{display:flex;align-items:center;gap:6px}.table-toolbar select,.table-toolbar button{padding:5px 8px;font-size:12px}.table-scroll{overflow:auto;max-height:65vh}table{width:100%;border-collapse:collapse;text-align:left}th{position:sticky;top:0;background:var(--soft);font-size:10px;text-transform:uppercase;letter-spacing:1px;color:var(--muted);z-index:1}th,td{padding:11px 16px;border-bottom:1px solid var(--line);vertical-align:top}td{font-size:12px}td small{display:block;color:var(--muted);margin-top:2px}td button{padding:0;border:0;background:none;text-align:left;font-size:13px;color:var(--text)}tr.selected{background:var(--soft)}tr:hover{background:var(--soft)}.pagination{display:flex;justify-content:space-between;align-items:center;padding:14px 16px;font-size:12px}.pagination button{font-size:12px;padding:6px 10px}.live-note{font-size:10px;color:var(--muted);padding:0 16px 12px;margin:0}.detail{padding:24px;position:sticky;top:20px;max-height:83vh;overflow:auto}.detail h2{font-size:25px}.detail dl{display:grid;grid-template-columns:90px 1fr;gap:8px;margin:22px 0}.detail dt{font-size:11px;color:var(--muted)}.detail dd{margin:0;font-size:12px;overflow-wrap:anywhere}.detail dd span{display:block}.pill{display:inline-block;border:1px solid var(--line);border-radius:99px;padding:2px 8px;margin:0 4px 5px 0;color:var(--muted);font-size:10px}.notes-heading{border-top:1px solid var(--line);padding-top:22px;margin-top:24px;display:flex;justify-content:space-between;align-items:center}.notes-heading h3{margin:0;font-weight:550}.note-card{border-bottom:1px solid var(--line);padding:16px 0}.note-card>button{font-size:12px;padding:5px 9px;margin:8px 6px 0 0}.note-meta{font-size:10px;color:var(--muted);margin:4px 0 10px}.note-title{font-size:14px;font-weight:600;margin:0}.markdown{font-size:13px;overflow-wrap:anywhere}.markdown h1,.markdown h2,.markdown h3{font-size:17px;margin:12px 0}.markdown pre{overflow:auto;background:var(--soft);padding:12px;border-radius:6px}.markdown img{display:none}.markdown blockquote{margin-left:0;padding-left:12px;border-left:3px solid var(--line);color:var(--muted)}.markdown p{margin:8px 0}.markdown td,.markdown th{padding:5px;position:static}.editor{display:grid;gap:10px}.editor label{display:grid;gap:4px;font-size:11px;color:var(--muted)}.editor textarea{min-height:230px;resize:vertical;font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;tab-size:2}.editor-actions{display:flex;gap:6px;flex-wrap:wrap}.editor-actions button{font-size:12px}.primary{background:var(--primary);color:var(--primary-foreground)}.primary:hover{background:var(--primary);color:var(--primary-foreground)}.empty{padding:44px 22px;color:var(--muted);text-align:center}.empty h2{font-size:21px}.muted{color:var(--muted);font-size:12px}.danger{color:var(--danger)}#status{background:var(--panel);border:1px solid var(--line);padding:12px 16px;border-radius:8px}#status:empty{display:none}footer{color:var(--muted);font-size:11px;text-align:center;padding:20px}.skip{position:absolute;left:12px;top:-100px;padding:8px;background:var(--panel);z-index:5}.skip:focus{top:8px}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}[hidden]{display:none!important}
|
|
6436
6635
|
@media(max-width:1000px){.workspace-grid{grid-template-columns:1fr}.detail{position:static;max-height:none}.privacy{display:none}}@media(max-width:600px){header{padding:18px;gap:18px}header p{font-size:11px}main{padding:26px 14px}.unlock{padding:22px;margin:25px 0}h1{font-size:28px}.filters{gap:8px}.filters label{flex:1;min-width:125px}.filters .search{flex-basis:100%}th,td{padding:10px}.detail{padding:18px}.table-toolbar{padding:10px}.page-heading{align-items:start}.page-heading button{font-size:11px}.table-scroll{max-height:50vh}}
|
|
6437
6636
|
`;
|
|
6438
6637
|
function workspaceClient() {
|
|
@@ -13697,8 +13896,8 @@ function invokeCapabilitySync(request, options = {}) {
|
|
|
13697
13896
|
return parsed.live;
|
|
13698
13897
|
}
|
|
13699
13898
|
|
|
13700
|
-
// node_modules/@hraness/ghostget/dist/index-
|
|
13701
|
-
var GHOSTGET_VERSION = "0.17.
|
|
13899
|
+
// node_modules/@hraness/ghostget/dist/index-rxvsqb3f.js
|
|
13900
|
+
var GHOSTGET_VERSION = "0.17.3";
|
|
13702
13901
|
|
|
13703
13902
|
// node_modules/@hraness/ghostget/dist/beeper-client.js
|
|
13704
13903
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
@@ -18683,8 +18882,8 @@ function projectBeeperContactInteractions(summary2, coordinates) {
|
|
|
18683
18882
|
// src/local/providers/beeper-compatibility.ts
|
|
18684
18883
|
var BEEPER_GHOSTGET_COMPATIBILITY = Object.freeze({
|
|
18685
18884
|
ghostget: Object.freeze({
|
|
18686
|
-
version: "0.17.
|
|
18687
|
-
commit: "
|
|
18885
|
+
version: "0.17.3",
|
|
18886
|
+
commit: "67b53cc1917bb09a48b22042cc988c3afbc0aaf8"
|
|
18688
18887
|
}),
|
|
18689
18888
|
adapter: Object.freeze({
|
|
18690
18889
|
id: "beeper-local",
|
|
@@ -18721,6 +18920,22 @@ var BEEPER_GHOSTGET_COMPATIBILITY = Object.freeze({
|
|
|
18721
18920
|
commit: "b9c1714410139c2139b597338cd002d785653e85"
|
|
18722
18921
|
})
|
|
18723
18922
|
});
|
|
18923
|
+
var PREVIOUS_GHOSTGET_0171_BEEPER_SOURCE_BINDING_METADATA = Object.freeze({
|
|
18924
|
+
transport: "local-cli",
|
|
18925
|
+
wrenchVersion: "0.17.1",
|
|
18926
|
+
wrenchCommit: "b74ae2ac594bcf07c3def71dc4bb2e5a98c826dd",
|
|
18927
|
+
adapterId: "beeper-local",
|
|
18928
|
+
adapterVersion: "2.4.0",
|
|
18929
|
+
adapterSha256: "2714af7e821b799c1d0b57106e8e193cb9787e5a6da37e1269f4aae0710a4ddc",
|
|
18930
|
+
cliId: "beeper-cli",
|
|
18931
|
+
cliVersion: "0.6.2",
|
|
18932
|
+
cliCommit: "a416af06023449a87312dc11e54643fd9dc94b8c",
|
|
18933
|
+
contracts: Object.freeze({
|
|
18934
|
+
contactsList: "8048565547c2a09078528d0a35f7a0c3a2e8850b17b075dc572e8b59115fcb57",
|
|
18935
|
+
contactsSearch: "e57cdcc9012ed5fb8f956e16a2368e95adf0a6b8bb404f903e64df85061b1ca1",
|
|
18936
|
+
messagingSearch: "2fc54d139a54d19c31a82237e4b417994e12679db3da5fd71ec9265e3622b0bf"
|
|
18937
|
+
})
|
|
18938
|
+
});
|
|
18724
18939
|
var PREVIOUS_WRENCH_0168_BEEPER_SOURCE_BINDING_METADATA = Object.freeze({
|
|
18725
18940
|
transport: "local-cli",
|
|
18726
18941
|
wrenchVersion: "0.16.8",
|
|
@@ -19292,8 +19507,11 @@ function classifyReviewedBeeperPredecessor(value, currentMetadata) {
|
|
|
19292
19507
|
if (encoded === canonicalJson(PREVIOUS_WRENCH_0168_BEEPER_SOURCE_BINDING_METADATA)) {
|
|
19293
19508
|
return "wrench-0.16.8-beeper-2.4";
|
|
19294
19509
|
}
|
|
19295
|
-
if (encoded ===
|
|
19510
|
+
if (encoded === canonicalJson(PREVIOUS_GHOSTGET_0171_BEEPER_SOURCE_BINDING_METADATA)) {
|
|
19296
19511
|
return "ghostget-0.17.1-beeper-2.4";
|
|
19512
|
+
}
|
|
19513
|
+
if (encoded === currentMetadata)
|
|
19514
|
+
return "ghostget-0.17.3-beeper-2.4";
|
|
19297
19515
|
return null;
|
|
19298
19516
|
}
|
|
19299
19517
|
function assertStoredRealmInventory(database, sourceBindingId, accounts, storedSha256) {
|
|
@@ -19372,7 +19590,7 @@ function rebindBeeperSource(database, options = {}) {
|
|
|
19372
19590
|
if (predecessor === null) {
|
|
19373
19591
|
throw new Error("Beeper rebind source coordinates are not a reviewed predecessor.");
|
|
19374
19592
|
}
|
|
19375
|
-
const metadataOnly = sameIdentity && (predecessor === "peopleblade-0.1.0-wrench-0.15-beeper-2.0" || predecessor === "wrench-0.16.7-beeper-2.4" || predecessor === "wrench-0.16.8-beeper-2.4");
|
|
19593
|
+
const metadataOnly = sameIdentity && (predecessor === "peopleblade-0.1.0-wrench-0.15-beeper-2.0" || predecessor === "wrench-0.16.7-beeper-2.4" || predecessor === "wrench-0.16.8-beeper-2.4" || predecessor === "ghostget-0.17.1-beeper-2.4");
|
|
19376
19594
|
if (sameIdentity && !metadataOnly) {
|
|
19377
19595
|
throw new Error("Beeper metadata-only rebind requires an exact reviewed same-identity predecessor.");
|
|
19378
19596
|
}
|
|
@@ -24589,6 +24807,9 @@ function linkedInAccountKey(value) {
|
|
|
24589
24807
|
}
|
|
24590
24808
|
return key;
|
|
24591
24809
|
}
|
|
24810
|
+
function canonicalLinkedInProfileUrl(value, label) {
|
|
24811
|
+
return profileUrl(value, label);
|
|
24812
|
+
}
|
|
24592
24813
|
function ownerProfile(messages, supplied) {
|
|
24593
24814
|
const explicit = supplied === undefined ? null : profileUrl(supplied, "LinkedIn archive owner profile URL");
|
|
24594
24815
|
if (supplied !== undefined && explicit === null)
|
|
@@ -24993,6 +25214,342 @@ function linkedInStatsAll(database) {
|
|
|
24993
25214
|
WHERE account.provider='linkedin' ORDER BY account.account_key`, MODE7).map((row) => ({ ...row, remote_set_complete: false }));
|
|
24994
25215
|
}
|
|
24995
25216
|
|
|
25217
|
+
// src/local/providers/linkedin-contact-info.ts
|
|
25218
|
+
var LINKEDIN_CONTACT_INFO_ADAPTER_ID = "linkedin-web";
|
|
25219
|
+
var LINKEDIN_CONTACT_INFO_OPERATION_ID = "contacts.read";
|
|
25220
|
+
var LINKEDIN_CONTACT_INFO_ADAPTER_VERSION = "1.23.0";
|
|
25221
|
+
var LINKEDIN_CONTACT_INFO_CONTRACT_SHA256 = "8f80c234ca2b7706a49a5000012a11da78a36d38f3023c21854a240e16a13f43";
|
|
25222
|
+
var DEFAULT_LINKEDIN_AUTH = "linkedin-main";
|
|
25223
|
+
var PROVIDER11 = "linkedin";
|
|
25224
|
+
var MODE8 = "ghostget-contact-info";
|
|
25225
|
+
var RECORD_KIND = "contact-info";
|
|
25226
|
+
var OBSERVATION_BASIS2 = "linkedin-contact-info";
|
|
25227
|
+
var OBSERVATION_PRIORITY2 = 720;
|
|
25228
|
+
var MAX_PHONES = 8;
|
|
25229
|
+
var MAX_WEBSITES = 8;
|
|
25230
|
+
var EMAIL5 = /^(?=[\x21-\x7E]+$)[^@\s]+@[^@\s]+\.[^@\s]+$/u;
|
|
25231
|
+
var PHONE = /^\+?[0-9][0-9 .\-()]{6,30}[0-9]$/u;
|
|
25232
|
+
var VIEWER_SUBJECT = /^urn:li:fsd_profile:[0-9]{1,32}$/u;
|
|
25233
|
+
var PROFILE_URN = /^urn:li:fsd_profile:[A-Za-z0-9_-]{1,256}$/u;
|
|
25234
|
+
var VANITY = /^[A-Za-z0-9%._-]{1,1024}$/u;
|
|
25235
|
+
var DATE = /^[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])$/u;
|
|
25236
|
+
var BIRTHDAY = /^(?:[0-9]{4}-)?(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])$/u;
|
|
25237
|
+
var AUTH_ID3 = /^[a-z0-9][a-z0-9._:-]{0,255}$/u;
|
|
25238
|
+
function getRow9(database, sql, ...bindings) {
|
|
25239
|
+
return database.query(sql).get(...bindings);
|
|
25240
|
+
}
|
|
25241
|
+
function allRows7(database, sql, ...bindings) {
|
|
25242
|
+
return database.query(sql).all(...bindings);
|
|
25243
|
+
}
|
|
25244
|
+
function run8(database, sql, ...bindings) {
|
|
25245
|
+
return database.run(sql, bindings);
|
|
25246
|
+
}
|
|
25247
|
+
function insertedId9(value) {
|
|
25248
|
+
return Number(value.lastInsertRowid);
|
|
25249
|
+
}
|
|
25250
|
+
function record6(value, label) {
|
|
25251
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
25252
|
+
throw new Error(`${label} must be an object`);
|
|
25253
|
+
return value;
|
|
25254
|
+
}
|
|
25255
|
+
function exact4(value, keys, label) {
|
|
25256
|
+
const actual = Object.keys(value).sort();
|
|
25257
|
+
const expected = [...keys].sort();
|
|
25258
|
+
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
|
|
25259
|
+
throw new Error(`${label} changed its reviewed shape`);
|
|
25260
|
+
}
|
|
25261
|
+
}
|
|
25262
|
+
function text6(value, label, maximum) {
|
|
25263
|
+
if (typeof value !== "string")
|
|
25264
|
+
throw new Error(`${label} must be a string`);
|
|
25265
|
+
const normalized = value.normalize("NFC");
|
|
25266
|
+
if (normalized.length < 1 || Buffer.byteLength(normalized, "utf8") > maximum || /[\u0000-\u001F\u007F-\u009F]/u.test(normalized))
|
|
25267
|
+
throw new Error(`${label} is invalid`);
|
|
25268
|
+
return normalized;
|
|
25269
|
+
}
|
|
25270
|
+
function optionalText3(value, label, maximum) {
|
|
25271
|
+
return value === null ? null : text6(value, label, maximum);
|
|
25272
|
+
}
|
|
25273
|
+
function timestamp6(value, label) {
|
|
25274
|
+
const parsed = text6(value, label, 64);
|
|
25275
|
+
const milliseconds2 = Date.parse(parsed);
|
|
25276
|
+
if (!Number.isFinite(milliseconds2) || new Date(milliseconds2).toISOString() !== parsed) {
|
|
25277
|
+
throw new Error(`${label} must be a canonical UTC timestamp`);
|
|
25278
|
+
}
|
|
25279
|
+
return parsed;
|
|
25280
|
+
}
|
|
25281
|
+
function boundedList(value, label, maximum) {
|
|
25282
|
+
if (!Array.isArray(value) || value.length > maximum)
|
|
25283
|
+
throw new Error(`${label} exceeded its reviewed bound`);
|
|
25284
|
+
const items = value.map((item, index) => text6(item, `${label}[${index}]`, 2048));
|
|
25285
|
+
if (new Set(items).size !== items.length)
|
|
25286
|
+
throw new Error(`${label} contains duplicates`);
|
|
25287
|
+
return items;
|
|
25288
|
+
}
|
|
25289
|
+
function website(value, label) {
|
|
25290
|
+
let parsed;
|
|
25291
|
+
try {
|
|
25292
|
+
parsed = new URL(value);
|
|
25293
|
+
} catch (error) {
|
|
25294
|
+
throw new Error(`${label} is not an absolute URL`, { cause: error });
|
|
25295
|
+
}
|
|
25296
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:" || parsed.username !== "" || parsed.password !== "")
|
|
25297
|
+
throw new Error(`${label} must be a plain web URL`);
|
|
25298
|
+
return parsed.href;
|
|
25299
|
+
}
|
|
25300
|
+
function normalizedWebsite(value) {
|
|
25301
|
+
const parsed = new URL(value);
|
|
25302
|
+
parsed.hash = "";
|
|
25303
|
+
const path = parsed.pathname.length > 1 && parsed.pathname.endsWith("/") ? parsed.pathname.slice(0, -1) : parsed.pathname;
|
|
25304
|
+
return `${parsed.protocol}//${parsed.host.toLowerCase()}${path}${parsed.search}`;
|
|
25305
|
+
}
|
|
25306
|
+
function normalizedEmail3(value, label) {
|
|
25307
|
+
if (!EMAIL5.test(value) || Buffer.byteLength(value, "utf8") > 320)
|
|
25308
|
+
throw new Error(`${label} is not an email address`);
|
|
25309
|
+
return value.toLowerCase();
|
|
25310
|
+
}
|
|
25311
|
+
function parseFields(value, label) {
|
|
25312
|
+
const parsed = record6(value, label);
|
|
25313
|
+
exact4(parsed, ["email", "profileUrl", "connectedSince", "phones", "websites", "birthday"], label);
|
|
25314
|
+
const email2 = optionalText3(parsed.email, `${label}.email`, 320);
|
|
25315
|
+
if (email2 !== null)
|
|
25316
|
+
normalizedEmail3(email2, `${label}.email`);
|
|
25317
|
+
const profileUrl2 = optionalText3(parsed.profileUrl, `${label}.profileUrl`, 4096);
|
|
25318
|
+
if (profileUrl2 !== null && canonicalLinkedInProfileUrl(profileUrl2, `${label}.profileUrl`) === null) {
|
|
25319
|
+
throw new Error(`${label}.profileUrl is invalid`);
|
|
25320
|
+
}
|
|
25321
|
+
const connectedSince = optionalText3(parsed.connectedSince, `${label}.connectedSince`, 10);
|
|
25322
|
+
if (connectedSince !== null && !DATE.test(connectedSince))
|
|
25323
|
+
throw new Error(`${label}.connectedSince is invalid`);
|
|
25324
|
+
const phones = boundedList(parsed.phones, `${label}.phones`, MAX_PHONES);
|
|
25325
|
+
for (const [index, phone] of phones.entries()) {
|
|
25326
|
+
if (!PHONE.test(phone))
|
|
25327
|
+
throw new Error(`${label}.phones[${index}] is invalid`);
|
|
25328
|
+
}
|
|
25329
|
+
const websites = boundedList(parsed.websites, `${label}.websites`, MAX_WEBSITES).map((item, index) => website(item, `${label}.websites[${index}]`));
|
|
25330
|
+
const birthday2 = optionalText3(parsed.birthday, `${label}.birthday`, 10);
|
|
25331
|
+
if (birthday2 !== null && !BIRTHDAY.test(birthday2))
|
|
25332
|
+
throw new Error(`${label}.birthday is invalid`);
|
|
25333
|
+
return Object.freeze({ email: email2, profileUrl: profileUrl2, connectedSince, phones, websites, birthday: birthday2 });
|
|
25334
|
+
}
|
|
25335
|
+
function projectLinkedInContactInfo(value, expectedProfileUrl) {
|
|
25336
|
+
const output = record6(value, "Ghostget LinkedIn contact output");
|
|
25337
|
+
exact4(output, ["schemaVersion", "provider", "profile", "viewer", "observedAt", "completeness", "contact"], "contact output");
|
|
25338
|
+
if (output.schemaVersion !== 1 || output.provider !== PROVIDER11)
|
|
25339
|
+
throw new Error("Ghostget LinkedIn contact semantics drifted");
|
|
25340
|
+
const profile = record6(output.profile, "contact output.profile");
|
|
25341
|
+
exact4(profile, ["vanity", "profileUrn", "url", "relationship"], "contact output.profile");
|
|
25342
|
+
const vanity = text6(profile.vanity, "contact output.profile.vanity", 1024);
|
|
25343
|
+
if (!VANITY.test(vanity))
|
|
25344
|
+
throw new Error("Ghostget LinkedIn profile vanity is invalid");
|
|
25345
|
+
const profileUrn = text6(profile.profileUrn, "contact output.profile.profileUrn", 300);
|
|
25346
|
+
if (!PROFILE_URN.test(profileUrn))
|
|
25347
|
+
throw new Error("Ghostget LinkedIn profile URN is invalid");
|
|
25348
|
+
const url = canonicalLinkedInProfileUrl(text6(profile.url, "contact output.profile.url", 4096), "contact output.profile.url");
|
|
25349
|
+
if (url === null || url !== expectedProfileUrl) {
|
|
25350
|
+
throw new Error("Ghostget LinkedIn contact output does not bind the requested profile");
|
|
25351
|
+
}
|
|
25352
|
+
if (profile.relationship !== "first-degree")
|
|
25353
|
+
throw new Error("Ghostget LinkedIn contact output is not a first-degree read");
|
|
25354
|
+
const viewer = record6(output.viewer, "contact output.viewer");
|
|
25355
|
+
exact4(viewer, ["subject"], "contact output.viewer");
|
|
25356
|
+
const viewerSubject = text6(viewer.subject, "contact output.viewer.subject", 64);
|
|
25357
|
+
if (!VIEWER_SUBJECT.test(viewerSubject))
|
|
25358
|
+
throw new Error("Ghostget LinkedIn viewer subject is invalid");
|
|
25359
|
+
const observedAt = timestamp6(output.observedAt, "contact output.observedAt");
|
|
25360
|
+
if (output.completeness !== "complete" && output.completeness !== "partial") {
|
|
25361
|
+
throw new Error("Ghostget LinkedIn contact completeness is invalid");
|
|
25362
|
+
}
|
|
25363
|
+
const contact = parseFields(output.contact, "contact output.contact");
|
|
25364
|
+
const hasAny = contact.email !== null || contact.connectedSince !== null || contact.phones.length > 0 || contact.websites.length > 0 || contact.birthday !== null;
|
|
25365
|
+
if (hasAny !== (output.completeness === "complete")) {
|
|
25366
|
+
throw new Error("Ghostget LinkedIn contact completeness disagrees with its fields");
|
|
25367
|
+
}
|
|
25368
|
+
return Object.freeze({
|
|
25369
|
+
profileUrl: url,
|
|
25370
|
+
vanity,
|
|
25371
|
+
profileUrn,
|
|
25372
|
+
relationship: "first-degree",
|
|
25373
|
+
viewerSubject,
|
|
25374
|
+
observedAt,
|
|
25375
|
+
completeness: output.completeness,
|
|
25376
|
+
contact
|
|
25377
|
+
});
|
|
25378
|
+
}
|
|
25379
|
+
function authId2(value) {
|
|
25380
|
+
const parsed = (value ?? DEFAULT_LINKEDIN_AUTH).trim();
|
|
25381
|
+
if (!AUTH_ID3.test(parsed))
|
|
25382
|
+
throw new Error("Ghostget auth ID is invalid");
|
|
25383
|
+
return parsed;
|
|
25384
|
+
}
|
|
25385
|
+
function envelope(value, input, auth) {
|
|
25386
|
+
return validatePeopleBladeGhostgetInvocation(value, {
|
|
25387
|
+
adapterId: LINKEDIN_CONTACT_INFO_ADAPTER_ID,
|
|
25388
|
+
operationId: LINKEDIN_CONTACT_INFO_OPERATION_ID,
|
|
25389
|
+
authId: auth,
|
|
25390
|
+
input
|
|
25391
|
+
}, {
|
|
25392
|
+
adapterVersion: LINKEDIN_CONTACT_INFO_ADAPTER_VERSION,
|
|
25393
|
+
authKind: "browser-profile",
|
|
25394
|
+
transport: "web-session-api",
|
|
25395
|
+
contractSha256: LINKEDIN_CONTACT_INFO_CONTRACT_SHA256,
|
|
25396
|
+
finalOrigin: "https://www.linkedin.com"
|
|
25397
|
+
});
|
|
25398
|
+
}
|
|
25399
|
+
function storedResultFromJson(value) {
|
|
25400
|
+
const parsed = record6(JSON.parse(value), "stored LinkedIn contact-info result");
|
|
25401
|
+
exact4(parsed, [
|
|
25402
|
+
"account_key",
|
|
25403
|
+
"person_id",
|
|
25404
|
+
"provider_resource_id",
|
|
25405
|
+
"profile_url",
|
|
25406
|
+
"vanity",
|
|
25407
|
+
"observed_at",
|
|
25408
|
+
"completeness",
|
|
25409
|
+
"connected_since",
|
|
25410
|
+
"email_recorded",
|
|
25411
|
+
"phones_recorded",
|
|
25412
|
+
"phones_unresolved",
|
|
25413
|
+
"websites_recorded",
|
|
25414
|
+
"birthday_recorded"
|
|
25415
|
+
], "stored LinkedIn contact-info result");
|
|
25416
|
+
return parsed;
|
|
25417
|
+
}
|
|
25418
|
+
function metadata3(value) {
|
|
25419
|
+
return record6(JSON.parse(value), "LinkedIn resource metadata");
|
|
25420
|
+
}
|
|
25421
|
+
function readLinkedInContactInfo(database, options) {
|
|
25422
|
+
const account = linkedInAccountKey(options.accountKey);
|
|
25423
|
+
const auth = authId2(options.authId);
|
|
25424
|
+
if (!Number.isSafeInteger(options.personId) || options.personId < 1)
|
|
25425
|
+
throw new Error("person ID is invalid");
|
|
25426
|
+
const resources = allRows7(database, `SELECT id,person_id,profile_url,resource_id,metadata_json FROM provider_resources
|
|
25427
|
+
WHERE provider=? AND account_key=? AND resource_type='profile' AND active=1 AND person_id=?
|
|
25428
|
+
ORDER BY id`, PROVIDER11, account, options.personId);
|
|
25429
|
+
if (resources.length === 0) {
|
|
25430
|
+
throw new Error("This person has no active LinkedIn profile in that account; import the official LinkedIn export first");
|
|
25431
|
+
}
|
|
25432
|
+
if (resources.length > 1) {
|
|
25433
|
+
throw new Error("This person has more than one active LinkedIn profile in that account; review its identity decisions first");
|
|
25434
|
+
}
|
|
25435
|
+
const resource = resources[0];
|
|
25436
|
+
const profileUrl2 = canonicalLinkedInProfileUrl(resource.profile_url ?? resource.resource_id, "stored LinkedIn profile URL");
|
|
25437
|
+
if (profileUrl2 === null)
|
|
25438
|
+
throw new Error("stored LinkedIn profile resource lacks an exact profile URL");
|
|
25439
|
+
const owner = getRow9(database, "SELECT owner_key FROM archive_accounts WHERE provider=? AND account_key=?", PROVIDER11, account);
|
|
25440
|
+
if (owner !== null && owner.owner_key === profileUrl2) {
|
|
25441
|
+
throw new Error("Contact info is read for connections, not the account owner's own profile");
|
|
25442
|
+
}
|
|
25443
|
+
const input = Object.freeze({ profile_url: profileUrl2 });
|
|
25444
|
+
const invocation = callPeopleBladeGhostget({
|
|
25445
|
+
adapterId: LINKEDIN_CONTACT_INFO_ADAPTER_ID,
|
|
25446
|
+
operationId: LINKEDIN_CONTACT_INFO_OPERATION_ID,
|
|
25447
|
+
authId: auth,
|
|
25448
|
+
input
|
|
25449
|
+
}, options.invoke);
|
|
25450
|
+
const parsed = envelope(invocation, input, auth);
|
|
25451
|
+
const projection2 = projectLinkedInContactInfo(parsed.output, profileUrl2);
|
|
25452
|
+
const sourceSha256 = sha256(canonicalJson(projection2));
|
|
25453
|
+
const execution = parsed.execution;
|
|
25454
|
+
const personId = resource.person_id;
|
|
25455
|
+
const resourceId = resource.id;
|
|
25456
|
+
const resolvedPhones = projection2.contact.phones.map((phone) => ({ phone, normalized: resolvePhoneIdentity(phone, null) }));
|
|
25457
|
+
const stored = {
|
|
25458
|
+
account_key: account,
|
|
25459
|
+
person_id: personId,
|
|
25460
|
+
provider_resource_id: resourceId,
|
|
25461
|
+
profile_url: profileUrl2,
|
|
25462
|
+
vanity: projection2.vanity,
|
|
25463
|
+
observed_at: projection2.observedAt,
|
|
25464
|
+
completeness: projection2.completeness,
|
|
25465
|
+
connected_since: projection2.contact.connectedSince,
|
|
25466
|
+
email_recorded: projection2.contact.email !== null,
|
|
25467
|
+
phones_recorded: resolvedPhones.filter((entry) => entry.normalized !== null).length,
|
|
25468
|
+
phones_unresolved: resolvedPhones.filter((entry) => entry.normalized === null).length,
|
|
25469
|
+
websites_recorded: projection2.contact.websites.length,
|
|
25470
|
+
birthday_recorded: projection2.contact.birthday !== null
|
|
25471
|
+
};
|
|
25472
|
+
database.exec("BEGIN IMMEDIATE");
|
|
25473
|
+
try {
|
|
25474
|
+
const cached = getRow9(database, "SELECT id,result_json FROM source_runs WHERE provider=? AND account_key=? AND mode=? AND source_sha256=?", PROVIDER11, account, MODE8, sourceSha256);
|
|
25475
|
+
if (cached !== null) {
|
|
25476
|
+
database.exec("COMMIT");
|
|
25477
|
+
return { ...storedResultFromJson(cached.result_json), run_id: cached.id, replayed: true };
|
|
25478
|
+
}
|
|
25479
|
+
const runId = insertedId9(run8(database, `INSERT INTO source_runs(
|
|
25480
|
+
provider,account_key,mode,source_sha256,source_locator_sha256,completeness,result_json,started_at,completed_at
|
|
25481
|
+
) VALUES (?,?,?,?,?,?,?,?,?)`, PROVIDER11, account, MODE8, sourceSha256, sha256(profileUrl2), projection2.completeness, canonicalJson(stored), execution.startedAt, execution.finishedAt));
|
|
25482
|
+
const methodLabel = `${OBSERVATION_BASIS2}:${sha256(profileUrl2).slice(0, 16)}`;
|
|
25483
|
+
const methodMetadata = canonicalJson({ observedIn: OBSERVATION_BASIS2, observedAt: projection2.observedAt });
|
|
25484
|
+
run8(database, "UPDATE contact_methods SET active=0 WHERE provider_resource_id=? AND label=?", resourceId, methodLabel);
|
|
25485
|
+
if (projection2.contact.email !== null) {
|
|
25486
|
+
run8(database, `INSERT INTO contact_methods(
|
|
25487
|
+
person_id,kind,value,normalized_value,label,confidence,first_seen_run_id,last_seen_run_id,
|
|
25488
|
+
metadata_json,provider_resource_id,active,identity_eligible
|
|
25489
|
+
) VALUES (?,'email',?,?,?,'exact',?,?,?,?,1,1)
|
|
25490
|
+
ON CONFLICT(person_id,kind,normalized_value,label) DO UPDATE SET
|
|
25491
|
+
value=excluded.value,last_seen_run_id=excluded.last_seen_run_id,metadata_json=excluded.metadata_json,
|
|
25492
|
+
provider_resource_id=excluded.provider_resource_id,active=1,identity_eligible=1`, personId, projection2.contact.email, normalizedEmail3(projection2.contact.email, "contact email"), methodLabel, runId, runId, methodMetadata, resourceId);
|
|
25493
|
+
}
|
|
25494
|
+
for (const { phone, normalized } of resolvedPhones) {
|
|
25495
|
+
if (normalized === null)
|
|
25496
|
+
continue;
|
|
25497
|
+
run8(database, `INSERT INTO contact_methods(
|
|
25498
|
+
person_id,kind,value,normalized_value,label,confidence,first_seen_run_id,last_seen_run_id,
|
|
25499
|
+
metadata_json,provider_resource_id,active,identity_eligible
|
|
25500
|
+
) VALUES (?,'phone',?,?,?,'exact',?,?,?,?,1,1)
|
|
25501
|
+
ON CONFLICT(person_id,kind,normalized_value,label) DO UPDATE SET
|
|
25502
|
+
value=excluded.value,last_seen_run_id=excluded.last_seen_run_id,metadata_json=excluded.metadata_json,
|
|
25503
|
+
provider_resource_id=excluded.provider_resource_id,active=1,identity_eligible=1`, personId, phone, normalized, methodLabel, runId, runId, methodMetadata, resourceId);
|
|
25504
|
+
}
|
|
25505
|
+
for (const site of projection2.contact.websites) {
|
|
25506
|
+
run8(database, `INSERT INTO contact_methods(
|
|
25507
|
+
person_id,kind,value,normalized_value,label,confidence,first_seen_run_id,last_seen_run_id,
|
|
25508
|
+
metadata_json,provider_resource_id,active,identity_eligible
|
|
25509
|
+
) VALUES (?,'url',?,?,?,'exact',?,?,?,?,1,0)
|
|
25510
|
+
ON CONFLICT(person_id,kind,normalized_value,label) DO UPDATE SET
|
|
25511
|
+
value=excluded.value,last_seen_run_id=excluded.last_seen_run_id,metadata_json=excluded.metadata_json,
|
|
25512
|
+
provider_resource_id=excluded.provider_resource_id,active=1,identity_eligible=0`, personId, site, normalizedWebsite(site), methodLabel, runId, runId, methodMetadata, resourceId);
|
|
25513
|
+
}
|
|
25514
|
+
run8(database, "UPDATE person_field_observations SET active=0 WHERE provider_resource_id=? AND basis=?", resourceId, OBSERVATION_BASIS2);
|
|
25515
|
+
if (projection2.contact.birthday !== null) {
|
|
25516
|
+
run8(database, `INSERT INTO person_field_observations(
|
|
25517
|
+
provider_resource_id,field,value,basis,priority,active,first_seen_run_id,last_seen_run_id,metadata_json
|
|
25518
|
+
) VALUES (?,'birthday',?,?,?,1,?,?,'{}')
|
|
25519
|
+
ON CONFLICT(provider_resource_id,field,value,basis) DO UPDATE SET
|
|
25520
|
+
priority=excluded.priority,active=1,last_seen_run_id=excluded.last_seen_run_id`, resourceId, projection2.contact.birthday, OBSERVATION_BASIS2, OBSERVATION_PRIORITY2, runId, runId);
|
|
25521
|
+
run8(database, "UPDATE people SET birthday=coalesce(birthday,?) WHERE id=?", projection2.contact.birthday, personId);
|
|
25522
|
+
}
|
|
25523
|
+
const resourceMetadata = {
|
|
25524
|
+
...metadata3(resource.metadata_json),
|
|
25525
|
+
linkedInContactInfo: {
|
|
25526
|
+
observedAt: projection2.observedAt,
|
|
25527
|
+
completeness: projection2.completeness,
|
|
25528
|
+
connectedSince: projection2.contact.connectedSince,
|
|
25529
|
+
viewerSubject: projection2.viewerSubject,
|
|
25530
|
+
runId
|
|
25531
|
+
}
|
|
25532
|
+
};
|
|
25533
|
+
run8(database, "UPDATE provider_resources SET last_seen_run_id=?,metadata_json=? WHERE id=?", runId, canonicalJson(resourceMetadata), resourceId);
|
|
25534
|
+
run8(database, `INSERT INTO source_records(
|
|
25535
|
+
run_id,ordinal,record_kind,record_sha256,person_id,provider_resource_id,resolution,projection_json,error_text
|
|
25536
|
+
) VALUES (?,1,?,?,?,?,'matched',?,NULL)`, runId, RECORD_KIND, sourceSha256, personId, resourceId, canonicalJson(projection2));
|
|
25537
|
+
ledgerPeopleBladeGhostgetInvocation(database, {
|
|
25538
|
+
provider: PROVIDER11,
|
|
25539
|
+
accountKey: account,
|
|
25540
|
+
capability: "contact_info",
|
|
25541
|
+
execution,
|
|
25542
|
+
costBasis: "local-browser-read",
|
|
25543
|
+
metadata: { profileUrlSha256: sha256(profileUrl2), completeness: projection2.completeness }
|
|
25544
|
+
});
|
|
25545
|
+
database.exec("COMMIT");
|
|
25546
|
+
return { ...stored, run_id: runId, replayed: false };
|
|
25547
|
+
} catch (error) {
|
|
25548
|
+
database.exec("ROLLBACK");
|
|
25549
|
+
throw error;
|
|
25550
|
+
}
|
|
25551
|
+
}
|
|
25552
|
+
|
|
24996
25553
|
// src/local/providers/whatsapp.ts
|
|
24997
25554
|
import { createHash as createHash9 } from "crypto";
|
|
24998
25555
|
var CONTACTS_CONTRACT_SHA2562 = "0bd3c994f16608d5886f1192dd58d41736e4bd01add66a5a3e029efca8b14a38";
|
|
@@ -25013,18 +25570,18 @@ var DEVICE_LID_JID = /^([0-9]{5,32}):[0-9]{1,5}@lid$/u;
|
|
|
25013
25570
|
var GROUP_JID = /^[0-9]{5,32}(?:-[0-9]{5,20})?@g\.us$/u;
|
|
25014
25571
|
var OTHER_JID = /^(?:0@s\.whatsapp\.net|[0-9]{5,32}@newsletter|(?:status|[0-9]{5,32})@broadcast)$/u;
|
|
25015
25572
|
var MESSAGE_ID = /^[A-Za-z0-9._~:-]{1,256}$/u;
|
|
25016
|
-
var
|
|
25573
|
+
var PROVIDER12 = "whatsapp";
|
|
25017
25574
|
var STREAM2 = "messages";
|
|
25018
|
-
function
|
|
25575
|
+
function getRow10(database, sql, ...values) {
|
|
25019
25576
|
return database.query(sql).get(...values);
|
|
25020
25577
|
}
|
|
25021
|
-
function
|
|
25578
|
+
function run9(database, sql, ...values) {
|
|
25022
25579
|
return database.run(sql, values);
|
|
25023
25580
|
}
|
|
25024
|
-
function
|
|
25581
|
+
function insertedId10(value) {
|
|
25025
25582
|
return Number(value.lastInsertRowid);
|
|
25026
25583
|
}
|
|
25027
|
-
function
|
|
25584
|
+
function record7(value, label) {
|
|
25028
25585
|
if (typeof value !== "object" || value === null || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
|
|
25029
25586
|
throw new Error(`${label} must be a plain object`);
|
|
25030
25587
|
}
|
|
@@ -25036,14 +25593,14 @@ function record6(value, label) {
|
|
|
25036
25593
|
throw new Error(`${label} has symbol keys`);
|
|
25037
25594
|
return value;
|
|
25038
25595
|
}
|
|
25039
|
-
function
|
|
25596
|
+
function exact5(value, keys, label) {
|
|
25040
25597
|
const actual = Object.keys(value).sort();
|
|
25041
25598
|
const expected = [...keys].sort();
|
|
25042
25599
|
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
|
|
25043
25600
|
throw new Error(`${label} contains unsupported or missing fields`);
|
|
25044
25601
|
}
|
|
25045
25602
|
}
|
|
25046
|
-
function
|
|
25603
|
+
function text7(value, label, maximum = 4096, empty = false) {
|
|
25047
25604
|
if (typeof value !== "string" || !empty && value.length === 0 || Buffer.byteLength(value, "utf8") > maximum || /[\u0000\r]/u.test(value)) {
|
|
25048
25605
|
throw new Error(`${label} must be bounded text`);
|
|
25049
25606
|
}
|
|
@@ -25060,7 +25617,7 @@ function bool3(value, label) {
|
|
|
25060
25617
|
return value;
|
|
25061
25618
|
}
|
|
25062
25619
|
function digest5(value, label) {
|
|
25063
|
-
const parsed =
|
|
25620
|
+
const parsed = text7(value, label, 64);
|
|
25064
25621
|
if (!SHA2563.test(parsed))
|
|
25065
25622
|
throw new Error(`${label} is invalid`);
|
|
25066
25623
|
return parsed;
|
|
@@ -25078,11 +25635,11 @@ function stableJson6(value) {
|
|
|
25078
25635
|
}
|
|
25079
25636
|
if (Array.isArray(value))
|
|
25080
25637
|
return `[${value.map(stableJson6).join(",")}]`;
|
|
25081
|
-
const parsed =
|
|
25638
|
+
const parsed = record7(value, "canonical JSON");
|
|
25082
25639
|
return `{${Object.keys(parsed).sort().map((key) => `${JSON.stringify(key)}:${stableJson6(parsed[key])}`).join(",")}}`;
|
|
25083
25640
|
}
|
|
25084
|
-
function
|
|
25085
|
-
const parsed =
|
|
25641
|
+
function timestamp7(value, label) {
|
|
25642
|
+
const parsed = text7(value, label, 40);
|
|
25086
25643
|
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/u.test(parsed))
|
|
25087
25644
|
throw new Error(`${label} is invalid`);
|
|
25088
25645
|
const milliseconds2 = Date.parse(parsed);
|
|
@@ -25091,25 +25648,25 @@ function timestamp6(value, label) {
|
|
|
25091
25648
|
return new Date(milliseconds2).toISOString();
|
|
25092
25649
|
}
|
|
25093
25650
|
function decimal(value, label) {
|
|
25094
|
-
const parsed =
|
|
25651
|
+
const parsed = text7(value, label, 19);
|
|
25095
25652
|
if (!DECIMAL.test(parsed) || BigInt(parsed) > BigInt("9223372036854775807"))
|
|
25096
25653
|
throw new Error(`${label} is invalid`);
|
|
25097
25654
|
return parsed;
|
|
25098
25655
|
}
|
|
25099
|
-
function
|
|
25100
|
-
const parsed =
|
|
25656
|
+
function authId3(value) {
|
|
25657
|
+
const parsed = text7(value, "WhatsApp auth ID", 48);
|
|
25101
25658
|
if (!/^[a-z][a-z0-9-]{0,47}$/u.test(parsed))
|
|
25102
25659
|
throw new Error("WhatsApp auth ID is invalid");
|
|
25103
25660
|
return parsed;
|
|
25104
25661
|
}
|
|
25105
25662
|
function subject(value) {
|
|
25106
|
-
const parsed =
|
|
25663
|
+
const parsed = text7(value, "WhatsApp account subject", 64);
|
|
25107
25664
|
if (!SUBJECT2.test(parsed))
|
|
25108
25665
|
throw new Error("WhatsApp account subject is invalid");
|
|
25109
25666
|
return parsed;
|
|
25110
25667
|
}
|
|
25111
25668
|
function canonicalJid(value, label, device = false) {
|
|
25112
|
-
const parsed =
|
|
25669
|
+
const parsed = text7(value, label, 96);
|
|
25113
25670
|
const user = USER_JID.exec(parsed) ?? (device ? DEVICE_USER_JID.exec(parsed) : null);
|
|
25114
25671
|
if (user?.[1] !== undefined)
|
|
25115
25672
|
return { jid: `${user[1]}@s.whatsapp.net`, kind: "user" };
|
|
@@ -25131,7 +25688,7 @@ function directJid(value, label) {
|
|
|
25131
25688
|
function ownerJid(accountSubject) {
|
|
25132
25689
|
return accountSubject.startsWith("whatsapp:pn:") ? `${accountSubject.slice(12)}@s.whatsapp.net` : `${accountSubject.slice(13)}@lid`;
|
|
25133
25690
|
}
|
|
25134
|
-
function
|
|
25691
|
+
function envelope2(value, input, auth) {
|
|
25135
25692
|
const parsed = validatePeopleBladeGhostgetInvocation(value, {
|
|
25136
25693
|
adapterId: "whatsapp-web",
|
|
25137
25694
|
operationId: "contacts.list",
|
|
@@ -25144,25 +25701,25 @@ function envelope(value, input, auth) {
|
|
|
25144
25701
|
contractSha256: CONTACTS_CONTRACT_SHA2562,
|
|
25145
25702
|
finalOrigin: "https://web.whatsapp.com"
|
|
25146
25703
|
});
|
|
25147
|
-
return { output:
|
|
25704
|
+
return { output: record7(parsed.output, "Ghostget output"), execution: parsed.execution };
|
|
25148
25705
|
}
|
|
25149
25706
|
var CONTACT_KEYS = ["providerId", "jidKind", "phone", "redactedPhone", "firstName", "fullName", "pushName", "businessName", "displayName", "displayNameBasis", "alias", "tags", "updatedAt", "localProjectionStatsComplete", "sentCount", "sentCountComplete", "sentCountLowerBound", "sentCountTruncated", "receivedCount", "receivedCountComplete", "receivedCountLowerBound", "receivedCountTruncated", "lastSentAt", "lastSentAtComplete", "lastSentAtBasis", "sentStatsIncompleteReasons", "lastReceivedAt", "lastReceivedAtComplete", "lastReceivedAtBasis", "receivedStatsIncompleteReasons"];
|
|
25150
25707
|
function parseContact4(value, label) {
|
|
25151
|
-
const parsed =
|
|
25152
|
-
|
|
25708
|
+
const parsed = record7(value, label);
|
|
25709
|
+
exact5(parsed, CONTACT_KEYS, label);
|
|
25153
25710
|
const identity = directJid(parsed.providerId, `${label}.providerId`);
|
|
25154
25711
|
if (parsed.jidKind !== identity.kind)
|
|
25155
25712
|
throw new Error(`${label}.jidKind drifted`);
|
|
25156
|
-
const phone = parsed.phone === null ? null :
|
|
25713
|
+
const phone = parsed.phone === null ? null : text7(parsed.phone, `${label}.phone`, 20);
|
|
25157
25714
|
if (phone !== (USER_JID.exec(identity.jid)?.[1] ?? null))
|
|
25158
25715
|
throw new Error(`${label}.phone drifted`);
|
|
25159
|
-
const displayName2 = parsed.displayName === null ? null :
|
|
25716
|
+
const displayName2 = parsed.displayName === null ? null : text7(parsed.displayName, `${label}.displayName`, 512);
|
|
25160
25717
|
const bases = ["full-name", "push-name", "business-name", "first-name", "redacted-phone", "phone", "unavailable"];
|
|
25161
25718
|
if (!bases.includes(parsed.displayNameBasis))
|
|
25162
25719
|
throw new Error(`${label}.displayNameBasis is invalid`);
|
|
25163
25720
|
for (const key of ["redactedPhone", "firstName", "fullName", "pushName", "businessName"])
|
|
25164
25721
|
if (parsed[key] !== null)
|
|
25165
|
-
|
|
25722
|
+
text7(parsed[key], `${label}.${key}`, 512);
|
|
25166
25723
|
if (parsed.alias !== null || parsed.updatedAt !== null || !Array.isArray(parsed.tags) || parsed.tags.length !== 0 || parsed.localProjectionStatsComplete !== false || parsed.sentCount !== null || parsed.sentCountComplete !== false || parsed.sentCountLowerBound !== false || parsed.sentCountTruncated !== false || parsed.receivedCount !== null || parsed.receivedCountComplete !== false || parsed.receivedCountLowerBound !== false || parsed.receivedCountTruncated !== false || parsed.lastSentAt !== null || parsed.lastSentAtComplete !== false || parsed.lastSentAtBasis !== "unavailable" || parsed.lastReceivedAt !== null || parsed.lastReceivedAtComplete !== false || parsed.lastReceivedAtBasis !== "unavailable" || stableJson6(parsed.sentStatsIncompleteReasons) !== stableJson6(["whatsapp-message-store-account-owner-unavailable"]) || stableJson6(parsed.receivedStatsIncompleteReasons) !== stableJson6(["whatsapp-message-store-account-owner-unavailable"])) {
|
|
25167
25724
|
throw new Error(`${label} changed bounded contact semantics`);
|
|
25168
25725
|
}
|
|
@@ -25175,9 +25732,9 @@ function parseContact4(value, label) {
|
|
|
25175
25732
|
};
|
|
25176
25733
|
}
|
|
25177
25734
|
function parseContactPage(value, input, auth) {
|
|
25178
|
-
const parsed =
|
|
25735
|
+
const parsed = envelope2(value, input, auth);
|
|
25179
25736
|
const output = parsed.output;
|
|
25180
|
-
|
|
25737
|
+
exact5(output, ["provider", "operation", "accountSubject", "projection", "contacts", "nextCursor", "localContactTablePageComplete", "remoteContactSetComplete", "contactSetIncompleteReasons", "statsScope", "statsCompleteness"], "contact output");
|
|
25181
25738
|
if (output.provider !== "whatsapp" || output.operation !== "contacts.list" || output.projection !== "quiescent-account-bound-session-store" || output.remoteContactSetComplete !== false || output.statsScope !== "unavailable" || output.statsCompleteness !== "unavailable" || stableJson6(output.contactSetIncompleteReasons) !== stableJson6(["linked-device-contact-sync-coverage-unknown"]) || input.collection !== "contacts") {
|
|
25182
25739
|
throw new Error("Ghostget contact semantics drifted");
|
|
25183
25740
|
}
|
|
@@ -25202,10 +25759,10 @@ function parseContactPage(value, input, auth) {
|
|
|
25202
25759
|
return { input, accountSubject, contacts, nextCursor, localPageComplete: complete, safeOutputSha256, execution: parsed.execution };
|
|
25203
25760
|
}
|
|
25204
25761
|
function generation(value) {
|
|
25205
|
-
const parsed =
|
|
25206
|
-
|
|
25207
|
-
const identity =
|
|
25208
|
-
|
|
25762
|
+
const parsed = record7(value, "projection generation");
|
|
25763
|
+
exact5(parsed, ["messageStoreIdentity", "schemaFingerprint"], "projection generation");
|
|
25764
|
+
const identity = record7(parsed.messageStoreIdentity, "message-store identity");
|
|
25765
|
+
exact5(identity, ["dev", "ino"], "message-store identity");
|
|
25209
25766
|
const dev = decimal(identity.dev, "message-store dev");
|
|
25210
25767
|
const ino = decimal(identity.ino, "message-store ino");
|
|
25211
25768
|
if (ino === "0" || parsed.schemaFingerprint !== PROJECTION_SCHEMA_FINGERPRINT)
|
|
@@ -25213,8 +25770,8 @@ function generation(value) {
|
|
|
25213
25770
|
return { messageStoreIdentity: { dev, ino }, schemaFingerprint: PROJECTION_SCHEMA_FINGERPRINT };
|
|
25214
25771
|
}
|
|
25215
25772
|
function checkpoint(value, label) {
|
|
25216
|
-
const parsed =
|
|
25217
|
-
|
|
25773
|
+
const parsed = record7(value, label);
|
|
25774
|
+
exact5(parsed, ["cursor", "anchor"], label);
|
|
25218
25775
|
const cursor = decimal(parsed.cursor, `${label}.cursor`);
|
|
25219
25776
|
const anchor = parsed.anchor === null ? null : digest5(parsed.anchor, `${label}.anchor`);
|
|
25220
25777
|
if (cursor === "0" !== (anchor === null))
|
|
@@ -25222,14 +25779,14 @@ function checkpoint(value, label) {
|
|
|
25222
25779
|
return { cursor, anchor };
|
|
25223
25780
|
}
|
|
25224
25781
|
function parseInteraction(value, label) {
|
|
25225
|
-
const parsed =
|
|
25226
|
-
|
|
25782
|
+
const parsed = record7(value, label);
|
|
25783
|
+
exact5(parsed, ["rowid", "chatJid", "messageId", "senderJid", "timestamp", "fromMe", "chatKind"], label);
|
|
25227
25784
|
const rowid = decimal(parsed.rowid, `${label}.rowid`);
|
|
25228
25785
|
if (rowid === "0")
|
|
25229
25786
|
throw new Error(`${label}.rowid is invalid`);
|
|
25230
25787
|
const chat = canonicalJid(parsed.chatJid, `${label}.chatJid`);
|
|
25231
25788
|
const sender = parsed.senderJid === null ? null : canonicalJid(parsed.senderJid, `${label}.senderJid`, true).jid;
|
|
25232
|
-
const messageId =
|
|
25789
|
+
const messageId = text7(parsed.messageId, `${label}.messageId`, 256);
|
|
25233
25790
|
if (!MESSAGE_ID.test(messageId))
|
|
25234
25791
|
throw new Error(`${label}.messageId is invalid`);
|
|
25235
25792
|
const kinds = ["dm", "group", "broadcast", "newsletter", "unknown"];
|
|
@@ -25243,7 +25800,7 @@ function parseInteraction(value, label) {
|
|
|
25243
25800
|
chatJid: chat.jid,
|
|
25244
25801
|
messageId,
|
|
25245
25802
|
senderJid: sender,
|
|
25246
|
-
timestamp:
|
|
25803
|
+
timestamp: timestamp7(parsed.timestamp, `${label}.timestamp`),
|
|
25247
25804
|
fromMe: bool3(parsed.fromMe, `${label}.fromMe`),
|
|
25248
25805
|
chatKind: parsed.chatKind
|
|
25249
25806
|
};
|
|
@@ -25252,9 +25809,9 @@ function interactionAnchor(item) {
|
|
|
25252
25809
|
return createHash9("sha256").update(item.rowid).update("\x00").update(item.chatJid).update("\x00").update(item.messageId).digest("hex");
|
|
25253
25810
|
}
|
|
25254
25811
|
function parseInteractionPage(value, input, auth) {
|
|
25255
|
-
const parsed =
|
|
25812
|
+
const parsed = envelope2(value, input, auth);
|
|
25256
25813
|
const output = parsed.output;
|
|
25257
|
-
|
|
25814
|
+
exact5(output, ["provider", "operation", "accountSubject", "contactCollection", "projection", "projectionGeneration", "interactions", "nextCursor", "localInsertPageComplete", "checkpoint", "remoteHistoryComplete", "incompleteReasons"], "interaction output");
|
|
25258
25815
|
if (output.provider !== "whatsapp" || output.operation !== "contacts.list" || output.contactCollection !== "interactions" || output.projection !== "quiescent-account-bound-local-message-inserts" || output.remoteHistoryComplete !== false || stableJson6(output.incompleteReasons) !== stableJson6(["linked-device-history-coverage-unknown", "rowid-cursor-discovers-inserts-only"]) || input.collection !== "interactions") {
|
|
25259
25816
|
throw new Error("interaction projection semantics drifted");
|
|
25260
25817
|
}
|
|
@@ -25291,7 +25848,7 @@ function parseInteractionPage(value, input, auth) {
|
|
|
25291
25848
|
};
|
|
25292
25849
|
}
|
|
25293
25850
|
function ghostgetWhatsAppSnapshot(options = {}) {
|
|
25294
|
-
const auth =
|
|
25851
|
+
const auth = authId3(options.authId ?? DEFAULT_WHATSAPP_AUTH);
|
|
25295
25852
|
const contactPageSize = options.contactPageSize === undefined ? DEFAULT_WHATSAPP_CONTACT_PAGE_SIZE : integer6(options.contactPageSize, "contact page size", 1, 100);
|
|
25296
25853
|
const interactionPageSize = options.interactionPageSize === undefined ? DEFAULT_WHATSAPP_INTERACTION_PAGE_SIZE : integer6(options.interactionPageSize, "interaction page size", 1, 1000);
|
|
25297
25854
|
const supplied = options.checkpoint;
|
|
@@ -25377,47 +25934,47 @@ function generationSha256(value) {
|
|
|
25377
25934
|
return sha2569(stableJson6(value));
|
|
25378
25935
|
}
|
|
25379
25936
|
function ensureResource2(database, account, sourceRealmId, jid, name, basis, runId) {
|
|
25380
|
-
const existing =
|
|
25381
|
-
WHERE provider=? AND account_key=? AND resource_type='jid' AND resource_id=?`,
|
|
25937
|
+
const existing = getRow10(database, `SELECT id,person_id,source_realm_id FROM provider_resources
|
|
25938
|
+
WHERE provider=? AND account_key=? AND resource_type='jid' AND resource_id=?`, PROVIDER12, account, jid);
|
|
25382
25939
|
if (existing !== null) {
|
|
25383
25940
|
if (existing.source_realm_id !== null && existing.source_realm_id !== sourceRealmId) {
|
|
25384
25941
|
throw new Error("WhatsApp contact resource crossed its bound account realm");
|
|
25385
25942
|
}
|
|
25386
|
-
|
|
25943
|
+
run9(database, `UPDATE provider_resources SET source_realm_id=?,display_name=coalesce(?,display_name),active=1,last_seen_run_id=?,
|
|
25387
25944
|
metadata_json=? WHERE id=?`, sourceRealmId, name, runId, stableJson6({ jidKind: USER_JID.test(jid) ? "user" : "lid", displayNameBasis: basis }), existing.id);
|
|
25388
25945
|
if (name !== null)
|
|
25389
|
-
|
|
25946
|
+
run9(database, "UPDATE people SET display_name=coalesce(display_name,?) WHERE id=?", name, existing.person_id);
|
|
25390
25947
|
return { personId: existing.person_id, resourceId: existing.id, imported: false };
|
|
25391
25948
|
}
|
|
25392
|
-
const personId =
|
|
25949
|
+
const personId = insertedId10(run9(database, "INSERT INTO people(stable_key,display_name,metadata_json) VALUES (?,?,?)", `whatsapp:${sha2569(`${account}\x00${jid}`)}`, name, stableJson6({ createdBy: "exact-whatsapp-jid" })));
|
|
25393
25950
|
const phone = phoneForJid(jid);
|
|
25394
25951
|
if (phone !== null)
|
|
25395
|
-
|
|
25952
|
+
run9(database, `INSERT INTO contact_methods(
|
|
25396
25953
|
person_id,kind,value,normalized_value,label,confidence,first_seen_run_id,last_seen_run_id,metadata_json
|
|
25397
25954
|
) VALUES (?,'phone',?,?,'whatsapp','exact',?,?,?)`, personId, phone, phone, runId, runId, stableJson6({ derivedFromJid: true }));
|
|
25398
|
-
const resourceId =
|
|
25955
|
+
const resourceId = insertedId10(run9(database, `INSERT INTO provider_resources(
|
|
25399
25956
|
provider,account_key,source_realm_id,resource_type,resource_id,person_id,display_name,active,
|
|
25400
25957
|
first_seen_run_id,last_seen_run_id,metadata_json
|
|
25401
|
-
) VALUES (?,?,?,'jid',?,?,?,1,?,?,?)`,
|
|
25958
|
+
) VALUES (?,?,?,'jid',?,?,?,1,?,?,?)`, PROVIDER12, account, sourceRealmId, jid, personId, name, runId, runId, stableJson6({ jidKind: USER_JID.test(jid) ? "user" : "lid", displayNameBasis: basis })));
|
|
25402
25959
|
return { personId, resourceId, imported: true };
|
|
25403
25960
|
}
|
|
25404
25961
|
function parseStoredGeneration(value) {
|
|
25405
|
-
const parsed =
|
|
25406
|
-
|
|
25962
|
+
const parsed = record7(JSON.parse(value), "stored WhatsApp checkpoint metadata");
|
|
25963
|
+
exact5(parsed, ["generation", "coverage"], "stored WhatsApp checkpoint metadata");
|
|
25407
25964
|
if (parsed.coverage !== "local-insert-rowid-scan")
|
|
25408
25965
|
throw new Error("stored WhatsApp coverage drifted");
|
|
25409
25966
|
return generation(parsed.generation);
|
|
25410
25967
|
}
|
|
25411
25968
|
function databaseCheckpoint(database, auth) {
|
|
25412
|
-
const row =
|
|
25969
|
+
const row = getRow10(database, `SELECT checkpoint.cursor,checkpoint.cursor_anchor_sha256,checkpoint.metadata_json
|
|
25413
25970
|
FROM provider_accounts account JOIN provider_checkpoints checkpoint
|
|
25414
25971
|
ON checkpoint.provider=account.provider AND checkpoint.account_key=account.account_key
|
|
25415
|
-
WHERE account.provider=? AND account.auth_id=? AND checkpoint.stream_key=?`,
|
|
25972
|
+
WHERE account.provider=? AND account.auth_id=? AND checkpoint.stream_key=?`, PROVIDER12, auth, STREAM2);
|
|
25416
25973
|
return row === null ? { cursor: "0", anchor: null } : { cursor: row.cursor, anchor: row.cursor_anchor_sha256, generation: parseStoredGeneration(row.metadata_json) };
|
|
25417
25974
|
}
|
|
25418
25975
|
function refreshMetrics2(database, account, runId) {
|
|
25419
|
-
|
|
25420
|
-
|
|
25976
|
+
run9(database, "DELETE FROM interaction_metrics WHERE provider=? AND account_key=?", PROVIDER12, account);
|
|
25977
|
+
run9(database, `INSERT INTO interaction_metrics(
|
|
25421
25978
|
provider,account_key,person_id,sent_count,received_count,interaction_count,conversation_count,
|
|
25422
25979
|
first_interaction_at,last_interaction_at,reciprocal,completeness,last_seen_run_id,metadata_json
|
|
25423
25980
|
) SELECT ?,?,person_id,sum(direction='outgoing'),sum(direction='incoming'),count(*),
|
|
@@ -25425,11 +25982,11 @@ function refreshMetrics2(database, account, runId) {
|
|
|
25425
25982
|
CASE WHEN sum(direction='outgoing')>0 AND sum(direction='incoming')>0 THEN 1 ELSE 0 END,
|
|
25426
25983
|
'lower-bound',?,'{"coverage":"local-insert-rowid-scan"}'
|
|
25427
25984
|
FROM interaction_events WHERE provider=? AND account_key=? AND resolution='matched' AND person_id IS NOT NULL
|
|
25428
|
-
GROUP BY person_id`,
|
|
25985
|
+
GROUP BY person_id`, PROVIDER12, account, runId, PROVIDER12, account);
|
|
25429
25986
|
}
|
|
25430
25987
|
function resultFromJson2(value) {
|
|
25431
|
-
const parsed =
|
|
25432
|
-
|
|
25988
|
+
const parsed = record7(JSON.parse(value), "stored WhatsApp result");
|
|
25989
|
+
exact5(parsed, ["account_subject", "contacts_seen", "contacts_imported", "contacts_matched", "contacts_skipped_self", "messages_seen", "messages_inserted", "matched_messages", "skipped_messages", "checkpoint_cursor", "local_insert_scan_complete", "remote_history_complete", "counts_complete", "counts_lower_bound"], "stored WhatsApp result");
|
|
25433
25990
|
const numeric = ["contacts_seen", "contacts_imported", "contacts_matched", "contacts_skipped_self", "messages_seen", "messages_inserted", "matched_messages", "skipped_messages"];
|
|
25434
25991
|
for (const key of numeric)
|
|
25435
25992
|
integer6(parsed[key], `stored WhatsApp result.${key}`, 0, Number.MAX_SAFE_INTEGER);
|
|
@@ -25480,26 +26037,26 @@ function importWhatsAppSnapshot(database, snapshot) {
|
|
|
25480
26037
|
}));
|
|
25481
26038
|
database.exec("BEGIN IMMEDIATE");
|
|
25482
26039
|
try {
|
|
25483
|
-
const byAuth =
|
|
25484
|
-
const byAccount =
|
|
26040
|
+
const byAuth = getRow10(database, "SELECT account_key,auth_sha256 FROM provider_accounts WHERE provider=? AND auth_id=?", PROVIDER12, snapshot.authId);
|
|
26041
|
+
const byAccount = getRow10(database, "SELECT auth_id,auth_sha256 FROM provider_accounts WHERE provider=? AND account_key=?", PROVIDER12, account);
|
|
25485
26042
|
if (byAuth !== null && (byAuth.account_key !== account || byAuth.auth_sha256 !== firstExecution.authSha256) || byAccount !== null && (byAccount.auth_id !== snapshot.authId || byAccount.auth_sha256 !== firstExecution.authSha256)) {
|
|
25486
26043
|
throw new Error("WhatsApp durable auth-to-account binding drifted");
|
|
25487
26044
|
}
|
|
25488
26045
|
if (byAuth === null)
|
|
25489
|
-
|
|
25490
|
-
VALUES (?,?,?,?,?,?)`,
|
|
26046
|
+
run9(database, `INSERT INTO provider_accounts(provider,auth_id,account_key,auth_sha256,first_seen_at,last_seen_at)
|
|
26047
|
+
VALUES (?,?,?,?,?,?)`, PROVIDER12, snapshot.authId, account, firstExecution.authSha256, firstExecution.startedAt, lastExecution.finishedAt);
|
|
25491
26048
|
else
|
|
25492
|
-
|
|
26049
|
+
run9(database, "UPDATE provider_accounts SET last_seen_at=? WHERE provider=? AND auth_id=?", lastExecution.finishedAt, PROVIDER12, snapshot.authId);
|
|
25493
26050
|
const sourceRealmId = bindProviderAccountRealm(database, {
|
|
25494
|
-
provider:
|
|
26051
|
+
provider: PROVIDER12,
|
|
25495
26052
|
authId: snapshot.authId,
|
|
25496
26053
|
accountKey: account,
|
|
25497
26054
|
authSha256: firstExecution.authSha256,
|
|
25498
26055
|
firstSeenAt: firstExecution.startedAt,
|
|
25499
26056
|
lastSeenAt: lastExecution.finishedAt
|
|
25500
26057
|
}).id;
|
|
25501
|
-
const storedCheckpoint =
|
|
25502
|
-
FROM provider_checkpoints WHERE provider=? AND account_key=? AND stream_key=?`,
|
|
26058
|
+
const storedCheckpoint = getRow10(database, `SELECT cursor,cursor_anchor_sha256,generation_sha256,metadata_json
|
|
26059
|
+
FROM provider_checkpoints WHERE provider=? AND account_key=? AND stream_key=?`, PROVIDER12, account, STREAM2);
|
|
25503
26060
|
if (storedCheckpoint === null) {
|
|
25504
26061
|
if (snapshot.startCheckpoint.cursor !== "0" || snapshot.startCheckpoint.anchor !== null || snapshot.startCheckpoint.generation !== undefined)
|
|
25505
26062
|
throw new Error("WhatsApp initial scan must start at zero");
|
|
@@ -25509,8 +26066,8 @@ function importWhatsAppSnapshot(database, snapshot) {
|
|
|
25509
26066
|
throw new Error("WhatsApp durable checkpoint or message-store generation drifted");
|
|
25510
26067
|
}
|
|
25511
26068
|
}
|
|
25512
|
-
const existingRun =
|
|
25513
|
-
WHERE provider=? AND account_key=? AND mode='linked-device-sync' AND source_sha256=?`,
|
|
26069
|
+
const existingRun = getRow10(database, `SELECT result_json FROM source_runs
|
|
26070
|
+
WHERE provider=? AND account_key=? AND mode='linked-device-sync' AND source_sha256=?`, PROVIDER12, account, sourceDigest);
|
|
25514
26071
|
if (existingRun !== null) {
|
|
25515
26072
|
database.exec("COMMIT");
|
|
25516
26073
|
return resultFromJson2(existingRun.result_json);
|
|
@@ -25527,8 +26084,8 @@ function importWhatsAppSnapshot(database, snapshot) {
|
|
|
25527
26084
|
expectedContactsSkippedSelf += 1;
|
|
25528
26085
|
continue;
|
|
25529
26086
|
}
|
|
25530
|
-
const exists =
|
|
25531
|
-
WHERE provider=? AND account_key=? AND resource_type='jid' AND resource_id=?`,
|
|
26087
|
+
const exists = getRow10(database, `SELECT 1 FROM provider_resources
|
|
26088
|
+
WHERE provider=? AND account_key=? AND resource_type='jid' AND resource_id=?`, PROVIDER12, account, contact.providerId) !== null;
|
|
25532
26089
|
if (exists)
|
|
25533
26090
|
expectedContactsMatched += 1;
|
|
25534
26091
|
else
|
|
@@ -25561,11 +26118,11 @@ function importWhatsAppSnapshot(database, snapshot) {
|
|
|
25561
26118
|
counts_complete: false,
|
|
25562
26119
|
counts_lower_bound: true
|
|
25563
26120
|
};
|
|
25564
|
-
const runInsert =
|
|
26121
|
+
const runInsert = run9(database, `INSERT INTO source_runs(
|
|
25565
26122
|
provider,account_key,mode,source_sha256,source_locator_sha256,completeness,
|
|
25566
26123
|
result_json,started_at,completed_at
|
|
25567
|
-
) VALUES (?,?,'linked-device-sync',?,?,'lower-bound',?,?,?)`,
|
|
25568
|
-
const runId =
|
|
26124
|
+
) VALUES (?,?,'linked-device-sync',?,?,'lower-bound',?,?,?)`, PROVIDER12, account, sourceDigest, sha2569(snapshot.authId), stableJson6(expectedResult), firstExecution.startedAt, lastExecution.finishedAt);
|
|
26125
|
+
const runId = insertedId10(runInsert);
|
|
25569
26126
|
let ordinal = 0;
|
|
25570
26127
|
let contactsSeen = 0;
|
|
25571
26128
|
let contactsImported = 0;
|
|
@@ -25578,7 +26135,7 @@ function importWhatsAppSnapshot(database, snapshot) {
|
|
|
25578
26135
|
if (contact.providerId === owner) {
|
|
25579
26136
|
contactsSkippedSelf += 1;
|
|
25580
26137
|
const item2 = stableJson6({ type: "contact", ...contact, resolution: "skipped-self" });
|
|
25581
|
-
|
|
26138
|
+
run9(database, `INSERT INTO source_records(run_id,ordinal,record_kind,record_sha256,resolution,projection_json,error_text)
|
|
25582
26139
|
VALUES (?,?,'contact',?,'skipped',?,'owner-self-contact')`, runId, ordinal, sha2569(item2), item2);
|
|
25583
26140
|
continue;
|
|
25584
26141
|
}
|
|
@@ -25588,7 +26145,7 @@ function importWhatsAppSnapshot(database, snapshot) {
|
|
|
25588
26145
|
else
|
|
25589
26146
|
contactsMatched += 1;
|
|
25590
26147
|
const item = stableJson6({ type: "contact", ...contact });
|
|
25591
|
-
|
|
26148
|
+
run9(database, `INSERT INTO source_records(
|
|
25592
26149
|
run_id,ordinal,record_kind,record_sha256,person_id,provider_resource_id,resolution,projection_json
|
|
25593
26150
|
) VALUES (?,?,'contact',?,?,?,?,?)`, runId, ordinal, sha2569(item), identity.personId, identity.resourceId, identity.imported ? "imported" : "matched", item);
|
|
25594
26151
|
}
|
|
@@ -25632,13 +26189,13 @@ function importWhatsAppSnapshot(database, snapshot) {
|
|
|
25632
26189
|
reason
|
|
25633
26190
|
};
|
|
25634
26191
|
const evidenceJson = stableJson6(evidence);
|
|
25635
|
-
|
|
26192
|
+
run9(database, `INSERT INTO source_records(
|
|
25636
26193
|
run_id,ordinal,record_kind,record_sha256,person_id,provider_resource_id,resolution,projection_json,error_text
|
|
25637
26194
|
) VALUES (?,?,'message',?,?,?,?,?,?)`, runId, ordinal, sha2569(evidenceJson), personId, resourceId, resolution, evidenceJson, resolution === "matched" ? null : reason);
|
|
25638
|
-
|
|
26195
|
+
run9(database, `INSERT INTO interaction_events(
|
|
25639
26196
|
provider,account_key,external_id,person_id,occurred_at,direction,conversation_sha256,
|
|
25640
26197
|
event_kind,resolution,run_id,metadata_json
|
|
25641
|
-
) VALUES (?,?,?,?,?,?,?,'message',?,?,?)`,
|
|
26198
|
+
) VALUES (?,?,?,?,?,?,?,'message',?,?,?)`, PROVIDER12, account, `row:${message.rowid}`, personId, message.timestamp, direction, conversationHash, resolution, runId, stableJson6({ conversationKind, reason, messageIdSha256: sha2569(message.messageId) }));
|
|
25642
26199
|
messagesInserted += 1;
|
|
25643
26200
|
}
|
|
25644
26201
|
refreshMetrics2(database, account, runId);
|
|
@@ -25660,8 +26217,8 @@ function importWhatsAppSnapshot(database, snapshot) {
|
|
|
25660
26217
|
};
|
|
25661
26218
|
if (stableJson6(result) !== stableJson6(expectedResult))
|
|
25662
26219
|
throw new Error("WhatsApp applied counts drifted from the reviewed snapshot");
|
|
25663
|
-
const
|
|
25664
|
-
|
|
26220
|
+
const metadata4 = stableJson6({ generation: scanGeneration, coverage: "local-insert-rowid-scan" });
|
|
26221
|
+
run9(database, `INSERT INTO provider_checkpoints(
|
|
25665
26222
|
provider,account_key,source_realm_id,stream_key,source_locator_sha256,schema_sha256,generation_sha256,
|
|
25666
26223
|
cursor,cursor_anchor_sha256,last_run_id,metadata_json
|
|
25667
26224
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
|
@@ -25669,7 +26226,7 @@ function importWhatsAppSnapshot(database, snapshot) {
|
|
|
25669
26226
|
source_realm_id=excluded.source_realm_id,source_locator_sha256=excluded.source_locator_sha256,schema_sha256=excluded.schema_sha256,
|
|
25670
26227
|
generation_sha256=excluded.generation_sha256,cursor=excluded.cursor,
|
|
25671
26228
|
cursor_anchor_sha256=excluded.cursor_anchor_sha256,last_run_id=excluded.last_run_id,
|
|
25672
|
-
metadata_json=excluded.metadata_json,updated_at=CURRENT_TIMESTAMP`,
|
|
26229
|
+
metadata_json=excluded.metadata_json,updated_at=CURRENT_TIMESTAMP`, PROVIDER12, account, sourceRealmId, STREAM2, sha2569(snapshot.authId), CONTACTS_CONTRACT_SHA2562, generationSha256(scanGeneration), end2.cursor, end2.anchor, runId, metadata4);
|
|
25673
26230
|
database.exec("COMMIT");
|
|
25674
26231
|
return result;
|
|
25675
26232
|
} catch (error) {
|
|
@@ -25678,13 +26235,13 @@ function importWhatsAppSnapshot(database, snapshot) {
|
|
|
25678
26235
|
}
|
|
25679
26236
|
}
|
|
25680
26237
|
function syncWhatsAppRelationships(database, options = {}) {
|
|
25681
|
-
const auth =
|
|
26238
|
+
const auth = authId3(options.authId ?? DEFAULT_WHATSAPP_AUTH);
|
|
25682
26239
|
if (options.checkpoint !== undefined)
|
|
25683
26240
|
throw new Error("sync checkpoint is database-owned");
|
|
25684
26241
|
const snapshot = ghostgetWhatsAppSnapshot({ ...options, authId: auth, checkpoint: databaseCheckpoint(database, auth) });
|
|
25685
26242
|
for (const page of [...snapshot.contactPages, ...snapshot.interactionPages]) {
|
|
25686
26243
|
ledgerPeopleBladeGhostgetInvocation(database, {
|
|
25687
|
-
provider:
|
|
26244
|
+
provider: PROVIDER12,
|
|
25688
26245
|
accountKey: snapshot.accountSubject,
|
|
25689
26246
|
capability: "contact_interactions",
|
|
25690
26247
|
execution: page.execution,
|
|
@@ -25698,7 +26255,7 @@ function syncWhatsAppRelationships(database, options = {}) {
|
|
|
25698
26255
|
}
|
|
25699
26256
|
|
|
25700
26257
|
// src/cli/version.ts
|
|
25701
|
-
var peoplebladeVersion = "0.
|
|
26258
|
+
var peoplebladeVersion = "0.3.3";
|
|
25702
26259
|
|
|
25703
26260
|
// src/cli/main.ts
|
|
25704
26261
|
var usage = `PeopleBlade \u2014 local-first contact intelligence
|
|
@@ -25752,6 +26309,9 @@ Sources (append --auto-accept to run identity auto-accept after a successful imp
|
|
|
25752
26309
|
x mutuals sync Sync X mutuals through Ghostget (requires reviewed Ghostget capability)
|
|
25753
26310
|
whatsapp sync [--auth ID] Sync WhatsApp through Ghostget's linked-device projection
|
|
25754
26311
|
linkedin import ZIP Import an official LinkedIn data-export ZIP
|
|
26312
|
+
linkedin contact-info --person-id ID
|
|
26313
|
+
Read one first-degree connection's Contact info through Ghostget
|
|
26314
|
+
[--account KEY] [--auth ID]
|
|
25755
26315
|
linkedin stats Show aggregate LinkedIn import statistics
|
|
25756
26316
|
instagram import ZIP Import an official Instagram JSON archive
|
|
25757
26317
|
instagram stats Show aggregate Instagram import statistics
|
|
@@ -26481,14 +27041,14 @@ ${verification}`) });
|
|
|
26481
27041
|
}
|
|
26482
27042
|
if (command === "beeper" && subcommand === "sync") {
|
|
26483
27043
|
const options = [...rest];
|
|
26484
|
-
const
|
|
27044
|
+
const authId4 = valueAfter(options, "--auth");
|
|
26485
27045
|
const contactLimit = positive(valueAfter(options, "--contact-limit"), "--contact-limit", 200, 200);
|
|
26486
27046
|
const withRelationships = options.includes("--with-relationships");
|
|
26487
27047
|
if (withRelationships)
|
|
26488
27048
|
options.splice(options.indexOf("--with-relationships"), 1);
|
|
26489
27049
|
if (options.length)
|
|
26490
27050
|
fail3(`Unknown argument: ${options[0]}`);
|
|
26491
|
-
const effectiveAuthId =
|
|
27051
|
+
const effectiveAuthId = authId4 ?? DEFAULT_BEEPER_AUTH;
|
|
26492
27052
|
if (withRelationships)
|
|
26493
27053
|
console.error("Beeper relationships \xB7 deriving a body-free interaction summary through Ghostget");
|
|
26494
27054
|
console.error("Beeper contacts \xB7 discovering connected accounts through Ghostget");
|
|
@@ -26510,7 +27070,7 @@ ${verification}`) });
|
|
|
26510
27070
|
}
|
|
26511
27071
|
if (command === "beeper" && subcommand === "rebind") {
|
|
26512
27072
|
const options = [...rest];
|
|
26513
|
-
const
|
|
27073
|
+
const authId4 = valueAfter(options, "--auth");
|
|
26514
27074
|
const confirm = options.includes("--confirm");
|
|
26515
27075
|
if (confirm)
|
|
26516
27076
|
options.splice(options.indexOf("--confirm"), 1);
|
|
@@ -26519,14 +27079,14 @@ ${verification}`) });
|
|
|
26519
27079
|
if (!confirm)
|
|
26520
27080
|
fail3("beeper rebind requires --confirm after reviewing the current Ghostget binding, release, and connected-account inventory.");
|
|
26521
27081
|
console.error("Beeper binding \xB7 verifying the complete connected-account inventory before an append-only transition");
|
|
26522
|
-
print(rebindBeeperSource(database, { ...
|
|
27082
|
+
print(rebindBeeperSource(database, { ...authId4 === undefined ? {} : { authId: authId4 } }), asJson);
|
|
26523
27083
|
return;
|
|
26524
27084
|
}
|
|
26525
27085
|
if (command === "beeper" && subcommand === "search") {
|
|
26526
27086
|
const options = [...rest];
|
|
26527
27087
|
const queries = valuesAfter(options, "--query");
|
|
26528
27088
|
const services = valuesAfter(options, "--service");
|
|
26529
|
-
const
|
|
27089
|
+
const authId4 = valueAfter(options, "--auth");
|
|
26530
27090
|
const limit = positive(valueAfter(options, "--limit"), "--limit", 20, 20);
|
|
26531
27091
|
if (queries.length === 0)
|
|
26532
27092
|
fail3("beeper search requires one or more --query values.");
|
|
@@ -26535,18 +27095,18 @@ ${verification}`) });
|
|
|
26535
27095
|
print(backfillBeeperSearch(database, {
|
|
26536
27096
|
queries,
|
|
26537
27097
|
...services.length === 0 ? {} : { services },
|
|
26538
|
-
...
|
|
27098
|
+
...authId4 === undefined ? {} : { authId: authId4 },
|
|
26539
27099
|
limit
|
|
26540
27100
|
}), asJson);
|
|
26541
27101
|
return;
|
|
26542
27102
|
}
|
|
26543
27103
|
if (command === "google" && subcommand === "sync") {
|
|
26544
27104
|
const options = [...rest];
|
|
26545
|
-
const
|
|
27105
|
+
const authId4 = valueAfter(options, "--auth");
|
|
26546
27106
|
const pageSize = positive(valueAfter(options, "--page-size"), "--page-size", 100, 100);
|
|
26547
27107
|
if (options.length)
|
|
26548
27108
|
fail3(`Unknown argument: ${options[0]}`);
|
|
26549
|
-
printImport(syncGoogleContacts(database, { ...
|
|
27109
|
+
printImport(syncGoogleContacts(database, { ...authId4 === undefined ? {} : { authId: authId4 }, pageSize }), asJson);
|
|
26550
27110
|
return;
|
|
26551
27111
|
}
|
|
26552
27112
|
if (command === "google" && subcommand === "stats") {
|
|
@@ -26571,13 +27131,13 @@ ${verification}`) });
|
|
|
26571
27131
|
}
|
|
26572
27132
|
if (command === "whatsapp" && subcommand === "sync") {
|
|
26573
27133
|
const options = [...rest];
|
|
26574
|
-
const
|
|
27134
|
+
const authId4 = valueAfter(options, "--auth");
|
|
26575
27135
|
const contactPageSize = positive(valueAfter(options, "--contact-page-size"), "--contact-page-size", 100, 100);
|
|
26576
27136
|
const interactionPageSize = positive(valueAfter(options, "--interaction-page-size"), "--interaction-page-size", 1000, 1000);
|
|
26577
27137
|
if (options.length)
|
|
26578
27138
|
fail3(`Unknown argument: ${options[0]}`);
|
|
26579
27139
|
printImport(syncWhatsAppRelationships(database, {
|
|
26580
|
-
...
|
|
27140
|
+
...authId4 === undefined ? {} : { authId: authId4 },
|
|
26581
27141
|
contactPageSize,
|
|
26582
27142
|
interactionPageSize
|
|
26583
27143
|
}), asJson);
|
|
@@ -26598,6 +27158,25 @@ ${verification}`) });
|
|
|
26598
27158
|
}), asJson);
|
|
26599
27159
|
return;
|
|
26600
27160
|
}
|
|
27161
|
+
if (command === "linkedin" && subcommand === "contact-info") {
|
|
27162
|
+
const options = [...rest];
|
|
27163
|
+
const personText = valueAfter(options, "--person-id");
|
|
27164
|
+
if (personText === undefined)
|
|
27165
|
+
fail3("linkedin contact-info requires --person-id ID.");
|
|
27166
|
+
const personId = Number(personText);
|
|
27167
|
+
if (!Number.isSafeInteger(personId) || personId < 1)
|
|
27168
|
+
fail3("--person-id must be a positive integer.");
|
|
27169
|
+
const accountKey5 = valueAfter(options, "--account");
|
|
27170
|
+
const authId4 = valueAfter(options, "--auth");
|
|
27171
|
+
if (options.length)
|
|
27172
|
+
fail3(`Unknown argument: ${options[0]}`);
|
|
27173
|
+
print(readLinkedInContactInfo(database, {
|
|
27174
|
+
personId,
|
|
27175
|
+
...accountKey5 === undefined ? {} : { accountKey: accountKey5 },
|
|
27176
|
+
...authId4 === undefined ? {} : { authId: authId4 }
|
|
27177
|
+
}), asJson);
|
|
27178
|
+
return;
|
|
27179
|
+
}
|
|
26601
27180
|
if (command === "linkedin" && subcommand === "stats") {
|
|
26602
27181
|
if (rest.length)
|
|
26603
27182
|
fail3(`Unknown argument: ${rest[0]}`);
|