gscdump 0.17.4 → 0.18.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.
@@ -16,7 +16,14 @@ interface TenantCtx {
16
16
  userId: string;
17
17
  siteId?: string;
18
18
  }
19
- type GscSearchAnalyticsDimension = 'page' | 'query' | 'country' | 'device' | 'date' | 'searchAppearance';
19
+ type GscSearchAnalyticsDimension = 'page' | 'query' | 'country' | 'device' | 'date' | 'hour' | 'searchAppearance';
20
+ type GscDataState = 'final' | 'all' | 'hourly_all';
21
+ interface GscSearchAnalyticsMetadata {
22
+ /** First date (YYYY-MM-DD, PT) still being collected. Populated when dataState=`all` and grouped by `date`. */
23
+ first_incomplete_date?: string;
24
+ /** First hour (YYYY-MM-DDThh:mm:ss±hh:mm, PT) still being collected. Populated when dataState=`hourly_all` and grouped by `hour`. */
25
+ first_incomplete_hour?: string;
26
+ }
20
27
  type GscSearchAnalyticsFilterOperator = 'equals' | 'notEquals' | 'contains' | 'notContains' | 'includingRegex' | 'excludingRegex';
21
28
  interface GscSearchAnalyticsFilter {
22
29
  dimension: GscSearchAnalyticsDimension;
@@ -28,6 +35,8 @@ interface GscSearchAnalyticsFilterGroup {
28
35
  filters: GscSearchAnalyticsFilter[];
29
36
  }
30
37
  type GscSearchType = 'web' | 'image' | 'video' | 'news' | 'discover' | 'googleNews';
38
+ type GscAggregationType = 'auto' | 'byPage' | 'byProperty' | 'byNewsShowcasePanel';
39
+ type GscResponseAggregationType = 'auto' | 'byPage' | 'byProperty' | 'byNewsShowcasePanel';
31
40
  interface GscSearchAnalyticsRequest {
32
41
  startDate: string;
33
42
  endDate: string;
@@ -35,7 +44,10 @@ interface GscSearchAnalyticsRequest {
35
44
  dimensionFilterGroups?: GscSearchAnalyticsFilterGroup[];
36
45
  rowLimit?: number;
37
46
  startRow?: number;
38
- searchType?: GscSearchType;
47
+ /** GSC search corpus. Maps to wire field `type` (the API still accepts the deprecated `searchType` alias). */
48
+ type?: GscSearchType;
49
+ dataState?: GscDataState;
50
+ aggregationType?: GscAggregationType;
39
51
  }
40
52
  interface GscSearchAnalyticsRow {
41
53
  keys: string[];
@@ -46,6 +58,7 @@ interface GscSearchAnalyticsRow {
46
58
  }
47
59
  interface GscSearchAnalyticsResponse {
48
60
  rows?: GscSearchAnalyticsRow[];
49
- responseAggregationType?: string;
61
+ responseAggregationType?: GscResponseAggregationType;
62
+ metadata?: GscSearchAnalyticsMetadata;
50
63
  }
51
- export { ColumnDef, ColumnType, GscSearchAnalyticsDimension, GscSearchAnalyticsFilter, GscSearchAnalyticsFilterGroup, GscSearchAnalyticsFilterOperator, GscSearchAnalyticsRequest, GscSearchAnalyticsResponse, GscSearchAnalyticsRow, GscSearchType, Row, TableName, TableSchema, TenantCtx };
64
+ export { ColumnDef, ColumnType, GscAggregationType, GscDataState, GscResponseAggregationType, GscSearchAnalyticsDimension, GscSearchAnalyticsFilter, GscSearchAnalyticsFilterGroup, GscSearchAnalyticsFilterOperator, GscSearchAnalyticsMetadata, GscSearchAnalyticsRequest, GscSearchAnalyticsResponse, GscSearchAnalyticsRow, GscSearchType, Row, TableName, TableSchema, TenantCtx };
package/dist/index.d.mts CHANGED
@@ -12,7 +12,14 @@ declare function runSequentialBatch<I, R>(items: I[], operation: (item: I, index
12
12
  concurrency?: number;
13
13
  onProgress?: (result: R, index: number, total: number) => void;
14
14
  }): Promise<R[]>;
15
- type GscSearchAnalyticsDimension = 'page' | 'query' | 'country' | 'device' | 'date' | 'searchAppearance';
15
+ type GscSearchAnalyticsDimension = 'page' | 'query' | 'country' | 'device' | 'date' | 'hour' | 'searchAppearance';
16
+ type GscDataState = 'final' | 'all' | 'hourly_all';
17
+ interface GscSearchAnalyticsMetadata {
18
+ /** First date (YYYY-MM-DD, PT) still being collected. Populated when dataState=`all` and grouped by `date`. */
19
+ first_incomplete_date?: string;
20
+ /** First hour (YYYY-MM-DDThh:mm:ss±hh:mm, PT) still being collected. Populated when dataState=`hourly_all` and grouped by `hour`. */
21
+ first_incomplete_hour?: string;
22
+ }
16
23
  type GscSearchAnalyticsFilterOperator = 'equals' | 'notEquals' | 'contains' | 'notContains' | 'includingRegex' | 'excludingRegex';
17
24
  interface GscSearchAnalyticsFilter {
18
25
  dimension: GscSearchAnalyticsDimension;
@@ -24,6 +31,8 @@ interface GscSearchAnalyticsFilterGroup {
24
31
  filters: GscSearchAnalyticsFilter[];
25
32
  }
26
33
  type GscSearchType = 'web' | 'image' | 'video' | 'news' | 'discover' | 'googleNews';
34
+ type GscAggregationType = 'auto' | 'byPage' | 'byProperty' | 'byNewsShowcasePanel';
35
+ type GscResponseAggregationType = 'auto' | 'byPage' | 'byProperty' | 'byNewsShowcasePanel';
27
36
  interface GscSearchAnalyticsRequest {
28
37
  startDate: string;
29
38
  endDate: string;
@@ -31,7 +40,10 @@ interface GscSearchAnalyticsRequest {
31
40
  dimensionFilterGroups?: GscSearchAnalyticsFilterGroup[];
32
41
  rowLimit?: number;
33
42
  startRow?: number;
34
- searchType?: GscSearchType;
43
+ /** GSC search corpus. Maps to wire field `type` (the API still accepts the deprecated `searchType` alias). */
44
+ type?: GscSearchType;
45
+ dataState?: GscDataState;
46
+ aggregationType?: GscAggregationType;
35
47
  }
36
48
  declare const _default: {
37
49
  name: string;
@@ -64,6 +76,8 @@ interface DimensionValueMap {
64
76
  device: Device;
65
77
  searchAppearance: string;
66
78
  date: string;
79
+ /** Hour bucket — ISO-8601 with PT offset, e.g. `2025-07-14T13:00:00-07:00`. Use with `dataState: 'hourly_all'`. */
80
+ hour: string;
67
81
  }
68
82
  type Dimension = keyof DimensionValueMap;
69
83
  interface QueryParamValueMap {
@@ -113,6 +127,12 @@ interface BuilderState {
113
127
  };
114
128
  rowLimit?: number;
115
129
  startRow?: number;
130
+ /** GSC `dataState`. `'hourly_all'` is required when grouping by `hour`. */
131
+ dataState?: GscDataState;
132
+ /** GSC `aggregationType`. `'byNewsShowcasePanel'` requires `type=discover|googleNews` with NEWS_SHOWCASE searchAppearance. */
133
+ aggregationType?: GscAggregationType;
134
+ /** GSC search corpus. Wins over any `searchType` filter when both are set. */
135
+ searchType?: SearchType;
116
136
  }
117
137
  type SelectableColumn = Column<Dimension> | MetricColumn<Metric>;
118
138
  type OrderableColumn = MetricColumn<Metric> | Column<'date'>;
@@ -127,6 +147,10 @@ interface GSCQueryBuilder<D extends Dimension[] = [], C = object> {
127
147
  orderBy: (col: OrderableColumn, dir: 'asc' | 'desc') => GSCQueryBuilder<D, C>;
128
148
  limit: (n: number) => GSCQueryBuilder<D, C>;
129
149
  offset: (n: number) => GSCQueryBuilder<D, C>;
150
+ dataState: (state: GscDataState) => GSCQueryBuilder<D, C>;
151
+ aggregationType: (type: GscAggregationType) => GSCQueryBuilder<D, C>;
152
+ /** GSC search corpus (`type` on the wire). Equivalent to filtering by `searchType`. */
153
+ type: (t: GscSearchType) => GSCQueryBuilder<D, C>;
130
154
  toBody: () => GscSearchAnalyticsRequest;
131
155
  getState: () => BuilderState;
132
156
  }
@@ -239,16 +263,23 @@ declare function createFetch(auth: Auth, options?: FetchOptions): $Fetch;
239
263
  interface CallOptions {
240
264
  signal?: AbortSignal;
241
265
  }
266
+ /** Generator return value for `client.query` — exposes API response metadata plus the resolved aggregation type Google actually used (may differ from the requested `aggregationType: 'auto'`). */
267
+ interface QueryReturn {
268
+ metadata?: GscSearchAnalyticsMetadata;
269
+ /** Aggregation type Google actually used. Useful when requesting `auto` to know if rows were aggregated `byPage` vs `byProperty`. */
270
+ responseAggregationType?: GscResponseAggregationType;
271
+ }
242
272
  interface GoogleSearchConsoleClient {
243
273
  /** Query search analytics with builder, returns async generator yielding typed row batches */
244
- query: <D extends Dimension[], C>(siteUrl: string, builder: GSCQueryBuilder<D, C>, opts?: CallOptions) => AsyncGenerator<GSCRow<D, C>[]>;
274
+ query: <D extends Dimension[], C>(siteUrl: string, builder: GSCQueryBuilder<D, C>, opts?: CallOptions) => AsyncGenerator<GSCRow<D, C>[], QueryReturn>;
245
275
  /**
246
276
  * List all sites. Also exposes write ops as `client.sites.add(siteUrl)` and
247
277
  * `client.sites.delete(siteUrl)`. Calling `client.sites()` is equivalent to
248
278
  * `client.sites.list()`.
249
279
  */
250
280
  sites: ((opts?: CallOptions) => Promise<ApiSite[]>) & {
251
- list: (opts?: CallOptions) => Promise<ApiSite[]>; /** Add a property in unverified state. Caller must verify ownership separately. */
281
+ list: (opts?: CallOptions) => Promise<ApiSite[]>; /** Retrieve a single property (with permission level). 404 if not in the user's account. */
282
+ get: (siteUrl: string, opts?: CallOptions) => Promise<ApiSite>; /** Add a property in unverified state. Caller must verify ownership separately. */
252
283
  add: (siteUrl: string, opts?: CallOptions) => Promise<void>; /** Remove a property from the user's account. */
253
284
  delete: (siteUrl: string, opts?: CallOptions) => Promise<void>;
254
285
  };
@@ -266,11 +297,15 @@ interface GoogleSearchConsoleClient {
266
297
  get: (id: string, opts?: CallOptions) => Promise<VerificationWebResource>;
267
298
  delete: (id: string, opts?: CallOptions) => Promise<void>;
268
299
  };
269
- /** Inspect a URL */
270
- inspect: (siteUrl: string, url: string, opts?: CallOptions) => Promise<InspectUrlIndexResponse>;
300
+ /** Inspect a URL. `languageCode` is a BCP-47 tag for translating result strings; omit to let Google pick. */
301
+ inspect: (siteUrl: string, url: string, opts?: CallOptions & {
302
+ languageCode?: string;
303
+ }) => Promise<InspectUrlIndexResponse>;
271
304
  /** Sitemap operations */
272
305
  sitemaps: {
273
- list: (siteUrl: string, opts?: CallOptions) => Promise<ApiSitemap[]>;
306
+ /** List sitemaps. Pass `sitemapIndex` to list children of a sitemap-index file. */list: (siteUrl: string, opts?: CallOptions & {
307
+ sitemapIndex?: string;
308
+ }) => Promise<ApiSitemap[]>;
274
309
  get: (siteUrl: string, feedpath: string, opts?: CallOptions) => Promise<ApiSitemap>;
275
310
  submit: (siteUrl: string, feedpath: string, opts?: CallOptions) => Promise<void>;
276
311
  delete: (siteUrl: string, feedpath: string, opts?: CallOptions) => Promise<void>;
@@ -363,6 +398,14 @@ interface ParsedIndexingResult {
363
398
  mobileIssues: string | null;
364
399
  richResultsVerdict: string | null;
365
400
  richResultsItems: string | null;
401
+ ampVerdict: string | null;
402
+ ampUrl: string | null;
403
+ ampIndexingState: string | null;
404
+ ampIndexStatusVerdict: string | null;
405
+ ampRobotsTxtState: string | null;
406
+ ampPageFetchState: string | null;
407
+ ampLastCrawlTime: string | null;
408
+ ampIssues: string | null;
366
409
  inspectionResultLink: string | null;
367
410
  }
368
411
  declare function inspectUrlFlat(client: GoogleSearchConsoleClient, siteUrl: string, inspectionUrl: string): Promise<ParsedIndexingResult>;
@@ -406,7 +449,7 @@ declare function fetchSites(client: GoogleSearchConsoleClient): Promise<ApiSite[
406
449
  * Fetches all verified sites with their sitemaps from Google Search Console.
407
450
  */
408
451
  declare function fetchSitesWithSitemaps(client: GoogleSearchConsoleClient): Promise<(Site & {
409
- sitemaps: RequiredNonNullable<ApiSitemap>[];
452
+ sitemaps: ApiSitemap[];
410
453
  })[]>;
411
454
  /**
412
455
  * Fetches all sitemaps for a site.
@@ -690,8 +733,8 @@ declare function findBestGscProperty<T extends GscPropertyCandidate>(targetDomai
690
733
  };
691
734
  declare function findExactGscProperty<T extends GscPropertyCandidate>(propertyUrl: string, properties: readonly T[]): T | null;
692
735
  declare function formatGscPropertyCandidates(candidates: ReadonlyArray<GscPropertyCandidate | null | undefined>): string;
693
- declare const INDEXING_DAILY_LIMIT = 2000;
694
- declare const INDEXING_EFFECTIVE_LIMIT = 1800;
736
+ declare const URL_INSPECTION_DAILY_LIMIT = 2000;
737
+ declare const URL_INSPECTION_EFFECTIVE_LIMIT = 1800;
695
738
  declare function hasGscReadScope(scopes: string | null | undefined): boolean;
696
739
  declare function hasGscWriteScope(scopes: string | null | undefined): boolean;
697
740
  declare function hasIndexingScope(scopes: string | null | undefined): boolean;
@@ -706,6 +749,14 @@ interface ParsedGscSiteUrl {
706
749
  isDomain: boolean;
707
750
  }
708
751
  declare function parseGscSiteUrl(siteUrl: string): ParsedGscSiteUrl;
752
+ /**
753
+ * Comparison-canonical form of a GSC property URL. Strips `sc-domain:`,
754
+ * protocol, leading `www.`, trailing slash, and lowercases. Use when matching
755
+ * the same logical site across `https://www.example.com/` vs
756
+ * `sc-domain:example.com` vs bare hostnames — properties Google sometimes
757
+ * returns in different shapes for the same site.
758
+ */
759
+ declare function normalizeGscSiteUrl(siteUrl: string): string;
709
760
  /**
710
761
  * Normalize a user-input URL/hostname into a canonical registration target.
711
762
  * Returns lowercase hostname stripped of protocol, or null if unparseable.
@@ -842,4 +893,4 @@ interface FetchSitemapUrlsOptions extends DiscoverSitemapOptions {
842
893
  * `<loc>https://...</loc>` shape but doesn't validate the schema.
843
894
  */
844
895
  declare function fetchSitemapUrls(sitemapUrl: string, options?: FetchSitemapUrlsOptions): Promise<string[]>;
845
- export { AccountNextAction, AccountStatus, AnalyticsNextAction, AnalyticsStatus, ApiSite, ApiSitemap, ApiSitemapContent, Auth, AuthClient, AuthOptions, BackfillProgress, CallOptions, DAYS_PER_RANGE, DataRow, DimensionFilter, DimensionFilterGroup, DiscoverSitemapOptions, FetchSitemapUrlsOptions, GSCDUMP_ONBOARDING_CONTRACT_VERSION, GSCDUMP_OPTIONAL_INDEXING_SCOPE, GSCDUMP_REQUIRED_ANALYTICS_SCOPE, GSC_FINALIZED_LAG_DAYS, GSC_FRESHEST_LAG_DAYS, GSC_QUOTAS, GSC_RETENTION_MONTHS, GoogleSearchConsoleClient, GoogleSearchConsoleClientOptions, GscApiError, GscApiErrorInfo, GscError, GscErrorKind, GscPropertyCandidate, GscdumpApiOptions, INDEXING_DAILY_LIMIT, INDEXING_EFFECTIVE_LIMIT, INDEXING_ISSUE_FILTERS, INDEXING_ISSUE_LABELS, INDEXING_ISSUE_SEVERITY, IndexStatusResult, IndexingEligibility, IndexingIneligibleReason, IndexingIssueType, IndexingMetadata, IndexingNextAction, IndexingNotificationType, IndexingResult, IndexingStatus, InspectUrlIndexResponse, InspectUrlResult, InspectionPriority, LifecycleError, LifecycleErrorCode, LifecycleProgress, LifecycleWebhookEnvelope, LifecycleWebhookEvent, MS_PER_DAY, MobileUsabilityResult, OAuthTokens, ParsedGscSiteUrl, ParsedIndexingResult, PartnerLifecycleAccount, PartnerLifecycleResponse, PartnerLifecycleSite, Period, PropertyNextAction, PropertyStatus, PublishUrlNotificationResponse, QuerySourceMode, RequiredNonNullable, ResolvedAnalyticsRange, RichResultsResult, SearchAnalyticsQuery, SearchAnalyticsResponse, Site, SiteAnalytics, SitemapNextAction, SitemapStatus, UrlInspectionResult, UrlNotificationMetadata, VerificationMethod, VerificationSite, VerificationSiteType, VerificationToken, VerificationWebResource, accountNextActions, accountStatuses, addDays, addSite, analyticsNextActions, analyticsStatuses, batchInspectUrls, batchRequestIndexing, canUseUrlInspection, classifyError, countDays, createAuth, createFetch, daysAgo, deleteSite, deleteSitemap, discoverSitemap, exchangeAuthCode, fetchSitemap, fetchSitemapUrls, fetchSitemaps, fetchSites, fetchSitesWithSitemaps, findBestGscProperty, findExactGscProperty, formatErrorForCli, formatGscPropertyCandidates, generateGscDateRange, getBackfillProgress, getDateRange, getFreshestGscDate, getIndexingEligibility, getIndexingMetadata, getLatestGscDate, getNextCheckAfter, getNextCheckPriority, getNextDate, getOldestGscDate, getPendingDates, getPreviousDate, getPstDate, getVerificationToken, getVerifiedSite, googleSearchConsole, grantedScopeList, groupIntoRanges, gscPropertyMatchesTarget, gscdumpApi, hasGscReadScope, hasGscWriteScope, hasIndexingScope, hasOptionalIndexingScope, hasRequiredAnalyticsScope, indexingNextActions, indexingStatuses, inspectUrl, inspectUrlFlat, isPermissionDeniedError, isValidGscDate, isVerifiedGscPermission, isVerifiedGscProperty, lifecycleErrorCodes, lifecycleWebhookEvents, listVerifiedSites, matchGscSite, normalizeRegistrationTarget, parseGoogleError, parseGrantedScopes, parseGscSiteUrl, pickBestGscProperty, progressBar, propertyNextActions, propertyStatuses, querySourceModes, refreshAccessToken, requestIndexing, rethrowAsGscApiError, rowWithMetricDefaults, runSequentialBatch, siteUrlToVerificationSite, sitemapNextActions, sitemapStatuses, storageError, submitSitemap, toIsoDate, unverifySite, verificationMethodsFor, verifySite };
896
+ export { AccountNextAction, AccountStatus, AnalyticsNextAction, AnalyticsStatus, ApiSite, ApiSitemap, ApiSitemapContent, Auth, AuthClient, AuthOptions, BackfillProgress, CallOptions, DAYS_PER_RANGE, DataRow, DimensionFilter, DimensionFilterGroup, DiscoverSitemapOptions, FetchSitemapUrlsOptions, GSCDUMP_ONBOARDING_CONTRACT_VERSION, GSCDUMP_OPTIONAL_INDEXING_SCOPE, GSCDUMP_REQUIRED_ANALYTICS_SCOPE, GSC_FINALIZED_LAG_DAYS, GSC_FRESHEST_LAG_DAYS, GSC_QUOTAS, GSC_RETENTION_MONTHS, GoogleSearchConsoleClient, GoogleSearchConsoleClientOptions, GscApiError, GscApiErrorInfo, GscError, GscErrorKind, GscPropertyCandidate, GscdumpApiOptions, INDEXING_ISSUE_FILTERS, INDEXING_ISSUE_LABELS, INDEXING_ISSUE_SEVERITY, IndexStatusResult, IndexingEligibility, IndexingIneligibleReason, IndexingIssueType, IndexingMetadata, IndexingNextAction, IndexingNotificationType, IndexingResult, IndexingStatus, InspectUrlIndexResponse, InspectUrlResult, InspectionPriority, LifecycleError, LifecycleErrorCode, LifecycleProgress, LifecycleWebhookEnvelope, LifecycleWebhookEvent, MS_PER_DAY, MobileUsabilityResult, OAuthTokens, ParsedGscSiteUrl, ParsedIndexingResult, PartnerLifecycleAccount, PartnerLifecycleResponse, PartnerLifecycleSite, Period, PropertyNextAction, PropertyStatus, PublishUrlNotificationResponse, QueryReturn, QuerySourceMode, RequiredNonNullable, ResolvedAnalyticsRange, RichResultsResult, SearchAnalyticsQuery, SearchAnalyticsResponse, Site, SiteAnalytics, SitemapNextAction, SitemapStatus, URL_INSPECTION_DAILY_LIMIT, URL_INSPECTION_EFFECTIVE_LIMIT, UrlInspectionResult, UrlNotificationMetadata, VerificationMethod, VerificationSite, VerificationSiteType, VerificationToken, VerificationWebResource, accountNextActions, accountStatuses, addDays, addSite, analyticsNextActions, analyticsStatuses, batchInspectUrls, batchRequestIndexing, canUseUrlInspection, classifyError, countDays, createAuth, createFetch, daysAgo, deleteSite, deleteSitemap, discoverSitemap, exchangeAuthCode, fetchSitemap, fetchSitemapUrls, fetchSitemaps, fetchSites, fetchSitesWithSitemaps, findBestGscProperty, findExactGscProperty, formatErrorForCli, formatGscPropertyCandidates, generateGscDateRange, getBackfillProgress, getDateRange, getFreshestGscDate, getIndexingEligibility, getIndexingMetadata, getLatestGscDate, getNextCheckAfter, getNextCheckPriority, getNextDate, getOldestGscDate, getPendingDates, getPreviousDate, getPstDate, getVerificationToken, getVerifiedSite, googleSearchConsole, grantedScopeList, groupIntoRanges, gscPropertyMatchesTarget, gscdumpApi, hasGscReadScope, hasGscWriteScope, hasIndexingScope, hasOptionalIndexingScope, hasRequiredAnalyticsScope, indexingNextActions, indexingStatuses, inspectUrl, inspectUrlFlat, isPermissionDeniedError, isValidGscDate, isVerifiedGscPermission, isVerifiedGscProperty, lifecycleErrorCodes, lifecycleWebhookEvents, listVerifiedSites, matchGscSite, normalizeGscSiteUrl, normalizeRegistrationTarget, parseGoogleError, parseGrantedScopes, parseGscSiteUrl, pickBestGscProperty, progressBar, propertyNextActions, propertyStatuses, querySourceModes, refreshAccessToken, requestIndexing, rethrowAsGscApiError, rowWithMetricDefaults, runSequentialBatch, siteUrlToVerificationSite, sitemapNextActions, sitemapStatuses, storageError, submitSitemap, toIsoDate, unverifySite, verificationMethodsFor, verifySite };
package/dist/index.mjs CHANGED
@@ -50,17 +50,26 @@ async function batchRequestIndexing(client, urls, options = {}) {
50
50
  onProgress
51
51
  });
52
52
  }
53
+ const WEBMASTERS_READ = "https://www.googleapis.com/auth/webmasters.readonly";
54
+ const WEBMASTERS_WRITE = "https://www.googleapis.com/auth/webmasters";
55
+ const INDEXING = "https://www.googleapis.com/auth/indexing";
56
+ function tokenize(scopes) {
57
+ if (!scopes) return [];
58
+ return scopes.split(/\s+/).filter(Boolean);
59
+ }
60
+ function hasScope(scopes, scope) {
61
+ const tokens = tokenize(scopes);
62
+ const suffix = scope.replace("https://www.googleapis.com/auth/", "");
63
+ return tokens.includes(scope) || tokens.includes(suffix);
64
+ }
53
65
  function hasGscReadScope(scopes) {
54
- if (!scopes) return false;
55
- return scopes.includes("webmasters.readonly") || scopes.includes("webmasters");
66
+ return hasScope(scopes, WEBMASTERS_READ) || hasScope(scopes, WEBMASTERS_WRITE);
56
67
  }
57
68
  function hasGscWriteScope(scopes) {
58
- if (!scopes) return false;
59
- return scopes.includes("webmasters") && !scopes.includes("webmasters.readonly");
69
+ return hasScope(scopes, WEBMASTERS_WRITE);
60
70
  }
61
71
  function hasIndexingScope(scopes) {
62
- if (!scopes) return false;
63
- return scopes.includes("googleapis.com/auth/indexing");
72
+ return hasScope(scopes, INDEXING);
64
73
  }
65
74
  async function inspectUrl(client, siteUrl, inspectionUrl) {
66
75
  const inspection = (await client.inspect(siteUrl, inspectionUrl)).inspectionResult;
@@ -89,6 +98,7 @@ async function inspectUrlFlat(client, siteUrl, inspectionUrl) {
89
98
  const index = inspection?.indexStatusResult;
90
99
  const mobile = inspection?.mobileUsabilityResult;
91
100
  const rich = inspection?.richResultsResult;
101
+ const amp = inspection?.ampResult;
92
102
  return {
93
103
  url: inspectionUrl,
94
104
  verdict: index?.verdict ?? null,
@@ -106,6 +116,14 @@ async function inspectUrlFlat(client, siteUrl, inspectionUrl) {
106
116
  mobileIssues: mobile?.issues?.length ? JSON.stringify(mobile.issues) : null,
107
117
  richResultsVerdict: rich?.verdict ?? null,
108
118
  richResultsItems: rich?.detectedItems?.length ? JSON.stringify(rich.detectedItems) : null,
119
+ ampVerdict: amp?.verdict ?? null,
120
+ ampUrl: amp?.ampUrl ?? null,
121
+ ampIndexingState: amp?.indexingState ?? null,
122
+ ampIndexStatusVerdict: amp?.ampIndexStatusVerdict ?? null,
123
+ ampRobotsTxtState: amp?.robotsTxtState ?? null,
124
+ ampPageFetchState: amp?.pageFetchState ?? null,
125
+ ampLastCrawlTime: amp?.lastCrawlTime ?? null,
126
+ ampIssues: amp?.issues?.length ? JSON.stringify(amp.issues) : null,
109
127
  inspectionResultLink: inspection?.inspectionResultLink ?? null
110
128
  };
111
129
  }
@@ -123,7 +141,11 @@ function getNextCheckAfter(priority) {
123
141
  case "low": return now + 30 * 86400;
124
142
  }
125
143
  }
126
- const INSPECTION_ALLOWED_PERMISSIONS = new Set(["siteOwner", "siteFullUser"]);
144
+ const INSPECTION_ALLOWED_PERMISSIONS = new Set([
145
+ "siteOwner",
146
+ "siteFullUser",
147
+ "siteRestrictedUser"
148
+ ]);
127
149
  function canUseUrlInspection(permissionLevel) {
128
150
  return !!permissionLevel && INSPECTION_ALLOWED_PERMISSIONS.has(permissionLevel);
129
151
  }
@@ -216,6 +238,44 @@ function extractRetryAfter(error) {
216
238
  }
217
239
  }
218
240
  const QUOTA_MESSAGE_RE = /quota|rate\s*limit/i;
241
+ const QUOTA_REASONS = new Set([
242
+ "dailyLimitExceeded",
243
+ "dailyLimitExceededUnreg",
244
+ "rateLimitExceeded",
245
+ "rateLimitExceededUnreg",
246
+ "userRateLimitExceeded",
247
+ "userRateLimitExceededUnreg",
248
+ "quotaExceeded",
249
+ "concurrentLimitExceeded",
250
+ "variableTermLimitExceeded",
251
+ "variableTermExpiredDailyExceeded",
252
+ "servingLimitExceeded",
253
+ "responseTooLarge",
254
+ "limitExceeded",
255
+ "batchSizeTooLarge",
256
+ "RATE_LIMIT_EXCEEDED",
257
+ "RESOURCE_EXHAUSTED"
258
+ ]);
259
+ function extractReason(cause) {
260
+ const data = pickField(cause, [["data"]], (v) => true);
261
+ if (data) {
262
+ const errorInfo = pickField(data, [["error", "details"]], (v) => Array.isArray(v));
263
+ if (errorInfo) {
264
+ const reason = errorInfo.find((d) => typeof d["@type"] === "string" && d["@type"].includes("ErrorInfo"))?.reason;
265
+ if (typeof reason === "string") return reason;
266
+ }
267
+ const directErrors = pickField(data, [["error", "errors"]], (v) => Array.isArray(v));
268
+ if (directErrors) {
269
+ const reason = directErrors[0]?.reason;
270
+ if (typeof reason === "string") return reason;
271
+ }
272
+ }
273
+ }
274
+ function isQuotaCondition(cause, message) {
275
+ const reason = extractReason(cause);
276
+ if (reason && QUOTA_REASONS.has(reason)) return true;
277
+ return QUOTA_MESSAGE_RE.test(message);
278
+ }
219
279
  function classifyError(cause) {
220
280
  const status = extractStatus(cause);
221
281
  const message = extractMessage(cause);
@@ -231,7 +291,7 @@ function classifyError(cause) {
231
291
  cause
232
292
  };
233
293
  if (status === 403) {
234
- if (QUOTA_MESSAGE_RE.test(message)) return {
294
+ if (isQuotaCondition(cause, message)) return {
235
295
  kind: "rate-limited",
236
296
  message,
237
297
  retryAfter: extractRetryAfter(cause),
@@ -243,12 +303,12 @@ function classifyError(cause) {
243
303
  cause
244
304
  };
245
305
  }
246
- if (status === 404) return {
306
+ if (status === 404 || status === 410) return {
247
307
  kind: "not-found",
248
308
  message,
249
309
  cause
250
310
  };
251
- if (status === 400 || status === 422) return {
311
+ if (status === 400 || status === 402 || status === 409 || status === 413 || status === 422) return {
252
312
  kind: "validation",
253
313
  message,
254
314
  cause
@@ -412,7 +472,7 @@ async function fetchSites(client) {
412
472
  async function fetchSitesWithSitemaps(client) {
413
473
  const sites = (await client.sites()).filter((s) => !!s.siteUrl && s.permissionLevel !== "siteUnverifiedUser");
414
474
  return Promise.all(sites.map(async (site) => {
415
- const sitemaps = site.permissionLevel === "siteOwner" ? await client.sitemaps.list(site.siteUrl) : [];
475
+ const sitemaps = await client.sitemaps.list(site.siteUrl).catch(() => []);
416
476
  return {
417
477
  ...site,
418
478
  sitemaps
@@ -2152,9 +2212,9 @@ function extractSpecialFilters(filter) {
2152
2212
  else otherFilters.push(f);
2153
2213
  if (filter._nestedGroups) for (const nested of filter._nestedGroups) {
2154
2214
  const extracted = extractSpecialFilters(nested);
2155
- if (extracted.startDate) startDate = extracted.startDate;
2156
- if (extracted.endDate) endDate = extracted.endDate;
2157
- if (extracted.searchType) searchType = extracted.searchType;
2215
+ if (!startDate && extracted.startDate) startDate = extracted.startDate;
2216
+ if (!endDate && extracted.endDate) endDate = extracted.endDate;
2217
+ if (!searchType && extracted.searchType) searchType = extracted.searchType;
2158
2218
  if (extracted.dimensionFilter) cleanedNestedGroups.push(extracted.dimensionFilter);
2159
2219
  }
2160
2220
  const dimensionFilter = otherFilters.length > 0 || cleanedNestedGroups.length > 0 ? {
@@ -2177,11 +2237,40 @@ function resolveToBody(state) {
2177
2237
  startDate,
2178
2238
  endDate
2179
2239
  };
2180
- if (searchType) body.searchType = searchType;
2181
- if (state.rowLimit) body.rowLimit = state.rowLimit;
2182
- if (state.startRow) body.startRow = state.startRow;
2240
+ const resolvedType = state.searchType ?? searchType;
2241
+ if (resolvedType) body.type = resolvedType;
2242
+ if (state.rowLimit !== void 0) {
2243
+ if (!Number.isInteger(state.rowLimit) || state.rowLimit < 1) throw new Error(`rowLimit must be a positive integer, got ${state.rowLimit}`);
2244
+ body.rowLimit = state.rowLimit;
2245
+ }
2246
+ if (state.startRow !== void 0) {
2247
+ if (!Number.isInteger(state.startRow) || state.startRow < 0) throw new Error(`startRow must be a non-negative integer, got ${state.startRow}`);
2248
+ if (state.startRow > 0) body.startRow = state.startRow;
2249
+ }
2250
+ const hasHour = state.dimensions?.includes("hour");
2251
+ if (hasHour && state.dataState !== "hourly_all") throw new Error("hour dimension requires dataState: \"hourly_all\"");
2252
+ if (state.dataState === "hourly_all" && !hasHour) throw new Error("dataState: \"hourly_all\" requires grouping by hour dimension");
2253
+ if (state.dataState) body.dataState = state.dataState;
2183
2254
  const filterGroups = resolveFilter(dimensionFilter);
2184
2255
  if (filterGroups.length > 0) body.dimensionFilterGroups = filterGroups;
2256
+ if (state.aggregationType) {
2257
+ const groupsByPage = (body.dimensions ?? []).includes("page");
2258
+ const apiLeafFilters = filterGroups.flatMap((g) => g.filters ?? []);
2259
+ const filtersByPage = apiLeafFilters.some((f) => f.dimension === "page");
2260
+ if (state.aggregationType === "byProperty") {
2261
+ if (body.type === "discover" || body.type === "googleNews") throw new Error("aggregationType: \"byProperty\" is not supported for type \"discover\" or \"googleNews\"");
2262
+ if (groupsByPage || filtersByPage) throw new Error("aggregationType: \"byProperty\" is not allowed when grouping or filtering by page");
2263
+ }
2264
+ if (state.aggregationType === "byNewsShowcasePanel") {
2265
+ if (body.type !== "discover" && body.type !== "googleNews") throw new Error("aggregationType: \"byNewsShowcasePanel\" requires type \"discover\" or \"googleNews\"");
2266
+ if (groupsByPage || filtersByPage) throw new Error("aggregationType: \"byNewsShowcasePanel\" is not allowed when grouping or filtering by page");
2267
+ const saFilters = apiLeafFilters.filter((f) => f.dimension === "searchAppearance");
2268
+ const hasNewsShowcase = saFilters.some((f) => f.operator === "equals" && f.expression === "NEWS_SHOWCASE");
2269
+ const hasOther = saFilters.some((f) => !(f.operator === "equals" && f.expression === "NEWS_SHOWCASE"));
2270
+ if (!hasNewsShowcase || hasOther) throw new Error("aggregationType: \"byNewsShowcasePanel\" requires a searchAppearance equals \"NEWS_SHOWCASE\" filter and no other searchAppearance filter");
2271
+ }
2272
+ body.aggregationType = state.aggregationType;
2273
+ }
2185
2274
  return body;
2186
2275
  }
2187
2276
  function isApiFilter(f) {
@@ -2232,6 +2321,8 @@ function gscdumpApi(options) {
2232
2321
  retryDelay: 1e3,
2233
2322
  retryStatusCodes: [
2234
2323
  408,
2324
+ 409,
2325
+ 425,
2235
2326
  429,
2236
2327
  500,
2237
2328
  502,
@@ -2263,10 +2354,16 @@ function gscdumpApi(options) {
2263
2354
  async *query(siteId, builder, opts) {
2264
2355
  const state = builder.getState();
2265
2356
  const body = resolveToBody(state);
2266
- const rowLimit = body.rowLimit || 25e3;
2357
+ const totalCap = body.rowLimit;
2358
+ const pageSize = Math.min(totalCap ?? 25e3, 25e3);
2267
2359
  let startRow = body.startRow || 0;
2360
+ let yielded = 0;
2361
+ let metadata;
2268
2362
  while (true) {
2269
2363
  opts?.signal?.throwIfAborted();
2364
+ const remaining = totalCap ? totalCap - yielded : pageSize;
2365
+ if (remaining <= 0) break;
2366
+ const rowLimit = Math.min(pageSize, remaining);
2270
2367
  const response = await fetch(`/api/sites/${encodeURIComponent(siteId)}/query`, {
2271
2368
  method: "POST",
2272
2369
  body: {
@@ -2276,6 +2373,7 @@ function gscdumpApi(options) {
2276
2373
  },
2277
2374
  signal: opts?.signal
2278
2375
  });
2376
+ if (response.metadata) metadata = response.metadata;
2279
2377
  const rows = response.rows.map((row) => {
2280
2378
  const result = rowWithMetricDefaults(row);
2281
2379
  state.dimensions.forEach((dim) => {
@@ -2284,9 +2382,11 @@ function gscdumpApi(options) {
2284
2382
  return result;
2285
2383
  });
2286
2384
  yield rows;
2385
+ yielded += rows.length;
2287
2386
  if (!response.meta.hasMore || rows.length < rowLimit) break;
2288
2387
  startRow += rows.length;
2289
2388
  }
2389
+ return { metadata };
2290
2390
  },
2291
2391
  sites: (() => {
2292
2392
  const list = async (opts) => {
@@ -2300,6 +2400,7 @@ function gscdumpApi(options) {
2300
2400
  };
2301
2401
  return Object.assign(list, {
2302
2402
  list,
2403
+ get: unsupported("get"),
2303
2404
  add: unsupported("add"),
2304
2405
  delete: unsupported("delete")
2305
2406
  });
@@ -2352,6 +2453,15 @@ function gscdumpApi(options) {
2352
2453
  const GSC_API = "https://searchconsole.googleapis.com";
2353
2454
  const INDEXING_API = "https://indexing.googleapis.com";
2354
2455
  const SITE_VERIFICATION_API = "https://www.googleapis.com/siteVerification/v1";
2456
+ function encodeSiteUrl(siteUrl) {
2457
+ if (siteUrl.startsWith("sc-domain:")) return `sc-domain:${encodeURIComponent(siteUrl.slice(10))}`;
2458
+ return encodeURIComponent(siteUrl);
2459
+ }
2460
+ function assertValidSiteUrl(siteUrl) {
2461
+ if (siteUrl.startsWith("sc-domain:") && siteUrl.length > 10) return;
2462
+ if (/^https?:\/\/.+/.test(siteUrl)) return;
2463
+ throw new Error(`Invalid siteUrl: expected "https?://…" or "sc-domain:…", got "${siteUrl}"`);
2464
+ }
2355
2465
  function createAuth(options) {
2356
2466
  let credentials = { refresh_token: options.refreshToken };
2357
2467
  return {
@@ -2393,10 +2503,21 @@ function createFetch(auth, options) {
2393
2503
  return ofetch.create({
2394
2504
  ...options,
2395
2505
  retry: 3,
2396
- retryDelay: 1e3,
2506
+ retryDelay: (ctx) => {
2507
+ const status = ctx.response?.status;
2508
+ if (status === 429 || status === 503) {
2509
+ const header = ctx.response?.headers.get("retry-after");
2510
+ if (header) {
2511
+ const secs = Number.parseInt(header, 10);
2512
+ if (Number.isFinite(secs)) return secs * 1e3;
2513
+ const when = Date.parse(header);
2514
+ if (Number.isFinite(when)) return Math.max(0, when - Date.now());
2515
+ }
2516
+ }
2517
+ return 1e3;
2518
+ },
2397
2519
  retryStatusCodes: [
2398
2520
  408,
2399
- 409,
2400
2521
  425,
2401
2522
  429,
2402
2523
  500,
@@ -2439,7 +2560,7 @@ function googleSearchConsole(auth, options = {}) {
2439
2560
  }
2440
2561
  fetch = createFetch(authState, fetchOptions);
2441
2562
  }
2442
- const rawQuery = (siteUrl, body, opts) => fetch(`${GSC_API}/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/searchAnalytics/query`, {
2563
+ const rawQuery = (siteUrl, body, opts) => fetch(`${GSC_API}/webmasters/v3/sites/${encodeSiteUrl(siteUrl)}/searchAnalytics/query`, {
2443
2564
  method: "POST",
2444
2565
  body,
2445
2566
  signal: opts?.signal
@@ -2448,25 +2569,40 @@ function googleSearchConsole(auth, options = {}) {
2448
2569
  async *query(siteUrl, builder, opts) {
2449
2570
  const state = builder.getState();
2450
2571
  const body = resolveToBody(state);
2451
- const rowLimit = body.rowLimit || 25e3;
2572
+ const totalCap = body.rowLimit;
2573
+ const pageSize = Math.min(totalCap ?? 25e3, 25e3);
2452
2574
  let startRow = body.startRow || 0;
2575
+ let yielded = 0;
2576
+ let metadata;
2577
+ let responseAggregationType;
2453
2578
  while (true) {
2454
2579
  opts?.signal?.throwIfAborted();
2455
- const rows = ((await rawQuery(siteUrl, {
2580
+ const remaining = totalCap ? totalCap - yielded : pageSize;
2581
+ if (remaining <= 0) break;
2582
+ const rowLimit = Math.min(pageSize, remaining);
2583
+ const response = await rawQuery(siteUrl, {
2456
2584
  ...body,
2457
2585
  startRow,
2458
2586
  rowLimit
2459
- }, opts)).rows || []).map((row) => {
2587
+ }, opts);
2588
+ if (response.metadata) metadata = response.metadata;
2589
+ if (response.responseAggregationType) responseAggregationType = response.responseAggregationType;
2590
+ const rows = (response.rows || []).map((row) => {
2460
2591
  const result = rowWithMetricDefaults(row);
2461
2592
  state.dimensions.forEach((dim, i) => {
2462
2593
  result[dim] = row.keys?.[i];
2463
2594
  });
2464
2595
  return result;
2465
2596
  });
2597
+ if (rows.length === 0) break;
2466
2598
  yield rows;
2467
- if (rows.length < rowLimit) break;
2599
+ yielded += rows.length;
2468
2600
  startRow += rows.length;
2469
2601
  }
2602
+ return {
2603
+ metadata,
2604
+ responseAggregationType
2605
+ };
2470
2606
  },
2471
2607
  sites: (() => {
2472
2608
  const list = async (opts) => {
@@ -2474,11 +2610,15 @@ function googleSearchConsole(auth, options = {}) {
2474
2610
  };
2475
2611
  return Object.assign(list, {
2476
2612
  list,
2477
- add: (siteUrl, opts) => fetch(`${GSC_API}/webmasters/v3/sites/${encodeURIComponent(siteUrl)}`, {
2478
- method: "PUT",
2479
- signal: opts?.signal
2480
- }),
2481
- delete: (siteUrl, opts) => fetch(`${GSC_API}/webmasters/v3/sites/${encodeURIComponent(siteUrl)}`, {
2613
+ get: (siteUrl, opts) => fetch(`${GSC_API}/webmasters/v3/sites/${encodeSiteUrl(siteUrl)}`, { signal: opts?.signal }),
2614
+ add: (siteUrl, opts) => {
2615
+ assertValidSiteUrl(siteUrl);
2616
+ return fetch(`${GSC_API}/webmasters/v3/sites/${encodeSiteUrl(siteUrl)}`, {
2617
+ method: "PUT",
2618
+ signal: opts?.signal
2619
+ });
2620
+ },
2621
+ delete: (siteUrl, opts) => fetch(`${GSC_API}/webmasters/v3/sites/${encodeSiteUrl(siteUrl)}`, {
2482
2622
  method: "DELETE",
2483
2623
  signal: opts?.signal
2484
2624
  })
@@ -2507,7 +2647,11 @@ function googleSearchConsole(auth, options = {}) {
2507
2647
  },
2508
2648
  inspect: (siteUrl, url, opts) => fetch(`${GSC_API}/v1/urlInspection/index:inspect`, {
2509
2649
  method: "POST",
2510
- body: {
2650
+ body: opts?.languageCode ? {
2651
+ inspectionUrl: url,
2652
+ siteUrl,
2653
+ languageCode: opts.languageCode
2654
+ } : {
2511
2655
  inspectionUrl: url,
2512
2656
  siteUrl
2513
2657
  },
@@ -2515,14 +2659,17 @@ function googleSearchConsole(auth, options = {}) {
2515
2659
  }),
2516
2660
  sitemaps: {
2517
2661
  list: async (siteUrl, opts) => {
2518
- return (await fetch(`${GSC_API}/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/sitemaps`, { signal: opts?.signal })).sitemap || [];
2662
+ return (await fetch(`${GSC_API}/webmasters/v3/sites/${encodeSiteUrl(siteUrl)}/sitemaps`, {
2663
+ signal: opts?.signal,
2664
+ query: opts?.sitemapIndex ? { sitemapIndex: opts.sitemapIndex } : void 0
2665
+ })).sitemap || [];
2519
2666
  },
2520
- get: (siteUrl, feedpath, opts) => fetch(`${GSC_API}/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/sitemaps/${encodeURIComponent(feedpath)}`, { signal: opts?.signal }),
2521
- submit: (siteUrl, feedpath, opts) => fetch(`${GSC_API}/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/sitemaps/${encodeURIComponent(feedpath)}`, {
2667
+ get: (siteUrl, feedpath, opts) => fetch(`${GSC_API}/webmasters/v3/sites/${encodeSiteUrl(siteUrl)}/sitemaps/${encodeURIComponent(feedpath)}`, { signal: opts?.signal }),
2668
+ submit: (siteUrl, feedpath, opts) => fetch(`${GSC_API}/webmasters/v3/sites/${encodeSiteUrl(siteUrl)}/sitemaps/${encodeURIComponent(feedpath)}`, {
2522
2669
  method: "PUT",
2523
2670
  signal: opts?.signal
2524
2671
  }),
2525
- delete: (siteUrl, feedpath, opts) => fetch(`${GSC_API}/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/sitemaps/${encodeURIComponent(feedpath)}`, {
2672
+ delete: (siteUrl, feedpath, opts) => fetch(`${GSC_API}/webmasters/v3/sites/${encodeSiteUrl(siteUrl)}/sitemaps/${encodeURIComponent(feedpath)}`, {
2526
2673
  method: "DELETE",
2527
2674
  signal: opts?.signal
2528
2675
  })
@@ -2623,6 +2770,9 @@ function parseGscSiteUrl(siteUrl) {
2623
2770
  isDomain
2624
2771
  };
2625
2772
  }
2773
+ function normalizeGscSiteUrl(siteUrl) {
2774
+ return siteUrl.replace(/^sc-domain:/, "").replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "").toLowerCase();
2775
+ }
2626
2776
  function normalizeRegistrationTarget(inputUrl) {
2627
2777
  const trimmed = inputUrl.trim();
2628
2778
  if (!trimmed) return null;
@@ -2695,8 +2845,8 @@ function findExactGscProperty(propertyUrl, properties) {
2695
2845
  function formatGscPropertyCandidates(candidates) {
2696
2846
  return candidates.filter((c) => !!c).map((property) => `${property.siteUrl} (${property.permissionLevel})`).join(", ");
2697
2847
  }
2698
- const INDEXING_DAILY_LIMIT = 2e3;
2699
- const INDEXING_EFFECTIVE_LIMIT = 1800;
2848
+ const URL_INSPECTION_DAILY_LIMIT = 2e3;
2849
+ const URL_INSPECTION_EFFECTIVE_LIMIT = 1800;
2700
2850
  const GSCDUMP_ONBOARDING_CONTRACT_VERSION = "2026-05-11";
2701
2851
  const GSCDUMP_REQUIRED_ANALYTICS_SCOPE = "https://www.googleapis.com/auth/webmasters.readonly";
2702
2852
  const GSCDUMP_OPTIONAL_INDEXING_SCOPE = "https://www.googleapis.com/auth/indexing";
@@ -2885,4 +3035,4 @@ async function fetchSitemapUrls(sitemapUrl, options = {}) {
2885
3035
  await visit(sitemapUrl, 0);
2886
3036
  return out;
2887
3037
  }
2888
- export { DAYS_PER_RANGE, GSCDUMP_ONBOARDING_CONTRACT_VERSION, GSCDUMP_OPTIONAL_INDEXING_SCOPE, GSCDUMP_REQUIRED_ANALYTICS_SCOPE, GSC_FINALIZED_LAG_DAYS, GSC_FRESHEST_LAG_DAYS, GSC_QUOTAS, GSC_RETENTION_MONTHS, GscApiError, INDEXING_DAILY_LIMIT, INDEXING_EFFECTIVE_LIMIT, INDEXING_ISSUE_FILTERS, INDEXING_ISSUE_LABELS, INDEXING_ISSUE_SEVERITY, MS_PER_DAY, accountNextActions, accountStatuses, addDays, addSite, analyticsNextActions, analyticsStatuses, batchInspectUrls, batchRequestIndexing, canUseUrlInspection, classifyError, countDays, createAuth, createFetch, daysAgo, deleteSite, deleteSitemap, discoverSitemap, exchangeAuthCode, fetchSitemap, fetchSitemapUrls, fetchSitemaps, fetchSites, fetchSitesWithSitemaps, findBestGscProperty, findExactGscProperty, formatErrorForCli, formatGscPropertyCandidates, generateGscDateRange, getBackfillProgress, getDateRange, getFreshestGscDate, getIndexingEligibility, getIndexingMetadata, getLatestGscDate, getNextCheckAfter, getNextCheckPriority, getNextDate, getOldestGscDate, getPendingDates, getPreviousDate, getPstDate, getVerificationToken, getVerifiedSite, googleSearchConsole, grantedScopeList, groupIntoRanges, gscPropertyMatchesTarget, gscdumpApi, hasGscReadScope, hasGscWriteScope, hasIndexingScope, hasOptionalIndexingScope, hasRequiredAnalyticsScope, indexingNextActions, indexingStatuses, inspectUrl, inspectUrlFlat, isPermissionDeniedError, isValidGscDate, isVerifiedGscPermission, isVerifiedGscProperty, lifecycleErrorCodes, lifecycleWebhookEvents, listVerifiedSites, matchGscSite, normalizeRegistrationTarget, parseGoogleError, parseGrantedScopes, parseGscSiteUrl, pickBestGscProperty, progressBar, propertyNextActions, propertyStatuses, querySourceModes, refreshAccessToken, requestIndexing, rethrowAsGscApiError, rowWithMetricDefaults, runSequentialBatch, siteUrlToVerificationSite, sitemapNextActions, sitemapStatuses, storageError, submitSitemap, toIsoDate, unverifySite, verificationMethodsFor, verifySite };
3038
+ export { DAYS_PER_RANGE, GSCDUMP_ONBOARDING_CONTRACT_VERSION, GSCDUMP_OPTIONAL_INDEXING_SCOPE, GSCDUMP_REQUIRED_ANALYTICS_SCOPE, GSC_FINALIZED_LAG_DAYS, GSC_FRESHEST_LAG_DAYS, GSC_QUOTAS, GSC_RETENTION_MONTHS, GscApiError, INDEXING_ISSUE_FILTERS, INDEXING_ISSUE_LABELS, INDEXING_ISSUE_SEVERITY, MS_PER_DAY, URL_INSPECTION_DAILY_LIMIT, URL_INSPECTION_EFFECTIVE_LIMIT, accountNextActions, accountStatuses, addDays, addSite, analyticsNextActions, analyticsStatuses, batchInspectUrls, batchRequestIndexing, canUseUrlInspection, classifyError, countDays, createAuth, createFetch, daysAgo, deleteSite, deleteSitemap, discoverSitemap, exchangeAuthCode, fetchSitemap, fetchSitemapUrls, fetchSitemaps, fetchSites, fetchSitesWithSitemaps, findBestGscProperty, findExactGscProperty, formatErrorForCli, formatGscPropertyCandidates, generateGscDateRange, getBackfillProgress, getDateRange, getFreshestGscDate, getIndexingEligibility, getIndexingMetadata, getLatestGscDate, getNextCheckAfter, getNextCheckPriority, getNextDate, getOldestGscDate, getPendingDates, getPreviousDate, getPstDate, getVerificationToken, getVerifiedSite, googleSearchConsole, grantedScopeList, groupIntoRanges, gscPropertyMatchesTarget, gscdumpApi, hasGscReadScope, hasGscWriteScope, hasIndexingScope, hasOptionalIndexingScope, hasRequiredAnalyticsScope, indexingNextActions, indexingStatuses, inspectUrl, inspectUrlFlat, isPermissionDeniedError, isValidGscDate, isVerifiedGscPermission, isVerifiedGscProperty, lifecycleErrorCodes, lifecycleWebhookEvents, listVerifiedSites, matchGscSite, normalizeGscSiteUrl, normalizeRegistrationTarget, parseGoogleError, parseGrantedScopes, parseGscSiteUrl, pickBestGscProperty, progressBar, propertyNextActions, propertyStatuses, querySourceModes, refreshAccessToken, requestIndexing, rethrowAsGscApiError, rowWithMetricDefaults, runSequentialBatch, siteUrlToVerificationSite, sitemapNextActions, sitemapStatuses, storageError, submitSitemap, toIsoDate, unverifySite, verificationMethodsFor, verifySite };
@@ -1,6 +1,7 @@
1
1
  import _dayjs, { Dayjs } from "dayjs";
2
2
  type TableName = 'pages' | 'keywords' | 'countries' | 'devices' | 'page_keywords' | 'search_appearance';
3
- type GscSearchAnalyticsDimension = 'page' | 'query' | 'country' | 'device' | 'date' | 'searchAppearance';
3
+ type GscSearchAnalyticsDimension = 'page' | 'query' | 'country' | 'device' | 'date' | 'hour' | 'searchAppearance';
4
+ type GscDataState = 'final' | 'all' | 'hourly_all';
4
5
  type GscSearchAnalyticsFilterOperator = 'equals' | 'notEquals' | 'contains' | 'notContains' | 'includingRegex' | 'excludingRegex';
5
6
  interface GscSearchAnalyticsFilter {
6
7
  dimension: GscSearchAnalyticsDimension;
@@ -12,6 +13,7 @@ interface GscSearchAnalyticsFilterGroup {
12
13
  filters: GscSearchAnalyticsFilter[];
13
14
  }
14
15
  type GscSearchType = 'web' | 'image' | 'video' | 'news' | 'discover' | 'googleNews';
16
+ type GscAggregationType = 'auto' | 'byPage' | 'byProperty' | 'byNewsShowcasePanel';
15
17
  interface GscSearchAnalyticsRequest {
16
18
  startDate: string;
17
19
  endDate: string;
@@ -19,7 +21,10 @@ interface GscSearchAnalyticsRequest {
19
21
  dimensionFilterGroups?: GscSearchAnalyticsFilterGroup[];
20
22
  rowLimit?: number;
21
23
  startRow?: number;
22
- searchType?: GscSearchType;
24
+ /** GSC search corpus. Maps to wire field `type` (the API still accepts the deprecated `searchType` alias). */
25
+ type?: GscSearchType;
26
+ dataState?: GscDataState;
27
+ aggregationType?: GscAggregationType;
23
28
  }
24
29
  declare const _default: {
25
30
  name: string;
@@ -52,6 +57,8 @@ interface DimensionValueMap {
52
57
  device: Device;
53
58
  searchAppearance: string;
54
59
  date: string;
60
+ /** Hour bucket — ISO-8601 with PT offset, e.g. `2025-07-14T13:00:00-07:00`. Use with `dataState: 'hourly_all'`. */
61
+ hour: string;
55
62
  }
56
63
  type Dimension = keyof DimensionValueMap;
57
64
  interface QueryParamValueMap {
@@ -110,6 +117,12 @@ interface BuilderState {
110
117
  };
111
118
  rowLimit?: number;
112
119
  startRow?: number;
120
+ /** GSC `dataState`. `'hourly_all'` is required when grouping by `hour`. */
121
+ dataState?: GscDataState;
122
+ /** GSC `aggregationType`. `'byNewsShowcasePanel'` requires `type=discover|googleNews` with NEWS_SHOWCASE searchAppearance. */
123
+ aggregationType?: GscAggregationType;
124
+ /** GSC search corpus. Wins over any `searchType` filter when both are set. */
125
+ searchType?: SearchType;
113
126
  }
114
127
  interface JsonInternalFilter {
115
128
  dimension: string;
@@ -136,6 +149,10 @@ interface GSCQueryBuilder<D extends Dimension[] = [], C = object> {
136
149
  orderBy: (col: OrderableColumn, dir: 'asc' | 'desc') => GSCQueryBuilder<D, C>;
137
150
  limit: (n: number) => GSCQueryBuilder<D, C>;
138
151
  offset: (n: number) => GSCQueryBuilder<D, C>;
152
+ dataState: (state: GscDataState) => GSCQueryBuilder<D, C>;
153
+ aggregationType: (type: GscAggregationType) => GSCQueryBuilder<D, C>;
154
+ /** GSC search corpus (`type` on the wire). Equivalent to filtering by `searchType`. */
155
+ type: (t: GscSearchType) => GSCQueryBuilder<D, C>;
139
156
  toBody: () => GscSearchAnalyticsRequest;
140
157
  getState: () => BuilderState;
141
158
  }
@@ -147,6 +164,7 @@ declare const device: Column<"device">;
147
164
  declare const country: Column<"country">;
148
165
  declare const searchAppearance: Column<"searchAppearance">;
149
166
  declare const date: Column<"date">;
167
+ declare const hour: Column<"hour">;
150
168
  declare const clicks: MetricColumn<"clicks">;
151
169
  declare const impressions: MetricColumn<"impressions">;
152
170
  declare const ctr: MetricColumn<"ctr">;
@@ -164,15 +182,15 @@ declare function and<F extends Filter<any>[]>(...filters: F): Filter<MergeConstr
164
182
  declare function or<F extends Filter<any>[]>(...filters: F): Filter<object>;
165
183
  declare function not<F extends Filter<any>>(filter: F): Filter<object>;
166
184
  declare function gte<M extends Metric>(column: MetricColumn<M>, value: number): Filter<object>;
167
- declare function gte<D extends Dimension>(column: Column<D>, value: DimensionValueMap[D]): Filter<object>;
185
+ declare function gte(column: Column<'date'>, value: string): Filter<object>;
168
186
  declare function gt<M extends Metric>(column: MetricColumn<M>, value: number): Filter<object>;
169
- declare function gt<D extends Dimension>(column: Column<D>, value: DimensionValueMap[D]): Filter<object>;
187
+ declare function gt(column: Column<'date'>, value: string): Filter<object>;
170
188
  declare function lte<M extends Metric>(column: MetricColumn<M>, value: number): Filter<object>;
171
- declare function lte<D extends Dimension>(column: Column<D>, value: DimensionValueMap[D]): Filter<object>;
189
+ declare function lte(column: Column<'date'>, value: string): Filter<object>;
172
190
  declare function lt<M extends Metric>(column: MetricColumn<M>, value: number): Filter<object>;
173
- declare function lt<D extends Dimension>(column: Column<D>, value: DimensionValueMap[D]): Filter<object>;
191
+ declare function lt(column: Column<'date'>, value: string): Filter<object>;
174
192
  declare function between<M extends Metric>(column: MetricColumn<M>, start: number, end: number): Filter<object>;
175
- declare function between<D extends Dimension>(column: Column<D>, start: DimensionValueMap[D], end: DimensionValueMap[D]): Filter<object>;
193
+ declare function between(column: Column<'date'>, start: string, end: string): Filter<object>;
176
194
  declare function topLevel(column: Column<'page'>): Filter<object>;
177
195
  type LogicalDataset = TableName;
178
196
  type ComparisonFilter = 'new' | 'lost' | 'improving' | 'declining';
@@ -239,6 +257,7 @@ declare function buildLogicalComparisonPlan(current: BuilderState, previous: Bui
239
257
  declare function isJsonFilter(value: unknown): value is JsonFilter;
240
258
  declare function parseJsonFilter(json: JsonFilter): Filter<any>;
241
259
  declare function normalizeFilter(input?: FilterInput): Filter<any> | undefined;
260
+ declare function normalizeBuilderState(state: unknown): BuilderState;
242
261
  declare function extractDateRange(input?: FilterInput): {
243
262
  startDate?: string;
244
263
  endDate?: string;
@@ -257,4 +276,4 @@ declare function currentPstDate(): string;
257
276
  declare function dayjsPst(): Dayjs;
258
277
  declare function today(): string;
259
278
  declare function daysAgo(n: number): string;
260
- export { type BuilderState, type Column, type ComparisonFilter, Countries, type Country, type Device, Devices, type Dimension, type DimensionValueMap, type Filter, type FilterInput, type GSCQueryBuilder, type GSCResult, type GSCRow, type InternalFilter, type JsonFilter, type JsonInternalFilter, type LogicalComparisonPlan, type LogicalDataset, type LogicalDimensionFilter, type LogicalMetricFilter, type LogicalQueryPlan, type Metric, type MetricColumn, type PlannerCapabilities, type QueryParam, type QueryParamName, type QueryParamValueMap, type SearchType, SearchTypes, UnsupportedLogicalCapabilityError, and, between, buildLogicalComparisonPlan, buildLogicalPlan, clicks, contains, country, ctr, currentPstDate, date, dayjs, dayjsPst, daysAgo, device, eq, extractDateRange, extractMetricFilters, extractSearchType, extractSpecialOperatorFilters, gsc, gt, gte, impressions, inArray, isJsonFilter, like, lt, lte, ne, normalizeFilter, not, notRegex, or, page, parseJsonFilter, position, query, queryCanonical, regex, searchAppearance, searchType, today, topLevel };
279
+ export { type BuilderState, type Column, type ComparisonFilter, Countries, type Country, type Device, Devices, type Dimension, type DimensionValueMap, type Filter, type FilterInput, type GSCQueryBuilder, type GSCResult, type GSCRow, type InternalFilter, type JsonFilter, type JsonInternalFilter, type LogicalComparisonPlan, type LogicalDataset, type LogicalDimensionFilter, type LogicalMetricFilter, type LogicalQueryPlan, type Metric, type MetricColumn, type PlannerCapabilities, type QueryParam, type QueryParamName, type QueryParamValueMap, type SearchType, SearchTypes, UnsupportedLogicalCapabilityError, and, between, buildLogicalComparisonPlan, buildLogicalPlan, clicks, contains, country, ctr, currentPstDate, date, dayjs, dayjsPst, daysAgo, device, eq, extractDateRange, extractMetricFilters, extractSearchType, extractSpecialOperatorFilters, gsc, gt, gte, hour, impressions, inArray, isJsonFilter, like, lt, lte, ne, normalizeBuilderState, normalizeFilter, not, notRegex, or, page, parseJsonFilter, position, query, queryCanonical, regex, searchAppearance, searchType, today, topLevel };
@@ -1631,6 +1631,20 @@ function normalizeFilter(input) {
1631
1631
  if (isWireFilter(input)) return convertWireGroup(input) ?? void 0;
1632
1632
  return input;
1633
1633
  }
1634
+ function normalizeBuilderState(state) {
1635
+ if (!state || typeof state !== "object") throw new Error("Invalid state");
1636
+ const s = state;
1637
+ return {
1638
+ dimensions: s.dimensions,
1639
+ metrics: s.metrics,
1640
+ filter: normalizeFilter(s.filter),
1641
+ orderBy: s.orderBy,
1642
+ rowLimit: s.rowLimit,
1643
+ startRow: s.startRow,
1644
+ dataState: s.dataState,
1645
+ aggregationType: s.aggregationType
1646
+ };
1647
+ }
1634
1648
  function extractSpecialFilters(filter) {
1635
1649
  if (!filter) return {};
1636
1650
  let startDate;
@@ -1662,9 +1676,9 @@ function extractSpecialFilters(filter) {
1662
1676
  else otherFilters.push(f);
1663
1677
  if (filter._nestedGroups) for (const nested of filter._nestedGroups) {
1664
1678
  const extracted = extractSpecialFilters(nested);
1665
- if (extracted.startDate) startDate = extracted.startDate;
1666
- if (extracted.endDate) endDate = extracted.endDate;
1667
- if (extracted.searchType) searchType = extracted.searchType;
1679
+ if (!startDate && extracted.startDate) startDate = extracted.startDate;
1680
+ if (!endDate && extracted.endDate) endDate = extracted.endDate;
1681
+ if (!searchType && extracted.searchType) searchType = extracted.searchType;
1668
1682
  if (extracted.dimensionFilter) cleanedNestedGroups.push(extracted.dimensionFilter);
1669
1683
  }
1670
1684
  const dimensionFilter = otherFilters.length > 0 || cleanedNestedGroups.length > 0 ? {
@@ -1702,6 +1716,7 @@ function extractSpecialOperatorFilters(input) {
1702
1716
  }
1703
1717
  function extractSearchType(state) {
1704
1718
  if (!state) return void 0;
1719
+ if (state.searchType && KNOWN_SEARCH_TYPES.has(state.searchType)) return state.searchType;
1705
1720
  const filter = state.filter;
1706
1721
  if (!filter || typeof filter !== "object") return void 0;
1707
1722
  const raw = filter.searchType;
@@ -1716,11 +1731,40 @@ function resolveToBody(state) {
1716
1731
  startDate,
1717
1732
  endDate
1718
1733
  };
1719
- if (searchType) body.searchType = searchType;
1720
- if (state.rowLimit) body.rowLimit = state.rowLimit;
1721
- if (state.startRow) body.startRow = state.startRow;
1734
+ const resolvedType = state.searchType ?? searchType;
1735
+ if (resolvedType) body.type = resolvedType;
1736
+ if (state.rowLimit !== void 0) {
1737
+ if (!Number.isInteger(state.rowLimit) || state.rowLimit < 1) throw new Error(`rowLimit must be a positive integer, got ${state.rowLimit}`);
1738
+ body.rowLimit = state.rowLimit;
1739
+ }
1740
+ if (state.startRow !== void 0) {
1741
+ if (!Number.isInteger(state.startRow) || state.startRow < 0) throw new Error(`startRow must be a non-negative integer, got ${state.startRow}`);
1742
+ if (state.startRow > 0) body.startRow = state.startRow;
1743
+ }
1744
+ const hasHour = state.dimensions?.includes("hour");
1745
+ if (hasHour && state.dataState !== "hourly_all") throw new Error("hour dimension requires dataState: \"hourly_all\"");
1746
+ if (state.dataState === "hourly_all" && !hasHour) throw new Error("dataState: \"hourly_all\" requires grouping by hour dimension");
1747
+ if (state.dataState) body.dataState = state.dataState;
1722
1748
  const filterGroups = resolveFilter(dimensionFilter);
1723
1749
  if (filterGroups.length > 0) body.dimensionFilterGroups = filterGroups;
1750
+ if (state.aggregationType) {
1751
+ const groupsByPage = (body.dimensions ?? []).includes("page");
1752
+ const apiLeafFilters = filterGroups.flatMap((g) => g.filters ?? []);
1753
+ const filtersByPage = apiLeafFilters.some((f) => f.dimension === "page");
1754
+ if (state.aggregationType === "byProperty") {
1755
+ if (body.type === "discover" || body.type === "googleNews") throw new Error("aggregationType: \"byProperty\" is not supported for type \"discover\" or \"googleNews\"");
1756
+ if (groupsByPage || filtersByPage) throw new Error("aggregationType: \"byProperty\" is not allowed when grouping or filtering by page");
1757
+ }
1758
+ if (state.aggregationType === "byNewsShowcasePanel") {
1759
+ if (body.type !== "discover" && body.type !== "googleNews") throw new Error("aggregationType: \"byNewsShowcasePanel\" requires type \"discover\" or \"googleNews\"");
1760
+ if (groupsByPage || filtersByPage) throw new Error("aggregationType: \"byNewsShowcasePanel\" is not allowed when grouping or filtering by page");
1761
+ const saFilters = apiLeafFilters.filter((f) => f.dimension === "searchAppearance");
1762
+ const hasNewsShowcase = saFilters.some((f) => f.operator === "equals" && f.expression === "NEWS_SHOWCASE");
1763
+ const hasOther = saFilters.some((f) => !(f.operator === "equals" && f.expression === "NEWS_SHOWCASE"));
1764
+ if (!hasNewsShowcase || hasOther) throw new Error("aggregationType: \"byNewsShowcasePanel\" requires a searchAppearance equals \"NEWS_SHOWCASE\" filter and no other searchAppearance filter");
1765
+ }
1766
+ body.aggregationType = state.aggregationType;
1767
+ }
1724
1768
  return body;
1725
1769
  }
1726
1770
  function isApiFilter(f) {
@@ -1805,6 +1849,24 @@ function createBuilder(state) {
1805
1849
  startRow: n
1806
1850
  });
1807
1851
  },
1852
+ dataState(s) {
1853
+ return createBuilder({
1854
+ ...state,
1855
+ dataState: s
1856
+ });
1857
+ },
1858
+ aggregationType(t) {
1859
+ return createBuilder({
1860
+ ...state,
1861
+ aggregationType: t
1862
+ });
1863
+ },
1864
+ type(t) {
1865
+ return createBuilder({
1866
+ ...state,
1867
+ searchType: t
1868
+ });
1869
+ },
1808
1870
  toBody() {
1809
1871
  return resolveToBody(state);
1810
1872
  },
@@ -1830,6 +1892,7 @@ const device = createColumn("device");
1830
1892
  const country = createColumn("country");
1831
1893
  const searchAppearance = createColumn("searchAppearance");
1832
1894
  const date = createColumn("date");
1895
+ const hour = createColumn("hour");
1833
1896
  const clicks = createMetricColumn("clicks");
1834
1897
  const impressions = createMetricColumn("impressions");
1835
1898
  const ctr = createMetricColumn("ctr");
@@ -1857,6 +1920,7 @@ function ne(column, value) {
1857
1920
  return leafFilter(column.dimension, "notEquals", String(value));
1858
1921
  }
1859
1922
  function inArray(column, values) {
1923
+ if (values.length === 0) throw new Error(`inArray(${column.dimension}, []) requires at least one value`);
1860
1924
  return {
1861
1925
  _constraints: {},
1862
1926
  _filters: values.map((v) => ({
@@ -1871,7 +1935,9 @@ function contains(column, pattern) {
1871
1935
  return leafFilter(column.dimension, "contains", pattern);
1872
1936
  }
1873
1937
  function like(column, pattern) {
1874
- return leafFilter(column.dimension, "contains", pattern.replace(/%/g, ""));
1938
+ if (!/[%_]/.test(pattern)) return leafFilter(column.dimension, "contains", pattern);
1939
+ const regex = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".");
1940
+ return leafFilter(column.dimension, "includingRegex", regex);
1875
1941
  }
1876
1942
  function regex(column, pattern) {
1877
1943
  return leafFilter(column.dimension, "includingRegex", typeof pattern === "string" ? pattern : pattern.source);
@@ -1895,6 +1961,14 @@ function and(...filters) {
1895
1961
  };
1896
1962
  }
1897
1963
  function or(...filters) {
1964
+ for (const f of filters) {
1965
+ if (f._groupType === "and" && f._filters.length > 1) throw new Error("or() cannot contain a multi-leaf AND group: GSC filter groups do not nest. Restructure as flat OR or split into multiple queries.");
1966
+ if (f._nestedGroups && f._nestedGroups.length > 0) throw new Error("or() cannot contain nested filter groups: GSC filter groups do not nest.");
1967
+ for (const leaf of f._filters) {
1968
+ if (leaf.dimension === "date") throw new Error("or() cannot contain a date filter: GSC date range is a top-level AND-applied request field, not a filter. Apply the date range outside the or() group.");
1969
+ if (leaf.dimension === "searchType") throw new Error("or() cannot contain a searchType filter: GSC search type is a top-level AND-applied request field. Use .type() outside the or() group.");
1970
+ }
1971
+ }
1898
1972
  return {
1899
1973
  _constraints: {},
1900
1974
  _filters: filters.flatMap((f) => f._filters),
@@ -1902,23 +1976,30 @@ function or(...filters) {
1902
1976
  };
1903
1977
  }
1904
1978
  function not(filter) {
1905
- return {
1906
- _constraints: {},
1907
- _filters: filter._filters.filter((f) => !DATE_OPERATORS.includes(f.operator)).map((f) => ({
1979
+ const inverted = [];
1980
+ for (const f of filter._filters) {
1981
+ if (DATE_OPERATORS.includes(f.operator)) throw new Error(`not() cannot invert date operator "${f.operator}": GSC has no negated date filter.`);
1982
+ if (!(f.operator in INVERSIONS)) throw new Error(`not() cannot invert operator "${f.operator}".`);
1983
+ inverted.push({
1908
1984
  ...f,
1909
1985
  operator: invertOperator(f.operator)
1910
- }))
1986
+ });
1987
+ }
1988
+ return {
1989
+ _constraints: {},
1990
+ _filters: inverted
1911
1991
  };
1912
1992
  }
1993
+ const INVERSIONS = {
1994
+ equals: "notEquals",
1995
+ notEquals: "equals",
1996
+ contains: "notContains",
1997
+ notContains: "contains",
1998
+ includingRegex: "excludingRegex",
1999
+ excludingRegex: "includingRegex"
2000
+ };
1913
2001
  function invertOperator(op) {
1914
- return {
1915
- equals: "notEquals",
1916
- notEquals: "equals",
1917
- contains: "notContains",
1918
- notContains: "contains",
1919
- includingRegex: "excludingRegex",
1920
- excludingRegex: "includingRegex"
1921
- }[op];
2002
+ return INVERSIONS[op];
1922
2003
  }
1923
2004
  function gte(column, value) {
1924
2005
  return metricOrDimFilter(column, "metricGte", "gte", String(value));
@@ -2085,4 +2166,4 @@ function today() {
2085
2166
  function daysAgo(n) {
2086
2167
  return dayjsPst().subtract(n, "day").format("YYYY-MM-DD");
2087
2168
  }
2088
- export { Countries, Devices, SearchTypes, UnsupportedLogicalCapabilityError, and, between, buildLogicalComparisonPlan, buildLogicalPlan, clicks, contains, country, ctr, currentPstDate, date, dayjs, dayjsPst, daysAgo, device, eq, extractDateRange, extractMetricFilters, extractSearchType, extractSpecialOperatorFilters, gsc, gt, gte, impressions, inArray, isJsonFilter, like, lt, lte, ne, normalizeFilter, not, notRegex, or, page, parseJsonFilter, position, query, queryCanonical, regex, searchAppearance, searchType, today, topLevel };
2169
+ export { Countries, Devices, SearchTypes, UnsupportedLogicalCapabilityError, and, between, buildLogicalComparisonPlan, buildLogicalPlan, clicks, contains, country, ctr, currentPstDate, date, dayjs, dayjsPst, daysAgo, device, eq, extractDateRange, extractMetricFilters, extractSearchType, extractSpecialOperatorFilters, gsc, gt, gte, hour, impressions, inArray, isJsonFilter, like, lt, lte, ne, normalizeBuilderState, normalizeFilter, not, notRegex, or, page, parseJsonFilter, position, query, queryCanonical, regex, searchAppearance, searchType, today, topLevel };
@@ -1,4 +1,6 @@
1
1
  type TableName = 'pages' | 'keywords' | 'countries' | 'devices' | 'page_keywords' | 'search_appearance';
2
+ type GscDataState = 'final' | 'all' | 'hourly_all';
3
+ type GscAggregationType = 'auto' | 'byPage' | 'byProperty' | 'byNewsShowcasePanel';
2
4
  declare const _default: {
3
5
  name: string;
4
6
  'alpha-2': string;
@@ -30,6 +32,8 @@ interface DimensionValueMap {
30
32
  device: Device;
31
33
  searchAppearance: string;
32
34
  date: string;
35
+ /** Hour bucket — ISO-8601 with PT offset, e.g. `2025-07-14T13:00:00-07:00`. Use with `dataState: 'hourly_all'`. */
36
+ hour: string;
33
37
  }
34
38
  type Dimension = keyof DimensionValueMap;
35
39
  interface QueryParamValueMap {
@@ -65,6 +69,12 @@ interface BuilderState {
65
69
  };
66
70
  rowLimit?: number;
67
71
  startRow?: number;
72
+ /** GSC `dataState`. `'hourly_all'` is required when grouping by `hour`. */
73
+ dataState?: GscDataState;
74
+ /** GSC `aggregationType`. `'byNewsShowcasePanel'` requires `type=discover|googleNews` with NEWS_SHOWCASE searchAppearance. */
75
+ aggregationType?: GscAggregationType;
76
+ /** GSC search corpus. Wins over any `searchType` filter when both are set. */
77
+ searchType?: SearchType;
68
78
  }
69
79
  type LogicalDataset = TableName;
70
80
  type ComparisonFilter = 'new' | 'lost' | 'improving' | 'declining';
@@ -1627,9 +1627,9 @@ function extractSpecialFilters(filter) {
1627
1627
  else otherFilters.push(f);
1628
1628
  if (filter._nestedGroups) for (const nested of filter._nestedGroups) {
1629
1629
  const extracted = extractSpecialFilters(nested);
1630
- if (extracted.startDate) startDate = extracted.startDate;
1631
- if (extracted.endDate) endDate = extracted.endDate;
1632
- if (extracted.searchType) searchType = extracted.searchType;
1630
+ if (!startDate && extracted.startDate) startDate = extracted.startDate;
1631
+ if (!endDate && extracted.endDate) endDate = extracted.endDate;
1632
+ if (!searchType && extracted.searchType) searchType = extracted.searchType;
1633
1633
  if (extracted.dimensionFilter) cleanedNestedGroups.push(extracted.dimensionFilter);
1634
1634
  }
1635
1635
  const dimensionFilter = otherFilters.length > 0 || cleanedNestedGroups.length > 0 ? {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gscdump",
3
3
  "type": "module",
4
- "version": "0.17.4",
4
+ "version": "0.18.0",
5
5
  "description": "Google Search Console API wrapper with typed query builder, streaming pagination, and SEO analysis functions",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -108,7 +108,7 @@
108
108
  "defu": "^6.1.7",
109
109
  "ofetch": "^1.5.1",
110
110
  "ufo": "^1.6.4",
111
- "@gscdump/contracts": "0.17.4"
111
+ "@gscdump/contracts": "0.18.0"
112
112
  },
113
113
  "devDependencies": {
114
114
  "@googleapis/indexing": "^6.0.1",