@ainyc/canonry 4.80.0 → 4.82.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 (25) hide show
  1. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +24 -12
  2. package/assets/assets/{BacklinksPage-dRc62jAY.js → BacklinksPage-CHclt-pq.js} +1 -1
  3. package/assets/assets/{ChartPrimitives-D2_IvTkk.js → ChartPrimitives-2Ub4vNWe.js} +1 -1
  4. package/assets/assets/ProjectPage-UPmHfuxR.js +6 -0
  5. package/assets/assets/{RunRow-C0MA3yuQ.js → RunRow-rUL1UeA3.js} +1 -1
  6. package/assets/assets/{RunsPage-4uxTYgGy.js → RunsPage-BQpHfUJf.js} +1 -1
  7. package/assets/assets/{SettingsPage-3-SLhcJ7.js → SettingsPage-DjTJlr_1.js} +1 -1
  8. package/assets/assets/{TrafficPage-DZ50qwme.js → TrafficPage-D7rv3BrH.js} +1 -1
  9. package/assets/assets/TrafficSourceDetailPage-BysyuH2H.js +1 -0
  10. package/assets/assets/{arrow-left-BaZIkAXX.js → arrow-left-CR_FGlkE.js} +1 -1
  11. package/assets/assets/{extract-error-message-cpvfuFqW.js → extract-error-message-BKkAbWNp.js} +1 -1
  12. package/assets/assets/{index-EnY_OBRd.js → index-DzzTt20n.js} +87 -87
  13. package/assets/assets/{trash-2-JpcztiS5.js → trash-2-uSttujvh.js} +1 -1
  14. package/assets/index.html +1 -1
  15. package/dist/{chunk-CXIGHPBE.js → chunk-IEUTAQUF.js} +471 -124
  16. package/dist/{chunk-2QBSRHSN.js → chunk-JLAD6CYH.js} +88 -8
  17. package/dist/{chunk-AVN6Q6LM.js → chunk-KPSFRSS7.js} +96 -3
  18. package/dist/{chunk-LCABGFYN.js → chunk-NSZ3D3MM.js} +404 -242
  19. package/dist/cli.js +145 -18
  20. package/dist/index.js +4 -4
  21. package/dist/{intelligence-service-ZWW3I3NL.js → intelligence-service-2UUJ3YGI.js} +2 -2
  22. package/dist/mcp.js +23 -4
  23. package/package.json +8 -8
  24. package/assets/assets/ProjectPage-DSuvRUIf.js +0 -6
  25. package/assets/assets/TrafficSourceDetailPage-CzK5TMFp.js +0 -1
