@canonry/canonry 7.45.2 → 7.46.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +2 -0
  2. package/assets/assets/{AuditHistoryPanel-BJDLPf4D.js → AuditHistoryPanel-u65pF2eT.js} +1 -1
  3. package/assets/assets/{BacklinksPage-BDbYMh4s.js → BacklinksPage-CmYpSNGB.js} +1 -1
  4. package/assets/assets/{HistoryPage-B9c7cjV4.js → HistoryPage-CqzQSAil.js} +1 -1
  5. package/assets/assets/{MeasurementPropertyPage-B2wDvzLz.js → MeasurementPropertyPage-KfpmUisb.js} +1 -1
  6. package/assets/assets/{ProjectPage-Cgj5ZmRt.js → ProjectPage-BCiospUp.js} +1 -1
  7. package/assets/assets/{RunsPage-DNJpOT2C.js → RunsPage-CzPvTnwG.js} +1 -1
  8. package/assets/assets/{SettingsPage-BJABQP0W.js → SettingsPage-CZYckyPO.js} +1 -1
  9. package/assets/assets/SiteHealthSection-Bm5nWAoZ.js +4 -0
  10. package/assets/assets/{TrafficPage-DGks1U43.js → TrafficPage-DZw_GvaO.js} +1 -1
  11. package/assets/assets/{TrafficSourceDetailPage-B9EC-koW.js → TrafficSourceDetailPage-CD20PJ4A.js} +1 -1
  12. package/assets/assets/{extract-error-message-BnQj6MRg.js → extract-error-message-Cpzj_AMZ.js} +1 -1
  13. package/assets/assets/{index-CQ2txiby.js → index-BHLYJHa-.js} +18 -18
  14. package/assets/assets/{index-aBS1f2Bj.css → index-Bll2LqWk.css} +1 -1
  15. package/assets/assets/{react-sigma_core.esm.min-DMBVhOEe.js → react-sigma_core.esm.min-CK2U3T7S.js} +1 -1
  16. package/assets/assets/{v2-overview-adapter-DUYXYO7p.js → v2-overview-adapter-DL1IsAM2.js} +1 -1
  17. package/assets/index.html +2 -2
  18. package/dist/{chunk-MBOOXUPV.js → chunk-7R2HFAG4.js} +20 -13
  19. package/dist/{chunk-55QNMM7V.js → chunk-GWRIZSIL.js} +3 -3
  20. package/dist/{chunk-ZJUP7CHK.js → chunk-IBRZ23VQ.js} +233 -109
  21. package/dist/{chunk-KVUBP25C.js → chunk-OO64BWNB.js} +11 -7
  22. package/dist/{chunk-E7BG7QGP.js → chunk-PCZXWOYX.js} +8 -3
  23. package/dist/{chunk-KZS5N3JH.js → chunk-QBZPKXFW.js} +1 -1
  24. package/dist/{chunk-T2MBKWUP.js → chunk-QWLKYIQ4.js} +2 -2
  25. package/dist/{chunk-6333IHPS.js → chunk-V4TITINC.js} +12 -4
  26. package/dist/{chunk-5A57TIOK.js → chunk-WMG365JI.js} +2 -2
  27. package/dist/{chunk-KMMOYVAO.js → chunk-Y2QMWZT7.js} +3 -3
  28. package/dist/{cli-catalog-E45QWSSR.js → cli-catalog-TFCCKIE4.js} +3 -3
  29. package/dist/cli-commands-JEN27DWN.js +15 -0
  30. package/dist/cli.js +10 -10
  31. package/dist/{demo-server-M45RUD4A.js → demo-server-5TX4MJIS.js} +4 -4
  32. package/dist/index.d.ts +5 -4
  33. package/dist/index.js +6 -6
  34. package/dist/{intelligence-service-FWOKBYOM.js → intelligence-service-5BYJSFLW.js} +2 -2
  35. package/dist/mcp.js +4 -4
  36. package/package.json +12 -12
  37. package/assets/assets/SiteHealthSection-Co-YnYL1.js +0 -4
  38. package/dist/cli-commands-E2TXR3OQ.js +0 -15
@@ -888,6 +888,7 @@ import {
888
888
  unsupportedKind,
889
889
  upgradeCaveatFor,
890
890
  usableBrandAliases,
891
+ usageSurfaceSchema,
891
892
  userDtoSchema,
892
893
  userListDtoSchema,
893
894
  validationError,
@@ -913,7 +914,7 @@ import {
913
914
  wordpressSchemaDeployResultDtoSchema,
914
915
  wordpressSchemaStatusResultDtoSchema,
915
916
  wordpressStatusDtoSchema
916
- } from "./chunk-6333IHPS.js";
917
+ } from "./chunk-V4TITINC.js";
917
918
 
918
919
  // src/intelligence-service.ts
919
920
  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";
@@ -46517,6 +46518,7 @@ var routeCatalog = [
46517
46518
  // TODO: Add `GaConnectResponse` Zod schema in contracts.
46518
46519
  200: rawJsonResponse("GA4 connection established.", looseObjectSchema),
46519
46520
  400: errorResponse("Invalid GA4 connection request."),
46521
+ 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
46522
  404: errorResponse("Project not found.")
46521
46523
  }
46522
46524
  },
@@ -48253,7 +48255,7 @@ var routeCatalog = [
48253
48255
  method: "post",
48254
48256
  path: "/api/v1/projects/{name}/technical-aeo/runs",
48255
48257
  summary: "Trigger a Technical AEO site-audit run",
48256
- description: "Queues a `site-audit` run. The run discovers pages from the root, recursive sitemaps, and internal links. An active run is reused only when its persisted effective sitemap, budgets, depth, and dead-link option match exactly; a semantic mismatch returns 409.",
48258
+ description: "Queues a `site-audit` run. The run discovers pages from the root, recursive sitemaps, and internal links. An active run is reused only when its persisted effective sitemap, budgets, depth, dead-link option, and optional factor selections match exactly; a semantic mismatch returns 409.",
48257
48259
  tags: ["technical-aeo"],
48258
48260
  parameters: [nameParameter],
48259
48261
  requestBody: {
@@ -48268,7 +48270,9 @@ var routeCatalog = [
48268
48270
  maxPages: { ...integerSchema, minimum: 1, maximum: 5e4, description: "Crawl page budget. When omitted, the project's saved budget (siteAuditMaxPages) applies, else the full site; max 50000." },
48269
48271
  maxEdges: { ...integerSchema, minimum: 1, maximum: 1e6, description: "Internal-link observation budget. When omitted the crawl engine derives it from the page count; max 1000000." },
48270
48272
  maxDepth: { ...integerSchema, minimum: 0, maximum: 100, description: "Maximum crawl depth from the root. Max 100." },
48271
- checkDeadLinks: { ...booleanSchema, default: false, description: "Enable dead-link checking. Defaults to false." }
48273
+ checkDeadLinks: { ...booleanSchema, default: false, description: "Enable dead-link checking. Defaults to false." },
48274
+ includeGeo: { ...booleanSchema, default: false, description: "Opt in to geographic signals for location and service-area businesses. Part of scan identity." },
48275
+ includeAgentSkills: { ...booleanSchema, default: true, description: "Check discoverable actions, forms, and agent integrations by default. Set false to disable. Part of scan identity." }
48272
48276
  }
48273
48277
  }
48274
48278
  }
@@ -60098,11 +60102,76 @@ async function adsRoutes(app, opts) {
60098
60102
  }
60099
60103
 
60100
60104
  // ../api-routes/src/bing.ts
60101
- import crypto42 from "crypto";
60105
+ import crypto43 from "crypto";
60102
60106
  import { eq as eq66, and as and54, desc as desc32 } from "drizzle-orm";
