@ainyc/canonry 4.76.2 → 4.77.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 (27) hide show
  1. package/assets/assets/{BacklinksPage-GkTp8zLM.js → BacklinksPage-CwXveumn.js} +1 -1
  2. package/assets/assets/{ChartPrimitives-0noLg9_T.js → ChartPrimitives-DntKGI5J.js} +1 -1
  3. package/assets/assets/{ProjectPage-Bg5sKYhO.js → ProjectPage-CVudiU8X.js} +4 -4
  4. package/assets/assets/{RunRow-BEhaQDol.js → RunRow-DMtYXaxG.js} +1 -1
  5. package/assets/assets/{RunsPage-CV2wKkaz.js → RunsPage-Cz-YlucO.js} +1 -1
  6. package/assets/assets/{SettingsPage-Ctij-T9F.js → SettingsPage-BCuG3C-0.js} +1 -1
  7. package/assets/assets/TrafficPage-DV8X47wa.js +1 -0
  8. package/assets/assets/TrafficSourceDetailPage-BmYhK9jm.js +1 -0
  9. package/assets/assets/arrow-left-CUmHyNnF.js +1 -0
  10. package/assets/assets/extract-error-message-DFjy9_zi.js +1 -0
  11. package/assets/assets/{index-DRhoqa2-.css → index-BgWgJE7S.css} +1 -1
  12. package/assets/assets/{index-B-_Dwwhd.js → index-D9smxU6R.js} +4 -4
  13. package/assets/assets/{trash-2-C65kNIO5.js → trash-2-B_UtEEm8.js} +1 -1
  14. package/assets/index.html +2 -2
  15. package/dist/{chunk-BIT3JPMW.js → chunk-BPZWX7YI.js} +95 -15
  16. package/dist/{chunk-ZUQK4TLL.js → chunk-FB43IMZT.js} +35 -17
  17. package/dist/{chunk-ETM5EPQH.js → chunk-KPN22EWK.js} +2 -2
  18. package/dist/{chunk-42TDEKKM.js → chunk-XI6YSTGE.js} +1 -1
  19. package/dist/cli.js +10 -4
  20. package/dist/index.js +4 -4
  21. package/dist/{intelligence-service-QIGX7IVQ.js → intelligence-service-C76ZRMF5.js} +2 -2
  22. package/dist/mcp.js +2 -2
  23. package/package.json +8 -8
  24. package/assets/assets/TrafficPage-TnIPaMcL.js +0 -1
  25. package/assets/assets/TrafficSourceDetailPage-BBmIwaee.js +0 -1
  26. package/assets/assets/extract-error-message-BdLppivS.js +0 -1
  27. package/assets/assets/server-traffic-ScU00kCP.js +0 -1
@@ -1 +1 @@
1
- import{c}from"./index-B-_Dwwhd.js";const a=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],h=c("circle-check",a);const e=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3",key:"1u773s"}],["path",{d:"M12 17h.01",key:"p32p05"}]],n=c("circle-question-mark",e);const o=[["path",{d:"M12 15V3",key:"m9g1x1"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["path",{d:"m7 10 5 5 5-5",key:"brsn70"}]],s=c("download",o);const t=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],r=c("trash-2",t);export{h as C,s as D,r as T,n as a};
1
+ import{c}from"./index-D9smxU6R.js";const a=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],h=c("circle-check",a);const e=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3",key:"1u773s"}],["path",{d:"M12 17h.01",key:"p32p05"}]],n=c("circle-question-mark",e);const o=[["path",{d:"M12 15V3",key:"m9g1x1"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["path",{d:"m7 10 5 5 5-5",key:"brsn70"}]],s=c("download",o);const t=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],r=c("trash-2",t);export{h as C,s as D,r as T,n as a};
package/assets/index.html CHANGED
@@ -12,12 +12,12 @@
12
12
  <link rel="icon" type="image/png" sizes="32x32" href="./favicon-32.png" />
13
13
  <link rel="apple-touch-icon" href="./apple-touch-icon.png" />
14
14
  <title>Canonry</title>
15
- <script type="module" crossorigin src="./assets/index-B-_Dwwhd.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-D9smxU6R.js"></script>
16
16
  <link rel="modulepreload" crossorigin href="./assets/vendor-tanstack-Dq7p98wZ.js">
17
17
  <link rel="modulepreload" crossorigin href="./assets/vendor-radix-B57xfQbP.js">
18
18
  <link rel="modulepreload" crossorigin href="./assets/vendor-recharts-ClRVR6aX.js">
19
19
  <link rel="modulepreload" crossorigin href="./assets/vendor-markdown-DK7fbRNb.js">
20
- <link rel="stylesheet" crossorigin href="./assets/index-DRhoqa2-.css">
20
+ <link rel="stylesheet" crossorigin href="./assets/index-BgWgJE7S.css">
21
21
  </head>
22
22
  <body>
23
23
  <div id="root"></div>
@@ -232,7 +232,7 @@ import {
232
232
  wordpressSchemaDeployResultDtoSchema,
233
233
  wordpressSchemaStatusResultDtoSchema,
234
234
  wordpressStatusDtoSchema
235
- } from "./chunk-ETM5EPQH.js";
235
+ } from "./chunk-KPN22EWK.js";
236
236
 
237
237
  // src/intelligence-service.ts
238
238
  import { eq as eq33, desc as desc17, asc as asc4, and as and24, ne as ne5, or as or5, inArray as inArray12, gte as gte6, lte as lte3 } from "drizzle-orm";
@@ -19153,6 +19153,68 @@ async function getValidToken(store, domain, connectionType, clientId, clientSecr
19153
19153
  propertyId: conn.propertyId ?? null
19154
19154
  };
19155
19155
  }