@@ -3,6 +3,7 @@ import {
3
3
  AI_ENGINE_DOMAINS,
4
4
  AI_PROVIDER_INFRA_DOMAINS,
5
5
  AppError,
6
+ BacklinkSources,
6
7
  CcReleaseSyncStatuses,
7
8
  CheckCategories,
8
9
  CheckScopes,
@@ -52,6 +53,8 @@ import {
52
53
  authRequired,
53
54
  backlinkHistoryEntrySchema,
54
55
  backlinkListResponseSchema,
56
+ backlinkSourceSchema,
57
+ backlinkSourcesResponseSchema,
55
58
  backlinkSummaryDtoSchema,
56
59
  backlinksInstallResultDtoSchema,
57
60
  backlinksInstallStatusDtoSchema,
@@ -153,6 +156,7 @@ import {
153
156
  hasLocationLabel,
154
157
  indexingRequestResponseDtoSchema,
155
158
  internalError,
159
+ isReadOnlyKey,
156
160
  keywordDtoSchema,
157
161
  keywordGenerateRequestSchema,
158
162
  latestProjectRunDtoSchema,
@@ -242,10 +246,10 @@ import {
242
246
  wordpressSchemaDeployResultDtoSchema,
243
247
  wordpressSchemaStatusResultDtoSchema,
244
248
  wordpressStatusDtoSchema
245
- } from "./chunk-AVN6Q6LM.js";
249
+ } from "./chunk-KPSFRSS7.js";
246
250
 
247
251
  // src/intelligence-service.ts
248
- import { eq as eq35, desc as desc17, asc as asc5, and as and25, ne as ne5, or as or5, inArray as inArray13, gte as gte7, lte as lte4 } from "drizzle-orm";
252
+ import { eq as eq36, desc as desc17, asc as asc5, and as and26, ne as ne5, or as or5, inArray as inArray13, gte as gte7, lte as lte4 } from "drizzle-orm";
249
253
 
250
254
  // ../db/src/client.ts
251
255
  import { mkdirSync } from "fs";
@@ -850,7 +854,9 @@ var ccReleaseSyncs = sqliteTable("cc_release_syncs", {
850
854
  var backlinkDomains = sqliteTable("backlink_domains", {
851
855
  id: text("id").primaryKey(),
852
856
  projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
853
- releaseSyncId: text("release_sync_id").notNull().references(() => ccReleaseSyncs.id, { onDelete: "cascade" }),
857
+ // Nullable: Bing Webmaster backlink rows have no Common Crawl release sync.
858
+ releaseSyncId: text("release_sync_id").references(() => ccReleaseSyncs.id, { onDelete: "cascade" }),
859
+ source: text("source").$type().notNull().default("commoncrawl"),
854
860
  release: text("release").notNull(),
855
861
  targetDomain: text("target_domain").notNull(),
856
862
  linkingDomain: text("linking_domain").notNull(),
@@ -861,12 +867,14 @@ var backlinkDomains = sqliteTable("backlink_domains", {
861
867
  index("idx_backlink_domains_release_sync").on(table.releaseSyncId),
862
868
  index("idx_backlink_domains_project_release").on(table.projectId, table.release),
863
869
  index("idx_backlink_domains_hosts").on(table.numHosts),
864
- uniqueIndex("idx_backlink_domains_unique").on(table.projectId, table.release, table.linkingDomain)
870
+ uniqueIndex("idx_backlink_domains_unique").on(table.projectId, table.source, table.release, table.linkingDomain)
865
871
  ]);
866
872
  var backlinkSummaries = sqliteTable("backlink_summaries", {
867
873
  id: text("id").primaryKey(),
868
874
  projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
869
- releaseSyncId: text("release_sync_id").notNull().references(() => ccReleaseSyncs.id, { onDelete: "cascade" }),
875
+ // Nullable: Bing Webmaster summaries have no Common Crawl release sync.
876
+ releaseSyncId: text("release_sync_id").references(() => ccReleaseSyncs.id, { onDelete: "cascade" }),
877
+ source: text("source").$type().notNull().default("commoncrawl"),
870
878
  release: text("release").notNull(),
871
879
  targetDomain: text("target_domain").notNull(),
872
880
  totalLinkingDomains: integer("total_linking_domains").notNull(),
@@ -875,7 +883,7 @@ var backlinkSummaries = sqliteTable("backlink_summaries", {
875
883
  queriedAt: text("queried_at").notNull(),
876
884
  createdAt: text("created_at").notNull()
877
885
  }, (table) => [
878
- uniqueIndex("idx_backlink_summaries_project_release").on(table.projectId, table.release),
886
+ uniqueIndex("idx_backlink_summaries_project_release").on(table.projectId, table.source, table.release),
879
887
  index("idx_backlink_summaries_project").on(table.projectId)
880
888
  ]);
881
889
  var agentMemory = sqliteTable("agent_memory", {
@@ -3051,8 +3059,86 @@ var MIGRATION_VERSIONS = [
3051
3059
  `CREATE UNIQUE INDEX IF NOT EXISTS uniq_ads_insights_daily ON ads_insights_daily(project_id, level, entity_id, date)`,
3052
3060
  `CREATE INDEX IF NOT EXISTS idx_ads_insights_project_date ON ads_insights_daily(project_id, date)`
3053
3061
  ]
3062
+ },
3063
+ {
3064
+ // Bing Webmaster inbound links land in the SAME backlink store as Common
3065
+ // Crawl, tagged by a `source` discriminator (commoncrawl | bing-webmaster).
3066
+ // Bing rows have no `cc_release_syncs` row, so `release_sync_id` becomes
3067
+ // nullable and the per-window UNIQUE gains `source`. SQLite can't drop a
3068
+ // NOT NULL or rewrite a UNIQUE in place — canonical table rebuild (the
3069
+ // v58/v60 pattern). Guarded on the `source` column's absence so a replay
3070
+ // over the already-migrated schema is a no-op (the hardcoded
3071
+ // `source='commoncrawl'` backfill must never clobber real bing rows).
3072
+ version: 78,
3073
+ name: "backlinks-source-discriminator",
3074
+ statements: [],
3075
+ run: (tx) => {
3076
+ addBacklinkSourceDiscriminator(tx);
3077
+ }
3054
3078
  }
3055
3079
  ];
3080
+ function rebuildBacklinkTableWithSource(tx, table) {
3081
+ if (!tableExists(tx, table)) return;
3082
+ if (columnExists(tx, table, "source")) return;
3083
+ if (table === "backlink_domains") {
3084
+ tx.run(sql.raw(`DROP TABLE IF EXISTS backlink_domains_v78`));
3085
+ tx.run(sql.raw(`CREATE TABLE backlink_domains_v78 (
3086
+ id TEXT PRIMARY KEY,
3087
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
3088
+ release_sync_id TEXT REFERENCES cc_release_syncs(id) ON DELETE CASCADE,
3089
+ source TEXT NOT NULL DEFAULT 'commoncrawl',
3090
+ release TEXT NOT NULL,
3091
+ target_domain TEXT NOT NULL,
3092
+ linking_domain TEXT NOT NULL,
3093
+ num_hosts INTEGER NOT NULL,
3094
+ created_at TEXT NOT NULL
3095
+ )`));
3096
+ tx.run(sql.raw(`INSERT INTO backlink_domains_v78
3097
+ (id, project_id, release_sync_id, source, release, target_domain, linking_domain, num_hosts, created_at)
3098
+ SELECT bd.id, bd.project_id,
3099
+ CASE WHEN bd.release_sync_id IN (SELECT id FROM cc_release_syncs) THEN bd.release_sync_id ELSE NULL END,
3100
+ 'commoncrawl', bd.release, bd.target_domain, bd.linking_domain, bd.num_hosts, bd.created_at
3101
+ FROM backlink_domains bd
3102
+ WHERE bd.project_id IN (SELECT id FROM projects)`));
3103
+ tx.run(sql.raw(`DROP TABLE backlink_domains`));
3104
+ tx.run(sql.raw(`ALTER TABLE backlink_domains_v78 RENAME TO backlink_domains`));
3105
+ tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_project ON backlink_domains(project_id)`));
3106
+ tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_release_sync ON backlink_domains(release_sync_id)`));
3107
+ tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_project_release ON backlink_domains(project_id, release)`));
3108
+ tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_hosts ON backlink_domains(num_hosts)`));
3109
+ tx.run(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS idx_backlink_domains_unique ON backlink_domains(project_id, source, release, linking_domain)`));
3110
+ return;
3111
+ }
3112
+ tx.run(sql.raw(`DROP TABLE IF EXISTS backlink_summaries_v78`));
3113
+ tx.run(sql.raw(`CREATE TABLE backlink_summaries_v78 (
3114
+ id TEXT PRIMARY KEY,
3115
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
3116
+ release_sync_id TEXT REFERENCES cc_release_syncs(id) ON DELETE CASCADE,
3117
+ source TEXT NOT NULL DEFAULT 'commoncrawl',
3118
+ release TEXT NOT NULL,
3119
+ target_domain TEXT NOT NULL,
3120
+ total_linking_domains INTEGER NOT NULL,
3121
+ total_hosts INTEGER NOT NULL,
3122
+ top_10_hosts_share TEXT NOT NULL,
3123
+ queried_at TEXT NOT NULL,
3124
+ created_at TEXT NOT NULL
3125
+ )`));
3126
+ tx.run(sql.raw(`INSERT INTO backlink_summaries_v78
3127
+ (id, project_id, release_sync_id, source, release, target_domain, total_linking_domains, total_hosts, top_10_hosts_share, queried_at, created_at)
3128
+ SELECT bs.id, bs.project_id,
3129
+ CASE WHEN bs.release_sync_id IN (SELECT id FROM cc_release_syncs) THEN bs.release_sync_id ELSE NULL END,
3130
+ 'commoncrawl', bs.release, bs.target_domain, bs.total_linking_domains, bs.total_hosts, bs.top_10_hosts_share, bs.queried_at, bs.created_at
3131
+ FROM backlink_summaries bs
3132
+ WHERE bs.project_id IN (SELECT id FROM projects)`));
3133
+ tx.run(sql.raw(`DROP TABLE backlink_summaries`));
3134
+ tx.run(sql.raw(`ALTER TABLE backlink_summaries_v78 RENAME TO backlink_summaries`));
3135
+ tx.run(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS idx_backlink_summaries_project_release ON backlink_summaries(project_id, source, release)`));
3136
+ tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_summaries_project ON backlink_summaries(project_id)`));
3137
+ }
3138
+ function addBacklinkSourceDiscriminator(tx) {
3139
+ rebuildBacklinkTableWithSource(tx, "backlink_domains");
3140
+ rebuildBacklinkTableWithSource(tx, "backlink_summaries");
3141
+ }
3056
3142
  function isDuplicateColumnError(err) {
3057
3143
  if (!(err instanceof Error)) return false;
3058
3144
  if (err.message.includes("duplicate column name")) return true;
@@ -5205,6 +5291,7 @@ import fs8 from "fs";
5205
5291
  // ../api-routes/src/auth.ts
5206
5292
  import crypto2 from "crypto";
5207
5293
  import { eq } from "drizzle-orm";
5294
+ var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
5208
5295
  function requireScope(request, scope) {
5209
5296
  const key = request.apiKey;
5210
5297
  if (!key) return;
@@ -5272,6 +5359,9 @@ async function authPlugin(app, opts = {}) {
5272
5359
  app.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(apiKeys.id, key.id)).run();
5273
5360
  const scopes = Array.isArray(key.scopes) ? key.scopes : [];
5274
5361
  request.apiKey = { id: key.id, name: key.name, scopes };
5362
+ if (isReadOnlyKey(scopes) && WRITE_METHODS.has(request.method)) {
5363
+ throw forbidden("This API key is read-only and cannot perform write operations.");
5364
+ }
5275
5365
  });
5276
5366
  }
5277
5367
 
@@ -13206,6 +13296,7 @@ var SCHEMA_TABLE = {
13206
13296
  AuditLogEntry: auditLogEntrySchema,
13207
13297
  BacklinkHistoryEntry: backlinkHistoryEntrySchema,
13208
13298
  BacklinkListResponse: backlinkListResponseSchema,
13299
+ BacklinkSourcesResponse: backlinkSourcesResponseSchema,
13209
13300
  BacklinkSummaryDto: backlinkSummaryDtoSchema,
13210
13301
  BacklinksInstallResultDto: backlinksInstallResultDtoSchema,
13211
13302
  BacklinksInstallStatusDto: backlinksInstallStatusDtoSchema,
@@ -14345,6 +14436,17 @@ var routeCatalog = [
14345
14436
  200: jsonResponse("Keys returned.", "ApiKeyListDto")
14346
14437
  }
14347
14438
  },
14439
+ {
14440
+ method: "get",
14441
+ path: "/api/v1/keys/self",
14442
+ summary: "Introspect the current API key",
14443
+ description: "Returns SAFE metadata for the key that authenticated this request, including the derived `readOnly` flag. Lets a caller (or the MCP adapter at startup) discover whether its configured key is read-only without listing every key on the instance. Ungated read \u2014 a read-only key can call it.",
14444
+ tags: ["keys"],
14445
+ responses: {
14446
+ 200: jsonResponse("Current key returned.", "ApiKeyDto"),
14447
+ 404: errorResponse("No key on the request (auth skipped).")
14448
+ }
14449
+ },
14348
14450
  {
14349
14451
  method: "post",
14350
14452
  path: "/api/v1/keys",
@@ -16666,7 +16768,8 @@ var routeCatalog = [
16666
16768
  tags: ["backlinks"],
16667
16769
  parameters: [
16668
16770
  nameParameter,
16669
- { name: "release", in: "query", description: "Release id filter.", schema: stringSchema }
16771
+ { name: "release", in: "query", description: "Release id filter.", schema: stringSchema },
16772
+ { name: "source", in: "query", description: "Backlink source: commoncrawl (default) or bing-webmaster.", schema: stringSchema }
16670
16773
  ],
16671
16774
  responses: {
16672
16775
  200: rawJsonResponse("Summary returned, or null when no backlinks exist.", {
@@ -16684,7 +16787,8 @@ var routeCatalog = [
16684
16787
  nameParameter,
16685
16788
  { name: "release", in: "query", description: "Release id filter.", schema: stringSchema },
16686
16789
  { name: "limit", in: "query", description: "Max results (1-500).", schema: stringSchema },
16687
- { name: "offset", in: "query", description: "Pagination offset.", schema: stringSchema }
16790
+ { name: "offset", in: "query", description: "Pagination offset.", schema: stringSchema },
16791
+ { name: "source", in: "query", description: "Backlink source: commoncrawl (default) or bing-webmaster.", schema: stringSchema }
16688
16792
  ],
16689
16793
  responses: {
16690
16794
  200: jsonResponse("Domain list returned.", "BacklinkListResponse"),
@@ -16696,12 +16800,44 @@ var routeCatalog = [
16696
16800
  path: "/api/v1/projects/{name}/backlinks/history",
16697
16801
  summary: "Get per-release backlink summaries for a project",
16698
16802
  tags: ["backlinks"],
16699
- parameters: [nameParameter],
16803
+ parameters: [
16804
+ nameParameter,
16805
+ { name: "source", in: "query", description: "Backlink source: commoncrawl (default) or bing-webmaster.", schema: stringSchema }
16806
+ ],
16700
16807
  responses: {
16701
16808
  200: jsonArrayResponse("History returned oldest-first by queriedAt.", "BacklinkHistoryEntry"),
16702
16809
  404: errorResponse("Project not found.")
16703
16810
  }
16704
16811
  },
16812
+ {
16813
+ method: "get",
16814
+ path: "/api/v1/projects/{name}/backlinks/sources",
16815
+ summary: "Report per-source backlink availability for a project",
16816
+ description: "Returns connection + data availability for every backlink source (commoncrawl, bing-webmaster) so callers can degrade gracefully across CC-only / Bing-only / both / neither.",
16817
+ tags: ["backlinks"],
16818
+ parameters: [
16819
+ nameParameter,
16820
+ { name: "excludeCrawlers", in: "query", description: 'When "1"/"true", count linking domains excluding crawler/proxy hosts (matches the dashboard). Default off.', schema: stringSchema }
16821
+ ],
16822
+ responses: {
16823
+ 200: jsonResponse("Per-source availability returned.", "BacklinkSourcesResponse"),
16824
+ 404: errorResponse("Project not found.")
16825
+ }
16826
+ },
16827
+ {
16828
+ method: "post",
16829
+ path: "/api/v1/projects/{name}/backlinks/bing-sync",
16830
+ summary: "Sync a project's inbound links from Bing Webmaster Tools",
16831
+ description: 'Creates a tracking run and pulls inbound links live from the connected Bing Webmaster account, writing source="bing-webmaster" backlink rows. Requires a Bing connection for the project domain.',
16832
+ tags: ["backlinks"],
16833
+ parameters: [nameParameter],
16834
+ responses: {
16835
+ 201: jsonResponse("Bing sync run queued.", "RunDto"),
16836
+ 400: errorResponse("No Bing Webmaster connection for this project."),
16837
+ 404: errorResponse("Project not found."),
16838
+ 422: errorResponse("Bing backlinks sync is not available on this deployment.")
16839
+ }
16840
+ },
16705
16841
  {
16706
16842
  method: "post",
16707
16843
  path: "/api/v1/projects/{name}/traffic/connect/cloud-run",
@@ -17466,11 +17602,13 @@ import crypto12 from "crypto";
17466
17602
  import { desc as desc9, eq as eq17 } from "drizzle-orm";
17467
17603
  var KEYS_WRITE_SCOPE = "keys.write";
17468
17604
  function toApiKeyDto(row) {
17605
+ const scopes = Array.isArray(row.scopes) ? row.scopes : [];
17469
17606
  return {
17470
17607
  id: row.id,
17471
17608
  name: row.name,
17472
17609
  keyPrefix: row.keyPrefix,
17473
- scopes: Array.isArray(row.scopes) ? row.scopes : [],
17610
+ scopes,
17611
+ readOnly: isReadOnlyKey(scopes),
17474
17612
  createdAt: row.createdAt,
17475
17613
  lastUsedAt: row.lastUsedAt ?? null,
17476
17614
  revokedAt: row.revokedAt ?? null
@@ -17481,6 +17619,17 @@ async function keysRoutes(app) {
17481
17619
  const rows = app.db.select().from(apiKeys).orderBy(desc9(apiKeys.createdAt)).all();
17482
17620
  return { keys: rows.map(toApiKeyDto) };
17483
17621
  });
17622
+ app.get("/keys/self", async (request) => {
17623
+ const id = request.apiKey?.id;
17624
+ if (!id) {
17625
+ throw notFound("API key", "self");
17626
+ }
17627
+ const row = app.db.select().from(apiKeys).where(eq17(apiKeys.id, id)).get();
17628
+ if (!row) {
17629
+ throw notFound("API key", id);
17630
+ }
17631
+ return toApiKeyDto(row);
17632
+ });
17484
17633
  app.post("/keys", async (request) => {
17485
17634
  requireScope(request, KEYS_WRITE_SCOPE);
17486
17635
  const parsed = createApiKeyRequestSchema.safeParse(request.body);
@@ -17516,6 +17665,7 @@ async function keysRoutes(app) {
17516
17665
  name,
17517
17666
  keyPrefix,
17518
17667
  scopes: effectiveScopes,
17668
+ readOnly: isReadOnlyKey(effectiveScopes),
17519
17669
  createdAt: now,
17520
17670
  lastUsedAt: null,
17521
17671
  revokedAt: null,
@@ -20901,6 +21051,7 @@ var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
20901
21051
  var BING_SUBMIT_URL_BATCH_LIMIT = 500;
20902
21052
  var BING_SUBMIT_URL_DAILY_LIMIT = 1e4;
20903
21053
  var BING_REQUEST_TIMEOUT_MS = 3e4;
21054
+ var BING_LINKS_MAX_PAGES = 20;
20904
21055
 
20905
21056
  // ../integration-bing/src/types.ts
20906
21057
  var BingApiError = class extends Error {
@@ -21064,6 +21215,51 @@ async function getCrawlIssues(apiKey, siteUrl) {
21064
21215
  const data = await bingFetch(apiKey, `GetCrawlIssues?siteUrl=${encodedSite}`);
21065
21216
  return data ?? [];
21066
21217
  }
21218
+ async function getLinkCounts(apiKey, siteUrl, opts = {}) {
21219
+ validateApiKey(apiKey);
21220
+ validateSiteUrl2(siteUrl);
21221
+ const encodedSite = encodeURIComponent(siteUrl);
21222
+ const maxPages = Math.max(1, opts.maxPages ?? BING_LINKS_MAX_PAGES);
21223
+ const out = [];
21224
+ let page = 0;
21225
+ let totalPages = 1;
21226
+ while (page < totalPages && page < maxPages) {
21227
+ const data = await bingFetch(apiKey, `GetLinkCounts?siteUrl=${encodedSite}&page=${page}`);
21228
+ for (const link of data?.Links ?? []) {
21229
+ if (link && typeof link.Url === "string") {
21230
+ out.push({ Url: link.Url, Count: Number(link.Count ?? 0) });
21231
+ }
21232
+ }
21233
+ totalPages = Number(data?.TotalPages ?? 1) || 1;
21234
+ page++;
21235
+ }
21236
+ return out;
21237
+ }
21238
+ async function getUrlLinks(apiKey, siteUrl, link, opts = {}) {
21239
+ validateApiKey(apiKey);
21240
+ validateSiteUrl2(siteUrl);
21241
+ validateUrl2(link);
21242
+ const encodedSite = encodeURIComponent(siteUrl);
21243
+ const encodedLink = encodeURIComponent(link);
21244
+ const maxPages = Math.max(1, opts.maxPages ?? BING_LINKS_MAX_PAGES);
21245
+ const out = [];
21246
+ let page = 0;
21247
+ let totalPages = 1;
21248
+ while (page < totalPages && page < maxPages) {
21249
+ const data = await bingFetch(
21250
+ apiKey,
21251
+ `GetUrlLinks?siteUrl=${encodedSite}&link=${encodedLink}&page=${page}`
21252
+ );
21253
+ for (const detail of data?.Details ?? []) {
21254
+ if (detail && typeof detail.Url === "string") {
21255
+ out.push({ Url: detail.Url, AnchorText: detail.AnchorText });
21256
+ }
21257
+ }
21258
+ totalPages = Number(data?.TotalPages ?? 1) || 1;
21259
+ page++;
21260
+ }
21261
+ return out;
21262
+ }
21067
21263
 
21068
21264
  // ../api-routes/src/bing.ts
21069
21265
  function parseBingDate(value) {
@@ -24662,9 +24858,18 @@ function mapSummaryRow(row) {
24662
24858
  totalLinkingDomains: row.totalLinkingDomains,
24663
24859
  totalHosts: row.totalHosts,
24664
24860
  top10HostsShare: row.top10HostsShare,
24665
- queriedAt: row.queriedAt
24861
+ queriedAt: row.queriedAt,
24862
+ source: row.source
24666
24863
  };
24667
24864
  }
24865
+ function parseSourceParam(value) {
24866
+ if (value === void 0 || value === "") return BacklinkSources.commoncrawl;
24867
+ const parsed = backlinkSourceSchema.safeParse(value);
24868
+ if (!parsed.success) {
24869
+ throw validationError(`Invalid source "${value}". Expected one of: ${Object.values(BacklinkSources).join(", ")}.`);
24870
+ }
24871
+ return parsed.data;
24872
+ }
24668
24873
  function mapRunRow(row) {
24669
24874
  return {
24670
24875
  id: row.id,
@@ -24679,9 +24884,13 @@ function mapRunRow(row) {
24679
24884
  createdAt: row.createdAt
24680
24885
  };
24681
24886
  }
24682
- function latestSummaryForProject(db, projectId, release) {
24683
- const condition = release ? and19(eq25(backlinkSummaries.projectId, projectId), eq25(backlinkSummaries.release, release)) : eq25(backlinkSummaries.projectId, projectId);
24684
- return db.select().from(backlinkSummaries).where(condition).orderBy(desc13(backlinkSummaries.queriedAt)).limit(1).get();
24887
+ function latestSummaryForProject(db, projectId, source, release) {
24888
+ const conditions = [
24889
+ eq25(backlinkSummaries.projectId, projectId),
24890
+ eq25(backlinkSummaries.source, source)
24891
+ ];
24892
+ if (release) conditions.push(eq25(backlinkSummaries.release, release));
24893
+ return db.select().from(backlinkSummaries).where(and19(...conditions)).orderBy(desc13(backlinkSummaries.queriedAt)).limit(1).get();
24685
24894
  }
24686
24895
  function parseExcludeCrawlers(value) {
24687
24896
  if (!value) return false;
@@ -24691,6 +24900,7 @@ function parseExcludeCrawlers(value) {
24691
24900
  function computeFilteredSummary(db, base) {
24692
24901
  const baseDomainCondition = and19(
24693
24902
  eq25(backlinkDomains.projectId, base.projectId),
24903
+ eq25(backlinkDomains.source, base.source),
24694
24904
  eq25(backlinkDomains.release, base.release)
24695
24905
  );
24696
24906
  const filteredCondition = and19(baseDomainCondition, backlinkCrawlerExclusionClause());
@@ -24717,10 +24927,48 @@ function computeFilteredSummary(db, base) {
24717
24927
  totalHosts,
24718
24928
  top10HostsShare: top10Share.toFixed(6),
24719
24929
  queriedAt: base.queriedAt,
24930
+ source: base.source,
24720
24931
  excludedLinkingDomains: Math.max(0, unfilteredLinkingDomains - totalLinkingDomains),
24721
24932
  excludedHosts: Math.max(0, unfilteredHosts - totalHosts)
24722
24933
  };
24723
24934
  }
24935
+ function buildSourceAvailability(db, projectId, source, connected, excludeCrawlers) {
24936
+ const summary = db.select().from(backlinkSummaries).where(and19(eq25(backlinkSummaries.projectId, projectId), eq25(backlinkSummaries.source, source))).orderBy(desc13(backlinkSummaries.queriedAt)).limit(1).get();
24937
+ let totalLinkingDomains = summary?.totalLinkingDomains ?? 0;
24938
+ if (summary && excludeCrawlers) {
24939
+ const filtered = db.select({ count: sql10`count(*)` }).from(backlinkDomains).where(and19(
24940
+ eq25(backlinkDomains.projectId, projectId),
24941
+ eq25(backlinkDomains.source, source),
24942
+ eq25(backlinkDomains.release, summary.release),
24943
+ backlinkCrawlerExclusionClause()
24944
+ )).get();
24945
+ totalLinkingDomains = Number(filtered?.count ?? 0);
24946
+ }
24947
+ return {
24948
+ source,
24949
+ connected,
24950
+ hasData: !!summary,
24951
+ latestRelease: summary?.release ?? null,
24952
+ totalLinkingDomains,
24953
+ lastSyncedAt: summary?.queriedAt ?? null
24954
+ };
24955
+ }
24956
+ function computeSourceAvailability(db, project, bingStore, excludeCrawlers) {
24957
+ const ccReadySync = db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq25(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
24958
+ const ccConnected = project.autoExtractBacklinks === true && !!ccReadySync;
24959
+ const bingConnected = !!bingStore?.getConnection(project.canonicalDomain);
24960
+ const sources = [
24961
+ buildSourceAvailability(db, project.id, BacklinkSources.commoncrawl, ccConnected, excludeCrawlers),
24962
+ buildSourceAvailability(db, project.id, BacklinkSources["bing-webmaster"], bingConnected, excludeCrawlers)
24963
+ ];
24964
+ return {
24965
+ projectId: project.id,
24966
+ targetDomain: project.canonicalDomain,
24967
+ sources,
24968
+ anyConnected: sources.some((s) => s.connected),
24969
+ anyData: sources.some((s) => s.hasData)
24970
+ };
24971
+ }
24724
24972
  async function backlinksRoutes(app, opts) {
24725
24973
  app.get("/backlinks/status", async (_request, reply) => {
24726
24974
  if (!opts.getBacklinksStatus) {
@@ -24852,7 +25100,8 @@ async function backlinksRoutes(app, opts) {
24852
25100
  "/projects/:name/backlinks/summary",
24853
25101
  async (request, reply) => {
24854
25102
  const project = resolveProject(app.db, request.params.name);
24855
- const row = latestSummaryForProject(app.db, project.id, request.query.release);
25103
+ const source = parseSourceParam(request.query.source);
25104
+ const row = latestSummaryForProject(app.db, project.id, source, request.query.release);
24856
25105
  if (!row) return reply.send(null);
24857
25106
  const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
24858
25107
  return reply.send(excludeCrawlers ? computeFilteredSummary(app.db, row) : mapSummaryRow(row));
@@ -24860,10 +25109,11 @@ async function backlinksRoutes(app, opts) {
24860
25109
  );
24861
25110
  app.get("/projects/:name/backlinks/domains", async (request, reply) => {
24862
25111
  const project = resolveProject(app.db, request.params.name);
24863
- const summaryRow = latestSummaryForProject(app.db, project.id, request.query.release);
25112
+ const source = parseSourceParam(request.query.source);
25113
+ const summaryRow = latestSummaryForProject(app.db, project.id, source, request.query.release);
24864
25114
  const targetRelease = request.query.release ?? summaryRow?.release;
24865
25115
  if (!targetRelease) {
24866
- const response2 = { summary: null, total: 0, rows: [] };
25116
+ const response2 = { source, summary: null, total: 0, rows: [] };
24867
25117
  return reply.send(response2);
24868
25118
  }
24869
25119
  const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
@@ -24871,19 +25121,22 @@ async function backlinksRoutes(app, opts) {
24871
25121
  const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
24872
25122
  const baseDomainCondition = and19(
24873
25123
  eq25(backlinkDomains.projectId, project.id),
25124
+ eq25(backlinkDomains.source, source),
24874
25125
  eq25(backlinkDomains.release, targetRelease)
24875
25126
  );
24876
25127
  const domainCondition = excludeCrawlers ? and19(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
24877
25128
  const totalRow = app.db.select({ count: sql10`count(*)` }).from(backlinkDomains).where(domainCondition).get();
24878
25129
  const rows = app.db.select({
24879
25130
  linkingDomain: backlinkDomains.linkingDomain,
24880
- numHosts: backlinkDomains.numHosts
25131
+ numHosts: backlinkDomains.numHosts,
25132
+ source: backlinkDomains.source
24881
25133
  }).from(backlinkDomains).where(domainCondition).orderBy(desc13(backlinkDomains.numHosts)).limit(limit).offset(offset).all();
24882
25134
  let summary = null;
24883
25135
  if (summaryRow) {
24884
25136
  summary = excludeCrawlers ? computeFilteredSummary(app.db, summaryRow) : mapSummaryRow(summaryRow);
24885
25137
  }
24886
25138
  const response = {
25139
+ source,
24887
25140
  summary,
24888
25141
  total: Number(totalRow?.count ?? 0),
24889
25142
  rows
@@ -24894,17 +25147,58 @@ async function backlinksRoutes(app, opts) {
24894
25147
  "/projects/:name/backlinks/history",
24895
25148
  async (request, reply) => {
24896
25149
  const project = resolveProject(app.db, request.params.name);
24897
- const rows = app.db.select().from(backlinkSummaries).where(eq25(backlinkSummaries.projectId, project.id)).orderBy(asc3(backlinkSummaries.queriedAt)).all();
25150
+ const source = parseSourceParam(request.query.source);
25151
+ const rows = app.db.select().from(backlinkSummaries).where(and19(eq25(backlinkSummaries.projectId, project.id), eq25(backlinkSummaries.source, source))).orderBy(asc3(backlinkSummaries.queriedAt)).all();
24898
25152
  const response = rows.map((r) => ({
24899
25153
  release: r.release,
24900
25154
  totalLinkingDomains: r.totalLinkingDomains,
24901
25155
  totalHosts: r.totalHosts,
24902
25156
  top10HostsShare: r.top10HostsShare,
24903
- queriedAt: r.queriedAt
25157
+ queriedAt: r.queriedAt,
25158
+ source: r.source
24904
25159
  }));
24905
25160
  return reply.send(response);
24906
25161
  }
24907
25162
  );
25163
+ app.get(
25164
+ "/projects/:name/backlinks/sources",
25165
+ async (request, reply) => {
25166
+ const project = resolveProject(app.db, request.params.name);
25167
+ const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
25168
+ const response = computeSourceAvailability(app.db, project, opts.bingConnectionStore, excludeCrawlers);
25169
+ return reply.send(response);
25170
+ }
25171
+ );
25172
+ app.post(
25173
+ "/projects/:name/backlinks/bing-sync",
25174
+ async (request, reply) => {
25175
+ const project = resolveProject(app.db, request.params.name);
25176
+ if (!opts.onBingBacklinkSyncRequested) {
25177
+ throw missingDependency(
25178
+ "Bing backlinks sync is only available from a local canonry install with Bing Webmaster connected."
25179
+ );
25180
+ }
25181
+ const conn = opts.bingConnectionStore?.getConnection(project.canonicalDomain);
25182
+ if (!conn) {
25183
+ throw validationError(
25184
+ `No Bing Webmaster connection for "${project.name}". Run \`canonry bing connect ${project.name} --api-key <key>\` first.`
25185
+ );
25186
+ }
25187
+ const now = (/* @__PURE__ */ new Date()).toISOString();
25188
+ const runId = crypto22.randomUUID();
25189
+ app.db.insert(runs).values({
25190
+ id: runId,
25191
+ projectId: project.id,
25192
+ kind: RunKinds["backlink-extract"],
25193
+ status: RunStatuses.queued,
25194
+ trigger: RunTriggers.manual,
25195
+ createdAt: now
25196
+ }).run();
25197
+ opts.onBingBacklinkSyncRequested(runId, project.id);
25198
+ const run = app.db.select().from(runs).where(eq25(runs.id, runId)).get();
25199
+ return reply.status(201).send(mapRunRow(run));
25200
+ }
25201
+ );
24908
25202
  }