60103
60107
 
60104
- // ../integration-bing/src/bing-client.ts
60108
+ // ../api-routes/src/connection-verify-cache.ts
60105
60109
  import crypto41 from "crypto";
60110
+ var CONNECTION_VERIFY_TTL_MS = 15 * 60 * 1e3;
60111
+ var REFUSALS_BEFORE_BACKOFF = 3;
60112
+ var FIRST_BACKOFF_MS = 5 * 60 * 1e3;
60113
+ var MAX_BACKOFF_MS = 60 * 60 * 1e3;
60114
+ var MAX_ENTRIES = 500;
60115
+ function remember(entries, key, value) {
60116
+ entries.delete(key);
60117
+ entries.set(key, value);
60118
+ if (entries.size > MAX_ENTRIES) entries.delete(entries.keys().next().value);
60119
+ }
60120
+ function isInteractiveCaller() {
60121
+ const attribution = currentOutcomeAttribution();
60122
+ const label = usageSurfaceSchema.safeParse(attribution?.surfaceLabel);
60123
+ if (label.success) return label.data === "cli" || label.data === "dashboard";
60124
+ const ua = attribution?.userAgent ?? "";
60125
+ return ua.startsWith("canonry-cli") || ua.startsWith("Mozilla/");
60126
+ }
60127
+ function verifyCacheKey(...parts) {
60128
+ return crypto41.createHash("sha256").update(parts.join("\0")).digest("hex");
60129
+ }
60130
+ function createVerifyCache(ttlMs = CONNECTION_VERIFY_TTL_MS) {
60131
+ const entries = /* @__PURE__ */ new Map();
60132
+ return {
60133
+ get(key) {
60134
+ const entry = entries.get(key);
60135
+ if (!entry) return void 0;
60136
+ if (Date.now() - entry.at < ttlMs) return entry.value;
60137
+ entries.delete(key);
60138
+ return void 0;
60139
+ },
60140
+ set(key, value) {
60141
+ remember(entries, key, { value, at: Date.now() });
60142
+ }
60143
+ };
60144
+ }
60145
+ function createRefusalBackoff() {
60146
+ const entries = /* @__PURE__ */ new Map();
60147
+ return {
60148
+ /** The refusal to answer with instead of asking the upstream, while this key is backing off. */
60149
+ replay(key) {
60150
+ const entry = entries.get(key);
60151
+ if (!entry || entry.count < REFUSALS_BEFORE_BACKOFF) return void 0;
60152
+ const wait = Math.min(FIRST_BACKOFF_MS * 2 ** (entry.count - REFUSALS_BEFORE_BACKOFF), MAX_BACKOFF_MS);
60153
+ const nextCheckAt = entry.at + wait;
60154
+ if (Date.now() >= nextCheckAt) return void 0;
60155
+ const { error } = entry;
60156
+ return {
60157
+ error: new AppError(error.code, error.message, error.statusCode, {
60158
+ ...error.details,
60159
+ nextCheckAt: new Date(nextCheckAt).toISOString()
60160
+ }),
60161
+ cause: entry.cause
60162
+ };
60163
+ },
60164
+ refused(key, error, cause) {
60165
+ remember(entries, key, { count: (entries.get(key)?.count ?? 0) + 1, at: Date.now(), error, cause });
60166
+ },
60167
+ cleared(key) {
60168
+ entries.delete(key);
60169
+ }
60170
+ };
60171
+ }
60172
+
60173
+ // ../integration-bing/src/bing-client.ts
60174
+ import crypto42 from "crypto";
60106
60175
 
60107
60176
  // ../integration-bing/src/constants.ts
60108
60177
  var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