19156
+ function parseGscApiDisabled(message) {
19157
+ if (!/accessNotConfigured|SERVICE_DISABLED|has not been used in project|is disabled/i.test(message)) {
19158
+ return null;
19159
+ }
19160
+ const projectNumber = message.match(/[?&]project=(\d+)/)?.[1] ?? message.match(/project\s+(\d+)/i)?.[1] ?? null;
19161
+ const base = "https://console.developers.google.com/apis/api";
19162
+ const qs = projectNumber ? `?project=${projectNumber}` : "";
19163
+ return {
19164
+ projectNumber,
19165
+ enableUrl: `${base}/searchconsole.googleapis.com/overview${qs}`,
19166
+ indexingApiUrl: `${base}/indexing.googleapis.com/overview${qs}`
19167
+ };
19168
+ }
19169
+ function googleAuthErrorStatus(err) {
19170
+ if (err.statusCode != null) return err.statusCode;
19171
+ const match = err.message.match(/failed \((\d{3})\)/);
19172
+ return match ? Number(match[1]) : null;
19173
+ }
19174
+ function gscErrorToAppError(err, context) {
19175
+ if (err instanceof AppError) return err;
19176
+ if (err instanceof GoogleApiError) {
19177
+ if (err.status === 429) {
19178
+ return quotaExceeded("Google Search Console API (rate limited; retries exhausted)");
19179
+ }
19180
+ if (err.status === 403) {
19181
+ const disabled = parseGscApiDisabled(err.message);
19182
+ if (disabled) {
19183
+ const inProject = disabled.projectNumber ? ` (project ${disabled.projectNumber})` : "";
19184
+ return forbidden(
19185
+ `${context}: the Google Search Console API is not enabled for your Google Cloud project${inProject}. Enable the Search Console API and the Indexing API, wait ~2\u20135 minutes, then retry: ${disabled.enableUrl}`,
19186
+ { reason: "gsc-api-disabled", upstreamStatus: 403, ...disabled }
19187
+ );
19188
+ }
19189
+ return forbidden(
19190
+ `${context}: the connected Google account does not have access to a verified Search Console property for this domain. Connect the account that owns the property.`,
19191
+ { reason: "gsc-no-property-access", upstreamStatus: 403 }
19192
+ );
19193
+ }
19194
+ if (err.status === 401) {
19195
+ return forbidden(
19196
+ `${context}: the Google connection has expired or was revoked. Reconnect Google Search Console.`,
19197
+ { reason: "gsc-reconnect", upstreamStatus: 401 }
19198
+ );
19199
+ }
19200
+ return providerError(`${context}: ${err.message}`, { upstreamStatus: err.status });
19201
+ }
19202
+ if (err instanceof GoogleAuthError) {
19203
+ const status = googleAuthErrorStatus(err);
19204
+ if (status === 429) return quotaExceeded("Google OAuth token refresh (rate limited)");
19205
+ if (status != null && status >= 400 && status < 500) {
19206
+ return forbidden(
19207
+ `${context}: the stored Google credentials are no longer valid (token refresh failed). Reconnect Google Search Console.`,
19208
+ { reason: "gsc-reconnect", upstreamStatus: status }
19209
+ );
19210
+ }
19211
+ return providerError(
19212
+ `${context}: ${err.message}. Reconnect Google Search Console if this persists.`,
19213
+ status != null ? { upstreamStatus: status } : void 0
19214
+ );
19215
+ }
19216
+ return providerError(`${context}: ${err instanceof Error ? err.message : String(err)}`);
19217
+ }
19156
19218
  async function googleRoutes(app, opts) {
19157
19219
  if (opts.googleStateSecret === void 0) {
19158
19220
  app.log.warn(
@@ -19375,9 +19437,13 @@ async function googleRoutes(app, opts) {
19375
19437
  }
19376
19438
  const store = requireConnectionStore();
19377
19439
  const project = resolveProject(app.db, request.params.name);
19378
- const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
19379
- const sites = await listSites(accessToken);
19380
- return { sites };
19440
+ try {
19441
+ const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
19442
+ const sites = await listSites(accessToken);
19443
+ return { sites };
19444
+ } catch (err) {
19445
+ throw gscErrorToAppError(err, "Failed to list Search Console properties");
19446
+ }
19381
19447
  });
19382
19448
  app.post("/projects/:name/google/gsc/sync", async (request) => {
19383
19449
  const store = requireConnectionStore();
@@ -19470,11 +19536,16 @@ async function googleRoutes(app, opts) {
19470
19536
  if (!url) {
19471
19537
  throw validationError("url is required");
19472
19538
  }
19473
- const { accessToken, propertyId } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
19474
- if (!propertyId) {
19475
- throw validationError("No GSC property configured for this connection");
19539
+ let result;
19540
+ try {
19541
+ const { accessToken, propertyId } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
19542
+ if (!propertyId) {
19543
+ throw validationError("No GSC property configured for this connection");
19544
+ }
19545
+ result = await inspectUrl(accessToken, url, propertyId);
19546
+ } catch (err) {
19547
+ throw gscErrorToAppError(err, "Failed to inspect URL in Search Console");
19476
19548
  }
19477
- const result = await inspectUrl(accessToken, url, propertyId);
19478
19549
  const ir = result.inspectionResult;
19479
19550
  const idx = ir.indexStatusResult;
19480
19551
  const mob = ir.mobileUsabilityResult;
@@ -19682,12 +19753,16 @@ async function googleRoutes(app, opts) {
19682
19753
  }
19683
19754
  const store = requireConnectionStore();
19684
19755
  const project = resolveProject(app.db, request.params.name);
19685
- const { accessToken, propertyId } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
19686
- if (!propertyId) {
19687
- throw validationError('No GSC property configured for this connection. Set one with "canonry google set-property".');
19756
+ try {
19757
+ const { accessToken, propertyId } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
19758
+ if (!propertyId) {
19759
+ throw validationError('No GSC property configured for this connection. Set one with "canonry google set-property".');
19760
+ }
19761
+ const sitemaps = await listSitemaps(accessToken, propertyId);
19762
+ return { sitemaps };
19763
+ } catch (err) {
19764
+ throw gscErrorToAppError(err, "Failed to list Search Console sitemaps");
19688
19765
  }
19689
- const sitemaps = await listSitemaps(accessToken, propertyId);
19690
- return { sitemaps };
19691
19766
  });
19692
19767
  app.post("/projects/:name/google/gsc/discover-sitemaps", async (request) => {
19693
19768
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
@@ -19703,8 +19778,13 @@ async function googleRoutes(app, opts) {
19703
19778
  if (!conn.propertyId) {
19704
19779
  throw validationError("No GSC property configured for this connection");
19705
19780
  }
19706
- const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
19707
- const sitemaps = await listSitemaps(accessToken, conn.propertyId);
19781
+ let sitemaps;
19782
+ try {
19783
+ const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
19784
+ sitemaps = await listSitemaps(accessToken, conn.propertyId);
19785
+ } catch (err) {
19786
+ throw gscErrorToAppError(err, "Failed to discover Search Console sitemaps");
19787
+ }
19708
19788
  if (sitemaps.length === 0) {
19709
19789
  throw validationError("No sitemaps found for this GSC property. Submit a sitemap in Google Search Console first.");
19710
19790
  }
@@ -9,7 +9,7 @@ import {
9
9
  loadConfig,
10
10
  loadConfigRaw,
11
11
  saveConfigPatch
12
- } from "./chunk-42TDEKKM.js";
12
+ } from "./chunk-XI6YSTGE.js";
13
13
  import {
14
14
  CC_CACHE_DIR,
15
15
  DUCKDB_SPEC,
@@ -97,7 +97,7 @@ import {
97
97
  siteAuditPages,
98
98
  siteAuditSnapshots,
99
99
  usageCounters
100
- } from "./chunk-BIT3JPMW.js";
100
+ } from "./chunk-BPZWX7YI.js";
101
101
  import {
102
102
  AGENT_MEMORY_VALUE_MAX_BYTES,
103
103
  AGENT_PROVIDER_IDS,
@@ -149,7 +149,7 @@ import {
149
149
  validationError,
150
150
  winnabilityClassLabel,
151
151
  withRetry
152
- } from "./chunk-ETM5EPQH.js";
152
+ } from "./chunk-KPN22EWK.js";
153
153
 
154
154
  // src/telemetry.ts
155
155
  import crypto from "crypto";
@@ -476,15 +476,17 @@ function resolveModel(config) {
476
476
  return config.model || DEFAULT_MODEL;
477
477
  }
478
478
  function createClient2(config) {
479
+ const httpOptions = config.baseUrl ? { httpOptions: { baseUrl: config.baseUrl } } : {};
479
480
  if (isVertexConfig(config)) {
480
481
  return new GoogleGenAI({
481
482
  vertexai: true,
482
483
  project: config.vertexProject,
483
484
  location: config.vertexRegion || "us-central1",
484
- ...config.vertexCredentials ? { googleAuthOptions: { keyFilename: config.vertexCredentials } } : {}
485
+ ...config.vertexCredentials ? { googleAuthOptions: { keyFilename: config.vertexCredentials } } : {},
486
+ ...httpOptions
485
487
  });
486
488
  }
487
- return new GoogleGenAI({ apiKey: config.apiKey });
489
+ return new GoogleGenAI({ apiKey: config.apiKey, ...httpOptions });
488
490
  }
489
491
  function validateConfig(config) {
490
492
  if ("vertexProject" in config && config.vertexProject !== void 0 && config.vertexProject.trim().length === 0) {
@@ -749,7 +751,7 @@ async function embedQueries(queries2, options) {
749
751
  if (!options.apiKey && !options.client) {
750
752
  throw new Error("embedQueries: missing apiKey");
751
753
  }
752
- const client = options.client ?? createGeminiEmbedClient(options.apiKey);
754
+ const client = options.client ?? createGeminiEmbedClient(options.apiKey, options.baseUrl);
753
755
  return client.embedBatch(queries2, {
754
756
  model: options.model ?? DEFAULT_EMBED_MODEL,
755
757
  taskType: CLUSTERING_TASK_TYPE,
@@ -770,8 +772,11 @@ function extractEmbeddingVectors(response, expectedLength) {
770
772
  return e.values;
771
773
  });
772
774
  }
773
- function createGeminiEmbedClient(apiKey) {
774
- const genai = new GoogleGenAI2({ apiKey });
775
+ function createEmbedGenAI(apiKey, baseUrl) {
776
+ return new GoogleGenAI2({ apiKey, ...baseUrl ? { httpOptions: { baseUrl } } : {} });
777
+ }
778
+ function createGeminiEmbedClient(apiKey, baseUrl) {
779
+ const genai = createEmbedGenAI(apiKey, baseUrl);
775
780
  return {
776
781
  async embedBatch(queries2, opts) {
777
782
  const response = await genai.models.embedContent({
@@ -792,6 +797,7 @@ function toGeminiConfig(config) {
792
797
  return {
793
798
  apiKey: config.apiKey ?? "",
794
799
  model: config.model,
800
+ baseUrl: config.baseUrl,
795
801
  quotaPolicy: config.quotaPolicy,
796
802
  vertexProject: config.vertexProject,
797
803
  vertexRegion: config.vertexRegion,
@@ -895,6 +901,12 @@ async function withRetry3(fn, options = {}) {
895
901
 
896
902
  // ../provider-openai/src/normalize.ts
897
903
  var DEFAULT_MODEL2 = "gpt-5.4";
904
+ function createClient3(config) {
905
+ return new OpenAI({
906
+ apiKey: config.apiKey,
907
+ ...config.baseUrl ? { baseURL: config.baseUrl } : {}
908
+ });
909
+ }
898
910
  function validateConfig2(config) {
899
911
  if (!config.apiKey || config.apiKey.length === 0) {
900
912
  return { ok: false, provider: "openai", message: "missing api key" };
@@ -910,7 +922,7 @@ async function healthcheck2(config) {
910
922
  const validation = validateConfig2(config);
911
923
  if (!validation.ok) return validation;
912
924
  try {
913
- const client = new OpenAI({ apiKey: config.apiKey });
925
+ const client = createClient3(config);
914
926
  const response = await withRetry3(
915
927
  () => client.responses.create({
916
928
  model: config.model ?? DEFAULT_MODEL2,
@@ -935,7 +947,7 @@ async function healthcheck2(config) {
935
947
  }
936
948
  async function executeTrackedQuery2(input) {
937
949
  const model = input.config.model ?? DEFAULT_MODEL2;
938
- const client = new OpenAI({ apiKey: input.config.apiKey });
950
+ const client = createClient3(input.config);
939
951
  const webSearchTool = { type: "web_search" };
940
952
  if (input.location) {
941
953
  webSearchTool.user_location = {
@@ -1108,7 +1120,7 @@ function extractDomainFromUri2(uri) {
1108
1120
  }
1109
1121
  async function generateText2(prompt, config) {
1110
1122
  const model = config.model ?? DEFAULT_MODEL2;
1111
- const client = new OpenAI({ apiKey: config.apiKey });
1123
+ const client = createClient3(config);
1112
1124
  const response = await withRetry3(
1113
1125
  () => client.responses.create({
1114
1126
  model,
@@ -1130,6 +1142,7 @@ function toOpenAIConfig(config) {
1130
1142
  return {
1131
1143
  apiKey: config.apiKey ?? "",
1132
1144
  model: config.model,
1145
+ baseUrl: config.baseUrl,
1133
1146
  quotaPolicy: config.quotaPolicy
1134
1147
  };
1135
1148
  }
@@ -4933,7 +4946,7 @@ function buildDefaultDeps(registry) {
4933
4946
  },
4934
4947
  async embed(queries2) {
4935
4948
  if (cfg.apiKey) {
4936
- return embedQueries(queries2, { apiKey: cfg.apiKey });
4949
+ return embedQueries(queries2, { apiKey: cfg.apiKey, baseUrl: cfg.baseUrl });
4937
4950
  }
4938
4951
  throw new Error("Discovery currently requires a Gemini API key. Vertex-mode embeddings are not yet implemented.");
4939
4952
  },
@@ -5761,7 +5774,7 @@ function readStoredGroundingSources(rawResponse) {
5761
5774
  return result;
5762
5775
  }
5763
5776
  async function backfillInsightsCommand(project, opts) {
5764
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-QIGX7IVQ.js");
5777
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-C76ZRMF5.js");
5765
5778
  const config = loadConfig();
5766
5779
  const db = createClient(config.database);
5767
5780
  migrate(db);
@@ -9452,11 +9465,16 @@ var BROWSER_ADAPTERS = [
9452
9465
  var adapterMap = Object.fromEntries(
9453
9466
  API_ADAPTERS.map((a) => [a.name, a])
9454
9467
  );
9455
- function summarizeProviderConfig(provider, config) {
9468
+ function summarizeProviderConfig(config) {
9456
9469
  return {
9457
9470
  configured: Boolean(config?.apiKey || config?.baseUrl),
9458
9471
  model: config?.model ?? null,
9459
- baseUrl: provider === "local" ? config?.baseUrl ?? null : null,
9472
+ // baseUrl is surfaced for ALL providers, not just local — gemini/openai now
9473
+ // honor a custom endpoint, so repointing one must show in the settings
9474
+ // summary AND produce an audit diff. Omitting it for API providers would let
9475
+ // an endpoint redirect (a credential-exfiltration vector on a box where the
9476
+ // provider key is the carrier) happen with no audit trail.
9477
+ baseUrl: config?.baseUrl ?? null,
9460
9478
  quota: { ...config?.quota ?? DEFAULT_QUOTA }
9461
9479
  };
9462
9480
  }
@@ -10330,7 +10348,7 @@ async function createServer(opts) {
10330
10348
  if (!adapterMap[name]) return null;
10331
10349
  if (!opts.config.providers) opts.config.providers = {};
10332
10350
  const existing = opts.config.providers[name];
10333
- const beforeConfig = summarizeProviderConfig(name, existing);
10351
+ const beforeConfig = summarizeProviderConfig(existing);
10334
10352
  const mergedQuota = incomingQuota ? { ...existing?.quota ?? DEFAULT_QUOTA, ...incomingQuota } : existing?.quota;
10335
10353
  opts.config.providers[name] = {
10336
10354
  apiKey: apiKey || existing?.apiKey,
@@ -10369,7 +10387,7 @@ async function createServer(opts) {
10369
10387
  entry.vertexConfigured = !!opts.config.providers?.[name]?.vertexProject;
10370
10388
  }
10371
10389
  }
10372
- const afterConfig = summarizeProviderConfig(name, opts.config.providers[name]);
10390
+ const afterConfig = summarizeProviderConfig(opts.config.providers[name]);
10373
10391
  if (JSON.stringify(beforeConfig) !== JSON.stringify(afterConfig)) {
10374
10392
  const diff = JSON.stringify({
10375
10393
  before: existing ? beforeConfig : null,
@@ -41,8 +41,8 @@ function authRequired(message = "Authentication required") {
41
41
  function authInvalid() {
42
42
  return new AppError("AUTH_INVALID", "Invalid API key", 401);
43
43
  }
44
- function forbidden(message = "Forbidden") {
45
- return new AppError("FORBIDDEN", message, 403);
44
+ function forbidden(message = "Forbidden", details) {
45
+ return new AppError("FORBIDDEN", message, 403, details);
46
46
  }
47
47
  function quotaExceeded(metric) {
48
48
  return new AppError("QUOTA_EXCEEDED", `Quota exceeded for ${metric}`, 429);
@@ -22,7 +22,7 @@ import {
22
22
  trafficConnectVercelRequestSchema,
23
23
  trafficConnectWordpressRequestSchema,
24
24
  trafficEventKindSchema
25
- } from "./chunk-ETM5EPQH.js";
25
+ } from "./chunk-KPN22EWK.js";
26
26
 
27
27
  // src/config.ts
28
28
  import fs from "fs";
package/dist/cli.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  setTelemetrySource,
28
28
  showFirstRunNotice,
29
29
  trackEvent
30
- } from "./chunk-ZUQK4TLL.js";
30
+ } from "./chunk-FB43IMZT.js";
31
31
  import {
32
32
  CliError,
33
33
  EXIT_SYSTEM_ERROR,
@@ -44,7 +44,7 @@ import {
44
44
  saveConfig,
45
45
  saveConfigPatch,
46
46
  usageError
47
- } from "./chunk-42TDEKKM.js";
47
+ } from "./chunk-XI6YSTGE.js";
48
48
  import {
49
49
  apiKeys,
50
50
  createClient,
@@ -52,7 +52,7 @@ import {
52
52
  projects,
53
53
  queries,
54
54
  renderReportHtml
55
- } from "./chunk-BIT3JPMW.js";
55
+ } from "./chunk-BPZWX7YI.js";
56
56
  import {
57
57
  CcReleaseSyncStatuses,
58
58
  CheckScopes,
@@ -72,7 +72,7 @@ import {
72
72
  providerQuotaPolicySchema,
73
73
  resolveProviderInput,
74
74
  winnabilityClassSchema
75
- } from "./chunk-ETM5EPQH.js";
75
+ } from "./chunk-KPN22EWK.js";
76
76
 
77
77
  // src/cli.ts
78
78
  import { pathToFileURL } from "url";
@@ -9633,6 +9633,7 @@ var envSchema = z.object({
9633
9633
  // Gemini
9634
9634
  GEMINI_API_KEY: z.string().optional(),
9635
9635
  GEMINI_MODEL: z.string().optional(),
9636
+ GEMINI_BASE_URL: z.string().optional(),
9636
9637
  GEMINI_MAX_CONCURRENCY: z.coerce.number().int().positive().default(2),
9637
9638
  GEMINI_MAX_REQUESTS_PER_MINUTE: z.coerce.number().int().positive().default(10),
9638
9639
  GEMINI_MAX_REQUESTS_PER_DAY: z.coerce.number().int().positive().default(1e3),
@@ -9643,6 +9644,7 @@ var envSchema = z.object({
9643
9644
  // OpenAI
9644
9645
  OPENAI_API_KEY: z.string().optional(),
9645
9646
  OPENAI_MODEL: z.string().optional(),
9647
+ OPENAI_BASE_URL: z.string().optional(),
9646
9648
  OPENAI_MAX_CONCURRENCY: z.coerce.number().int().positive().default(2),
9647
9649
  OPENAI_MAX_REQUESTS_PER_MINUTE: z.coerce.number().int().positive().default(10),
9648
9650
  OPENAI_MAX_REQUESTS_PER_DAY: z.coerce.number().int().positive().default(1e3),
@@ -9669,11 +9671,13 @@ var bootstrapEnvSchema = z.object({
9669
9671
  CANONRY_DATABASE_PATH: z.string().optional(),
9670
9672
  GEMINI_API_KEY: z.string().optional(),
9671
9673
  GEMINI_MODEL: z.string().optional(),
9674
+ GEMINI_BASE_URL: z.string().optional(),
9672
9675
  GEMINI_VERTEX_PROJECT: z.string().optional(),
9673
9676
  GEMINI_VERTEX_REGION: z.string().optional(),
9674
9677
  GEMINI_VERTEX_CREDENTIALS: z.string().optional(),
9675
9678
  OPENAI_API_KEY: z.string().optional(),
9676
9679
  OPENAI_MODEL: z.string().optional(),
9680
+ OPENAI_BASE_URL: z.string().optional(),
9677
9681
  ANTHROPIC_API_KEY: z.string().optional(),
9678
9682
  ANTHROPIC_MODEL: z.string().optional(),
9679
9683
  PERPLEXITY_API_KEY: z.string().optional(),
@@ -9692,6 +9696,7 @@ function getBootstrapEnv(source, overrides) {
9692
9696
  providers.gemini = {
9693
9697
  apiKey: parsed.GEMINI_API_KEY ?? "",
9694
9698
  model: parsed.GEMINI_MODEL || "gemini-2.5-flash",
9699
+ baseUrl: parsed.GEMINI_BASE_URL,
9695
9700
  quota: providerQuotaPolicySchema.parse({
9696
9701
  maxConcurrency: 2,
9697
9702
  maxRequestsPerMinute: 10,
@@ -9706,6 +9711,7 @@ function getBootstrapEnv(source, overrides) {
9706
9711
  providers.openai = {
9707
9712
  apiKey: parsed.OPENAI_API_KEY,
9708
9713
  model: parsed.OPENAI_MODEL || "gpt-5.4",
9714
+ baseUrl: parsed.OPENAI_BASE_URL,
9709
9715
  quota: providerQuotaPolicySchema.parse({
9710
9716
  maxConcurrency: 2,
9711
9717
  maxRequestsPerMinute: 10,
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  createServer
3
- } from "./chunk-ZUQK4TLL.js";
3
+ } from "./chunk-FB43IMZT.js";
4
4
  import {
5
5
  loadConfig
6
- } from "./chunk-42TDEKKM.js";
7
- import "./chunk-BIT3JPMW.js";
8
- import "./chunk-ETM5EPQH.js";
6
+ } from "./chunk-XI6YSTGE.js";
7
+ import "./chunk-BPZWX7YI.js";
8
+ import "./chunk-KPN22EWK.js";
9
9
  export {
10
10
  createServer,
11
11
  loadConfig
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  IntelligenceService
3
- } from "./chunk-BIT3JPMW.js";
4
- import "./chunk-ETM5EPQH.js";
3
+ } from "./chunk-BPZWX7YI.js";
4
+ import "./chunk-KPN22EWK.js";
5
5
  export {
6
6
  IntelligenceService
7
7
  };
package/dist/mcp.js CHANGED
@@ -3,8 +3,8 @@ import {
3
3
  PACKAGE_VERSION,
4
4
  canonryMcpTools,
5
5
  createApiClient
6
- } from "./chunk-42TDEKKM.js";
7
- import "./chunk-ETM5EPQH.js";
6
+ } from "./chunk-XI6YSTGE.js";
7
+ import "./chunk-KPN22EWK.js";
8
8
 
9
9
  // src/mcp/cli.ts
10
10
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainyc/canonry",
3
- "version": "4.76.2",
3
+ "version": "4.77.0",
4
4
  "type": "module",
5
5
  "description": "Agent-first open-source AEO operating platform - track how answer engines cite your domain",
6
6
  "license": "FSL-1.1-ALv2",
@@ -64,24 +64,24 @@
64
64
  "tsx": "^4.19.0",
65
65
  "@ainyc/canonry-api-client": "0.0.0",
66
66
  "@ainyc/canonry-api-routes": "0.0.0",
67
+ "@ainyc/canonry-config": "0.0.0",
67
68
  "@ainyc/canonry-contracts": "0.0.0",
68
69
  "@ainyc/canonry-db": "0.0.0",
69
70
  "@ainyc/canonry-integration-bing": "0.0.0",
70
- "@ainyc/canonry-config": "0.0.0",
71
71
  "@ainyc/canonry-integration-cloud-run": "0.0.0",
72
72
  "@ainyc/canonry-integration-commoncrawl": "0.0.0",
73
73
  "@ainyc/canonry-integration-google": "0.0.0",
74
- "@ainyc/canonry-integration-google-places": "0.0.0",
75
74
  "@ainyc/canonry-integration-traffic": "0.0.0",
75
+ "@ainyc/canonry-integration-google-places": "0.0.0",
76
76
  "@ainyc/canonry-integration-google-business-profile": "0.0.0",
77
- "@ainyc/canonry-provider-cdp": "0.0.0",
78
- "@ainyc/canonry-intelligence": "0.0.0",
79
77
  "@ainyc/canonry-integration-wordpress": "0.0.0",
78
+ "@ainyc/canonry-intelligence": "0.0.0",
79
+ "@ainyc/canonry-provider-cdp": "0.0.0",
80
+ "@ainyc/canonry-provider-local": "0.0.0",
80
81
  "@ainyc/canonry-provider-gemini": "0.0.0",
81
- "@ainyc/canonry-provider-claude": "0.0.0",
82
82
  "@ainyc/canonry-provider-openai": "0.0.0",
83
- "@ainyc/canonry-provider-perplexity": "0.0.0",
84
- "@ainyc/canonry-provider-local": "0.0.0"
83
+ "@ainyc/canonry-provider-claude": "0.0.0",
84
+ "@ainyc/canonry-provider-perplexity": "0.0.0"
85
85
  },
86
86
  "scripts": {
87
87
  "build": "tsx scripts/copy-agent-assets.ts && tsup && tsx build-web.ts",
@@ -1 +0,0 @@
1
- import{r as l,j as e,h as V,u as R,f as L,L as U}from"./vendor-tanstack-Dq7p98wZ.js";import{c as T,bS as E,bT as $,bU as I,bV as q,bW as A,bX as B,B as v,i as O,bY as W,b4 as _,l as F,bZ as k,g as C,b_ as G,T as H,b$ as M}from"./index-B-_Dwwhd.js";import{a as J,b as Q,c as Z,d as K,t as Y,R as X}from"./server-traffic-ScU00kCP.js";import{e as P,A as ee}from"./extract-error-message-BdLppivS.js";import"./vendor-radix-B57xfQbP.js";import"./vendor-recharts-ClRVR6aX.js";import"./vendor-markdown-DK7fbRNb.js";const te=[["path",{d:"M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z",key:"p7xjir"}]],ne=T("cloud",te);const re=[["path",{d:"M13.73 4a2 2 0 0 0-3.46 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z",key:"14u9p9"}]],se=T("triangle",re);function oe({open:t,onOpenChange:a,projectName:s}){const[r,n]=l.useState("pick");l.useEffect(()=>{t&&n("pick")},[t]);const o=()=>{a(!1),n("pick")};return e.jsx(E,{open:t,onOpenChange:i=>{i?a(!0):o()},children:e.jsx($,{children:r==="pick"?e.jsx(ie,{onPick:n}):r==="wordpress"?e.jsx(de,{projectName:s,onBack:()=>n("pick"),onClose:o}):r==="vercel"?e.jsx(pe,{projectName:s,onBack:()=>n("pick"),onClose:o}):e.jsx(ue,{projectName:s,onBack:()=>n("pick"),onClose:o})})})}const ae=[{type:"wordpress",name:"WordPress site",tagline:"Easiest if you run WordPress",description:"Install the Canonry Traffic Logger plugin and connect with an Application Password. No cloud account needed.",icon:B},{type:"cloud-run",name:"Google Cloud Run",tagline:"For apps hosted on Cloud Run",description:"Connect a Google Cloud service account so Canonry can read your Cloud Run request logs.",icon:ne},{type:"vercel",name:"Vercel project",tagline:"For sites hosted on Vercel",description:"Connect a Vercel personal access token so Canonry can pull request logs straight from Vercel, no in-app instrumentation needed.",icon:se}];function ie({onPick:t}){return e.jsxs(e.Fragment,{children:[e.jsxs(I,{children:[e.jsx(q,{children:"Connect a traffic source"}),e.jsx(A,{children:"Canonry reads your server logs to see when AI crawlers and AI-referred visitors hit your site. Pick where your site is hosted to get started."})]}),e.jsx("div",{className:"mt-6 flex flex-col gap-3",children:ae.map(({type:a,name:s,tagline:r,description:n,icon:o})=>e.jsxs("button",{type:"button",onClick:()=>t(a),className:"group flex items-start gap-4 rounded-lg border border-zinc-800 bg-zinc-900/30 p-4 text-left transition-colors hover:border-zinc-600 hover:bg-zinc-900/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400",children:[e.jsx("span",{className:"mt-0.5 inline-flex size-9 shrink-0 items-center justify-center rounded-md border border-zinc-800 bg-zinc-950 text-zinc-300 group-hover:text-zinc-100",children:e.jsx(o,{className:"size-4"})}),e.jsxs("span",{className:"flex flex-col gap-0.5",children:[e.jsxs("span",{className:"flex flex-wrap items-baseline gap-x-2",children:[e.jsx("span",{className:"text-sm font-medium text-zinc-100",children:s}),e.jsx("span",{className:"text-[11px] text-zinc-500",children:r})]}),e.jsx("span",{className:"text-xs leading-5 text-zinc-500",children:n})]})]},a))})]})}function ce({title:t,description:a,onBack:s}){return e.jsxs(I,{children:[e.jsxs("button",{type:"button",onClick:s,className:"mb-1 inline-flex w-fit items-center gap-1 rounded text-xs text-zinc-500 transition-colors hover:text-zinc-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400",children:[e.jsx(ee,{className:"size-3"}),"Choose a different source"]}),e.jsx(q,{children:t}),e.jsx(A,{children:a})]})}function le(t,a){const s=V();return async(r,n)=>{await W(t,r),n?.(),a(),s({to:"/traffic/$projectName/$sourceId",params:{projectName:t,sourceId:r}})}}function w(t,a){const[s,r]=l.useState(null),n=le(t,a);return{error:s,runConnect:async i=>{r(null);const d=i.validate();if(d){r(d);return}let u;try{u=await i.mutate()}catch(c){r(P(c));return}try{await n(u.id,i.onConnected)}catch(c){r(`Source connected, but starting the initial backfill failed: ${P(c)}`)}}}}function S({title:t,description:a,projectName:s,onBack:r,onClose:n,onSubmit:o,isPending:i,error:d,children:u}){return e.jsxs(e.Fragment,{children:[e.jsx(ce,{title:t,description:a,onBack:r}),e.jsxs("form",{onSubmit:O(async c=>{c.preventDefault(),await o()}),className:"mt-6 flex flex-col gap-5 overflow-y-auto pr-1",children:[e.jsx(p,{label:"Project",description:"Canonry project this source attaches to.",children:e.jsx("input",{type:"text",value:s,disabled:!0,className:"w-full rounded border border-zinc-700 bg-zinc-900/50 px-2 py-1.5 text-sm text-zinc-300"})}),u,d?e.jsx("p",{className:"rounded-md border border-rose-800/50 bg-rose-950/30 px-3 py-2 text-xs text-rose-200",children:d}):null,e.jsxs("div",{className:"mt-2 flex items-center justify-end gap-2 border-t border-zinc-800/60 pt-4",children:[e.jsx(v,{type:"button",variant:"ghost",size:"sm",onClick:n,children:"Close"}),e.jsx(v,{type:"submit",disabled:i,size:"sm",children:i?"Connecting…":"Connect"})]})]})]})}function de({projectName:t,onBack:a,onClose:s}){const[r,n]=l.useState(""),[o,i]=l.useState(""),[d,u]=l.useState(""),[c,j]=l.useState(""),f=J(t||null),{error:g,runConnect:b}=w(t,s),y=()=>b({validate:()=>r.trim()?o.trim()?d.trim()?null:"Application Password is required.":"Username is required.":"WordPress site URL is required.",mutate:()=>f.mutateAsync({baseUrl:r.trim(),username:o.trim(),applicationPassword:d.trim(),displayName:c.trim()||void 0}),onConnected:()=>u("")});return e.jsxs(S,{title:"Connect a WordPress site",description:e.jsxs(e.Fragment,{children:["Pulls request events from the Canonry Traffic Logger plugin. The Application Password is stored in ",e.jsx("code",{children:"~/.canonry/config.yaml"})," on the server and never echoed back to the dashboard."]}),projectName:t,onBack:a,onClose:s,onSubmit:y,isPending:f.isPending,error:g,children:[e.jsx(p,{label:"WordPress site URL",description:"Base URL of the site running the Canonry Traffic Logger plugin.",required:!0,children:e.jsx("input",{type:"url",value:r,onChange:m=>n(m.target.value),required:!0,autoComplete:"url",placeholder:"https://example.com",className:"w-full rounded border border-zinc-700 bg-transparent px-2 py-1.5 text-sm text-zinc-200 placeholder-zinc-600 focus:border-zinc-500 focus:outline-none"})}),e.jsx(p,{label:"Username",description:"WordPress user that owns the Application Password.",required:!0,children:e.jsx("input",{type:"text",value:o,onChange:m=>i(m.target.value),required:!0,autoComplete:"username",className:"w-full rounded border border-zinc-700 bg-transparent px-2 py-1.5 text-sm text-zinc-200 placeholder-zinc-600 focus:border-zinc-500 focus:outline-none"})}),e.jsx(p,{label:"Application Password",description:"Create one in wp-admin under Users -> Profile -> Application Passwords.",required:!0,children:e.jsx("input",{type:"password",value:d,onChange:m=>u(m.target.value),required:!0,autoComplete:"new-password",className:"w-full rounded border border-zinc-700 bg-transparent px-2 py-1.5 text-sm text-zinc-200 placeholder-zinc-600 focus:border-zinc-500 focus:outline-none"})}),e.jsx(p,{label:"Display name (optional)",description:"Friendly label shown in the dashboard. Defaults to the WordPress host.",children:e.jsx("input",{type:"text",value:c,onChange:m=>j(m.target.value),autoComplete:"off",className:"w-full rounded border border-zinc-700 bg-transparent px-2 py-1.5 text-sm text-zinc-200 placeholder-zinc-600 focus:border-zinc-500 focus:outline-none"})})]})}function ue({projectName:t,onBack:a,onClose:s}){const[r,n]=l.useState(""),[o,i]=l.useState(""),[d,u]=l.useState(""),[c,j]=l.useState(""),[f,g]=l.useState(""),b=Z(t||null),{error:y,runConnect:m}=w(t,s),z=()=>m({validate:()=>r.trim()?f.trim()?null:"Service-account JSON content is required.":"GCP project ID is required.",mutate:()=>b.mutateAsync({gcpProjectId:r.trim(),serviceName:o.trim()||void 0,location:d.trim()||void 0,displayName:c.trim()||void 0,keyJson:f.trim()}),onConnected:()=>g("")}),h=async x=>{if(!x)return;const D=await x.text();g(D)};return e.jsxs(S,{title:"Connect a Cloud Run service",description:e.jsxs(e.Fragment,{children:["v1 supports service-account JSON only. The private key is stored in"," ",e.jsx("code",{children:"~/.canonry/config.yaml"})," on the server and never echoed back to the dashboard."]}),projectName:t,onBack:a,onClose:s,onSubmit:z,isPending:b.isPending,error:y,children:[e.jsx(p,{label:"GCP project ID",description:"The Google Cloud project hosting the Cloud Run service (e.g. my-prod-foo).",required:!0,children:e.jsx("input",{type:"text",value:r,onChange:x=>n(x.target.value),required:!0,autoComplete:"off",className:"w-full rounded border border-zinc-700 bg-transparent px-2 py-1.5 text-sm text-zinc-200 placeholder-zinc-600 focus:border-zinc-500 focus:outline-none"})}),e.jsx(p,{label:"Service name (optional)",description:"Restrict log pulls to a specific Cloud Run service. Omit to pull all services in the project.",children:e.jsx("input",{type:"text",value:o,onChange:x=>i(x.target.value),autoComplete:"off",className:"w-full rounded border border-zinc-700 bg-transparent px-2 py-1.5 text-sm text-zinc-200 placeholder-zinc-600 focus:border-zinc-500 focus:outline-none"})}),e.jsx(p,{label:"Location (optional)",description:"Region of the Cloud Run service (e.g. us-central1). Helpful when multiple regions emit logs.",children:e.jsx("input",{type:"text",value:d,onChange:x=>u(x.target.value),autoComplete:"off",className:"w-full rounded border border-zinc-700 bg-transparent px-2 py-1.5 text-sm text-zinc-200 placeholder-zinc-600 focus:border-zinc-500 focus:outline-none"})}),e.jsx(p,{label:"Display name (optional)",description:"Friendly label shown in the dashboard. Defaults to the project + service combo.",children:e.jsx("input",{type:"text",value:c,onChange:x=>j(x.target.value),autoComplete:"off",className:"w-full rounded border border-zinc-700 bg-transparent px-2 py-1.5 text-sm text-zinc-200 placeholder-zinc-600 focus:border-zinc-500 focus:outline-none"})}),e.jsxs(p,{label:"Service-account JSON",description:"Paste the contents of the SA key (JSON). The SA needs roles/logging.viewer (or any role granting logging.logEntries.list).",required:!0,children:[e.jsx("textarea",{value:f,onChange:x=>g(x.target.value),rows:6,spellCheck:!1,autoComplete:"off",className:"w-full rounded border border-zinc-700 bg-transparent px-2 py-1.5 font-mono text-[11px] text-zinc-200 placeholder-zinc-600 focus:border-zinc-500 focus:outline-none",placeholder:'{"type":"service_account","project_id":"…","private_key":"…"}',required:!0}),e.jsxs("label",{className:"mt-2 inline-flex cursor-pointer items-center gap-2 text-xs text-zinc-400 hover:text-zinc-200",children:[e.jsx("input",{type:"file",accept:"application/json,.json",className:"hidden",onChange:x=>{h(x.target.files?.[0]??null)}}),e.jsx("span",{className:"rounded-md border border-zinc-800 px-2 py-1",children:"Or upload a key file"})]})]})]})}function pe({projectName:t,onBack:a,onClose:s}){const[r,n]=l.useState(""),[o,i]=l.useState(""),[d,u]=l.useState(""),[c,j]=l.useState("production"),[f,g]=l.useState(""),b=Q(t||null),{error:y,runConnect:m}=w(t,s),z=()=>m({validate:()=>r.trim()?o.trim()?d.trim()?null:"Vercel personal access token is required.":"Vercel team / account ID is required.":"Vercel project ID is required.",mutate:()=>b.mutateAsync({projectId:r.trim(),teamId:o.trim(),token:d.trim(),environment:c,displayName:f.trim()||void 0}),onConnected:()=>u("")});return e.jsxs(S,{title:"Connect a Vercel project",description:e.jsxs(e.Fragment,{children:["Pulls request logs straight from Vercel, no in-app instrumentation needed. The personal access token is stored in ",e.jsx("code",{children:"~/.canonry/config.yaml"})," on the server and never echoed back to the dashboard."]}),projectName:t,onBack:a,onClose:s,onSubmit:z,isPending:b.isPending,error:y,children:[e.jsx(p,{label:"Vercel project ID",description:"The prj_… id from the Vercel dashboard or .vercel/project.json.",required:!0,children:e.jsx("input",{type:"text",value:r,onChange:h=>n(h.target.value),required:!0,autoComplete:"off",placeholder:"prj_…",className:"w-full rounded border border-zinc-700 bg-transparent px-2 py-1.5 text-sm text-zinc-200 placeholder-zinc-600 focus:border-zinc-500 focus:outline-none"})}),e.jsx(p,{label:"Vercel team / account ID",description:"The Vercel team or personal account that owns the project. Find it as orgId in your .vercel/project.json.",required:!0,children:e.jsx("input",{type:"text",value:o,onChange:h=>i(h.target.value),required:!0,autoComplete:"off",className:"w-full rounded border border-zinc-700 bg-transparent px-2 py-1.5 text-sm text-zinc-200 placeholder-zinc-600 focus:border-zinc-500 focus:outline-none"})}),e.jsx(p,{label:"Personal access token",description:"Create a Vercel personal access token under Account Settings → Tokens. Tokens can expire, so use a long-lived one.",required:!0,children:e.jsx("input",{type:"password",value:d,onChange:h=>u(h.target.value),required:!0,autoComplete:"new-password",className:"w-full rounded border border-zinc-700 bg-transparent px-2 py-1.5 text-sm text-zinc-200 placeholder-zinc-600 focus:border-zinc-500 focus:outline-none"})}),e.jsx(p,{label:"Environment",description:"Which deployment environment's request logs to pull.",children:e.jsxs("select",{value:c,onChange:h=>j(h.target.value),className:"w-full rounded border border-zinc-700 bg-transparent px-2 py-1.5 text-sm text-zinc-200 focus:border-zinc-500 focus:outline-none",children:[e.jsx("option",{value:"production",children:"production"}),e.jsx("option",{value:"preview",children:"preview"})]})}),e.jsx(p,{label:"Display name (optional)",description:"Friendly label shown in the dashboard. Defaults to the Vercel project ID.",children:e.jsx("input",{type:"text",value:f,onChange:h=>g(h.target.value),autoComplete:"off",className:"w-full rounded border border-zinc-700 bg-transparent px-2 py-1.5 text-sm text-zinc-200 placeholder-zinc-600 focus:border-zinc-500 focus:outline-none"})})]})}function p({label:t,description:a,required:s,children:r}){return e.jsxs("label",{className:"flex flex-col gap-1",children:[e.jsxs("span",{className:"text-xs font-medium text-zinc-200",children:[t,s?e.jsx("span",{className:"ml-1 text-rose-400",children:"*"}):null]}),r,e.jsx("span",{className:"text-[11px] text-zinc-500",children:a})]})}function xe(t){if(!t)return"never";const a=Date.now()-new Date(t).getTime(),s=Math.floor(a/6e4);if(s<1)return"just now";if(s<60)return`${s}m ago`;const r=Math.floor(s/60);return r<24?`${r}h ago`:`${Math.floor(r/24)}d ago`}function N(t){return t>=1e6?`${(t/1e6).toFixed(1)}M`:t>=1e3?`${(t/1e3).toFixed(1)}K`:t.toLocaleString()}function ze(){const[t,a]=l.useState(""),[s,r]=l.useState(!1),o=R(_({client:F})).data??[],i=l.useMemo(()=>t||(o[0]?.name??""),[t,o]),d=K(i||null),u=d.data?.sources??[];return e.jsxs("div",{className:"page-container",children:[e.jsxs("div",{className:"page-header",children:[e.jsxs("div",{className:"page-header-left",children:[e.jsx("h1",{className:"page-title",children:"Server traffic"}),e.jsx("p",{className:"page-subtitle",children:"Bulk crawler hits, AI user fetches (ChatGPT-User, Perplexity-User), and AI referral sessions pulled directly from your server logs. Independent of GA — useful when you need server-side evidence that an AI engine actually hit a page."})]}),e.jsx("div",{className:"flex flex-wrap items-center justify-end gap-2",children:e.jsxs(v,{type:"button",variant:"outline",size:"sm",onClick:()=>r(!0),disabled:!i,children:[e.jsx(k,{className:"size-3.5"}),"Connect a source"]})})]}),e.jsxs("section",{children:[e.jsx("div",{className:"filter-row",role:"toolbar","aria-label":"Project picker",children:o.map(c=>e.jsx("button",{type:"button",className:`filter-chip ${i===c.name?"filter-chip-active":""}`,"aria-pressed":i===c.name,onClick:()=>a(c.name),children:c.displayName??c.name},c.id))}),i?d.isLoading?e.jsx(C,{className:"p-6 text-center text-sm text-zinc-500",children:"Loading sources…"}):u.length===0?e.jsxs(C,{className:"p-8 text-center",children:[e.jsxs("p",{className:"text-sm text-zinc-300",children:["No traffic sources connected for ",i,"."]}),e.jsx("p",{className:"mt-1 text-xs text-zinc-500",children:"Connect a traffic source to start ingesting crawler hits and AI-referral sessions from your server logs."}),e.jsx("div",{className:"mt-4 flex flex-wrap items-center justify-center gap-2",children:e.jsxs(v,{type:"button",variant:"outline",size:"sm",onClick:()=>r(!0),children:[e.jsx(k,{className:"size-3.5"}),"Connect a source"]})})]}):e.jsx(me,{projectName:i,sources:u}):e.jsx(C,{className:"p-6 text-center text-sm text-zinc-500",children:"No projects yet."})]}),e.jsx(oe,{open:s,onOpenChange:r,projectName:i})]})}function me({projectName:t,sources:a}){const s=L({queries:a.map(n=>({...G({client:F,path:{name:t,id:n.id}}),staleTime:3e4}))}),r=a.map((n,o)=>({source:n,detail:s[o]?.data,isLoading:s[o]?.isLoading??!1}));return e.jsxs("div",{className:"rounded-xl border border-zinc-800/60 bg-zinc-900/30 overflow-hidden",children:[e.jsxs("table",{className:"w-full text-sm",children:[e.jsx("thead",{className:"bg-zinc-900/50 text-[10px] font-semibold uppercase tracking-wider text-zinc-500",children:e.jsxs("tr",{children:[e.jsx("th",{className:"px-4 py-2 text-left",children:"Source"}),e.jsx("th",{className:"px-4 py-2 text-left",children:"Status"}),e.jsx("th",{className:"px-4 py-2 text-left",children:"Last sync"}),e.jsx("th",{className:"px-4 py-2 text-right",children:"24h crawler"}),e.jsx("th",{className:"px-4 py-2 text-right",children:"24h AI hits"}),e.jsx("th",{className:"px-4 py-2 text-right",children:"24h AI sessions"}),e.jsx("th",{className:"px-4 py-2 text-right"})]})}),e.jsx("tbody",{className:"divide-y divide-zinc-800/60",children:r.map(({source:n,detail:o,isLoading:i})=>e.jsxs("tr",{className:"hover:bg-zinc-900/40 transition-colors",children:[e.jsxs("td",{className:"px-4 py-3",children:[e.jsx("div",{className:"font-medium text-zinc-100",children:n.displayName}),e.jsxs("div",{className:"text-[11px] text-zinc-500 font-mono",children:[n.sourceType," · ",n.id.slice(0,8)]})]}),e.jsxs("td",{className:"px-4 py-3",children:[e.jsx(H,{tone:Y(n.status),children:n.status}),n.lastError?e.jsx("p",{className:"mt-1 max-w-[18rem] truncate text-[11px] text-rose-400/80",title:n.lastError,children:n.lastError}):null]}),e.jsx("td",{className:"px-4 py-3 text-zinc-300",children:xe(n.lastSyncedAt)}),e.jsx("td",{className:"px-4 py-3 text-right tabular-nums text-zinc-100",children:i?"—":N(o?.totals24h.crawlerHits??0)}),e.jsx("td",{className:"px-4 py-3 text-right tabular-nums text-zinc-100",children:i?"—":N(o?.totals24h.aiUserFetchHits??0)}),e.jsx("td",{className:"px-4 py-3 text-right tabular-nums text-zinc-100",children:i?"—":N(o?.totals24h.aiReferralHits??0)}),e.jsx("td",{className:"px-4 py-3 text-right",children:e.jsxs(U,{to:"/traffic/$projectName/$sourceId",params:{projectName:t,sourceId:n.id},className:"inline-flex items-center gap-1 text-xs text-zinc-300 hover:text-zinc-100",children:[e.jsx(X,{className:"size-3"}),"View"]})})]},n.id))})]}),e.jsxs("p",{className:"border-t border-zinc-800/60 px-4 py-2 text-[11px] text-zinc-600",children:["Showing ",a.filter(n=>n.status!==M.archived).length," active source",a.length===1?"":"s"," for ",t,". Same shape as ",e.jsxs("code",{className:"text-zinc-400",children:["canonry traffic status ",t," --format json"]}),"."]})]})}export{ze as TrafficPage};