@ainyc/canonry 4.33.1 → 4.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ import {
5
5
  loadConfig,
6
6
  loadConfigRaw,
7
7
  saveConfigPatch
8
- } from "./chunk-5EBN7736.js";
8
+ } from "./chunk-B3FBOECD.js";
9
9
  import {
10
10
  DEFAULT_RUN_HISTORY_LIMIT,
11
11
  IntelligenceService,
@@ -39,11 +39,13 @@ import {
39
39
  ccReleaseSyncs,
40
40
  competitors,
41
41
  crawlerEventsHourly,
42
+ createClient,
42
43
  createLogger,
43
44
  discoveryProbes,
44
45
  discoverySessions,
45
46
  dropLegacyCredentialColumns,
46
47
  extractLegacyCredentials,
48
+ filterTrackedSnapshots,
47
49
  gaAiReferrals,
48
50
  gaSocialReferrals,
49
51
  gaTrafficSnapshots,
@@ -59,6 +61,7 @@ import {
59
61
  isBlogShapedQuery,
60
62
  isTrendBaseline,
61
63
  mapOpportunitiesToNextSteps,
64
+ migrate,
62
65
  notifications,
63
66
  parseJsonColumn,
64
67
  pickGroupRepresentative,
@@ -70,7 +73,7 @@ import {
70
73
  schedules,
71
74
  trafficSources,
72
75
  usageCounters
73
- } from "./chunk-BJXHETQW.js";
76
+ } from "./chunk-MLS5KJWK.js";
74
77
  import {
75
78
  AGENT_MEMORY_VALUE_MAX_BYTES,
76
79
  AGENT_PROVIDER_IDS,
@@ -89,6 +92,7 @@ import {
89
92
  DiscoveryCompetitorTypes,
90
93
  DiscoverySessionStatuses,
91
94
  MemorySources,
95
+ ProviderNames,
92
96
  RunKinds,
93
97
  RunStatuses,
94
98
  RunTriggers,
@@ -126,6 +130,7 @@ import {
126
130
  discoveryBucketSchema,
127
131
  discoveryPromoteRequestSchema,
128
132
  discoveryRunRequestSchema,
133
+ effectiveBrandNames,
129
134
  effectiveDomains,
130
135
  emptyCitationVisibility,
131
136
  extractAnswerMentions,
@@ -143,7 +148,9 @@ import {
143
148
  isBrowserProvider,
144
149
  keywordGenerateRequestSchema,
145
150
  locationContextSchema,
151
+ mentionStateFromAnswerMentioned,
146
152
  missingDependency,
153
+ normalizeProjectAliases,
147
154
  normalizeProjectDomain,
148
155
  normalizeUrlPath,
149
156
  notFound,
@@ -179,7 +186,7 @@ import {
179
186
  visibilityStateFromAnswerMentioned,
180
187
  windowCutoff,
181
188
  wordpressEnvSchema
182
- } from "./chunk-XW3F5EEW.js";
189
+ } from "./chunk-EM5GVF3C.js";
183
190
 
184
191
  // src/telemetry.ts
185
192
  import crypto from "crypto";
@@ -332,13 +339,146 @@ function trackEvent(event, properties, options) {
332
339
  }).finally(() => clearTimeout(timeout));
333
340
  }
334
341
 
342
+ // src/update-check.ts
343
+ import { createRequire as createRequire2 } from "module";
344
+ var _require2 = createRequire2(import.meta.url);
345
+ var { version: PKG_VERSION } = _require2("../package.json");
346
+ var PKG_NAME = "@ainyc/canonry";
347
+ var NPM_DIST_TAGS_URL = `https://registry.npmjs.org/-/package/${PKG_NAME}/dist-tags`;
348
+ var NPM_PACKAGE_URL = `https://www.npmjs.com/package/${PKG_NAME}`;
349
+ var FETCH_TIMEOUT_MS = 1500;
350
+ function isUpdateCheckEnabled() {
351
+ if (process.env.CANONRY_DISABLE_UPDATE_CHECK === "1") return false;
352
+ if (process.env.DO_NOT_TRACK === "1") return false;
353
+ if (process.env.CI) return false;
354
+ if (!configExists()) return true;
355
+ try {
356
+ const raw = loadConfigRaw();
357
+ return raw?.updateCheck !== false;
358
+ } catch {
359
+ return true;
360
+ }
361
+ }
362
+ function compareSemver(a, b) {
363
+ const parse = (v) => {
364
+ const core = v.split(/[-+]/)[0];
365
+ if (!core) return null;
366
+ const parts = core.split(".");
367
+ if (parts.length < 3) return null;
368
+ const nums = [];
369
+ for (let i = 0; i < 3; i++) {
370
+ const n = Number(parts[i]);
371
+ if (!Number.isInteger(n) || n < 0) return null;
372
+ nums.push(n);
373
+ }
374
+ return [nums[0], nums[1], nums[2]];
375
+ };
376
+ const pa = parse(a);
377
+ const pb = parse(b);
378
+ if (!pa || !pb) return 0;
379
+ for (let i = 0; i < 3; i++) {
380
+ if (pa[i] > pb[i]) return 1;
381
+ if (pa[i] < pb[i]) return -1;
382
+ }
383
+ return 0;
384
+ }
385
+ async function fetchLatestVersion(opts) {
386
+ const controller = new AbortController();
387
+ const timeout = setTimeout(() => controller.abort(), opts?.timeoutMs ?? FETCH_TIMEOUT_MS);
388
+ timeout.unref();
389
+ try {
390
+ const res = await fetch(NPM_DIST_TAGS_URL, {
391
+ signal: controller.signal,
392
+ headers: { accept: "application/json" }
393
+ });
394
+ if (!res.ok) return null;
395
+ const data = await res.json();
396
+ if (typeof data.latest !== "string") return null;
397
+ return data.latest;
398
+ } catch {
399
+ return null;
400
+ } finally {
401
+ clearTimeout(timeout);
402
+ }
403
+ }
404
+ function buildUpdateAvailable(current, latest) {
405
+ if (compareSemver(latest, current) <= 0) return null;
406
+ return {
407
+ current,
408
+ latest,
409
+ url: NPM_PACKAGE_URL,
410
+ upgradeCommand: `npm install -g ${PKG_NAME}`
411
+ };
412
+ }
413
+ async function checkLatestVersionForCli(opts) {
414
+ if (!isUpdateCheckEnabled()) return null;
415
+ if (!configExists()) return null;
416
+ const now = opts?.now ? opts.now() : /* @__PURE__ */ new Date();
417
+ const ttlMs = (opts?.ttlHours ?? 24) * 60 * 60 * 1e3;
418
+ let raw;
419
+ try {
420
+ raw = loadConfigRaw();
421
+ } catch {
422
+ return null;
423
+ }
424
+ if (!raw) return null;
425
+ const lastCheckedAt = raw.lastUpdateCheckAt ? Date.parse(raw.lastUpdateCheckAt) : NaN;
426
+ const cachedLatest = typeof raw.lastKnownLatestVersion === "string" ? raw.lastKnownLatestVersion : void 0;
427
+ if (Number.isFinite(lastCheckedAt) && now.getTime() - lastCheckedAt < ttlMs) {
428
+ if (!cachedLatest) return null;
429
+ return buildUpdateAvailable(PKG_VERSION, cachedLatest);
430
+ }
431
+ const latest = await fetchLatestVersion();
432
+ if (!latest) {
433
+ try {
434
+ saveConfigPatch({ lastUpdateCheckAt: now.toISOString() });
435
+ } catch {
436
+ }
437
+ return null;
438
+ }
439
+ try {
440
+ saveConfigPatch({
441
+ lastUpdateCheckAt: now.toISOString(),
442
+ lastKnownLatestVersion: latest
443
+ });
444
+ } catch {
445
+ }
446
+ return buildUpdateAvailable(PKG_VERSION, latest);
447
+ }
448
+ var memoryCache = null;
449
+ var inFlight = null;
450
+ function startBackgroundRefresh(getNow) {
451
+ if (inFlight) return;
452
+ inFlight = (async () => {
453
+ try {
454
+ const latest = await fetchLatestVersion();
455
+ memoryCache = { fetchedAt: getNow(), latest };
456
+ } catch {
457
+ memoryCache = { fetchedAt: getNow(), latest: null };
458
+ } finally {
459
+ inFlight = null;
460
+ }
461
+ })();
462
+ }
463
+ function checkLatestVersionForServer(opts) {
464
+ if (!isUpdateCheckEnabled()) return null;
465
+ const getNow = opts?.now ?? Date.now;
466
+ const ttl = opts?.ttlMs ?? 60 * 60 * 1e3;
467
+ const now = getNow();
468
+ if (!memoryCache || now - memoryCache.fetchedAt >= ttl) {
469
+ startBackgroundRefresh(getNow);
470
+ }
471
+ if (!memoryCache || !memoryCache.latest) return null;
472
+ return buildUpdateAvailable(PKG_VERSION, memoryCache.latest);
473
+ }
474
+
335
475
  // src/server.ts
336
- import { createRequire as createRequire3 } from "module";
476
+ import { createRequire as createRequire4 } from "module";
337
477
  import crypto34 from "crypto";
338
478
  import fs12 from "fs";
339
479
  import path14 from "path";
340
480
  import { fileURLToPath as fileURLToPath2 } from "url";
341
- import { eq as eq40 } from "drizzle-orm";
481
+ import { eq as eq41 } from "drizzle-orm";
342
482
  import Fastify from "fastify";
343
483
 
344
484
  // ../api-routes/src/auth.ts
@@ -439,7 +579,11 @@ function resolveSnapshotMentionResult(snapshot, project) {
439
579
  canonicalDomain: project.canonicalDomain,
440
580
  ownedDomains: normalizeOwnedDomains(project.ownedDomains)
441
581
  });
442
- return extractAnswerMentions(snapshot.answerText, project.displayName, domains);
582
+ const brandNames = effectiveBrandNames({
583
+ displayName: project.displayName,
584
+ aliases: normalizeOwnedDomains(project.aliases)
585
+ });
586
+ return extractAnswerMentions(snapshot.answerText, brandNames, domains);
443
587
  }
444
588
  if (typeof snapshot.answerMentioned === "boolean") {
445
589
  return { mentioned: snapshot.answerMentioned, matchedTerms: [] };
@@ -452,6 +596,9 @@ function resolveSnapshotAnswerMentioned(snapshot, project) {
452
596
  function resolveSnapshotVisibilityState(snapshot, project) {
453
597
  return visibilityStateFromAnswerMentioned(resolveSnapshotMentionResult(snapshot, project).mentioned);
454
598
  }
599
+ function resolveSnapshotMentionState(snapshot, project) {
600
+ return mentionStateFromAnswerMentioned(resolveSnapshotMentionResult(snapshot, project).mentioned);
601
+ }
455
602
  function resolveSnapshotMatchedTerms(snapshot, project) {
456
603
  return resolveSnapshotMentionResult(snapshot, project).matchedTerms;
457
604
  }
@@ -502,12 +649,16 @@ async function projectRoutes(app, opts) {
502
649
  });
503
650
  }
504
651
  const nextAutoExtractBacklinks = body.autoExtractBacklinks !== void 0 ? body.autoExtractBacklinks ? 1 : 0 : existing?.autoExtractBacklinks ?? 0;
652
+ const nextAliases = normalizeProjectAliases(body.displayName, body.aliases ?? []);
505
653
  if (existing) {
654
+ const prevAliases = parseJsonColumn(existing.aliases, []);
655
+ const aliasesChanged = !aliasArraysEqual(prevAliases, nextAliases);
506
656
  app.db.transaction((tx) => {
507
657
  tx.update(projects).set({
508
658
  displayName: body.displayName,
509
659
  canonicalDomain: body.canonicalDomain,
510
660
  ownedDomains: JSON.stringify(body.ownedDomains ?? []),
661
+ aliases: JSON.stringify(nextAliases),
511
662
  country: body.country,
512
663
  language: body.language,
513
664
  tags: JSON.stringify(body.tags ?? []),
@@ -529,6 +680,7 @@ async function projectRoutes(app, opts) {
529
680
  });
530
681
  });
531
682
  opts.onProjectUpserted?.(existing.id, name);
683
+ if (aliasesChanged) opts.onAliasesChanged?.(existing.id, name);
532
684
  const updated = app.db.select().from(projects).where(eq3(projects.id, existing.id)).get();
533
685
  return reply.status(200).send(formatProject(updated));
534
686
  }
@@ -540,6 +692,7 @@ async function projectRoutes(app, opts) {
540
692
  displayName: body.displayName,
541
693
  canonicalDomain: body.canonicalDomain,
542
694
  ownedDomains: JSON.stringify(body.ownedDomains ?? []),
695
+ aliases: JSON.stringify(nextAliases),
543
696
  country: body.country,
544
697
  language: body.language,
545
698
  tags: JSON.stringify(body.tags ?? []),
@@ -687,6 +840,7 @@ async function projectRoutes(app, opts) {
687
840
  displayName: project.displayName,
688
841
  canonicalDomain: project.canonicalDomain,
689
842
  ownedDomains: parseJsonColumn(project.ownedDomains, []),
843
+ aliases: parseJsonColumn(project.aliases, []),
690
844
  country: project.country,
691
845
  language: project.language,
692
846
  queries: qs.map((q) => q.query),
@@ -722,6 +876,7 @@ function formatProject(row) {
722
876
  displayName: row.displayName,
723
877
  canonicalDomain: row.canonicalDomain,
724
878
  ownedDomains: parseJsonColumn(row.ownedDomains, []),
879
+ aliases: parseJsonColumn(row.aliases, []),
725
880
  country: row.country,
726
881
  language: row.language,
727
882
  tags: parseJsonColumn(row.tags, []),
@@ -736,6 +891,13 @@ function formatProject(row) {
736
891
  updatedAt: row.updatedAt
737
892
  };
738
893
  }
894
+ function aliasArraysEqual(a, b) {
895
+ if (a.length !== b.length) return false;
896
+ for (let i = 0; i < a.length; i++) {
897
+ if (a[i].toLowerCase() !== b[i].toLowerCase()) return false;
898
+ }
899
+ return true;
900
+ }
739
901
 
740
902
  // ../api-routes/src/queries.ts
741
903
  import crypto5 from "crypto";
@@ -1138,7 +1300,7 @@ function parseCompetitorBatch(value) {
1138
1300
 
1139
1301
  // ../api-routes/src/runs.ts
1140
1302
  import crypto8 from "crypto";
1141
- import { eq as eq7, asc, desc, sql as sql2 } from "drizzle-orm";
1303
+ import { and as and2, eq as eq7, asc, desc, or as or2, sql as sql2 } from "drizzle-orm";
1142
1304
 
1143
1305
  // ../api-routes/src/run-queue.ts
1144
1306
  import crypto7 from "crypto";
@@ -1232,23 +1394,36 @@ async function runRoutes(app, opts) {
1232
1394
  if (projectLocations.length === 0) {
1233
1395
  throw validationError("No locations configured for this project");
1234
1396
  }
1235
- const newRuns = [];
1236
- for (const loc of projectLocations) {
1237
- const runId2 = crypto8.randomUUID();
1238
- app.db.insert(runs).values({
1239
- id: runId2,
1240
- projectId: project.id,
1241
- kind,
1242
- status: "queued",
1243
- trigger,
1244
- location: loc.label,
1245
- queries: queriesColumn,
1246
- createdAt: now
1247
- }).run();
1248
- newRuns.push({ runId: runId2, loc });
1397
+ const result = app.db.transaction((tx) => {
1398
+ const activeRun = tx.select({ id: runs.id }).from(runs).where(and2(
1399
+ eq7(runs.projectId, project.id),
1400
+ or2(eq7(runs.status, "queued"), eq7(runs.status, "running"))
1401
+ )).get();
1402
+ if (activeRun) {
1403
+ return { conflict: true };
1404
+ }
1405
+ const inserted = [];
1406
+ for (const loc of projectLocations) {
1407
+ const runId2 = crypto8.randomUUID();
1408
+ tx.insert(runs).values({
1409
+ id: runId2,
1410
+ projectId: project.id,
1411
+ kind,
1412
+ status: "queued",
1413
+ trigger,
1414
+ location: loc.label,
1415
+ queries: queriesColumn,
1416
+ createdAt: now
1417
+ }).run();
1418
+ inserted.push({ runId: runId2, loc });
1419
+ }
1420
+ return { conflict: false, inserted };
1421
+ });
1422
+ if (result.conflict) {
1423
+ throw runInProgress(project.name);
1249
1424
  }
1250
1425
  const results = [];
1251
- for (const { runId: runId2, loc } of newRuns) {
1426
+ for (const { runId: runId2, loc } of result.inserted) {
1252
1427
  writeAuditLog(app.db, {
1253
1428
  projectId: project.id,
1254
1429
  actor: "api",
@@ -1436,7 +1611,8 @@ function loadRunDetail(app, run) {
1436
1611
  const project = app.db.select({
1437
1612
  displayName: projects.displayName,
1438
1613
  canonicalDomain: projects.canonicalDomain,
1439
- ownedDomains: projects.ownedDomains
1614
+ ownedDomains: projects.ownedDomains,
1615
+ aliases: projects.aliases
1440
1616
  }).from(projects).where(eq7(projects.id, run.projectId)).get();
1441
1617
  const snapshots = app.db.select({
1442
1618
  id: querySnapshots.id,
@@ -1468,7 +1644,10 @@ function loadRunDetail(app, run) {
1468
1644
  provider: s.provider,
1469
1645
  citationState: s.citationState,
1470
1646
  answerMentioned,
1647
+ // Legacy alias of `mentionState`, retained for backwards compatibility.
1471
1648
  visibilityState: project ? resolveSnapshotVisibilityState(s, project) : answerMentioned ? "visible" : "not-visible",
1649
+ // Canonical vocabulary for answer-text presence; new consumers prefer this.
1650
+ mentionState: project ? resolveSnapshotMentionState(s, project) : answerMentioned ? "mentioned" : "not-mentioned",
1472
1651
  answerText: s.answerText,
1473
1652
  citedDomains: parseJsonColumn(s.citedDomains, []),
1474
1653
  competitorOverlap: parseJsonColumn(s.competitorOverlap, []),
@@ -1486,7 +1665,7 @@ function loadRunDetail(app, run) {
1486
1665
 
1487
1666
  // ../api-routes/src/apply.ts
1488
1667
  import crypto10 from "crypto";
1489
- import { and as and2, eq as eq8 } from "drizzle-orm";
1668
+ import { and as and3, eq as eq8 } from "drizzle-orm";
1490
1669
 
1491
1670
  // ../api-routes/src/schedule-utils.ts
1492
1671
  var DAY_MAP = {
@@ -1583,7 +1762,7 @@ import http from "http";
1583
1762
  import https from "https";
1584
1763
  import net from "net";
1585
1764
  var REQUEST_TIMEOUT_MS = 1e4;
1586
- async function resolveWebhookTarget(raw) {
1765
+ async function resolveWebhookTarget(raw, options = {}) {
1587
1766
  let parsed;
1588
1767
  try {
1589
1768
  parsed = new URL(raw);
@@ -1604,7 +1783,7 @@ async function resolveWebhookTarget(raw) {
1604
1783
  if (addresses.length === 0) {
1605
1784
  return { ok: false, message: '"url" hostname could not be resolved' };
1606
1785
  }
1607
- const blocked = addresses.find((entry) => isBlockedAddress(entry.address));
1786
+ const blocked = addresses.find((entry) => isBlockedAddress(entry.address, options));
1608
1787
  if (blocked) {
1609
1788
  return { ok: false, message: '"url" must not resolve to a private or loopback address' };
1610
1789
  }
@@ -1682,34 +1861,40 @@ async function resolveHostAddresses(hostname) {
1682
1861
  return [];
1683
1862
  }
1684
1863
  }
1685
- function isBlockedAddress(address) {
1864
+ function isBlockedAddress(address, options) {
1686
1865
  const normalized = stripIpv6Brackets(address).toLowerCase();
1687
1866
  const family = net.isIP(normalized);
1688
1867
  if (family === 4) {
1689
- return isBlockedIpv4(normalized);
1868
+ return isBlockedIpv4(normalized, options);
1690
1869
  }
1691
1870
  if (family === 6) {
1692
1871
  const mappedIpv4 = extractMappedIpv4(normalized);
1693
1872
  if (mappedIpv4) {
1694
- return isBlockedIpv4(mappedIpv4);
1873
+ return isBlockedIpv4(mappedIpv4, options);
1695
1874
  }
1696
- return isBlockedIpv6(normalized);
1875
+ return isBlockedIpv6(normalized, options);
1697
1876
  }
1698
1877
  return true;
1699
1878
  }
1700
- function isBlockedIpv4(address) {
1879
+ function isBlockedIpv4(address, options) {
1701
1880
  const octets = address.split(".").map((part) => Number.parseInt(part, 10));
1702
1881
  if (octets.length !== 4 || octets.some(Number.isNaN)) {
1703
1882
  return true;
1704
1883
  }
1705
1884
  const [first, second] = octets;
1885
+ if (first === 127 && !options.allowLoopback) {
1886
+ return true;
1887
+ }
1706
1888
  return first === 0 || first === 10 || first === 100 && second >= 64 && second <= 127 || first === 169 && second === 254 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168 || first === 198 && (second === 18 || second === 19);
1707
1889
  }
1708
- function isBlockedIpv6(address) {
1890
+ function isBlockedIpv6(address, options) {
1709
1891
  const normalized = address.split("%")[0].toLowerCase();
1710
1892
  if (normalized === "::") {
1711
1893
  return true;
1712
1894
  }
1895
+ if (normalized === "::1") {
1896
+ return !options.allowLoopback;
1897
+ }
1713
1898
  const firstHextetText = normalized.split(":")[0] ?? "";
1714
1899
  const firstHextet = firstHextetText === "" ? 0 : Number.parseInt(firstHextetText, 16);
1715
1900
  if (Number.isNaN(firstHextet)) {
@@ -1740,6 +1925,7 @@ function stripIpv6Brackets(value) {
1740
1925
 
1741
1926
  // ../api-routes/src/apply.ts
1742
1927
  async function applyRoutes(app, opts) {
1928
+ const allowLoopback = opts?.allowLoopbackWebhooks === true;
1743
1929
  app.post("/apply", async (request, reply) => {
1744
1930
  const parsed = projectConfigSchema.safeParse(request.body);
1745
1931
  if (!parsed.success) {
@@ -1794,7 +1980,7 @@ async function applyRoutes(app, opts) {
1794
1980
  const hasNotifications = "notifications" in rawSpec;
1795
1981
  if (hasNotifications) {
1796
1982
  for (const notif of config.spec.notifications) {
1797
- const urlCheck = await resolveWebhookTarget(notif.url ?? "");
1983
+ const urlCheck = await resolveWebhookTarget(notif.url ?? "", { allowLoopback });
1798
1984
  if (!urlCheck.ok) throw validationError(`Notification URL invalid: ${urlCheck.message}`);
1799
1985
  }
1800
1986
  }
@@ -1803,14 +1989,21 @@ async function applyRoutes(app, opts) {
1803
1989
  const configQueries = resolveConfigSpecQueries(config.spec);
1804
1990
  let projectId;
1805
1991
  let scheduleAction = null;
1992
+ let aliasesChanged = false;
1806
1993
  app.db.transaction((tx) => {
1807
1994
  const existing = tx.select().from(projects).where(eq8(projects.name, name)).get();
1995
+ const nextAliases = normalizeProjectAliases(config.spec.displayName, config.spec.aliases ?? []);
1996
+ if (existing) {
1997
+ const prevAliases = parseJsonColumn(existing.aliases, []);
1998
+ aliasesChanged = !aliasArraysEqual2(prevAliases, nextAliases);
1999
+ }
1808
2000
  if (existing) {
1809
2001
  projectId = existing.id;
1810
2002
  tx.update(projects).set({
1811
2003
  displayName: config.spec.displayName,
1812
2004
  canonicalDomain: config.spec.canonicalDomain,
1813
2005
  ownedDomains: JSON.stringify(config.spec.ownedDomains ?? []),
2006
+ aliases: JSON.stringify(nextAliases),
1814
2007
  country: config.spec.country,
1815
2008
  language: config.spec.language,
1816
2009
  labels: JSON.stringify(config.metadata.labels),
@@ -1837,6 +2030,7 @@ async function applyRoutes(app, opts) {
1837
2030
  displayName: config.spec.displayName,
1838
2031
  canonicalDomain: config.spec.canonicalDomain,
1839
2032
  ownedDomains: JSON.stringify(config.spec.ownedDomains ?? []),
2033
+ aliases: JSON.stringify(nextAliases),
1840
2034
  country: config.spec.country,
1841
2035
  language: config.spec.language,
1842
2036
  tags: "[]",
@@ -1895,7 +2089,7 @@ async function applyRoutes(app, opts) {
1895
2089
  });
1896
2090
  const AV_KIND = SchedulableRunKinds["answer-visibility"];
1897
2091
  if (resolvedSchedule) {
1898
- const existingSched = tx.select().from(schedules).where(and2(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).get();
2092
+ const existingSched = tx.select().from(schedules).where(and3(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).get();
1899
2093
  if (existingSched) {
1900
2094
  tx.update(schedules).set({
1901
2095
  cronExpr: resolvedSchedule.cronExpr,
@@ -1921,9 +2115,9 @@ async function applyRoutes(app, opts) {
1921
2115
  }
1922
2116
  scheduleAction = "upsert";
1923
2117
  } else if (deleteSchedule) {
1924
- const existingSched = tx.select().from(schedules).where(and2(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).get();
2118
+ const existingSched = tx.select().from(schedules).where(and3(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).get();
1925
2119
  if (existingSched) {
1926
- tx.delete(schedules).where(and2(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).run();
2120
+ tx.delete(schedules).where(and3(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).run();
1927
2121
  scheduleAction = "delete";
1928
2122
  }
1929
2123
  }
@@ -1956,6 +2150,9 @@ async function applyRoutes(app, opts) {
1956
2150
  if (!hasNotifications) {
1957
2151
  opts?.onProjectUpserted?.(projectId, config.metadata.name);
1958
2152
  }
2153
+ if (aliasesChanged) {
2154
+ opts?.onAliasesChanged?.(projectId, config.metadata.name);
2155
+ }
1959
2156
  if ("google" in rawSpec && config.spec.google?.gsc?.propertyUrl) {
1960
2157
  opts?.onGoogleConnectionPropertyUpdated?.(config.spec.canonicalDomain, "gsc", config.spec.google.gsc.propertyUrl);
1961
2158
  }
@@ -1966,6 +2163,7 @@ async function applyRoutes(app, opts) {
1966
2163
  displayName: project.displayName,
1967
2164
  canonicalDomain: project.canonicalDomain,
1968
2165
  ownedDomains: parseJsonColumn(project.ownedDomains, []),
2166
+ aliases: parseJsonColumn(project.aliases, []),
1969
2167
  country: project.country,
1970
2168
  language: project.language,
1971
2169
  tags: parseJsonColumn(project.tags, []),
@@ -1981,6 +2179,13 @@ async function applyRoutes(app, opts) {
1981
2179
  });
1982
2180
  });
1983
2181
  }
2182
+ function aliasArraysEqual2(a, b) {
2183
+ if (a.length !== b.length) return false;
2184
+ for (let i = 0; i < a.length; i++) {
2185
+ if (a[i].toLowerCase() !== b[i].toLowerCase()) return false;
2186
+ }
2187
+ return true;
2188
+ }
1984
2189
  function normalizeCompetitorList2(domains) {
1985
2190
  const seen = /* @__PURE__ */ new Set();
1986
2191
  const result = [];
@@ -2088,6 +2293,7 @@ async function historyRoutes(app) {
2088
2293
  citationState: s.citationState,
2089
2294
  answerMentioned: resolveSnapshotAnswerMentioned(s, project),
2090
2295
  visibilityState: resolveSnapshotVisibilityState(s, project),
2296
+ mentionState: resolveSnapshotMentionState(s, project),
2091
2297
  answerText: s.answerText,
2092
2298
  citedDomains: parseJsonColumn(s.citedDomains, []),
2093
2299
  competitorOverlap: parseJsonColumn(s.competitorOverlap, []),
@@ -2141,9 +2347,11 @@ async function historyRoutes(app) {
2141
2347
  const run = projectRuns.find((r) => r.id === snap.runId);
2142
2348
  let transition = snap.citationState === CitationStates.cited ? "cited" : "not-cited";
2143
2349
  let visibilityTransition = snap.answerMentioned ? "visible" : "not-visible";
2350
+ let mentionTransition = snap.answerMentioned ? "mentioned" : "not-mentioned";
2144
2351
  if (idx === 0) {
2145
2352
  transition = "new";
2146
2353
  visibilityTransition = "new";
2354
+ mentionTransition = "new";
2147
2355
  } else {
2148
2356
  const prev = snaps[idx - 1];
2149
2357
  if (prev.citationState === CitationStates["not-cited"] && snap.citationState === CitationStates.cited) {
@@ -2153,8 +2361,10 @@ async function historyRoutes(app) {
2153
2361
  }
2154
2362
  if (!prev.answerMentioned && snap.answerMentioned) {
2155
2363
  visibilityTransition = "emerging";
2364
+ mentionTransition = "emerging";
2156
2365
  } else if (prev.answerMentioned && !snap.answerMentioned) {
2157
2366
  visibilityTransition = "lost";
2367
+ mentionTransition = "lost";
2158
2368
  }
2159
2369
  }
2160
2370
  return {
@@ -2163,8 +2373,12 @@ async function historyRoutes(app) {
2163
2373
  citationState: snap.citationState,
2164
2374
  transition,
2165
2375
  answerMentioned: snap.answerMentioned,
2376
+ // Legacy aliases of `mentionState` / `mentionTransition`.
2166
2377
  visibilityState: snap.answerMentioned ? "visible" : "not-visible",
2167
2378
  visibilityTransition,
2379
+ // Canonical-vocabulary fields — new consumers prefer these.
2380
+ mentionState: snap.answerMentioned ? "mentioned" : "not-mentioned",
2381
+ mentionTransition,
2168
2382
  location: snap.location
2169
2383
  };
2170
2384
  });
@@ -2220,7 +2434,8 @@ async function historyRoutes(app) {
2220
2434
  const resolved = {
2221
2435
  ...s,
2222
2436
  resolvedAnswerMentioned: resolveSnapshotAnswerMentioned(s, project),
2223
- resolvedVisibilityState: resolveSnapshotVisibilityState(s, project)
2437
+ resolvedVisibilityState: resolveSnapshotVisibilityState(s, project),
2438
+ resolvedMentionState: resolveSnapshotMentionState(s, project)
2224
2439
  };
2225
2440
  const existing = map1.get(s.queryId);
2226
2441
  if (!existing || !existing.resolvedAnswerMentioned && resolved.resolvedAnswerMentioned || existing.resolvedAnswerMentioned === resolved.resolvedAnswerMentioned && resolved.citationState === CitationStates.cited) {
@@ -2232,7 +2447,8 @@ async function historyRoutes(app) {
2232
2447
  const resolved = {
2233
2448
  ...s,
2234
2449
  resolvedAnswerMentioned: resolveSnapshotAnswerMentioned(s, project),
2235
- resolvedVisibilityState: resolveSnapshotVisibilityState(s, project)
2450
+ resolvedVisibilityState: resolveSnapshotVisibilityState(s, project),
2451
+ resolvedMentionState: resolveSnapshotMentionState(s, project)
2236
2452
  };
2237
2453
  const existing = map2.get(s.queryId);
2238
2454
  if (!existing || !existing.resolvedAnswerMentioned && resolved.resolvedAnswerMentioned || existing.resolvedAnswerMentioned === resolved.resolvedAnswerMentioned && resolved.citationState === CitationStates.cited) {
@@ -2250,8 +2466,11 @@ async function historyRoutes(app) {
2250
2466
  run2State: s2?.citationState ?? null,
2251
2467
  run1AnswerMentioned: s1?.resolvedAnswerMentioned ?? null,
2252
2468
  run2AnswerMentioned: s2?.resolvedAnswerMentioned ?? null,
2469
+ // Legacy aliases — same data as run{1,2}MentionState below.
2253
2470
  run1VisibilityState: s1?.resolvedVisibilityState ?? null,
2254
2471
  run2VisibilityState: s2?.resolvedVisibilityState ?? null,
2472
+ run1MentionState: s1?.resolvedMentionState ?? null,
2473
+ run2MentionState: s2?.resolvedMentionState ?? null,
2255
2474
  changed: (s1?.citationState ?? null) !== (s2?.citationState ?? null),
2256
2475
  visibilityChanged: (s1?.resolvedAnswerMentioned ?? null) !== (s2?.resolvedAnswerMentioned ?? null)
2257
2476
  };
@@ -2292,7 +2511,7 @@ async function analyticsRoutes(app) {
2292
2511
  });
2293
2512
  }
2294
2513
  const runIds = projectRuns.map((r) => r.id);
2295
- const rawSnapshots = app.db.select({
2514
+ const rawSnapshots = filterTrackedSnapshots(app.db.select({
2296
2515
  runId: querySnapshots.runId,
2297
2516
  queryId: querySnapshots.queryId,
2298
2517
  provider: querySnapshots.provider,
@@ -2300,7 +2519,7 @@ async function analyticsRoutes(app) {
2300
2519
  answerMentioned: querySnapshots.answerMentioned,
2301
2520
  answerText: querySnapshots.answerText,
2302
2521
  createdAt: querySnapshots.createdAt
2303
- }).from(querySnapshots).where(inArray2(querySnapshots.runId, runIds)).all();
2522
+ }).from(querySnapshots).where(inArray2(querySnapshots.runId, runIds)).all());
2304
2523
  const allSnapshots = rawSnapshots.map((s) => ({
2305
2524
  ...s,
2306
2525
  resolvedMentioned: resolveSnapshotAnswerMentioned(s, project)
@@ -2339,13 +2558,13 @@ async function analyticsRoutes(app) {
2339
2558
  const runIdToCreatedAt = new Map(windowRuns.map((r) => [r.id, r.createdAt]));
2340
2559
  const consistencyMap = /* @__PURE__ */ new Map();
2341
2560
  if (windowRunIds.length > 0) {
2342
- const allWindowSnaps = app.db.select({
2561
+ const allWindowSnaps = filterTrackedSnapshots(app.db.select({
2343
2562
  queryId: querySnapshots.queryId,
2344
2563
  runId: querySnapshots.runId,
2345
2564
  citationState: querySnapshots.citationState,
2346
2565
  answerMentioned: querySnapshots.answerMentioned,
2347
2566
  answerText: querySnapshots.answerText
2348
- }).from(querySnapshots).where(inArray2(querySnapshots.runId, windowRunIds)).all();
2567
+ }).from(querySnapshots).where(inArray2(querySnapshots.runId, windowRunIds)).all());
2349
2568
  for (const s of allWindowSnaps) {
2350
2569
  const timePoint = runIdToCreatedAt.get(s.runId) ?? s.runId;
2351
2570
  let entry = consistencyMap.get(s.queryId);
@@ -2358,7 +2577,7 @@ async function analyticsRoutes(app) {
2358
2577
  if (resolveSnapshotAnswerMentioned(s, project)) entry.mentionedRuns.add(timePoint);
2359
2578
  }
2360
2579
  }
2361
- const rawSnapshots = app.db.select({
2580
+ const rawSnapshots = filterTrackedSnapshots(app.db.select({
2362
2581
  queryId: querySnapshots.queryId,
2363
2582
  query: queries.query,
2364
2583
  provider: querySnapshots.provider,
@@ -2366,7 +2585,7 @@ async function analyticsRoutes(app) {
2366
2585
  answerMentioned: querySnapshots.answerMentioned,
2367
2586
  answerText: querySnapshots.answerText,
2368
2587
  competitorOverlap: querySnapshots.competitorOverlap
2369
- }).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(inArray2(querySnapshots.runId, latestGroupRunIds)).all();
2588
+ }).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(inArray2(querySnapshots.runId, latestGroupRunIds)).all());
2370
2589
  const snapshots = rawSnapshots.map((s) => ({
2371
2590
  ...s,
2372
2591
  resolvedMentioned: resolveSnapshotAnswerMentioned(s, project)
@@ -2620,7 +2839,7 @@ function buildCategoryCounts(counts) {
2620
2839
  }
2621
2840
 
2622
2841
  // ../api-routes/src/intelligence.ts
2623
- import { eq as eq11, desc as desc4, and as and3, inArray as inArray3 } from "drizzle-orm";
2842
+ import { eq as eq11, desc as desc4, and as and4, inArray as inArray3 } from "drizzle-orm";
2624
2843
  function emptyHealthSnapshot(projectId) {
2625
2844
  return {
2626
2845
  id: `no-data:${projectId}`,
@@ -2709,7 +2928,7 @@ async function intelligenceRoutes(app) {
2709
2928
  if (request.query.runId) {
2710
2929
  conditions.push(eq11(insights.runId, request.query.runId));
2711
2930
  }
2712
- const rows = app.db.select().from(insights).where(conditions.length === 1 ? conditions[0] : and3(...conditions)).orderBy(desc4(insights.createdAt)).all();
2931
+ const rows = app.db.select().from(insights).where(conditions.length === 1 ? conditions[0] : and4(...conditions)).orderBy(desc4(insights.createdAt)).all();
2713
2932
  const showDismissed = request.query.dismissed === "true";
2714
2933
  const result = rows.filter((r) => showDismissed || !r.dismissed).map(mapInsightRow);
2715
2934
  return reply.send(result);
@@ -2733,7 +2952,7 @@ async function intelligenceRoutes(app) {
2733
2952
  });
2734
2953
  app.get("/projects/:name/health/latest", async (request, reply) => {
2735
2954
  const project = resolveProject(app.db, request.params.name);
2736
- const projectVisRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(and3(
2955
+ const projectVisRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(and4(
2737
2956
  eq11(runs.projectId, project.id),
2738
2957
  eq11(runs.kind, RunKinds["answer-visibility"]),
2739
2958
  inArray3(runs.status, [RunStatuses.completed, RunStatuses.partial])
@@ -2741,7 +2960,7 @@ async function intelligenceRoutes(app) {
2741
2960
  const latestGroup = groupRunsByCreatedAt(projectVisRuns)[0] ?? [];
2742
2961
  const latestGroupRunIds = latestGroup.map((r) => r.id);
2743
2962
  if (latestGroupRunIds.length > 0) {
2744
- const groupRows = app.db.select().from(healthSnapshots).where(and3(
2963
+ const groupRows = app.db.select().from(healthSnapshots).where(and4(
2745
2964
  eq11(healthSnapshots.projectId, project.id),
2746
2965
  inArray3(healthSnapshots.runId, latestGroupRunIds)
2747
2966
  )).all();
@@ -2765,7 +2984,7 @@ async function intelligenceRoutes(app) {
2765
2984
  }
2766
2985
 
2767
2986
  // ../api-routes/src/report.ts
2768
- import { and as and5, desc as desc6, eq as eq13, gte, inArray as inArray5, lt, lte, ne, or as or2, sql as sql3 } from "drizzle-orm";
2987
+ import { and as and6, desc as desc6, eq as eq13, gte, inArray as inArray5, lt, lte, ne, or as or3, sql as sql3 } from "drizzle-orm";
2769
2988
 
2770
2989
  // ../api-routes/src/report-renderer.ts
2771
2990
  var COLORS = {
@@ -5001,7 +5220,7 @@ function renderReportHtml(report, opts = {}) {
5001
5220
  }
5002
5221
 
5003
5222
  // ../api-routes/src/content-data.ts
5004
- import { and as and4, eq as eq12, desc as desc5, inArray as inArray4 } from "drizzle-orm";
5223
+ import { and as and5, eq as eq12, desc as desc5, inArray as inArray4 } from "drizzle-orm";
5005
5224
  var RECENT_RUNS_WINDOW = 5;
5006
5225
  function loadOrchestratorInput(db, project, locationFilter = void 0) {
5007
5226
  const projectId = project.id;
@@ -5125,7 +5344,7 @@ function listCompetitorDomains(db, projectId) {
5125
5344
  }
5126
5345
  function listRecentAnswerVisibilityRunIds(db, projectId, limit, locationFilter) {
5127
5346
  const rows = db.select({ id: runs.id, location: runs.location }).from(runs).where(
5128
- and4(
5347
+ and5(
5129
5348
  eq12(runs.projectId, projectId),
5130
5349
  eq12(runs.kind, RunKinds["answer-visibility"]),
5131
5350
  // Queued/running/failed/cancelled runs may have partial or no
@@ -5173,7 +5392,7 @@ function buildCandidateQueries(opts) {
5173
5392
  const queryRows = opts.db.select({ id: queries.id, text: queries.query }).from(queries).where(eq12(queries.projectId, opts.projectId)).all();
5174
5393
  const queryIdByText = new Map(queryRows.map((r) => [r.text, r.id]));
5175
5394
  const candidateQueryIds = opts.candidateQueryStrings.map((q) => queryIdByText.get(q)).filter((id) => Boolean(id));
5176
- const snapshotRows = opts.db.select().from(querySnapshots).where(inArray4(querySnapshots.runId, opts.recentRunIds)).all().filter((r) => candidateQueryIds.includes(r.queryId));
5395
+ const snapshotRows = filterTrackedSnapshots(opts.db.select().from(querySnapshots).where(inArray4(querySnapshots.runId, opts.recentRunIds)).all()).filter((r) => candidateQueryIds.includes(r.queryId));
5177
5396
  const snapshotsByQuery = /* @__PURE__ */ new Map();
5178
5397
  for (const row of snapshotRows) {
5179
5398
  const list = snapshotsByQuery.get(row.queryId) ?? [];
@@ -5383,8 +5602,8 @@ function safeNum(value) {
5383
5602
  }
5384
5603
  return 0;
5385
5604
  }
5386
- function categorizeQuery(query, projectDisplayName, canonicalDomain) {
5387
- return categorizeQueryByIntent(query, buildBrandTokens(canonicalDomain, projectDisplayName));
5605
+ function categorizeQuery(query, projectBrandNames, canonicalDomain) {
5606
+ return categorizeQueryByIntent(query, buildBrandTokens(canonicalDomain, projectBrandNames));
5388
5607
  }
5389
5608
  function loadSnapshotsForRun(db, runId) {
5390
5609
  return loadSnapshotsForRunIds(db, [runId]);
@@ -5392,7 +5611,7 @@ function loadSnapshotsForRun(db, runId) {
5392
5611
  function loadSnapshotsForRunIds(db, runIds) {
5393
5612
  if (runIds.length === 0) return [];
5394
5613
  const rows = db.select().from(querySnapshots).where(inArray5(querySnapshots.runId, [...runIds])).all();
5395
- return rows.map((r) => ({
5614
+ return rows.filter((r) => r.queryId !== null).map((r) => ({
5396
5615
  id: r.id,
5397
5616
  runId: r.runId,
5398
5617
  queryId: r.queryId,
@@ -5413,7 +5632,7 @@ function loadQueryLookup(db, projectId) {
5413
5632
  for (const row of rows) byId.set(row.id, row.query);
5414
5633
  return { byId };
5415
5634
  }
5416
- function buildGscSection(db, projectId, projectDisplayName, canonicalDomain, trackedQueries) {
5635
+ function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trackedQueries) {
5417
5636
  const allRows = db.select().from(gscSearchData).where(eq13(gscSearchData.projectId, projectId)).all();
5418
5637
  if (allRows.length === 0) return null;
5419
5638
  let maxDate = "";
@@ -5448,11 +5667,11 @@ function buildGscSection(db, projectId, projectDisplayName, canonicalDomain, tra
5448
5667
  impressions: agg.impressions,
5449
5668
  ctr: agg.impressions > 0 ? agg.clicks / agg.impressions : 0,
5450
5669
  avgPosition: agg.impressions > 0 ? agg.weightedPositionSum / agg.impressions : 0,
5451
- category: categorizeQuery(query, projectDisplayName, canonicalDomain)
5670
+ category: categorizeQuery(query, projectBrandNames, canonicalDomain)
5452
5671
  })).sort((a, b) => b.clicks - a.clicks).slice(0, TOP_QUERIES_LIMIT);
5453
5672
  const categoryAgg = /* @__PURE__ */ new Map();
5454
5673
  for (const [query, agg] of queryAgg) {
5455
- const cat = categorizeQuery(query, projectDisplayName, canonicalDomain);
5674
+ const cat = categorizeQuery(query, projectBrandNames, canonicalDomain);
5456
5675
  const bucket = categoryAgg.get(cat) ?? { clicks: 0, impressions: 0 };
5457
5676
  bucket.clicks += agg.clicks;
5458
5677
  bucket.impressions += agg.impressions;
@@ -5470,7 +5689,7 @@ function buildGscSection(db, projectId, projectDisplayName, canonicalDomain, tra
5470
5689
  const trackedSet = new Set(trackedQueries.map((q) => q.toLowerCase()));
5471
5690
  const gscQuerySet = new Set([...queryAgg.keys()].map((q) => q.toLowerCase()));
5472
5691
  const trackedButNoGsc = trackedQueries.filter((q) => !gscQuerySet.has(q.toLowerCase())).sort();
5473
- const gscButNotTracked = [...queryAgg.entries()].filter(([q]) => !trackedSet.has(q.toLowerCase())).filter(([q]) => categorizeQuery(q, projectDisplayName, canonicalDomain) !== "brand").sort((a, b) => b[1].impressions - a[1].impressions).map(([q]) => q).slice(0, TOP_QUERIES_LIMIT);
5692
+ const gscButNotTracked = [...queryAgg.entries()].filter(([q]) => !trackedSet.has(q.toLowerCase())).filter(([q]) => categorizeQuery(q, projectBrandNames, canonicalDomain) !== "brand").sort((a, b) => b[1].impressions - a[1].impressions).map(([q]) => q).slice(0, TOP_QUERIES_LIMIT);
5474
5693
  return {
5475
5694
  periodStart,
5476
5695
  periodEnd,
@@ -5487,7 +5706,7 @@ function buildGscSection(db, projectId, projectDisplayName, canonicalDomain, tra
5487
5706
  }
5488
5707
  function buildGaSection(db, projectId) {
5489
5708
  const windowSummary = db.select().from(gaTrafficWindowSummaries).where(
5490
- and5(
5709
+ and6(
5491
5710
  eq13(gaTrafficWindowSummaries.projectId, projectId),
5492
5711
  eq13(gaTrafficWindowSummaries.windowKey, "30d")
5493
5712
  )
@@ -5665,7 +5884,7 @@ function nonSubresourceReferralPathCondition() {
5665
5884
  }
5666
5885
  function buildServerActivity(db, projectId) {
5667
5886
  const sourceRows = db.select({ id: trafficSources.id }).from(trafficSources).where(
5668
- and5(
5887
+ and6(
5669
5888
  eq13(trafficSources.projectId, projectId),
5670
5889
  ne(trafficSources.status, TrafficSourceStatuses.archived)
5671
5890
  )
@@ -5681,7 +5900,7 @@ function buildServerActivity(db, projectId) {
5681
5900
  const trendStart = new Date(trendStartMs).toISOString();
5682
5901
  const sumVerifiedCrawlers = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
5683
5902
  db.select({ total: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
5684
- and5(
5903
+ and6(
5685
5904
  eq13(crawlerEventsHourly.projectId, projectId),
5686
5905
  eq13(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
5687
5906
  gte(crawlerEventsHourly.tsHour, windowStartIso),
@@ -5691,7 +5910,7 @@ function buildServerActivity(db, projectId) {
5691
5910
  );
5692
5911
  const sumUnverifiedCrawlers = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
5693
5912
  db.select({ total: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
5694
- and5(
5913
+ and6(
5695
5914
  eq13(crawlerEventsHourly.projectId, projectId),
5696
5915
  ne(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
5697
5916
  gte(crawlerEventsHourly.tsHour, windowStartIso),
@@ -5701,7 +5920,7 @@ function buildServerActivity(db, projectId) {
5701
5920
  );
5702
5921
  const sumReferrals = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
5703
5922
  db.select({ total: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
5704
- and5(
5923
+ and6(
5705
5924
  eq13(aiReferralEventsHourly.projectId, projectId),
5706
5925
  nonSubresourceReferralPathCondition(),
5707
5926
  gte(aiReferralEventsHourly.tsHour, windowStartIso),
@@ -5720,7 +5939,7 @@ function buildServerActivity(db, projectId) {
5720
5939
  verificationStatus: crawlerEventsHourly.verificationStatus,
5721
5940
  hits: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
5722
5941
  }).from(crawlerEventsHourly).where(
5723
- and5(
5942
+ and6(
5724
5943
  eq13(crawlerEventsHourly.projectId, projectId),
5725
5944
  gte(crawlerEventsHourly.tsHour, headlineStart),
5726
5945
  lte(crawlerEventsHourly.tsHour, headlineEnd)
@@ -5730,7 +5949,7 @@ function buildServerActivity(db, projectId) {
5730
5949
  operator: crawlerEventsHourly.operator,
5731
5950
  hits: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
5732
5951
  }).from(crawlerEventsHourly).where(
5733
- and5(
5952
+ and6(
5734
5953
  eq13(crawlerEventsHourly.projectId, projectId),
5735
5954
  eq13(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
5736
5955
  gte(crawlerEventsHourly.tsHour, priorStart),
@@ -5741,7 +5960,7 @@ function buildServerActivity(db, projectId) {
5741
5960
  operator: aiReferralEventsHourly.operator,
5742
5961
  hits: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
5743
5962
  }).from(aiReferralEventsHourly).where(
5744
- and5(
5963
+ and6(
5745
5964
  eq13(aiReferralEventsHourly.projectId, projectId),
5746
5965
  nonSubresourceReferralPathCondition(),
5747
5966
  gte(aiReferralEventsHourly.tsHour, headlineStart),
@@ -5782,7 +6001,7 @@ function buildServerActivity(db, projectId) {
5782
6001
  hits: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`,
5783
6002
  operators: sql3`COUNT(DISTINCT ${crawlerEventsHourly.operator})`
5784
6003
  }).from(crawlerEventsHourly).where(
5785
- and5(
6004
+ and6(
5786
6005
  eq13(crawlerEventsHourly.projectId, projectId),
5787
6006
  eq13(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
5788
6007
  gte(crawlerEventsHourly.tsHour, headlineStart),
@@ -5799,7 +6018,7 @@ function buildServerActivity(db, projectId) {
5799
6018
  arrivals: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
5800
6019
  landingPaths: sql3`COUNT(DISTINCT ${aiReferralEventsHourly.landingPathNormalized})`
5801
6020
  }).from(aiReferralEventsHourly).where(
5802
- and5(
6021
+ and6(
5803
6022
  eq13(aiReferralEventsHourly.projectId, projectId),
5804
6023
  nonSubresourceReferralPathCondition(),
5805
6024
  gte(aiReferralEventsHourly.tsHour, headlineStart),
@@ -5816,7 +6035,7 @@ function buildServerActivity(db, projectId) {
5816
6035
  arrivals: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
5817
6036
  products: sql3`COUNT(DISTINCT ${aiReferralEventsHourly.product})`
5818
6037
  }).from(aiReferralEventsHourly).where(
5819
- and5(
6038
+ and6(
5820
6039
  eq13(aiReferralEventsHourly.projectId, projectId),
5821
6040
  nonSubresourceReferralPathCondition(),
5822
6041
  gte(aiReferralEventsHourly.tsHour, headlineStart),
@@ -5832,7 +6051,7 @@ function buildServerActivity(db, projectId) {
5832
6051
  date: sql3`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`,
5833
6052
  hits: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
5834
6053
  }).from(crawlerEventsHourly).where(
5835
- and5(
6054
+ and6(
5836
6055
  eq13(crawlerEventsHourly.projectId, projectId),
5837
6056
  eq13(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
5838
6057
  gte(crawlerEventsHourly.tsHour, trendStart),
@@ -5843,7 +6062,7 @@ function buildServerActivity(db, projectId) {
5843
6062
  date: sql3`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`,
5844
6063
  hits: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
5845
6064
  }).from(aiReferralEventsHourly).where(
5846
- and5(
6065
+ and6(
5847
6066
  eq13(aiReferralEventsHourly.projectId, projectId),
5848
6067
  nonSubresourceReferralPathCondition(),
5849
6068
  gte(aiReferralEventsHourly.tsHour, trendStart),
@@ -5918,7 +6137,7 @@ function buildIndexingHealth(db, projectId) {
5918
6137
  return null;
5919
6138
  }
5920
6139
  function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
5921
- const visibilityRuns = db.select().from(runs).where(and5(eq13(runs.projectId, projectId), eq13(runs.kind, RunKinds["answer-visibility"]))).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter);
6140
+ const visibilityRuns = db.select().from(runs).where(and6(eq13(runs.projectId, projectId), eq13(runs.kind, RunKinds["answer-visibility"]))).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter);
5922
6141
  const totalQueries = queryLookup.byId.size;
5923
6142
  const points = [];
5924
6143
  for (const run of visibilityRuns) {
@@ -5964,14 +6183,14 @@ function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
5964
6183
  }
5965
6184
  function buildInsightList(db, projectId, locationFilter) {
5966
6185
  const recentRunIds = db.select({ id: runs.id, location: runs.location }).from(runs).where(
5967
- and5(
6186
+ and6(
5968
6187
  eq13(runs.projectId, projectId),
5969
6188
  eq13(runs.kind, RunKinds["answer-visibility"]),
5970
- or2(eq13(runs.status, RunStatuses.completed), eq13(runs.status, RunStatuses.partial))
6189
+ or3(eq13(runs.status, RunStatuses.completed), eq13(runs.status, RunStatuses.partial))
5971
6190
  )
5972
6191
  ).orderBy(desc6(runs.createdAt)).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter).slice(0, INSIGHT_LOOKBACK_RUNS).map((r) => r.id);
5973
6192
  if (recentRunIds.length === 0) return [];
5974
- const rows = db.select().from(insights).where(and5(eq13(insights.projectId, projectId), inArray5(insights.runId, recentRunIds))).orderBy(desc6(insights.createdAt)).all();
6193
+ const rows = db.select().from(insights).where(and6(eq13(insights.projectId, projectId), inArray5(insights.runId, recentRunIds))).orderBy(desc6(insights.createdAt)).all();
5975
6194
  const severityRank = { critical: 0, high: 1, medium: 2, low: 3 };
5976
6195
  const flat = rows.filter((r) => !r.dismissed).map((r) => {
5977
6196
  const recommendation = parseJsonColumn(r.recommendation, null);
@@ -6159,7 +6378,8 @@ function buildReportActionPlan(input) {
6159
6378
  }
6160
6379
  for (const [index, opportunity] of input.contentOpportunities.slice(0, 2).entries()) {
6161
6380
  const verb = contentActionVerb(opportunity.action);
6162
- const target = opportunity.ourBestPage?.url ?? `a new page for "${opportunity.query}"`;
6381
+ const bestPageUrl = opportunity.ourBestPage?.url;
6382
+ const hasUsablePage = !!bestPageUrl && bestPageUrl !== "/" && bestPageUrl.trim() !== "";
6163
6383
  const evidence = [
6164
6384
  `Opportunity score ${Math.round(opportunity.score)} with ${opportunity.actionConfidence} confidence`,
6165
6385
  `Demand source: ${opportunity.demandSource}`
@@ -6168,17 +6388,22 @@ function buildReportActionPlan(input) {
6168
6388
  evidence.push(`${opportunity.winningCompetitor.domain} is the current winning cited source`);
6169
6389
  }
6170
6390
  if (opportunity.ourBestPage) {
6171
- evidence.push(`Best matching owned page: ${opportunity.ourBestPage.url}`);
6391
+ if (bestPageUrl === "/") {
6392
+ evidence.push("No topical page yet \u2014 homepage is the closest slug match");
6393
+ } else {
6394
+ evidence.push(`Best matching owned page: ${bestPageUrl}`);
6395
+ }
6172
6396
  } else {
6173
6397
  evidence.push("No matching owned page was found");
6174
6398
  }
6399
+ const targetIsExistingPage = opportunity.action !== "create" && hasUsablePage;
6175
6400
  actions.push({
6176
6401
  audience: "both",
6177
6402
  priority: 20 + index,
6178
6403
  horizon: opportunity.actionConfidence === "high" ? "short-term" : "medium-term",
6179
6404
  category: "content",
6180
6405
  title: `${verb} content for "${opportunity.query}"`,
6181
- action: opportunity.ourBestPage ? `${verb} ${target} so it directly answers the tracked query and cites the strongest supporting evidence.` : `${verb} ${target} that directly answers the query and earns citations from AI answer engines.`,
6406
+ action: targetIsExistingPage ? `${verb} the existing page so it directly answers the tracked query and cites the strongest supporting evidence.` : `${verb} a new page for "${opportunity.query}" that directly answers the query and earns citations from AI answer engines.`,
6182
6407
  why: opportunity.drivers.length > 0 ? opportunity.drivers : ["Canonry ranked this as a content opportunity from search-demand and citation evidence."],
6183
6408
  evidence,
6184
6409
  successMetric: `A future check cites ${input.canonicalDomain} for "${opportunity.query}" and the matching GSC query/page improves.`,
@@ -6523,6 +6748,11 @@ function buildProjectReport(db, projectName) {
6523
6748
  const competitorDomains = competitorRows.map((c) => c.domain);
6524
6749
  const ownedDomains = parseJsonColumn(project.ownedDomains, []);
6525
6750
  const projectDomains = [project.canonicalDomain, ...ownedDomains];
6751
+ const projectAliases = parseJsonColumn(project.aliases, []);
6752
+ const projectBrandNames = effectiveBrandNames({
6753
+ displayName: project.displayName,
6754
+ aliases: projectAliases
6755
+ });
6526
6756
  const citationScorecard = buildCitationScorecard(latestSnapshots, queryLookup);
6527
6757
  const competitorLandscape = buildCompetitorLandscape(
6528
6758
  latestSnapshots,
@@ -6533,7 +6763,7 @@ function buildProjectReport(db, projectName) {
6533
6763
  const mentionLandscape = buildMentionLandscape(
6534
6764
  latestSnapshots,
6535
6765
  competitorDomains,
6536
- project.displayName,
6766
+ projectBrandNames,
6537
6767
  projectDomains,
6538
6768
  queryLookup
6539
6769
  );
@@ -6542,7 +6772,7 @@ function buildProjectReport(db, projectName) {
6542
6772
  const gscSection = buildGscSection(
6543
6773
  db,
6544
6774
  project.id,
6545
- project.displayName,
6775
+ projectBrandNames,
6546
6776
  project.canonicalDomain,
6547
6777
  trackedQueries
6548
6778
  );
@@ -6761,8 +6991,9 @@ async function citationRoutes(app) {
6761
6991
  if (rawSnapshots.length === 0) {
6762
6992
  return reply.send(emptyCitationVisibility("no-runs-yet"));
6763
6993
  }
6764
- const snapshots = rawSnapshots.map((s) => ({
6994
+ const snapshots = rawSnapshots.filter((s) => s.queryId !== null).map((s) => ({
6765
6995
  ...s,
6996
+ queryId: s.queryId,
6766
6997
  runCreatedAt: runCreatedAt.get(s.runId) ?? s.createdAt
6767
6998
  }));
6768
6999
  const projectCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq14(competitors.projectId, project.id)).all().map((c) => normalizeDomain2(c.domain)).filter((d) => d.length > 0);
@@ -6897,7 +7128,7 @@ function normalizeDomain2(domain) {
6897
7128
  }
6898
7129
 
6899
7130
  // ../api-routes/src/composites.ts
6900
- import { eq as eq15, and as and6, desc as desc7, sql as sql4, like, or as or3, inArray as inArray7 } from "drizzle-orm";
7131
+ import { eq as eq15, and as and7, desc as desc7, sql as sql4, like, or as or4, inArray as inArray7 } from "drizzle-orm";
6901
7132
  var TOP_INSIGHT_LIMIT = 5;
6902
7133
  var SEARCH_HIT_HARD_LIMIT = 50;
6903
7134
  var SEARCH_SNIPPET_RADIUS = 80;
@@ -6998,7 +7229,7 @@ async function compositeRoutes(app) {
6998
7229
  const limit = clampSearchLimit(request.query.limit);
6999
7230
  const escaped = escapeLikePattern(rawQuery);
7000
7231
  const pattern = `%${escaped}%`;
7001
- const snapshotMatches = app.db.select({
7232
+ const snapshotMatches = filterTrackedSnapshots(app.db.select({
7002
7233
  id: querySnapshots.id,
7003
7234
  runId: querySnapshots.runId,
7004
7235
  queryId: querySnapshots.queryId,
@@ -7011,20 +7242,20 @@ async function compositeRoutes(app) {
7011
7242
  rawResponse: querySnapshots.rawResponse,
7012
7243
  createdAt: querySnapshots.createdAt
7013
7244
  }).from(querySnapshots).innerJoin(queries, eq15(querySnapshots.queryId, queries.id)).where(
7014
- and6(
7245
+ and7(
7015
7246
  eq15(queries.projectId, project.id),
7016
- or3(
7247
+ or4(
7017
7248
  sql4`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
7018
7249
  sql4`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
7019
7250
  sql4`${querySnapshots.rawResponse} LIKE ${pattern} ESCAPE '\\'`,
7020
7251
  like(queries.query, pattern)
7021
7252
  )
7022
7253
  )
7023
- ).orderBy(desc7(querySnapshots.createdAt)).limit(limit + 1).all();
7254
+ ).orderBy(desc7(querySnapshots.createdAt)).limit(limit + 1).all());
7024
7255
  const insightMatches = app.db.select().from(insights).where(
7025
- and6(
7256
+ and7(
7026
7257
  eq15(insights.projectId, project.id),
7027
- or3(
7258
+ or4(
7028
7259
  like(insights.title, pattern),
7029
7260
  like(insights.query, pattern),
7030
7261
  sql4`${insights.recommendation} LIKE ${pattern} ESCAPE '\\'`,
@@ -7094,7 +7325,7 @@ function summarizeRun(run) {
7094
7325
  function loadSnapshotsByRunIds(app, runIds) {
7095
7326
  const result = /* @__PURE__ */ new Map();
7096
7327
  if (runIds.length === 0) return result;
7097
- const rows = app.db.select({
7328
+ const rows = filterTrackedSnapshots(app.db.select({
7098
7329
  runId: querySnapshots.runId,
7099
7330
  queryId: querySnapshots.queryId,
7100
7331
  provider: querySnapshots.provider,
@@ -7102,7 +7333,7 @@ function loadSnapshotsByRunIds(app, runIds) {
7102
7333
  citationState: querySnapshots.citationState,
7103
7334
  competitorOverlap: querySnapshots.competitorOverlap,
7104
7335
  citedDomains: querySnapshots.citedDomains
7105
- }).from(querySnapshots).where(inArray7(querySnapshots.runId, [...runIds])).all();
7336
+ }).from(querySnapshots).where(inArray7(querySnapshots.runId, [...runIds])).all());
7106
7337
  for (const row of rows) {
7107
7338
  const list = result.get(row.runId) ?? [];
7108
7339
  list.push({
@@ -7352,6 +7583,7 @@ function formatProject2(row) {
7352
7583
  displayName: row.displayName,
7353
7584
  canonicalDomain: row.canonicalDomain,
7354
7585
  ownedDomains: parseJsonColumn(row.ownedDomains, []),
7586
+ aliases: parseJsonColumn(row.aliases, []),
7355
7587
  country: row.country,
7356
7588
  language: row.language,
7357
7589
  tags: parseJsonColumn(row.tags, []),
@@ -7654,6 +7886,7 @@ var routeCatalog = [
7654
7886
  displayName: stringSchema,
7655
7887
  canonicalDomain: stringSchema,
7656
7888
  ownedDomains: stringArraySchema,
7889
+ aliases: stringArraySchema,
7657
7890
  country: stringSchema,
7658
7891
  language: stringSchema,
7659
7892
  tags: stringArraySchema,
@@ -10873,7 +11106,7 @@ async function telemetryRoutes(app, opts) {
10873
11106
 
10874
11107
  // ../api-routes/src/schedules.ts
10875
11108
  import crypto11 from "crypto";
10876
- import { and as and7, eq as eq16 } from "drizzle-orm";
11109
+ import { and as and8, eq as eq16 } from "drizzle-orm";
10877
11110
  function parseKindParam(raw) {
10878
11111
  if (raw === void 0 || raw === null || raw === "") return SchedulableRunKinds["answer-visibility"];
10879
11112
  const parsed = schedulableRunKindSchema.safeParse(raw);
@@ -10939,7 +11172,7 @@ async function scheduleRoutes(app, opts) {
10939
11172
  }
10940
11173
  const now = (/* @__PURE__ */ new Date()).toISOString();
10941
11174
  const enabledInt = enabled === false ? 0 : 1;
10942
- const existing = app.db.select().from(schedules).where(and7(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
11175
+ const existing = app.db.select().from(schedules).where(and8(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
10943
11176
  if (existing) {
10944
11177
  app.db.update(schedules).set({
10945
11178
  cronExpr,
@@ -10973,13 +11206,13 @@ async function scheduleRoutes(app, opts) {
10973
11206
  diff: { kind, cronExpr, preset, timezone, providers, sourceId }
10974
11207
  });
10975
11208
  opts.onScheduleUpdated?.("upsert", project.id, kind);
10976
- const schedule = app.db.select().from(schedules).where(and7(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
11209
+ const schedule = app.db.select().from(schedules).where(and8(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
10977
11210
  return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
10978
11211
  });
10979
11212
  app.get("/projects/:name/schedule", async (request, reply) => {
10980
11213
  const project = resolveProject(app.db, request.params.name);
10981
11214
  const kind = parseKindParam(request.query?.kind);
10982
- const schedule = app.db.select().from(schedules).where(and7(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
11215
+ const schedule = app.db.select().from(schedules).where(and8(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
10983
11216
  if (!schedule) {
10984
11217
  throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
10985
11218
  }
@@ -10988,7 +11221,7 @@ async function scheduleRoutes(app, opts) {
10988
11221
  app.delete("/projects/:name/schedule", async (request, reply) => {
10989
11222
  const project = resolveProject(app.db, request.params.name);
10990
11223
  const kind = parseKindParam(request.query?.kind);
10991
- const schedule = app.db.select().from(schedules).where(and7(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
11224
+ const schedule = app.db.select().from(schedules).where(and8(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
10992
11225
  if (!schedule) {
10993
11226
  throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
10994
11227
  }
@@ -11027,7 +11260,8 @@ function formatSchedule(row) {
11027
11260
  import crypto12 from "crypto";
11028
11261
  import { eq as eq17 } from "drizzle-orm";
11029
11262
  var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed", "insight.critical", "insight.high"];
11030
- async function notificationRoutes(app) {
11263
+ async function notificationRoutes(app, opts = {}) {
11264
+ const allowLoopback = opts.allowLoopbackWebhooks === true;
11031
11265
  app.get("/notifications/events", async (_request, reply) => {
11032
11266
  return reply.send(VALID_EVENTS);
11033
11267
  });
@@ -11035,7 +11269,7 @@ async function notificationRoutes(app) {
11035
11269
  const project = resolveProject(app.db, request.params.name);
11036
11270
  const { channel, url, events, source } = request.body ?? {};
11037
11271
  if (channel !== "webhook") throw validationError('Only "webhook" channel is supported');
11038
- const urlCheck = await resolveWebhookTarget(url ?? "");
11272
+ const urlCheck = await resolveWebhookTarget(url ?? "", { allowLoopback });
11039
11273
  if (!urlCheck.ok) throw validationError(urlCheck.message);
11040
11274
  if (!events?.length) throw validationError('"events" must be a non-empty array');
11041
11275
  const invalid = events.filter((e) => !VALID_EVENTS.includes(e));
@@ -11096,7 +11330,7 @@ async function notificationRoutes(app) {
11096
11330
  throw notFound("Notification", request.params.id);
11097
11331
  }
11098
11332
  const config = parseJsonColumn(notification.config, { url: "", events: [] });
11099
- const urlCheck = await resolveWebhookTarget(config.url);
11333
+ const urlCheck = await resolveWebhookTarget(config.url, { allowLoopback });
11100
11334
  if (!urlCheck.ok) throw validationError(`Stored webhook URL is invalid: ${urlCheck.message}`);
11101
11335
  const payload = {
11102
11336
  source: "canonry",
@@ -11144,7 +11378,7 @@ function formatNotification(row) {
11144
11378
 
11145
11379
  // ../api-routes/src/google.ts
11146
11380
  import crypto14 from "crypto";
11147
- import { eq as eq18, and as and8, desc as desc8, sql as sql5 } from "drizzle-orm";
11381
+ import { eq as eq18, and as and9, desc as desc8, sql as sql5 } from "drizzle-orm";
11148
11382
 
11149
11383
  // ../integration-google/src/constants.ts
11150
11384
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
@@ -12154,7 +12388,23 @@ async function getValidToken(store, domain, connectionType, clientId, clientSecr
12154
12388
  };
12155
12389
  }
12156
12390
  async function googleRoutes(app, opts) {
12157
- const stateSecret = opts.googleStateSecret ?? "insecure-default-secret";
12391
+ if (opts.googleStateSecret === void 0) {
12392
+ app.log.warn(
12393
+ "googleStateSecret is not configured \u2014 Google OAuth routes will not be registered. Set GOOGLE_STATE_SECRET to enable Google integrations."
12394
+ );
12395
+ return;
12396
+ }
12397
+ if (opts.googleStateSecret === "") {
12398
+ throw new Error(
12399
+ "googleStateSecret is empty. Set a non-empty secret (e.g. `openssl rand -hex 32`) via the GOOGLE_STATE_SECRET environment variable."
12400
+ );
12401
+ }
12402
+ if (opts.googleStateSecret === "insecure-default-secret") {
12403
+ throw new Error(
12404
+ "googleStateSecret is set to the legacy insecure default. Generate a real secret (e.g. `openssl rand -hex 32`) and set GOOGLE_STATE_SECRET."
12405
+ );
12406
+ }
12407
+ const stateSecret = opts.googleStateSecret;
12158
12408
  function getAuthConfig() {
12159
12409
  return opts.getGoogleAuthConfig?.() ?? {};
12160
12410
  }
@@ -12361,7 +12611,7 @@ async function googleRoutes(app, opts) {
12361
12611
  if (page) conditions.push(sql5`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
12362
12612
  const limitVal = Math.max(parseInt(limit ?? "500", 10) || 0, 1);
12363
12613
  const offsetVal = Math.max(parseInt(offset ?? "0", 10) || 0, 0);
12364
- const rows = app.db.select().from(gscSearchData).where(and8(...conditions)).orderBy(desc8(gscSearchData.date)).limit(limitVal).offset(offsetVal).all();
12614
+ const rows = app.db.select().from(gscSearchData).where(and9(...conditions)).orderBy(desc8(gscSearchData.date)).limit(limitVal).offset(offsetVal).all();
12365
12615
  return rows.map((r) => ({
12366
12616
  date: r.date,
12367
12617
  query: r.query,
@@ -12435,7 +12685,7 @@ async function googleRoutes(app, opts) {
12435
12685
  const { url, limit } = request.query;
12436
12686
  const conditions = [eq18(gscUrlInspections.projectId, project.id)];
12437
12687
  if (url) conditions.push(eq18(gscUrlInspections.url, url));
12438
- const rows = app.db.select().from(gscUrlInspections).where(and8(...conditions)).orderBy(desc8(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
12688
+ const rows = app.db.select().from(gscUrlInspections).where(and9(...conditions)).orderBy(desc8(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
12439
12689
  return rows.map((r) => ({
12440
12690
  id: r.id,
12441
12691
  url: r.url,
@@ -12787,7 +13037,7 @@ async function googleRoutes(app, opts) {
12787
13037
 
12788
13038
  // ../api-routes/src/bing.ts
12789
13039
  import crypto15 from "crypto";
12790
- import { eq as eq19, and as and9, desc as desc9 } from "drizzle-orm";
13040
+ import { eq as eq19, and as and10, desc as desc9 } from "drizzle-orm";
12791
13041
 
12792
13042
  // ../integration-bing/src/constants.ts
12793
13043
  var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
@@ -13201,7 +13451,7 @@ async function bingRoutes(app, opts) {
13201
13451
  requireConnectionStore();
13202
13452
  const project = resolveProject(app.db, request.params.name);
13203
13453
  const { url, limit } = request.query;
13204
- const whereClause = url ? and9(eq19(bingUrlInspections.projectId, project.id), eq19(bingUrlInspections.url, url)) : eq19(bingUrlInspections.projectId, project.id);
13454
+ const whereClause = url ? and10(eq19(bingUrlInspections.projectId, project.id), eq19(bingUrlInspections.url, url)) : eq19(bingUrlInspections.projectId, project.id);
13205
13455
  const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc9(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
13206
13456
  return filtered.map((r) => ({
13207
13457
  id: r.id,
@@ -13433,7 +13683,7 @@ async function bingRoutes(app, opts) {
13433
13683
  import fs from "fs";
13434
13684
  import path from "path";
13435
13685
  import os2 from "os";
13436
- import { eq as eq20, and as and10 } from "drizzle-orm";
13686
+ import { eq as eq20, and as and11 } from "drizzle-orm";
13437
13687
  function getScreenshotDir() {
13438
13688
  return path.join(os2.homedir(), ".canonry", "screenshots");
13439
13689
  }
@@ -13506,12 +13756,12 @@ async function cdpRoutes(app, opts) {
13506
13756
  async (request, reply) => {
13507
13757
  const project = resolveProject(app.db, request.params.name);
13508
13758
  const { runId } = request.params;
13509
- const run = app.db.select().from(runs).where(and10(eq20(runs.id, runId), eq20(runs.projectId, project.id))).get();
13759
+ const run = app.db.select().from(runs).where(and11(eq20(runs.id, runId), eq20(runs.projectId, project.id))).get();
13510
13760
  if (!run) {
13511
13761
  const err = notFound("Run", runId);
13512
13762
  return reply.code(err.statusCode).send(err.toJSON());
13513
13763
  }
13514
- const snapshots = app.db.select({
13764
+ const snapshots = filterTrackedSnapshots(app.db.select({
13515
13765
  id: querySnapshots.id,
13516
13766
  queryId: querySnapshots.queryId,
13517
13767
  provider: querySnapshots.provider,
@@ -13519,7 +13769,7 @@ async function cdpRoutes(app, opts) {
13519
13769
  citedDomains: querySnapshots.citedDomains,
13520
13770
  screenshotPath: querySnapshots.screenshotPath,
13521
13771
  rawResponse: querySnapshots.rawResponse
13522
- }).from(querySnapshots).where(eq20(querySnapshots.runId, runId)).all();
13772
+ }).from(querySnapshots).where(eq20(querySnapshots.runId, runId)).all());
13523
13773
  const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq20(queries.projectId, project.id)).all();
13524
13774
  const queryMap = new Map(queryRows.map((q) => [q.id, q.query]));
13525
13775
  const byQuery = /* @__PURE__ */ new Map();
@@ -13603,7 +13853,7 @@ async function cdpRoutes(app, opts) {
13603
13853
 
13604
13854
  // ../api-routes/src/ga.ts
13605
13855
  import crypto16 from "crypto";
13606
- import { eq as eq21, desc as desc10, and as and11, sql as sql6 } from "drizzle-orm";
13856
+ import { eq as eq21, desc as desc10, and as and12, sql as sql6 } from "drizzle-orm";
13607
13857
  function gaLog(level, action, ctx) {
13608
13858
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
13609
13859
  const stream = level === "error" ? process.stderr : process.stdout;
@@ -13898,7 +14148,7 @@ async function ga4Routes(app, opts) {
13898
14148
  app.db.transaction((tx) => {
13899
14149
  if (syncTraffic) {
13900
14150
  tx.delete(gaTrafficSnapshots).where(
13901
- and11(
14151
+ and12(
13902
14152
  eq21(gaTrafficSnapshots.projectId, project.id),
13903
14153
  sql6`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
13904
14154
  sql6`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
@@ -13922,7 +14172,7 @@ async function ga4Routes(app, opts) {
13922
14172
  }
13923
14173
  if (syncAi) {
13924
14174
  tx.delete(gaAiReferrals).where(
13925
- and11(
14175
+ and12(
13926
14176
  eq21(gaAiReferrals.projectId, project.id),
13927
14177
  sql6`${gaAiReferrals.date} >= ${summary.periodStart}`,
13928
14178
  sql6`${gaAiReferrals.date} <= ${summary.periodEnd}`
@@ -13948,7 +14198,7 @@ async function ga4Routes(app, opts) {
13948
14198
  }
13949
14199
  if (syncSocial) {
13950
14200
  tx.delete(gaSocialReferrals).where(
13951
- and11(
14201
+ and12(
13952
14202
  eq21(gaSocialReferrals.projectId, project.id),
13953
14203
  sql6`${gaSocialReferrals.date} >= ${summary.periodStart}`,
13954
14204
  sql6`${gaSocialReferrals.date} <= ${summary.periodEnd}`
@@ -14052,7 +14302,7 @@ async function ga4Routes(app, opts) {
14052
14302
  totalDirectSessions: gaTrafficWindowSummaries.totalDirectSessions,
14053
14303
  totalUsers: gaTrafficWindowSummaries.totalUsers
14054
14304
  }).from(gaTrafficWindowSummaries).where(
14055
- and11(
14305
+ and12(
14056
14306
  eq21(gaTrafficWindowSummaries.projectId, project.id),
14057
14307
  eq21(gaTrafficWindowSummaries.windowKey, window)
14058
14308
  )
@@ -14061,7 +14311,7 @@ async function ga4Routes(app, opts) {
14061
14311
  totalSessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
14062
14312
  totalOrganicSessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
14063
14313
  totalUsers: sql6`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
14064
- }).from(gaTrafficSnapshots).where(and11(...snapshotConditions)).get() : null;
14314
+ }).from(gaTrafficSnapshots).where(and12(...snapshotConditions)).get() : null;
14065
14315
  const summaryRow = cutoffDate ? windowSummaryRow ?? snapshotTotalsRow : app.db.select({
14066
14316
  totalSessions: gaTrafficSummaries.totalSessions,
14067
14317
  totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
@@ -14069,7 +14319,7 @@ async function ga4Routes(app, opts) {
14069
14319
  }).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).get();
14070
14320
  const directTotalRow = windowSummaryRow ? { totalDirectSessions: windowSummaryRow.totalDirectSessions } : app.db.select({
14071
14321
  totalDirectSessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
14072
- }).from(gaTrafficSnapshots).where(and11(...snapshotConditions)).get();
14322
+ }).from(gaTrafficSnapshots).where(and12(...snapshotConditions)).get();
14073
14323
  const summaryMeta = app.db.select({
14074
14324
  periodStart: gaTrafficSummaries.periodStart,
14075
14325
  periodEnd: gaTrafficSummaries.periodEnd
@@ -14080,14 +14330,14 @@ async function ga4Routes(app, opts) {
14080
14330
  organicSessions: sql6`SUM(${gaTrafficSnapshots.organicSessions})`,
14081
14331
  directSessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
14082
14332
  users: sql6`SUM(${gaTrafficSnapshots.users})`
14083
- }).from(gaTrafficSnapshots).where(and11(...snapshotConditions)).groupBy(sql6`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql6`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
14333
+ }).from(gaTrafficSnapshots).where(and12(...snapshotConditions)).groupBy(sql6`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql6`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
14084
14334
  const aiReferralRows = app.db.select({
14085
14335
  source: gaAiReferrals.source,
14086
14336
  medium: gaAiReferrals.medium,
14087
14337
  sourceDimension: gaAiReferrals.sourceDimension,
14088
14338
  sessions: sql6`SUM(${gaAiReferrals.sessions})`,
14089
14339
  users: sql6`SUM(${gaAiReferrals.users})`
14090
- }).from(gaAiReferrals).where(and11(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).all();
14340
+ }).from(gaAiReferrals).where(and12(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).all();
14091
14341
  const aiReferralLandingPageRows = app.db.select({
14092
14342
  source: gaAiReferrals.source,
14093
14343
  medium: gaAiReferrals.medium,
@@ -14095,7 +14345,7 @@ async function ga4Routes(app, opts) {
14095
14345
  landingPage: sql6`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
14096
14346
  sessions: sql6`SUM(${gaAiReferrals.sessions})`,
14097
14347
  users: sql6`SUM(${gaAiReferrals.users})`
14098
- }).from(gaAiReferrals).where(and11(...aiConditions)).groupBy(
14348
+ }).from(gaAiReferrals).where(and12(...aiConditions)).groupBy(
14099
14349
  gaAiReferrals.source,
14100
14350
  gaAiReferrals.medium,
14101
14351
  gaAiReferrals.sourceDimension,
@@ -14132,7 +14382,7 @@ async function ga4Routes(app, opts) {
14132
14382
  channelGroup: gaAiReferrals.channelGroup,
14133
14383
  sessions: sql6`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
14134
14384
  users: sql6`COALESCE(SUM(${gaAiReferrals.users}), 0)`
14135
- }).from(gaAiReferrals).where(and11(...aiConditions, eq21(gaAiReferrals.sourceDimension, "session"))).groupBy(gaAiReferrals.channelGroup).all();
14385
+ }).from(gaAiReferrals).where(and12(...aiConditions, eq21(gaAiReferrals.sourceDimension, "session"))).groupBy(gaAiReferrals.channelGroup).all();
14136
14386
  const aiSessionsByChannelGroup = /* @__PURE__ */ new Map();
14137
14387
  let aiBySessionUsers = 0;
14138
14388
  for (const row of aiBySessionRows) {
@@ -14146,11 +14396,11 @@ async function ga4Routes(app, opts) {
14146
14396
  channelGroup: gaSocialReferrals.channelGroup,
14147
14397
  sessions: sql6`SUM(${gaSocialReferrals.sessions})`,
14148
14398
  users: sql6`SUM(${gaSocialReferrals.users})`
14149
- }).from(gaSocialReferrals).where(and11(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql6`SUM(${gaSocialReferrals.sessions}) DESC`).all();
14399
+ }).from(gaSocialReferrals).where(and12(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql6`SUM(${gaSocialReferrals.sessions}) DESC`).all();
14150
14400
  const socialTotals = app.db.select({
14151
14401
  sessions: sql6`SUM(${gaSocialReferrals.sessions})`,
14152
14402
  users: sql6`SUM(${gaSocialReferrals.users})`
14153
- }).from(gaSocialReferrals).where(and11(...socialConditions)).get();
14403
+ }).from(gaSocialReferrals).where(and12(...socialConditions)).get();
14154
14404
  const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).orderBy(desc10(gaTrafficSummaries.syncedAt)).limit(1).get();
14155
14405
  const total = summaryRow?.totalSessions ?? 0;
14156
14406
  const totalDirectSessions = directTotalRow?.totalDirectSessions ?? 0;
@@ -14241,7 +14491,7 @@ async function ga4Routes(app, opts) {
14241
14491
  sourceDimension: gaAiReferrals.sourceDimension,
14242
14492
  sessions: sql6`SUM(${gaAiReferrals.sessions})`,
14243
14493
  users: sql6`SUM(${gaAiReferrals.users})`
14244
- }).from(gaAiReferrals).where(and11(...conditions)).groupBy(
14494
+ }).from(gaAiReferrals).where(and12(...conditions)).groupBy(
14245
14495
  gaAiReferrals.date,
14246
14496
  gaAiReferrals.source,
14247
14497
  gaAiReferrals.medium,
@@ -14263,7 +14513,7 @@ async function ga4Routes(app, opts) {
14263
14513
  channelGroup: gaSocialReferrals.channelGroup,
14264
14514
  sessions: gaSocialReferrals.sessions,
14265
14515
  users: gaSocialReferrals.users
14266
- }).from(gaSocialReferrals).where(and11(...conditions)).orderBy(gaSocialReferrals.date).all();
14516
+ }).from(gaSocialReferrals).where(and12(...conditions)).orderBy(gaSocialReferrals.date).all();
14267
14517
  return rows;
14268
14518
  });
14269
14519
  app.get("/projects/:name/ga/social-referral-trend", async (request, _reply) => {
@@ -14276,7 +14526,7 @@ async function ga4Routes(app, opts) {
14276
14526
  d.setDate(d.getDate() - n);
14277
14527
  return fmt(d);
14278
14528
  };
14279
- const sumSocial = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and11(
14529
+ const sumSocial = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and12(
14280
14530
  eq21(gaSocialReferrals.projectId, project.id),
14281
14531
  sql6`${gaSocialReferrals.date} >= ${from}`,
14282
14532
  sql6`${gaSocialReferrals.date} < ${to}`
@@ -14289,7 +14539,7 @@ async function ga4Routes(app, opts) {
14289
14539
  const sourceCurrent = app.db.select({
14290
14540
  source: gaSocialReferrals.source,
14291
14541
  sessions: sql6`SUM(${gaSocialReferrals.sessions})`
14292
- }).from(gaSocialReferrals).where(and11(
14542
+ }).from(gaSocialReferrals).where(and12(
14293
14543
  eq21(gaSocialReferrals.projectId, project.id),
14294
14544
  sql6`${gaSocialReferrals.date} >= ${daysAgo2(7)}`,
14295
14545
  sql6`${gaSocialReferrals.date} < ${fmt(today)}`
@@ -14297,7 +14547,7 @@ async function ga4Routes(app, opts) {
14297
14547
  const sourcePrev = app.db.select({
14298
14548
  source: gaSocialReferrals.source,
14299
14549
  sessions: sql6`SUM(${gaSocialReferrals.sessions})`
14300
- }).from(gaSocialReferrals).where(and11(
14550
+ }).from(gaSocialReferrals).where(and12(
14301
14551
  eq21(gaSocialReferrals.projectId, project.id),
14302
14552
  sql6`${gaSocialReferrals.date} >= ${daysAgo2(14)}`,
14303
14553
  sql6`${gaSocialReferrals.date} < ${daysAgo2(7)}`
@@ -14339,16 +14589,16 @@ async function ga4Routes(app, opts) {
14339
14589
  return fmt(d);
14340
14590
  };
14341
14591
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
14342
- const sumTotal = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and11(eq21(gaTrafficSnapshots.projectId, project.id), sql6`${gaTrafficSnapshots.date} >= ${from}`, sql6`${gaTrafficSnapshots.date} < ${to}`)).get();
14343
- const sumOrganic = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and11(eq21(gaTrafficSnapshots.projectId, project.id), sql6`${gaTrafficSnapshots.date} >= ${from}`, sql6`${gaTrafficSnapshots.date} < ${to}`)).get();
14344
- const sumDirect = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and11(eq21(gaTrafficSnapshots.projectId, project.id), sql6`${gaTrafficSnapshots.date} >= ${from}`, sql6`${gaTrafficSnapshots.date} < ${to}`)).get();
14345
- const sumAi = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and11(
14592
+ const sumTotal = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and12(eq21(gaTrafficSnapshots.projectId, project.id), sql6`${gaTrafficSnapshots.date} >= ${from}`, sql6`${gaTrafficSnapshots.date} < ${to}`)).get();
14593
+ const sumOrganic = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and12(eq21(gaTrafficSnapshots.projectId, project.id), sql6`${gaTrafficSnapshots.date} >= ${from}`, sql6`${gaTrafficSnapshots.date} < ${to}`)).get();
14594
+ const sumDirect = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and12(eq21(gaTrafficSnapshots.projectId, project.id), sql6`${gaTrafficSnapshots.date} >= ${from}`, sql6`${gaTrafficSnapshots.date} < ${to}`)).get();
14595
+ const sumAi = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and12(
14346
14596
  eq21(gaAiReferrals.projectId, project.id),
14347
14597
  sql6`${gaAiReferrals.date} >= ${from}`,
14348
14598
  sql6`${gaAiReferrals.date} < ${to}`,
14349
14599
  eq21(gaAiReferrals.sourceDimension, "session")
14350
14600
  )).get();
14351
- const sumSocial = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and11(eq21(gaSocialReferrals.projectId, project.id), sql6`${gaSocialReferrals.date} >= ${from}`, sql6`${gaSocialReferrals.date} < ${to}`)).get();
14601
+ const sumSocial = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and12(eq21(gaSocialReferrals.projectId, project.id), sql6`${gaSocialReferrals.date} >= ${from}`, sql6`${gaSocialReferrals.date} < ${to}`)).get();
14352
14602
  const todayStr = fmt(today);
14353
14603
  const buildTrend = (sum) => {
14354
14604
  const c7 = sum(daysAgo2(7), todayStr)?.sessions ?? 0;
@@ -14357,13 +14607,13 @@ async function ga4Routes(app, opts) {
14357
14607
  const p30 = sum(daysAgo2(60), daysAgo2(30))?.sessions ?? 0;
14358
14608
  return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
14359
14609
  };
14360
- const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql6`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and11(
14610
+ const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql6`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and12(
14361
14611
  eq21(gaAiReferrals.projectId, project.id),
14362
14612
  sql6`${gaAiReferrals.date} >= ${daysAgo2(7)}`,
14363
14613
  sql6`${gaAiReferrals.date} < ${todayStr}`,
14364
14614
  eq21(gaAiReferrals.sourceDimension, "session")
14365
14615
  )).groupBy(gaAiReferrals.source).all();
14366
- const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql6`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and11(
14616
+ const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql6`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and12(
14367
14617
  eq21(gaAiReferrals.projectId, project.id),
14368
14618
  sql6`${gaAiReferrals.date} >= ${daysAgo2(14)}`,
14369
14619
  sql6`${gaAiReferrals.date} < ${daysAgo2(7)}`,
@@ -14383,8 +14633,8 @@ async function ga4Routes(app, opts) {
14383
14633
  }
14384
14634
  return mover;
14385
14635
  };
14386
- const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql6`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and11(eq21(gaSocialReferrals.projectId, project.id), sql6`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql6`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
14387
- const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql6`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and11(eq21(gaSocialReferrals.projectId, project.id), sql6`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql6`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
14636
+ const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql6`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and12(eq21(gaSocialReferrals.projectId, project.id), sql6`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql6`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
14637
+ const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql6`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and12(eq21(gaSocialReferrals.projectId, project.id), sql6`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql6`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
14388
14638
  return {
14389
14639
  total: buildTrend(sumTotal),
14390
14640
  organic: buildTrend(sumOrganic),
@@ -14406,7 +14656,7 @@ async function ga4Routes(app, opts) {
14406
14656
  sessions: sql6`SUM(${gaTrafficSnapshots.sessions})`,
14407
14657
  organicSessions: sql6`SUM(${gaTrafficSnapshots.organicSessions})`,
14408
14658
  users: sql6`SUM(${gaTrafficSnapshots.users})`
14409
- }).from(gaTrafficSnapshots).where(and11(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
14659
+ }).from(gaTrafficSnapshots).where(and12(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
14410
14660
  return rows.map((r) => ({
14411
14661
  date: r.date,
14412
14662
  sessions: r.sessions ?? 0,
@@ -16059,7 +16309,7 @@ async function wordpressRoutes(app, opts) {
16059
16309
 
16060
16310
  // ../api-routes/src/backlinks.ts
16061
16311
  import crypto18 from "crypto";
16062
- import { and as and13, asc as asc2, desc as desc11, eq as eq22, sql as sql7 } from "drizzle-orm";
16312
+ import { and as and14, asc as asc2, desc as desc11, eq as eq22, sql as sql7 } from "drizzle-orm";
16063
16313
 
16064
16314
  // ../integration-commoncrawl/src/constants.ts
16065
16315
  import os3 from "os";
@@ -16234,7 +16484,7 @@ function parseContentLength2(value) {
16234
16484
 
16235
16485
  // ../integration-commoncrawl/src/plugin-resolver.ts
16236
16486
  import fs3 from "fs";
16237
- import { createRequire as createRequire2 } from "module";
16487
+ import { createRequire as createRequire3 } from "module";
16238
16488
  import path4 from "path";
16239
16489
  function pluginDirFor(pkgJson) {
16240
16490
  return path4.dirname(pkgJson);
@@ -16253,7 +16503,7 @@ function loadDuckdb(opts = {}) {
16253
16503
  );
16254
16504
  }
16255
16505
  try {
16256
- const pluginRequire = createRequire2(duckdbPkg);
16506
+ const pluginRequire = createRequire3(duckdbPkg);
16257
16507
  return pluginRequire("@duckdb/node-api");
16258
16508
  } catch {
16259
16509
  throw missingDependency(
@@ -16456,7 +16706,7 @@ function pruneCachedRelease(release, opts = {}) {
16456
16706
  }
16457
16707
 
16458
16708
  // ../api-routes/src/backlinks-filter.ts
16459
- import { and as and12, ne as ne2, notLike } from "drizzle-orm";
16709
+ import { and as and13, ne as ne2, notLike } from "drizzle-orm";
16460
16710
  var BACKLINK_FILTER_PATTERNS = [
16461
16711
  "*.google.com",
16462
16712
  "*.googleusercontent.com",
@@ -16479,7 +16729,7 @@ function backlinkCrawlerExclusionClause() {
16479
16729
  conditions.push(ne2(backlinkDomains.linkingDomain, pattern));
16480
16730
  }
16481
16731
  }
16482
- const combined = and12(...conditions);
16732
+ const combined = and13(...conditions);
16483
16733
  if (!combined) throw new Error("BACKLINK_FILTER_PATTERNS is unexpectedly empty");
16484
16734
  return combined;
16485
16735
  }
@@ -16540,7 +16790,7 @@ function mapRunRow(row) {
16540
16790
  };
16541
16791
  }
16542
16792
  function latestSummaryForProject(db, projectId, release) {
16543
- const condition = release ? and13(eq22(backlinkSummaries.projectId, projectId), eq22(backlinkSummaries.release, release)) : eq22(backlinkSummaries.projectId, projectId);
16793
+ const condition = release ? and14(eq22(backlinkSummaries.projectId, projectId), eq22(backlinkSummaries.release, release)) : eq22(backlinkSummaries.projectId, projectId);
16544
16794
  return db.select().from(backlinkSummaries).where(condition).orderBy(desc11(backlinkSummaries.queriedAt)).limit(1).get();
16545
16795
  }
16546
16796
  function parseExcludeCrawlers(value) {
@@ -16549,11 +16799,11 @@ function parseExcludeCrawlers(value) {
16549
16799
  return lower === "1" || lower === "true" || lower === "yes";
16550
16800
  }
16551
16801
  function computeFilteredSummary(db, base) {
16552
- const baseDomainCondition = and13(
16802
+ const baseDomainCondition = and14(
16553
16803
  eq22(backlinkDomains.projectId, base.projectId),
16554
16804
  eq22(backlinkDomains.release, base.release)
16555
16805
  );
16556
- const filteredCondition = and13(baseDomainCondition, backlinkCrawlerExclusionClause());
16806
+ const filteredCondition = and14(baseDomainCondition, backlinkCrawlerExclusionClause());
16557
16807
  const unfilteredAgg = db.select({
16558
16808
  count: sql7`count(*)`,
16559
16809
  total: sql7`coalesce(sum(${backlinkDomains.numHosts}), 0)`
@@ -16729,11 +16979,11 @@ async function backlinksRoutes(app, opts) {
16729
16979
  const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
16730
16980
  const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
16731
16981
  const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
16732
- const baseDomainCondition = and13(
16982
+ const baseDomainCondition = and14(
16733
16983
  eq22(backlinkDomains.projectId, project.id),
16734
16984
  eq22(backlinkDomains.release, targetRelease)
16735
16985
  );
16736
- const domainCondition = excludeCrawlers ? and13(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
16986
+ const domainCondition = excludeCrawlers ? and14(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
16737
16987
  const totalRow = app.db.select({ count: sql7`count(*)` }).from(backlinkDomains).where(domainCondition).get();
16738
16988
  const rows = app.db.select({
16739
16989
  linkingDomain: backlinkDomains.linkingDomain,
@@ -16769,7 +17019,7 @@ async function backlinksRoutes(app, opts) {
16769
17019
 
16770
17020
  // ../api-routes/src/traffic.ts
16771
17021
  import crypto20 from "crypto";
16772
- import { and as and14, desc as desc12, eq as eq23, gte as gte2, lte as lte2, sql as sql8 } from "drizzle-orm";
17022
+ import { and as and15, desc as desc12, eq as eq23, gte as gte2, lte as lte2, sql as sql8 } from "drizzle-orm";
16773
17023
 
16774
17024
  // ../integration-cloud-run/src/auth.ts
16775
17025
  import crypto19 from "crypto";
@@ -17837,7 +18087,7 @@ var DEFAULT_WP_MAX_PAGES = 20;
17837
18087
  var DEFAULT_VERCEL_MAX_PAGES = 50;
17838
18088
  var MAX_TRACKED_EVENT_IDS = 1e3;
17839
18089
  var DEFAULT_BACKFILL_DAYS = 30;
17840
- var MAX_BACKFILL_DAYS = 30;
18090
+ var MAX_BACKFILL_DAYS = 90;
17841
18091
  var BACKFILL_MAX_PAGES = 1e3;
17842
18092
  var BACKFILL_SAMPLE_LIMIT = 500;
17843
18093
  function parseSourceConfig(row) {
@@ -17917,21 +18167,21 @@ async function runBackfillTask(options) {
17917
18167
  try {
17918
18168
  app.db.transaction((tx) => {
17919
18169
  tx.delete(crawlerEventsHourly).where(
17920
- and14(
18170
+ and15(
17921
18171
  eq23(crawlerEventsHourly.sourceId, sourceRow.id),
17922
18172
  gte2(crawlerEventsHourly.tsHour, windowStartIso),
17923
18173
  lte2(crawlerEventsHourly.tsHour, windowEndIso)
17924
18174
  )
17925
18175
  ).run();
17926
18176
  tx.delete(aiReferralEventsHourly).where(
17927
- and14(
18177
+ and15(
17928
18178
  eq23(aiReferralEventsHourly.sourceId, sourceRow.id),
17929
18179
  gte2(aiReferralEventsHourly.tsHour, windowStartIso),
17930
18180
  lte2(aiReferralEventsHourly.tsHour, windowEndIso)
17931
18181
  )
17932
18182
  ).run();
17933
18183
  tx.delete(rawEventSamples).where(
17934
- and14(
18184
+ and15(
17935
18185
  eq23(rawEventSamples.sourceId, sourceRow.id),
17936
18186
  gte2(rawEventSamples.ts, windowStartIso),
17937
18187
  lte2(rawEventSamples.ts, windowEndIso)
@@ -18354,7 +18604,8 @@ async function trafficRoutes(app, opts) {
18354
18604
  endTime: windowEnd.toISOString(),
18355
18605
  pageSize,
18356
18606
  maxPages,
18357
- firstSync: isFirstSync
18607
+ firstSync: isFirstSync,
18608
+ requestUrlSubstrings: [project.canonicalDomain]
18358
18609
  });
18359
18610
  allEvents = page.events;
18360
18611
  } catch (e) {
@@ -18680,7 +18931,8 @@ async function trafficRoutes(app, opts) {
18680
18931
  // ring-buffer reseed at the end takes the most-recent IDs from the
18681
18932
  // dedupedEvents anyway.
18682
18933
  firstSync: false,
18683
- orderBy: "timestamp asc"
18934
+ orderBy: "timestamp asc",
18935
+ requestUrlSubstrings: [project.canonicalDomain]
18684
18936
  });
18685
18937
  return page.events;
18686
18938
  };
@@ -18793,25 +19045,25 @@ async function trafficRoutes(app, opts) {
18793
19045
  });
18794
19046
  function buildSourceDetail(projectId, row, since) {
18795
19047
  const crawlerTotals = app.db.select({ total: sql8`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
18796
- and14(
19048
+ and15(
18797
19049
  eq23(crawlerEventsHourly.sourceId, row.id),
18798
19050
  gte2(crawlerEventsHourly.tsHour, since)
18799
19051
  )
18800
19052
  ).get();
18801
19053
  const aiTotals = app.db.select({ total: sql8`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
18802
- and14(
19054
+ and15(
18803
19055
  eq23(aiReferralEventsHourly.sourceId, row.id),
18804
19056
  gte2(aiReferralEventsHourly.tsHour, since)
18805
19057
  )
18806
19058
  ).get();
18807
19059
  const sampleTotals = app.db.select({ total: sql8`COUNT(*)` }).from(rawEventSamples).where(
18808
- and14(
19060
+ and15(
18809
19061
  eq23(rawEventSamples.sourceId, row.id),
18810
19062
  gte2(rawEventSamples.ts, since)
18811
19063
  )
18812
19064
  ).get();
18813
19065
  const latestRun = app.db.select().from(runs).where(
18814
- and14(
19066
+ and15(
18815
19067
  eq23(runs.projectId, projectId),
18816
19068
  eq23(runs.kind, RunKinds["traffic-sync"]),
18817
19069
  eq23(runs.sourceId, row.id)
@@ -18905,7 +19157,7 @@ async function trafficRoutes(app, opts) {
18905
19157
  lte2(crawlerEventsHourly.tsHour, untilIso)
18906
19158
  ];
18907
19159
  if (sourceIdParam) crawlerFilters.push(eq23(crawlerEventsHourly.sourceId, sourceIdParam));
18908
- const crawlerWhere = and14(...crawlerFilters);
19160
+ const crawlerWhere = and15(...crawlerFilters);
18909
19161
  const total = app.db.select({ total: sql8`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(crawlerWhere).get();
18910
19162
  crawlerTotal = Number(total?.total ?? 0);
18911
19163
  const rows = app.db.select().from(crawlerEventsHourly).where(crawlerWhere).orderBy(desc12(crawlerEventsHourly.tsHour)).limit(limit).all();
@@ -18930,7 +19182,7 @@ async function trafficRoutes(app, opts) {
18930
19182
  lte2(aiReferralEventsHourly.tsHour, untilIso)
18931
19183
  ];
18932
19184
  if (sourceIdParam) aiFilters.push(eq23(aiReferralEventsHourly.sourceId, sourceIdParam));
18933
- const aiWhere = and14(...aiFilters);
19185
+ const aiWhere = and15(...aiFilters);
18934
19186
  const total = app.db.select({ total: sql8`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(aiWhere).get();
18935
19187
  aiReferralTotal = Number(total?.total ?? 0);
18936
19188
  const rows = app.db.select().from(aiReferralEventsHourly).where(aiWhere).orderBy(desc12(aiReferralEventsHourly.tsHour)).limit(limit).all();
@@ -19585,7 +19837,7 @@ var providersConfiguredCheck = {
19585
19837
  var PROVIDERS_CHECKS = [providersConfiguredCheck];
19586
19838
 
19587
19839
  // ../api-routes/src/doctor/checks/traffic-source.ts
19588
- import { and as and15, eq as eq24, gte as gte3, ne as ne3, sql as sql9 } from "drizzle-orm";
19840
+ import { and as and16, eq as eq24, gte as gte3, ne as ne3, sql as sql9 } from "drizzle-orm";
19589
19841
  var RECENT_DATA_WARN_DAYS = 7;
19590
19842
  var RECENT_DATA_FAIL_DAYS = 30;
19591
19843
  function skippedNoProject2() {
@@ -19599,7 +19851,7 @@ function skippedNoProject2() {
19599
19851
  function loadProbes(ctx) {
19600
19852
  if (!ctx.project) return [];
19601
19853
  const rows = ctx.db.select().from(trafficSources).where(
19602
- and15(
19854
+ and16(
19603
19855
  eq24(trafficSources.projectId, ctx.project.id),
19604
19856
  ne3(trafficSources.status, TrafficSourceStatuses.archived)
19605
19857
  )
@@ -19680,7 +19932,7 @@ var recentDataCheck = {
19680
19932
  const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
19681
19933
  const recentCrawlers = Number(
19682
19934
  ctx.db.select({ total: sql9`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
19683
- and15(
19935
+ and16(
19684
19936
  eq24(crawlerEventsHourly.projectId, ctx.project.id),
19685
19937
  gte3(crawlerEventsHourly.tsHour, warnCutoff)
19686
19938
  )
@@ -19688,7 +19940,7 @@ var recentDataCheck = {
19688
19940
  );
19689
19941
  const recentReferrals = Number(
19690
19942
  ctx.db.select({ total: sql9`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
19691
- and15(
19943
+ and16(
19692
19944
  eq24(aiReferralEventsHourly.projectId, ctx.project.id),
19693
19945
  gte3(aiReferralEventsHourly.tsHour, warnCutoff)
19694
19946
  )
@@ -19704,7 +19956,7 @@ var recentDataCheck = {
19704
19956
  }
19705
19957
  const olderCrawlers = Number(
19706
19958
  ctx.db.select({ total: sql9`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
19707
- and15(
19959
+ and16(
19708
19960
  eq24(crawlerEventsHourly.projectId, ctx.project.id),
19709
19961
  gte3(crawlerEventsHourly.tsHour, failCutoff)
19710
19962
  )
@@ -19712,7 +19964,7 @@ var recentDataCheck = {
19712
19964
  );
19713
19965
  const olderReferrals = Number(
19714
19966
  ctx.db.select({ total: sql9`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
19715
- and15(
19967
+ and16(
19716
19968
  eq24(aiReferralEventsHourly.projectId, ctx.project.id),
19717
19969
  gte3(aiReferralEventsHourly.tsHour, failCutoff)
19718
19970
  )
@@ -20010,7 +20262,7 @@ async function doctorRoutes(app, opts) {
20010
20262
 
20011
20263
  // ../api-routes/src/discovery/routes.ts
20012
20264
  import crypto21 from "crypto";
20013
- import { and as and16, desc as desc13, eq as eq25, gte as gte4, inArray as inArray8 } from "drizzle-orm";
20265
+ import { and as and17, desc as desc13, eq as eq25, gte as gte4, inArray as inArray8 } from "drizzle-orm";
20014
20266
  var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
20015
20267
  async function discoveryRoutes(app, opts) {
20016
20268
  app.post("/projects/:name/discover/run", async (request, reply) => {
@@ -20042,7 +20294,7 @@ async function discoveryRoutes(app, opts) {
20042
20294
  const now = (/* @__PURE__ */ new Date()).toISOString();
20043
20295
  const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
20044
20296
  const decision = app.db.transaction((tx) => {
20045
- const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and16(
20297
+ const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and17(
20046
20298
  eq25(discoverySessions.projectId, project.id),
20047
20299
  eq25(discoverySessions.icpDescription, icpDescription),
20048
20300
  inArray8(discoverySessions.status, [
@@ -20506,6 +20758,7 @@ async function apiRoutes(app, opts) {
20506
20758
  await api.register(projectRoutes, {
20507
20759
  onProjectDeleted: opts.onProjectDeleted,
20508
20760
  onProjectUpserted: opts.onProjectUpserted,
20761
+ onAliasesChanged: opts.onAliasesChanged,
20509
20762
  validProviderNames: opts.providerAdapters?.map((a) => a.name)
20510
20763
  });
20511
20764
  await api.register(queryRoutes, {
@@ -20520,7 +20773,9 @@ async function apiRoutes(app, opts) {
20520
20773
  await api.register(applyRoutes, {
20521
20774
  onScheduleUpdated: opts.onScheduleUpdated,
20522
20775
  onProjectUpserted: opts.onProjectUpserted,
20776
+ onAliasesChanged: opts.onAliasesChanged,
20523
20777
  validProviderNames: opts.providerAdapters?.map((a) => a.name),
20778
+ allowLoopbackWebhooks: opts.allowLoopbackWebhooks,
20524
20779
  onGoogleConnectionPropertyUpdated: (domain, connectionType, propertyId) => {
20525
20780
  opts.googleConnectionStore?.updateConnection(domain, connectionType, {
20526
20781
  propertyId,
@@ -20551,7 +20806,9 @@ async function apiRoutes(app, opts) {
20551
20806
  onScheduleUpdated: opts.onScheduleUpdated,
20552
20807
  validProviderNames: opts.providerAdapters?.map((a) => a.name)
20553
20808
  });
20554
- await api.register(notificationRoutes);
20809
+ await api.register(notificationRoutes, {
20810
+ allowLoopbackWebhooks: opts.allowLoopbackWebhooks
20811
+ });
20555
20812
  await api.register(telemetryRoutes, {
20556
20813
  getTelemetryStatus: opts.getTelemetryStatus,
20557
20814
  setTelemetryEnabled: opts.setTelemetryEnabled
@@ -20806,7 +21063,7 @@ function isVertexConfig(config) {
20806
21063
  function resolveModel(config) {
20807
21064
  return config.model || DEFAULT_MODEL;
20808
21065
  }
20809
- function createClient(config) {
21066
+ function createClient2(config) {
20810
21067
  if (isVertexConfig(config)) {
20811
21068
  return new GoogleGenAI({
20812
21069
  vertexai: true,
@@ -20846,7 +21103,7 @@ async function healthcheck(config) {
20846
21103
  if (!validation.ok) return validation;
20847
21104
  try {
20848
21105
  const model = resolveModel(config);
20849
- const client = createClient(config);
21106
+ const client = createClient2(config);
20850
21107
  const result = await withRetry(
20851
21108
  () => client.models.generateContent({
20852
21109
  model,
@@ -20873,7 +21130,7 @@ async function healthcheck(config) {
20873
21130
  async function executeTrackedQuery(input) {
20874
21131
  const model = resolveModel(input.config);
20875
21132
  const prompt = buildPrompt(input.query, input.location);
20876
- const client = createClient(input.config);
21133
+ const client = createClient2(input.config);
20877
21134
  try {
20878
21135
  const result = await withRetry(
20879
21136
  () => client.models.generateContent({
@@ -21038,7 +21295,7 @@ function extractDomainFromUri(uri) {
21038
21295
  }
21039
21296
  async function generateText(prompt, config) {
21040
21297
  const model = resolveModel(config);
21041
- const client = createClient(config);
21298
+ const client = createClient2(config);
21042
21299
  const result = await withRetry(
21043
21300
  () => client.models.generateContent({
21044
21301
  model,
@@ -23358,7 +23615,7 @@ import crypto24 from "crypto";
23358
23615
  import fs7 from "fs";
23359
23616
  import path9 from "path";
23360
23617
  import os5 from "os";
23361
- import { and as and17, eq as eq27, inArray as inArray9, sql as sql10 } from "drizzle-orm";
23618
+ import { and as and18, eq as eq27, inArray as inArray9, sql as sql10 } from "drizzle-orm";
23362
23619
 
23363
23620
  // src/run-telemetry.ts
23364
23621
  import crypto23 from "crypto";
@@ -23465,11 +23722,15 @@ function computeCompetitorOverlap(normalized, competitorDomains) {
23465
23722
  function escapeRegExp5(value) {
23466
23723
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23467
23724
  }
23468
- function extractRecommendedCompetitors(answerText, ownDomains, citedDomains, competitorDomains) {
23725
+ function extractRecommendedCompetitors(answerText, ownDomains, citedDomains, competitorDomains, ownBrandNames = []) {
23469
23726
  if (!answerText || answerText.length < 20) return [];
23470
23727
  const ownBrandKeys = new Set(
23471
23728
  ownDomains.flatMap((domain) => collectBrandKeysFromDomain(domain))
23472
23729
  );
23730
+ for (const name of ownBrandNames) {
23731
+ const key = brandKeyFromText(name);
23732
+ if (key.length >= 4) ownBrandKeys.add(key);
23733
+ }
23473
23734
  const knownCompetitorKeys = new Set(
23474
23735
  [...citedDomains, ...competitorDomains].flatMap((domain) => collectBrandKeysFromDomain(domain)).filter((key) => !ownBrandKeys.has(key))
23475
23736
  );
@@ -23737,7 +23998,7 @@ var JobRunner = class {
23737
23998
  throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
23738
23999
  }
23739
24000
  if (existingRun.status === "queued") {
23740
- this.db.update(runs).set({ status: "running", startedAt: now }).where(and17(eq27(runs.id, runId), eq27(runs.status, "queued"))).run();
24001
+ this.db.update(runs).set({ status: "running", startedAt: now }).where(and18(eq27(runs.id, runId), eq27(runs.status, "queued"))).run();
23741
24002
  }
23742
24003
  this.throwIfRunCancelled(runId);
23743
24004
  const project = this.db.select().from(projects).where(eq27(projects.id, projectId)).get();
@@ -23762,13 +24023,17 @@ var JobRunner = class {
23762
24023
  }
23763
24024
  log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
23764
24025
  const scopedQueryNames = parseJsonColumn(existingRun.queries, null);
23765
- projectQueries = scopedQueryNames ? this.db.select().from(queries).where(and17(eq27(queries.projectId, projectId), inArray9(queries.query, scopedQueryNames))).all() : this.db.select().from(queries).where(eq27(queries.projectId, projectId)).all();
24026
+ projectQueries = scopedQueryNames ? this.db.select().from(queries).where(and18(eq27(queries.projectId, projectId), inArray9(queries.query, scopedQueryNames))).all() : this.db.select().from(queries).where(eq27(queries.projectId, projectId)).all();
23766
24027
  const projectCompetitors = this.db.select().from(competitors).where(eq27(competitors.projectId, projectId)).all();
23767
24028
  const competitorDomains = projectCompetitors.map((c) => c.domain);
23768
24029
  const allDomains = effectiveDomains({
23769
24030
  canonicalDomain: project.canonicalDomain,
23770
24031
  ownedDomains: parseJsonColumn(project.ownedDomains, [])
23771
24032
  });
24033
+ const allBrandNames = effectiveBrandNames({
24034
+ displayName: project.displayName,
24035
+ aliases: parseJsonColumn(project.aliases, [])
24036
+ });
23772
24037
  const executionContext = {
23773
24038
  providerCount: activeProviders.length,
23774
24039
  providers: activeProviders.map((provider) => provider.adapter.name),
@@ -23829,7 +24094,7 @@ var JobRunner = class {
23829
24094
  const citationState = determineCitationState(normalized, allDomains);
23830
24095
  const answerMentioned = determineAnswerMentioned(
23831
24096
  normalized.answerText,
23832
- project.displayName,
24097
+ allBrandNames,
23833
24098
  allDomains
23834
24099
  );
23835
24100
  const overlap = computeCompetitorOverlap(normalized, competitorDomains);
@@ -23837,7 +24102,8 @@ var JobRunner = class {
23837
24102
  normalized.answerText,
23838
24103
  allDomains,
23839
24104
  normalized.citedDomains,
23840
- competitorDomains
24105
+ competitorDomains,
24106
+ allBrandNames
23841
24107
  );
23842
24108
  let screenshotRelPath = null;
23843
24109
  if (raw.screenshotPath && fs7.existsSync(raw.screenshotPath)) {
@@ -23851,6 +24117,7 @@ var JobRunner = class {
23851
24117
  id: snapshotId,
23852
24118
  runId,
23853
24119
  queryId: q.id,
24120
+ queryText: q.query,
23854
24121
  provider: providerName,
23855
24122
  model: raw.model,
23856
24123
  citationState,
@@ -23874,6 +24141,7 @@ var JobRunner = class {
23874
24141
  id: crypto24.randomUUID(),
23875
24142
  runId,
23876
24143
  queryId: q.id,
24144
+ queryText: q.query,
23877
24145
  provider: providerName,
23878
24146
  model: raw.model,
23879
24147
  citationState,
@@ -24098,7 +24366,7 @@ function buildPhases(input) {
24098
24366
 
24099
24367
  // src/gsc-sync.ts
24100
24368
  import crypto25 from "crypto";
24101
- import { eq as eq28, and as and18, sql as sql11 } from "drizzle-orm";
24369
+ import { eq as eq28, and as and19, sql as sql11 } from "drizzle-orm";
24102
24370
  var log2 = createLogger("GscSync");
24103
24371
  function formatDate3(d) {
24104
24372
  return d.toISOString().split("T")[0];
@@ -24150,7 +24418,7 @@ async function executeGscSync(db, runId, projectId, opts) {
24150
24418
  });
24151
24419
  log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
24152
24420
  db.delete(gscSearchData).where(
24153
- and18(
24421
+ and19(
24154
24422
  eq28(gscSearchData.projectId, projectId),
24155
24423
  sql11`${gscSearchData.date} >= ${startDate}`,
24156
24424
  sql11`${gscSearchData.date} <= ${endDate}`
@@ -24239,7 +24507,7 @@ async function executeGscSync(db, runId, projectId, opts) {
24239
24507
  }
24240
24508
  }
24241
24509
  const snapshotDate = formatDate3(/* @__PURE__ */ new Date());
24242
- db.delete(gscCoverageSnapshots).where(and18(eq28(gscCoverageSnapshots.projectId, projectId), eq28(gscCoverageSnapshots.date, snapshotDate))).run();
24510
+ db.delete(gscCoverageSnapshots).where(and19(eq28(gscCoverageSnapshots.projectId, projectId), eq28(gscCoverageSnapshots.date, snapshotDate))).run();
24243
24511
  db.insert(gscCoverageSnapshots).values({
24244
24512
  id: crypto25.randomUUID(),
24245
24513
  projectId,
@@ -24262,7 +24530,7 @@ async function executeGscSync(db, runId, projectId, opts) {
24262
24530
 
24263
24531
  // src/gsc-inspect-sitemap.ts
24264
24532
  import crypto26 from "crypto";
24265
- import { eq as eq29, and as and19 } from "drizzle-orm";
24533
+ import { eq as eq29, and as and20 } from "drizzle-orm";
24266
24534
 
24267
24535
  // src/sitemap-parser.ts
24268
24536
  var log3 = createLogger("SitemapParser");
@@ -24478,7 +24746,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
24478
24746
  }
24479
24747
  }
24480
24748
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
24481
- db.delete(gscCoverageSnapshots).where(and19(eq29(gscCoverageSnapshots.projectId, projectId), eq29(gscCoverageSnapshots.date, snapshotDate))).run();
24749
+ db.delete(gscCoverageSnapshots).where(and20(eq29(gscCoverageSnapshots.projectId, projectId), eq29(gscCoverageSnapshots.date, snapshotDate))).run();
24482
24750
  db.insert(gscCoverageSnapshots).values({
24483
24751
  id: crypto26.randomUUID(),
24484
24752
  projectId,
@@ -24689,7 +24957,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
24689
24957
  // src/commoncrawl-sync.ts
24690
24958
  import crypto28 from "crypto";
24691
24959
  import path10 from "path";
24692
- import { and as and20, eq as eq31, sql as sql12 } from "drizzle-orm";
24960
+ import { and as and21, eq as eq31, sql as sql12 } from "drizzle-orm";
24693
24961
  var log6 = createLogger("CommonCrawlSync");
24694
24962
  var INSERT_CHUNK_SIZE = 1e4;
24695
24963
  function defaultDeps() {
@@ -24880,7 +25148,7 @@ function computeSummary(rows) {
24880
25148
  // src/backlink-extract.ts
24881
25149
  import crypto29 from "crypto";
24882
25150
  import fs8 from "fs";
24883
- import { and as and21, desc as desc15, eq as eq32 } from "drizzle-orm";
25151
+ import { and as and22, desc as desc15, eq as eq32 } from "drizzle-orm";
24884
25152
  var log7 = createLogger("BacklinkExtract");
24885
25153
  function defaultDeps2() {
24886
25154
  return {
@@ -24926,7 +25194,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
24926
25194
  const targetDomain = project.canonicalDomain;
24927
25195
  db.transaction((tx) => {
24928
25196
  tx.delete(backlinkDomains).where(
24929
- and21(eq32(backlinkDomains.projectId, projectId), eq32(backlinkDomains.release, release))
25197
+ and22(eq32(backlinkDomains.projectId, projectId), eq32(backlinkDomains.release, release))
24930
25198
  ).run();
24931
25199
  if (rows.length > 0) {
24932
25200
  const values = rows.map((r) => ({
@@ -24997,9 +25265,10 @@ function computeSummary2(rows) {
24997
25265
 
24998
25266
  // src/discovery-run.ts
24999
25267
  import crypto30 from "crypto";
25000
- import { eq as eq33 } from "drizzle-orm";
25268
+ import { and as and23, eq as eq33 } from "drizzle-orm";
25001
25269
  var log8 = createLogger("DiscoveryRun");
25002
25270
  var DEFAULT_SEED_COUNT = 30;
25271
+ var QUERIES_PER_INTENT_BUCKET = 6;
25003
25272
  async function executeDiscoveryRun(opts) {
25004
25273
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
25005
25274
  opts.db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq33(runs.id, opts.runId)).run();
@@ -25204,6 +25473,7 @@ function buildLocationConstraint(locations) {
25204
25473
  }
25205
25474
  function buildSeedPrompt(input) {
25206
25475
  const locationConstraint = buildLocationConstraint(input.locations ?? []);
25476
+ const currentYear = (/* @__PURE__ */ new Date()).getFullYear();
25207
25477
  return [
25208
25478
  "You are an AEO (Answer Engine Optimization) analyst expanding a tracked-query basket for a customer.",
25209
25479
  "",
@@ -25211,14 +25481,15 @@ function buildSeedPrompt(input) {
25211
25481
  `ICP: ${input.icpDescription}`,
25212
25482
  ...locationConstraint.length > 0 ? ["", ...locationConstraint] : [],
25213
25483
  "",
25214
- "Brainstorm a wide set of queries a member of this ICP would type into an AI answer engine (Gemini, ChatGPT, Perplexity) when they are about to make a decision in this space. Aim for 30+ candidates covering:",
25215
- ' - Comparison queries ("best X for Y")',
25216
- " - Specific feature / capability queries",
25217
- " - Pricing / vendor-shortlist queries",
25218
- " - Workflow / how-to queries",
25219
- " - Adjacent jobs-to-be-done queries",
25484
+ "Brainstorm queries a member of this ICP would type into an AI answer engine (Gemini, ChatGPT, Perplexity). Generate candidates across the five intent buckets below \u2014 these are SEMANTICALLY DISTINCT search intents, not stylistic variants. A query should fit one bucket cleanly. Diversity across buckets is the point: it keeps the list from collapsing into near-synonyms of a single intent.",
25485
+ "",
25486
+ ' 1. Informational \u2014 the searcher wants to understand a concept, market, or problem. Templates: "what is X", "how does X work", "X explained", "why X matters".',
25487
+ ` 2. Commercial \u2014 the searcher is researching category leaders before a purchase. Templates: "best X for Y", "top X ${currentYear}", "leading X providers", "X for [use case]".`,
25488
+ ' 3. Navigational \u2014 the searcher is looking for a specific brand, place, or directory. Templates: "X near me", "X reviews", "X website", "X directory".',
25489
+ ' 4. Comparative \u2014 the searcher is weighing named alternatives head-to-head. Templates: "X vs Y", "X or Y for Z", "alternatives to X".',
25490
+ ' 5. Transactional \u2014 the searcher is ready to act on a purchase or booking. Templates: "book X", "X pricing", "X discount code", "buy X online".',
25220
25491
  "",
25221
- "Return ONE query per line. Plain text only \u2014 no numbering, bullets, quotes, or commentary."
25492
+ `Generate EXACTLY ${QUERIES_PER_INTENT_BUCKET} queries per bucket \u2014 ${QUERIES_PER_INTENT_BUCKET * 5} total. Return ONE query per line. Plain text only \u2014 no numbering, bullets, quotes, bucket labels, or commentary.`
25222
25493
  ].join("\n");
25223
25494
  }
25224
25495
  function parseQueryLines(text, max) {
@@ -25253,33 +25524,40 @@ function writeDiscoveryInsight(db, input) {
25253
25524
  aspirational: buckets.aspirational,
25254
25525
  totalProbes
25255
25526
  });
25256
- db.insert(insights).values({
25257
- id: crypto30.randomUUID(),
25258
- projectId: input.projectId,
25259
- runId: input.runId,
25260
- type: "discovery.basket-divergence",
25261
- severity,
25262
- title,
25263
- // query/provider fields don't fit the visibility-snapshot model for a
25264
- // session-level insight. Use the session marker so the
25265
- // (query, provider) index stays distinct across sessions; PR 5 will
25266
- // formalize a session-scoped insight subtype.
25267
- query: `discovery:${input.sessionId}`,
25268
- provider: input.seedProvider,
25269
- recommendation: JSON.stringify({
25270
- action: "review-discovered-basket",
25271
- summary: `Run \`canonry discover show ${input.sessionId} --format json\` to inspect the per-query breakdown, then \`canonry discover promote <project> ${input.sessionId}\` to merge cited + aspirational findings into the project.`,
25272
- bucketCounts: buckets,
25273
- topCompetitors
25274
- }),
25275
- cause: JSON.stringify({
25276
- sessionId: input.sessionId,
25277
- totalProbes,
25278
- seedProvider: input.seedProvider
25279
- }),
25280
- dismissed: false,
25281
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
25282
- }).run();
25527
+ db.transaction((tx) => {
25528
+ tx.update(insights).set({ dismissed: true }).where(and23(
25529
+ eq33(insights.projectId, input.projectId),
25530
+ eq33(insights.type, "discovery.basket-divergence"),
25531
+ eq33(insights.dismissed, false)
25532
+ )).run();
25533
+ tx.insert(insights).values({
25534
+ id: crypto30.randomUUID(),
25535
+ projectId: input.projectId,
25536
+ runId: input.runId,
25537
+ type: "discovery.basket-divergence",
25538
+ severity,
25539
+ title,
25540
+ // query/provider fields don't fit the visibility-snapshot model for
25541
+ // a session-level insight. Use the session marker so the
25542
+ // (query, provider) index stays distinct across sessions; PR 5 will
25543
+ // formalize a session-scoped insight subtype.
25544
+ query: `discovery:${input.sessionId}`,
25545
+ provider: input.seedProvider,
25546
+ recommendation: JSON.stringify({
25547
+ action: "review-discovered-basket",
25548
+ summary: `Run \`canonry discover show ${input.sessionId} --format json\` to inspect the per-query breakdown, then \`canonry discover promote <project> ${input.sessionId}\` to merge cited + aspirational findings into the project.`,
25549
+ bucketCounts: buckets,
25550
+ topCompetitors
25551
+ }),
25552
+ cause: JSON.stringify({
25553
+ sessionId: input.sessionId,
25554
+ totalProbes,
25555
+ seedProvider: input.seedProvider
25556
+ }),
25557
+ dismissed: false,
25558
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
25559
+ }).run();
25560
+ });
25283
25561
  }
25284
25562
  function buildDiscoveryInsightTitle(input) {
25285
25563
  const parts = [];
@@ -25290,6 +25568,512 @@ function buildDiscoveryInsightTitle(input) {
25290
25568
  return parts.join(" \u2022 ");
25291
25569
  }
25292
25570
 
25571
+ // src/commands/backfill.ts
25572
+ import { and as and24, eq as eq34, inArray as inArray10 } from "drizzle-orm";
25573
+ var SNAPSHOT_BATCH_SIZE = 500;
25574
+ async function backfillAnswerVisibilityCommand(opts) {
25575
+ const config = loadConfig();
25576
+ const db = createClient(config.database);
25577
+ migrate(db);
25578
+ const projectFilter = opts?.project?.trim();
25579
+ const scopedProjects = projectFilter ? db.select().from(projects).where(eq34(projects.name, projectFilter)).all() : db.select().from(projects).all();
25580
+ let examined = 0;
25581
+ let updated = 0;
25582
+ let mentioned = 0;
25583
+ let reparsed = 0;
25584
+ let providerErrors = 0;
25585
+ if (scopedProjects.length > 0) {
25586
+ const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and24(
25587
+ eq34(runs.kind, RunKinds["answer-visibility"]),
25588
+ inArray10(runs.projectId, scopedProjects.map((project) => project.id))
25589
+ )).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(eq34(runs.kind, RunKinds["answer-visibility"])).all();
25590
+ const runIdsByProject = /* @__PURE__ */ new Map();
25591
+ for (const run of runRows) {
25592
+ const existing = runIdsByProject.get(run.projectId);
25593
+ if (existing) existing.push(run.id);
25594
+ else runIdsByProject.set(run.projectId, [run.id]);
25595
+ }
25596
+ for (const project of scopedProjects) {
25597
+ const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq34(competitors.projectId, project.id)).all().map((row) => row.domain);
25598
+ const runIds = runIdsByProject.get(project.id) ?? [];
25599
+ if (runIds.length === 0) continue;
25600
+ const projectDomains = effectiveDomains({
25601
+ canonicalDomain: project.canonicalDomain,
25602
+ ownedDomains: parseJsonColumn(project.ownedDomains, [])
25603
+ });
25604
+ const projectBrandNames = effectiveBrandNames({
25605
+ displayName: project.displayName,
25606
+ aliases: parseJsonColumn(project.aliases, [])
25607
+ });
25608
+ for (let offset = 0; offset < runIds.length; offset += SNAPSHOT_BATCH_SIZE) {
25609
+ const batchRunIds = runIds.slice(offset, offset + SNAPSHOT_BATCH_SIZE);
25610
+ const snapshotRows = db.select({
25611
+ id: querySnapshots.id,
25612
+ provider: querySnapshots.provider,
25613
+ citationState: querySnapshots.citationState,
25614
+ answerMentioned: querySnapshots.answerMentioned,
25615
+ answerText: querySnapshots.answerText,
25616
+ citedDomains: querySnapshots.citedDomains,
25617
+ competitorOverlap: querySnapshots.competitorOverlap,
25618
+ recommendedCompetitors: querySnapshots.recommendedCompetitors,
25619
+ rawResponse: querySnapshots.rawResponse
25620
+ }).from(querySnapshots).where(inArray10(querySnapshots.runId, batchRunIds)).all();
25621
+ const pendingUpdates = [];
25622
+ for (const snapshot of snapshotRows) {
25623
+ examined++;
25624
+ const reparsedResult = reparseProviderSnapshot(snapshot.provider, snapshot.rawResponse);
25625
+ if (reparsedResult) reparsed++;
25626
+ if (reparsedResult?.providerError) providerErrors++;
25627
+ const answerText = reparsedResult?.answerText ?? snapshot.answerText ?? "";
25628
+ const nextValue = determineAnswerMentioned(answerText, projectBrandNames, projectDomains);
25629
+ if (nextValue) mentioned++;
25630
+ const nextPatch = {};
25631
+ if (snapshot.answerMentioned !== nextValue) {
25632
+ nextPatch.answerMentioned = nextValue;
25633
+ }
25634
+ if ((snapshot.answerText ?? "") !== answerText) {
25635
+ nextPatch.answerText = answerText;
25636
+ }
25637
+ if (reparsedResult) {
25638
+ const normalized = {
25639
+ provider: snapshot.provider,
25640
+ answerText,
25641
+ citedDomains: reparsedResult.citedDomains,
25642
+ groundingSources: reparsedResult.groundingSources,
25643
+ searchQueries: reparsedResult.searchQueries
25644
+ };
25645
+ const nextCitationState = determineCitationState(normalized, projectDomains);
25646
+ const nextCitedDomains = JSON.stringify(reparsedResult.citedDomains);
25647
+ const nextCompetitorOverlap = JSON.stringify(
25648
+ computeCompetitorOverlap(normalized, competitorDomains)
25649
+ );
25650
+ const nextRecommendedCompetitors = JSON.stringify(
25651
+ extractRecommendedCompetitors(
25652
+ normalized.answerText,
25653
+ projectDomains,
25654
+ normalized.citedDomains,
25655
+ competitorDomains,
25656
+ projectBrandNames
25657
+ )
25658
+ );
25659
+ const nextRawResponse = stringifyStoredSnapshotEnvelope(
25660
+ snapshot.rawResponse,
25661
+ reparsedResult
25662
+ );
25663
+ if (snapshot.citationState !== nextCitationState) {
25664
+ nextPatch.citationState = nextCitationState;
25665
+ }
25666
+ if (snapshot.citedDomains !== nextCitedDomains) {
25667
+ nextPatch.citedDomains = nextCitedDomains;
25668
+ }
25669
+ if (snapshot.competitorOverlap !== nextCompetitorOverlap) {
25670
+ nextPatch.competitorOverlap = nextCompetitorOverlap;
25671
+ }
25672
+ if (snapshot.recommendedCompetitors !== nextRecommendedCompetitors) {
25673
+ nextPatch.recommendedCompetitors = nextRecommendedCompetitors;
25674
+ }
25675
+ if (snapshot.rawResponse !== nextRawResponse) {
25676
+ nextPatch.rawResponse = nextRawResponse;
25677
+ }
25678
+ }
25679
+ if (Object.keys(nextPatch).length > 0) {
25680
+ pendingUpdates.push({ id: snapshot.id, patch: nextPatch });
25681
+ }
25682
+ }
25683
+ if (pendingUpdates.length > 0) {
25684
+ db.transaction((tx) => {
25685
+ for (const update of pendingUpdates) {
25686
+ tx.update(querySnapshots).set(update.patch).where(eq34(querySnapshots.id, update.id)).run();
25687
+ }
25688
+ });
25689
+ updated += pendingUpdates.length;
25690
+ }
25691
+ }
25692
+ }
25693
+ }
25694
+ const result = {
25695
+ project: projectFilter ?? null,
25696
+ projects: scopedProjects.length,
25697
+ examined,
25698
+ updated,
25699
+ mentioned,
25700
+ reparsed,
25701
+ providerErrors
25702
+ };
25703
+ if (opts?.format === "json") {
25704
+ console.log(JSON.stringify(result, null, 2));
25705
+ return;
25706
+ }
25707
+ console.log("Answer visibility backfill complete.\n");
25708
+ if (projectFilter) {
25709
+ console.log(` Project: ${projectFilter}`);
25710
+ }
25711
+ console.log(` Projects: ${scopedProjects.length}`);
25712
+ console.log(` Examined: ${examined}`);
25713
+ console.log(` Updated: ${updated}`);
25714
+ console.log(` Mentioned: ${mentioned}`);
25715
+ console.log(` Reparsed: ${reparsed}`);
25716
+ console.log(` Errors: ${providerErrors}`);
25717
+ }
25718
+ function backfillNormalizedPaths(db, opts) {
25719
+ const baseConditions = [];
25720
+ if (opts?.projectId) {
25721
+ baseConditions.push(eq34(gaTrafficSnapshots.projectId, opts.projectId));
25722
+ }
25723
+ const rows = db.select({
25724
+ id: gaTrafficSnapshots.id,
25725
+ landingPage: gaTrafficSnapshots.landingPage,
25726
+ landingPageNormalized: gaTrafficSnapshots.landingPageNormalized
25727
+ }).from(gaTrafficSnapshots).where(baseConditions.length > 0 ? and24(...baseConditions) : void 0).all();
25728
+ let updated = 0;
25729
+ let unchanged = 0;
25730
+ if (rows.length > 0) {
25731
+ db.transaction((tx) => {
25732
+ for (const row of rows) {
25733
+ const next = normalizeUrlPath(row.landingPage);
25734
+ if (next === null) {
25735
+ unchanged++;
25736
+ continue;
25737
+ }
25738
+ if (row.landingPageNormalized === next) {
25739
+ unchanged++;
25740
+ continue;
25741
+ }
25742
+ tx.update(gaTrafficSnapshots).set({ landingPageNormalized: next }).where(eq34(gaTrafficSnapshots.id, row.id)).run();
25743
+ updated++;
25744
+ }
25745
+ });
25746
+ }
25747
+ return { examined: rows.length, updated, unchanged };
25748
+ }
25749
+ async function backfillNormalizedPathsCommand(opts) {
25750
+ const config = loadConfig();
25751
+ const db = createClient(config.database);
25752
+ migrate(db);
25753
+ const projectFilter = opts?.project?.trim();
25754
+ let projectId;
25755
+ if (projectFilter) {
25756
+ const project = db.select({ id: projects.id }).from(projects).where(eq34(projects.name, projectFilter)).get();
25757
+ if (!project) {
25758
+ const result2 = {
25759
+ project: projectFilter,
25760
+ examined: 0,
25761
+ updated: 0,
25762
+ unchanged: 0
25763
+ };
25764
+ if (opts?.format === "json") {
25765
+ console.log(JSON.stringify(result2, null, 2));
25766
+ return;
25767
+ }
25768
+ console.log(`Backfill normalized-paths: project "${projectFilter}" not found.`);
25769
+ return;
25770
+ }
25771
+ projectId = project.id;
25772
+ }
25773
+ const { examined, updated, unchanged } = backfillNormalizedPaths(db, { projectId });
25774
+ const result = {
25775
+ project: projectFilter ?? null,
25776
+ examined,
25777
+ updated,
25778
+ unchanged
25779
+ };
25780
+ if (opts?.format === "json") {
25781
+ console.log(JSON.stringify(result, null, 2));
25782
+ return;
25783
+ }
25784
+ console.log("Normalized-path backfill complete.\n");
25785
+ if (projectFilter) console.log(` Project: ${projectFilter}`);
25786
+ console.log(` Examined: ${examined}`);
25787
+ console.log(` Updated: ${updated}`);
25788
+ console.log(` Unchanged: ${unchanged}`);
25789
+ }
25790
+ function backfillAiReferralPaths(db, opts) {
25791
+ const baseConditions = [];
25792
+ if (opts?.projectId) {
25793
+ baseConditions.push(eq34(gaAiReferrals.projectId, opts.projectId));
25794
+ }
25795
+ const rows = db.select({
25796
+ id: gaAiReferrals.id,
25797
+ landingPage: gaAiReferrals.landingPage,
25798
+ landingPageNormalized: gaAiReferrals.landingPageNormalized
25799
+ }).from(gaAiReferrals).where(baseConditions.length > 0 ? and24(...baseConditions) : void 0).all();
25800
+ let updated = 0;
25801
+ let unchanged = 0;
25802
+ if (rows.length > 0) {
25803
+ db.transaction((tx) => {
25804
+ for (const row of rows) {
25805
+ const next = normalizeUrlPath(row.landingPage);
25806
+ if (next === null) {
25807
+ unchanged++;
25808
+ continue;
25809
+ }
25810
+ if (row.landingPageNormalized === next) {
25811
+ unchanged++;
25812
+ continue;
25813
+ }
25814
+ tx.update(gaAiReferrals).set({ landingPageNormalized: next }).where(eq34(gaAiReferrals.id, row.id)).run();
25815
+ updated++;
25816
+ }
25817
+ });
25818
+ }
25819
+ return { examined: rows.length, updated, unchanged };
25820
+ }
25821
+ async function backfillAiReferralPathsCommand(opts) {
25822
+ const config = loadConfig();
25823
+ const db = createClient(config.database);
25824
+ migrate(db);
25825
+ const projectFilter = opts?.project?.trim();
25826
+ let projectId;
25827
+ if (projectFilter) {
25828
+ const project = db.select({ id: projects.id }).from(projects).where(eq34(projects.name, projectFilter)).get();
25829
+ if (!project) {
25830
+ const result2 = {
25831
+ project: projectFilter,
25832
+ examined: 0,
25833
+ updated: 0,
25834
+ unchanged: 0
25835
+ };
25836
+ if (opts?.format === "json") {
25837
+ console.log(JSON.stringify(result2, null, 2));
25838
+ return;
25839
+ }
25840
+ console.log(`Backfill ai-referral-paths: project "${projectFilter}" not found.`);
25841
+ return;
25842
+ }
25843
+ projectId = project.id;
25844
+ }
25845
+ const { examined, updated, unchanged } = backfillAiReferralPaths(db, { projectId });
25846
+ const result = {
25847
+ project: projectFilter ?? null,
25848
+ examined,
25849
+ updated,
25850
+ unchanged
25851
+ };
25852
+ if (opts?.format === "json") {
25853
+ console.log(JSON.stringify(result, null, 2));
25854
+ return;
25855
+ }
25856
+ console.log("AI referral landing-page backfill complete.\n");
25857
+ if (projectFilter) console.log(` Project: ${projectFilter}`);
25858
+ console.log(` Examined: ${examined}`);
25859
+ console.log(` Updated: ${updated}`);
25860
+ console.log(` Unchanged: ${unchanged}`);
25861
+ }
25862
+ function backfillProjectAnswerMentions(db, projectId) {
25863
+ const project = db.select().from(projects).where(eq34(projects.id, projectId)).get();
25864
+ if (!project) return { examined: 0, updated: 0, mentioned: 0 };
25865
+ const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq34(competitors.projectId, projectId)).all().map((row) => row.domain);
25866
+ const runRows = db.select({ id: runs.id }).from(runs).where(and24(eq34(runs.kind, RunKinds["answer-visibility"]), eq34(runs.projectId, projectId))).all();
25867
+ const runIds = runRows.map((r) => r.id);
25868
+ let examined = 0;
25869
+ let updated = 0;
25870
+ let mentioned = 0;
25871
+ if (runIds.length === 0) return { examined, updated, mentioned };
25872
+ const projectDomains = effectiveDomains({
25873
+ canonicalDomain: project.canonicalDomain,
25874
+ ownedDomains: parseJsonColumn(project.ownedDomains, [])
25875
+ });
25876
+ const projectBrandNames = effectiveBrandNames({
25877
+ displayName: project.displayName,
25878
+ aliases: parseJsonColumn(project.aliases, [])
25879
+ });
25880
+ for (let offset = 0; offset < runIds.length; offset += SNAPSHOT_BATCH_SIZE) {
25881
+ const batchRunIds = runIds.slice(offset, offset + SNAPSHOT_BATCH_SIZE);
25882
+ const snapshotRows = db.select({
25883
+ id: querySnapshots.id,
25884
+ provider: querySnapshots.provider,
25885
+ answerMentioned: querySnapshots.answerMentioned,
25886
+ answerText: querySnapshots.answerText,
25887
+ citedDomains: querySnapshots.citedDomains,
25888
+ competitorOverlap: querySnapshots.competitorOverlap,
25889
+ recommendedCompetitors: querySnapshots.recommendedCompetitors,
25890
+ rawResponse: querySnapshots.rawResponse
25891
+ }).from(querySnapshots).where(inArray10(querySnapshots.runId, batchRunIds)).all();
25892
+ const pendingUpdates = [];
25893
+ for (const snapshot of snapshotRows) {
25894
+ examined++;
25895
+ const answerText = snapshot.answerText ?? "";
25896
+ const nextAnswerMentioned = determineAnswerMentioned(answerText, projectBrandNames, projectDomains);
25897
+ if (nextAnswerMentioned) mentioned++;
25898
+ const citedDomains = parseJsonColumn(snapshot.citedDomains, []);
25899
+ const groundingSources = readStoredGroundingSources(snapshot.rawResponse);
25900
+ const normalized = {
25901
+ provider: snapshot.provider,
25902
+ answerText,
25903
+ citedDomains,
25904
+ groundingSources,
25905
+ searchQueries: []
25906
+ };
25907
+ const nextCompetitorOverlap = JSON.stringify(
25908
+ computeCompetitorOverlap(normalized, competitorDomains)
25909
+ );
25910
+ const nextRecommendedCompetitors = JSON.stringify(
25911
+ extractRecommendedCompetitors(
25912
+ answerText,
25913
+ projectDomains,
25914
+ citedDomains,
25915
+ competitorDomains,
25916
+ projectBrandNames
25917
+ )
25918
+ );
25919
+ const nextPatch = {};
25920
+ if (snapshot.answerMentioned !== nextAnswerMentioned) {
25921
+ nextPatch.answerMentioned = nextAnswerMentioned;
25922
+ }
25923
+ if (snapshot.competitorOverlap !== nextCompetitorOverlap) {
25924
+ nextPatch.competitorOverlap = nextCompetitorOverlap;
25925
+ }
25926
+ if (snapshot.recommendedCompetitors !== nextRecommendedCompetitors) {
25927
+ nextPatch.recommendedCompetitors = nextRecommendedCompetitors;
25928
+ }
25929
+ if (Object.keys(nextPatch).length > 0) {
25930
+ pendingUpdates.push({ id: snapshot.id, patch: nextPatch });
25931
+ }
25932
+ }
25933
+ if (pendingUpdates.length > 0) {
25934
+ db.transaction((tx) => {
25935
+ for (const update of pendingUpdates) {
25936
+ tx.update(querySnapshots).set(update.patch).where(eq34(querySnapshots.id, update.id)).run();
25937
+ }
25938
+ });
25939
+ updated += pendingUpdates.length;
25940
+ }
25941
+ }
25942
+ return { examined, updated, mentioned };
25943
+ }
25944
+ async function backfillAnswerMentionsCommand(opts) {
25945
+ const config = loadConfig();
25946
+ const db = createClient(config.database);
25947
+ migrate(db);
25948
+ const projectFilter = opts?.project?.trim();
25949
+ const scopedProjects = projectFilter ? db.select().from(projects).where(eq34(projects.name, projectFilter)).all() : db.select().from(projects).all();
25950
+ let examined = 0;
25951
+ let updated = 0;
25952
+ let mentioned = 0;
25953
+ for (const project of scopedProjects) {
25954
+ const result2 = backfillProjectAnswerMentions(db, project.id);
25955
+ examined += result2.examined;
25956
+ updated += result2.updated;
25957
+ mentioned += result2.mentioned;
25958
+ }
25959
+ const result = {
25960
+ project: projectFilter ?? null,
25961
+ projects: scopedProjects.length,
25962
+ examined,
25963
+ updated,
25964
+ mentioned
25965
+ };
25966
+ if (opts?.format === "json") {
25967
+ console.log(JSON.stringify(result, null, 2));
25968
+ return;
25969
+ }
25970
+ console.log("Answer mentions backfill complete.\n");
25971
+ if (projectFilter) console.log(` Project: ${projectFilter}`);
25972
+ console.log(` Projects: ${scopedProjects.length}`);
25973
+ console.log(` Examined: ${examined}`);
25974
+ console.log(` Updated: ${updated}`);
25975
+ console.log(` Mentioned: ${mentioned}`);
25976
+ }
25977
+ function readStoredGroundingSources(rawResponse) {
25978
+ const envelope = parseJsonColumn(rawResponse, {});
25979
+ const sources = envelope.groundingSources;
25980
+ if (!Array.isArray(sources)) return [];
25981
+ const result = [];
25982
+ for (const source of sources) {
25983
+ if (source && typeof source === "object") {
25984
+ const uri = source.uri;
25985
+ const title = source.title;
25986
+ if (typeof uri === "string") {
25987
+ result.push({ uri, title: typeof title === "string" ? title : "" });
25988
+ }
25989
+ }
25990
+ }
25991
+ return result;
25992
+ }
25993
+ async function backfillInsightsCommand(project, opts) {
25994
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-WAJOEOJV.js");
25995
+ const config = loadConfig();
25996
+ const db = createClient(config.database);
25997
+ migrate(db);
25998
+ const service = new IntelligenceService2(db);
25999
+ const isJson = opts?.format === "json";
26000
+ if (!isJson) {
26001
+ process.stderr.write(`Backfilling insights for "${project}"...
26002
+ `);
26003
+ }
26004
+ const result = service.backfill(project, {
26005
+ fromRunId: opts?.fromRun,
26006
+ toRunId: opts?.toRun
26007
+ }, (info) => {
26008
+ if (!isJson) {
26009
+ process.stderr.write(` [${info.index}/${info.total}] ${info.runId} \u2014 ${info.insights} insights
26010
+ `);
26011
+ }
26012
+ });
26013
+ const output = {
26014
+ project,
26015
+ processed: result.processed,
26016
+ skipped: result.skipped,
26017
+ totalInsights: result.totalInsights
26018
+ };
26019
+ if (isJson) {
26020
+ console.log(JSON.stringify(output, null, 2));
26021
+ return;
26022
+ }
26023
+ console.log(`
26024
+ Backfill complete.`);
26025
+ console.log(` Processed: ${result.processed}`);
26026
+ console.log(` Skipped: ${result.skipped}`);
26027
+ console.log(` Insights: ${result.totalInsights}`);
26028
+ }
26029
+ function reparseProviderSnapshot(provider, rawResponse) {
26030
+ const envelope = parseJsonColumn(rawResponse, {});
26031
+ const apiResponse = resolveStoredApiResponse(envelope);
26032
+ if (!apiResponse) return null;
26033
+ switch (provider) {
26034
+ case ProviderNames.openai:
26035
+ return reparseStoredResult2(apiResponse);
26036
+ case ProviderNames.claude:
26037
+ return reparseStoredResult3(apiResponse);
26038
+ case ProviderNames.gemini:
26039
+ return reparseStoredResult(apiResponse);
26040
+ case ProviderNames.perplexity:
26041
+ return reparseStoredResult4(apiResponse);
26042
+ default:
26043
+ return null;
26044
+ }
26045
+ }
26046
+ function resolveStoredApiResponse(parsed) {
26047
+ const nested = parsed.apiResponse;
26048
+ if (nested !== null && typeof nested === "object" && !Array.isArray(nested)) {
26049
+ return nested;
26050
+ }
26051
+ if (looksLikeProviderApiResponse(parsed)) {
26052
+ return parsed;
26053
+ }
26054
+ return null;
26055
+ }
26056
+ function looksLikeProviderApiResponse(value) {
26057
+ return Array.isArray(value.output) || Array.isArray(value.content) || Array.isArray(value.candidates) || Array.isArray(value.choices);
26058
+ }
26059
+ function stringifyStoredSnapshotEnvelope(rawResponse, reparsed) {
26060
+ const parsed = parseJsonColumn(rawResponse, {});
26061
+ const apiResponse = resolveStoredApiResponse(parsed);
26062
+ const envelope = apiResponse === parsed ? {} : { ...parsed };
26063
+ delete envelope.answerText;
26064
+ delete envelope.citedDomains;
26065
+ delete envelope.competitorOverlap;
26066
+ delete envelope.recommendedCompetitors;
26067
+ delete envelope.providerError;
26068
+ return JSON.stringify({
26069
+ ...envelope,
26070
+ groundingSources: reparsed.groundingSources,
26071
+ searchQueries: reparsed.searchQueries,
26072
+ ...reparsed.providerError ? { providerError: reparsed.providerError } : {},
26073
+ ...apiResponse ? { apiResponse } : {}
26074
+ });
26075
+ }
26076
+
25293
26077
  // src/provider-registry.ts
25294
26078
  var ProviderRegistry = class {
25295
26079
  providers = /* @__PURE__ */ new Map();
@@ -25343,7 +26127,7 @@ var ProviderRegistry = class {
25343
26127
 
25344
26128
  // src/scheduler.ts
25345
26129
  import cron from "node-cron";
25346
- import { and as and22, eq as eq34 } from "drizzle-orm";
26130
+ import { and as and25, eq as eq35 } from "drizzle-orm";
25347
26131
  var log9 = createLogger("Scheduler");
25348
26132
  function taskKey(projectId, kind) {
25349
26133
  return `${projectId}::${kind}`;
@@ -25358,7 +26142,7 @@ var Scheduler = class {
25358
26142
  }
25359
26143
  /** Load all enabled schedules from DB and register cron jobs. */
25360
26144
  start() {
25361
- const allSchedules = this.db.select().from(schedules).where(eq34(schedules.enabled, 1)).all();
26145
+ const allSchedules = this.db.select().from(schedules).where(eq35(schedules.enabled, 1)).all();
25362
26146
  for (const schedule of allSchedules) {
25363
26147
  const missedRunAt = schedule.nextRunAt;
25364
26148
  this.registerCronTask(schedule);
@@ -25388,7 +26172,7 @@ var Scheduler = class {
25388
26172
  this.stopTask(key, existing, "Stopped");
25389
26173
  this.tasks.delete(key);
25390
26174
  }
25391
- const schedule = this.db.select().from(schedules).where(and22(eq34(schedules.projectId, projectId), eq34(schedules.kind, kind))).get();
26175
+ const schedule = this.db.select().from(schedules).where(and25(eq35(schedules.projectId, projectId), eq35(schedules.kind, kind))).get();
25392
26176
  if (schedule && schedule.enabled === 1) {
25393
26177
  this.registerCronTask(schedule);
25394
26178
  }
@@ -25429,14 +26213,14 @@ var Scheduler = class {
25429
26213
  this.db.update(schedules).set({
25430
26214
  nextRunAt: task.getNextRun()?.toISOString() ?? null,
25431
26215
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
25432
- }).where(eq34(schedules.id, scheduleId)).run();
26216
+ }).where(eq35(schedules.id, scheduleId)).run();
25433
26217
  const label = schedule.preset ?? cronExpr;
25434
26218
  log9.info("cron.registered", { projectId, kind, schedule: label, timezone });
25435
26219
  }
25436
26220
  triggerRun(scheduleId, projectId, kind) {
25437
26221
  try {
25438
26222
  const now = (/* @__PURE__ */ new Date()).toISOString();
25439
- const currentSchedule = this.db.select().from(schedules).where(eq34(schedules.id, scheduleId)).get();
26223
+ const currentSchedule = this.db.select().from(schedules).where(eq35(schedules.id, scheduleId)).get();
25440
26224
  if (!currentSchedule || currentSchedule.enabled !== 1) {
25441
26225
  log9.warn("schedule.stale", { scheduleId, projectId, kind, msg: "schedule no longer exists or is disabled" });
25442
26226
  this.remove(projectId, kind);
@@ -25444,7 +26228,7 @@ var Scheduler = class {
25444
26228
  }
25445
26229
  const task = this.tasks.get(taskKey(projectId, kind));
25446
26230
  const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
25447
- const project = this.db.select().from(projects).where(eq34(projects.id, projectId)).get();
26231
+ const project = this.db.select().from(projects).where(eq35(projects.id, projectId)).get();
25448
26232
  if (!project) {
25449
26233
  log9.error("project.not-found", { projectId, kind, msg: "skipping scheduled run" });
25450
26234
  this.remove(projectId, kind);
@@ -25464,7 +26248,7 @@ var Scheduler = class {
25464
26248
  lastRunAt: now,
25465
26249
  nextRunAt,
25466
26250
  updatedAt: now
25467
- }).where(eq34(schedules.id, currentSchedule.id)).run();
26251
+ }).where(eq35(schedules.id, currentSchedule.id)).run();
25468
26252
  log9.info("traffic-sync.triggered", { projectName: project.name, sourceId });
25469
26253
  this.callbacks.onTrafficSyncRequested(project.name, sourceId);
25470
26254
  return;
@@ -25492,7 +26276,7 @@ var Scheduler = class {
25492
26276
  this.db.update(schedules).set({
25493
26277
  nextRunAt,
25494
26278
  updatedAt: now
25495
- }).where(eq34(schedules.id, currentSchedule.id)).run();
26279
+ }).where(eq35(schedules.id, currentSchedule.id)).run();
25496
26280
  return;
25497
26281
  }
25498
26282
  const runId = queueResult.runId;
@@ -25500,7 +26284,7 @@ var Scheduler = class {
25500
26284
  lastRunAt: now,
25501
26285
  nextRunAt,
25502
26286
  updatedAt: now
25503
- }).where(eq34(schedules.id, currentSchedule.id)).run();
26287
+ }).where(eq35(schedules.id, currentSchedule.id)).run();
25504
26288
  const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
25505
26289
  const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
25506
26290
  log9.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
@@ -25512,7 +26296,7 @@ var Scheduler = class {
25512
26296
  };
25513
26297
 
25514
26298
  // src/notifier.ts
25515
- import { eq as eq35, desc as desc16, and as and23, inArray as inArray10, or as or4 } from "drizzle-orm";
26299
+ import { eq as eq36, desc as desc16, and as and26, inArray as inArray11, or as or5 } from "drizzle-orm";
25516
26300
  import crypto31 from "crypto";
25517
26301
  var log10 = createLogger("Notifier");
25518
26302
  var Notifier = class {
@@ -25525,18 +26309,18 @@ var Notifier = class {
25525
26309
  /** Called after a run completes (success, partial, or failed). */
25526
26310
  async onRunCompleted(runId, projectId) {
25527
26311
  log10.info("run.completed", { runId, projectId });
25528
- const notifs = this.db.select().from(notifications).where(eq35(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
26312
+ const notifs = this.db.select().from(notifications).where(eq36(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
25529
26313
  if (notifs.length === 0) {
25530
26314
  log10.info("notifications.none-enabled", { projectId });
25531
26315
  return;
25532
26316
  }
25533
26317
  log10.info("notifications.found", { projectId, count: notifs.length });
25534
- const run = this.db.select().from(runs).where(eq35(runs.id, runId)).get();
26318
+ const run = this.db.select().from(runs).where(eq36(runs.id, runId)).get();
25535
26319
  if (!run) {
25536
26320
  log10.error("run.not-found", { runId, msg: "skipping notification dispatch" });
25537
26321
  return;
25538
26322
  }
25539
- const project = this.db.select().from(projects).where(eq35(projects.id, projectId)).get();
26323
+ const project = this.db.select().from(projects).where(eq36(projects.id, projectId)).get();
25540
26324
  if (!project) {
25541
26325
  log10.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
25542
26326
  return;
@@ -25583,11 +26367,11 @@ var Notifier = class {
25583
26367
  if (criticalInsights.length > 0) insightEvents.push("insight.critical");
25584
26368
  if (highInsights.length > 0) insightEvents.push("insight.high");
25585
26369
  if (insightEvents.length === 0) return;
25586
- const notifs = this.db.select().from(notifications).where(eq35(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
26370
+ const notifs = this.db.select().from(notifications).where(eq36(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
25587
26371
  if (notifs.length === 0) return;
25588
- const run = this.db.select().from(runs).where(eq35(runs.id, runId)).get();
26372
+ const run = this.db.select().from(runs).where(eq36(runs.id, runId)).get();
25589
26373
  if (!run) return;
25590
- const project = this.db.select().from(projects).where(eq35(projects.id, projectId)).get();
26374
+ const project = this.db.select().from(projects).where(eq36(projects.id, projectId)).get();
25591
26375
  if (!project) return;
25592
26376
  for (const notif of notifs) {
25593
26377
  const config = parseJsonColumn(notif.config, { url: "", events: [] });
@@ -25617,12 +26401,12 @@ var Notifier = class {
25617
26401
  }
25618
26402
  }
25619
26403
  computeTransitions(runId, projectId) {
25620
- const thisRun = this.db.select().from(runs).where(eq35(runs.id, runId)).get();
26404
+ const thisRun = this.db.select().from(runs).where(eq36(runs.id, runId)).get();
25621
26405
  if (!thisRun) return [];
25622
- const groupSiblings = this.db.select().from(runs).where(and23(
25623
- eq35(runs.projectId, projectId),
25624
- eq35(runs.kind, thisRun.kind),
25625
- eq35(runs.createdAt, thisRun.createdAt)
26406
+ const groupSiblings = this.db.select().from(runs).where(and26(
26407
+ eq36(runs.projectId, projectId),
26408
+ eq36(runs.kind, thisRun.kind),
26409
+ eq36(runs.createdAt, thisRun.createdAt)
25626
26410
  )).all();
25627
26411
  const stillPending = groupSiblings.some((r) => r.status === "queued" || r.status === "running");
25628
26412
  if (stillPending) return [];
@@ -25638,17 +26422,17 @@ var Notifier = class {
25638
26422
  return candidate.id > best.id ? candidate : best;
25639
26423
  });
25640
26424
  if (winner.id !== runId) return [];
25641
- const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(eq35(projects.id, projectId)).get();
26425
+ const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(eq36(projects.id, projectId)).get();
25642
26426
  const locationCount = Math.max(
25643
26427
  1,
25644
26428
  parseJsonColumn(projectLocations?.locations ?? null, []).length
25645
26429
  );
25646
26430
  const RECENT_FETCH_LIMIT = Math.max(8, locationCount * 4);
25647
26431
  const recentRuns = this.db.select().from(runs).where(
25648
- and23(
25649
- eq35(runs.projectId, projectId),
25650
- eq35(runs.kind, thisRun.kind),
25651
- or4(eq35(runs.status, "completed"), eq35(runs.status, "partial"))
26432
+ and26(
26433
+ eq36(runs.projectId, projectId),
26434
+ eq36(runs.kind, thisRun.kind),
26435
+ or5(eq36(runs.status, "completed"), eq36(runs.status, "partial"))
25652
26436
  )
25653
26437
  ).orderBy(desc16(runs.createdAt), desc16(runs.id)).limit(RECENT_FETCH_LIMIT).all();
25654
26438
  const groups = groupRunsByCreatedAt(recentRuns);
@@ -25665,19 +26449,21 @@ var Notifier = class {
25665
26449
  provider: querySnapshots.provider,
25666
26450
  location: querySnapshots.location,
25667
26451
  citationState: querySnapshots.citationState
25668
- }).from(querySnapshots).leftJoin(queries, eq35(querySnapshots.queryId, queries.id)).where(inArray10(querySnapshots.runId, currentRunIds)).all();
26452
+ }).from(querySnapshots).leftJoin(queries, eq36(querySnapshots.queryId, queries.id)).where(inArray11(querySnapshots.runId, currentRunIds)).all();
25669
26453
  const previousSnapshots = this.db.select({
25670
26454
  queryId: querySnapshots.queryId,
25671
26455
  provider: querySnapshots.provider,
25672
26456
  location: querySnapshots.location,
25673
26457
  citationState: querySnapshots.citationState
25674
- }).from(querySnapshots).where(inArray10(querySnapshots.runId, previousRunIds)).all();
26458
+ }).from(querySnapshots).where(inArray11(querySnapshots.runId, previousRunIds)).all();
25675
26459
  const prevMap = /* @__PURE__ */ new Map();
25676
26460
  for (const s of previousSnapshots) {
26461
+ if (s.queryId == null) continue;
25677
26462
  prevMap.set(`${s.queryId}:${s.provider}:${s.location ?? ""}`, s.citationState);
25678
26463
  }
25679
26464
  const transitions = [];
25680
26465
  for (const s of currentSnapshots) {
26466
+ if (s.queryId == null) continue;
25681
26467
  const key = `${s.queryId}:${s.provider}:${s.location ?? ""}`;
25682
26468
  const prevState = prevMap.get(key);
25683
26469
  if (prevState && prevState !== s.citationState) {
@@ -25743,7 +26529,7 @@ var Notifier = class {
25743
26529
  };
25744
26530
 
25745
26531
  // src/run-coordinator.ts
25746
- import { eq as eq36 } from "drizzle-orm";
26532
+ import { eq as eq37 } from "drizzle-orm";
25747
26533
  var log11 = createLogger("RunCoordinator");
25748
26534
  var RunCoordinator = class {
25749
26535
  constructor(db, notifier, intelligenceService, onInsightsGenerated, onAeroEvent) {
@@ -25754,7 +26540,7 @@ var RunCoordinator = class {
25754
26540
  this.onAeroEvent = onAeroEvent;
25755
26541
  }
25756
26542
  async onRunCompleted(runId, projectId) {
25757
- const runRow = this.db.select().from(runs).where(eq36(runs.id, runId)).get();
26543
+ const runRow = this.db.select().from(runs).where(eq37(runs.id, runId)).get();
25758
26544
  const kind = runRow?.kind ?? RunKinds["answer-visibility"];
25759
26545
  let insightCount = 0;
25760
26546
  let criticalOrHigh = 0;
@@ -25809,7 +26595,7 @@ var RunCoordinator = class {
25809
26595
  * so the Aero queue is never starved of a follow-up.
25810
26596
  */
25811
26597
  buildDiscoveryAeroContext(runId, projectId, status, error) {
25812
- const session = this.db.select().from(discoverySessions).where(eq36(discoverySessions.runId, runId)).get();
26598
+ const session = this.db.select().from(discoverySessions).where(eq37(discoverySessions.runId, runId)).get();
25813
26599
  const competitorMap = session ? parseJsonColumn(session.competitorMap, []) : [];
25814
26600
  return {
25815
26601
  kind: RunKinds["aeo-discover-probe"],
@@ -25832,7 +26618,7 @@ var RunCoordinator = class {
25832
26618
 
25833
26619
  // src/agent/session-registry.ts
25834
26620
  import crypto33 from "crypto";
25835
- import { eq as eq38 } from "drizzle-orm";
26621
+ import { eq as eq39 } from "drizzle-orm";
25836
26622
 
25837
26623
  // src/agent/session.ts
25838
26624
  import fs11 from "fs";
@@ -26182,7 +26968,7 @@ function resolveSessionProviderAndModel(config, opts) {
26182
26968
 
26183
26969
  // src/agent/memory-store.ts
26184
26970
  import crypto32 from "crypto";
26185
- import { and as and24, desc as desc17, eq as eq37, like as like2, sql as sql13 } from "drizzle-orm";
26971
+ import { and as and27, desc as desc17, eq as eq38, like as like2, sql as sql13 } from "drizzle-orm";
26186
26972
  var COMPACTION_KEY_PREFIX = "compaction:";
26187
26973
  var COMPACTION_NOTES_PER_SESSION = 3;
26188
26974
  function rowToDto2(row) {
@@ -26196,7 +26982,7 @@ function rowToDto2(row) {
26196
26982
  };
26197
26983
  }
26198
26984
  function listMemoryEntries(db, projectId, opts = {}) {
26199
- const query = db.select().from(agentMemory).where(eq37(agentMemory.projectId, projectId)).orderBy(desc17(agentMemory.updatedAt));
26985
+ const query = db.select().from(agentMemory).where(eq38(agentMemory.projectId, projectId)).orderBy(desc17(agentMemory.updatedAt));
26200
26986
  const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
26201
26987
  return rows.map(rowToDto2);
26202
26988
  }
@@ -26227,12 +27013,12 @@ function upsertMemoryEntry(db, args) {
26227
27013
  updatedAt: now
26228
27014
  }
26229
27015
  }).run();
26230
- const row = db.select().from(agentMemory).where(and24(eq37(agentMemory.projectId, args.projectId), eq37(agentMemory.key, args.key))).get();
27016
+ const row = db.select().from(agentMemory).where(and27(eq38(agentMemory.projectId, args.projectId), eq38(agentMemory.key, args.key))).get();
26231
27017
  if (!row) throw new Error("memory upsert produced no row");
26232
27018
  return rowToDto2(row);
26233
27019
  }
26234
27020
  function deleteMemoryEntry(db, projectId, key) {
26235
- const result = db.delete(agentMemory).where(and24(eq37(agentMemory.projectId, projectId), eq37(agentMemory.key, key))).run();
27021
+ const result = db.delete(agentMemory).where(and27(eq38(agentMemory.projectId, projectId), eq38(agentMemory.key, key))).run();
26236
27022
  const changes = result.changes ?? 0;
26237
27023
  return changes > 0;
26238
27024
  }
@@ -26261,8 +27047,8 @@ function writeCompactionNote(db, args) {
26261
27047
  }).run();
26262
27048
  const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
26263
27049
  const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
26264
- and24(
26265
- eq37(agentMemory.projectId, args.projectId),
27050
+ and27(
27051
+ eq38(agentMemory.projectId, args.projectId),
26266
27052
  like2(agentMemory.key, `${sessionPrefix}%`)
26267
27053
  )
26268
27054
  ).orderBy(desc17(agentMemory.updatedAt)).all();
@@ -26270,7 +27056,7 @@ function writeCompactionNote(db, args) {
26270
27056
  if (stale.length > 0) {
26271
27057
  tx.delete(agentMemory).where(sql13`${agentMemory.id} IN (${sql13.join(stale.map((s) => sql13`${s}`), sql13`, `)})`).run();
26272
27058
  }
26273
- const row = tx.select().from(agentMemory).where(and24(eq37(agentMemory.projectId, args.projectId), eq37(agentMemory.key, key))).get();
27059
+ const row = tx.select().from(agentMemory).where(and27(eq38(agentMemory.projectId, args.projectId), eq38(agentMemory.key, key))).get();
26274
27060
  if (row) inserted = rowToDto2(row);
26275
27061
  });
26276
27062
  if (!inserted) throw new Error("compaction note write produced no row");
@@ -26452,7 +27238,7 @@ var SessionRegistry = class {
26452
27238
  modelProvider: effectiveProvider,
26453
27239
  modelId: effectiveModelId,
26454
27240
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
26455
- }).where(eq38(agentSessions.projectId, projectId)).run();
27241
+ }).where(eq39(agentSessions.projectId, projectId)).run();
26456
27242
  }
26457
27243
  const agent2 = createAeroSession({
26458
27244
  projectName,
@@ -26666,7 +27452,7 @@ ${lines.join("\n")}
26666
27452
  modelProvider: nextProvider,
26667
27453
  modelId: nextModelId,
26668
27454
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
26669
- }).where(eq38(agentSessions.projectId, projectId)).run();
27455
+ }).where(eq39(agentSessions.projectId, projectId)).run();
26670
27456
  }
26671
27457
  /** Persist a session's transcript back to the DB. Call after any run settles. */
26672
27458
  save(projectName) {
@@ -26828,11 +27614,11 @@ ${lines.join("\n")}
26828
27614
  return id;
26829
27615
  }
26830
27616
  tryResolveProjectId(projectName) {
26831
- const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq38(projects.name, projectName)).get();
27617
+ const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq39(projects.name, projectName)).get();
26832
27618
  return row?.id;
26833
27619
  }
26834
27620
  loadRow(projectId) {
26835
- const row = this.opts.db.select().from(agentSessions).where(eq38(agentSessions.projectId, projectId)).get();
27621
+ const row = this.opts.db.select().from(agentSessions).where(eq39(agentSessions.projectId, projectId)).get();
26836
27622
  return row ?? null;
26837
27623
  }
26838
27624
  insertRow(params) {
@@ -26851,14 +27637,14 @@ ${lines.join("\n")}
26851
27637
  }
26852
27638
  updateRow(projectId, patch) {
26853
27639
  const now = (/* @__PURE__ */ new Date()).toISOString();
26854
- this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq38(agentSessions.projectId, projectId)).run();
27640
+ this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq39(agentSessions.projectId, projectId)).run();
26855
27641
  }
26856
27642
  };
26857
27643
 
26858
27644
  // src/agent/agent-routes.ts
26859
- import { eq as eq39 } from "drizzle-orm";
27645
+ import { eq as eq40 } from "drizzle-orm";
26860
27646
  function resolveProject2(db, name) {
26861
- const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq39(projects.name, name)).get();
27647
+ const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq40(projects.name, name)).get();
26862
27648
  if (!row) throw notFound("project", name);
26863
27649
  return row;
26864
27650
  }
@@ -26867,7 +27653,7 @@ function registerAgentRoutes(app, opts) {
26867
27653
  "/projects/:name/agent/transcript",
26868
27654
  async (request) => {
26869
27655
  const project = resolveProject2(opts.db, request.params.name);
26870
- const row = opts.db.select().from(agentSessions).where(eq39(agentSessions.projectId, project.id)).get();
27656
+ const row = opts.db.select().from(agentSessions).where(eq40(agentSessions.projectId, project.id)).get();
26871
27657
  if (!row) {
26872
27658
  return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
26873
27659
  }
@@ -26891,7 +27677,7 @@ function registerAgentRoutes(app, opts) {
26891
27677
  async (request) => {
26892
27678
  const project = resolveProject2(opts.db, request.params.name);
26893
27679
  opts.sessionRegistry.reset(project.name);
26894
- opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq39(agentSessions.projectId, project.id)).run();
27680
+ opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq40(agentSessions.projectId, project.id)).run();
26895
27681
  return { status: "reset" };
26896
27682
  }
26897
27683
  );
@@ -27009,7 +27795,7 @@ import { runAeoAudit } from "@ainyc/aeo-audit";
27009
27795
 
27010
27796
  // src/site-fetch.ts
27011
27797
  import https2 from "https";
27012
- var FETCH_TIMEOUT_MS = 1e4;
27798
+ var FETCH_TIMEOUT_MS2 = 1e4;
27013
27799
  var MAX_TEXT_LENGTH = 4e3;
27014
27800
  var MAX_BODY_BYTES = 512e3;
27015
27801
  var USER_AGENT = "Canonry/1.0 (site-analysis)";
@@ -27034,7 +27820,7 @@ function fetchWithPinnedAddress(target) {
27034
27820
  port,
27035
27821
  path: path15,
27036
27822
  method: "GET",
27037
- timeout: FETCH_TIMEOUT_MS,
27823
+ timeout: FETCH_TIMEOUT_MS2,
27038
27824
  servername: target.url.hostname,
27039
27825
  // SNI for TLS
27040
27826
  headers: {
@@ -27355,7 +28141,7 @@ var SnapshotService = class {
27355
28141
  provider: provider.adapter.name,
27356
28142
  displayName: provider.adapter.displayName,
27357
28143
  model: raw.model,
27358
- mentioned: determineAnswerMentioned(normalized.answerText, ctx.companyName, answerVisibilityDomains),
28144
+ mentioned: determineAnswerMentioned(normalized.answerText, [ctx.companyName], answerVisibilityDomains),
27359
28145
  cited: citesTargetDomain(normalized.citedDomains, normalized.groundingSources, ctx.domain),
27360
28146
  describedAccurately: "unknown",
27361
28147
  accuracyNotes: null,
@@ -27722,8 +28508,8 @@ function clipText(value, length) {
27722
28508
  }
27723
28509
 
27724
28510
  // src/server.ts
27725
- var _require2 = createRequire3(import.meta.url);
27726
- var { version: PKG_VERSION } = _require2("../package.json");
28511
+ var _require3 = createRequire4(import.meta.url);
28512
+ var { version: PKG_VERSION2 } = _require3("../package.json");
27727
28513
  var log14 = createLogger("Server");
27728
28514
  var DEFAULT_QUOTA = {
27729
28515
  maxConcurrency: 2,
@@ -27914,7 +28700,7 @@ async function createServer(opts) {
27914
28700
  intelligenceService,
27915
28701
  (runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
27916
28702
  async (ctx) => {
27917
- const project = opts.db.select({ name: projects.name }).from(projects).where(eq40(projects.id, ctx.projectId)).get();
28703
+ const project = opts.db.select({ name: projects.name }).from(projects).where(eq41(projects.id, ctx.projectId)).get();
27918
28704
  if (!project) return;
27919
28705
  let content;
27920
28706
  if (ctx.kind === RunKinds["aeo-discover-probe"]) {
@@ -28115,7 +28901,7 @@ async function createServer(opts) {
28115
28901
  const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
28116
28902
  if (opts.config.apiKey) {
28117
28903
  const keyHash = hashApiKey(opts.config.apiKey);
28118
- const existing = opts.db.select().from(apiKeys).where(eq40(apiKeys.keyHash, keyHash)).get();
28904
+ const existing = opts.db.select().from(apiKeys).where(eq41(apiKeys.keyHash, keyHash)).get();
28119
28905
  if (!existing) {
28120
28906
  const prefix = opts.config.apiKey.slice(0, 12);
28121
28907
  opts.db.insert(apiKeys).values({
@@ -28167,7 +28953,7 @@ async function createServer(opts) {
28167
28953
  };
28168
28954
  const getDefaultApiKey = () => {
28169
28955
  if (!opts.config.apiKey) return void 0;
28170
- return opts.db.select().from(apiKeys).where(eq40(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
28956
+ return opts.db.select().from(apiKeys).where(eq41(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
28171
28957
  };
28172
28958
  const createPasswordSession = (reply) => {
28173
28959
  const key = getDefaultApiKey();
@@ -28224,12 +29010,12 @@ async function createServer(opts) {
28224
29010
  return reply.send({ authenticated: true });
28225
29011
  }
28226
29012
  if (apiKey) {
28227
- const key = opts.db.select().from(apiKeys).where(eq40(apiKeys.keyHash, hashApiKey(apiKey))).get();
29013
+ const key = opts.db.select().from(apiKeys).where(eq41(apiKeys.keyHash, hashApiKey(apiKey))).get();
28228
29014
  if (!key || key.revokedAt) {
28229
29015
  const err2 = authInvalid();
28230
29016
  return reply.status(err2.statusCode).send(err2.toJSON());
28231
29017
  }
28232
- opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq40(apiKeys.id, key.id)).run();
29018
+ opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq41(apiKeys.id, key.id)).run();
28233
29019
  const sessionId = createSession(key.id);
28234
29020
  reply.header("set-cookie", serializeSessionCookie({
28235
29021
  name: SESSION_COOKIE_NAME,
@@ -28283,6 +29069,12 @@ async function createServer(opts) {
28283
29069
  skipAuth: false,
28284
29070
  sessionCookieName: SESSION_COOKIE_NAME,
28285
29071
  resolveSessionApiKeyId,
29072
+ // Local canonry serve runs on the operator's machine, where pointing a
29073
+ // webhook at localhost (Discord test container, Pipedream-mock dev server,
29074
+ // etc.) is a legitimate workflow. Default to allowing it for the local
29075
+ // installer; cloud deployments inherit the secure default of `false` by
29076
+ // not passing this option. Override with CANONRY_ALLOW_LOOPBACK_WEBHOOKS=0.
29077
+ allowLoopbackWebhooks: process.env.CANONRY_ALLOW_LOOPBACK_WEBHOOKS !== "0",
28286
29078
  // Local-only Aero agent routes. Registered here so they inherit api-routes'
28287
29079
  // auth plugin — bare `registerAgentRoutes(app, ...)` would skip auth.
28288
29080
  registerAuthenticatedRoutes: async (scope) => {
@@ -28403,7 +29195,7 @@ async function createServer(opts) {
28403
29195
  discoverLatestRelease,
28404
29196
  openApiInfo: {
28405
29197
  title: "Canonry API",
28406
- version: PKG_VERSION,
29198
+ version: PKG_VERSION2,
28407
29199
  includeCanonryLocal: true
28408
29200
  },
28409
29201
  providerSummary,
@@ -28550,6 +29342,19 @@ async function createServer(opts) {
28550
29342
  onProjectDeleted: (projectId) => {
28551
29343
  scheduler.removeAllForProject(projectId);
28552
29344
  },
29345
+ onAliasesChanged: (projectId, projectName) => {
29346
+ setImmediate(() => {
29347
+ try {
29348
+ const result = backfillProjectAnswerMentions(opts.db, projectId);
29349
+ app.log.info(
29350
+ { projectId, projectName, ...result },
29351
+ "aliases changed \u2014 recomputed mention fields on historical snapshots"
29352
+ );
29353
+ } catch (err) {
29354
+ app.log.error({ err, projectId, projectName }, "alias-triggered backfill failed");
29355
+ }
29356
+ });
29357
+ },
28553
29358
  getTelemetryStatus: () => {
28554
29359
  const enabled = isTelemetryEnabled();
28555
29360
  return {
@@ -28683,16 +29488,21 @@ async function createServer(opts) {
28683
29488
  return reply.status(404).send({ error: "Not found" });
28684
29489
  });
28685
29490
  }
28686
- const healthHandler = async () => ({
28687
- status: "ok",
28688
- service: "canonry",
28689
- version: PKG_VERSION,
28690
- ...basePath ? { basePath: basePath.replace(/\/$/, "") } : {}
28691
- });
29491
+ const healthHandler = () => {
29492
+ const update = checkLatestVersionForServer();
29493
+ return {
29494
+ status: "ok",
29495
+ service: "canonry",
29496
+ version: PKG_VERSION2,
29497
+ ...basePath ? { basePath: basePath.replace(/\/$/, "") } : {},
29498
+ ...update ? { updateAvailable: update } : {}
29499
+ };
29500
+ };
28692
29501
  app.get("/health", healthHandler);
28693
29502
  if (basePath) {
28694
29503
  app.get(`${basePath}health`, healthHandler);
28695
29504
  }
29505
+ checkLatestVersionForServer();
28696
29506
  scheduler.start();
28697
29507
  app.addHook("onClose", async () => {
28698
29508
  scheduler.stop();
@@ -28753,16 +29563,17 @@ export {
28753
29563
  showFirstRunNotice,
28754
29564
  detectAndTrackUpgrade,
28755
29565
  trackEvent,
28756
- reparseStoredResult2 as reparseStoredResult,
28757
- reparseStoredResult3 as reparseStoredResult2,
28758
- reparseStoredResult as reparseStoredResult3,
28759
- reparseStoredResult4,
28760
- determineCitationState,
28761
- computeCompetitorOverlap,
28762
- extractRecommendedCompetitors,
29566
+ backfillAnswerVisibilityCommand,
29567
+ backfillNormalizedPaths,
29568
+ backfillNormalizedPathsCommand,
29569
+ backfillAiReferralPaths,
29570
+ backfillAiReferralPathsCommand,
29571
+ backfillAnswerMentionsCommand,
29572
+ backfillInsightsCommand,
28763
29573
  renderReportHtml,
28764
29574
  setGoogleAuthConfig,
28765
29575
  formatAuditFactorScore,
29576
+ checkLatestVersionForCli,
28766
29577
  listAgentProviders,
28767
29578
  coerceAgentProvider,
28768
29579
  createServer