@@ -60257,7 +60326,7 @@ async function bingFetchOnce(apiKey, endpoint, opts) {
60257
60326
  var throttleCooldownUntil = /* @__PURE__ */ new Map();
60258
60327
  var cooldownNow = () => Date.now();
60259
60328
  function cooldownKey(apiKey) {
60260
- return crypto41.createHash("sha256").update(apiKey).digest("hex").slice(0, 12);
60329
+ return crypto42.createHash("sha256").update(apiKey).digest("hex").slice(0, 12);
60261
60330
  }
60262
60331
  function throttleCooldownRemainingMs(apiKey) {
60263
60332
  const until = throttleCooldownUntil.get(cooldownKey(apiKey));
@@ -60401,6 +60470,7 @@ async function loadBlockingCrawlIssues(apiKey, siteUrl, domain) {
60401
60470
  return blockedUrls;
60402
60471
  }
60403
60472
  async function bingRoutes(app, opts) {
60473
+ const keyChecks = createVerifyCache();
60404
60474
  function requireConnectionStore() {
60405
60475
  if (opts.bingConnectionStore) return opts.bingConnectionStore;
60406
60476
  throw validationError("Bing connection storage is not configured for this deployment");
@@ -60419,16 +60489,21 @@ async function bingRoutes(app, opts) {
60419
60489
  throw validationError("apiKey is required");
60420
60490
  }
60421
60491
  const project = resolveProject(app.db, request.params.name);
60422
- let sites;
60423
- try {
60424
- sites = await getSites(apiKey);
60425
- bingLog("info", "connect.verify-key", { domain: project.canonicalDomain, siteCount: sites.length });
60426
- } catch (e) {
60427
- const msg = describeError(e);
60428
- bingLog("error", "connect.verify-key-failed", { domain: project.canonicalDomain, error: msg });
60429
- attempt.failed(e);
60430
- throw validationError(`Failed to verify Bing API key: ${msg}`);
60492
+ const keyCheck = verifyCacheKey("bing-key", apiKey);
60493
+ let sites = isInteractiveCaller() ? void 0 : keyChecks.get(keyCheck);
60494
+ if (!sites) {
60495
+ try {
60496
+ sites = await getSites(apiKey);
60497
+ keyChecks.set(keyCheck, sites);
60498
+ bingLog("info", "connect.verify-key", { domain: project.canonicalDomain, siteCount: sites.length });
60499
+ } catch (e) {
60500
+ const msg = describeError(e);
60501
+ bingLog("error", "connect.verify-key-failed", { domain: project.canonicalDomain, error: msg });
60502
+ attempt.failed(e);
60503
+ throw validationError(`Failed to verify Bing API key: ${msg}`);
60504
+ }
60431
60505
  }
60506
+ const availableSites = sites.map((s) => ({ url: s.Url, verified: s.IsVerified ?? false }));
60432
60507
  const now = (/* @__PURE__ */ new Date()).toISOString();
60433
60508
  const existing = store.getConnection(project.canonicalDomain);
60434
60509
  if (existing) attempt.update({ action: "reauth" });
@@ -60438,6 +60513,9 @@ async function bingRoutes(app, opts) {
60438
60513
  `This domain already has a Bing connection owned by another project. Disconnect it from that project first before re-connecting from "${project.name}".`
60439
60514
  );
60440
60515
  }
60516
+ if (existing?.apiKey === apiKey && existing.createdByProjectId) {
60517
+ return { connected: true, domain: project.canonicalDomain, siteUrl: existing.siteUrl ?? null, availableSites };
60518
+ }
60441
60519
  store.upsertConnection({
60442
60520
  domain: project.canonicalDomain,
60443
60521
  apiKey,
@@ -60457,7 +60535,7 @@ async function bingRoutes(app, opts) {
60457
60535
  connected: true,
60458
60536
  domain: project.canonicalDomain,
60459
60537
  siteUrl: existing?.siteUrl ?? null,
60460
- availableSites: sites.map((s) => ({ url: s.Url, verified: s.IsVerified ?? false }))
60538
+ availableSites
60461
60539
  };
60462
60540
  }));
60463
60541
  app.delete("/projects/:name/bing/disconnect", connectionRoute(app, () => ({ integration: "bing", action: "disconnect" }), async (request, reply, attempt) => {
@@ -60587,7 +60665,7 @@ async function bingRoutes(app, opts) {
60587
60665
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
60588
60666
  const now = (/* @__PURE__ */ new Date()).toISOString();
60589
60667
  app.db.insert(bingCoverageSnapshots).values({
60590
- id: crypto42.randomUUID(),
60668
+ id: crypto43.randomUUID(),
60591
60669
  projectId: project.id,
60592
60670
  syncRunId: snapshotRunId,
60593
60671
  date: snapshotDate,
@@ -60662,7 +60740,7 @@ async function bingRoutes(app, opts) {
60662
60740
  throw validationError("url is required");
60663
60741
  }
60664
60742
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
60665
- const runId = crypto42.randomUUID();
60743
+ const runId = crypto43.randomUUID();
60666
60744
  app.db.insert(runs).values({
60667
60745
  id: runId,
60668
60746
  projectId: project.id,
@@ -60683,7 +60761,7 @@ async function bingRoutes(app, opts) {
60683
60761
  discoveryDate: result.DiscoveryDate ?? null
60684
60762
  });
60685
60763
  const now = (/* @__PURE__ */ new Date()).toISOString();
60686
- const id = crypto42.randomUUID();
60764
+ const id = crypto43.randomUUID();
60687
60765
  const httpCode = result.HttpStatus ?? result.HttpCode ?? null;
60688
60766
  const lastCrawledDate = parseBingDate(result.LastCrawledDate);
60689
60767
  const inIndexDate = parseBingDate(result.InIndexDate);
@@ -60760,7 +60838,7 @@ async function bingRoutes(app, opts) {
60760
60838
  throw validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
60761
60839
  }
60762
60840
  const now = (/* @__PURE__ */ new Date()).toISOString();
60763
- const runId = crypto42.randomUUID();
60841
+ const runId = crypto43.randomUUID();
60764
60842
  app.db.insert(runs).values({
60765
60843
  id: runId,
60766
60844
  projectId: project.id,
@@ -61058,7 +61136,7 @@ async function cdpRoutes(app, opts) {
61058
61136
  }
61059
61137
 
61060
61138
  // ../api-routes/src/ga.ts
61061
- import crypto44 from "crypto";
61139
+ import crypto45 from "crypto";
61062
61140
  import { eq as eq69, desc as desc34, and as and57, sql as sql27 } from "drizzle-orm";
61063
61141
 
61064
61142
  // ../api-routes/src/ga-session-history.ts
@@ -61118,14 +61196,14 @@ function findBiggestMover(current, prior) {
61118
61196
  }
61119
61197
 
61120
61198
  // ../api-routes/src/ga-search-landing-pages.ts
61121
- import crypto43 from "crypto";
61199
+ import crypto44 from "crypto";
61122
61200
  import { and as and56, asc as asc13, count as count4, desc as desc33, eq as eq68 } from "drizzle-orm";
61123
61201
  function persistSearchLandingSnapshot(db, input) {
61124
61202
  db.transaction((tx) => {
61125
61203
  deleteSearchLandingRows(tx, input.projectId);
61126
61204
  for (const window of input.windows) {
61127
61205
  tx.insert(gaSearchLandingWindows).values({
61128
- id: crypto43.randomUUID(),
61206
+ id: crypto44.randomUUID(),
61129
61207
  projectId: input.projectId,
61130
61208
  propertyId: input.propertyId,
61131
61209
  windowKey: window.window,
@@ -61147,7 +61225,7 @@ function persistSearchLandingSnapshot(db, input) {
61147
61225
  }).run();
61148
61226
  for (const row of window.rows) {
61149
61227
  tx.insert(gaSearchLandingPages).values({
61150
- id: crypto43.randomUUID(),
61228
+ id: crypto44.randomUUID(),
61151
61229
  projectId: input.projectId,
61152
61230
  windowKey: window.window,
61153
61231
  landingPage: row.landingPage,
@@ -61379,7 +61457,7 @@ function persistAcquisitionMeasurement(db, input) {
61379
61457
  )).run();
61380
61458
  for (const row of input.report.rows) {
61381
61459
  tx.insert(gaAcquisitionDaily).values({
61382
- id: crypto44.randomUUID(),
61460
+ id: crypto45.randomUUID(),
61383
61461
  projectId: input.projectId,
61384
61462
  date: row.date,
61385
61463
  channelGroup: row.channelGroup,
@@ -61421,7 +61499,7 @@ function persistLeadMeasurement(db, input) {
61421
61499
  )).run();
61422
61500
  for (const row of input.report.rows) {
61423
61501
  tx.insert(gaLeadEventsDaily).values({
61424
- id: crypto44.randomUUID(),
61502
+ id: crypto45.randomUUID(),
61425
61503
  projectId: input.projectId,
61426
61504
  date: row.date,
61427
61505
  eventName: row.eventName,
@@ -61531,6 +61609,18 @@ function ga4ErrorToAppError(err, context) {
61531
61609
  }
61532
61610
  return providerError(`${context}: ${describeError(err)}`);
61533
61611
  }
61612
+ function ga4ConnectError(err, propertyId, principal, context) {
61613
+ if (err instanceof GA4ApiError && err.status === 403) {
61614
+ return forbidden(
61615
+ `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.`,
61616
+ { reason: "ga4-permission-missing", propertyId, retryable: false }
61617
+ );
61618
+ }
61619
+ return validationError(`${context}: ${describeError(err)}`);
61620
+ }
61621
+ function isPermissionRefusal(error) {
61622
+ return error.details?.reason === "ga4-permission-missing";
61623
+ }
61534
61624
  async function resolveGa4AccessToken(opts, projectName, canonicalDomain) {
61535
61625
  const saConn = opts.ga4CredentialStore?.getConnection(projectName);
61536
61626
  if (saConn?.clientEmail && saConn?.privateKey && saConn?.propertyId) {
@@ -61581,6 +61671,25 @@ function requireGa4Connection(opts, projectName, canonicalDomain) {
61581
61671
  }
61582
61672
  }
61583
61673
  async function ga4Routes(app, opts) {
61674
+ const connectPasses = createVerifyCache();
61675
+ const connectRefusals = createRefusalBackoff();
61676
+ async function checkGa4Access(key, verify, toError) {
61677
+ if (!isInteractiveCaller()) {
61678
+ if (connectPasses.get(key)) return { ok: true };
61679
+ const replay = connectRefusals.replay(key);
61680
+ if (replay) return { ok: false, ...replay };
61681
+ }
61682
+ try {
61683
+ await verify();
61684
+ connectPasses.set(key, true);
61685
+ connectRefusals.cleared(key);
61686
+ return { ok: true };
61687
+ } catch (cause) {
61688
+ const error = toError(cause);
61689
+ if (isPermissionRefusal(error)) connectRefusals.refused(key, error, cause);
61690
+ return { ok: false, error, cause };
61691
+ }
61692
+ }
61584
61693
  app.post("/projects/:name/ga/connect", connectionRoute(app, (request) => {
61585
61694
  const body = request.body;
61586
61695
  return { integration: "ga4", action: typeof body?.keyJson === "string" && body.keyJson ? "connect" : "select" };
@@ -61605,18 +61714,23 @@ async function ga4Routes(app, opts) {
61605
61714
  }
61606
61715
  const clientEmail = parsed.client_email;
61607
61716
  const privateKey = parsed.private_key;
61608
- try {
61609
- await verifyConnection(clientEmail, privateKey, propertyId);
61610
- gaLog("info", "connect.verified.service-account", { projectId: project.id, propertyId });
61611
- } catch (e) {
61612
- const msg = describeError(e);
61613
- gaLog("error", "connect.verify-failed", { projectId: project.id, propertyId, error: msg });
61614
- attempt.failed(e);
61615
- throw validationError(`Failed to verify GA4 credentials: ${msg}`);
61717
+ const check3 = await checkGa4Access(
61718
+ verifyCacheKey("ga4-service-account", project.id, propertyId, clientEmail, privateKey),
61719
+ () => verifyConnection(clientEmail, privateKey, propertyId),
61720
+ (err) => ga4ConnectError(err, propertyId, clientEmail, "Failed to verify GA4 credentials")
61721
+ );
61722
+ if (!check3.ok) {
61723
+ gaLog("error", "connect.verify-failed", { projectId: project.id, propertyId, error: check3.error.message });
61724
+ attempt.failed(check3.cause);
61725
+ throw check3.error;
61616
61726
  }
61727
+ gaLog("info", "connect.verified.service-account", { projectId: project.id, propertyId });
61617
61728
  const now = (/* @__PURE__ */ new Date()).toISOString();
61618
61729
  const existing = opts.ga4CredentialStore.getConnection(project.name);
61619
61730
  if (existing) attempt.update({ action: "reauth" });
61731
+ if (existing?.clientEmail === clientEmail && existing.privateKey === privateKey && existing.propertyId === propertyId) {
61732
+ return { connected: true, propertyId, authMethod: "service-account", clientEmail };
61733
+ }
61620
61734
  const previousPropertyId2 = resolveCurrentGa4PropertyId(opts, project.name, project.canonicalDomain);
61621
61735
  opts.ga4CredentialStore.upsertConnection({
61622
61736
  projectName: project.name,
@@ -61653,19 +61767,27 @@ async function ga4Routes(app, opts) {
61653
61767
  'No GA4 OAuth token found. Run "canonry google connect <project> --type ga4" first, or pass --key-file to use a service account.'
61654
61768
  );
61655
61769
  }
61656
- const accessToken = await refreshOAuthTokenIfNeeded(googleStore, authConfig, project.canonicalDomain, {
61657
- accessToken: oauthConn.accessToken,
61658
- refreshToken: oauthConn.refreshToken,
61659
- tokenExpiresAt: oauthConn.tokenExpiresAt
61660
- });
61661
- try {
61662
- await verifyConnectionWithToken(accessToken, propertyId);
61663
- gaLog("info", "connect.verified.oauth", { projectId: project.id, propertyId });
61664
- } catch (e) {
61665
- const msg = describeError(e);
61666
- gaLog("error", "connect.verify-failed.oauth", { projectId: project.id, propertyId, error: msg });
61667
- attempt.failed(e);
61668
- throw validationError(`Failed to verify GA4 access: ${msg}`);
61770
+ const { accessToken: storedAccessToken, refreshToken } = oauthConn;
61771
+ const check2 = await checkGa4Access(
61772
+ verifyCacheKey("ga4-oauth", project.id, propertyId, refreshToken),
61773
+ async () => {
61774
+ const accessToken = await refreshOAuthTokenIfNeeded(googleStore, authConfig, project.canonicalDomain, {
61775
+ accessToken: storedAccessToken,
61776
+ refreshToken,
61777
+ tokenExpiresAt: oauthConn.tokenExpiresAt
61778
+ });
61779
+ return verifyConnectionWithToken(accessToken, propertyId);
61780
+ },
61781
+ (err) => err instanceof AppError ? err : ga4ConnectError(err, propertyId, "the connected Google account", "Failed to verify GA4 access")
61782
+ );
61783
+ if (!check2.ok) {
61784
+ gaLog("error", "connect.verify-failed.oauth", { projectId: project.id, propertyId, error: check2.error.message });
61785
+ attempt.failed(check2.cause);
61786
+ throw check2.error;
61787
+ }
61788
+ gaLog("info", "connect.verified.oauth", { projectId: project.id, propertyId });
61789
+ if (oauthConn.propertyId === propertyId) {
61790
+ return { connected: true, propertyId, authMethod: "oauth" };
61669
61791
  }
61670
61792
  const previousPropertyId = resolveCurrentGa4PropertyId(opts, project.name, project.canonicalDomain);
61671
61793
  const connectedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -61775,7 +61897,7 @@ async function ga4Routes(app, opts) {
61775
61897
  const leadDays = measurementState?.leadSyncedAt != null ? days : GA4_MAX_SYNC_DAYS;
61776
61898
  const leadEventNames = project.measurement.leadEventNames;
61777
61899
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
61778
- const runId = crypto44.randomUUID();
61900
+ const runId = crypto45.randomUUID();
61779
61901
  app.db.insert(runs).values({
61780
61902
  id: runId,
61781
61903
  projectId: project.id,
@@ -61835,7 +61957,7 @@ async function ga4Routes(app, opts) {
61835
61957
  ).run();
61836
61958
  for (const row of rows) {
61837
61959
  tx.insert(gaTrafficSnapshots).values({
61838
- id: crypto44.randomUUID(),
61960
+ id: crypto45.randomUUID(),
61839
61961
  projectId: project.id,
61840
61962
  date: row.date,
61841
61963
  landingPage: row.landingPage,
@@ -61858,7 +61980,7 @@ async function ga4Routes(app, opts) {
61858
61980
  ).run();
61859
61981
  for (const row of dailyTotals) {
61860
61982
  tx.insert(gaDailyTotals).values({
61861
- id: crypto44.randomUUID(),
61983
+ id: crypto45.randomUUID(),
61862
61984
  projectId: project.id,
61863
61985
  date: row.date,
61864
61986
  sessions: row.sessions,
@@ -61884,7 +62006,7 @@ async function ga4Routes(app, opts) {
61884
62006
  ).run();
61885
62007
  for (const row of aiReferrals) {
61886
62008
  tx.insert(gaAiReferrals).values({
61887
- id: crypto44.randomUUID(),
62009
+ id: crypto45.randomUUID(),
61888
62010
  projectId: project.id,
61889
62011
  date: row.date,
61890
62012
  source: row.source,
@@ -61916,7 +62038,7 @@ async function ga4Routes(app, opts) {
61916
62038
  ).run();
61917
62039
  for (const row of socialReferrals) {
61918
62040
  tx.insert(gaSocialReferrals).values({
61919
- id: crypto44.randomUUID(),
62041
+ id: crypto45.randomUUID(),
61920
62042
  projectId: project.id,
61921
62043
  date: row.date,
61922
62044
  source: row.source,
@@ -61932,7 +62054,7 @@ async function ga4Routes(app, opts) {
61932
62054
  if (syncSummary) {
61933
62055
  tx.delete(gaTrafficSummaries).where(eq69(gaTrafficSummaries.projectId, project.id)).run();
61934
62056
  tx.insert(gaTrafficSummaries).values({
61935
- id: crypto44.randomUUID(),
62057
+ id: crypto45.randomUUID(),
61936
62058
  projectId: project.id,
61937
62059
  periodStart: summary3.periodStart,
61938
62060
  periodEnd: summary3.periodEnd,
@@ -61945,7 +62067,7 @@ async function ga4Routes(app, opts) {
61945
62067
  tx.delete(gaTrafficWindowSummaries).where(eq69(gaTrafficWindowSummaries.projectId, project.id)).run();
61946
62068
  for (const ws of windowSummaries) {
61947
62069
  tx.insert(gaTrafficWindowSummaries).values({
61948
- id: crypto44.randomUUID(),
62070
+ id: crypto45.randomUUID(),
61949
62071
  projectId: project.id,
61950
62072
  windowKey: ws.windowKey,
61951
62073
  periodStart: ws.periodStart,
@@ -62742,7 +62864,7 @@ function parseSchemaPageEntry(entry) {
62742
62864
  }
62743
62865
 
62744
62866
  // ../integration-wordpress/src/wordpress-client.ts
62745
- import crypto45 from "crypto";
62867
+ import crypto46 from "crypto";
62746
62868
  function validateUsername(username) {
62747
62869
  if (!username || typeof username !== "string" || username.trim().length === 0) {
62748
62870
  throw new WordpressApiError("AUTH_INVALID", "Username is required and must be a non-empty string", 400);
@@ -62962,7 +63084,7 @@ function buildSnippet(content) {
62962
63084
  return `${text2.slice(0, 157)}...`;
62963
63085
  }
62964
63086
  function contentHash(content) {
62965
- return crypto45.createHash("sha256").update(content).digest("hex");
63087
+ return crypto46.createHash("sha256").update(content).digest("hex");
62966
63088
  }
62967
63089
  function buildAmbiguousSlugMessage(slug, pages) {
62968
63090
  const candidates = pages.map((page3) => {
@@ -64455,7 +64577,7 @@ async function wordpressRoutes(app, opts) {
64455
64577
  }
64456
64578
 
64457
64579
  // ../api-routes/src/backlinks.ts
64458
- import crypto46 from "crypto";
64580
+ import crypto47 from "crypto";
64459
64581
  import { and as and61, asc as asc15, desc as desc36, eq as eq72, sql as sql29 } from "drizzle-orm";
64460
64582
 
64461
64583
  // ../integration-commoncrawl/src/constants.ts
@@ -65108,7 +65230,7 @@ async function backlinksRoutes(app, opts) {
65108
65230
  const refreshed = app.db.select().from(ccReleaseSyncs).where(eq72(ccReleaseSyncs.id, existing.id)).get();
65109
65231
  return reply.status(200).send(mapSyncRow(refreshed));
65110
65232
  }
65111
- const id = crypto46.randomUUID();
65233
+ const id = crypto47.randomUUID();
65112
65234
  app.db.insert(ccReleaseSyncs).values({
65113
65235
  id,
65114
65236
  release,
@@ -65165,7 +65287,7 @@ async function backlinksRoutes(app, opts) {
65165
65287
  throw validationError("Invalid release id");
65166
65288
  }
65167
65289
  const now = (/* @__PURE__ */ new Date()).toISOString();
65168
- const runId = crypto46.randomUUID();
65290
+ const runId = crypto47.randomUUID();
65169
65291
  app.db.insert(runs).values({
65170
65292
  id: runId,
65171
65293
  projectId: project.id,
@@ -65254,7 +65376,7 @@ async function backlinksRoutes(app, opts) {
65254
65376
  }
65255
65377
 
65256
65378
  // ../api-routes/src/traffic.ts
65257
- import crypto49 from "crypto";
65379
+ import crypto50 from "crypto";
65258
65380
  import { isIP } from "net";
65259
65381
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
65260
65382
  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";
@@ -65282,7 +65404,7 @@ function resolveVercelSyncDeadlineMs(env = process.env) {
65282
65404
  }
65283
65405
 
65284
65406
  // ../integration-cloud-run/src/auth.ts
65285
- import crypto47 from "crypto";
65407
+ import crypto48 from "crypto";
65286
65408
  var GOOGLE_TOKEN_URL3 = "https://oauth2.googleapis.com/token";
65287
65409
  var CLOUD_LOGGING_READ_SCOPE = "https://www.googleapis.com/auth/logging.read";
65288
65410
  var TOKEN_REQUEST_TIMEOUT_MS = 3e4;
@@ -65313,7 +65435,7 @@ function createServiceAccountJwt2(clientEmail, privateKey, scope) {
65313
65435
  const headerB64 = encode(header2);
65314
65436
  const payloadB64 = encode(payload);
65315
65437
  const signingInput = `${headerB64}.${payloadB64}`;
65316
- const sign = crypto47.createSign("RSA-SHA256");
65438
+ const sign = crypto48.createSign("RSA-SHA256");
65317
65439
  sign.update(signingInput);
65318
65440
  const signature = sign.sign(privateKey, "base64url");
65319
65441
  return `${signingInput}.${signature}`;
@@ -81922,7 +82044,7 @@ async function ackCloudflareQueueMessages(client, options) {
81922
82044
  }
81923
82045
 
81924
82046
  // ../api-routes/src/traffic-event-ingest.ts
81925
- import crypto48 from "crypto";
82047
+ import crypto49 from "crypto";
81926
82048
  import { and as and63, eq as eq74, gte as gte17, lt as lt12, lte as lte18, sql as sql31 } from "drizzle-orm";
81927
82049
 
81928
82050
  // ../api-routes/src/raw-event-sample-retention.ts
@@ -82221,7 +82343,7 @@ function writeTrafficEventBatch(opts) {
82221
82343
  }
82222
82344
  })();
82223
82345
  tx.insert(rawEventSamples).values({
82224
- id: crypto48.randomUUID(),
82346
+ id: crypto49.randomUUID(),
82225
82347
  projectId: opts.projectId,
82226
82348
  sourceId: opts.sourceId,
82227
82349
  ts: sampleTimestamp,
@@ -82322,7 +82444,7 @@ var WORDPRESS_SYNC_LEASE_TTL_MS = 5 * 6e4;
82322
82444
  var DEFAULT_CLOUDFLARE_INGEST_RATE_LIMIT_MAX = 6e3;
82323
82445
  var SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/i;
82324
82446
  function sha256Hex3(value) {
82325
- return crypto49.createHash("sha256").update(value).digest("hex");
82447
+ return crypto50.createHash("sha256").update(value).digest("hex");
82326
82448
  }
82327
82449
  function hashCloudflareBearerToken(value) {
82328
82450
  return sha256Hex3(value);
@@ -82332,7 +82454,7 @@ function timingSafeEqualHex(a, b) {
82332
82454
  if (!SHA256_HEX_PATTERN.test(a) || !SHA256_HEX_PATTERN.test(b)) return false;
82333
82455
  const left = Buffer.from(a, "hex");
82334
82456
  const right = Buffer.from(b, "hex");
82335
- return left.length === right.length && crypto49.timingSafeEqual(left, right);
82457
+ return left.length === right.length && crypto50.timingSafeEqual(left, right);
82336
82458
  }
82337
82459
  function parseDirectPushCloudflareSourceConfig(config) {
82338
82460
  const parsed = cloudflareTrafficSourceConfigSchema.safeParse(config);
@@ -82650,7 +82772,7 @@ function bindTrafficSyncSchedule(tx, projectId, sourceId, now, createIfMissing =
82650
82772
  }
82651
82773
  if (!createIfMissing) return { changed: false, created: false };
82652
82774
  tx.insert(schedules).values({
82653
- id: crypto49.randomUUID(),
82775
+ id: crypto50.randomUUID(),
82654
82776
  projectId,
82655
82777
  kind: SchedulableRunKinds["traffic-sync"],
82656
82778
  cronExpr: DEFAULT_TRAFFIC_SYNC_CRON,
@@ -82971,7 +83093,7 @@ async function runBackfillTask(options) {
82971
83093
  }
82972
83094
  })();
82973
83095
  tx.insert(rawEventSamples).values({
82974
- id: crypto49.randomUUID(),
83096
+ id: crypto50.randomUUID(),
82975
83097
  projectId: project.id,
82976
83098
  sourceId: sourceRow2.id,
82977
83099
  ts: sampleTimestamp,
@@ -83162,7 +83284,7 @@ async function trafficRoutes(app, opts) {
83162
83284
  const fallbackName = displayName ?? `Cloud Run \xB7 ${gcpProjectId}${serviceName ? ` / ${serviceName}` : ""}`;
83163
83285
  const { sourceRow: sourceRow2, scheduleChanged } = app.db.transaction((tx) => {
83164
83286
  const activeSource = tx.select().from(trafficSources).where(eq76(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes["cloud-run"] && row.status !== TrafficSourceStatuses.archived);
83165
- const sourceId = activeSource?.id ?? crypto49.randomUUID();
83287
+ const sourceId = activeSource?.id ?? crypto50.randomUUID();
83166
83288
  const sourceStatus = trafficConnectStatus(tx, project.id, activeSource);
83167
83289
  if (activeSource) {
83168
83290
  tx.update(trafficSources).set({
@@ -83254,7 +83376,7 @@ async function trafficRoutes(app, opts) {
83254
83376
  const { sourceRow: sourceRow2, scheduleChanged } = app.db.transaction((tx) => {
83255
83377
  const activeSource = tx.select().from(trafficSources).where(eq76(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.wordpress && row.status !== TrafficSourceStatuses.archived);
83256
83378
  const endpointChanged = activeSource !== void 0 && wordpressBaseUrlIdentity(activeSource.configJson.baseUrl) !== wordpressBaseUrlIdentity(baseUrl);
83257
- const sourceId = endpointChanged ? crypto49.randomUUID() : activeSource?.id ?? crypto49.randomUUID();
83379
+ const sourceId = endpointChanged ? crypto50.randomUUID() : activeSource?.id ?? crypto50.randomUUID();
83258
83380
  const sourceStatus = trafficConnectStatus(tx, project.id, activeSource);
83259
83381
  if (activeSource && !endpointChanged) {
83260
83382
  tx.update(trafficSources).set({
@@ -83366,7 +83488,7 @@ async function trafficRoutes(app, opts) {
83366
83488
  }).where(eq76(trafficSources.id, activeSource.id)).run();
83367
83489
  row = tx.select().from(trafficSources).where(eq76(trafficSources.id, activeSource.id)).get();
83368
83490
  } else {
83369
- const newId = crypto49.randomUUID();
83491
+ const newId = crypto50.randomUUID();
83370
83492
  tx.insert(trafficSources).values({
83371
83493
  id: newId,
83372
83494
  projectId: project.id,
@@ -83436,7 +83558,7 @@ async function trafficRoutes(app, opts) {
83436
83558
  const cloudflareSources = app.db.select().from(trafficSources).where(eq76(trafficSources.projectId, project.id)).all().filter((row) => row.sourceType === TrafficSourceTypes.cloudflare && row.status !== TrafficSourceStatuses.archived);
83437
83559
  const sameModeSource = cloudflareSources.find((row) => parseQueuePullCloudflareSourceConfig(row.configJson));
83438
83560
  if (sameModeSource) attempt.update({ action: "reauth" });
83439
- const sourceId2 = sameModeSource?.id ?? crypto49.randomUUID();
83561
+ const sourceId2 = sameModeSource?.id ?? crypto50.randomUUID();
83440
83562
  const previousCredential2 = credentialStore.getConnectionBySourceId(sourceId2);
83441
83563
  const workerVersion2 = CURRENT_CLOUDFLARE_WORKER_VERSION;
83442
83564
  const workerScript2 = generateWorkerScript({
@@ -83572,7 +83694,7 @@ async function trafficRoutes(app, opts) {
83572
83694
  const now = (/* @__PURE__ */ new Date()).toISOString();
83573
83695
  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);
83574
83696
  if (activeSource) attempt.update({ action: "reauth" });
83575
- const sourceId = activeSource?.id ?? crypto49.randomUUID();
83697
+ const sourceId = activeSource?.id ?? crypto50.randomUUID();
83576
83698
  const parsedActiveConfig = activeSource ? parseDirectPushCloudflareSourceConfig(activeSource.configJson) : null;
83577
83699
  if (activeSource && !parsedActiveConfig) {
83578
83700
  throw validationError("Existing Cloudflare source is not configured for direct-push delivery");
@@ -83588,8 +83710,8 @@ async function trafficRoutes(app, opts) {
83588
83710
  const previousAccountId = reusableCredential?.accountId ?? parsedActiveConfig?.accountId ?? null;
83589
83711
  const effectiveZoneId = zoneId ?? previousZoneId;
83590
83712
  const effectiveAccountId = accountId ?? previousAccountId;
83591
- const bearerToken = reusableCredential?.bearerToken ?? `cnry_cfw_${crypto49.randomBytes(32).toString("hex")}`;
83592
- const hmacSecret = reusableCredential?.hmacSecret ?? crypto49.randomBytes(32).toString("hex");
83713
+ const bearerToken = reusableCredential?.bearerToken ?? `cnry_cfw_${crypto50.randomBytes(32).toString("hex")}`;
83714
+ const hmacSecret = reusableCredential?.hmacSecret ?? crypto50.randomBytes(32).toString("hex");
83593
83715
  const ingestTokenHash = sha256Hex3(bearerToken);
83594
83716
  const ingestUrl = opts.cloudflareTrafficIngestUrl.replace("{name}", encodeURIComponent(project.name));
83595
83717
  assertCloudflareIngestUrlIsPublicHttps(ingestUrl);
@@ -83935,7 +84057,7 @@ async function trafficRoutes(app, opts) {
83935
84057
  apiToken: credential.apiToken
83936
84058
  };
83937
84059
  const now = (/* @__PURE__ */ new Date()).toISOString();
83938
- const leaseOwner = crypto49.randomUUID();
84060
+ const leaseOwner = crypto50.randomUUID();
83939
84061
  if (!tryClaimTrafficSyncLease({
83940
84062
  db: app.db,
83941
84063
  sourceId: sourceRow2.id,
@@ -83945,7 +84067,7 @@ async function trafficRoutes(app, opts) {
83945
84067
  })) {
83946
84068
  throw operationInProgress("Cloudflare Queue source sync is already in progress", { sourceId: sourceRow2.id });
83947
84069
  }
83948
- const runId = crypto49.randomUUID();
84070
+ const runId = crypto50.randomUUID();
83949
84071
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
83950
84072
  const startedMs = Date.now();
83951
84073
  try {
@@ -84185,7 +84307,7 @@ async function trafficRoutes(app, opts) {
84185
84307
  if (sourceRow2.status === TrafficSourceStatuses.paused || sourceRow2.status === TrafficSourceStatuses.archived || hasConnectedSibling) {
84186
84308
  throw validationError("Traffic source must be active before it can sync");
84187
84309
  }
84188
- const wordpressLeaseOwner = sourceRow2.sourceType === TrafficSourceTypes.wordpress ? crypto49.randomUUID() : void 0;
84310
+ const wordpressLeaseOwner = sourceRow2.sourceType === TrafficSourceTypes.wordpress ? crypto50.randomUUID() : void 0;
84189
84311
  if (wordpressLeaseOwner && !tryClaimTrafficSyncLease({
84190
84312
  db: app.db,
84191
84313
  sourceId: sourceRow2.id,
@@ -84200,7 +84322,7 @@ async function trafficRoutes(app, opts) {
84200
84322
  let windowEnd = syncWindowEnd;
84201
84323
  const startedAt = syncWindowEnd.toISOString();
84202
84324
  const syncStartedAtMs = syncWindowEnd.getTime();
84203
- const runId = crypto49.randomUUID();
84325
+ const runId = crypto50.randomUUID();
84204
84326
  app.db.insert(runs).values({
84205
84327
  id: runId,
84206
84328
  projectId: project.id,
@@ -84749,7 +84871,7 @@ async function trafficRoutes(app, opts) {
84749
84871
  }
84750
84872
  })();
84751
84873
  tx.insert(rawEventSamples).values({
84752
- id: crypto49.randomUUID(),
84874
+ id: crypto50.randomUUID(),
84753
84875
  projectId: project.id,
84754
84876
  sourceId: sourceRow2.id,
84755
84877
  ts: sampleTimestamp,
@@ -84981,7 +85103,7 @@ async function trafficRoutes(app, opts) {
84981
85103
  };
84982
85104
  }
84983
85105
  const startedAt = windowEnd.toISOString();
84984
- const runId = crypto49.randomUUID();
85106
+ const runId = crypto50.randomUUID();
84985
85107
  app.db.transaction((tx) => {
84986
85108
  const latestSource = tx.select().from(trafficSources).where(eq76(trafficSources.id, sourceRow2.id)).get();
84987
85109
  if (!latestSource || !isAuthoritativeTrafficSource(tx, latestSource) || !isSameTrafficSourceGeneration(latestSource, sourceRow2)) {
@@ -85862,7 +85984,7 @@ var REFERRAL_ASSESSMENT_CHECKS = [{
85862
85984
  }];
85863
85985
 
85864
85986
  // ../api-routes/src/doctor/checks/agent.ts
85865
- import crypto50 from "crypto";
85987
+ import crypto51 from "crypto";
85866
85988
  import fs6 from "fs";
85867
85989
  import path7 from "path";
85868
85990
  var REQUIRED_SKILLS = ["canonry", "aero"];
@@ -86096,7 +86218,7 @@ function isInstalled(dir) {
86096
86218
  }
86097
86219
  function hashInstalledFile(filePath) {
86098
86220
  try {
86099
- return crypto50.createHash("sha256").update(fs6.readFileSync(filePath)).digest("hex");
86221
+ return crypto51.createHash("sha256").update(fs6.readFileSync(filePath)).digest("hex");
86100
86222
  } catch {
86101
86223
  return void 0;
86102
86224
  }
@@ -89979,7 +90101,7 @@ async function statusRoutes(app, opts) {
89979
90101
  }
89980
90102
 
89981
90103
  // ../api-routes/src/discovery/routes.ts
89982
- import crypto51 from "crypto";
90104
+ import crypto52 from "crypto";
89983
90105
  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";
89984
90106
  var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
89985
90107
  async function discoveryRoutes(app, opts) {
@@ -90045,8 +90167,8 @@ async function discoveryRoutes(app, opts) {
90045
90167
  if (existing && existing.runId) {
90046
90168
  return { reused: true, sessionId: existing.id, runId: existing.runId };
90047
90169
  }
90048
- const sessionId = crypto51.randomUUID();
90049
- const runId = crypto51.randomUUID();
90170
+ const sessionId = crypto52.randomUUID();
90171
+ const runId = crypto52.randomUUID();
90050
90172
  tx.insert(discoverySessions).values({
90051
90173
  id: sessionId,
90052
90174
  projectId: project.id,
@@ -90301,7 +90423,7 @@ async function discoveryRoutes(app, opts) {
90301
90423
  const marketNames = beginMarketNameWrite(tx, project.id);
90302
90424
  for (const query of promotedQueries) {
90303
90425
  tx.insert(queries).values({
90304
- id: crypto51.randomUUID(),
90426
+ id: crypto52.randomUUID(),
90305
90427
  projectId: project.id,
90306
90428
  query,
90307
90429
  provenance,
@@ -90482,7 +90604,7 @@ function planCompetitorPromotion(input) {
90482
90604
  }
90483
90605
 
90484
90606
  // ../api-routes/src/discovery/orchestrate.ts
90485
- import crypto52 from "crypto";
90607
+ import crypto53 from "crypto";
90486
90608
  import { eq as eq93 } from "drizzle-orm";
90487
90609
  var DEFAULT_MAX_PROBES = 100;
90488
90610
  var ABSOLUTE_MAX_PROBES = 500;
@@ -90648,7 +90770,7 @@ async function executeDiscovery(opts) {
90648
90770
  probeRows.push({ citedDomains: probe.citedDomains, bucket });
90649
90771
  buckets[bucket]++;
90650
90772
  return {
90651
- id: crypto52.randomUUID(),
90773
+ id: crypto53.randomUUID(),
90652
90774
  sessionId: opts.sessionId,
90653
90775
  projectId: opts.project.id,
90654
90776
  query: probedCanonicals[index2],
@@ -90700,7 +90822,7 @@ function upsertDomainClassifications(db, projectId, sessionId, competitorMap) {
90700
90822
  const domain = hostOf(entry.domain) ?? "";
90701
90823
  if (!domain) continue;
90702
90824
  db.insert(domainClassifications).values({
90703
- id: crypto52.randomUUID(),
90825
+ id: crypto53.randomUUID(),
90704
90826
  projectId,
90705
90827
  domain,
90706
90828
  competitorType: entry.competitorType,
@@ -90740,7 +90862,7 @@ function dedupeStrings(input) {
90740
90862
  }
90741
90863
 
90742
90864
  // ../api-routes/src/technical-aeo.ts
90743
- import crypto53 from "crypto";
90865
+ import crypto54 from "crypto";
90744
90866
  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";
90745
90867
  import { alias } from "drizzle-orm/sqlite-core";
90746
90868
 
@@ -92733,7 +92855,7 @@ async function technicalAeoRoutes(app, opts) {
92733
92855
  const healthState = parseSiteHealthState(request.query.healthState);
92734
92856
  const limit = parseBoundedLimit(request.query.limit, 100, 200);
92735
92857
  const sort = ["score-desc", "score-asc", "path"].includes(request.query.sort ?? "") ? request.query.sort : "url";
92736
- const selectionKey = crypto53.createHash("sha256").update(JSON.stringify({
92858
+ const selectionKey = crypto54.createHash("sha256").update(JSON.stringify({
92737
92859
  projectId: project.id,
92738
92860
  runId: snapshot.runId,
92739
92861
  attemptId: snapshot.attemptId,
@@ -93233,7 +93355,7 @@ async function technicalAeoRoutes(app, opts) {
93233
93355
  );
93234
93356
  }
93235
93357
  const now = (/* @__PURE__ */ new Date()).toISOString();
93236
- const runId = crypto53.randomUUID();
93358
+ const runId = crypto54.randomUUID();
93237
93359
  tx.insert(runs).values({
93238
93360
  id: runId,
93239
93361
  projectId: project.id,
@@ -93260,7 +93382,9 @@ async function technicalAeoRoutes(app, opts) {
93260
93382
  // from the page count instead of inheriting a flat ceiling here.
93261
93383
  maxEdges: effectiveRequest.maxEdges ?? void 0,
93262
93384
  maxDepth: effectiveRequest.maxDepth ?? void 0,
93263
- checkDeadLinks: effectiveRequest.checkDeadLinks
93385
+ checkDeadLinks: effectiveRequest.checkDeadLinks,
93386
+ includeGeo: effectiveRequest.includeGeo,
93387
+ includeAgentSkills: effectiveRequest.includeAgentSkills
93264
93388
  });
93265
93389
  }
93266
93390
  return { runId: result.runId, status: result.status };
@@ -93268,7 +93392,7 @@ async function technicalAeoRoutes(app, opts) {
93268
93392
  }
93269
93393
 
93270
93394
  // ../api-routes/src/research.ts
93271
- import crypto54 from "crypto";
93395
+ import crypto55 from "crypto";
93272
93396
  import { z as z5 } from "zod";
93273
93397
  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";
93274
93398
  var sameLocation = (a, b) => a.label === b.label && a.city === b.city && a.region === b.region && a.country === b.country && a.timezone === b.timezone;
@@ -93338,9 +93462,9 @@ async function researchRoutes(app, opts) {
93338
93462
  throw researchDailyLimitExceeded(project.name, viewerDailyRunLimit, date);
93339
93463
  }
93340
93464
  }
93341
- const id = crypto54.randomUUID();
93465
+ const id = crypto55.randomUUID();
93342
93466
  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();
93343
- 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();
93467
+ 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();
93344
93468
  writeAuditLog(tx, { projectId: project.id, actor: "api", action: "research.created", entityType: "research_run", entityId: id });
93345
93469
  return { reused: false, id, shouldDispatch: true };
93346
93470
  });
@@ -93393,7 +93517,7 @@ async function researchRoutes(app, opts) {
93393
93517
  }
93394
93518
  const ids = [];
93395
93519
  for (const [index2, run] of prepared.entries()) {
93396
- const id = crypto54.randomUUID();
93520
+ const id = crypto55.randomUUID();
93397
93521
  ids.push(id);
93398
93522
  tx.insert(researchRuns).values({
93399
93523
  id,
@@ -93412,7 +93536,7 @@ async function researchRoutes(app, opts) {
93412
93536
  createdAt: now
93413
93537
  }).run();
93414
93538
  for (const [position, query] of run.input.queries.entries()) tx.insert(researchRunQueries).values({
93415
- id: crypto54.randomUUID(),
93539
+ id: crypto55.randomUUID(),
93416
93540
  researchRunId: id,
93417
93541
  position,
93418
93542
  queryText: query,
@@ -93471,7 +93595,7 @@ async function researchRoutes(app, opts) {
93471
93595
  var DIRECT_REQUEST_HASH_VERSION = "direct-v2:";
93472
93596
  function directResearchRequestHash(input) {
93473
93597
  const { idempotencyKey: _key, ...request } = input;
93474
- return DIRECT_REQUEST_HASH_VERSION + crypto54.createHash("sha256").update(JSON.stringify(request)).digest("hex");
93598
+ return DIRECT_REQUEST_HASH_VERSION + crypto55.createHash("sha256").update(JSON.stringify(request)).digest("hex");
93475
93599
  }
93476
93600
  function assertSameDirectResearchRequest(existing, input, hash2) {
93477
93601
  if (existing.requestHash?.startsWith(DIRECT_REQUEST_HASH_VERSION)) {
@@ -93489,7 +93613,7 @@ function assertSameDirectResearchRequest(existing, input, hash2) {
93489
93613
  ...scope ? { scope } : {},
93490
93614
  ...template ? { template } : {}
93491
93615
  };
93492
- if (crypto54.createHash("sha256").update(JSON.stringify(normalized)).digest("hex") !== existing.requestHash) throw alreadyExists("Research idempotency key", input.idempotencyKey);
93616
+ if (crypto55.createHash("sha256").update(JSON.stringify(normalized)).digest("hex") !== existing.requestHash) throw alreadyExists("Research idempotency key", input.idempotencyKey);
93493
93617
  }
93494
93618
  var researchCursorSchema = z5.object({ projectId: z5.string().min(1), createdAt: z5.string().datetime({ offset: true }), id: z5.string().min(1).max(256) }).strict();
93495
93619
  function parseResearchCursor(value, projectId) {
@@ -93509,11 +93633,11 @@ function resolveStoredResearchScope(stored, selection, idempotencyKey) {
93509
93633
  return stored;
93510
93634
  }
93511
93635
  function batchChildReceiptKey(rootKey, index2) {
93512
- const derived = crypto54.createHash("sha256").update(`${rootKey}\0${index2}`).digest("hex");
93636
+ const derived = crypto55.createHash("sha256").update(`${rootKey}\0${index2}`).digest("hex");
93513
93637
  return `${BATCH_RECEIPT_PREFIX}${derived}:${index2}`;
93514
93638
  }
93515
93639
  function researchBatchRequestHash(input) {
93516
- return crypto54.createHash("sha256").update(JSON.stringify({ runs: input.runs })).digest("hex");
93640
+ return crypto55.createHash("sha256").update(JSON.stringify({ runs: input.runs })).digest("hex");
93517
93641
  }
93518
93642
  function prepareBatchRun(app, opts, project, active, input) {
93519
93643
  const adapters = opts.providerAdapters ?? [];
@@ -93785,7 +93909,7 @@ function readStoredGroundingSources(rawResponse) {
93785
93909
  }
93786
93910
 
93787
93911
  // ../api-routes/src/simple-measurement-definitions.ts
93788
- import crypto55 from "crypto";
93912
+ import crypto56 from "crypto";
93789
93913
  import { and as and81, eq as eq98 } from "drizzle-orm";
93790
93914
  function captureSimpleMeasurementDefinition(db, input) {
93791
93915
  return db.transaction((tx) => {
@@ -93827,11 +93951,11 @@ function captureSimpleMeasurementDefinition(db, input) {
93827
93951
  ...frozen.identity.qualifiedAliases?.length ? { qualifiedAliases: frozen.identity.qualifiedAliases } : {}
93828
93952
  }
93829
93953
  };
93830
- const replayChecksum = crypto55.createHash("sha256").update(canonicalSimpleMeasurementDefinitionJson(replayDefinition)).digest("hex");
93954
+ const replayChecksum = crypto56.createHash("sha256").update(canonicalSimpleMeasurementDefinitionJson(replayDefinition)).digest("hex");
93831
93955
  if (existing.checksum !== replayChecksum) {
93832
93956
  if (frozen.competitors === void 0) {
93833
93957
  const { competitors: _currentCompetitors, ...legacyReplayDefinition } = replayDefinition;
93834
- const legacyReplayChecksum = crypto55.createHash("sha256").update(canonicalSimpleMeasurementDefinitionJson(legacyReplayDefinition)).digest("hex");
93958
+ const legacyReplayChecksum = crypto56.createHash("sha256").update(canonicalSimpleMeasurementDefinitionJson(legacyReplayDefinition)).digest("hex");
93835
93959
  if (existing.checksum === legacyReplayChecksum) return frozen;
93836
93960
  }
93837
93961
  throw validationError("This run already has a captured measurement definition. Start a new run for changed inputs.");
@@ -93855,7 +93979,7 @@ function captureSimpleMeasurementDefinition(db, input) {
93855
93979
  throw validationError("Captured competitors must exactly match the project competitors dispatched for this run.");
93856
93980
  }
93857
93981
  }
93858
- const checksum = crypto55.createHash("sha256").update(canonicalSimpleMeasurementDefinitionJson(definition2)).digest("hex");
93982
+ const checksum = crypto56.createHash("sha256").update(canonicalSimpleMeasurementDefinitionJson(definition2)).digest("hex");
93859
93983
  tx.insert(simpleMeasurementDefinitions).values({
93860
93984
  projectId: input.projectId,
93861
93985
  runId: input.runId,
@@ -94470,7 +94594,7 @@ function buildTrafficSourceValidators(opts) {
94470
94594
  }
94471
94595
 
94472
94596
  // src/intelligence-service.ts
94473
- import crypto56 from "crypto";
94597
+ import crypto57 from "crypto";
94474
94598
 
94475
94599
  // ../api-routes/src/runtime-logger.ts
94476
94600
  import { format } from "util";
@@ -95272,7 +95396,7 @@ var IntelligenceService = class {
95272
95396
  }).run();
95273
95397
  }
95274
95398
  tx.insert(healthSnapshots).values({
95275
- id: crypto56.randomUUID(),
95399
+ id: crypto57.randomUUID(),
95276
95400
  projectId,
95277
95401
  runId,
95278
95402
  overallCitedRate: String(result.health.overallCitedRate),