@canonry/canonry 4.136.0 → 4.136.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +4 -0
  2. package/assets/agent-workspace/skills/canonry/references/indexing.md +26 -12
  3. package/assets/assets/{AuditHistoryPanel-DKaiIlcO.js → AuditHistoryPanel-B73VrWNb.js} +1 -1
  4. package/assets/assets/{BacklinksPage-Z2_ws8c2.js → BacklinksPage-CLVMg_qx.js} +1 -1
  5. package/assets/assets/{ChartPrimitives-CF_ocuvj.js → ChartPrimitives-P81CbvWc.js} +1 -1
  6. package/assets/assets/{HistoryPage-DDKO8_D8.js → HistoryPage-IcDqWBYW.js} +1 -1
  7. package/assets/assets/ProjectPage-DTivvVIl.js +8 -0
  8. package/assets/assets/{RunRow-BhtCgyqA.js → RunRow-BTH3-Tml.js} +1 -1
  9. package/assets/assets/{RunsPage-B8Nw3wP-.js → RunsPage-B0k5wjFN.js} +1 -1
  10. package/assets/assets/{SettingsPage-DsmQhytl.js → SettingsPage-DGnT3Vdr.js} +1 -1
  11. package/assets/assets/{TrafficPage-COJs6eRH.js → TrafficPage-D79PC0bB.js} +1 -1
  12. package/assets/assets/{TrafficSourceDetailPage-BI6IwzMQ.js → TrafficSourceDetailPage-BYEVjfFm.js} +1 -1
  13. package/assets/assets/{arrow-left-C-lGPq-u.js → arrow-left-DCdFYJK-.js} +1 -1
  14. package/assets/assets/{extract-error-message-8h0hF1I1.js → extract-error-message-bM0aUBUI.js} +1 -1
  15. package/assets/assets/index-CMuA-uEN.js +210 -0
  16. package/assets/assets/index-CglduHCN.css +1 -0
  17. package/assets/assets/{trash-2-CnOZwYKe.js → trash-2-CEz2iXBv.js} +1 -1
  18. package/assets/index.html +2 -2
  19. package/dist/{chunk-PAR6P5H6.js → chunk-OESGFFUO.js} +132 -6
  20. package/dist/{chunk-PXEEWJRV.js → chunk-PIMWEIJ3.js} +51 -2
  21. package/dist/{chunk-3PFRVEWP.js → chunk-YMF35LN5.js} +4 -4
  22. package/dist/{chunk-TPVMHYHD.js → chunk-YQ2RXMU2.js} +205 -27
  23. package/dist/cli.js +184 -15
  24. package/dist/index.js +4 -4
  25. package/dist/{intelligence-service-BMEJ5GFZ.js → intelligence-service-BKLR57RQ.js} +2 -2
  26. package/dist/mcp.js +2 -2
  27. package/package.json +10 -10
  28. package/assets/assets/ProjectPage-hmNsqBov.js +0 -8
  29. package/assets/assets/index-B4ksyQXo.css +0 -1
  30. package/assets/assets/index-BoSfFv1t.js +0 -210
@@ -42,7 +42,7 @@ import {
42
42
  trafficConnectWordpressRequestSchema,
43
43
  trafficEventKindSchema,
44
44
  trafficSeriesGranularitySchema
45
- } from "./chunk-PXEEWJRV.js";
45
+ } from "./chunk-PIMWEIJ3.js";
46
46
 
47
47
  // src/config.ts
48
48
  import fs from "fs";
