@canonry/canonry 7.46.0 → 7.47.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.
Files changed (35) hide show
  1. package/assets/assets/{AuditHistoryPanel-u65pF2eT.js → AuditHistoryPanel-BTlvVW6p.js} +1 -1
  2. package/assets/assets/{BacklinksPage-CmYpSNGB.js → BacklinksPage-DQu_3poO.js} +1 -1
  3. package/assets/assets/{HistoryPage-CqzQSAil.js → HistoryPage-D_3qu4SQ.js} +1 -1
  4. package/assets/assets/{MeasurementPropertyPage-KfpmUisb.js → MeasurementPropertyPage-FKmEMw_G.js} +1 -1
  5. package/assets/assets/{ProjectPage-BCiospUp.js → ProjectPage-H7nsLkhG.js} +1 -1
  6. package/assets/assets/{RunsPage-CzPvTnwG.js → RunsPage-CE5wFNdP.js} +1 -1
  7. package/assets/assets/{SettingsPage-CZYckyPO.js → SettingsPage-D2zKpqOv.js} +1 -1
  8. package/assets/assets/{SiteHealthSection-Bm5nWAoZ.js → SiteHealthSection-ClZRxJ-A.js} +3 -3
  9. package/assets/assets/{TrafficPage-DZw_GvaO.js → TrafficPage-BDH4Dzk0.js} +1 -1
  10. package/assets/assets/{TrafficSourceDetailPage-CD20PJ4A.js → TrafficSourceDetailPage-DaVUcHXu.js} +1 -1
  11. package/assets/assets/{extract-error-message-Cpzj_AMZ.js → extract-error-message-YXzyZAEc.js} +1 -1
  12. package/assets/assets/{index-BHLYJHa-.js → index-Zj8hd7aE.js} +3 -3
  13. package/assets/assets/{react-sigma_core.esm.min-CK2U3T7S.js → react-sigma_core.esm.min-9j5NHFWR.js} +1 -1
  14. package/assets/assets/{v2-overview-adapter-DL1IsAM2.js → v2-overview-adapter-BsKpkQvx.js} +1 -1
  15. package/assets/index.html +1 -1
  16. package/dist/{chunk-KPOZR6MV.js → chunk-3F4KBEMY.js} +2 -2
  17. package/dist/{chunk-Y2QMWZT7.js → chunk-66K6U5H5.js} +3 -3
  18. package/dist/{chunk-WMG365JI.js → chunk-CIYCCA2I.js} +11 -2
  19. package/dist/{chunk-QBZPKXFW.js → chunk-DGKSQ5QD.js} +1 -1
  20. package/dist/{chunk-GWRIZSIL.js → chunk-HQS2Y7HO.js} +3 -3
  21. package/dist/{chunk-ZOTA3MOL.js → chunk-KCSU2OSL.js} +242 -112
  22. package/dist/{chunk-V4TITINC.js → chunk-QS262E4F.js} +9 -1
  23. package/dist/{chunk-AYMS4LCP.js → chunk-RUAQAQ7X.js} +225 -210
  24. package/dist/{chunk-PCZXWOYX.js → chunk-XHRJYIJQ.js} +1 -1
  25. package/dist/{chunk-2UQ6WVWP.js → chunk-XLKWD5WO.js} +9 -9
  26. package/dist/{cli-catalog-TFCCKIE4.js → cli-catalog-MRYQK7LI.js} +3 -3
  27. package/dist/cli-commands-RJF4TCEZ.js +15 -0
  28. package/dist/cli.js +10 -10
  29. package/dist/{demo-server-7YFRG3JM.js → demo-server-ASCEFIVC.js} +4 -4
  30. package/dist/index.d.ts +12 -0
  31. package/dist/index.js +6 -6
  32. package/dist/{intelligence-service-P3UJNCVS.js → intelligence-service-VRWJUHNM.js} +2 -2
  33. package/dist/mcp.js +4 -4
  34. package/package.json +11 -11
  35. package/dist/cli-commands-GYWXFE3P.js +0 -15