24909
25203
 
24910
25204
  // ../api-routes/src/traffic.ts
@@ -30300,6 +30594,54 @@ function readInstalledManifest(skillDir) {
30300
30594
  }
30301
30595
  var AGENT_CHECKS = [skillsInstalledCheck, skillsCurrentCheck];
30302
30596
 
30597
+ // ../api-routes/src/doctor/checks/backlinks.ts
30598
+ import { and as and21, eq as eq27 } from "drizzle-orm";
30599
+ function skippedNoProject() {
30600
+ return {
30601
+ status: CheckStatuses.skipped,
30602
+ code: "backlinks.source.no-project",
30603
+ summary: "Project context required."
30604
+ };
30605
+ }
30606
+ var BACKLINKS_CHECKS = [
30607
+ {
30608
+ id: "backlinks.source.connected",
30609
+ category: CheckCategories.integrations,
30610
+ scope: CheckScopes.project,
30611
+ title: "Backlinks source connected",
30612
+ run: (ctx) => {
30613
+ if (!ctx.project) return skippedNoProject();
30614
+ const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(eq27(projects.id, ctx.project.id)).get();
30615
+ const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq27(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
30616
+ const ccConnected = projectRow?.autoExtract === true && !!readySync;
30617
+ const bingConnected = !!ctx.bingConnectionStore?.getConnection(ctx.project.canonicalDomain);
30618
+ const connected = [];
30619
+ if (ccConnected) connected.push(BacklinkSources.commoncrawl);
30620
+ if (bingConnected) connected.push(BacklinkSources["bing-webmaster"]);
30621
+ if (connected.length === 0) {
30622
+ return {
30623
+ status: CheckStatuses.warn,
30624
+ code: "backlinks.source.none",
30625
+ summary: `No backlink source is set up for ${ctx.project.name}.`,
30626
+ remediation: `Enable Common Crawl (set autoExtractBacklinks on the project + run \`canonry backlinks sync\`) or connect Bing Webmaster (\`canonry bing connect ${ctx.project.name} --api-key <key>\` then \`canonry backlinks bing-sync ${ctx.project.name}\`).`,
30627
+ details: { commoncrawl: ccConnected, bingWebmaster: bingConnected }
30628
+ };
30629
+ }
30630
+ const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(and21(
30631
+ eq27(backlinkSummaries.projectId, ctx.project.id),
30632
+ eq27(backlinkSummaries.source, BacklinkSources.commoncrawl)
30633
+ )).limit(1).get() : false;
30634
+ return {
30635
+ status: CheckStatuses.ok,
30636
+ code: "backlinks.source.connected",
30637
+ summary: `${connected.length} backlink source${connected.length === 1 ? "" : "s"} set up: ${connected.join(", ")}.`,
30638
+ remediation: ccConnected && !ccHasData ? `Common Crawl is ready but no backlinks have been extracted for ${ctx.project.name} yet \u2014 run \`canonry backlinks extract ${ctx.project.name}\`.` : null,
30639
+ details: { commoncrawl: ccConnected, bingWebmaster: bingConnected, connected, commoncrawlHasData: ccHasData }
30640
+ };
30641
+ }
30642
+ }
30643
+ ];
30644
+
30303
30645
  // ../api-routes/src/doctor/checks/bing-auth.ts
30304
30646
  var BING_AUTH_CHECKS = [
30305
30647
  {
@@ -30447,10 +30789,10 @@ var BING_AUTH_CHECKS = [
30447
30789
  ];
30448
30790
 
30449
30791
  // ../api-routes/src/doctor/checks/content.ts
30450
- import { eq as eq27 } from "drizzle-orm";
30792
+ import { eq as eq28 } from "drizzle-orm";
30451
30793
  var WINNABILITY_COVERAGE_WARN_THRESHOLD = 0.8;
30452
30794
  var UNCLASSIFIED_DOMAIN_SAMPLE_LIMIT = 10;
30453
- function skippedNoProject() {
30795
+ function skippedNoProject2() {
30454
30796
  return {
30455
30797
  status: CheckStatuses.skipped,
30456
30798
  code: "content.winnability.no-project",
@@ -30460,7 +30802,7 @@ function skippedNoProject() {
30460
30802
  }
30461
30803
  function loadProject(ctx) {
30462
30804
  if (!ctx.project) return null;
30463
- return ctx.db.select().from(projects).where(eq27(projects.id, ctx.project.id)).get() ?? null;
30805
+ return ctx.db.select().from(projects).where(eq28(projects.id, ctx.project.id)).get() ?? null;
30464
30806
  }
30465
30807
  function percent(value) {
30466
30808
  return Math.round(value * 100);
@@ -30471,7 +30813,7 @@ var winnabilityCoverageCheck = {
30471
30813
  scope: CheckScopes.project,
30472
30814
  title: "Content winnability classification coverage",
30473
30815
  run: (ctx) => {
30474
- if (!ctx.project) return skippedNoProject();
30816
+ if (!ctx.project) return skippedNoProject2();
30475
30817
  const project = loadProject(ctx);
30476
30818
  if (!project) {
30477
30819
  return {
@@ -30552,7 +30894,7 @@ var CONTENT_CHECK_BY_ID = Object.fromEntries(
30552
30894
  );
30553
30895
 
30554
30896
  // ../api-routes/src/doctor/checks/ads.ts
30555
- import { eq as eq28 } from "drizzle-orm";
30897
+ import { eq as eq29 } from "drizzle-orm";
30556
30898
  var RECENT_SYNC_WARN_DAYS = 7;
30557
30899
  var RECENT_SYNC_FAIL_DAYS = 30;
30558
30900
  var adsConnectionCheck = {
@@ -30569,7 +30911,7 @@ var adsConnectionCheck = {
30569
30911
  remediation: null
30570
30912
  };
30571
30913
  }
30572
- const row = ctx.db.select().from(adsConnections).where(eq28(adsConnections.projectId, ctx.project.id)).get();
30914
+ const row = ctx.db.select().from(adsConnections).where(eq29(adsConnections.projectId, ctx.project.id)).get();
30573
30915
  if (!row) {
30574
30916
  return {
30575
30917
  status: CheckStatuses.skipped,
@@ -30619,7 +30961,7 @@ var adsRecentSyncCheck = {
30619
30961
  remediation: null
30620
30962
  };
30621
30963
  }
30622
- const row = ctx.db.select().from(adsConnections).where(eq28(adsConnections.projectId, ctx.project.id)).get();
30964
+ const row = ctx.db.select().from(adsConnections).where(eq29(adsConnections.projectId, ctx.project.id)).get();
30623
30965
  if (!row) {
30624
30966
  return {
30625
30967
  status: CheckStatuses.skipped,
@@ -30810,10 +31152,10 @@ var ga4ConnectionCheck = {
30810
31152
  var GA_AUTH_CHECKS = [ga4ConnectionCheck];
30811
31153
 
30812
31154
  // ../api-routes/src/doctor/checks/gbp-auth.ts
30813
- import { and as and21, eq as eq29 } from "drizzle-orm";
31155
+ import { and as and22, eq as eq30 } from "drizzle-orm";
30814
31156
  var RECENT_SYNC_WARN_DAYS2 = 7;
30815
31157
  var RECENT_SYNC_FAIL_DAYS2 = 30;
30816
- function skippedNoProject2() {
31158
+ function skippedNoProject3() {
30817
31159
  return {
30818
31160
  status: CheckStatuses.skipped,
30819
31161
  code: "gbp.auth.no-project",
@@ -30830,7 +31172,7 @@ function storeUnavailable() {
30830
31172
  };
30831
31173
  }
30832
31174
  async function resolveGbpToken(ctx) {
30833
- if (!ctx.project) return { ok: false, output: skippedNoProject2() };
31175
+ if (!ctx.project) return { ok: false, output: skippedNoProject3() };
30834
31176
  const store = ctx.googleConnectionStore;
30835
31177
  if (!store) return { ok: false, output: storeUnavailable() };
30836
31178
  const auth = ctx.getGoogleAuthConfig?.() ?? {};
@@ -30908,7 +31250,7 @@ var scopesCheck = {
30908
31250
  scope: CheckScopes.project,
30909
31251
  title: "GBP granted scopes",
30910
31252
  run: async (ctx) => {
30911
- if (!ctx.project) return skippedNoProject2();
31253
+ if (!ctx.project) return skippedNoProject3();
30912
31254
  const store = ctx.googleConnectionStore;
30913
31255
  if (!store) return storeUnavailable();
30914
31256
  const conn = store.getConnection(ctx.project.canonicalDomain, "gbp");
@@ -30945,7 +31287,7 @@ var accountAccessCheck = {
30945
31287
  scope: CheckScopes.project,
30946
31288
  title: "GBP account access",
30947
31289
  run: async (ctx) => {
30948
- if (!ctx.project) return skippedNoProject2();
31290
+ if (!ctx.project) return skippedNoProject3();
30949
31291
  const store = ctx.googleConnectionStore;
30950
31292
  if (!store) return storeUnavailable();
30951
31293
  const conn = store.getConnection(ctx.project.canonicalDomain, "gbp");
@@ -31042,8 +31384,8 @@ var recentSyncCheck = {
31042
31384
  scope: CheckScopes.project,
31043
31385
  title: "GBP recent sync",
31044
31386
  run: (ctx) => {
31045
- if (!ctx.project) return skippedNoProject2();
31046
- const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and21(eq29(gbpLocations.projectId, ctx.project.id), eq29(gbpLocations.selected, true))).all();
31387
+ if (!ctx.project) return skippedNoProject3();
31388
+ const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and22(eq30(gbpLocations.projectId, ctx.project.id), eq30(gbpLocations.selected, true))).all();
31047
31389
  if (selected.length === 0) {
31048
31390
  return {
31049
31391
  status: CheckStatuses.skipped,
@@ -31103,7 +31445,7 @@ var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
31103
31445
  );
31104
31446
 
31105
31447
  // ../api-routes/src/doctor/checks/places.ts
31106
- import { eq as eq30 } from "drizzle-orm";
31448
+ import { eq as eq31 } from "drizzle-orm";
31107
31449
  var apiKeyCheck = {
31108
31450
  id: "gbp.places.api-key",
31109
31451
  category: CheckCategories.auth,
@@ -31148,7 +31490,7 @@ var apiKeyCheck = {
31148
31490
  details: { tier: cfg.tier }
31149
31491
  };
31150
31492
  }
31151
- const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq30(gbpLocations.projectId, ctx.project.id)).all();
31493
+ const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq31(gbpLocations.projectId, ctx.project.id)).all();
31152
31494
  const selected = rows.filter((r) => r.selected);
31153
31495
  const locationsWithPlaceId = selected.filter((r) => Boolean(r.placeId)).length;
31154
31496
  const details = {
@@ -31184,7 +31526,7 @@ var PLACES_CHECK_BY_ID = Object.fromEntries(
31184
31526
  var REQUIRED_GSC_SCOPES = [GSC_SCOPE, INDEXING_SCOPE];
31185
31527
  async function resolveAccessToken(ctx) {
31186
31528
  if (!ctx.project) {
31187
- return { ok: false, output: skippedNoProject3() };
31529
+ return { ok: false, output: skippedNoProject4() };
31188
31530
  }
31189
31531
  const store = ctx.googleConnectionStore;
31190
31532
  if (!store) {
@@ -31251,7 +31593,7 @@ async function resolveAccessToken(ctx) {
31251
31593
  };
31252
31594
  }
31253
31595
  }
31254
- function skippedNoProject3() {
31596
+ function skippedNoProject4() {
31255
31597
  return {
31256
31598
  status: CheckStatuses.skipped,
31257
31599
  code: "google.auth.no-project",
@@ -31281,7 +31623,7 @@ var propertyAccessCheck = {
31281
31623
  scope: CheckScopes.project,
31282
31624
  title: "GSC property access",
31283
31625
  run: async (ctx) => {
31284
- if (!ctx.project) return skippedNoProject3();
31626
+ if (!ctx.project) return skippedNoProject4();
31285
31627
  const store = ctx.googleConnectionStore;
31286
31628
  if (!store) {
31287
31629
  return {
@@ -31382,7 +31724,7 @@ var redirectUriCheck = {
31382
31724
  scope: CheckScopes.project,
31383
31725
  title: "OAuth redirect URI",
31384
31726
  run: async (ctx) => {
31385
- if (!ctx.project) return skippedNoProject3();
31727
+ if (!ctx.project) return skippedNoProject4();
31386
31728
  const auth = ctx.getGoogleAuthConfig?.() ?? {};
31387
31729
  if (!auth.clientId || !auth.clientSecret) {
31388
31730
  return {
@@ -31436,7 +31778,7 @@ var scopesCheck2 = {
31436
31778
  scope: CheckScopes.project,
31437
31779
  title: "GSC granted scopes",
31438
31780
  run: async (ctx) => {
31439
- if (!ctx.project) return skippedNoProject3();
31781
+ if (!ctx.project) return skippedNoProject4();
31440
31782
  const store = ctx.googleConnectionStore;
31441
31783
  if (!store) {
31442
31784
  return {
@@ -31599,10 +31941,10 @@ var RUNTIME_STATE_CHECKS = [
31599
31941
  ];
31600
31942
 
31601
31943
  // ../api-routes/src/doctor/checks/traffic-source.ts
31602
- import { and as and22, eq as eq31, gte as gte5, ne as ne4, sql as sql12 } from "drizzle-orm";
31944
+ import { and as and23, eq as eq32, gte as gte5, ne as ne4, sql as sql12 } from "drizzle-orm";
31603
31945
  var RECENT_DATA_WARN_DAYS = 7;
31604
31946
  var RECENT_DATA_FAIL_DAYS = 30;
31605
- function skippedNoProject4() {
31947
+ function skippedNoProject5() {
31606
31948
  return {
31607
31949
  status: CheckStatuses.skipped,
31608
31950
  code: "traffic.no-project",
@@ -31613,8 +31955,8 @@ function skippedNoProject4() {
31613
31955
  function loadProbes(ctx) {
31614
31956
  if (!ctx.project) return [];
31615
31957
  const rows = ctx.db.select().from(trafficSources).where(
31616
- and22(
31617
- eq31(trafficSources.projectId, ctx.project.id),
31958
+ and23(
31959
+ eq32(trafficSources.projectId, ctx.project.id),
31618
31960
  ne4(trafficSources.status, TrafficSourceStatuses.archived)
31619
31961
  )
31620
31962
  ).all();
@@ -31636,7 +31978,7 @@ var sourceConnectedCheck = {
31636
31978
  scope: CheckScopes.project,
31637
31979
  title: "Traffic source connected",
31638
31980
  run: (ctx) => {
31639
- if (!ctx.project) return skippedNoProject4();
31981
+ if (!ctx.project) return skippedNoProject5();
31640
31982
  const sources = loadProbes(ctx);
31641
31983
  if (sources.length === 0) {
31642
31984
  return {
@@ -31680,7 +32022,7 @@ var recentDataCheck = {
31680
32022
  scope: CheckScopes.project,
31681
32023
  title: "Traffic source recent data",
31682
32024
  run: (ctx) => {
31683
- if (!ctx.project) return skippedNoProject4();
32025
+ if (!ctx.project) return skippedNoProject5();
31684
32026
  const sources = loadProbes(ctx);
31685
32027
  if (sources.length === 0) {
31686
32028
  return {
@@ -31694,16 +32036,16 @@ var recentDataCheck = {
31694
32036
  const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
31695
32037
  const recentCrawlers = Number(
31696
32038
  ctx.db.select({ total: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
31697
- and22(
31698
- eq31(crawlerEventsHourly.projectId, ctx.project.id),
32039
+ and23(
32040
+ eq32(crawlerEventsHourly.projectId, ctx.project.id),
31699
32041
  gte5(crawlerEventsHourly.tsHour, warnCutoff)
31700
32042
  )
31701
32043
  ).get()?.total ?? 0
31702
32044
  );
31703
32045
  const recentReferrals = Number(
31704
32046
  ctx.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
31705
- and22(
31706
- eq31(aiReferralEventsHourly.projectId, ctx.project.id),
32047
+ and23(
32048
+ eq32(aiReferralEventsHourly.projectId, ctx.project.id),
31707
32049
  gte5(aiReferralEventsHourly.tsHour, warnCutoff)
31708
32050
  )
31709
32051
  ).get()?.total ?? 0
@@ -31718,16 +32060,16 @@ var recentDataCheck = {
31718
32060
  }
31719
32061
  const olderCrawlers = Number(
31720
32062
  ctx.db.select({ total: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
31721
- and22(
31722
- eq31(crawlerEventsHourly.projectId, ctx.project.id),
32063
+ and23(
32064
+ eq32(crawlerEventsHourly.projectId, ctx.project.id),
31723
32065
  gte5(crawlerEventsHourly.tsHour, failCutoff)
31724
32066
  )
31725
32067
  ).get()?.total ?? 0
31726
32068
  );
31727
32069
  const olderReferrals = Number(
31728
32070
  ctx.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
31729
- and22(
31730
- eq31(aiReferralEventsHourly.projectId, ctx.project.id),
32071
+ and23(
32072
+ eq32(aiReferralEventsHourly.projectId, ctx.project.id),
31731
32073
  gte5(aiReferralEventsHourly.tsHour, failCutoff)
31732
32074
  )
31733
32075
  ).get()?.total ?? 0
@@ -31842,7 +32184,7 @@ var credentialsCheck = {
31842
32184
  scope: CheckScopes.project,
31843
32185
  title: "Traffic source credentials",
31844
32186
  run: async (ctx) => {
31845
- if (!ctx.project) return skippedNoProject4();
32187
+ if (!ctx.project) return skippedNoProject5();
31846
32188
  const sources = loadProbes(ctx);
31847
32189
  if (sources.length === 0) {
31848
32190
  return {
@@ -31871,7 +32213,7 @@ var scopesCheck3 = {
31871
32213
  scope: CheckScopes.project,
31872
32214
  title: "Traffic source scopes",
31873
32215
  run: async (ctx) => {
31874
- if (!ctx.project) return skippedNoProject4();
32216
+ if (!ctx.project) return skippedNoProject5();
31875
32217
  const sources = loadProbes(ctx);
31876
32218
  if (sources.length === 0) {
31877
32219
  return {
@@ -31900,7 +32242,7 @@ var cacheBlindSpotCheck = {
31900
32242
  scope: CheckScopes.project,
31901
32243
  title: "WordPress traffic cache blind spot",
31902
32244
  run: (ctx) => {
31903
- if (!ctx.project) return skippedNoProject4();
32245
+ if (!ctx.project) return skippedNoProject5();
31904
32246
  const wpSources = loadProbes(ctx).filter(
31905
32247
  (s) => s.sourceType === TrafficSourceTypes.wordpress
31906
32248
  );
@@ -32015,6 +32357,7 @@ var ALL_CHECKS = [
32015
32357
  ...ADS_CHECKS,
32016
32358
  ...PROVIDERS_CHECKS,
32017
32359
  ...TRAFFIC_SOURCE_CHECKS,
32360
+ ...BACKLINKS_CHECKS,
32018
32361
  ...CONTENT_CHECKS,
32019
32362
  ...AGENT_CHECKS
32020
32363
  ];
@@ -32140,7 +32483,7 @@ async function doctorRoutes(app, opts) {
32140
32483
 
32141
32484
  // ../api-routes/src/discovery/routes.ts
32142
32485
  import crypto26 from "crypto";
32143
- import { and as and23, desc as desc15, eq as eq32, gte as gte6, inArray as inArray11 } from "drizzle-orm";
32486
+ import { and as and24, desc as desc15, eq as eq33, gte as gte6, inArray as inArray11 } from "drizzle-orm";
32144
32487
  var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
32145
32488
  async function discoveryRoutes(app, opts) {
32146
32489
  app.post("/projects/:name/discover/run", async (request, reply) => {
@@ -32172,9 +32515,9 @@ async function discoveryRoutes(app, opts) {
32172
32515
  const now = (/* @__PURE__ */ new Date()).toISOString();
32173
32516
  const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
32174
32517
  const decision = app.db.transaction((tx) => {
32175
- const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and23(
32176
- eq32(discoverySessions.projectId, project.id),
32177
- eq32(discoverySessions.icpDescription, icpDescription),
32518
+ const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and24(
32519
+ eq33(discoverySessions.projectId, project.id),
32520
+ eq33(discoverySessions.icpDescription, icpDescription),
32178
32521
  inArray11(discoverySessions.status, [
32179
32522
  DiscoverySessionStatuses.queued,
32180
32523
  DiscoverySessionStatuses.seeding,
@@ -32244,7 +32587,7 @@ async function discoveryRoutes(app, opts) {
32244
32587
  const project = resolveProject(app.db, request.params.name);
32245
32588
  const parsedLimit = parseInt(request.query.limit ?? "", 10);
32246
32589
  const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? 50 : parsedLimit;
32247
- const rows = app.db.select().from(discoverySessions).where(eq32(discoverySessions.projectId, project.id)).orderBy(desc15(discoverySessions.createdAt)).limit(limit).all();
32590
+ const rows = app.db.select().from(discoverySessions).where(eq33(discoverySessions.projectId, project.id)).orderBy(desc15(discoverySessions.createdAt)).limit(limit).all();
32248
32591
  return reply.send(rows.map(serializeSession));
32249
32592
  }
32250
32593
  );
@@ -32252,11 +32595,11 @@ async function discoveryRoutes(app, opts) {
32252
32595
  "/projects/:name/discover/sessions/:id",
32253
32596
  async (request, reply) => {
32254
32597
  const project = resolveProject(app.db, request.params.name);
32255
- const session = app.db.select().from(discoverySessions).where(eq32(discoverySessions.id, request.params.id)).get();
32598
+ const session = app.db.select().from(discoverySessions).where(eq33(discoverySessions.id, request.params.id)).get();
32256
32599
  if (!session || session.projectId !== project.id) {
32257
32600
  throw notFound("Discovery session", request.params.id);
32258
32601
  }
32259
- const probeRows = app.db.select().from(discoveryProbes).where(eq32(discoveryProbes.sessionId, session.id)).all();
32602
+ const probeRows = app.db.select().from(discoveryProbes).where(eq33(discoveryProbes.sessionId, session.id)).all();
32260
32603
  const detail = {
32261
32604
  ...serializeSession(session),
32262
32605
  probes: probeRows.map(serializeProbe)
@@ -32268,12 +32611,12 @@ async function discoveryRoutes(app, opts) {
32268
32611
  "/projects/:name/discover/sessions/:id/promote",
32269
32612
  async (request, reply) => {
32270
32613
  const project = resolveProject(app.db, request.params.name);
32271
- const session = app.db.select().from(discoverySessions).where(eq32(discoverySessions.id, request.params.id)).get();
32614
+ const session = app.db.select().from(discoverySessions).where(eq33(discoverySessions.id, request.params.id)).get();
32272
32615
  if (!session || session.projectId !== project.id) {
32273
32616
  throw notFound("Discovery session", request.params.id);
32274
32617
  }
32275
- const probeRows = app.db.select().from(discoveryProbes).where(eq32(discoveryProbes.sessionId, session.id)).all();
32276
- const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq32(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase());
32618
+ const probeRows = app.db.select().from(discoveryProbes).where(eq33(discoveryProbes.sessionId, session.id)).all();
32619
+ const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq33(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase());
32277
32620
  const seenCompetitors = new Set(existingCompetitors);
32278
32621
  const cited = /* @__PURE__ */ new Set();
32279
32622
  const aspirational = /* @__PURE__ */ new Set();
@@ -32302,7 +32645,7 @@ async function discoveryRoutes(app, opts) {
32302
32645
  );
32303
32646
  app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
32304
32647
  const project = resolveProject(app.db, request.params.name);
32305
- const session = app.db.select().from(discoverySessions).where(eq32(discoverySessions.id, request.params.id)).get();
32648
+ const session = app.db.select().from(discoverySessions).where(eq33(discoverySessions.id, request.params.id)).get();
32306
32649
  if (!session || session.projectId !== project.id) {
32307
32650
  throw notFound("Discovery session", request.params.id);
32308
32651
  }
@@ -32325,7 +32668,7 @@ async function discoveryRoutes(app, opts) {
32325
32668
  const bucketSet = new Set(buckets);
32326
32669
  const includeCompetitors = parsed.data.includeCompetitors ?? true;
32327
32670
  const competitorTypes = parsed.data.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES;
32328
- const probeRows = app.db.select().from(discoveryProbes).where(eq32(discoveryProbes.sessionId, session.id)).all();
32671
+ const probeRows = app.db.select().from(discoveryProbes).where(eq33(discoveryProbes.sessionId, session.id)).all();
32329
32672
  const candidateQueries = /* @__PURE__ */ new Set();
32330
32673
  for (const probe of probeRows) {
32331
32674
  if (!probe.bucket) continue;
@@ -32333,7 +32676,7 @@ async function discoveryRoutes(app, opts) {
32333
32676
  if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
32334
32677
  }
32335
32678
  const existingQueries = new Set(
32336
- app.db.select({ query: queries.query }).from(queries).where(eq32(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
32679
+ app.db.select({ query: queries.query }).from(queries).where(eq33(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
32337
32680
  );
32338
32681
  const promotedQueries = [];
32339
32682
  const skippedQueries = [];
@@ -32349,7 +32692,7 @@ async function discoveryRoutes(app, opts) {
32349
32692
  const skippedCompetitors = [];
32350
32693
  if (includeCompetitors) {
32351
32694
  const existingCompetitors = new Set(
32352
- app.db.select({ domain: competitors.domain }).from(competitors).where(eq32(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
32695
+ app.db.select({ domain: competitors.domain }).from(competitors).where(eq33(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
32353
32696
  );
32354
32697
  const competitorMap = parseCompetitorMap(session.competitorMap);
32355
32698
  for (const entry of selectEligibleCompetitors(competitorMap, competitorTypes)) {
@@ -32453,7 +32796,7 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
32453
32796
 
32454
32797
  // ../api-routes/src/discovery/orchestrate.ts
32455
32798
  import crypto27 from "crypto";
32456
- import { eq as eq33 } from "drizzle-orm";
32799
+ import { eq as eq34 } from "drizzle-orm";
32457
32800
  var DEFAULT_MAX_PROBES = 100;
32458
32801
  var ABSOLUTE_MAX_PROBES = 500;
32459
32802
  function classifyProbeBucket(input) {
@@ -32507,7 +32850,7 @@ async function executeDiscovery(opts) {
32507
32850
  status: DiscoverySessionStatuses.seeding,
32508
32851
  dedupThreshold,
32509
32852
  startedAt
32510
- }).where(eq33(discoverySessions.id, opts.sessionId)).run();
32853
+ }).where(eq34(discoverySessions.id, opts.sessionId)).run();
32511
32854
  const seedResult = await opts.deps.seed({
32512
32855
  project: opts.project,
32513
32856
  icpDescription: opts.icpDescription,
@@ -32533,7 +32876,7 @@ async function executeDiscovery(opts) {
32533
32876
  seedCountRaw,
32534
32877
  seedCount,
32535
32878
  warning
32536
- }).where(eq33(discoverySessions.id, opts.sessionId)).run();
32879
+ }).where(eq34(discoverySessions.id, opts.sessionId)).run();
32537
32880
  const probeRows = [];
32538
32881
  const buckets = { cited: 0, aspirational: 0, "wasted-surface": 0 };
32539
32882
  for (const query of probedCanonicals) {
@@ -32573,7 +32916,7 @@ async function executeDiscovery(opts) {
32573
32916
  wastedCount: buckets["wasted-surface"],
32574
32917
  competitorMap,
32575
32918
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
32576
- }).where(eq33(discoverySessions.id, opts.sessionId)).run();
32919
+ }).where(eq34(discoverySessions.id, opts.sessionId)).run();
32577
32920
  upsertDomainClassifications(opts.db, opts.project.id, opts.sessionId, competitorMap);
32578
32921
  return {
32579
32922
  buckets,
@@ -32613,7 +32956,7 @@ function markSessionFailed(db, sessionId, error) {
32613
32956
  status: DiscoverySessionStatuses.failed,
32614
32957
  error,
32615
32958
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
32616
- }).where(eq33(discoverySessions.id, sessionId)).run();
32959
+ }).where(eq34(discoverySessions.id, sessionId)).run();
32617
32960
  }
32618
32961
  function dedupeStrings(input) {
32619
32962
  const seen = /* @__PURE__ */ new Set();
@@ -32631,7 +32974,7 @@ function dedupeStrings(input) {
32631
32974
 
32632
32975
  // ../api-routes/src/technical-aeo.ts
32633
32976
  import crypto28 from "crypto";
32634
- import { and as and24, asc as asc4, count, desc as desc16, eq as eq34, inArray as inArray12 } from "drizzle-orm";
32977
+ import { and as and25, asc as asc4, count, desc as desc16, eq as eq35, inArray as inArray12 } from "drizzle-orm";
32635
32978
  var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
32636
32979
  function emptyScore(projectName) {
32637
32980
  return {
@@ -32663,9 +33006,9 @@ function parsePositiveInt(value, fallback, max) {
32663
33006
  async function technicalAeoRoutes(app, opts) {
32664
33007
  app.get("/projects/:name/technical-aeo", async (request) => {
32665
33008
  const project = resolveProject(app.db, request.params.name);
32666
- const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq34(siteAuditSnapshots.runId, runs.id)).where(and24(
32667
- eq34(siteAuditSnapshots.projectId, project.id),
32668
- eq34(runs.kind, RunKinds["site-audit"]),
33009
+ const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq35(siteAuditSnapshots.runId, runs.id)).where(and25(
33010
+ eq35(siteAuditSnapshots.projectId, project.id),
33011
+ eq35(runs.kind, RunKinds["site-audit"]),
32669
33012
  inArray12(runs.status, SURFACEABLE_STATUSES),
32670
33013
  notProbeRun()
32671
33014
  )).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(2).all();
@@ -32698,9 +33041,9 @@ async function technicalAeoRoutes(app, opts) {
32698
33041
  });
32699
33042
  app.get("/projects/:name/technical-aeo/pages", async (request) => {
32700
33043
  const project = resolveProject(app.db, request.params.name);
32701
- const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq34(siteAuditSnapshots.runId, runs.id)).where(and24(
32702
- eq34(siteAuditSnapshots.projectId, project.id),
32703
- eq34(runs.kind, RunKinds["site-audit"]),
33044
+ const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq35(siteAuditSnapshots.runId, runs.id)).where(and25(
33045
+ eq35(siteAuditSnapshots.projectId, project.id),
33046
+ eq35(runs.kind, RunKinds["site-audit"]),
32704
33047
  inArray12(runs.status, SURFACEABLE_STATUSES),
32705
33048
  notProbeRun()
32706
33049
  )).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(1).get();
@@ -32708,9 +33051,9 @@ async function technicalAeoRoutes(app, opts) {
32708
33051
  return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
32709
33052
  }
32710
33053
  const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
32711
- const conds = [eq34(siteAuditPages.runId, latest.runId)];
32712
- if (statusFilter) conds.push(eq34(siteAuditPages.status, statusFilter));
32713
- const where = and24(...conds);
33054
+ const conds = [eq35(siteAuditPages.runId, latest.runId)];
33055
+ if (statusFilter) conds.push(eq35(siteAuditPages.status, statusFilter));
33056
+ const where = and25(...conds);
32714
33057
  const totalRow = app.db.select({ value: count() }).from(siteAuditPages).where(where).get();
32715
33058
  const total = totalRow?.value ?? 0;
32716
33059
  const limit = parsePositiveInt(request.query.limit, 100, 500);
@@ -32734,9 +33077,9 @@ async function technicalAeoRoutes(app, opts) {
32734
33077
  auditedAt: siteAuditSnapshots.auditedAt,
32735
33078
  aggregateScore: siteAuditSnapshots.aggregateScore,
32736
33079
  pagesAudited: siteAuditSnapshots.pagesAudited
32737
- }).from(siteAuditSnapshots).innerJoin(runs, eq34(siteAuditSnapshots.runId, runs.id)).where(and24(
32738
- eq34(siteAuditSnapshots.projectId, project.id),
32739
- eq34(runs.kind, RunKinds["site-audit"]),
33080
+ }).from(siteAuditSnapshots).innerJoin(runs, eq35(siteAuditSnapshots.runId, runs.id)).where(and25(
33081
+ eq35(siteAuditSnapshots.projectId, project.id),
33082
+ eq35(runs.kind, RunKinds["site-audit"]),
32740
33083
  inArray12(runs.status, SURFACEABLE_STATUSES),
32741
33084
  notProbeRun()
32742
33085
  )).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(limit).all();
@@ -32748,9 +33091,9 @@ async function technicalAeoRoutes(app, opts) {
32748
33091
  if (!parsed.success) {
32749
33092
  throw validationError(parsed.error.issues[0]?.message ?? "Invalid site-audit request");
32750
33093
  }
32751
- const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and24(
32752
- eq34(runs.projectId, project.id),
32753
- eq34(runs.kind, RunKinds["site-audit"]),
33094
+ const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and25(
33095
+ eq35(runs.projectId, project.id),
33096
+ eq35(runs.kind, RunKinds["site-audit"]),
32754
33097
  inArray12(runs.status, [RunStatuses.queued, RunStatuses.running])
32755
33098
  )).get();
32756
33099
  if (existing) {
@@ -32937,6 +33280,8 @@ async function apiRoutes(app, opts) {
32937
33280
  onInstallBacklinks: opts.onInstallBacklinks,
32938
33281
  onReleaseSyncRequested: opts.onReleaseSyncRequested,
32939
33282
  onBacklinkExtractRequested: opts.onBacklinkExtractRequested,
33283
+ onBingBacklinkSyncRequested: opts.onBingBacklinkSyncRequested,
33284
+ bingConnectionStore: opts.bingConnectionStore,
32940
33285
  onBacklinksPruneCache: opts.onBacklinksPruneCache,
32941
33286
  listCachedReleases: opts.listCachedReleases,
32942
33287
  discoverLatestRelease: opts.discoverLatestRelease
@@ -33335,9 +33680,9 @@ var IntelligenceService = class {
33335
33680
  */
33336
33681
  analyzeAndPersist(runId, projectId) {
33337
33682
  const recentRuns = this.db.select().from(runs).where(
33338
- and25(
33339
- eq35(runs.projectId, projectId),
33340
- or5(eq35(runs.status, "completed"), eq35(runs.status, "partial")),
33683
+ and26(
33684
+ eq36(runs.projectId, projectId),
33685
+ or5(eq36(runs.status, "completed"), eq36(runs.status, "partial")),
33341
33686
  // Defensive: RunCoordinator already skips probes before this is
33342
33687
  // called, but if a future call site invokes analyzeAndPersist
33343
33688
  // directly for a probe, probes still must not pollute the
@@ -33419,7 +33764,7 @@ var IntelligenceService = class {
33419
33764
  * Returns the persisted insights so the coordinator can count critical/high.
33420
33765
  */
33421
33766
  analyzeAndPersistGbp(runId, projectId) {
33422
- const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq35(runs.id, runId)).get();
33767
+ const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq36(runs.id, runId)).get();
33423
33768
  if (!runRow) {
33424
33769
  log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
33425
33770
  this.persistGbpInsights(runId, projectId, [], []);
@@ -33427,9 +33772,9 @@ var IntelligenceService = class {
33427
33772
  }
33428
33773
  const windowStart = runRow.startedAt ?? runRow.createdAt;
33429
33774
  const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
33430
- const selected = this.db.select().from(gbpLocations).where(and25(
33431
- eq35(gbpLocations.projectId, projectId),
33432
- eq35(gbpLocations.selected, true),
33775
+ const selected = this.db.select().from(gbpLocations).where(and26(
33776
+ eq36(gbpLocations.projectId, projectId),
33777
+ eq36(gbpLocations.selected, true),
33433
33778
  gte7(gbpLocations.syncedAt, windowStart),
33434
33779
  lte4(gbpLocations.syncedAt, windowEnd)
33435
33780
  )).all();
@@ -33464,10 +33809,10 @@ var IntelligenceService = class {
33464
33809
  }
33465
33810
  /** Build the per-location signal bundle the GBP analyzer consumes. */
33466
33811
  buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
33467
- const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and25(eq35(gbpDailyMetrics.projectId, projectId), eq35(gbpDailyMetrics.locationName, locationName))).all();
33468
- const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and25(eq35(gbpPlaceActions.projectId, projectId), eq35(gbpPlaceActions.locationName, locationName))).all();
33469
- const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and25(eq35(gbpLodgingSnapshots.projectId, projectId), eq35(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc17(gbpLodgingSnapshots.syncedAt)).limit(1).get();
33470
- const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and25(eq35(gbpPlaceDetails.projectId, projectId), eq35(gbpPlaceDetails.locationName, locationName))).orderBy(desc17(gbpPlaceDetails.syncedAt)).limit(1).get();
33812
+ const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and26(eq36(gbpDailyMetrics.projectId, projectId), eq36(gbpDailyMetrics.locationName, locationName))).all();
33813
+ const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and26(eq36(gbpPlaceActions.projectId, projectId), eq36(gbpPlaceActions.locationName, locationName))).all();
33814
+ const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and26(eq36(gbpLodgingSnapshots.projectId, projectId), eq36(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc17(gbpLodgingSnapshots.syncedAt)).limit(1).get();
33815
+ const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and26(eq36(gbpPlaceDetails.projectId, projectId), eq36(gbpPlaceDetails.locationName, locationName))).orderBy(desc17(gbpPlaceDetails.syncedAt)).limit(1).get();
33471
33816
  const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
33472
33817
  const summary = buildGbpSummary({
33473
33818
  locationName,
@@ -33499,7 +33844,7 @@ var IntelligenceService = class {
33499
33844
  /** Build the month-over-month keyword series for a location from the
33500
33845
  * accumulating gbp_keyword_monthly table (latest complete month vs prior). */
33501
33846
  buildGbpKeywordTrend(projectId, locationName) {
33502
- const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and25(eq35(gbpKeywordMonthly.projectId, projectId), eq35(gbpKeywordMonthly.locationName, locationName))).all();
33847
+ const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and26(eq36(gbpKeywordMonthly.projectId, projectId), eq36(gbpKeywordMonthly.locationName, locationName))).all();
33503
33848
  if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
33504
33849
  const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
33505
33850
  const recentMonth = months[0] ?? null;
@@ -33530,7 +33875,7 @@ var IntelligenceService = class {
33530
33875
  */
33531
33876
  persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
33532
33877
  const covered = new Set(coveredLocationNames);
33533
- const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and25(eq35(insights.projectId, projectId), eq35(insights.provider, GBP_INSIGHT_PROVIDER))).all();
33878
+ const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and26(eq36(insights.projectId, projectId), eq36(insights.provider, GBP_INSIGHT_PROVIDER))).all();
33534
33879
  const staleIds = [];
33535
33880
  const dismissedSlots = /* @__PURE__ */ new Set();
33536
33881
  for (const row of existing) {
@@ -33541,7 +33886,7 @@ var IntelligenceService = class {
33541
33886
  }
33542
33887
  this.db.transaction((tx) => {
33543
33888
  for (const id of staleIds) {
33544
- tx.delete(insights).where(eq35(insights.id, id)).run();
33889
+ tx.delete(insights).where(eq36(insights.id, id)).run();
33545
33890
  }
33546
33891
  for (const insight of gbpInsights) {
33547
33892
  const parsed = parseGbpInsightId(insight.id);
@@ -33619,7 +33964,7 @@ var IntelligenceService = class {
33619
33964
  * create per run + aggregate). DB is left untouched.
33620
33965
  */
33621
33966
  backfill(projectName, opts, onProgress) {
33622
- const project = this.db.select().from(projects).where(eq35(projects.name, projectName)).get();
33967
+ const project = this.db.select().from(projects).where(eq36(projects.name, projectName)).get();
33623
33968
  if (!project) {
33624
33969
  throw new Error(`Project "${projectName}" not found`);
33625
33970
  }
@@ -33632,9 +33977,9 @@ var IntelligenceService = class {
33632
33977
  sinceTimestamp = parsed;
33633
33978
  }
33634
33979
  const allRuns = this.db.select().from(runs).where(
33635
- and25(
33636
- eq35(runs.projectId, project.id),
33637
- or5(eq35(runs.status, "completed"), eq35(runs.status, "partial")),
33980
+ and26(
33981
+ eq36(runs.projectId, project.id),
33982
+ or5(eq36(runs.status, "completed"), eq36(runs.status, "partial")),
33638
33983
  // Backfill must not replay probe runs as if they were real sweeps.
33639
33984
  ne5(runs.trigger, RunTriggers.probe)
33640
33985
  )
@@ -33713,7 +34058,7 @@ var IntelligenceService = class {
33713
34058
  return { processed, skipped, totalInsights };
33714
34059
  }
33715
34060
  loadTrackedCompetitors(projectId) {
33716
- return this.db.select({ domain: competitors.domain }).from(competitors).where(eq35(competitors.projectId, projectId)).all().map((r) => r.domain);
34061
+ return this.db.select({ domain: competitors.domain }).from(competitors).where(eq36(competitors.projectId, projectId)).all().map((r) => r.domain);
33717
34062
  }
33718
34063
  /**
33719
34064
  * Wipe transition signals from an analysis result while keeping health.
@@ -33734,15 +34079,15 @@ var IntelligenceService = class {
33734
34079
  }
33735
34080
  persistResult(result, runId, projectId) {
33736
34081
  const previouslyDismissed = /* @__PURE__ */ new Set();
33737
- const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq35(insights.runId, runId)).all();
34082
+ const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq36(insights.runId, runId)).all();
33738
34083
  for (const row of existingInsights) {
33739
34084
  if (row.dismissed) {
33740
34085
  previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
33741
34086
  }
33742
34087
  }
33743
34088
  this.db.transaction((tx) => {
33744
- tx.delete(insights).where(eq35(insights.runId, runId)).run();
33745
- tx.delete(healthSnapshots).where(eq35(healthSnapshots.runId, runId)).run();
34089
+ tx.delete(insights).where(eq36(insights.runId, runId)).run();
34090
+ tx.delete(healthSnapshots).where(eq36(healthSnapshots.runId, runId)).run();
33746
34091
  const now = (/* @__PURE__ */ new Date()).toISOString();
33747
34092
  for (const insight of result.insights) {
33748
34093
  const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
@@ -33793,24 +34138,24 @@ var IntelligenceService = class {
33793
34138
  applySeverityTiering(rawInsights, excludeRunId, projectId) {
33794
34139
  const regressions = rawInsights.filter((i) => i.type === "regression");
33795
34140
  if (regressions.length === 0) return rawInsights;
33796
- const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq35(gscSearchData.projectId, projectId)).all();
34141
+ const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq36(gscSearchData.projectId, projectId)).all();
33797
34142
  const gscConnected = gscRows.length > 0;
33798
34143
  const gscImpressionsByQuery = /* @__PURE__ */ new Map();
33799
34144
  for (const row of gscRows) {
33800
34145
  const key = row.query.toLowerCase();
33801
34146
  gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
33802
34147
  }
33803
- const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq35(projects.id, projectId)).get();
34148
+ const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq36(projects.id, projectId)).get();
33804
34149
  const locationCount = Math.max(
33805
34150
  1,
33806
34151
  (projectRow?.locations ?? []).length
33807
34152
  );
33808
34153
  const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
33809
34154
  const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
33810
- and25(
33811
- eq35(runs.projectId, projectId),
33812
- eq35(runs.kind, RunKinds["answer-visibility"]),
33813
- or5(eq35(runs.status, "completed"), eq35(runs.status, "partial")),
34155
+ and26(
34156
+ eq36(runs.projectId, projectId),
34157
+ eq36(runs.kind, RunKinds["answer-visibility"]),
34158
+ or5(eq36(runs.status, "completed"), eq36(runs.status, "partial")),
33814
34159
  // Defensive — see top of file.
33815
34160
  ne5(runs.trigger, RunTriggers.probe)
33816
34161
  )
@@ -33830,7 +34175,7 @@ var IntelligenceService = class {
33830
34175
  const haveHistory = recentRunIds.length > 0;
33831
34176
  const priorRegressionsByPair = /* @__PURE__ */ new Map();
33832
34177
  if (haveHistory) {
33833
- const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and25(eq35(insights.type, "regression"), inArray13(insights.runId, recentRunIds))).all();
34178
+ const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and26(eq36(insights.type, "regression"), inArray13(insights.runId, recentRunIds))).all();
33834
34179
  const regressionGroups = /* @__PURE__ */ new Map();
33835
34180
  for (const row of priorRows) {
33836
34181
  if (!row.runId) continue;
@@ -33859,7 +34204,7 @@ var IntelligenceService = class {
33859
34204
  });
33860
34205
  }
33861
34206
  buildRunData(runId, projectId, completedAt, location = null) {
33862
- const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq35(projects.id, projectId)).get();
34207
+ const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq36(projects.id, projectId)).get();
33863
34208
  const projectDomains = projectDomainRow ? effectiveDomains({
33864
34209
  canonicalDomain: projectDomainRow.canonicalDomain,
33865
34210
  ownedDomains: projectDomainRow.ownedDomains
@@ -33875,7 +34220,7 @@ var IntelligenceService = class {
33875
34220
  citedDomains: querySnapshots.citedDomains,
33876
34221
  competitorOverlap: querySnapshots.competitorOverlap,
33877
34222
  snapshotLocation: querySnapshots.location
33878
- }).from(querySnapshots).leftJoin(queries, eq35(querySnapshots.queryId, queries.id)).where(eq35(querySnapshots.runId, runId)).all();
34223
+ }).from(querySnapshots).leftJoin(queries, eq36(querySnapshots.queryId, queries.id)).where(eq36(querySnapshots.runId, runId)).all();
33879
34224
  const snapshots = [];
33880
34225
  let orphanCount = 0;
33881
34226
  for (const r of rows) {
@@ -33987,6 +34332,8 @@ export {
33987
34332
  hashLodging,
33988
34333
  getUrlInfo,
33989
34334
  getCrawlIssues,
34335
+ getLinkCounts,
34336
+ getUrlLinks,
33990
34337
  PLUGIN_DIR,
33991
34338
  DUCKDB_SPEC,
33992
34339
  CC_CACHE_DIR,