@canonry/canonry 7.77.2 → 7.78.2

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 (139) hide show
  1. package/assets/agent-workspace/skills/aero/references/seo-analysis.md +13 -2
  2. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +4 -2
  3. package/assets/assets/{AgentSetupPage-BZXWC3x6.js → AgentSetupPage-BGdvyQFL.js} +4 -4
  4. package/assets/assets/{AuditHistoryPanel-DcrUuwRu.js → AuditHistoryPanel-Bb5wq-Bk.js} +1 -1
  5. package/assets/assets/{BacklinksPage-BSpHJBiy.js → BacklinksPage-i1cfxTDj.js} +1 -1
  6. package/assets/assets/{HistoryPage-0a0CK9zF.js → HistoryPage-C2NRdNrn.js} +1 -1
  7. package/assets/assets/MeasurementPropertyPage-CwPuJMBy.js +4 -0
  8. package/assets/assets/ProjectPage-Bbnbg7c9.js +11 -0
  9. package/assets/assets/{RunsPage-D0vbz1Ho.js → RunsPage-CJHJ7kHk.js} +1 -1
  10. package/assets/assets/{SettingsPage-DjRm5cmn.js → SettingsPage-r8VrtFyJ.js} +1 -1
  11. package/assets/assets/SiteHealthSection-D1oSuo9O.js +2 -0
  12. package/assets/assets/ToneBadge-CUe-WrzN.js +60 -0
  13. package/assets/assets/{TrafficPage-Z8Z_z0k9.js → TrafficPage-DY2ykwH1.js} +1 -1
  14. package/assets/assets/TrafficSourceDetailPage-D3KHgRvF.js +1 -0
  15. package/assets/assets/{account-context-zaSxLBst.js → account-context-B50QqDEA.js} +1 -1
  16. package/assets/assets/{card-COqALGzB.js → card-BE2fqwVq.js} +1 -1
  17. package/assets/assets/{extract-error-message-DKve0SNr.js → extract-error-message-xuN_xeRJ.js} +1 -1
  18. package/assets/assets/{format-helpers-DKrHlaV7.js → format-helpers-CiC6gALY.js} +1 -1
  19. package/assets/assets/{index-CKZK95pI.css → index-CHHZDc6d.css} +1 -1
  20. package/assets/assets/{index-fzqtwr6m.js → index-Dfm8z5cG.js} +23 -23
  21. package/assets/assets/{query-client-COM9uzxL.js → query-client-C5GhDMY7.js} +1 -1
  22. package/assets/assets/react-query-CmHyfYFq.js +1 -0
  23. package/assets/assets/{react-sigma_core.esm.min-e91U3Myi.js → react-sigma_core.esm.min-Bjdtl0TQ.js} +1 -1
  24. package/assets/assets/{service-BgFTYqJA.js → service-B2i_ZzWP.js} +1 -1
  25. package/assets/index.html +8 -8
  26. package/dist/{ads-5K5IQDCD.js → ads-MNMND5VH.js} +3 -3
  27. package/dist/{agent-Q4PKMBEJ.js → agent-4KNDL3D3.js} +7 -7
  28. package/dist/{agent-ask-JEPVVW2J.js → agent-ask-32RTOL2M.js} +3 -3
  29. package/dist/{agent-conversations-EHE7QEBC.js → agent-conversations-BXVWHUZC.js} +3 -3
  30. package/dist/{agent-memory-AB72YQET.js → agent-memory-7R7UVFS5.js} +3 -3
  31. package/dist/{agent-providers-NQ7HJYAQ.js → agent-providers-QGVLCCH4.js} +3 -3
  32. package/dist/{agent-transcript-PRTWIQ2A.js → agent-transcript-7Y6BJCIT.js} +3 -3
  33. package/dist/{analytics-IY3J4VZE.js → analytics-YOGEHW6J.js} +3 -3
  34. package/dist/{apply-HNOG674K.js → apply-TB3MF3PH.js} +3 -3
  35. package/dist/{backfill-TO7TPPO6.js → backfill-IRVLT6VT.js} +9 -9
  36. package/dist/{backlinks-3NY5GNT4.js → backlinks-6GTL324T.js} +3 -3
  37. package/dist/{bing-Z7W7U2CT.js → bing-OMQACSDY.js} +3 -3
  38. package/dist/{bootstrap-DXXS3LRH.js → bootstrap-WZ6YDQIG.js} +10 -10
  39. package/dist/{cdp-CKPRS6VE.js → cdp-3WBLAWOL.js} +7 -7
  40. package/dist/{chunk-G26YB6JK.js → chunk-2EWZ3DJ4.js} +3 -3
  41. package/dist/{chunk-JPOIDGGF.js → chunk-3MGM3U6V.js} +1 -1
  42. package/dist/{chunk-7JC4Z6FQ.js → chunk-3QJWOF2B.js} +4 -3
  43. package/dist/{chunk-2BDWXHYF.js → chunk-45RGH7XI.js} +3 -3
  44. package/dist/{chunk-6KPBYCPE.js → chunk-4GEEYY3N.js} +2 -2
  45. package/dist/{chunk-C3UFOS7J.js → chunk-5NBXPO4G.js} +2 -2
  46. package/dist/{chunk-RPJJOBKN.js → chunk-6ACWXQNZ.js} +3 -1
  47. package/dist/{chunk-4XVXP6VR.js → chunk-72YPSNCG.js} +2 -2
  48. package/dist/{chunk-5QR7D4RQ.js → chunk-7HB6QLX4.js} +1 -1
  49. package/dist/{chunk-EM2HAM2U.js → chunk-7HE4LAJY.js} +8 -8
  50. package/dist/{chunk-JRVKW4OO.js → chunk-7OAXEMOF.js} +26 -2
  51. package/dist/{chunk-7UVCZZQJ.js → chunk-AI44ZCMK.js} +1 -1
  52. package/dist/{chunk-LKEFPJQ2.js → chunk-BKOG3GEI.js} +4 -4
  53. package/dist/{chunk-CMS4YM5M.js → chunk-BLZCNL3B.js} +3 -3
  54. package/dist/{chunk-HD2VVIPD.js → chunk-BTMLE3EN.js} +5 -5
  55. package/dist/{chunk-CHTKK43G.js → chunk-COUM462G.js} +152 -24
  56. package/dist/{chunk-2WBT6TEJ.js → chunk-FCYGY4FT.js} +1 -1
  57. package/dist/{chunk-JMCURBZG.js → chunk-GEHRGKQT.js} +4 -4
  58. package/dist/{chunk-ORZBHCZR.js → chunk-IYMTQHPD.js} +4 -4
  59. package/dist/{chunk-XYYWKCJQ.js → chunk-JERATA5R.js} +1 -1
  60. package/dist/{chunk-AAUMUHUI.js → chunk-KF7I7FBI.js} +2 -2
  61. package/dist/{chunk-PSNTEGNY.js → chunk-KZB6HLFT.js} +1 -1
  62. package/dist/{chunk-46KV7CZU.js → chunk-LBNUTWPK.js} +2 -2
  63. package/dist/{chunk-4QHTXT4A.js → chunk-MUYTDD35.js} +40 -4
  64. package/dist/{chunk-TESDUMVK.js → chunk-O4WBYEGV.js} +1 -1
  65. package/dist/{chunk-5LKK2MCJ.js → chunk-OES6PSQK.js} +7 -7
  66. package/dist/{chunk-D5XPB7MM.js → chunk-PH4AESRE.js} +3 -1
  67. package/dist/{chunk-JEXBP7CR.js → chunk-PLLDJUG2.js} +4 -4
  68. package/dist/{chunk-FCGYYZHL.js → chunk-PM2DB3HE.js} +164 -10
  69. package/dist/{chunk-GDKMVOZI.js → chunk-PRCBHOVR.js} +146 -19
  70. package/dist/{chunk-JZ4UFSVR.js → chunk-R26CF4WT.js} +2 -2
  71. package/dist/{chunk-Z2FY6AGZ.js → chunk-TZRYVW75.js} +2 -2
  72. package/dist/{chunk-WFFZAQRG.js → chunk-UHCOJCT3.js} +371 -340
  73. package/dist/{chunk-ZXYCPRNY.js → chunk-WSWHLKKJ.js} +1 -1
  74. package/dist/{chunk-NDTU7FGL.js → chunk-WVCDUKJ2.js} +1 -1
  75. package/dist/{chunk-JSCO223C.js → chunk-ZHFE6J3A.js} +1 -1
  76. package/dist/{citations-4AV6HLUZ.js → citations-PGG7JEIN.js} +3 -3
  77. package/dist/{cli-catalog-ZVFLJXID.js → cli-catalog-KC6JYHSW.js} +3 -3
  78. package/dist/{cli-commands-NJDUBQ3R.js → cli-commands-NGXSLPIA.js} +7 -7
  79. package/dist/cli.js +13 -13
  80. package/dist/{competitor-SG4N37UD.js → competitor-B3CZOKZU.js} +3 -3
  81. package/dist/{content-EDLZAPUV.js → content-Z4XJMZB4.js} +3 -3
  82. package/dist/{dashboard-5ZFVJPHX.js → dashboard-6MMYA2GG.js} +6 -6
  83. package/dist/{demo-5CU3JDHC.js → demo-3R2SHQPO.js} +1 -1
  84. package/dist/{demo-server-43QV3UKM.js → demo-server-ISJOEJN5.js} +12 -12
  85. package/dist/{discover-7X2WMNFV.js → discover-OT3K7KQK.js} +3 -3
  86. package/dist/{discover-eval-ECOPSJJ4.js → discover-eval-DN4J4KD5.js} +3 -3
  87. package/dist/{doctor-NMFBKNYR.js → doctor-UDF6OCSR.js} +3 -3
  88. package/dist/{evidence-C52ZPRYB.js → evidence-Z364UFOD.js} +3 -3
  89. package/dist/{export-cmd-2WSEETUC.js → export-cmd-K27XN4G5.js} +3 -3
  90. package/dist/{ga-YG4O2FT5.js → ga-Q2CELDLG.js} +3 -3
  91. package/dist/{gbp-BDGV6F74.js → gbp-V2APGSIQ.js} +3 -3
  92. package/dist/{google-P2AX6CUF.js → google-WFHYA3QT.js} +4 -4
  93. package/dist/{google-marketing-XPE6CVQI.js → google-marketing-5ROIKBC7.js} +1 -1
  94. package/dist/{health-cmd-MIQT2JNL.js → health-cmd-RGFAJ4YP.js} +3 -3
  95. package/dist/{history-3XDDJDAV.js → history-RUSU66BZ.js} +3 -3
  96. package/dist/index.d.ts +208 -13
  97. package/dist/index.js +26 -26
  98. package/dist/{init-GT3QM7FC.js → init-SMBVJSP6.js} +12 -12
  99. package/dist/{insights-ZJUDCBBP.js → insights-4IFQLPS3.js} +3 -3
  100. package/dist/intelligence-service-MPNONLCW.js +16 -0
  101. package/dist/{journeys-L2CXQXRP.js → journeys-PTC2QY57.js} +3 -3
  102. package/dist/{keys-ODRGIQS7.js → keys-JASXI2SR.js} +3 -3
  103. package/dist/{keyword-LE3CAXO2.js → keyword-SK2HJXCJ.js} +3 -3
  104. package/dist/{logs-OXK43XN2.js → logs-AS5I5V2A.js} +3 -3
  105. package/dist/{mcp-6OTPCDGW.js → mcp-BQSOYHEX.js} +4 -4
  106. package/dist/mcp.js +10 -10
  107. package/dist/{measurement-plan-NXRCDUYL.js → measurement-plan-LZYYH3OD.js} +5 -5
  108. package/dist/{notify-VNNJTNC6.js → notify-JM6252F6.js} +3 -3
  109. package/dist/{organic-evidence-LYDB7IID.js → organic-evidence-L3Y6RZIH.js} +3 -3
  110. package/dist/{overview-2GPPQ6JP.js → overview-VIF2GXYW.js} +4 -4
  111. package/dist/{project-CQJPXC66.js → project-FQEPFPRD.js} +3 -3
  112. package/dist/{query-PPE4EHMY.js → query-VKRESS6B.js} +3 -3
  113. package/dist/{research-GDYZKQY2.js → research-6A6QWKN3.js} +3 -3
  114. package/dist/{results-export-OAJHDEIQ.js → results-export-VUB2ZD5B.js} +3 -3
  115. package/dist/{run-XDPDQS44.js → run-PG5GYZT7.js} +4 -4
  116. package/dist/{schedule-C6SYV54I.js → schedule-EBGOLCQZ.js} +3 -3
  117. package/dist/{search-DONTDHQK.js → search-NS4XPZRL.js} +3 -3
  118. package/dist/{serve-PJXATOSY.js → serve-J7WR3J6I.js} +27 -27
  119. package/dist/{session-PMZV27GX.js → session-FBHHWILQ.js} +14 -14
  120. package/dist/{settings-PMATG7GG.js → settings-LNCAYCDF.js} +3 -3
  121. package/dist/{skills-CYHWYN42.js → skills-DWXA55QA.js} +2 -2
  122. package/dist/{snapshot-E4GHH6DD.js → snapshot-LX7OR7SO.js} +3 -3
  123. package/dist/{src-SXB3MOP3.js → src-AQ5MHQGQ.js} +2 -2
  124. package/dist/{status-MEGXEEW6.js → status-DQ6FNWUT.js} +4 -4
  125. package/dist/{technical-aeo-GE6KOFKT.js → technical-aeo-5GU2IY6W.js} +3 -3
  126. package/dist/{telemetry-Y63F4XFX.js → telemetry-QK2GPII3.js} +4 -4
  127. package/dist/{tools-V546ALG7.js → tools-3ACR2D2V.js} +11 -11
  128. package/dist/{traffic-5ELSKXBM.js → traffic-2JFQKVAX.js} +5 -5
  129. package/dist/{users-7YK7XYWY.js → users-7OJ2RGV3.js} +3 -3
  130. package/dist/{visibility-stats-GYPUKXVS.js → visibility-stats-VR25XMMY.js} +3 -3
  131. package/dist/{wordpress-WUGJT5NN.js → wordpress-JYXTF4PL.js} +3 -3
  132. package/package.json +1 -1
  133. package/assets/assets/MeasurementPropertyPage-DbfTFN3F.js +0 -4
  134. package/assets/assets/ProjectPage-D0m-Xpp3.js +0 -9
  135. package/assets/assets/SiteHealthSection-BCdPVFEJ.js +0 -2
  136. package/assets/assets/ToneBadge-DSJSzqI6.js +0 -60
  137. package/assets/assets/TrafficSourceDetailPage-DJyDO527.js +0 -1
  138. package/assets/assets/react-query-BVRWpXWq.js +0 -1
  139. package/dist/intelligence-service-NXZYQJBH.js +0 -16