@@ -1087,6 +1087,22 @@ var createClient = (config = {}) => {
1087
1087
  var client = createClient(createConfig());
1088
1088
 
1089
1089
  // ../api-client-generated/src/generated/sdk.gen.ts
1090
+ var postApiV1ProjectsByNameGoogleGscSitemapsSubmit = (options) => {
1091
+ return (options.client ?? client).post({
1092
+ security: [
1093
+ {
1094
+ scheme: "bearer",
1095
+ type: "http"
1096
+ }
1097
+ ],
1098
+ url: "/api/v1/projects/{name}/google/gsc/sitemaps/submit",
1099
+ ...options,
1100
+ headers: {
1101
+ "Content-Type": "application/json",
1102
+ ...options.headers
1103
+ }
1104
+ });
1105
+ };
1090
1106
  var deleteApiV1ProjectsByName = (options) => {
1091
1107
  return (options.client ?? client).delete({
1092
1108
  security: [
@@ -5181,9 +5197,22 @@ var ApiClient = class {
5181
5197
  })
5182
5198
  );
5183
5199
  }
5184
- async gscSitemaps(project) {
5200
+ async gscSitemaps(project, params) {
5201
+ return this.invoke(
5202
+ () => getApiV1ProjectsByNameGoogleGscSitemaps({
5203
+ client: this.heyClient,
5204
+ path: { name: project },
5205
+ query: { sitemapIndex: params?.sitemapIndex }
5206
+ })
5207
+ );
5208
+ }
5209
+ async gscSubmitSitemaps(project, body) {
5185
5210
  return this.invoke(
5186
- () => getApiV1ProjectsByNameGoogleGscSitemaps({ client: this.heyClient, path: { name: project } })
5211
+ () => postApiV1ProjectsByNameGoogleGscSitemapsSubmit({
5212
+ client: this.heyClient,
5213
+ path: { name: project },
5214
+ body
5215
+ })
5187
5216
  );
5188
5217
  }
5189
5218
  async gscDiscoverSitemaps(project) {
@@ -6149,6 +6178,92 @@ var gscCoverageHistoryInputSchema = z2.object({
6149
6178
  project: projectNameSchema,
6150
6179
  limit: z2.number().int().positive().max(500).optional()
6151
6180
  });
6181
+ var gscSitemapsInputSchema = z2.object({
6182
+ project: projectNameSchema,
6183
+ sitemapIndex: z2.string().url().optional()
6184
+ });
6185
+ var gscSitemapsSubmitInputSchema = z2.union([
6186
+ z2.object({
6187
+ project: projectNameSchema,
6188
+ sitemapUrls: z2.array(z2.string().url()).min(1).max(50)
6189
+ }).strict(),
6190
+ z2.object({
6191
+ project: projectNameSchema,
6192
+ mode: z2.enum(["indexes", "all-files"])
6193
+ }).strict()
6194
+ ]);
6195
+ async function submitGscSitemapsFromMcp(client2, input) {
6196
+ let sitemapUrls;
6197
+ if ("sitemapUrls" in input) {
6198
+ sitemapUrls = uniqueStrings(input.sitemapUrls);
6199
+ } else {
6200
+ const topLevel = await client2.gscSitemaps(input.project);
6201
+ if (input.mode === "indexes") {
6202
+ sitemapUrls = uniqueStrings(
6203
+ topLevel.preferredSubmissionUrls.length > 0 ? topLevel.preferredSubmissionUrls : topLevel.sitemaps.map((sitemap) => sitemap.path)
6204
+ );
6205
+ } else {
6206
+ const indexes = topLevel.sitemaps.filter((sitemap) => sitemap.isSitemapsIndex).map((sitemap) => sitemap.path);
6207
+ const expandedIndexUrls = [];
6208
+ for (let offset = 0; offset < indexes.length; offset += 4) {
6209
+ const children = await Promise.all(
6210
+ indexes.slice(offset, offset + 4).map(
6211
+ (sitemapIndex) => client2.gscSitemaps(input.project, { sitemapIndex })
6212
+ )
6213
+ );
6214
+ children.forEach((result, index) => {
6215
+ const sitemapIndex = indexes[offset + index];
6216
+ expandedIndexUrls.push(...result.sitemaps.length > 0 ? result.sitemaps.map((sitemap) => sitemap.path) : [sitemapIndex]);
6217
+ });
6218
+ }
6219
+ sitemapUrls = uniqueStrings([
6220
+ ...topLevel.sitemaps.filter((sitemap) => !sitemap.isSitemapsIndex).map((sitemap) => sitemap.path),
6221
+ ...expandedIndexUrls
6222
+ ]);
6223
+ }
6224
+ }
6225
+ if (sitemapUrls.length === 0) {
6226
+ throw new Error("No GSC sitemaps found. Submit an explicit sitemap URL first.");
6227
+ }
6228
+ const aggregate = {
6229
+ summary: { total: 0, accepted: 0, failed: 0 },
6230
+ results: []
6231
+ };
6232
+ for (let offset = 0; offset < sitemapUrls.length; offset += 50) {
6233
+ const batchSitemapUrls = sitemapUrls.slice(offset, offset + 50);
6234
+ try {
6235
+ const result = await client2.gscSubmitSitemaps(input.project, {
6236
+ sitemapUrls: batchSitemapUrls
6237
+ });
6238
+ aggregate.summary.total += result.summary.total;
6239
+ aggregate.summary.accepted += result.summary.accepted;
6240
+ aggregate.summary.failed += result.summary.failed;
6241
+ aggregate.results.push(...result.results);
6242
+ } catch (cause) {
6243
+ if (offset === 0) throw cause;
6244
+ const attempted = aggregate.summary.total + batchSitemapUrls.length;
6245
+ const remaining = sitemapUrls.length - attempted;
6246
+ throw new CliError({
6247
+ code: "GOOGLE_SITEMAP_SUBMISSION_PARTIAL",
6248
+ message: `Sitemap submission stopped after ${aggregate.summary.total} completed results; ${batchSitemapUrls.length} are unconfirmed and ${remaining} were not attempted.`,
6249
+ exitCode: cause instanceof CliError && cause.exitCode === 1 ? 1 : EXIT_SYSTEM_ERROR,
6250
+ details: {
6251
+ project: input.project,
6252
+ accepted: aggregate.summary.accepted,
6253
+ failed: aggregate.summary.failed,
6254
+ completed: aggregate.summary.total,
6255
+ attempted,
6256
+ unconfirmed: batchSitemapUrls.length,
6257
+ remaining,
6258
+ unconfirmedBatch: { index: Math.floor(offset / 50) + 1, sitemapUrls: batchSitemapUrls },
6259
+ partialResult: aggregate,
6260
+ cause: cause instanceof CliError ? { code: cause.code, message: cause.message, details: cause.details } : { message: cause instanceof Error ? cause.message : String(cause) }
6261
+ }
6262
+ });
6263
+ }
6264
+ }
6265
+ return aggregate;
6266
+ }
6152
6267
  var gaWindowInputSchema = z2.object({
6153
6268
  project: projectNameSchema,
6154
6269
  window: analyticsWindowSchema.optional()
@@ -6486,7 +6601,7 @@ var canonryMcpTools = [
6486
6601
  description: "Get a Canonry project by name.",
6487
6602
  access: "read",
6488
6603
  tier: "core",
6489
- inputSchema: projectInputSchema,
6604
+ inputSchema: gscSitemapsInputSchema,
6490
6605
  annotations: readAnnotations(),
6491
6606
  openApiOperations: ["GET /api/v1/projects/{name}"],
6492
6607
  handler: (client2, input) => client2.getProject(input.project)
@@ -7048,10 +7163,21 @@ var canonryMcpTools = [
7048
7163
  description: "Get sitemap data from Google Search Console for a Canonry project.",
7049
7164
  access: "read",
7050
7165
  tier: "gsc",
7051
- inputSchema: projectInputSchema,
7166
+ inputSchema: gscSitemapsInputSchema,
7052
7167
  annotations: readAnnotations(true),
7053
7168
  openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/sitemaps"],
7054
- handler: (client2, input) => client2.gscSitemaps(input.project)
7169
+ handler: (client2, input) => client2.gscSitemaps(input.project, { sitemapIndex: input.sitemapIndex })
7170
+ }),
7171
+ defineTool({
7172
+ name: "canonry_gsc_sitemaps_submit",
7173
+ title: "Submit GSC sitemaps",
7174
+ description: "Submit up to 50 explicit sitemap URLs, preferred sitemap indexes, or every standalone top-level file plus index child to Google Search Console for refetching. Parent indexes are not redundantly submitted in all-files mode. Google acceptance does not guarantee indexing.",
7175
+ access: "write",
7176
+ tier: "gsc",
7177
+ inputSchema: gscSitemapsSubmitInputSchema,
7178
+ annotations: writeAnnotations({ idempotentHint: false, openWorldHint: true }),
7179
+ openApiOperations: ["POST /api/v1/projects/{name}/google/gsc/sitemaps/submit"],
7180
+ handler: submitGscSitemapsFromMcp
7055
7181
  }),
7056
7182
  defineTool({
7057
7183
  name: "canonry_ga_status",
@@ -1493,10 +1493,13 @@ var gscSiteListResponseDtoSchema = z9.object({
1493
1493
  var gscSitemapContentDtoSchema = z9.object({
1494
1494
  type: z9.string(),
1495
1495
  submitted: z9.string(),
1496
- indexed: z9.string()
1496
+ // Google still returns this for existing sitemap reads, but it is not a
1497
+ // reliable submission outcome. Retained only for response compatibility.
1498
+ indexed: z9.string().optional().describe("Deprecated compatibility field.")
1497
1499
  });
1498
1500
  var gscSitemapDtoSchema = z9.object({
1499
1501
  path: z9.string(),
1502
+ parentSitemapUrl: z9.string().optional(),
1500
1503
  lastSubmitted: z9.string().optional(),
1501
1504
  isPending: z9.boolean().optional(),
1502
1505
  isSitemapsIndex: z9.boolean().optional(),
@@ -1507,7 +1510,50 @@ var gscSitemapDtoSchema = z9.object({
1507
1510
  contents: z9.array(gscSitemapContentDtoSchema).optional()
1508
1511
  });
1509
1512
  var gscSitemapListResponseDtoSchema = z9.object({
1510
- sitemaps: z9.array(gscSitemapDtoSchema).default([])
1513
+ sitemaps: z9.array(gscSitemapDtoSchema).default([]),
1514
+ summary: z9.object({
1515
+ total: z9.number().int().nonnegative(),
1516
+ indexes: z9.number().int().nonnegative(),
1517
+ files: z9.number().int().nonnegative()
1518
+ }),
1519
+ preferredSubmissionUrls: z9.array(z9.string()).default([])
1520
+ });
1521
+ var gscSubmitSitemapsRequestDtoSchema = z9.object({
1522
+ sitemapUrls: z9.array(
1523
+ z9.string().url().refine(
1524
+ (value) => {
1525
+ const protocol = new URL(value).protocol;
1526
+ return protocol === "http:" || protocol === "https:";
1527
+ },
1528
+ { message: "Sitemap URL must use http or https." }
1529
+ )
1530
+ ).min(1).max(50)
1531
+ });
1532
+ var gscSubmitSitemapResultDtoSchema = z9.object({
1533
+ sitemapUrl: z9.string(),
1534
+ status: z9.enum(["accepted", "error"]),
1535
+ submittedAt: z9.string().optional(),
1536
+ error: z9.string().optional()
1537
+ });
1538
+ var gscSubmitSitemapsResponseDtoSchema = z9.object({
1539
+ summary: z9.object({
1540
+ total: z9.number().int().nonnegative(),
1541
+ accepted: z9.number().int().nonnegative(),
1542
+ failed: z9.number().int().nonnegative()
1543
+ }),
1544
+ results: z9.array(gscSubmitSitemapResultDtoSchema)
1545
+ });
1546
+ var gscDiscoverSitemapsResponseDtoSchema = z9.object({
1547
+ sitemaps: z9.array(gscSitemapDtoSchema),
1548
+ primarySitemapUrl: z9.string(),
1549
+ run: z9.object({
1550
+ id: z9.string(),
1551
+ projectId: z9.string(),
1552
+ kind: z9.string(),
1553
+ status: z9.string(),
1554
+ trigger: z9.string(),
1555
+ createdAt: z9.string()
1556
+ }).nullable()
1511
1557
  });
1512
1558
 
1513
1559
  // ../contracts/src/gbp.ts
@@ -7882,6 +7928,9 @@ export {
7882
7928
  gscCoverageSnapshotDtoSchema,
7883
7929
  gscSiteListResponseDtoSchema,
7884
7930
  gscSitemapListResponseDtoSchema,
7931
+ gscSubmitSitemapsRequestDtoSchema,
7932
+ gscSubmitSitemapsResponseDtoSchema,
7933
+ gscDiscoverSitemapsResponseDtoSchema,
7885
7934
  gbpAccountListResponseSchema,
7886
7935
  gbpLocationDtoSchema,
7887
7936
  gbpLocationListResponseSchema,
@@ -10,7 +10,7 @@ import {
10
10
  loadConfig,
11
11
  loadConfigRaw,
12
12
  saveConfigPatch
13
- } from "./chunk-PAR6P5H6.js";
13
+ } from "./chunk-OESGFFUO.js";
14
14
  import {
15
15
  CC_CACHE_DIR,
16
16
  DUCKDB_SPEC,
@@ -119,7 +119,7 @@ import {
119
119
  siteAuditPages,
120
120
  siteAuditSnapshots,
121
121
  usageCounters
122
- } from "./chunk-TPVMHYHD.js";
122
+ } from "./chunk-YQ2RXMU2.js";
123
123
  import {
124
124
  AGENT_MEMORY_VALUE_MAX_BYTES,
125
125
  AGENT_PROVIDER_IDS,
@@ -194,7 +194,7 @@ import {
194
194
  validationError,
195
195
  winnabilityClassLabel,
196
196
  withRetry
197
- } from "./chunk-PXEEWJRV.js";
197
+ } from "./chunk-PIMWEIJ3.js";
198
198
 
199
199
  // src/telemetry.ts
200
200
  import crypto from "crypto";
@@ -7066,7 +7066,7 @@ function readStoredGroundingSources(rawResponse) {
7066
7066
  return result;
7067
7067
  }
7068
7068
  async function backfillInsightsCommand(project, opts) {
7069
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-BMEJ5GFZ.js");
7069
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-BKLR57RQ.js");
7070
7070
  const config = loadConfig();
7071
7071
  const db = createClient(config.database);
7072
7072
  migrate(db);
@@ -238,10 +238,13 @@ import {
238
238
  gscCoverageSnapshotDtoSchema,
239
239
  gscCoverageSummaryDtoSchema,
240
240
  gscDeindexedRowSchema,
241
+ gscDiscoverSitemapsResponseDtoSchema,
241
242
  gscPerformanceDailyDtoSchema,
242
243
  gscSearchDataDtoSchema,
243
244
  gscSiteListResponseDtoSchema,
244
245
  gscSitemapListResponseDtoSchema,
246
+ gscSubmitSitemapsRequestDtoSchema,
247
+ gscSubmitSitemapsResponseDtoSchema,
245
248
  gscUrlInspectionDtoSchema,
246
249
  hasLocationLabel,
247
250
  healthSnapshotDtoSchema,
@@ -365,7 +368,7 @@ import {
365
368
  wordpressSchemaDeployResultDtoSchema,
366
369
  wordpressSchemaStatusResultDtoSchema,
367
370
  wordpressStatusDtoSchema
368
- } from "./chunk-PXEEWJRV.js";
371
+ } from "./chunk-PIMWEIJ3.js";
369
372
 
370
373
  // src/intelligence-service.ts
371
374
  import { eq as eq44, desc as desc21, asc as asc9, and as and35, ne as ne6, or as or10, inArray as inArray15, gte as gte12, lte as lte8 } from "drizzle-orm";
@@ -17487,7 +17490,9 @@ var SCHEMA_TABLE = {
17487
17490
  GscDeindexedRowDto: gscDeindexedRowSchema,
17488
17491
  GscPerformanceDailyDto: gscPerformanceDailyDtoSchema,
17489
17492
  GscSearchDataDto: gscSearchDataDtoSchema,
17493
+ GscDiscoverSitemapsResponseDto: gscDiscoverSitemapsResponseDtoSchema,
17490
17494
  GscSiteListResponseDto: gscSiteListResponseDtoSchema,
17495
+ GscSubmitSitemapsResponseDto: gscSubmitSitemapsResponseDtoSchema,
17491
17496
  GscSitemapListResponseDto: gscSitemapListResponseDtoSchema,
17492
17497
  GscUrlInspectionDto: gscUrlInspectionDtoSchema,
17493
17498
  HealthSnapshotDto: healthSnapshotDtoSchema,
@@ -17910,6 +17915,34 @@ var routeCatalog = [
17910
17915
  200: rawJsonResponse("OpenAPI document.", looseObjectSchema)
17911
17916
  }
17912
17917
  },
17918
+ {
17919
+ method: "post",
17920
+ path: "/api/v1/projects/{name}/google/gsc/sitemaps/submit",
17921
+ summary: "Submit GSC sitemaps",
17922
+ description: "Submits sitemap URLs to Google Search Console sequentially. An accepted result means Google accepted the submit/refetch request; it does not indicate indexing.",
17923
+ tags: ["google"],
17924
+ parameters: [nameParameter],
17925
+ requestBody: {
17926
+ required: true,
17927
+ content: {
17928
+ "application/json": {
17929
+ schema: {
17930
+ type: "object",
17931
+ required: ["sitemapUrls"],
17932
+ additionalProperties: false,
17933
+ properties: {
17934
+ sitemapUrls: { type: "array", minItems: 1, maxItems: 50, items: { type: "string", format: "uri", pattern: "^https?://" } }
17935
+ }
17936
+ }
17937
+ }
17938
+ }
17939
+ },
17940
+ responses: {
17941
+ 200: jsonResponse("Per-sitemap submission results returned.", "GscSubmitSitemapsResponseDto"),
17942
+ 400: errorResponse("Invalid sitemap request, property ownership, or OAuth scope."),
17943
+ 404: errorResponse("Project or connection not found.")
17944
+ }
17945
+ },
17913
17946
  {
17914
17947
  method: "put",
17915
17948
  path: "/api/v1/projects/{name}",
@@ -19487,7 +19520,15 @@ var routeCatalog = [
19487
19520
  path: "/api/v1/projects/{name}/google/gsc/sitemaps",
19488
19521
  summary: "List GSC sitemaps",
19489
19522
  tags: ["google"],
19490
- parameters: [nameParameter],
19523
+ parameters: [
19524
+ nameParameter,
19525
+ {
19526
+ name: "sitemapIndex",
19527
+ in: "query",
19528
+ description: "Optional owned sitemap-index URL. When present, returns that index's child entries.",
19529
+ schema: { type: "string", format: "uri", pattern: "^https?://" }
19530
+ }
19531
+ ],
19491
19532
  responses: {
19492
19533
  200: jsonResponse("GSC sitemaps returned.", "GscSitemapListResponseDto"),
19493
19534
  400: errorResponse("Invalid sitemap request."),
@@ -19501,8 +19542,7 @@ var routeCatalog = [
19501
19542
  tags: ["google"],
19502
19543
  parameters: [nameParameter],
19503
19544
  responses: {
19504
- // TODO: Add `DiscoverSitemapsResponse` Zod schema in contracts.
19505
- 200: rawJsonResponse("Discovered sitemaps and queued run returned.", looseObjectSchema),
19545
+ 200: jsonResponse("Discovered sitemaps and queued run returned.", "GscDiscoverSitemapsResponseDto"),
19506
19546
  400: errorResponse("Invalid sitemap discovery request."),
19507
19547
  404: errorResponse("Project or connection not found.")
19508
19548
  }
@@ -23230,7 +23270,8 @@ function buildGbpSummary(input) {
23230
23270
  // ../integration-google/src/constants.ts
23231
23271
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
23232
23272
  var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
23233
- var GSC_SCOPE = "https://www.googleapis.com/auth/webmasters.readonly";
23273
+ var GSC_SCOPE = "https://www.googleapis.com/auth/webmasters";
23274
+ var GSC_READONLY_SCOPE = "https://www.googleapis.com/auth/webmasters.readonly";
23234
23275
  var INDEXING_SCOPE = "https://www.googleapis.com/auth/indexing";
23235
23276
  var GSC_API_BASE = "https://www.googleapis.com/webmasters/v3";
23236
23277
  var URL_INSPECTION_API = "https://searchconsole.googleapis.com/v1/urlInspection/index:inspect";
@@ -23480,7 +23521,7 @@ function gscClientLog(level, action, ctx) {
23480
23521
  const stream = level === "error" ? process.stderr : process.stdout;
23481
23522
  stream.write(JSON.stringify(entry) + "\n");
23482
23523
  }
23483
- async function gscFetch(accessToken, url, opts) {
23524
+ async function gscFetchResponse(accessToken, url, opts) {
23484
23525
  const method = opts?.method ?? "GET";
23485
23526
  const headers = {
23486
23527
  Authorization: `Bearer ${accessToken}`,
@@ -23506,26 +23547,50 @@ async function gscFetch(accessToken, url, opts) {
23506
23547
  const detail = body.length <= 500 ? body : `${body.slice(0, 500)}... [truncated]`;
23507
23548
  throw new GoogleApiError(`GSC API error (${res.status}): ${detail}`, res.status);
23508
23549
  }
23509
- return await res.json();
23550
+ return res;
23551
+ }
23552
+ async function gscFetchJson(accessToken, url, opts) {
23553
+ const res = await gscFetchResponse(accessToken, url, opts);
23554
+ const text2 = await res.text();
23555
+ if (!text2) {
23556
+ throw new GoogleApiError("GSC API returned an empty response where JSON was expected", res.status);
23557
+ }
23558
+ try {
23559
+ return JSON.parse(text2);
23560
+ } catch {
23561
+ throw new GoogleApiError("GSC API returned invalid JSON", res.status);
23562
+ }
23510
23563
  }
23511
23564
  async function listSites(accessToken) {
23512
23565
  validateAccessToken(accessToken);
23513
- const data = await gscFetch(
23566
+ const data = await gscFetchJson(
23514
23567
  accessToken,
23515
23568
  `${GSC_API_BASE}/sites`
23516
23569
  );
23517
23570
  return data.siteEntry ?? [];
23518
23571
  }
23519
- async function listSitemaps(accessToken, siteUrl) {
23572
+ async function listSitemaps(accessToken, siteUrl, sitemapIndex) {
23520
23573
  validateAccessToken(accessToken);
23521
23574
  validateSiteUrl(siteUrl);
23575
+ if (sitemapIndex != null) validateUrl(sitemapIndex);
23522
23576
  const encodedSiteUrl = encodeURIComponent(siteUrl);
23523
- const data = await gscFetch(
23577
+ const sitemapIndexQuery = sitemapIndex == null ? "" : `?sitemapIndex=${encodeURIComponent(sitemapIndex)}`;
23578
+ const data = await gscFetchJson(
23524
23579
  accessToken,
23525
- `${GSC_API_BASE}/sites/${encodedSiteUrl}/sitemaps`
23580
+ `${GSC_API_BASE}/sites/${encodedSiteUrl}/sitemaps${sitemapIndexQuery}`
23526
23581
  );
23527
23582
  return data.sitemap ?? [];
23528
23583
  }
23584
+ async function submitSitemap(accessToken, siteUrl, sitemapUrl) {
23585
+ validateAccessToken(accessToken);
23586
+ validateSiteUrl(siteUrl);
23587
+ validateUrl(sitemapUrl);
23588
+ await gscFetchResponse(
23589
+ accessToken,
23590
+ `${GSC_API_BASE}/sites/${encodeURIComponent(siteUrl)}/sitemaps/${encodeURIComponent(sitemapUrl)}`,
23591
+ { method: "PUT" }
23592
+ );
23593
+ }
23529
23594
  async function fetchSearchAnalytics(accessToken, siteUrl, opts) {
23530
23595
  validateAccessToken(accessToken);
23531
23596
  validateSiteUrl(siteUrl);
@@ -23555,7 +23620,7 @@ async function fetchSearchAnalytics(accessToken, siteUrl, opts) {
23555
23620
  requestBody.dimensionFilterGroups = filters;
23556
23621
  }
23557
23622
  const encodedSiteUrl = encodeURIComponent(siteUrl);
23558
- const data = await gscFetch(
23623
+ const data = await gscFetchJson(
23559
23624
  accessToken,
23560
23625
  `${GSC_API_BASE}/sites/${encodedSiteUrl}/searchAnalytics/query`,
23561
23626
  { method: "POST", body: requestBody }
@@ -23572,7 +23637,7 @@ async function fetchSearchAnalytics(accessToken, siteUrl, opts) {
23572
23637
  async function publishUrlNotification(accessToken, url, type = "URL_UPDATED") {
23573
23638
  validateAccessToken(accessToken);
23574
23639
  validateUrl(url);
23575
- return gscFetch(
23640
+ return gscFetchJson(
23576
23641
  accessToken,
23577
23642
  `${INDEXING_API_BASE}/urlNotifications:publish`,
23578
23643
  {
@@ -23585,7 +23650,7 @@ async function inspectUrl(accessToken, inspectionUrl, siteUrl) {
23585
23650
  validateAccessToken(accessToken);
23586
23651
  validateUrl(inspectionUrl);
23587
23652
  validateSiteUrl(siteUrl);
23588
- return gscFetch(
23653
+ return gscFetchJson(
23589
23654
  accessToken,
23590
23655
  URL_INSPECTION_API,
23591
23656
  {
@@ -24795,6 +24860,33 @@ function scopesForConnectionType(type) {
24795
24860
  return [GBP_SCOPE];
24796
24861
  }
24797
24862
  }
24863
+ function isSitemapOwnedByProperty(sitemapUrl, propertyId, canonicalDomain) {
24864
+ let sitemap;
24865
+ try {
24866
+ sitemap = new URL(sitemapUrl);
24867
+ } catch {
24868
+ return false;
24869
+ }
24870
+ if (sitemap.protocol !== "http:" && sitemap.protocol !== "https:") return false;
24871
+ if (/^sc-domain:/i.test(propertyId)) {
24872
+ const domain = propertyId.slice("sc-domain:".length).toLowerCase();
24873
+ const host = sitemap.hostname.toLowerCase();
24874
+ return host === domain || host.endsWith(`.${domain}`);
24875
+ }
24876
+ if (/^\d+$/.test(propertyId)) {
24877
+ const domain = normalizeProjectDomain(canonicalDomain);
24878
+ return sitemap.hostname.toLowerCase().replace(/^www\./, "") === domain;
24879
+ }
24880
+ try {
24881
+ const property = new URL(propertyId);
24882
+ if (property.protocol !== "http:" && property.protocol !== "https:") return false;
24883
+ if (sitemap.origin !== property.origin) return false;
24884
+ const prefix = property.pathname.endsWith("/") ? property.pathname : `${property.pathname}/`;
24885
+ return sitemap.pathname === property.pathname || sitemap.pathname.startsWith(prefix);
24886
+ } catch {
24887
+ return false;
24888
+ }
24889
+ }
24798
24890
  function signState(payload, secret) {
24799
24891
  return crypto19.createHmac("sha256", secret).update(payload).digest("hex");
24800
24892
  }
@@ -25454,17 +25546,83 @@ async function googleRoutes(app, opts) {
25454
25546
  }
25455
25547
  const store = requireConnectionStore();
25456
25548
  const project = resolveProject(app.db, request.params.name);
25549
+ const conn = store.getConnection(project.canonicalDomain, "gsc");
25550
+ if (!conn?.propertyId) {
25551
+ throw validationError('No GSC property configured for this connection. Set one with "canonry google set-property".');
25552
+ }
25553
+ const sitemapIndex = request.query.sitemapIndex;
25554
+ if (sitemapIndex && !isSitemapOwnedByProperty(sitemapIndex, conn.propertyId, project.canonicalDomain)) {
25555
+ throw validationError(`sitemapIndex must belong to the configured GSC property "${conn.propertyId}".`);
25556
+ }
25457
25557
  try {
25458
25558
  const { accessToken, propertyId } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
25459
25559
  if (!propertyId) {
25460
25560
  throw validationError('No GSC property configured for this connection. Set one with "canonry google set-property".');
25461
25561
  }
25462
- const sitemaps = await listSitemaps(accessToken, propertyId);
25463
- return { sitemaps };
25562
+ const entries = await listSitemaps(accessToken, propertyId, sitemapIndex);
25563
+ const sitemaps = sitemapIndex ? entries.map((sitemap) => ({ ...sitemap, parentSitemapUrl: sitemapIndex })) : entries;
25564
+ const indexes = sitemaps.filter((sitemap) => sitemap.isSitemapsIndex).length;
25565
+ return {
25566
+ sitemaps,
25567
+ summary: { total: sitemaps.length, indexes, files: sitemaps.length - indexes },
25568
+ preferredSubmissionUrls: indexes > 0 ? sitemaps.filter((sitemap) => sitemap.isSitemapsIndex).map((sitemap) => sitemap.path) : sitemaps.map((sitemap) => sitemap.path)
25569
+ };
25464
25570
  } catch (err) {
25465
25571
  throw gscErrorToAppError(err, "Failed to list Search Console sitemaps");
25466
25572
  }
25467
25573
  });
25574
+ app.post("/projects/:name/google/gsc/sitemaps/submit", async (request) => {
25575
+ const parsed = gscSubmitSitemapsRequestDtoSchema.safeParse(request.body ?? {});
25576
+ if (!parsed.success) {
25577
+ throw validationError("sitemapUrls must contain between 1 and 50 valid sitemap URLs");
25578
+ }
25579
+ const sitemapUrls = [...new Set(parsed.data.sitemapUrls)];
25580
+ const project = resolveProject(app.db, request.params.name);
25581
+ const store = requireConnectionStore();
25582
+ const conn = store.getConnection(project.canonicalDomain, "gsc");
25583
+ if (!conn) {
25584
+ throw validationError('No GSC connection found for this domain. Run "canonry google connect" first.');
25585
+ }
25586
+ if (!conn.propertyId) {
25587
+ throw validationError('No GSC property configured for this connection. Set one with "canonry google set-property".');
25588
+ }
25589
+ const invalidUrls = sitemapUrls.filter((url) => !isSitemapOwnedByProperty(url, conn.propertyId, project.canonicalDomain));
25590
+ if (invalidUrls.length > 0) {
25591
+ throw validationError(`Sitemap URLs must belong to the configured GSC property "${conn.propertyId}". Invalid: ${invalidUrls.slice(0, 5).join(", ")}`);
25592
+ }
25593
+ if (!(conn.scopes ?? []).includes(GSC_SCOPE)) {
25594
+ throw validationError('This GSC connection has the read-only webmasters scope and cannot submit sitemaps. Reconnect with "canonry google connect" to grant the full webmasters scope.');
25595
+ }
25596
+ const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
25597
+ if (!googleClientId || !googleClientSecret) {
25598
+ throw validationError("Google OAuth is not configured");
25599
+ }
25600
+ let accessToken;
25601
+ try {
25602
+ ({ accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret));
25603
+ } catch (err) {
25604
+ throw gscErrorToAppError(err, "Failed to authorize Search Console sitemap submission");
25605
+ }
25606
+ const results = [];
25607
+ for (const sitemapUrl of sitemapUrls) {
25608
+ try {
25609
+ await submitSitemap(accessToken, conn.propertyId, sitemapUrl);
25610
+ const submittedAt = (/* @__PURE__ */ new Date()).toISOString();
25611
+ writeAuditLog(app.db, {
25612
+ projectId: project.id,
25613
+ actor: "api",
25614
+ action: "google.sitemap.submitted",
25615
+ entityType: "sitemap",
25616
+ entityId: sitemapUrl
25617
+ });
25618
+ results.push({ sitemapUrl, status: "accepted", submittedAt });
25619
+ } catch (err) {
25620
+ results.push({ sitemapUrl, status: "error", error: err instanceof Error ? err.message : String(err) });
25621
+ }
25622
+ }
25623
+ const accepted = results.filter((result) => result.status === "accepted").length;
25624
+ return { summary: { total: results.length, accepted, failed: results.length - accepted }, results };
25625
+ });
25468
25626
  app.post("/projects/:name/google/gsc/discover-sitemaps", async (request) => {
25469
25627
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
25470
25628
  if (!googleClientId || !googleClientSecret) {
@@ -25487,9 +25645,11 @@ async function googleRoutes(app, opts) {
25487
25645
  throw gscErrorToAppError(err, "Failed to discover Search Console sitemaps");
25488
25646
  }
25489
25647
  if (sitemaps.length === 0) {
25490
- throw validationError("No sitemaps found for this GSC property. Submit a sitemap in Google Search Console first.");
25648
+ throw validationError(
25649
+ `No sitemaps found for this GSC property. Submit one with "canonry google submit-sitemap ${request.params.name} <url>" first.`
25650
+ );
25491
25651
  }
25492
- const primary = sitemaps.find((s) => !s.isSitemapsIndex) ?? sitemaps[0];
25652
+ const primary = sitemaps.find((s) => s.isSitemapsIndex) ?? sitemaps[0];
25493
25653
  const sitemapUrl = primary.path;
25494
25654
  store.updateConnection(project.canonicalDomain, "gsc", {
25495
25655
  sitemapUrl,
@@ -42064,7 +42224,6 @@ var PLACES_CHECK_BY_ID = Object.fromEntries(
42064
42224
  );
42065
42225
 
42066
42226
  // ../api-routes/src/doctor/checks/google-auth.ts
42067
- var REQUIRED_GSC_SCOPES = [GSC_SCOPE, INDEXING_SCOPE];
42068
42227
  async function resolveAccessToken(ctx) {
42069
42228
  if (!ctx.project) {
42070
42229
  return { ok: false, output: skippedNoProject4() };
@@ -42339,8 +42498,19 @@ var scopesCheck2 = {
42339
42498
  };
42340
42499
  }
42341
42500
  const granted = new Set(conn.scopes ?? []);
42342
- const missing = REQUIRED_GSC_SCOPES.filter((scope) => !granted.has(scope));
42343
- if (missing.length === 0) {
42501
+ const hasFullGscScope = granted.has(GSC_SCOPE);
42502
+ const hasReadOnlyGscScope = granted.has(GSC_READONLY_SCOPE);
42503
+ const hasIndexingScope = granted.has(INDEXING_SCOPE);
42504
+ if (!hasFullGscScope && !hasReadOnlyGscScope) {
42505
+ return {
42506
+ status: CheckStatuses.fail,
42507
+ code: "google.auth.required-scope-missing",
42508
+ summary: "Neither the full nor read-only Search Console scope is granted, so GSC reads will fail.",
42509
+ remediation: `Reconnect to grant a Search Console scope: \`canonry google connect ${ctx.project.name} --type gsc\`.`,
42510
+ details: { granted: [...granted], missing: [GSC_SCOPE, GSC_READONLY_SCOPE] }
42511
+ };
42512
+ }
42513
+ if (hasFullGscScope && hasIndexingScope) {
42344
42514
  return {
42345
42515
  status: CheckStatuses.ok,
42346
42516
  code: "google.auth.scopes-ok",
@@ -42349,14 +42519,22 @@ var scopesCheck2 = {
42349
42519
  details: { granted: [...granted] }
42350
42520
  };
42351
42521
  }
42352
- const gscMissing = missing.includes(GSC_SCOPE);
42353
- const indexingOnlyMissing = !gscMissing && missing.includes(INDEXING_SCOPE) && missing.length === 1;
42522
+ if (!hasFullGscScope) {
42523
+ const missing = [GSC_SCOPE, ...hasIndexingScope ? [] : [INDEXING_SCOPE]];
42524
+ return {
42525
+ status: CheckStatuses.warn,
42526
+ code: "google.auth.sitemap-write-scope-missing",
42527
+ summary: hasIndexingScope ? "The read-only Search Console scope supports existing GSC reads, but sitemap submission is unavailable." : "The read-only Search Console scope supports existing GSC reads, but sitemap submission and the Indexing API are unavailable.",
42528
+ remediation: `Reconnect to grant the full Search Console webmasters scope: \`canonry google connect ${ctx.project.name} --type gsc\`.`,
42529
+ details: { granted: [...granted], missing }
42530
+ };
42531
+ }
42354
42532
  return {
42355
- status: indexingOnlyMissing ? CheckStatuses.warn : CheckStatuses.fail,
42356
- code: indexingOnlyMissing ? "google.auth.indexing-scope-missing" : "google.auth.required-scope-missing",
42357
- summary: indexingOnlyMissing ? "Indexing API scope is not granted \u2014 `canonry google request-indexing` will fail." : `Missing required scopes: ${missing.join(", ")}.`,
42533
+ status: CheckStatuses.warn,
42534
+ code: "google.auth.indexing-scope-missing",
42535
+ summary: "Indexing API scope is not granted \u2014 `canonry google request-indexing` will fail.",
42358
42536
  remediation: `Reconnect to grant missing scopes: \`canonry google connect ${ctx.project.name} --type gsc\`.`,
42359
- details: { granted: [...granted], missing }
42537
+ details: { granted: [...granted], missing: [INDEXING_SCOPE] }
42360
42538
  };
42361
42539
  }
42362
42540
  };