@ainyc/canonry 4.60.1 → 4.60.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.
@@ -3542,6 +3542,71 @@ function buildPhases(input) {
3542
3542
  // src/gsc-sync.ts
3543
3543
  import crypto4 from "crypto";
3544
3544
  import { eq as eq2, and as and2, sql as sql2 } from "drizzle-orm";
3545
+
3546
+ // src/gsc-inspect-paced.ts
3547
+ var INSPECT_BASE_DELAY_MS = 1e3;
3548
+ var INSPECT_PACING_JITTER_MS = 250;
3549
+ var INSPECT_MAX_RETRIES = 3;
3550
+ var INSPECT_MAX_BACKOFF_MS = 3e4;
3551
+ var INSPECT_FAILFAST_THRESHOLD = 5;
3552
+ function isRetryableGscInspectError(err) {
3553
+ if (err != null && typeof err === "object" && "status" in err) {
3554
+ if (err.status === 403) return true;
3555
+ }
3556
+ return isRetryableHttpError(err);
3557
+ }
3558
+ function defaultSleep(ms) {
3559
+ return new Promise((resolve) => setTimeout(resolve, ms));
3560
+ }
3561
+ async function inspectUrlsPaced(urls, cb, deps = {}) {
3562
+ const sleep3 = deps.sleep ?? defaultSleep;
3563
+ const jitter = deps.jitter ?? Math.random;
3564
+ let inspected = 0;
3565
+ let errors = 0;
3566
+ let consecutiveRetryableFailures = 0;
3567
+ for (let index = 0; index < urls.length; index++) {
3568
+ const url = urls[index];
3569
+ try {
3570
+ const result = await withRetry(() => cb.inspectOne(url), {
3571
+ maxRetries: INSPECT_MAX_RETRIES,
3572
+ baseDelayMs: INSPECT_BASE_DELAY_MS,
3573
+ maxDelayMs: INSPECT_MAX_BACKOFF_MS,
3574
+ isRetryable: isRetryableGscInspectError,
3575
+ sleep: sleep3,
3576
+ onRetry: ({ attempt, delayMs, err }) => deps.log?.info("inspect.retry", {
3577
+ url,
3578
+ attempt,
3579
+ delayMs: Math.round(delayMs),
3580
+ error: err instanceof Error ? err.message : String(err)
3581
+ })
3582
+ });
3583
+ cb.onResult(url, result, index);
3584
+ inspected++;
3585
+ consecutiveRetryableFailures = 0;
3586
+ } catch (err) {
3587
+ errors++;
3588
+ cb.onError(url, err, index);
3589
+ if (isRetryableGscInspectError(err)) {
3590
+ consecutiveRetryableFailures++;
3591
+ if (consecutiveRetryableFailures >= INSPECT_FAILFAST_THRESHOLD) {
3592
+ deps.log?.error("inspect.circuit-break", {
3593
+ consecutiveFailures: consecutiveRetryableFailures,
3594
+ inspected,
3595
+ errors,
3596
+ remaining: urls.length - index - 1
3597
+ });
3598
+ return { inspected, errors, aborted: true, abortError: err };
3599
+ }
3600
+ }
3601
+ }
3602
+ if (index < urls.length - 1) {
3603
+ await sleep3(INSPECT_BASE_DELAY_MS + jitter() * INSPECT_PACING_JITTER_MS);
3604
+ }
3605
+ }
3606
+ return { inspected, errors, aborted: false };
3607
+ }
3608
+
3609
+ // src/gsc-sync.ts
3545
3610
  var log2 = createLogger("GscSync");
3546
3611
  function formatDate(d) {
3547
3612
  return d.toISOString().split("T")[0];
@@ -3570,6 +3635,7 @@ async function executeGscSync(db, runId, projectId, opts) {
3570
3635
  if (!conn.propertyId) {
3571
3636
  throw new Error('No GSC property selected. Use "canonry google properties" to list available sites, then set one with the API.');
3572
3637
  }
3638
+ const propertyId = conn.propertyId;
3573
3639
  let accessToken = conn.accessToken;
3574
3640
  const expiresAt = conn.tokenExpiresAt ? new Date(conn.tokenExpiresAt).getTime() : 0;
3575
3641
  if (Date.now() > expiresAt - 5 * 60 * 1e3) {
@@ -3631,35 +3697,53 @@ async function executeGscSync(db, runId, projectId, opts) {
3631
3697
  }
3632
3698
  const topPages = [...pageClicks.entries()].sort((a, b) => b[1] - a[1]).slice(0, 50).map(([page]) => page);
3633
3699
  log2.info("inspect.start", { runId, projectId, urlCount: topPages.length });
3634
- for (const pageUrl of topPages) {
3635
- try {
3636
- const result = await inspectUrl(accessToken, pageUrl, conn.propertyId);
3637
- const ir = result.inspectionResult;
3638
- const idx = ir.indexStatusResult;
3639
- const mob = ir.mobileUsabilityResult;
3640
- const rich = ir.richResultsResult;
3641
- const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
3642
- db.insert(gscUrlInspections).values({
3643
- id: crypto4.randomUUID(),
3644
- projectId,
3645
- syncRunId: runId,
3646
- url: pageUrl,
3647
- indexingState: idx?.indexingState ?? null,
3648
- verdict: idx?.verdict ?? null,
3649
- coverageState: idx?.coverageState ?? null,
3650
- pageFetchState: idx?.pageFetchState ?? null,
3651
- robotsTxtState: idx?.robotsTxtState ?? null,
3652
- crawlTime: idx?.lastCrawlTime ?? null,
3653
- lastCrawlResult: idx?.crawlResult ?? null,
3654
- isMobileFriendly: mob?.verdict === "PASS" ? true : mob?.verdict === "FAIL" ? false : null,
3655
- richResults: rich?.detectedItems?.map((d) => d.richResultType) ?? [],
3656
- referringUrls: idx?.referringUrls ?? [],
3657
- inspectedAt,
3658
- createdAt: inspectedAt
3659
- }).run();
3660
- } catch (err) {
3661
- log2.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
3700
+ const inspectOutcome = await inspectUrlsPaced(
3701
+ topPages,
3702
+ {
3703
+ inspectOne: (pageUrl) => inspectUrl(accessToken, pageUrl, propertyId),
3704
+ onResult: (pageUrl, result) => {
3705
+ const ir = result.inspectionResult;
3706
+ const idx = ir.indexStatusResult;
3707
+ const mob = ir.mobileUsabilityResult;
3708
+ const rich = ir.richResultsResult;
3709
+ const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
3710
+ db.insert(gscUrlInspections).values({
3711
+ id: crypto4.randomUUID(),
3712
+ projectId,
3713
+ syncRunId: runId,
3714
+ url: pageUrl,
3715
+ indexingState: idx?.indexingState ?? null,
3716
+ verdict: idx?.verdict ?? null,
3717
+ coverageState: idx?.coverageState ?? null,
3718
+ pageFetchState: idx?.pageFetchState ?? null,
3719
+ robotsTxtState: idx?.robotsTxtState ?? null,
3720
+ crawlTime: idx?.lastCrawlTime ?? null,
3721
+ lastCrawlResult: idx?.crawlResult ?? null,
3722
+ isMobileFriendly: mob?.verdict === "PASS" ? true : mob?.verdict === "FAIL" ? false : null,
3723
+ richResults: rich?.detectedItems?.map((d) => d.richResultType) ?? [],
3724
+ referringUrls: idx?.referringUrls ?? [],
3725
+ inspectedAt,
3726
+ createdAt: inspectedAt
3727
+ }).run();
3728
+ },
3729
+ onError: (pageUrl, err) => {
3730
+ log2.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
3731
+ }
3732
+ },
3733
+ {
3734
+ log: {
3735
+ info: (action, ctx) => log2.info(action, { runId, projectId, ...ctx }),
3736
+ error: (action, ctx) => log2.error(action, { runId, projectId, ...ctx })
3737
+ }
3662
3738
  }
3739
+ );
3740
+ if (inspectOutcome.aborted) {
3741
+ log2.error("inspect.stopped-early", {
3742
+ runId,
3743
+ projectId,
3744
+ inspected: inspectOutcome.inspected,
3745
+ note: "URL inspection stopped early after sustained rate/access failures; search-analytics data was still saved"
3746
+ });
3663
3747
  }
3664
3748
  const allInspections = db.select().from(gscUrlInspections).where(eq2(gscUrlInspections.projectId, projectId)).all();
3665
3749
  const latestByUrl = /* @__PURE__ */ new Map();
@@ -4057,6 +4141,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
4057
4141
  if (!conn.propertyId) {
4058
4142
  throw new Error('No GSC property selected. Use "canonry google properties" to list available sites, then set one.');
4059
4143
  }
4144
+ const propertyId = conn.propertyId;
4060
4145
  let accessToken = conn.accessToken;
4061
4146
  const expiresAt = conn.tokenExpiresAt ? new Date(conn.tokenExpiresAt).getTime() : 0;
4062
4147
  if (Date.now() > expiresAt - 5 * 60 * 1e3) {
@@ -4076,43 +4161,52 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
4076
4161
  if (urls.length === 0) {
4077
4162
  throw new Error("No URLs found in sitemap");
4078
4163
  }
4079
- let inspected = 0;
4080
- let errors = 0;
4081
- for (const pageUrl of urls) {
4082
- try {
4083
- const result = await inspectUrl(accessToken, pageUrl, conn.propertyId);
4084
- const ir = result.inspectionResult;
4085
- const idx = ir.indexStatusResult;
4086
- const mob = ir.mobileUsabilityResult;
4087
- const rich = ir.richResultsResult;
4088
- const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
4089
- db.insert(gscUrlInspections).values({
4090
- id: crypto6.randomUUID(),
4091
- projectId,
4092
- syncRunId: runId,
4093
- url: pageUrl,
4094
- indexingState: idx?.indexingState ?? null,
4095
- verdict: idx?.verdict ?? null,
4096
- coverageState: idx?.coverageState ?? null,
4097
- pageFetchState: idx?.pageFetchState ?? null,
4098
- robotsTxtState: idx?.robotsTxtState ?? null,
4099
- crawlTime: idx?.lastCrawlTime ?? null,
4100
- lastCrawlResult: idx?.crawlResult ?? null,
4101
- isMobileFriendly: mob?.verdict === "PASS" ? true : mob?.verdict === "FAIL" ? false : null,
4102
- richResults: rich?.detectedItems?.map((d) => d.richResultType) ?? [],
4103
- referringUrls: idx?.referringUrls ?? [],
4104
- inspectedAt,
4105
- createdAt: inspectedAt
4106
- }).run();
4107
- inspected++;
4108
- log5.info("inspect.url-done", { runId, projectId, url: pageUrl, progress: `${inspected}/${urls.length}` });
4109
- } catch (err) {
4110
- errors++;
4111
- log5.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
4112
- }
4113
- if (inspected + errors < urls.length) {
4114
- await new Promise((r) => setTimeout(r, 1e3));
4164
+ const { inspected, errors, aborted, abortError } = await inspectUrlsPaced(
4165
+ urls,
4166
+ {
4167
+ inspectOne: (pageUrl) => inspectUrl(accessToken, pageUrl, propertyId),
4168
+ onResult: (pageUrl, result, index) => {
4169
+ const ir = result.inspectionResult;
4170
+ const idx = ir.indexStatusResult;
4171
+ const mob = ir.mobileUsabilityResult;
4172
+ const rich = ir.richResultsResult;
4173
+ const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
4174
+ db.insert(gscUrlInspections).values({
4175
+ id: crypto6.randomUUID(),
4176
+ projectId,
4177
+ syncRunId: runId,
4178
+ url: pageUrl,
4179
+ indexingState: idx?.indexingState ?? null,
4180
+ verdict: idx?.verdict ?? null,
4181
+ coverageState: idx?.coverageState ?? null,
4182
+ pageFetchState: idx?.pageFetchState ?? null,
4183
+ robotsTxtState: idx?.robotsTxtState ?? null,
4184
+ crawlTime: idx?.lastCrawlTime ?? null,
4185
+ lastCrawlResult: idx?.crawlResult ?? null,
4186
+ isMobileFriendly: mob?.verdict === "PASS" ? true : mob?.verdict === "FAIL" ? false : null,
4187
+ richResults: rich?.detectedItems?.map((d) => d.richResultType) ?? [],
4188
+ referringUrls: idx?.referringUrls ?? [],
4189
+ inspectedAt,
4190
+ createdAt: inspectedAt
4191
+ }).run();
4192
+ log5.info("inspect.url-done", { runId, projectId, url: pageUrl, progress: `${index + 1}/${urls.length}` });
4193
+ },
4194
+ onError: (pageUrl, err) => {
4195
+ log5.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
4196
+ }
4197
+ },
4198
+ {
4199
+ log: {
4200
+ info: (action, ctx) => log5.info(action, { runId, projectId, ...ctx }),
4201
+ error: (action, ctx) => log5.error(action, { runId, projectId, ...ctx })
4202
+ }
4115
4203
  }
4204
+ );
4205
+ if (aborted) {
4206
+ const detail = abortError instanceof Error ? abortError.message : String(abortError);
4207
+ throw new Error(
4208
+ `URL inspection aborted after ${INSPECT_FAILFAST_THRESHOLD} consecutive rate/access failures (likely GSC URL Inspection quota exhaustion or property access loss). Last error: ${detail}`
4209
+ );
4116
4210
  }
4117
4211
  const allInspections = db.select().from(gscUrlInspections).where(eq4(gscUrlInspections.projectId, projectId)).all();
4118
4212
  const latestByUrl = /* @__PURE__ */ new Map();
package/dist/cli.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  setTelemetrySource,
28
28
  showFirstRunNotice,
29
29
  trackEvent
30
- } from "./chunk-X4ZZFZQZ.js";
30
+ } from "./chunk-AUR7VMQF.js";
31
31
  import {
32
32
  CliError,
33
33
  EXIT_SYSTEM_ERROR,
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createServer
3
- } from "./chunk-X4ZZFZQZ.js";
3
+ } from "./chunk-AUR7VMQF.js";
4
4
  import {
5
5
  loadConfig
6
6
  } from "./chunk-CKWHFAVB.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainyc/canonry",
3
- "version": "4.60.1",
3
+ "version": "4.60.2",
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",
@@ -61,25 +61,25 @@
61
61
  "@types/node-cron": "^3.0.11",
62
62
  "tsup": "^8.5.1",
63
63
  "tsx": "^4.19.0",
64
+ "@ainyc/canonry-api-routes": "0.0.0",
64
65
  "@ainyc/canonry-api-client": "0.0.0",
65
- "@ainyc/canonry-contracts": "0.0.0",
66
66
  "@ainyc/canonry-config": "0.0.0",
67
+ "@ainyc/canonry-contracts": "0.0.0",
67
68
  "@ainyc/canonry-db": "0.0.0",
68
- "@ainyc/canonry-api-routes": "0.0.0",
69
69
  "@ainyc/canonry-integration-bing": "0.0.0",
70
- "@ainyc/canonry-integration-cloud-run": "0.0.0",
71
70
  "@ainyc/canonry-integration-commoncrawl": "0.0.0",
72
71
  "@ainyc/canonry-integration-google": "0.0.0",
73
- "@ainyc/canonry-integration-google-business-profile": "0.0.0",
72
+ "@ainyc/canonry-integration-cloud-run": "0.0.0",
74
73
  "@ainyc/canonry-integration-traffic": "0.0.0",
75
74
  "@ainyc/canonry-integration-wordpress": "0.0.0",
76
75
  "@ainyc/canonry-intelligence": "0.0.0",
76
+ "@ainyc/canonry-integration-google-business-profile": "0.0.0",
77
77
  "@ainyc/canonry-provider-cdp": "0.0.0",
78
78
  "@ainyc/canonry-provider-claude": "0.0.0",
79
- "@ainyc/canonry-provider-gemini": "0.0.0",
80
79
  "@ainyc/canonry-provider-local": "0.0.0",
81
- "@ainyc/canonry-provider-openai": "0.0.0",
82
- "@ainyc/canonry-provider-perplexity": "0.0.0"
80
+ "@ainyc/canonry-provider-gemini": "0.0.0",
81
+ "@ainyc/canonry-provider-perplexity": "0.0.0",
82
+ "@ainyc/canonry-provider-openai": "0.0.0"
83
83
  },
84
84
  "scripts": {
85
85
  "build": "tsx scripts/copy-agent-assets.ts && tsup && tsx build-web.ts",