@@ -9,14 +9,14 @@ import {
9
9
  normalizeCloudflareWorkerEvent,
10
10
  sessionizeJourneyRequests,
11
11
  verifyRequestSignature
12
- } from "./chunk-TESDUMVK.js";
12
+ } from "./chunk-O4WBYEGV.js";
13
13
  import {
14
14
  currentOutcomeAttribution,
15
15
  getRequestContext,
16
16
  registerRequestContext,
17
17
  requestAuditIdentity,
18
18
  sanitizeRequestContext
19
- } from "./chunk-JPOIDGGF.js";
19
+ } from "./chunk-3MGM3U6V.js";
20
20
  import {
21
21
  SentimentIdempotencyConflict,
22
22
  SentimentRepository,
@@ -157,7 +157,7 @@ import {
157
157
  userSessions,
158
158
  users,
159
159
  writeNegativeReviewMaxStars
160
- } from "./chunk-D5XPB7MM.js";
160
+ } from "./chunk-PH4AESRE.js";
161
161
  import {
162
162
  ADS_ACTIVATE_SCOPE,
163
163
  ADS_ACTIVATION_ABSOLUTE_MAX_ENTITIES,
@@ -371,6 +371,8 @@ import {
371
371
  SeoAdapterOutcomes,
372
372
  SeoAvailability,
373
373
  SeoCapabilities,
374
+ SeoKeywordGapKinds,
375
+ SeoKeywordGapModes,
374
376
  SeoMarketRankingSearchStatuses,
375
377
  SeoMarketRankingSorts,
376
378
  SeoRunStatuses,
@@ -534,6 +536,8 @@ import {
534
536
  compactDateToIso,
535
537
  compareMarketRankingsBy,
536
538
  compareSemver,
539
+ compareSeoCompetitorsBy,
540
+ compareSeoKeywordGapsBy,
537
541
  compareText,
538
542
  competitorAliasBlockRequestSchema,
539
543
  competitorAliasProjectIdentity,
@@ -1058,9 +1062,16 @@ import {
1058
1062
  sentimentSettingsUpdateSchema,
1059
1063
  sentimentSummaryRequestSchema,
1060
1064
  sentimentSummarySchema,
1065
+ seoCompetitorListSchema,
1066
+ seoCompetitorQuerySchema,
1067
+ seoCompetitorSortSchema,
1061
1068
  seoContextSchema,
1062
1069
  seoDomainIntelligenceListSchema,
1063
1070
  seoEvidenceQuerySchema,
1071
+ seoKeywordGapKindSchema,
1072
+ seoKeywordGapListSchema,
1073
+ seoKeywordGapQuerySchema,
1074
+ seoKeywordGapSortSchema,
1064
1075
  seoKeywordMetricsListSchema,
1065
1076
  seoMarketRankingFilterSchema,
1066
1077
  seoMarketRankingListSchema,
@@ -1196,7 +1207,7 @@ import {
1196
1207
  wordpressSchemaDeployResultDtoSchema,
1197
1208
  wordpressSchemaStatusResultDtoSchema,
1198
1209
  wordpressStatusDtoSchema
1199
- } from "./chunk-FCGYYZHL.js";
1210
+ } from "./chunk-PM2DB3HE.js";
1200
1211
 
1201
1212
  // ../api-routes/src/user-session.ts
1202
1213
  import crypto2 from "crypto";
@@ -7946,6 +7957,8 @@ h1 { margin: 0; font-size: 24px; line-height: 32px; font-weight: 600; letter-spa
7946
7957
  p { margin: 0; overflow-wrap: anywhere; }
7947
7958
  .intro { color: var(--secondary); margin-top: 8px; }
7948
7959
  strong { color: var(--text); font-weight: 550; }
7960
+ a { color: var(--text); text-decoration: underline; text-decoration-color: var(--border-strong); text-underline-offset: 3px; }
7961
+ a:hover { text-decoration-color: var(--text); }
7949
7962
  .err { margin-top: 16px; color: var(--negative); }
7950
7963
  .warn { margin-top: 16px; padding: 12px; border: 1px solid var(--caution-border); border-radius: 6px; background: var(--caution-surface); color: var(--caution); font-size: 13px; line-height: 20px; }
7951
7964
  .grant { margin: 24px 0 0; padding: 16px 0 20px; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); font-size: 13px; line-height: 20px; }
@@ -7968,7 +7981,7 @@ button { min-height: 36px; border: 1px solid transparent; border-radius: 6px; pa
7968
7981
  .secondary { border-color: var(--border); background: var(--surface); color: var(--text); }
7969
7982
  .secondary:hover { background: var(--hover); border-color: var(--border-strong); }
7970
7983
  .secondary:active { background: var(--border); }
7971
- :where(input, button):focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }
7984
+ :where(input, button, a):focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }
7972
7985
  @media (max-width: 480px) {
7973
7986
  main { padding: 32px 16px; align-items: flex-start; }
7974
7987
  .panel { padding: 24px; }
@@ -7993,11 +8006,11 @@ function fontFace(fontUrl) {
7993
8006
  src: url('${fontUrl}') format('woff2');
7994
8007
  }`;
7995
8008
  }
7996
- function gatePage(title, body, fontUrl) {
8009
+ function gatePage(title, body, fontUrl, head = "") {
7997
8010
  return `<!doctype html>
7998
8011
  <html lang="en"><head>
7999
8012
  <meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
8000
- <meta name="color-scheme" content="dark"><meta name="theme-color" content="#0a0a0a">
8013
+ <meta name="color-scheme" content="dark"><meta name="theme-color" content="#0a0a0a">${head}
8001
8014
  <title>${title} \xB7 Canonry</title>
8002
8015
  <style>${fontFace(fontUrl)}${styles}</style>
8003
8016
  </head><body><main><div class="gate">
@@ -8032,6 +8045,18 @@ ${o.scope.split(/\s+/).includes(RESEARCH_RUN_SCOPE) ? `<p class="warn">Run resea
8032
8045
  <div class="actions"><button class="primary" type="submit" name="approve" value="yes">Allow</button><button class="secondary" type="submit" name="approve" value="no">Deny</button></div>
8033
8046
  </form>`, o.fontUrl);
8034
8047
  }
8048
+ function renderAuthorizationHandoff(o) {
8049
+ const target = escapeHtml(o.target);
8050
+ const title = o.approved ? "Access allowed" : "Access denied";
8051
+ return gatePage(
8052
+ title,
8053
+ `<h1 id="gate-title">${title}</h1>
8054
+ <p class="intro">Returning you to <strong>${escapeHtml(o.clientName)}</strong>. If nothing happens, <a href="${target}" rel="noreferrer">continue</a>.</p>`,
8055
+ o.fontUrl,
8056
+ `
8057
+ <meta http-equiv="refresh" content="0;url=${target}">`
8058
+ );
8059
+ }
8035
8060
 
8036
8061
  // ../api-routes/src/oauth.ts
8037
8062
  var CODE_TTL_MS = 6e4;
@@ -8089,6 +8114,12 @@ function sendGatePage(reply, html) {
8089
8114
  "frame-ancestors 'none'; default-src 'none'; style-src 'unsafe-inline'; font-src 'self'; form-action 'self'"
8090
8115
  ).header("x-frame-options", "DENY").header("referrer-policy", "same-origin").header("cache-control", "no-store").type("text/html").send(html);
8091
8116
  }
8117
+ function sendHandoffPage(reply, html) {
8118
+ return reply.header(
8119
+ "content-security-policy",
8120
+ "frame-ancestors 'none'; default-src 'none'; style-src 'unsafe-inline'; font-src 'self'; form-action 'none'; base-uri 'none'"
8121
+ ).header("x-frame-options", "DENY").header("referrer-policy", "no-referrer").header("cache-control", "no-store").type("text/html").send(html);
8122
+ }
8092
8123
  var CONSENT_SECRET = crypto10.randomBytes(32);
8093
8124
  function registerOAuthRoutes(app, opts) {
8094
8125
  void app.register(async (scope) => {
@@ -8115,6 +8146,12 @@ function registerOAuthRoutesIn(app, opts) {
8115
8146
  function consentToken(userId, clientId, redirectUri, scope) {
8116
8147
  return crypto10.createHmac("sha256", CONSENT_SECRET).update([userId, clientId, redirectUri, scope].join("\0")).digest("base64url");
8117
8148
  }
8149
+ function selfRegistered(client) {
8150
+ if (client.registration !== "dynamic") return false;
8151
+ const gateway = opts.gatewayClient;
8152
+ const vouched = gateway !== void 0 && client.id === gateway.id && client.redirectUris.length > 0 && client.redirectUris.every((uri) => URL.canParse(uri) && new URL(uri).origin === gateway.origin);
8153
+ return !vouched;
8154
+ }
8118
8155
  function authorizationServerMetadata() {
8119
8156
  return {
8120
8157
  issuer,
@@ -8190,7 +8227,7 @@ function registerOAuthRoutesIn(app, opts) {
8190
8227
  return sendGatePage(reply, renderConsentPage({
8191
8228
  action: request.url,
8192
8229
  clientName: client.name,
8193
- selfRegistered: client.registration === "dynamic",
8230
+ selfRegistered: selfRegistered(client),
8194
8231
  redirectUri,
8195
8232
  scope: grantedScope,
8196
8233
  userName: user.name,
@@ -8228,7 +8265,12 @@ function registerOAuthRoutesIn(app, opts) {
8228
8265
  const denied = new URL(redirectUri);
8229
8266
  denied.searchParams.set("error", "access_denied");
8230
8267
  if (q.state) denied.searchParams.set("state", q.state);
8231
- return reply.redirect(denied.toString());
8268
+ return sendHandoffPage(reply, renderAuthorizationHandoff({
8269
+ target: denied.toString(),
8270
+ approved: false,
8271
+ clientName: client.name,
8272
+ fontUrl: opts.fontUrl
8273
+ }));
8232
8274
  }
8233
8275
  const code = newToken();
8234
8276
  const now = /* @__PURE__ */ new Date();
@@ -8247,7 +8289,12 @@ function registerOAuthRoutesIn(app, opts) {
8247
8289
  target.searchParams.set("code", code);
8248
8290
  if (q.state) target.searchParams.set("state", q.state);
8249
8291
  target.searchParams.set("iss", issuer);
8250
- return reply.redirect(target.toString());
8292
+ return sendHandoffPage(reply, renderAuthorizationHandoff({
8293
+ target: target.toString(),
8294
+ approved: true,
8295
+ clientName: client.name,
8296
+ fontUrl: opts.fontUrl
8297
+ }));
8251
8298
  });
8252
8299
  const registrations = /* @__PURE__ */ new Map();
8253
8300
  let registrationsThisWindow = [];
@@ -15435,7 +15482,9 @@ function resultRows(tasks) {
15435
15482
  keywordMetrics: kept.flatMap((task) => task.results.keywordMetrics),
15436
15483
  domainIntelligence: kept.flatMap((task) => task.results.domainIntelligence),
15437
15484
  rankedKeywords: kept.flatMap((task) => task.results.rankedKeywords),
15438
- marketRankings: tasks.flatMap(marketRankingRows)
15485
+ marketRankings: tasks.flatMap(marketRankingRows),
15486
+ competitors: kept.flatMap((task) => task.results.competitors ?? []),
15487
+ keywordGaps: kept.flatMap((task) => task.results.keywordGaps ?? [])
15439
15488
  };
15440
15489
  }
15441
15490
  function marketRankingRows(task) {
@@ -15471,14 +15520,14 @@ function scopedPlanTargets(plan, scope) {
15471
15520
  return { targets: plan.targets.filter((target) => targetKeys.includes(target.stableKey)), market, targetKeys };
15472
15521
  }
15473
15522
  function scopeCapabilities(kind, targets) {
15474
- if (kind === SeoScopeKinds.project) return [SeoCapabilities["keyword-metrics"], SeoCapabilities["domain-intelligence"], SeoCapabilities["ranked-keywords"], SeoCapabilities["market-rankings"]];
15523
+ if (kind === SeoScopeKinds.project) return Object.values(SeoCapabilities);
15475
15524
  const exactHosts = targets.length > 0 && targets.every((target) => target.urlMatchers.length > 0 && target.urlMatchers.every((matcher) => matcher.kind === "host"));
15476
15525
  const markets = kind === SeoScopeKinds.group || kind === SeoScopeKinds.market ? [SeoCapabilities["market-rankings"]] : [];
15477
15526
  return exactHosts ? [SeoCapabilities["keyword-metrics"], SeoCapabilities["ranked-keywords"], ...markets] : [SeoCapabilities["keyword-metrics"], ...markets];
15478
15527
  }
15479
15528
  function scopeReason(capabilities) {
15480
15529
  const markets = capabilities.includes(SeoCapabilities["market-rankings"]);
15481
- return capabilities.includes(SeoCapabilities["ranked-keywords"]) ? `Supports explicit keyword metrics${markets ? ", market rankings" : ""} and exact-host ranked keywords. Whole-domain intelligence requires project scope.` : `Supports explicit keyword metrics${markets ? " and market rankings" : " only"}. Path and entity Properties cannot be replaced by whole-domain data.`;
15530
+ return capabilities.includes(SeoCapabilities["ranked-keywords"]) ? `Supports explicit keyword metrics${markets ? ", market rankings" : ""} and exact-host ranked keywords. Use project scope for domain intelligence, competitor discovery and keyword gaps.` : `Supports explicit keyword metrics${markets ? " and market rankings" : " only"}. Use project scope for domain research; path and entity Properties cannot be replaced by whole-domain data.`;
15482
15531
  }
15483
15532
  function cancelSeoRefresh(db, projectId, id) {
15484
15533
  db.transaction((tx) => {
@@ -15503,6 +15552,7 @@ function resolveScope2(db, projectId, input) {
15503
15552
  const { market, targetKeys, targets } = scopedPlanTargets(plan, input.scope);
15504
15553
  const supported = scopeCapabilities(input.scope.kind, targets);
15505
15554
  if (input.capabilities.some((capability) => !supported.includes(capability))) {
15555
+ if (input.capabilities.includes(SeoCapabilities["competitor-discovery"]) || input.capabilities.includes(SeoCapabilities["keyword-gaps"])) throw validationError("Competitor discovery and keyword gaps compare whole domains. Select project scope with an explicit domain Target; Property, group and market scope cannot be broadened automatically.");
15506
15556
  if (supported.includes(SeoCapabilities["ranked-keywords"])) throw validationError("Domain intelligence requires project scope with a whole-domain Target. This Advanced scope supports exact-host ranked keywords and explicit keyword metrics.");
15507
15557
  throw validationError("This Advanced scope contains path or entity Properties and supports explicit keyword metrics only. Select a host-only scope for ranked keywords, or project scope for whole-domain intelligence.");
15508
15558
  }
@@ -15515,13 +15565,24 @@ function resolveScope2(db, projectId, input) {
15515
15565
  }
15516
15566
  function normalizeProfile(input) {
15517
15567
  if (new Set(input.capabilities).size !== input.capabilities.length) throw validationError("Select each SEO capability once.");
15518
- const targets = input.targets.map((target) => {
15568
+ const normalized = [...input.targets, ...input.competitors ?? []].map((target) => {
15519
15569
  const host = normalizeHostname(target.value);
15520
15570
  if (!host) throw validationError("SEO Targets must be plain hostnames without paths or ports.");
15521
15571
  if (target.kind === SeoTargetKinds.host && target.includeSubdomains) throw validationError("Exact host Targets cannot include subdomains.");
15522
15572
  return { ...target, value: host };
15523
15573
  });
15574
+ const targets = normalized.slice(0, input.targets.length);
15575
+ const comparisonTargets = normalized.slice(input.targets.length);
15524
15576
  if (new Set(targets.map((target) => `${target.kind}:${target.value}:${target.includeSubdomains}`)).size !== targets.length) throw validationError("SEO Targets must be distinct.");
15577
+ const competitive = input.capabilities.includes(SeoCapabilities["competitor-discovery"]) || input.capabilities.includes(SeoCapabilities["keyword-gaps"]);
15578
+ if (competitive && [...targets, ...comparisonTargets].some((target) => target.kind !== SeoTargetKinds.domain || !target.includeSubdomains || registrableDomain(target.value) !== target.value)) throw validationError("Competitor discovery and keyword gaps require root domain Targets with subdomains included. Exact host comparisons are not supported by this source.");
15579
+ if (input.capabilities.includes(SeoCapabilities["keyword-gaps"])) {
15580
+ if (!comparisonTargets.length) throw validationError("Keyword gaps requires at least one explicit competitor domain.");
15581
+ if (new Set(comparisonTargets.map((target) => target.value)).size !== comparisonTargets.length) throw validationError("Competitor domains must be distinct.");
15582
+ if (comparisonTargets.some((competitor) => targets.some((target) => target.value === competitor.value))) throw validationError("A keyword-gap competitor must differ from every Target domain.");
15583
+ } else if (input.competitors !== void 0) {
15584
+ throw validationError("Comparison competitors apply only to keyword gaps.");
15585
+ }
15525
15586
  const keywords = [...new Set(input.keywords)];
15526
15587
  if (input.capabilities.includes(SeoCapabilities["keyword-metrics"]) && !keywords.length) throw validationError("Keyword metrics requires at least one explicit keyword.");
15527
15588
  if (input.capabilities.some((capability) => capability !== SeoCapabilities["keyword-metrics"]) && !targets.length) throw validationError("Domain capabilities require at least one explicit Target.");
@@ -15535,7 +15596,7 @@ function normalizeProfile(input) {
15535
15596
  } else if (input.rankings) {
15536
15597
  throw validationError("Search settings apply only to market rankings.");
15537
15598
  }
15538
- return { ...input, targets, keywords };
15599
+ return { ...input, targets, keywords, ...input.competitors === void 0 ? {} : { competitors: comparisonTargets } };
15539
15600
  }
15540
15601
  var NO_MARKET_RANKINGS = { searches: [] };
15541
15602
  function advancedMarketQueries(plan, scope) {
@@ -15630,6 +15691,18 @@ function marketRankingPlan(db, projectId, input) {
15630
15691
  }
15631
15692
  function taskRequests(profile, rankings) {
15632
15693
  return profile.capabilities.flatMap((capability) => {
15694
+ if (capability === SeoCapabilities["keyword-gaps"]) {
15695
+ return profile.targets.flatMap((target) => (profile.competitors ?? []).flatMap((competitor) => Object.values(SeoKeywordGapModes).map((mode) => ({
15696
+ capability,
15697
+ target,
15698
+ keywords: [],
15699
+ locationCode: profile.locationCode,
15700
+ languageCode: profile.languageCode,
15701
+ limit: profile.limit,
15702
+ tag: "",
15703
+ comparison: { competitor, mode }
15704
+ }))));
15705
+ }
15633
15706
  if (capability === SeoCapabilities["market-rankings"]) {
15634
15707
  const settings = profile.rankings;
15635
15708
  return rankings.searches.map((search) => ({
@@ -15891,7 +15964,7 @@ async function seoRoutes(app, opts) {
15891
15964
  const row = job(resolveProject(app.db, request.params.name).id, request.params.id);
15892
15965
  const rows = tasks(row.id);
15893
15966
  const results = resultRows(rows);
15894
- return { ...runDto(row, rows.map((task) => ({ status: task.status, count: 1 }))), tasks: rows.map((task) => ({ id: task.id, capability: task.request.capability, target: task.request.target, status: task.status, error: task.error, remoteTaskId: task.receipts[0]?.remoteTaskId ?? null, ...taskSearch(task), retried: task.retryOf !== null })), results: { keywordMetrics: results.keywordMetrics.slice(0, 20), domainIntelligence: results.domainIntelligence.slice(0, 20), rankedKeywords: results.rankedKeywords.slice(0, 20), marketRankings: [...results.marketRankings].sort(compareMarketRankings).slice(0, 20) }, resultCounts: { keywordMetrics: results.keywordMetrics.length, domainIntelligence: results.domainIntelligence.length, rankedKeywords: results.rankedKeywords.length, marketRankings: results.marketRankings.length }, resultsTruncated: Object.values(results).some((items) => items.length > 20) };
15967
+ return { ...runDto(row, rows.map((task) => ({ status: task.status, count: 1 }))), tasks: rows.map((task) => ({ id: task.id, capability: task.request.capability, target: task.request.target, comparison: task.request.comparison ?? null, status: task.status, error: task.error, remoteTaskId: task.receipts[0]?.remoteTaskId ?? null, ...taskSearch(task), retried: task.retryOf !== null })), results: { keywordMetrics: results.keywordMetrics.slice(0, 20), domainIntelligence: results.domainIntelligence.slice(0, 20), rankedKeywords: results.rankedKeywords.slice(0, 20), marketRankings: [...results.marketRankings].sort(compareMarketRankings).slice(0, 20), competitors: results.competitors.slice(0, 20), keywordGaps: results.keywordGaps.slice(0, 20) }, resultCounts: { keywordMetrics: results.keywordMetrics.length, domainIntelligence: results.domainIntelligence.length, rankedKeywords: results.rankedKeywords.length, marketRankings: results.marketRankings.length, competitors: results.competitors.length, keywordGaps: results.keywordGaps.length }, resultsTruncated: Object.values(results).some((items) => items.length > 20) };
15895
15968
  });
15896
15969
  app.post("/projects/:name/seo/runs/:id/cancel", { config: { paidRead: true, writeScope: SEO_RUN_SCOPE } }, async (request) => {
15897
15970
  requireScope(request, SEO_RUN_SCOPE);
@@ -15910,7 +15983,7 @@ async function seoRoutes(app, opts) {
15910
15983
  const results = resultRows(rows);
15911
15984
  const primary = row?.profile.targets[0];
15912
15985
  const domain = primary ? results.domainIntelligence.find((result) => result.target.kind === primary.kind && result.target.value === primary.value && result.target.includeSubdomains === primary.includeSubdomains) : null;
15913
- return { status: status(project.id), profiles: profiles(project.id), latestRun: latest ? runSummary(latest) : null, evidenceRunId: row?.id ?? null, evidenceRun: row ? runDto(row, rows.map((task) => ({ status: task.status, count: 1 }))) : null, keywordMetricsCount: results.keywordMetrics.length, domainIntelligenceCount: results.domainIntelligence.length, rankedKeywordCount: results.rankedKeywords.length, marketRankingCount: results.marketRankings.length, summary: domain ? { target: domain.target, organicKeywords: domain.organicKeywords, inTop10: domain.top1 !== null && domain.top2To3 !== null && domain.top4To10 !== null ? domain.top1 + domain.top2To3 + domain.top4To10 : null, estimatedOrganicTraffic: domain.estimatedOrganicTraffic, provenance: domain.provenance } : null };
15986
+ return { status: status(project.id), profiles: profiles(project.id), latestRun: latest ? runSummary(latest) : null, evidenceRunId: row?.id ?? null, evidenceRun: row ? runDto(row, rows.map((task) => ({ status: task.status, count: 1 }))) : null, keywordMetricsCount: results.keywordMetrics.length, domainIntelligenceCount: results.domainIntelligence.length, rankedKeywordCount: results.rankedKeywords.length, marketRankingCount: results.marketRankings.length, competitorCount: results.competitors.length, keywordGapCount: results.keywordGaps.length, summary: domain ? { target: domain.target, organicKeywords: domain.organicKeywords, inTop10: domain.top1 !== null && domain.top2To3 !== null && domain.top4To10 !== null ? domain.top1 + domain.top2To3 + domain.top4To10 : null, estimatedOrganicTraffic: domain.estimatedOrganicTraffic, provenance: domain.provenance } : null };
15914
15987
  });
15915
15988
  for (const [path8, key] of [["keyword-metrics", "keywordMetrics"], ["domain-intelligence", "domainIntelligence"], ["ranked-keywords", "rankedKeywords"]]) app.get(`/projects/:name/seo/${path8}`, async (request) => {
15916
15989
  const query = queryInput(request.query);
@@ -15920,6 +15993,30 @@ async function seoRoutes(app, opts) {
15920
15993
  const offset = query.offset ?? 0;
15921
15994
  return { runId: row?.id ?? null, items: items.slice(offset, offset + limit), total: items.length, limit, offset };
15922
15995
  });
15996
+ app.get("/projects/:name/seo/competitors", async (request) => {
15997
+ const raw = request.query;
15998
+ const parsed = seoCompetitorQuerySchema.safeParse({ ...raw, ...raw.limit === void 0 ? {} : { limit: Number(raw.limit) }, ...raw.offset === void 0 ? {} : { offset: Number(raw.offset) } });
15999
+ if (!parsed.success) throw validationError("Invalid SEO competitor selection.");
16000
+ const query = parsed.data;
16001
+ const row = selected(resolveProject(app.db, request.params.name).id, query, true);
16002
+ const items = row ? resultRows(tasks(row.id)).competitors : [];
16003
+ if (query.sort) items.sort(compareSeoCompetitorsBy(query.sort));
16004
+ const limit = query.limit ?? 50;
16005
+ const offset = query.offset ?? 0;
16006
+ return { runId: row?.id ?? null, items: items.slice(offset, offset + limit), total: items.length, limit, offset };
16007
+ });
16008
+ app.get("/projects/:name/seo/keyword-gaps", async (request) => {
16009
+ const raw = request.query;
16010
+ const parsed = seoKeywordGapQuerySchema.safeParse({ ...raw, ...raw.limit === void 0 ? {} : { limit: Number(raw.limit) }, ...raw.offset === void 0 ? {} : { offset: Number(raw.offset) } });
16011
+ if (!parsed.success) throw validationError("Invalid SEO keyword-gap selection.");
16012
+ const query = parsed.data;
16013
+ const row = selected(resolveProject(app.db, request.params.name).id, query, true);
16014
+ const items = (row ? resultRows(tasks(row.id)).keywordGaps : []).filter((item2) => !query.kind || (query.kind === SeoKeywordGapKinds.shared ? item2.kind !== SeoKeywordGapKinds.missing : item2.kind === query.kind));
16015
+ if (query.sort) items.sort(compareSeoKeywordGapsBy(query.sort));
16016
+ const limit = query.limit ?? 50;
16017
+ const offset = query.offset ?? 0;
16018
+ return { runId: row?.id ?? null, items: items.slice(offset, offset + limit), total: items.length, limit, offset };
16019
+ });
15923
16020
  app.get("/projects/:name/seo/market-rankings", async (request) => {
15924
16021
  const raw = request.query ?? {};
15925
16022
  const parsed = seoMarketRankingQuerySchema.safeParse({ ...raw, ...raw.limit === void 0 ? {} : { limit: Number(raw.limit) }, ...raw.offset === void 0 ? {} : { offset: Number(raw.offset) } });
@@ -34192,6 +34289,8 @@ var SCHEMA_TABLE = {
34192
34289
  SeoRunList: seoRunListSchema,
34193
34290
  SeoOverview: seoOverviewSchema,
34194
34291
  SeoKeywordMetricsList: seoKeywordMetricsListSchema,
34292
+ SeoCompetitorList: seoCompetitorListSchema,
34293
+ SeoKeywordGapList: seoKeywordGapListSchema,
34195
34294
  SeoDomainIntelligenceList: seoDomainIntelligenceListSchema,
34196
34295
  SeoRankedKeywordList: seoRankedKeywordListSchema,
34197
34296
  SeoMarketRankingList: seoMarketRankingListSchema,
@@ -35425,12 +35524,40 @@ var routeCatalog = [
35425
35524
  409: errorResponse("Selection, intent or execution identity changed.")
35426
35525
  }
35427
35526
  },
35527
+ {
35528
+ method: "get",
35529
+ path: "/api/v1/projects/{name}/seo/competitors",
35530
+ summary: "Read stored organic search competitors",
35531
+ tags: ["seo"],
35532
+ description: "Stored whole-domain organic competitor discovery only; never calls a provider. Available for Simple and Advanced project profiles. Each row names its target and source locale. Results are bounded provider database observations, not an exhaustive competitor inventory or a market/Property measurement.",
35533
+ parameters: [nameParameter, ...seoEvidenceParameters, { name: "sort", in: "query", description: "Sort the complete saved result before pagination: domain, shared searches, average position or estimated traffic. Prefix with - for descending. Missing values stay last in both directions. Omitted preserves stored order.", schema: { type: "string", enum: [...seoCompetitorSortSchema.options] } }],
35534
+ responses: {
35535
+ 200: jsonResponse("Saved organic competitor evidence.", "SeoCompetitorList"),
35536
+ 400: errorResponse("Invalid scope or input."),
35537
+ 403: errorResponse("Insufficient authority."),
35538
+ 404: errorResponse("Project or selected evidence not found.")
35539
+ }
35540
+ },
35541
+ {
35542
+ method: "get",
35543
+ path: "/api/v1/projects/{name}/seo/keyword-gaps",
35544
+ summary: "Read stored organic keyword comparisons",
35545
+ tags: ["seo"],
35546
+ description: "Stored whole-domain comparisons only; never calls a provider. Missing means the source database has a competitor ranking and no target ranking, not absence from Google. Shared includes outranked searches where both domains rank; outranked selects only those with the competitor ahead. Filters apply before pagination; total counts saved rows, not the full provider keyword universe. A signed rankGap is target position minus competitor position and is null when the target is missing.",
35547
+ parameters: [nameParameter, ...seoEvidenceParameters, { name: "kind", in: "query", description: "shared includes outranked; missing and outranked select that exact kind. Omitted returns all saved comparisons.", schema: { type: "string", enum: [...seoKeywordGapKindSchema.options] } }, { name: "sort", in: "query", description: "Sort after filtering and before pagination: keyword, kind, target rank, competitor rank, rank gap or monthly search volume. Prefix with - for descending. Missing values stay last in both directions. Omitted preserves stored order.", schema: { type: "string", enum: [...seoKeywordGapSortSchema.options] } }],
35548
+ responses: {
35549
+ 200: jsonResponse("Saved keyword comparison evidence.", "SeoKeywordGapList"),
35550
+ 400: errorResponse("Invalid scope or input."),
35551
+ 403: errorResponse("Insufficient authority."),
35552
+ 404: errorResponse("Project or selected evidence not found.")
35553
+ }
35554
+ },
35428
35555
  {
35429
35556
  method: "get",
35430
35557
  path: "/api/v1/projects/{name}/seo/market-rankings",
35431
35558
  summary: "Read stored market rankings",
35432
35559
  tags: ["seo"],
35433
- description: "Stored evidence only; never calls a provider. The project's position for each tracked non-brand query, searched on Google from its market's location. searchStatus says how each search ended: in a completed or partial row a null rank means the site was not in the checked results (depth, which is lower when only part of the results came back); a failed or cancelled search keeps its rows with no rank. outcome says where each row landed (a partial search without the site is partly-checked, never not-ranked) and rivalAhead whether a tracked competitor ranks above the site.",
35560
+ description: "Stored evidence only; never calls a provider. The project's position for each tracked non-brand query, searched on Google from its market's location. searchStatus says how each search ended: in a completed or partial row a null rank means the site was not in the checked results (depth, which is lower when only part of the results came back); a failed or cancelled search keeps its rows with no rank. outcome says where each row landed (a partial search without the site is partly-checked, never not-ranked) and rivalAhead whether a tracked competitor ranks above the site. topCompetitor is the best tracked rival, and rankGap is the target position minus that rival position, null when either rank is unavailable.",
35434
35561
  parameters: [
35435
35562
  nameParameter,
35436
35563
  ...seoEvidenceParameters,
@@ -35474,7 +35601,7 @@ var routeCatalog = [
35474
35601
  path: "/api/v1/projects/{name}/seo/profiles/{id}",
35475
35602
  summary: "Save an SEO profile",
35476
35603
  tags: ["seo"],
35477
- description: "Requires seo.write and an administrator session. Saving never starts provider work. Scope, locale, targets, capabilities, keywords and row limit are identity parameters.",
35604
+ description: "Requires seo.write and an administrator session. Saving never starts provider work. Scope, locale, targets, comparison competitors, capabilities, keywords and row limit are identity parameters. Keyword gaps requires one to five explicit competitor root domains; shared and missing datasets are reserved separately for each target/competitor pair. Competitor discovery and keyword gaps support project scope with whole-domain targets only, including Advanced projects.",
35478
35605
  parameters: [nameParameter, { name: "id", in: "path", required: true, description: "Exact project-scoped identifier.", schema: stringSchema }],
35479
35606
  requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/SeoProfileInput" } } } },
35480
35607
  responses: {
@@ -4,14 +4,14 @@ import {
4
4
  } from "./chunk-VJWDBFSL.js";
5
5
  import {
6
6
  createApiClient
7
- } from "./chunk-JRVKW4OO.js";
7
+ } from "./chunk-7OAXEMOF.js";
8
8
  import {
9
9
  CliError,
10
10
  isMachineFormat
11
11
  } from "./chunk-HGMYEDAQ.js";
12
12
  import {
13
13
  describeError
14
- } from "./chunk-FCGYYZHL.js";
14
+ } from "./chunk-PM2DB3HE.js";
15
15
 
16
16
  // src/commands/mcp.ts
17
17
  import fs from "fs";
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  getRequestContext
3
- } from "./chunk-JPOIDGGF.js";
3
+ } from "./chunk-3MGM3U6V.js";
4
4
  import {
5
5
  diagnosticIdentity,
6
6
  redactLogString,
7
7
  redactLogValue
8
- } from "./chunk-FCGYYZHL.js";
8
+ } from "./chunk-PM2DB3HE.js";
9
9
 
10
10
  // ../api-routes/src/runtime-logger.ts
11
11
  import { format } from "util";