@@ -147,6 +147,7 @@ import {
147
147
  RatioUnits,
148
148
  ResearchQueryStatuses,
149
149
  ResearchRunStatuses,
150
+ RetrievalStatuses,
150
151
  RunFillRefusalCodes,
151
152
  RunKinds,
152
153
  RunStatuses,
@@ -888,6 +889,7 @@ import {
888
889
  unsupportedKind,
889
890
  upgradeCaveatFor,
890
891
  usableBrandAliases,
892
+ usageSurfaceSchema,
891
893
  userDtoSchema,
892
894
  userListDtoSchema,
893
895
  validationError,
@@ -913,7 +915,7 @@ import {
913
915
  wordpressSchemaDeployResultDtoSchema,
914
916
  wordpressSchemaStatusResultDtoSchema,
915
917
  wordpressStatusDtoSchema
916
- } from "./chunk-V4TITINC.js";
918
+ } from "./chunk-QS262E4F.js";
917
919
 
918
920
  // src/intelligence-service.ts
919
921
  import { eq as eq99, desc as desc45, asc as asc20, and as and82, ne as ne17, or as or24, inArray as inArray40, gte as gte26, lte as lte23, isNull as isNull19, sql as sql37, exists as exists2 } from "drizzle-orm";
@@ -30901,6 +30903,7 @@ function visibilityComparisonPopulation(run, selection) {
30901
30903
  answerMentioned: signals.mention,
30902
30904
  mentionApplicable: candidate.edges.some((edge) => targets.get(edge.targetKey)?.mentionEligible === true),
30903
30905
  citation: citationForCoverage(candidate, targets),
30906
+ retrievalStatus: candidate.observation.retrievalStatus ?? null,
30904
30907
  answerText: candidate.observation.answerText,
30905
30908
  competitorDomains: [...new Set(candidate.edges.flatMap((edge) => edge.competitorDomains))],
30906
30909
  competitorMentions: candidate.observation.competitorMentionDomains.filter((domain) => candidate.edges.some((edge) => edge.competitorDomains.includes(domain))),
@@ -30965,6 +30968,7 @@ function loadVisibilitySnapshots(db, runIds) {
30965
30968
  citedDomains: querySnapshots.citedDomains,
30966
30969
  citedUrls: querySnapshots.citedUrls,
30967
30970
  captureStatus: querySnapshots.captureStatus,
30971
+ retrievalStatus: querySnapshots.retrievalStatus,
30968
30972
  location: querySnapshots.location,
30969
30973
  measurementExecutionId: querySnapshots.measurementExecutionId,
30970
30974
  requestedContext: querySnapshots.requestedContext,
@@ -31110,6 +31114,7 @@ function v2Observations(plan, revision, run, snapshots, includeEvidence = true)
31110
31114
  unknownMentionTargetKeys: attribution?.unknownMentionTargetIds ?? [],
31111
31115
  citedTargetKeys: attribution?.citedTargetIds ?? [],
31112
31116
  citationComplete: attribution?.sourceComplete ?? false,
31117
+ retrievalStatus: raw.retrievalStatus ?? null,
31113
31118
  competitorMentionDomains: signals.mentioned,
31114
31119
  competitorCitationDomains: signals.cited,
31115
31120
  observedCompetitorNames: includeEvidence ? observedCompetitorNames2(raw.recommendedCompetitors) : [],
@@ -31233,6 +31238,7 @@ function frozenSimpleRun(run, definition2, snapshots, includeEvidence = true) {
31233
31238
  // Simple citation state comes from the stored cited domains, not from
31234
31239
  // source-URL capture, so a saved Simple answer is never `unchecked`.
31235
31240
  citationComplete: true,
31241
+ retrievalStatus: snapshot.retrievalStatus ?? null,
31236
31242
  competitorMentionDomains: competitorSignalsForSnapshot.mentioned,
31237
31243
  competitorCitationDomains: competitorSignalsForSnapshot.cited,
31238
31244
  observedCompetitorNames: includeEvidence ? observedCompetitorNames2(snapshot.recommendedCompetitors) : [],
@@ -31316,6 +31322,7 @@ function legacySimpleRun(project, run, snapshots, includeEvidence = true) {
31316
31322
  citedTargetKeys: snapshot.citationState === "cited" ? ["project"] : [],
31317
31323
  // Legacy rows carry only the stored citation state (see the frozen-simple note).
31318
31324
  citationComplete: true,
31325
+ retrievalStatus: snapshot.retrievalStatus ?? null,
31319
31326
  competitorMentionDomains: [],
31320
31327
  competitorCitationDomains: [],
31321
31328
  observedCompetitorNames: includeEvidence ? observedCompetitorNames2(snapshot.recommendedCompetitors) : [],
@@ -37543,6 +37550,7 @@ function countPeriod(snaps, competitors2, queryClassOf, classificationAvailable,
37543
37550
  let total = 0;
37544
37551
  let cited = 0;
37545
37552
  let citationUnknown = 0;
37553
+ let citationNotSearched = 0;
37546
37554
  let competitorCited = 0;
37547
37555
  const perProvider = /* @__PURE__ */ new Map();
37548
37556
  const mentionedQueries = /* @__PURE__ */ new Set();
@@ -37555,9 +37563,11 @@ function countPeriod(snaps, competitors2, queryClassOf, classificationAvailable,
37555
37563
  mentionedQueries.add(snap.queryId);
37556
37564
  }
37557
37565
  const citationKnown = snap.citationChecked !== false;
37558
- if (citationKnown) total += 1;
37559
- else citationUnknown += 1;
37560
- const isCited = citationKnown && snap.citationState === CitationStates.cited;
37566
+ const noRetrieval = citationKnown && snap.retrievalStatus === RetrievalStatuses["not-used"];
37567
+ if (!citationKnown) citationUnknown += 1;
37568
+ else if (noRetrieval) citationNotSearched += 1;
37569
+ else total += 1;
37570
+ const isCited = citationKnown && !noRetrieval && snap.citationState === CitationStates.cited;
37561
37571
  if (isCited) cited += 1;
37562
37572
  const pp = perProvider.get(snap.provider) ?? { checked: 0, mentioned: 0, cited: 0 };
37563
37573
  if (snap.answerMentioned === true || snap.answerMentioned === false) pp.checked += 1;
@@ -37602,6 +37612,7 @@ function countPeriod(snaps, competitors2, queryClassOf, classificationAvailable,
37602
37612
  total,
37603
37613
  cited,
37604
37614
  citationUnknown,
37615
+ citationNotSearched,
37605
37616
  citationEvidence: snaps.some((snapshot) => snapshot.citationChecked !== void 0),
37606
37617
  projectCited: cited,
37607
37618
  competitorCited,
@@ -37661,7 +37672,7 @@ function computeVisibilityCompare(input) {
37661
37672
  });
37662
37673
  const fromShare = shareCounts(fromShareCounts);
37663
37674
  const toShare = shareCounts(toShareCounts);
37664
- const citationPeriod = (c, value) => c.citationEvidence ? { ...value, excludedUnknown: c.citationUnknown } : value;
37675
+ const citationPeriod = (c, value) => c.citationEvidence ? { ...value, excludedUnknown: c.citationUnknown, ...c.citationNotSearched > 0 ? { excludedNoRetrieval: c.citationNotSearched } : {} } : value;
37665
37676
  const metrics = [
37666
37677
  metric(
37667
37678
  "mention-share-of-voice",
@@ -37708,7 +37719,7 @@ function computeVisibilityCompare(input) {
37708
37719
  const classPeriod = (snapshots, queryClass, signal) => {
37709
37720
  if (!classificationAvailable) return { ...period(0, 0), availability: "classification-unavailable" };
37710
37721
  const selected = snapshots.filter((snapshot) => queryClassOf(snapshot) === queryClass && (signal === "cited" || snapshot.mentionApplicable !== false));
37711
- const checked = selected.filter((snapshot) => signal === "mention" ? typeof snapshot.answerMentioned === "boolean" : snapshot.citationChecked !== false);
37722
+ const checked = selected.filter((snapshot) => signal === "mention" ? typeof snapshot.answerMentioned === "boolean" : snapshot.citationChecked !== false && snapshot.retrievalStatus !== RetrievalStatuses["not-used"]);
37712
37723
  const numerator = checked.filter((snapshot) => signal === "mention" ? snapshot.answerMentioned === true : snapshot.citationState === CitationStates.cited).length;
37713
37724
  return { ...period(numerator, checked.length), excludedUnknown: selected.length - checked.length };
37714
37725
  };
@@ -43354,7 +43365,7 @@ var routeCatalog = [
43354
43365
  method: "get",
43355
43366
  path: "/api/v1/projects/{name}/visibility-compare",
43356
43367
  summary: "Compare AEO visibility month over month",
43357
- description: "Statistically honest month-over-month AEO comparison in one call. PRIMARY metric is share of voice (brand vs competitor mentions in the same answers), which is less exposed to broad model-wide naming propensity than an absolute rate but never bypasses model continuity. Rates are pooled per-snapshot over each month (invariant to sweep count), restricted to query/provider pairs present in BOTH months, and then restricted again to providers with exactly one known, identical configured model id in both months. `continuity` reports every provider and its model evidence; changed, mixed mid-month, or legacy-unknown models are excluded. When no provider remains, metrics return a continuity-blocked verdict rather than a directional call. `from` must be a month strictly before `to`. Adds separate branded/non-brand mention and cited rates with classification-unavailable rather than pooled fallback. Advanced class metrics read frozen Property/market/execution assignments and reuse each answer once per class. Without a scope or marketKey the four original metrics keep the project frame (provider/location narrow it the same way for Simple and Advanced) and Advanced class metrics carry their own `classComparison` cohort. A scope or marketKey answers entirely from the frozen frame and is refused when either month holds runs whose plan cannot be reconstructed (schema v1). On frozen frames, answers with incomplete source capture leave every citation figure and are counted in `excludedUnknown`. A silent upstream version bump under an unchanged configured id remains undetectable. competitorIdentityChangedAt is when the project's competitor names last changed (a tracked competitor's curated or stored auto-detected names (a block counts only when it removes a stored name), a detection pass or a write that moved a market-only competitor's learned names, or a published revision that renames a market pin). Share of voice reads both months with the tracked competitors' current names only, so a comparison quoted before a change to those names was restated; a market pin or learned-name change moves the time without changing share of voice. A scoped comparison (a group, market or property scope, or a marketKey) reads frozen plan history and carries no competitorIdentityChangedAt.",
43368
+ description: "Statistically honest month-over-month AEO comparison in one call. PRIMARY metric is share of voice (brand vs competitor mentions in the same answers), which is less exposed to broad model-wide naming propensity than an absolute rate but never bypasses model continuity. Rates are pooled per-snapshot over each month (invariant to sweep count), restricted to query/provider pairs present in BOTH months, and then restricted again to providers with exactly one known, identical configured model id in both months. `continuity` reports every provider and its model evidence; changed, mixed mid-month, or legacy-unknown models are excluded. When no provider remains, metrics return a continuity-blocked verdict rather than a directional call. `from` must be a month strictly before `to`. Adds separate branded/non-brand mention and cited rates with classification-unavailable rather than pooled fallback. Advanced class metrics read frozen Property/market/execution assignments and reuse each answer once per class. Without a scope or marketKey the four original metrics keep the project frame (provider/location narrow it the same way for Simple and Advanced) and Advanced class metrics carry their own `classComparison` cohort. A scope or marketKey answers entirely from the frozen frame and is refused when either month holds runs whose plan cannot be reconstructed (schema v1). On frozen frames, answers with incomplete source capture leave every citation figure and are counted in `excludedUnknown`. An answer the provider wrote with no retrieval step at all (retrievalStatus of not-used) cites nothing by construction, so it also leaves the cited denominator and is counted in `excludedNoRetrieval`; it stays in the mention denominator, since brand recall does not need search. A silent upstream version bump under an unchanged configured id remains undetectable. competitorIdentityChangedAt is when the project's competitor names last changed (a tracked competitor's curated or stored auto-detected names (a block counts only when it removes a stored name), a detection pass or a write that moved a market-only competitor's learned names, or a published revision that renames a market pin). Share of voice reads both months with the tracked competitors' current names only, so a comparison quoted before a change to those names was restated; a market pin or learned-name change moves the time without changing share of voice. A scoped comparison (a group, market or property scope, or a marketKey) reads frozen plan history and carries no competitorIdentityChangedAt.",
43358
43369
  tags: ["analytics"],
43359
43370
  parameters: [
43360
43371
  nameParameter,
@@ -46517,6 +46528,7 @@ var routeCatalog = [
46517
46528
  // TODO: Add `GaConnectResponse` Zod schema in contracts.
46518
46529
  200: rawJsonResponse("GA4 connection established.", looseObjectSchema),
46519
46530
  400: errorResponse("Invalid GA4 connection request."),
46531
+ 403: errorResponse("Google refused the property (`details.reason` is `ga4-permission-missing`). Retrying will not help until access is granted; repeated refusals are answered without asking Google again until `details.nextCheckAt`."),
46520
46532
  404: errorResponse("Project not found.")
46521
46533
  }
46522
46534
  },
@@ -60100,11 +60112,76 @@ async function adsRoutes(app, opts) {
60100
60112
  }
60101
60113
 
60102
60114
  // ../api-routes/src/bing.ts
60103
- import crypto42 from "crypto";
60115
+ import crypto43 from "crypto";
60104
60116
  import { eq as eq66, and as and54, desc as desc32 } from "drizzle-orm";
60105
60117
 
60106
- // ../integration-bing/src/bing-client.ts
60118
+ // ../api-routes/src/connection-verify-cache.ts
60107
60119
  import crypto41 from "crypto";
60120
+ var CONNECTION_VERIFY_TTL_MS = 15 * 60 * 1e3;
60121
+ var REFUSALS_BEFORE_BACKOFF = 3;
60122
+ var FIRST_BACKOFF_MS = 5 * 60 * 1e3;
60123
+ var MAX_BACKOFF_MS = 60 * 60 * 1e3;
60124
+ var MAX_ENTRIES = 500;
60125
+ function remember(entries, key, value) {
60126
+ entries.delete(key);
60127
+ entries.set(key, value);
60128
+ if (entries.size > MAX_ENTRIES) entries.delete(entries.keys().next().value);
60129
+ }
60130
+ function isInteractiveCaller() {
60131
+ const attribution = currentOutcomeAttribution();
60132
+ const label = usageSurfaceSchema.safeParse(attribution?.surfaceLabel);
60133
+ if (label.success) return label.data === "cli" || label.data === "dashboard";
60134
+ const ua = attribution?.userAgent ?? "";
60135
+ return ua.startsWith("canonry-cli") || ua.startsWith("Mozilla/");
60136
+ }
60137
+ function verifyCacheKey(...parts) {
60138
+ return crypto41.createHash("sha256").update(parts.join("\0")).digest("hex");
60139
+ }
60140
+ function createVerifyCache(ttlMs = CONNECTION_VERIFY_TTL_MS) {
60141
+ const entries = /* @__PURE__ */ new Map();
60142
+ return {
60143
+ get(key) {
60144
+ const entry = entries.get(key);
60145
+ if (!entry) return void 0;
60146
+ if (Date.now() - entry.at < ttlMs) return entry.value;
60147
+ entries.delete(key);
60148
+ return void 0;
60149
+ },
60150
+ set(key, value) {
60151
+ remember(entries, key, { value, at: Date.now() });
60152
+ }
60153
+ };
60154
+ }
60155
+ function createRefusalBackoff() {
60156
+ const entries = /* @__PURE__ */ new Map();
60157
+ return {
60158
+ /** The refusal to answer with instead of asking the upstream, while this key is backing off. */
60159
+ replay(key) {
60160
+ const entry = entries.get(key);
60161
+ if (!entry || entry.count < REFUSALS_BEFORE_BACKOFF) return void 0;
60162
+ const wait = Math.min(FIRST_BACKOFF_MS * 2 ** (entry.count - REFUSALS_BEFORE_BACKOFF), MAX_BACKOFF_MS);
60163
+ const nextCheckAt = entry.at + wait;
60164
+ if (Date.now() >= nextCheckAt) return void 0;
60165
+ const { error } = entry;
60166
+ return {
60167
+ error: new AppError(error.code, error.message, error.statusCode, {
60168
+ ...error.details,
60169
+ nextCheckAt: new Date(nextCheckAt).toISOString()
60170
+ }),
60171
+ cause: entry.cause
60172
+ };
60173
+ },
60174
+ refused(key, error, cause) {
60175
+ remember(entries, key, { count: (entries.get(key)?.count ?? 0) + 1, at: Date.now(), error, cause });
60176
+ },
60177
+ cleared(key) {
60178
+ entries.delete(key);
60179
+ }
60180
+ };
60181
+ }
60182
+
60183
+ // ../integration-bing/src/bing-client.ts
60184
+ import crypto42 from "crypto";
60108
60185
 
60109
60186
  // ../integration-bing/src/constants.ts
60110
60187
  var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
@@ -60259,7 +60336,7 @@ async function bingFetchOnce(apiKey, endpoint, opts) {
60259
60336
  var throttleCooldownUntil = /* @__PURE__ */ new Map();
60260
60337
  var cooldownNow = () => Date.now();
60261
60338
  function cooldownKey(apiKey) {
60262
- return crypto41.createHash("sha256").update(apiKey).digest("hex").slice(0, 12);
60339
+ return crypto42.createHash("sha256").update(apiKey).digest("hex").slice(0, 12);
60263
60340
  }
60264
60341
  function throttleCooldownRemainingMs(apiKey) {
60265
60342
  const until = throttleCooldownUntil.get(cooldownKey(apiKey));
@@ -60403,6 +60480,7 @@ async function loadBlockingCrawlIssues(apiKey, siteUrl, domain) {
60403
60480
  return blockedUrls;
60404
60481
  }
60405
60482
  async function bingRoutes(app, opts) {
60483
+ const keyChecks = createVerifyCache();
60406
60484
  function requireConnectionStore() {
60407
60485
  if (opts.bingConnectionStore) return opts.bingConnectionStore;
60408
60486
  throw validationError("Bing connection storage is not configured for this deployment");
@@ -60421,16 +60499,21 @@ async function bingRoutes(app, opts) {
60421
60499
  throw validationError("apiKey is required");
60422
60500
  }
60423
60501
  const project = resolveProject(app.db, request.params.name);
60424
- let sites;
60425
- try {
60426
- sites = await getSites(apiKey);
60427
- bingLog("info", "connect.verify-key", { domain: project.canonicalDomain, siteCount: sites.length });
60428
- } catch (e) {
60429
- const msg = describeError(e);
60430
- bingLog("error", "connect.verify-key-failed", { domain: project.canonicalDomain, error: msg });
60431
- attempt.failed(e);
60432
- throw validationError(`Failed to verify Bing API key: ${msg}`);
60502
+ const keyCheck = verifyCacheKey("bing-key", apiKey);
60503
+ let sites = isInteractiveCaller() ? void 0 : keyChecks.get(keyCheck);
60504
+ if (!sites) {
60505
+ try {
60506
+ sites = await getSites(apiKey);
60507
+ keyChecks.set(keyCheck, sites);
60508
+ bingLog("info", "connect.verify-key", { domain: project.canonicalDomain, siteCount: sites.length });
60509
+ } catch (e) {
60510
+ const msg = describeError(e);
60511
+ bingLog("error", "connect.verify-key-failed", { domain: project.canonicalDomain, error: msg });
60512
+ attempt.failed(e);
60513
+ throw validationError(`Failed to verify Bing API key: ${msg}`);
60514
+ }
60433
60515
  }
60516
+ const availableSites = sites.map((s) => ({ url: s.Url, verified: s.IsVerified ?? false }));
60434
60517
  const now = (/* @__PURE__ */ new Date()).toISOString();
60435
60518
  const existing = store.getConnection(project.canonicalDomain);
60436
60519
  if (existing) attempt.update({ action: "reauth" });
@@ -60440,6 +60523,9 @@ async function bingRoutes(app, opts) {
60440
60523
  `This domain already has a Bing connection owned by another project. Disconnect it from that project first before re-connecting from "${project.name}".`
60441
60524
  );
60442
60525
  }
60526
+ if (existing?.apiKey === apiKey && existing.createdByProjectId) {
60527
+ return { connected: true, domain: project.canonicalDomain, siteUrl: existing.siteUrl ?? null, availableSites };
60528
+ }
60443
60529
  store.upsertConnection({
60444
60530
  domain: project.canonicalDomain,
60445
60531
  apiKey,
@@ -60459,7 +60545,7 @@ async function bingRoutes(app, opts) {
60459
60545
  connected: true,
60460
60546
  domain: project.canonicalDomain,
60461
60547
  siteUrl: existing?.siteUrl ?? null,
60462
- availableSites: sites.map((s) => ({ url: s.Url, verified: s.IsVerified ?? false }))
60548
+ availableSites
60463
60549
  };
60464
60550
  }));
60465
60551
  app.delete("/projects/:name/bing/disconnect", connectionRoute(app, () => ({ integration: "bing", action: "disconnect" }), async (request, reply, attempt) => {
@@ -60589,7 +60675,7 @@ async function bingRoutes(app, opts) {
60589
60675
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
60590
60676
  const now = (/* @__PURE__ */ new Date()).toISOString();
60591
60677
  app.db.insert(bingCoverageSnapshots).values({
60592
- id: crypto42.randomUUID(),
60678
+ id: crypto43.randomUUID(),
60593
60679
  projectId: project.id,
60594
60680
  syncRunId: snapshotRunId,
60595
60681
  date: snapshotDate,
@@ -60664,7 +60750,7 @@ async function bingRoutes(app, opts) {
60664
60750
  throw validationError("url is required");
60665
60751
  }
60666
60752
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
60667
- const runId = crypto42.randomUUID();
60753
+ const runId = crypto43.randomUUID();
60668
60754
  app.db.insert(runs).values({
60669
60755
  id: runId,
60670
60756
  projectId: project.id,
@@ -60685,7 +60771,7 @@ async function bingRoutes(app, opts) {
60685
60771
  discoveryDate: result.DiscoveryDate ?? null
60686
60772
  });
60687
60773
  const now = (/* @__PURE__ */ new Date()).toISOString();
60688
- const id = crypto42.randomUUID();
60774
+ const id = crypto43.randomUUID();
60689
60775
  const httpCode = result.HttpStatus ?? result.HttpCode ?? null;
60690
60776
  const lastCrawledDate = parseBingDate(result.LastCrawledDate);
60691
60777
  const inIndexDate = parseBingDate(result.InIndexDate);
@@ -60762,7 +60848,7 @@ async function bingRoutes(app, opts) {
60762
60848
  throw validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
60763
60849
  }
60764
60850
  const now = (/* @__PURE__ */ new Date()).toISOString();
60765
- const runId = crypto42.randomUUID();
60851
+ const runId = crypto43.randomUUID();
60766
60852
  app.db.insert(runs).values({
60767
60853
  id: runId,
60768
60854
  projectId: project.id,
@@ -61060,7 +61146,7 @@ async function cdpRoutes(app, opts) {
61060
61146
  }
61061
61147
 
61062
61148
  // ../api-routes/src/ga.ts
61063
- import crypto44 from "crypto";
61149
+ import crypto45 from "crypto";
61064
61150
  import { eq as eq69, desc as desc34, and as and57, sql as sql27 } from "drizzle-orm";
61065
61151
 
61066
61152
  // ../api-routes/src/ga-session-history.ts
@@ -61120,14 +61206,14 @@ function findBiggestMover(current, prior) {
61120
61206
  }
61121
61207
 
61122
61208
  // ../api-routes/src/ga-search-landing-pages.ts
61123
- import crypto43 from "crypto";
61209
+ import crypto44 from "crypto";
61124
61210
  import { and as and56, asc as asc13, count as count4, desc as desc33, eq as eq68 } from "drizzle-orm";
61125
61211
  function persistSearchLandingSnapshot(db, input) {
61126
61212
  db.transaction((tx) => {
61127
61213
  deleteSearchLandingRows(tx, input.projectId);
61128
61214
  for (const window of input.windows) {
61129
61215
  tx.insert(gaSearchLandingWindows).values({
61130
- id: crypto43.randomUUID(),
61216
+ id: crypto44.randomUUID(),
61131
61217
  projectId: input.projectId,
61132
61218
  propertyId: input.propertyId,
61133
61219
  windowKey: window.window,
@@ -61149,7 +61235,7 @@ function persistSearchLandingSnapshot(db, input) {
61149
61235
  }).run();
61150
61236
  for (const row of window.rows) {
61151
61237
  tx.insert(gaSearchLandingPages).values({
61152
- id: crypto43.randomUUID(),
61238
+ id: crypto44.randomUUID(),
61153
61239
  projectId: input.projectId,
61154
61240
  windowKey: window.window,
61155
61241
  landingPage: row.landingPage,
@@ -61381,7 +61467,7 @@ function persistAcquisitionMeasurement(db, input) {
61381
61467
  )).run();
61382
61468
  for (const row of input.report.rows) {
61383
61469
  tx.insert(gaAcquisitionDaily).values({
61384
- id: crypto44.randomUUID(),
61470
+ id: crypto45.randomUUID(),
61385
61471
  projectId: input.projectId,
61386
61472
  date: row.date,
61387
61473
  channelGroup: row.channelGroup,
@@ -61423,7 +61509,7 @@ function persistLeadMeasurement(db, input) {
61423
61509
  )).run();
61424
61510
  for (const row of input.report.rows) {
61425
61511
  tx.insert(gaLeadEventsDaily).values({
61426
- id: crypto44.randomUUID(),
61512
+ id: crypto45.randomUUID(),
61427
61513
  projectId: input.projectId,
61428
61514
  date: row.date,
61429
61515
  eventName: row.eventName,
@@ -61533,6 +61619,18 @@ function ga4ErrorToAppError(err, context) {
61533
61619
  }
61534
61620
  return providerError(`${context}: ${describeError(err)}`);
61535
61621
  }
61622
+ function ga4ConnectError(err, propertyId, principal, context) {
61623
+ if (err instanceof GA4ApiError && err.status === 403) {
61624
+ return forbidden(
61625
+ `Google refused access to GA4 property ${propertyId} for ${principal}. Add ${principal} as a Viewer in Google Analytics (Admin > Property access management), then connect again. Retrying will not help until access is granted.`,
61626
+ { reason: "ga4-permission-missing", propertyId, retryable: false }
61627
+ );
61628
+ }
61629
+ return validationError(`${context}: ${describeError(err)}`);
61630
+ }
61631
+ function isPermissionRefusal(error) {
61632
+ return error.details?.reason === "ga4-permission-missing";
61633
+ }
61536
61634
  async function resolveGa4AccessToken(opts, projectName, canonicalDomain) {
61537
61635
  const saConn = opts.ga4CredentialStore?.getConnection(projectName);
61538
61636
  if (saConn?.clientEmail && saConn?.privateKey && saConn?.propertyId) {
@@ -61583,6 +61681,25 @@ function requireGa4Connection(opts, projectName, canonicalDomain) {
61583
61681
  }
61584
61682
  }
61585
61683
  async function ga4Routes(app, opts) {
61684
+ const connectPasses = createVerifyCache();
61685
+ const connectRefusals = createRefusalBackoff();
61686
+ async function checkGa4Access(key, verify, toError) {
61687
+ if (!isInteractiveCaller()) {
61688
+ if (connectPasses.get(key)) return { ok: true };
61689
+ const replay = connectRefusals.replay(key);
61690
+ if (replay) return { ok: false, ...replay };
61691
+ }
61692
+ try {
61693
+ await verify();
61694
+ connectPasses.set(key, true);
61695
+ connectRefusals.cleared(key);
61696
+ return { ok: true };
61697
+ } catch (cause) {
61698
+ const error = toError(cause);
61699
+ if (isPermissionRefusal(error)) connectRefusals.refused(key, error, cause);
61700
+ return { ok: false, error, cause };
61701
+ }
61702
+ }
61586
61703
  app.post("/projects/:name/ga/connect", connectionRoute(app, (request) => {
61587
61704
  const body = request.body;
61588
61705
  return { integration: "ga4", action: typeof body?.keyJson === "string" && body.keyJson ? "connect" : "select" };
@@ -61607,18 +61724,23 @@ async function ga4Routes(app, opts) {
61607
61724
  }
61608
61725
  const clientEmail = parsed.client_email;
61609
61726
  const privateKey = parsed.private_key;
61610
- try {
61611
- await verifyConnection(clientEmail, privateKey, propertyId);
61612
- gaLog("info", "connect.verified.service-account", { projectId: project.id, propertyId });
61613
- } catch (e) {
61614
- const msg = describeError(e);
61615
- gaLog("error", "connect.verify-failed", { projectId: project.id, propertyId, error: msg });
61616
- attempt.failed(e);
61617
- throw validationError(`Failed to verify GA4 credentials: ${msg}`);
61727
+ const check3 = await checkGa4Access(
61728
+ verifyCacheKey("ga4-service-account", project.id, propertyId, clientEmail, privateKey),
61729
+ () => verifyConnection(clientEmail, privateKey, propertyId),
61730
+ (err) => ga4ConnectError(err, propertyId, clientEmail, "Failed to verify GA4 credentials")
61731
+ );
61732
+ if (!check3.ok) {
61733
+ gaLog("error", "connect.verify-failed", { projectId: project.id, propertyId, error: check3.error.message });
61734
+ attempt.failed(check3.cause);
61735
+ throw check3.error;
61618
61736
  }
61737
+ gaLog("info", "connect.verified.service-account", { projectId: project.id, propertyId });
61619
61738
  const now = (/* @__PURE__ */ new Date()).toISOString();
61620
61739
  const existing = opts.ga4CredentialStore.getConnection(project.name);
61621
61740
  if (existing) attempt.update({ action: "reauth" });
61741
+ if (existing?.clientEmail === clientEmail && existing.privateKey === privateKey && existing.propertyId === propertyId) {
61742
+ return { connected: true, propertyId, authMethod: "service-account", clientEmail };
61743
+ }
61622
61744
  const previousPropertyId2 = resolveCurrentGa4PropertyId(opts, project.name, project.canonicalDomain);
61623
61745
  opts.ga4CredentialStore.upsertConnection({
61624
61746
  projectName: project.name,
@@ -61655,19 +61777,27 @@ async function ga4Routes(app, opts) {
61655
61777
  'No GA4 OAuth token found. Run "canonry google connect <project> --type ga4" first, or pass --key-file to use a service account.'
61656
61778
  );
61657
61779
  }
61658
- const accessToken = await refreshOAuthTokenIfNeeded(googleStore, authConfig, project.canonicalDomain, {
61659
- accessToken: oauthConn.accessToken,
61660
- refreshToken: oauthConn.refreshToken,
61661
- tokenExpiresAt: oauthConn.tokenExpiresAt
61662
- });
61663
- try {
61664
- await verifyConnectionWithToken(accessToken, propertyId);
61665
- gaLog("info", "connect.verified.oauth", { projectId: project.id, propertyId });
61666
- } catch (e) {
61667
- const msg = describeError(e);
61668
- gaLog("error", "connect.verify-failed.oauth", { projectId: project.id, propertyId, error: msg });
61669
- attempt.failed(e);
61670
- throw validationError(`Failed to verify GA4 access: ${msg}`);
61780
+ const { accessToken: storedAccessToken, refreshToken } = oauthConn;
61781
+ const check2 = await checkGa4Access(
61782
+ verifyCacheKey("ga4-oauth", project.id, propertyId, refreshToken),
61783
+ async () => {
61784
+ const accessToken = await refreshOAuthTokenIfNeeded(googleStore, authConfig, project.canonicalDomain, {
61785
+ accessToken: storedAccessToken,
61786
+ refreshToken,
61787
+ tokenExpiresAt: oauthConn.tokenExpiresAt
61788
+ });
61789
+ return verifyConnectionWithToken(accessToken, propertyId);
61790
+ },
61791
+ (err) => err instanceof AppError ? err : ga4ConnectError(err, propertyId, "the connected Google account", "Failed to verify GA4 access")
61792
+ );
61793
+ if (!check2.ok) {
61794
+ gaLog("error", "connect.verify-failed.oauth", { projectId: project.id, propertyId, error: check2.error.message });
61795
+ attempt.failed(check2.cause);
61796
+ throw check2.error;
61797
+ }
61798
+ gaLog("info", "connect.verified.oauth", { projectId: project.id, propertyId });
61799
+ if (oauthConn.propertyId === propertyId) {
61800
+ return { connected: true, propertyId, authMethod: "oauth" };
61671
61801
  }
61672
61802
  const previousPropertyId = resolveCurrentGa4PropertyId(opts, project.name, project.canonicalDomain);
61673
61803
  const connectedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -61777,7 +61907,7 @@ async function ga4Routes(app, opts) {
61777
61907
  const leadDays = measurementState?.leadSyncedAt != null ? days : GA4_MAX_SYNC_DAYS;
61778
61908
  const leadEventNames = project.measurement.leadEventNames;
61779
61909
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
61780
- const runId = crypto44.randomUUID();
61910
+ const runId = crypto45.randomUUID();
61781
61911
  app.db.insert(runs).values({
61782
61912
  id: runId,
61783
61913
  projectId: project.id,
@@ -61837,7 +61967,7 @@ async function ga4Routes(app, opts) {
61837
61967
  ).run();
61838
61968
  for (const row of rows) {
61839
61969
  tx.insert(gaTrafficSnapshots).values({
61840
- id: crypto44.randomUUID(),
61970
+ id: crypto45.randomUUID(),
61841
61971
  projectId: project.id,
61842
61972
  date: row.date,
61843
61973
  landingPage: row.landingPage,
@@ -61860,7 +61990,7 @@ async function ga4Routes(app, opts) {
61860
61990
  ).run();
61861
61991
  for (const row of dailyTotals) {
61862
61992
  tx.insert(gaDailyTotals).values({
61863
- id: crypto44.randomUUID(),
61993
+ id: crypto45.randomUUID(),
61864
61994
  projectId: project.id,
61865
61995
  date: row.date,
61866
61996
  sessions: row.sessions,
@@ -61886,7 +62016,7 @@ async function ga4Routes(app, opts) {
61886
62016
  ).run();
61887
62017
  for (const row of aiReferrals) {
61888
62018
  tx.insert(gaAiReferrals).values({
61889
- id: crypto44.randomUUID(),
62019
+ id: crypto45.randomUUID(),
61890
62020
  projectId: project.id,
61891
62021
  date: row.date,
61892
62022
  source: row.source,
@@ -61918,7 +62048,7 @@ async function ga4Routes(app, opts) {
61918
62048
  ).run();
61919
62049
  for (const row of socialReferrals) {
61920
62050
  tx.insert(gaSocialReferrals).values({
61921
- id: crypto44.randomUUID(),
62051
+ id: crypto45.randomUUID(),
61922
62052
  projectId: project.id,
61923
62053
  date: row.date,
61924
62054
  source: row.source,
@@ -61934,7 +62064,7 @@ async function ga4Routes(app, opts) {
61934
62064
  if (syncSummary) {
61935
62065
  tx.delete(gaTrafficSummaries).where(eq69(gaTrafficSummaries.projectId, project.id)).run();
61936
62066
  tx.insert(gaTrafficSummaries).values({
61937
- id: crypto44.randomUUID(),
62067
+ id: crypto45.randomUUID(),
61938
62068
  projectId: project.id,
61939
62069
  periodStart: summary3.periodStart,
61940
62070
  periodEnd: summary3.periodEnd,
@@ -61947,7 +62077,7 @@ async function ga4Routes(app, opts) {
61947
62077
  tx.delete(gaTrafficWindowSummaries).where(eq69(gaTrafficWindowSummaries.projectId, project.id)).run();
61948
62078
  for (const ws of windowSummaries) {
61949
62079
  tx.insert(gaTrafficWindowSummaries).values({
61950
- id: crypto44.randomUUID(),
62080
+ id: crypto45.randomUUID(),
61951
62081
  projectId: project.id,
61952
62082
  windowKey: ws.windowKey,
61953
62083
  periodStart: ws.periodStart,
@@ -62744,7 +62874,7 @@ function parseSchemaPageEntry(entry) {
62744
62874
  }
62745
62875
 
62746
62876
  // ../integration-wordpress/src/wordpress-client.ts
62747
- import crypto45 from "crypto";
62877
+ import crypto46 from "crypto";
62748
62878
  function validateUsername(username) {
62749
62879
  if (!username || typeof username !== "string" || username.trim().length === 0) {
62750
62880
  throw new WordpressApiError("AUTH_INVALID", "Username is required and must be a non-empty string", 400);
@@ -62964,7 +63094,7 @@ function buildSnippet(content) {
62964
63094
  return `${text2.slice(0, 157)}...`;
62965
63095
  }
62966
63096
  function contentHash(content) {
62967
- return crypto45.createHash("sha256").update(content).digest("hex");
63097
+ return crypto46.createHash("sha256").update(content).digest("hex");
62968
63098
  }
62969
63099
  function buildAmbiguousSlugMessage(slug, pages) {
62970
63100
  const candidates = pages.map((page3) => {
@@ -64457,7 +64587,7 @@ async function wordpressRoutes(app, opts) {
64457
64587
  }
64458
64588
 
64459
64589
  // ../api-routes/src/backlinks.ts
64460
- import crypto46 from "crypto";
64590
+ import crypto47 from "crypto";
64461
64591
  import { and as and61, asc as asc15, desc as desc36, eq as eq72, sql as sql29 } from "drizzle-orm";
64462
64592
 
64463
64593
  // ../integration-commoncrawl/src/constants.ts
@@ -65110,7 +65240,7 @@ async function backlinksRoutes(app, opts) {
65110
65240
  const refreshed = app.db.select().from(ccReleaseSyncs).where(eq72(ccReleaseSyncs.id, existing.id)).get();
65111
65241
  return reply.status(200).send(mapSyncRow(refreshed));
65112
65242
  }
65113
- const id = crypto46.randomUUID();
65243
+ const id = crypto47.randomUUID();
65114
65244
  app.db.insert(ccReleaseSyncs).values({
65115
65245
  id,
65116
65246
  release,
@@ -65167,7 +65297,7 @@ async function backlinksRoutes(app, opts) {
65167
65297
  throw validationError("Invalid release id");
65168
65298
  }
65169
65299
  const now = (/* @__PURE__ */ new Date()).toISOString();
65170
- const runId = crypto46.randomUUID();
65300
+ const runId = crypto47.randomUUID();
65171
65301
  app.db.insert(runs).values({
65172
65302
  id: runId,
65173
65303
  projectId: project.id,
@@ -65256,7 +65386,7 @@ async function backlinksRoutes(app, opts) {
65256
65386
  }
65257
65387
 
65258
65388
  // ../api-routes/src/traffic.ts
65259
- import crypto49 from "crypto";
65389
+ import crypto50 from "crypto";
65260
65390
  import { isIP } from "net";
65261
65391
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
65262
65392
  import { and as and65, desc as desc37, eq as eq76, gte as gte18, inArray as inArray31, lte as lte20, sql as sql32 } from "drizzle-orm";
@@ -65284,7 +65414,7 @@ function resolveVercelSyncDeadlineMs(env = process.env) {
65284
65414
  }
65285
65415
 
65286
65416
  // ../integration-cloud-run/src/auth.ts
65287
- import crypto47 from "crypto";
65417
+ import crypto48 from "crypto";
65288
65418
  var GOOGLE_TOKEN_URL3 = "https://oauth2.googleapis.com/token";
65289
65419
  var CLOUD_LOGGING_READ_SCOPE = "https://www.googleapis.com/auth/logging.read";
65290
65420
  var TOKEN_REQUEST_TIMEOUT_MS = 3e4;
@@ -65315,7 +65445,7 @@ function createServiceAccountJwt2(clientEmail, privateKey, scope) {
65315
65445
  const headerB64 = encode(header2);
65316
65446
  const payloadB64 = encode(payload);
65317
65447
  const signingInput = `${headerB64}.${payloadB64}`;
65318
- const sign = crypto47.createSign("RSA-SHA256");
65448
+ const sign = crypto48.createSign("RSA-SHA256");
65319
65449
  sign.update(signingInput);
65320
65450
  const signature = sign.sign(privateKey, "base64url");
65321
65451
  return `${signingInput}.${signature}`;
@@ -81924,7 +82054,7 @@ async function ackCloudflareQueueMessages(client, options) {
81924
82054
  }
81925
82055
 
81926
82056
  // ../api-routes/src/traffic-event-ingest.ts
81927
- import crypto48 from "crypto";
82057
+ import crypto49 from "crypto";
81928
82058
  import { and as and63, eq as eq74, gte as gte17, lt as lt12, lte as lte18, sql as sql31 } from "drizzle-orm";
81929
82059
 
81930
82060
  // ../api-routes/src/raw-event-sample-retention.ts
@@ -82223,7 +82353,7 @@ function writeTrafficEventBatch(opts) {
82223
82353
  }
82224
82354
  })();
82225
82355
  tx.insert(rawEventSamples).values({
82226
- id: crypto48.randomUUID(),
82356
+ id: crypto49.randomUUID(),
82227
82357
  projectId: opts.projectId,
82228
82358
  sourceId: opts.sourceId,
82229
82359
  ts: sampleTimestamp,
@@ -82324,7 +82454,7 @@ var WORDPRESS_SYNC_LEASE_TTL_MS = 5 * 6e4;
82324
82454
  var DEFAULT_CLOUDFLARE_INGEST_RATE_LIMIT_MAX = 6e3;
82325
82455
  var SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/i;
82326
82456
  function sha256Hex3(value) {
82327
- return crypto49.createHash("sha256").update(value).digest("hex");
82457
+ return crypto50.createHash("sha256").update(value).digest("hex");
82328
82458
  }
82329
82459
  function hashCloudflareBearerToken(value) {
82330
82460
  return sha256Hex3(value);
@@ -82334,7 +82464,7 @@ function timingSafeEqualHex(a, b) {
82334
82464
  if (!SHA256_HEX_PATTERN.test(a) || !SHA256_HEX_PATTERN.test(b)) return false;
82335
82465
  const left = Buffer.from(a, "hex");
82336
82466
  const right = Buffer.from(b, "hex");
82337
- return left.length === right.length && crypto49.timingSafeEqual(left, right);
82467
+ return left.length === right.length && crypto50.timingSafeEqual(left, right);
82338
82468
  }
82339
82469
  function parseDirectPushCloudflareSourceConfig(config) {
82340
82470
  const parsed = cloudflareTrafficSourceConfigSchema.safeParse(config);
@@ -82652,7 +82782,7 @@ function bindTrafficSyncSchedule(tx, projectId, sourceId, now, createIfMissing =
82652
82782
  }
82653
82783
  if (!createIfMissing) return { changed: false, created: false };
82654
82784
  tx.insert(schedules).values({
82655
- id: crypto49.randomUUID(),
82785
+ id: crypto50.randomUUID(),
82656
82786
  projectId,
82657
82787
  kind: SchedulableRunKinds["traffic-sync"],
82658
82788
  cronExpr: DEFAULT_TRAFFIC_SYNC_CRON,
@@ -82973,7 +83103,7 @@ async function runBackfillTask(options) {
82973
83103
  }
82974
83104
  })();
82975
83105
  tx.insert(rawEventSamples).values({
82976
- id: crypto49.randomUUID(),
83106
+ id: crypto50.randomUUID(),
82977
83107
  projectId: project.id,
82978
83108
  sourceId: sourceRow2.id,
82979
83109
  ts: sampleTimestamp,
@@ -83164,7 +83294,7 @@ async function trafficRoutes(app, opts) {
83164
83294
  const fallbackName = displayName ?? `Cloud Run \xB7 ${gcpProjectId}${serviceName ? ` / ${serviceName}` : ""}`;
83165
83295
  const { sourceRow: sourceRow2, scheduleChanged } = app.db.transaction((tx) => {
83166
83296
  const activeSource = tx.select().from(trafficSources).where(eq76(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes["cloud-run"] && row.status !== TrafficSourceStatuses.archived);
83167
- const sourceId = activeSource?.id ?? crypto49.randomUUID();
83297
+ const sourceId = activeSource?.id ?? crypto50.randomUUID();
83168
83298
  const sourceStatus = trafficConnectStatus(tx, project.id, activeSource);
83169
83299
  if (activeSource) {
83170
83300
  tx.update(trafficSources).set({
@@ -83256,7 +83386,7 @@ async function trafficRoutes(app, opts) {
83256
83386
  const { sourceRow: sourceRow2, scheduleChanged } = app.db.transaction((tx) => {
83257
83387
  const activeSource = tx.select().from(trafficSources).where(eq76(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.wordpress && row.status !== TrafficSourceStatuses.archived);
83258
83388
  const endpointChanged = activeSource !== void 0 && wordpressBaseUrlIdentity(activeSource.configJson.baseUrl) !== wordpressBaseUrlIdentity(baseUrl);
83259
- const sourceId = endpointChanged ? crypto49.randomUUID() : activeSource?.id ?? crypto49.randomUUID();
83389
+ const sourceId = endpointChanged ? crypto50.randomUUID() : activeSource?.id ?? crypto50.randomUUID();
83260
83390
  const sourceStatus = trafficConnectStatus(tx, project.id, activeSource);
83261
83391
  if (activeSource && !endpointChanged) {
83262
83392
  tx.update(trafficSources).set({
@@ -83368,7 +83498,7 @@ async function trafficRoutes(app, opts) {
83368
83498
  }).where(eq76(trafficSources.id, activeSource.id)).run();
83369
83499
  row = tx.select().from(trafficSources).where(eq76(trafficSources.id, activeSource.id)).get();
83370
83500
  } else {
83371
- const newId = crypto49.randomUUID();
83501
+ const newId = crypto50.randomUUID();
83372
83502
  tx.insert(trafficSources).values({
83373
83503
  id: newId,
83374
83504
  projectId: project.id,
@@ -83438,7 +83568,7 @@ async function trafficRoutes(app, opts) {
83438
83568
  const cloudflareSources = app.db.select().from(trafficSources).where(eq76(trafficSources.projectId, project.id)).all().filter((row) => row.sourceType === TrafficSourceTypes.cloudflare && row.status !== TrafficSourceStatuses.archived);
83439
83569
  const sameModeSource = cloudflareSources.find((row) => parseQueuePullCloudflareSourceConfig(row.configJson));
83440
83570
  if (sameModeSource) attempt.update({ action: "reauth" });
83441
- const sourceId2 = sameModeSource?.id ?? crypto49.randomUUID();
83571
+ const sourceId2 = sameModeSource?.id ?? crypto50.randomUUID();
83442
83572
  const previousCredential2 = credentialStore.getConnectionBySourceId(sourceId2);
83443
83573
  const workerVersion2 = CURRENT_CLOUDFLARE_WORKER_VERSION;
83444
83574
  const workerScript2 = generateWorkerScript({
@@ -83574,7 +83704,7 @@ async function trafficRoutes(app, opts) {
83574
83704
  const now = (/* @__PURE__ */ new Date()).toISOString();
83575
83705
  const activeSource = app.db.select().from(trafficSources).where(eq76(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.cloudflare && row.status !== TrafficSourceStatuses.archived && parseDirectPushCloudflareSourceConfig(row.configJson) !== null);
83576
83706
  if (activeSource) attempt.update({ action: "reauth" });
83577
- const sourceId = activeSource?.id ?? crypto49.randomUUID();
83707
+ const sourceId = activeSource?.id ?? crypto50.randomUUID();
83578
83708
  const parsedActiveConfig = activeSource ? parseDirectPushCloudflareSourceConfig(activeSource.configJson) : null;
83579
83709
  if (activeSource && !parsedActiveConfig) {
83580
83710
  throw validationError("Existing Cloudflare source is not configured for direct-push delivery");
@@ -83590,8 +83720,8 @@ async function trafficRoutes(app, opts) {
83590
83720
  const previousAccountId = reusableCredential?.accountId ?? parsedActiveConfig?.accountId ?? null;
83591
83721
  const effectiveZoneId = zoneId ?? previousZoneId;
83592
83722
  const effectiveAccountId = accountId ?? previousAccountId;
83593
- const bearerToken = reusableCredential?.bearerToken ?? `cnry_cfw_${crypto49.randomBytes(32).toString("hex")}`;
83594
- const hmacSecret = reusableCredential?.hmacSecret ?? crypto49.randomBytes(32).toString("hex");
83723
+ const bearerToken = reusableCredential?.bearerToken ?? `cnry_cfw_${crypto50.randomBytes(32).toString("hex")}`;
83724
+ const hmacSecret = reusableCredential?.hmacSecret ?? crypto50.randomBytes(32).toString("hex");
83595
83725
  const ingestTokenHash = sha256Hex3(bearerToken);
83596
83726
  const ingestUrl = opts.cloudflareTrafficIngestUrl.replace("{name}", encodeURIComponent(project.name));
83597
83727
  assertCloudflareIngestUrlIsPublicHttps(ingestUrl);
@@ -83937,7 +84067,7 @@ async function trafficRoutes(app, opts) {
83937
84067
  apiToken: credential.apiToken
83938
84068
  };
83939
84069
  const now = (/* @__PURE__ */ new Date()).toISOString();
83940
- const leaseOwner = crypto49.randomUUID();
84070
+ const leaseOwner = crypto50.randomUUID();
83941
84071
  if (!tryClaimTrafficSyncLease({
83942
84072
  db: app.db,
83943
84073
  sourceId: sourceRow2.id,
@@ -83947,7 +84077,7 @@ async function trafficRoutes(app, opts) {
83947
84077
  })) {
83948
84078
  throw operationInProgress("Cloudflare Queue source sync is already in progress", { sourceId: sourceRow2.id });
83949
84079
  }
83950
- const runId = crypto49.randomUUID();
84080
+ const runId = crypto50.randomUUID();
83951
84081
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
83952
84082
  const startedMs = Date.now();
83953
84083
  try {
@@ -84187,7 +84317,7 @@ async function trafficRoutes(app, opts) {
84187
84317
  if (sourceRow2.status === TrafficSourceStatuses.paused || sourceRow2.status === TrafficSourceStatuses.archived || hasConnectedSibling) {
84188
84318
  throw validationError("Traffic source must be active before it can sync");
84189
84319
  }
84190
- const wordpressLeaseOwner = sourceRow2.sourceType === TrafficSourceTypes.wordpress ? crypto49.randomUUID() : void 0;
84320
+ const wordpressLeaseOwner = sourceRow2.sourceType === TrafficSourceTypes.wordpress ? crypto50.randomUUID() : void 0;
84191
84321
  if (wordpressLeaseOwner && !tryClaimTrafficSyncLease({
84192
84322
  db: app.db,
84193
84323
  sourceId: sourceRow2.id,
@@ -84202,7 +84332,7 @@ async function trafficRoutes(app, opts) {
84202
84332
  let windowEnd = syncWindowEnd;
84203
84333
  const startedAt = syncWindowEnd.toISOString();
84204
84334
  const syncStartedAtMs = syncWindowEnd.getTime();
84205
- const runId = crypto49.randomUUID();
84335
+ const runId = crypto50.randomUUID();
84206
84336
  app.db.insert(runs).values({
84207
84337
  id: runId,
84208
84338
  projectId: project.id,
@@ -84751,7 +84881,7 @@ async function trafficRoutes(app, opts) {
84751
84881
  }
84752
84882
  })();
84753
84883
  tx.insert(rawEventSamples).values({
84754
- id: crypto49.randomUUID(),
84884
+ id: crypto50.randomUUID(),
84755
84885
  projectId: project.id,
84756
84886
  sourceId: sourceRow2.id,
84757
84887
  ts: sampleTimestamp,
@@ -84983,7 +85113,7 @@ async function trafficRoutes(app, opts) {
84983
85113
  };
84984
85114
  }
84985
85115
  const startedAt = windowEnd.toISOString();
84986
- const runId = crypto49.randomUUID();
85116
+ const runId = crypto50.randomUUID();
84987
85117
  app.db.transaction((tx) => {
84988
85118
  const latestSource = tx.select().from(trafficSources).where(eq76(trafficSources.id, sourceRow2.id)).get();
84989
85119
  if (!latestSource || !isAuthoritativeTrafficSource(tx, latestSource) || !isSameTrafficSourceGeneration(latestSource, sourceRow2)) {
@@ -85864,7 +85994,7 @@ var REFERRAL_ASSESSMENT_CHECKS = [{
85864
85994
  }];
85865
85995
 
85866
85996
  // ../api-routes/src/doctor/checks/agent.ts
85867
- import crypto50 from "crypto";
85997
+ import crypto51 from "crypto";
85868
85998
  import fs6 from "fs";
85869
85999
  import path7 from "path";
85870
86000
  var REQUIRED_SKILLS = ["canonry", "aero"];
@@ -86098,7 +86228,7 @@ function isInstalled(dir) {
86098
86228
  }
86099
86229
  function hashInstalledFile(filePath) {
86100
86230
  try {
86101
- return crypto50.createHash("sha256").update(fs6.readFileSync(filePath)).digest("hex");
86231
+ return crypto51.createHash("sha256").update(fs6.readFileSync(filePath)).digest("hex");
86102
86232
  } catch {
86103
86233
  return void 0;
86104
86234
  }
@@ -89981,7 +90111,7 @@ async function statusRoutes(app, opts) {
89981
90111
  }
89982
90112
 
89983
90113
  // ../api-routes/src/discovery/routes.ts
89984
- import crypto51 from "crypto";
90114
+ import crypto52 from "crypto";
89985
90115
  import { and as and76, desc as desc42, eq as eq92, gte as gte23, inArray as inArray37, isNull as isNull17, or as or21 } from "drizzle-orm";
89986
90116
  var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
89987
90117
  async function discoveryRoutes(app, opts) {
@@ -90047,8 +90177,8 @@ async function discoveryRoutes(app, opts) {
90047
90177
  if (existing && existing.runId) {
90048
90178
  return { reused: true, sessionId: existing.id, runId: existing.runId };
90049
90179
  }
90050
- const sessionId = crypto51.randomUUID();
90051
- const runId = crypto51.randomUUID();
90180
+ const sessionId = crypto52.randomUUID();
90181
+ const runId = crypto52.randomUUID();
90052
90182
  tx.insert(discoverySessions).values({
90053
90183
  id: sessionId,
90054
90184
  projectId: project.id,
@@ -90303,7 +90433,7 @@ async function discoveryRoutes(app, opts) {
90303
90433
  const marketNames = beginMarketNameWrite(tx, project.id);
90304
90434
  for (const query of promotedQueries) {
90305
90435
  tx.insert(queries).values({
90306
- id: crypto51.randomUUID(),
90436
+ id: crypto52.randomUUID(),
90307
90437
  projectId: project.id,
90308
90438
  query,
90309
90439
  provenance,
@@ -90484,7 +90614,7 @@ function planCompetitorPromotion(input) {
90484
90614
  }
90485
90615
 
90486
90616
  // ../api-routes/src/discovery/orchestrate.ts
90487
- import crypto52 from "crypto";
90617
+ import crypto53 from "crypto";
90488
90618
  import { eq as eq93 } from "drizzle-orm";
90489
90619
  var DEFAULT_MAX_PROBES = 100;
90490
90620
  var ABSOLUTE_MAX_PROBES = 500;
@@ -90650,7 +90780,7 @@ async function executeDiscovery(opts) {
90650
90780
  probeRows.push({ citedDomains: probe.citedDomains, bucket });
90651
90781
  buckets[bucket]++;
90652
90782
  return {
90653
- id: crypto52.randomUUID(),
90783
+ id: crypto53.randomUUID(),
90654
90784
  sessionId: opts.sessionId,
90655
90785
  projectId: opts.project.id,
90656
90786
  query: probedCanonicals[index2],
@@ -90702,7 +90832,7 @@ function upsertDomainClassifications(db, projectId, sessionId, competitorMap) {
90702
90832
  const domain = hostOf(entry.domain) ?? "";
90703
90833
  if (!domain) continue;
90704
90834
  db.insert(domainClassifications).values({
90705
- id: crypto52.randomUUID(),
90835
+ id: crypto53.randomUUID(),
90706
90836
  projectId,
90707
90837
  domain,
90708
90838
  competitorType: entry.competitorType,
@@ -90742,7 +90872,7 @@ function dedupeStrings(input) {
90742
90872
  }
90743
90873
 
90744
90874
  // ../api-routes/src/technical-aeo.ts
90745
- import crypto53 from "crypto";
90875
+ import crypto54 from "crypto";
90746
90876
  import { and as and78, asc as asc18, count as count7, desc as desc43, eq as eq95, gte as gte24, inArray as inArray38, isNotNull as isNotNull9, isNull as isNull18, lt as lt13, or as or22, sql as sql35 } from "drizzle-orm";
90747
90877
  import { alias } from "drizzle-orm/sqlite-core";
90748
90878
 
@@ -92735,7 +92865,7 @@ async function technicalAeoRoutes(app, opts) {
92735
92865
  const healthState = parseSiteHealthState(request.query.healthState);
92736
92866
  const limit = parseBoundedLimit(request.query.limit, 100, 200);
92737
92867
  const sort = ["score-desc", "score-asc", "path"].includes(request.query.sort ?? "") ? request.query.sort : "url";
92738
- const selectionKey = crypto53.createHash("sha256").update(JSON.stringify({
92868
+ const selectionKey = crypto54.createHash("sha256").update(JSON.stringify({
92739
92869
  projectId: project.id,
92740
92870
  runId: snapshot.runId,
92741
92871
  attemptId: snapshot.attemptId,
@@ -93235,7 +93365,7 @@ async function technicalAeoRoutes(app, opts) {
93235
93365
  );
93236
93366
  }
93237
93367
  const now = (/* @__PURE__ */ new Date()).toISOString();
93238
- const runId = crypto53.randomUUID();
93368
+ const runId = crypto54.randomUUID();
93239
93369
  tx.insert(runs).values({
93240
93370
  id: runId,
93241
93371
  projectId: project.id,
@@ -93272,7 +93402,7 @@ async function technicalAeoRoutes(app, opts) {
93272
93402
  }
93273
93403
 
93274
93404
  // ../api-routes/src/research.ts
93275
- import crypto54 from "crypto";
93405
+ import crypto55 from "crypto";
93276
93406
  import { z as z5 } from "zod";
93277
93407
  import { and as and79, count as count8, desc as desc44, eq as eq96, gte as gte25, lt as lt14, or as or23, sql as sql36 } from "drizzle-orm";
93278
93408
  var sameLocation = (a, b) => a.label === b.label && a.city === b.city && a.region === b.region && a.country === b.country && a.timezone === b.timezone;
@@ -93342,9 +93472,9 @@ async function researchRoutes(app, opts) {
93342
93472
  throw researchDailyLimitExceeded(project.name, viewerDailyRunLimit, date);
93343
93473
  }
93344
93474
  }
93345
- const id = crypto54.randomUUID();
93475
+ const id = crypto55.randomUUID();
93346
93476
  tx.insert(researchRuns).values({ id, projectId: project.id, status: ResearchRunStatuses.queued, provider: providerName, requestedModel: requestedModel2, resolvedModel, location: location ?? null, totalQueries: input.queries.length, scope, template, idempotencyKey: input.idempotencyKey ?? null, requestHash: input.idempotencyKey ? requestHash2 : null, initiatedBy, createdAt: now }).run();
93347
- for (const [position, query] of input.queries.entries()) tx.insert(researchRunQueries).values({ id: crypto54.randomUUID(), researchRunId: id, position, queryText: query, queryClass: queryClasses[position] ?? null, status: ResearchQueryStatuses.queued, requestedModel: requestedModel2, resolvedModel, groundingSources: [], citedDomains: [], searchQueries: [], createdAt: now }).run();
93477
+ for (const [position, query] of input.queries.entries()) tx.insert(researchRunQueries).values({ id: crypto55.randomUUID(), researchRunId: id, position, queryText: query, queryClass: queryClasses[position] ?? null, status: ResearchQueryStatuses.queued, requestedModel: requestedModel2, resolvedModel, groundingSources: [], citedDomains: [], searchQueries: [], createdAt: now }).run();
93348
93478
  writeAuditLog(tx, { projectId: project.id, actor: "api", action: "research.created", entityType: "research_run", entityId: id });
93349
93479
  return { reused: false, id, shouldDispatch: true };
93350
93480
  });
@@ -93397,7 +93527,7 @@ async function researchRoutes(app, opts) {
93397
93527
  }
93398
93528
  const ids = [];
93399
93529
  for (const [index2, run] of prepared.entries()) {
93400
- const id = crypto54.randomUUID();
93530
+ const id = crypto55.randomUUID();
93401
93531
  ids.push(id);
93402
93532
  tx.insert(researchRuns).values({
93403
93533
  id,
@@ -93416,7 +93546,7 @@ async function researchRoutes(app, opts) {
93416
93546
  createdAt: now
93417
93547
  }).run();
93418
93548
  for (const [position, query] of run.input.queries.entries()) tx.insert(researchRunQueries).values({
93419
- id: crypto54.randomUUID(),
93549
+ id: crypto55.randomUUID(),
93420
93550
  researchRunId: id,
93421
93551
  position,
93422
93552
  queryText: query,
@@ -93475,7 +93605,7 @@ async function researchRoutes(app, opts) {
93475
93605
  var DIRECT_REQUEST_HASH_VERSION = "direct-v2:";
93476
93606
  function directResearchRequestHash(input) {
93477
93607
  const { idempotencyKey: _key, ...request } = input;
93478
- return DIRECT_REQUEST_HASH_VERSION + crypto54.createHash("sha256").update(JSON.stringify(request)).digest("hex");
93608
+ return DIRECT_REQUEST_HASH_VERSION + crypto55.createHash("sha256").update(JSON.stringify(request)).digest("hex");
93479
93609
  }
93480
93610
  function assertSameDirectResearchRequest(existing, input, hash2) {
93481
93611
  if (existing.requestHash?.startsWith(DIRECT_REQUEST_HASH_VERSION)) {
@@ -93493,7 +93623,7 @@ function assertSameDirectResearchRequest(existing, input, hash2) {
93493
93623
  ...scope ? { scope } : {},
93494
93624
  ...template ? { template } : {}
93495
93625
  };
93496
- if (crypto54.createHash("sha256").update(JSON.stringify(normalized)).digest("hex") !== existing.requestHash) throw alreadyExists("Research idempotency key", input.idempotencyKey);
93626
+ if (crypto55.createHash("sha256").update(JSON.stringify(normalized)).digest("hex") !== existing.requestHash) throw alreadyExists("Research idempotency key", input.idempotencyKey);
93497
93627
  }
93498
93628
  var researchCursorSchema = z5.object({ projectId: z5.string().min(1), createdAt: z5.string().datetime({ offset: true }), id: z5.string().min(1).max(256) }).strict();
93499
93629
  function parseResearchCursor(value, projectId) {
@@ -93513,11 +93643,11 @@ function resolveStoredResearchScope(stored, selection, idempotencyKey) {
93513
93643
  return stored;
93514
93644
  }
93515
93645
  function batchChildReceiptKey(rootKey, index2) {
93516
- const derived = crypto54.createHash("sha256").update(`${rootKey}\0${index2}`).digest("hex");
93646
+ const derived = crypto55.createHash("sha256").update(`${rootKey}\0${index2}`).digest("hex");
93517
93647
  return `${BATCH_RECEIPT_PREFIX}${derived}:${index2}`;
93518
93648
  }
93519
93649
  function researchBatchRequestHash(input) {
93520
- return crypto54.createHash("sha256").update(JSON.stringify({ runs: input.runs })).digest("hex");
93650
+ return crypto55.createHash("sha256").update(JSON.stringify({ runs: input.runs })).digest("hex");
93521
93651
  }
93522
93652
  function prepareBatchRun(app, opts, project, active, input) {
93523
93653
  const adapters = opts.providerAdapters ?? [];
@@ -93789,7 +93919,7 @@ function readStoredGroundingSources(rawResponse) {
93789
93919
  }
93790
93920
 
93791
93921
  // ../api-routes/src/simple-measurement-definitions.ts
93792
- import crypto55 from "crypto";
93922
+ import crypto56 from "crypto";
93793
93923
  import { and as and81, eq as eq98 } from "drizzle-orm";
93794
93924
  function captureSimpleMeasurementDefinition(db, input) {
93795
93925
  return db.transaction((tx) => {
@@ -93831,11 +93961,11 @@ function captureSimpleMeasurementDefinition(db, input) {
93831
93961
  ...frozen.identity.qualifiedAliases?.length ? { qualifiedAliases: frozen.identity.qualifiedAliases } : {}
93832
93962
  }
93833
93963
  };
93834
- const replayChecksum = crypto55.createHash("sha256").update(canonicalSimpleMeasurementDefinitionJson(replayDefinition)).digest("hex");
93964
+ const replayChecksum = crypto56.createHash("sha256").update(canonicalSimpleMeasurementDefinitionJson(replayDefinition)).digest("hex");
93835
93965
  if (existing.checksum !== replayChecksum) {
93836
93966
  if (frozen.competitors === void 0) {
93837
93967
  const { competitors: _currentCompetitors, ...legacyReplayDefinition } = replayDefinition;
93838
- const legacyReplayChecksum = crypto55.createHash("sha256").update(canonicalSimpleMeasurementDefinitionJson(legacyReplayDefinition)).digest("hex");
93968
+ const legacyReplayChecksum = crypto56.createHash("sha256").update(canonicalSimpleMeasurementDefinitionJson(legacyReplayDefinition)).digest("hex");
93839
93969
  if (existing.checksum === legacyReplayChecksum) return frozen;
93840
93970
  }
93841
93971
  throw validationError("This run already has a captured measurement definition. Start a new run for changed inputs.");
@@ -93859,7 +93989,7 @@ function captureSimpleMeasurementDefinition(db, input) {
93859
93989
  throw validationError("Captured competitors must exactly match the project competitors dispatched for this run.");
93860
93990
  }
93861
93991
  }
93862
- const checksum = crypto55.createHash("sha256").update(canonicalSimpleMeasurementDefinitionJson(definition2)).digest("hex");
93992
+ const checksum = crypto56.createHash("sha256").update(canonicalSimpleMeasurementDefinitionJson(definition2)).digest("hex");
93863
93993
  tx.insert(simpleMeasurementDefinitions).values({
93864
93994
  projectId: input.projectId,
93865
93995
  runId: input.runId,
@@ -94474,7 +94604,7 @@ function buildTrafficSourceValidators(opts) {
94474
94604
  }
94475
94605
 
94476
94606
  // src/intelligence-service.ts
94477
- import crypto56 from "crypto";
94607
+ import crypto57 from "crypto";
94478
94608
 
94479
94609
  // ../api-routes/src/runtime-logger.ts
94480
94610
  import { format } from "util";
@@ -95276,7 +95406,7 @@ var IntelligenceService = class {
95276
95406
  }).run();
95277
95407
  }
95278
95408
  tx.insert(healthSnapshots).values({
95279
- id: crypto56.randomUUID(),
95409
+ id: crypto57.randomUUID(),
95280
95410
  projectId,
95281
95411
  runId,
95282
95412
  overallCitedRate: String(result.health.overallCitedRate),