@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.
@@ -4313,9 +4313,9 @@ var peoplebladeCapabilities = Object.freeze({
4313
4313
  { id: "apple-contacts", command: "contacts sync", status: "supported", transport: "local-database", requirement: "macOS Contacts access" },
4314
4314
  { id: "imessage", command: "imessage sync", status: "supported", transport: "local-metadata", requirement: "macOS Messages access; no message bodies" },
4315
4315
  { id: "google-contacts", command: "google sync", status: "supported", transport: "ghostget", requirement: "Reviewed Gmail/People contacts.list and contact scopes" },
4316
- { id: "beeper", command: "beeper sync", status: "supported", transport: "ghostget", requirement: "Ghostget 0.17.1, Beeper adapter 2.4.0, CLI 0.6.2; body-free relationships require macOS arm64" },
4316
+ { id: "beeper", command: "beeper sync", status: "supported", transport: "ghostget", requirement: "Ghostget 0.17.3, Beeper adapter 2.4.0, CLI 0.6.2; body-free relationships require macOS arm64" },
4317
4317
  { id: "whatsapp", command: "whatsapp sync", status: "supported", transport: "ghostget", requirement: "Reviewed linked-device contacts.list v2" },
4318
- { id: "linkedin", command: "linkedin import", status: "supported", transport: "official-archive", requirement: "Owner-selected official ZIP; no authenticated browser collection" },
4318
+ { id: "linkedin", command: "linkedin import", status: "supported", transport: "official-archive", requirement: "Owner-selected official ZIP; connection lists are not collected from a signed-in browser" },
4319
4319
  { id: "instagram", command: "instagram import", status: "supported", transport: "official-archive", requirement: "Owner-selected JSON ZIP and exact owner username" },
4320
4320
  { id: "x-archive", command: "x import", status: "bounded", transport: "official-archive", requirement: "Owner-selected ZIP; unresolved IDs remain inert" },
4321
4321
  { id: "x-live", command: "x mutuals sync", status: "blocked", transport: "unavailable", requirement: "A reviewed bounded Ghostget mutuals capability is not yet available" },
@@ -4323,9 +4323,10 @@ var peoplebladeCapabilities = Object.freeze({
4323
4323
  ],
4324
4324
  enrichment: [
4325
4325
  { id: "local-anchors", priority: 1, status: "supported", commands: ["identity audit", "identity suggest", "identity decide"], paid: false, boundary: "Exact source evidence first; joins require reversible evidence-bound decisions" },
4326
- { id: "manual-public-research", priority: 2, status: "supported", commands: ["research prepare", "research apply"], paid: false, boundary: "Agent or human supplies cited public evidence; PeopleBlade performs no provider calls" },
4327
- { id: "cloud-professional-research", priority: 3, status: "optional", commands: ["cloud enrich --person-id ID", "cloud enrich --confirm TOKEN"], paid: true, boundary: "Exa exact profiles or bounded search, then cited model extraction; device-bound preview approval required" },
4328
- { id: "cloud-email-fallback", priority: 4, status: "optional", commands: ["cloud enrich --person-id ID"], paid: true, boundary: "Hunter is a separately configured narrow fallback, not a substitute for identity evidence" }
4326
+ { id: "linkedin-contact-info", priority: 2, status: "supported", commands: ["linkedin contact-info --person-id ID"], paid: false, boundary: "Reviewed Ghostget linkedin-web contacts.read for one exact first-degree connection already imported from the official export; signed-in browser profile, one bounded read, no connect, message, or feed access" },
4327
+ { id: "manual-public-research", priority: 3, status: "supported", commands: ["research prepare", "research apply"], paid: false, boundary: "Agent or human supplies cited public evidence; PeopleBlade performs no provider calls" },
4328
+ { id: "cloud-professional-research", priority: 4, status: "optional", commands: ["cloud enrich --person-id ID", "cloud enrich --confirm TOKEN"], paid: true, boundary: "Exa exact profiles or bounded search, then cited model extraction; device-bound preview approval required" },
4329
+ { id: "cloud-email-fallback", priority: 5, status: "optional", commands: ["cloud enrich --person-id ID"], paid: true, boundary: "Hunter is a separately configured narrow fallback, not a substitute for identity evidence" }
4329
4330
  ],
4330
4331
  interfaces: {
4331
4332
  workspace: { command: "ui [--port N]", availability: "included-in-this-build", binding: "127.0.0.1", defaultPort: "random", authentication: "per-process capability file", remoteAssets: false, noteUpload: false },
@@ -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{color-scheme:light dark;--bg:#f6f5f1;--panel:#fffefa;--text:#232721;--muted:#647064;--line:#dedfd6;--accent:#375a42;--soft:#edf1e8;--danger:#9c342d}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.55 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}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(--accent);background:var(--soft)}button:disabled{opacity:.45;cursor:default}a{color:var(--accent)}:focus-visible{outline:3px solid var(--accent);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(--accent);color:var(--panel)}.primary:hover{background:var(--accent);color:var(--panel)}.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}
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-471pfpgn.js
13701
- var GHOSTGET_VERSION = "0.17.1";
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.1",
18687
- commit: "b74ae2ac594bcf07c3def71dc4bb2e5a98c826dd"
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 === currentMetadata)
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 PROVIDER11 = "whatsapp";
25573
+ var PROVIDER12 = "whatsapp";
25017
25574
  var STREAM2 = "messages";
25018
- function getRow9(database, sql, ...values) {
25575
+ function getRow10(database, sql, ...values) {
25019
25576
  return database.query(sql).get(...values);
25020
25577
  }
25021
- function run8(database, sql, ...values) {
25578
+ function run9(database, sql, ...values) {
25022
25579
  return database.run(sql, values);
25023
25580
  }
25024
- function insertedId9(value) {
25581
+ function insertedId10(value) {
25025
25582
  return Number(value.lastInsertRowid);
25026
25583
  }
25027
- function record6(value, label) {
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 exact4(value, keys, label) {
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 text6(value, label, maximum = 4096, empty = false) {
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 = text6(value, label, 64);
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 = record6(value, "canonical JSON");
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 timestamp6(value, label) {
25085
- const parsed = text6(value, label, 40);
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 = text6(value, label, 19);
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 authId2(value) {
25100
- const parsed = text6(value, "WhatsApp auth ID", 48);
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 = text6(value, "WhatsApp account subject", 64);
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 = text6(value, label, 96);
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 envelope(value, input, auth) {
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: record6(parsed.output, "Ghostget output"), execution: parsed.execution };
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 = record6(value, label);
25152
- exact4(parsed, CONTACT_KEYS, label);
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 : text6(parsed.phone, `${label}.phone`, 20);
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 : text6(parsed.displayName, `${label}.displayName`, 512);
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
- text6(parsed[key], `${label}.${key}`, 512);
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 = envelope(value, input, auth);
25735
+ const parsed = envelope2(value, input, auth);
25179
25736
  const output = parsed.output;
25180
- exact4(output, ["provider", "operation", "accountSubject", "projection", "contacts", "nextCursor", "localContactTablePageComplete", "remoteContactSetComplete", "contactSetIncompleteReasons", "statsScope", "statsCompleteness"], "contact output");
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 = record6(value, "projection generation");
25206
- exact4(parsed, ["messageStoreIdentity", "schemaFingerprint"], "projection generation");
25207
- const identity = record6(parsed.messageStoreIdentity, "message-store identity");
25208
- exact4(identity, ["dev", "ino"], "message-store identity");
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 = record6(value, label);
25217
- exact4(parsed, ["cursor", "anchor"], label);
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 = record6(value, label);
25226
- exact4(parsed, ["rowid", "chatJid", "messageId", "senderJid", "timestamp", "fromMe", "chatKind"], label);
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 = text6(parsed.messageId, `${label}.messageId`, 256);
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: timestamp6(parsed.timestamp, `${label}.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 = envelope(value, input, auth);
25812
+ const parsed = envelope2(value, input, auth);
25256
25813
  const output = parsed.output;
25257
- exact4(output, ["provider", "operation", "accountSubject", "contactCollection", "projection", "projectionGeneration", "interactions", "nextCursor", "localInsertPageComplete", "checkpoint", "remoteHistoryComplete", "incompleteReasons"], "interaction output");
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 = authId2(options.authId ?? DEFAULT_WHATSAPP_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 = getRow9(database, `SELECT id,person_id,source_realm_id FROM provider_resources
25381
- WHERE provider=? AND account_key=? AND resource_type='jid' AND resource_id=?`, PROVIDER11, account, jid);
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
- run8(database, `UPDATE provider_resources SET source_realm_id=?,display_name=coalesce(?,display_name),active=1,last_seen_run_id=?,
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
- run8(database, "UPDATE people SET display_name=coalesce(display_name,?) WHERE id=?", name, existing.person_id);
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 = insertedId9(run8(database, "INSERT INTO people(stable_key,display_name,metadata_json) VALUES (?,?,?)", `whatsapp:${sha2569(`${account}\x00${jid}`)}`, name, stableJson6({ createdBy: "exact-whatsapp-jid" })));
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
- run8(database, `INSERT INTO contact_methods(
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 = insertedId9(run8(database, `INSERT INTO provider_resources(
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,?,?,?)`, PROVIDER11, account, sourceRealmId, jid, personId, name, runId, runId, stableJson6({ jidKind: USER_JID.test(jid) ? "user" : "lid", displayNameBasis: basis })));
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 = record6(JSON.parse(value), "stored WhatsApp checkpoint metadata");
25406
- exact4(parsed, ["generation", "coverage"], "stored WhatsApp checkpoint metadata");
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 = getRow9(database, `SELECT checkpoint.cursor,checkpoint.cursor_anchor_sha256,checkpoint.metadata_json
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=?`, PROVIDER11, auth, STREAM2);
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
- run8(database, "DELETE FROM interaction_metrics WHERE provider=? AND account_key=?", PROVIDER11, account);
25420
- run8(database, `INSERT INTO interaction_metrics(
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`, PROVIDER11, account, runId, PROVIDER11, account);
25985
+ GROUP BY person_id`, PROVIDER12, account, runId, PROVIDER12, account);
25429
25986
  }
25430
25987
  function resultFromJson2(value) {
25431
- const parsed = record6(JSON.parse(value), "stored WhatsApp result");
25432
- exact4(parsed, ["account_subject", "contacts_seen", "contacts_imported", "contacts_matched", "contacts_skipped_self", "messages_seen", "messages_inserted", "matched_messages", "skipped_messages", "checkpoint_cursor", "local_insert_scan_complete", "remote_history_complete", "counts_complete", "counts_lower_bound"], "stored WhatsApp result");
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 = getRow9(database, "SELECT account_key,auth_sha256 FROM provider_accounts WHERE provider=? AND auth_id=?", PROVIDER11, snapshot.authId);
25484
- const byAccount = getRow9(database, "SELECT auth_id,auth_sha256 FROM provider_accounts WHERE provider=? AND account_key=?", PROVIDER11, account);
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
- run8(database, `INSERT INTO provider_accounts(provider,auth_id,account_key,auth_sha256,first_seen_at,last_seen_at)
25490
- VALUES (?,?,?,?,?,?)`, PROVIDER11, snapshot.authId, account, firstExecution.authSha256, firstExecution.startedAt, lastExecution.finishedAt);
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
- run8(database, "UPDATE provider_accounts SET last_seen_at=? WHERE provider=? AND auth_id=?", lastExecution.finishedAt, PROVIDER11, snapshot.authId);
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: PROVIDER11,
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 = getRow9(database, `SELECT cursor,cursor_anchor_sha256,generation_sha256,metadata_json
25502
- FROM provider_checkpoints WHERE provider=? AND account_key=? AND stream_key=?`, PROVIDER11, account, STREAM2);
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 = getRow9(database, `SELECT result_json FROM source_runs
25513
- WHERE provider=? AND account_key=? AND mode='linked-device-sync' AND source_sha256=?`, PROVIDER11, account, sourceDigest);
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 = getRow9(database, `SELECT 1 FROM provider_resources
25531
- WHERE provider=? AND account_key=? AND resource_type='jid' AND resource_id=?`, PROVIDER11, account, contact.providerId) !== null;
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 = run8(database, `INSERT INTO source_runs(
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',?,?,?)`, PROVIDER11, account, sourceDigest, sha2569(snapshot.authId), stableJson6(expectedResult), firstExecution.startedAt, lastExecution.finishedAt);
25568
- const runId = insertedId9(runInsert);
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
- run8(database, `INSERT INTO source_records(run_id,ordinal,record_kind,record_sha256,resolution,projection_json,error_text)
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
- run8(database, `INSERT INTO source_records(
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
- run8(database, `INSERT INTO source_records(
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
- run8(database, `INSERT INTO interaction_events(
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',?,?,?)`, PROVIDER11, account, `row:${message.rowid}`, personId, message.timestamp, direction, conversationHash, resolution, runId, stableJson6({ conversationKind, reason, messageIdSha256: sha2569(message.messageId) }));
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 metadata3 = stableJson6({ generation: scanGeneration, coverage: "local-insert-rowid-scan" });
25664
- run8(database, `INSERT INTO provider_checkpoints(
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`, PROVIDER11, account, sourceRealmId, STREAM2, sha2569(snapshot.authId), CONTACTS_CONTRACT_SHA2562, generationSha256(scanGeneration), end2.cursor, end2.anchor, runId, metadata3);
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 = authId2(options.authId ?? DEFAULT_WHATSAPP_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: PROVIDER11,
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.2.2";
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 authId3 = valueAfter(options, "--auth");
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 = authId3 ?? DEFAULT_BEEPER_AUTH;
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 authId3 = valueAfter(options, "--auth");
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, { ...authId3 === undefined ? {} : { authId: authId3 } }), asJson);
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 authId3 = valueAfter(options, "--auth");
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
- ...authId3 === undefined ? {} : { authId: authId3 },
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 authId3 = valueAfter(options, "--auth");
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, { ...authId3 === undefined ? {} : { authId: authId3 }, pageSize }), asJson);
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 authId3 = valueAfter(options, "--auth");
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
- ...authId3 === undefined ? {} : { authId: authId3 },
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]}`);