gscdump 1.3.2 → 1.4.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 (71) hide show
  1. package/dist/api/batch.d.mts +12 -0
  2. package/dist/api/batch.mjs +29 -0
  3. package/dist/api/indexing.d.mts +37 -0
  4. package/dist/api/indexing.mjs +25 -0
  5. package/dist/api/inspection.d.mts +89 -0
  6. package/dist/api/inspection.mjs +128 -0
  7. package/dist/api/oauth.d.mts +48 -0
  8. package/dist/api/oauth.mjs +154 -0
  9. package/dist/api/sites.d.mts +45 -0
  10. package/dist/api/sites.mjs +34 -0
  11. package/dist/api/verification.d.mts +43 -0
  12. package/dist/api/verification.mjs +89 -0
  13. package/dist/contracts.d.mts +46 -1
  14. package/dist/core/cli-format.mjs +9 -0
  15. package/dist/core/client.d.mts +122 -0
  16. package/dist/core/client.mjs +246 -0
  17. package/dist/core/errors.d.mts +66 -0
  18. package/dist/core/errors.mjs +207 -0
  19. package/dist/core/indexing-issues.d.mts +60 -0
  20. package/dist/core/indexing-issues.mjs +139 -0
  21. package/dist/core/property.d.mts +41 -0
  22. package/dist/core/property.mjs +74 -0
  23. package/dist/core/quota.d.mts +2 -0
  24. package/dist/core/quota.mjs +2 -0
  25. package/dist/core/result.d.mts +19 -1
  26. package/dist/core/scope-values.d.mts +7 -0
  27. package/dist/core/scope-values.mjs +15 -0
  28. package/dist/core/scopes.d.mts +5 -0
  29. package/dist/core/scopes.mjs +11 -0
  30. package/dist/core/site-url.d.mts +25 -0
  31. package/dist/core/site-url.mjs +30 -0
  32. package/dist/core/types.d.mts +201 -0
  33. package/dist/core/types.mjs +1 -0
  34. package/dist/dates.d.mts +2 -2
  35. package/dist/dates.mjs +2 -2
  36. package/dist/index.d.mts +20 -814
  37. package/dist/index.mjs +19 -1243
  38. package/dist/onboarding.d.mts +2 -0
  39. package/dist/onboarding.mjs +2 -0
  40. package/dist/{_chunks → query}/builder.d.mts +2 -2
  41. package/dist/query/builder.mjs +86 -0
  42. package/dist/query/columns.d.mts +15 -0
  43. package/dist/query/columns.mjs +23 -0
  44. package/dist/query/constants.d.mts +19 -0
  45. package/dist/query/constants.mjs +16 -0
  46. package/dist/query/errors.d.mts +85 -0
  47. package/dist/query/errors.mjs +141 -0
  48. package/dist/query/index.d.mts +9 -75
  49. package/dist/query/index.mjs +7 -230
  50. package/dist/query/operator-meta.mjs +41 -0
  51. package/dist/query/operators.d.mts +24 -0
  52. package/dist/query/operators.mjs +124 -0
  53. package/dist/query/plan.d.mts +92 -2
  54. package/dist/query/plan.mjs +3 -1
  55. package/dist/query/resolver.d.mts +40 -0
  56. package/dist/query/resolver.mjs +243 -0
  57. package/dist/{_chunks → query}/types.d.mts +3 -25
  58. package/dist/query/utils/countries.d.mts +7 -0
  59. package/dist/{_chunks/resolver.mjs → query/utils/countries.mjs} +1 -434
  60. package/dist/{_chunks → query/utils}/dayjs.mjs +2 -4
  61. package/dist/sitemap.d.mts +26 -0
  62. package/dist/sitemap.mjs +70 -0
  63. package/dist/url.d.mts +9 -0
  64. package/dist/url.mjs +6 -0
  65. package/package.json +2 -3
  66. package/dist/_chunks/contracts.d.mts +0 -47
  67. package/dist/_chunks/plan.d.mts +0 -175
  68. package/dist/_chunks/result.d.mts +0 -20
  69. /package/dist/{_chunks → core}/gsc-dates.d.mts +0 -0
  70. /package/dist/{_chunks → core}/gsc-dates.mjs +0 -0
  71. /package/dist/{_chunks → query/utils}/dayjs.d.mts +0 -0
@@ -0,0 +1,207 @@
1
+ function pickField(error, paths, is) {
2
+ if (!error || typeof error !== "object") return void 0;
3
+ for (const path of paths) {
4
+ let current = error;
5
+ for (const key of path) {
6
+ if (!current || typeof current !== "object") {
7
+ current = void 0;
8
+ break;
9
+ }
10
+ current = current[key];
11
+ }
12
+ if (is(current)) return current;
13
+ }
14
+ }
15
+ const isNumber = (v) => typeof v === "number";
16
+ const isString = (v) => typeof v === "string";
17
+ function extractStatus(error) {
18
+ return pickField(error, [
19
+ ["statusCode"],
20
+ ["status"],
21
+ ["response", "status"],
22
+ ["code"]
23
+ ], isNumber);
24
+ }
25
+ function extractMessage(error) {
26
+ if (!error) return "Unknown error";
27
+ if (typeof error === "string") return error;
28
+ if (error instanceof Error) return error.message;
29
+ if (typeof error !== "object") return String(error);
30
+ return pickField(error, [
31
+ [
32
+ "data",
33
+ "error",
34
+ "message"
35
+ ],
36
+ ["message"],
37
+ ["statusMessage"]
38
+ ], isString) ?? String(error);
39
+ }
40
+ function extractRetryAfter(error) {
41
+ const raw = pickField(error, [
42
+ ["headers", "retry-after"],
43
+ ["headers", "Retry-After"],
44
+ [
45
+ "response",
46
+ "headers",
47
+ "retry-after"
48
+ ],
49
+ [
50
+ "response",
51
+ "headers",
52
+ "Retry-After"
53
+ ]
54
+ ], (v) => typeof v === "number" || typeof v === "string");
55
+ if (typeof raw === "number") return raw;
56
+ if (typeof raw === "string") {
57
+ const seconds = Number.parseInt(raw, 10);
58
+ return Number.isNaN(seconds) ? void 0 : seconds;
59
+ }
60
+ }
61
+ const QUOTA_MESSAGE_RE = /quota|rate\s*limit/i;
62
+ const QUOTA_REASONS = /* @__PURE__ */ new Set([
63
+ "dailyLimitExceeded",
64
+ "dailyLimitExceededUnreg",
65
+ "rateLimitExceeded",
66
+ "rateLimitExceededUnreg",
67
+ "userRateLimitExceeded",
68
+ "userRateLimitExceededUnreg",
69
+ "quotaExceeded",
70
+ "concurrentLimitExceeded",
71
+ "variableTermLimitExceeded",
72
+ "variableTermExpiredDailyExceeded",
73
+ "servingLimitExceeded",
74
+ "responseTooLarge",
75
+ "limitExceeded",
76
+ "batchSizeTooLarge",
77
+ "RATE_LIMIT_EXCEEDED",
78
+ "RESOURCE_EXHAUSTED"
79
+ ]);
80
+ function extractReason(cause) {
81
+ const data = pickField(cause, [["data"]], (_v) => true);
82
+ if (data) {
83
+ const errorInfo = pickField(data, [["error", "details"]], (v) => Array.isArray(v));
84
+ if (errorInfo) {
85
+ const reason = errorInfo.find((d) => typeof d["@type"] === "string" && d["@type"].includes("ErrorInfo"))?.reason;
86
+ if (typeof reason === "string") return reason;
87
+ }
88
+ const directErrors = pickField(data, [["error", "errors"]], (v) => Array.isArray(v));
89
+ if (directErrors) {
90
+ const reason = directErrors[0]?.reason;
91
+ if (typeof reason === "string") return reason;
92
+ }
93
+ }
94
+ }
95
+ function isQuotaCondition(cause, message) {
96
+ const reason = extractReason(cause);
97
+ if (reason && QUOTA_REASONS.has(reason)) return true;
98
+ return QUOTA_MESSAGE_RE.test(message);
99
+ }
100
+ function classifyError(cause) {
101
+ const status = extractStatus(cause);
102
+ const message = extractMessage(cause);
103
+ if (status === 401) return {
104
+ kind: "auth-expired",
105
+ message,
106
+ cause
107
+ };
108
+ if (status === 429) return {
109
+ kind: "rate-limited",
110
+ message,
111
+ retryAfter: extractRetryAfter(cause),
112
+ cause
113
+ };
114
+ if (status === 403) {
115
+ if (isQuotaCondition(cause, message)) return {
116
+ kind: "rate-limited",
117
+ message,
118
+ retryAfter: extractRetryAfter(cause),
119
+ cause
120
+ };
121
+ return {
122
+ kind: "auth-expired",
123
+ message,
124
+ cause
125
+ };
126
+ }
127
+ if (status === 404 || status === 410) return {
128
+ kind: "not-found",
129
+ message,
130
+ cause
131
+ };
132
+ if (status === 400 || status === 402 || status === 409 || status === 413 || status === 422) return {
133
+ kind: "validation",
134
+ message,
135
+ cause
136
+ };
137
+ return {
138
+ kind: "transport",
139
+ message,
140
+ status,
141
+ cause
142
+ };
143
+ }
144
+ function gscErrorToException(error) {
145
+ const exception = new Error(error.message);
146
+ if (error.cause !== void 0) exception.cause = error.cause;
147
+ exception.gscError = error;
148
+ return exception;
149
+ }
150
+ function parseGoogleError(text, httpStatus) {
151
+ let parsed = null;
152
+ try {
153
+ parsed = JSON.parse(text);
154
+ } catch (error) {
155
+ if (!(error instanceof SyntaxError)) throw error;
156
+ }
157
+ if (!parsed || !("error" in parsed)) return {
158
+ code: httpStatus ?? 500,
159
+ message: text || "Unknown Google API error"
160
+ };
161
+ if (typeof parsed.error === "string") {
162
+ const oauth = parsed;
163
+ return {
164
+ code: httpStatus ?? 400,
165
+ message: oauth.error_description || oauth.error,
166
+ reason: oauth.error
167
+ };
168
+ }
169
+ const err = parsed.error;
170
+ const errorInfo = err.details?.find((d) => d["@type"]?.includes("ErrorInfo"));
171
+ return {
172
+ code: err.code ?? httpStatus ?? 500,
173
+ message: err.message || err.status || text || `HTTP ${httpStatus ?? "?"}`,
174
+ reason: errorInfo?.reason,
175
+ status: err.status
176
+ };
177
+ }
178
+ var GscApiError = class extends Error {
179
+ info;
180
+ constructor(message, info) {
181
+ super(message);
182
+ this.info = info;
183
+ this.name = "GscApiError";
184
+ }
185
+ };
186
+ function rethrowAsGscApiError(prefix) {
187
+ return (err) => {
188
+ if (err instanceof GscApiError) throw err;
189
+ const maybe = err;
190
+ if (maybe && maybe.name === "FetchError") {
191
+ const info = parseGoogleError(typeof maybe.data === "string" ? maybe.data : JSON.stringify(maybe.data ?? {}), maybe.statusCode);
192
+ throw new GscApiError(`${prefix}: ${info.message}`, info);
193
+ }
194
+ throw err;
195
+ };
196
+ }
197
+ const PERMISSION_SIGNALS = [
198
+ "403 forbidden",
199
+ "permission_denied",
200
+ "does not have sufficient permission",
201
+ "insufficient permission"
202
+ ];
203
+ function isPermissionDeniedError(err) {
204
+ const msg = String(err?.message ?? err ?? "").toLowerCase();
205
+ return PERMISSION_SIGNALS.some((s) => msg.includes(s));
206
+ }
207
+ export { GscApiError, classifyError, gscErrorToException, isPermissionDeniedError, parseGoogleError, rethrowAsGscApiError };
@@ -0,0 +1,60 @@
1
+ declare const INDEXING_ISSUE_FILTERS: {
2
+ readonly canonical_mismatch: `user_canonical IS NOT NULL AND google_canonical IS NOT NULL AND user_canonical != google_canonical`;
3
+ readonly stale_crawl: `last_crawl_time < datetime('now', '-30 days')`;
4
+ readonly very_stale_crawl: `last_crawl_time < datetime('now', '-60 days')`;
5
+ readonly not_indexed: `verdict IN ('FAIL', 'PARTIAL', 'NEUTRAL')`;
6
+ readonly unknown_to_google: `coverage_state = 'URL is unknown to Google'`;
7
+ readonly crawled_not_indexed: `coverage_state = 'Crawled - currently not indexed'`;
8
+ readonly discovered_not_indexed: `coverage_state = 'Discovered - currently not indexed'`;
9
+ readonly not_found: `page_fetch_state = 'NOT_FOUND' OR coverage_state = 'Not found (404)'`;
10
+ readonly soft_404: `page_fetch_state = 'SOFT_404' OR coverage_state = 'Soft 404'`;
11
+ readonly server_error: `page_fetch_state = 'SERVER_ERROR' OR coverage_state = 'Server error (5xx)'`;
12
+ readonly access_forbidden: `page_fetch_state = 'ACCESS_FORBIDDEN' OR coverage_state = 'Blocked due to access forbidden (403)'`;
13
+ readonly access_denied: `page_fetch_state = 'ACCESS_DENIED' OR coverage_state = 'Blocked due to unauthorized request (401)'`;
14
+ readonly blocked_4xx: `page_fetch_state = 'BLOCKED_4XX' OR coverage_state = 'Blocked due to other 4xx issue'`;
15
+ readonly redirect_error: `page_fetch_state = 'REDIRECT_ERROR' OR coverage_state = 'Redirect error'`;
16
+ readonly crawl_error: `page_fetch_state IN ('INTERNAL_CRAWL_ERROR', 'INVALID_URL')`;
17
+ readonly blocked_robots: `robots_txt_state = 'DISALLOWED'`;
18
+ readonly noindex: `indexing_state LIKE '%noindex%' OR coverage_state LIKE '%noindex%'`;
19
+ readonly redirect: `coverage_state = 'Page with redirect'`;
20
+ readonly sitemap_redirect: `coverage_state = 'Page with redirect' AND sitemaps IS NOT NULL AND sitemaps != '[]'`;
21
+ readonly alternate_canonical: `coverage_state = 'Alternate page with proper canonical tag'`;
22
+ readonly duplicate_no_canonical: `coverage_state = 'Duplicate without user-selected canonical'`;
23
+ readonly page_removed: `coverage_state = 'Blocked by page removal tool'`;
24
+ readonly fragment_url: `url LIKE '%#%'`;
25
+ readonly mobile_fail: `mobile_verdict IN ('FAIL', 'PARTIAL')`;
26
+ readonly rich_results_fail: `rich_results_verdict = 'FAIL'`;
27
+ readonly rich_results_warning: `rich_results_verdict = 'PARTIAL'`;
28
+ readonly rich_results_pass: `rich_results_verdict = 'PASS'`;
29
+ };
30
+ type IndexingIssueType = keyof typeof INDEXING_ISSUE_FILTERS;
31
+ declare const INDEXING_ISSUE_LABELS: Record<IndexingIssueType, string>;
32
+ declare const INDEXING_ISSUE_SEVERITY: Record<IndexingIssueType, 'error' | 'warning' | 'info'>;
33
+ /**
34
+ * Every `pageFetchState` the URL Inspection API documents. `SUCCESSFUL` and the
35
+ * `_UNSPECIFIED` sentinel are non-faults; the rest each map to a filter above.
36
+ */
37
+ declare const KNOWN_PAGE_FETCH_STATES: ReadonlySet<string>;
38
+ /**
39
+ * Every `coverageState` string we have a bucket for, plus the indexed-state strings
40
+ * that are not faults. Google is free to invent new prose here at any time — that's
41
+ * exactly what {@link unmappedInspectionReasons} exists to catch.
42
+ */
43
+ declare const KNOWN_COVERAGE_STATES: ReadonlySet<string>;
44
+ interface UnmappedInspectionReasons {
45
+ coverageStates: string[];
46
+ pageFetchStates: string[];
47
+ }
48
+ /**
49
+ * Which reason strings in this batch of inspection results we have no bucket for.
50
+ *
51
+ * Pure, allocation-cheap, and safe to call on every ingest batch. A non-empty
52
+ * result means Google reports a state the filter map cannot see — the URLs are
53
+ * still counted in `not_indexed`, but nothing narrower, so the product will
54
+ * under-report exactly the way it did for `ACCESS_FORBIDDEN` before this existed.
55
+ */
56
+ declare function unmappedInspectionReasons(rows: ReadonlyArray<{
57
+ coverageState?: string | null;
58
+ pageFetchState?: string | null;
59
+ }>): UnmappedInspectionReasons;
60
+ export { INDEXING_ISSUE_FILTERS, INDEXING_ISSUE_LABELS, INDEXING_ISSUE_SEVERITY, IndexingIssueType, KNOWN_COVERAGE_STATES, KNOWN_PAGE_FETCH_STATES, UnmappedInspectionReasons, unmappedInspectionReasons };
@@ -0,0 +1,139 @@
1
+ const INDEXING_ISSUE_FILTERS = {
2
+ canonical_mismatch: `user_canonical IS NOT NULL AND google_canonical IS NOT NULL AND user_canonical != google_canonical`,
3
+ stale_crawl: `last_crawl_time < datetime('now', '-30 days')`,
4
+ very_stale_crawl: `last_crawl_time < datetime('now', '-60 days')`,
5
+ not_indexed: `verdict IN ('FAIL', 'PARTIAL', 'NEUTRAL')`,
6
+ unknown_to_google: `coverage_state = 'URL is unknown to Google'`,
7
+ crawled_not_indexed: `coverage_state = 'Crawled - currently not indexed'`,
8
+ discovered_not_indexed: `coverage_state = 'Discovered - currently not indexed'`,
9
+ not_found: `page_fetch_state = 'NOT_FOUND' OR coverage_state = 'Not found (404)'`,
10
+ soft_404: `page_fetch_state = 'SOFT_404' OR coverage_state = 'Soft 404'`,
11
+ server_error: `page_fetch_state = 'SERVER_ERROR' OR coverage_state = 'Server error (5xx)'`,
12
+ access_forbidden: `page_fetch_state = 'ACCESS_FORBIDDEN' OR coverage_state = 'Blocked due to access forbidden (403)'`,
13
+ access_denied: `page_fetch_state = 'ACCESS_DENIED' OR coverage_state = 'Blocked due to unauthorized request (401)'`,
14
+ blocked_4xx: `page_fetch_state = 'BLOCKED_4XX' OR coverage_state = 'Blocked due to other 4xx issue'`,
15
+ redirect_error: `page_fetch_state = 'REDIRECT_ERROR' OR coverage_state = 'Redirect error'`,
16
+ crawl_error: `page_fetch_state IN ('INTERNAL_CRAWL_ERROR', 'INVALID_URL')`,
17
+ blocked_robots: `robots_txt_state = 'DISALLOWED'`,
18
+ noindex: `indexing_state LIKE '%noindex%' OR coverage_state LIKE '%noindex%'`,
19
+ redirect: `coverage_state = 'Page with redirect'`,
20
+ sitemap_redirect: `coverage_state = 'Page with redirect' AND sitemaps IS NOT NULL AND sitemaps != '[]'`,
21
+ alternate_canonical: `coverage_state = 'Alternate page with proper canonical tag'`,
22
+ duplicate_no_canonical: `coverage_state = 'Duplicate without user-selected canonical'`,
23
+ page_removed: `coverage_state = 'Blocked by page removal tool'`,
24
+ fragment_url: `url LIKE '%#%'`,
25
+ mobile_fail: `mobile_verdict IN ('FAIL', 'PARTIAL')`,
26
+ rich_results_fail: `rich_results_verdict = 'FAIL'`,
27
+ rich_results_warning: `rich_results_verdict = 'PARTIAL'`,
28
+ rich_results_pass: `rich_results_verdict = 'PASS'`
29
+ };
30
+ const INDEXING_ISSUE_LABELS = {
31
+ canonical_mismatch: "Canonical mismatch",
32
+ stale_crawl: "Not crawled in 30+ days",
33
+ very_stale_crawl: "Not crawled in 60+ days",
34
+ not_indexed: "Not indexed",
35
+ unknown_to_google: "Unknown to Google",
36
+ crawled_not_indexed: "Crawled but not indexed",
37
+ discovered_not_indexed: "Discovered but not indexed",
38
+ not_found: "404 Not Found",
39
+ soft_404: "Soft 404",
40
+ server_error: "Server error",
41
+ access_forbidden: "Blocked: forbidden (403)",
42
+ access_denied: "Blocked: unauthorized (401)",
43
+ blocked_4xx: "Blocked: other 4xx",
44
+ redirect_error: "Redirect error",
45
+ crawl_error: "Crawl error",
46
+ blocked_robots: "Blocked by robots.txt",
47
+ noindex: "Noindex tag",
48
+ redirect: "Redirect",
49
+ sitemap_redirect: "Sitemap URL redirects",
50
+ alternate_canonical: "Alternate page with canonical",
51
+ duplicate_no_canonical: "Duplicate, no canonical declared",
52
+ page_removed: "Removed via removal tool",
53
+ fragment_url: "Fragment URL (#)",
54
+ mobile_fail: "Mobile usability issues",
55
+ rich_results_fail: "Rich results errors",
56
+ rich_results_warning: "Rich results warnings",
57
+ rich_results_pass: "Has rich results"
58
+ };
59
+ const INDEXING_ISSUE_SEVERITY = {
60
+ canonical_mismatch: "warning",
61
+ stale_crawl: "info",
62
+ very_stale_crawl: "warning",
63
+ not_indexed: "error",
64
+ unknown_to_google: "warning",
65
+ crawled_not_indexed: "error",
66
+ discovered_not_indexed: "warning",
67
+ not_found: "error",
68
+ soft_404: "error",
69
+ server_error: "error",
70
+ access_forbidden: "error",
71
+ access_denied: "error",
72
+ blocked_4xx: "error",
73
+ redirect_error: "error",
74
+ crawl_error: "warning",
75
+ blocked_robots: "warning",
76
+ noindex: "info",
77
+ redirect: "info",
78
+ sitemap_redirect: "warning",
79
+ alternate_canonical: "info",
80
+ duplicate_no_canonical: "warning",
81
+ page_removed: "info",
82
+ fragment_url: "warning",
83
+ mobile_fail: "warning",
84
+ rich_results_fail: "error",
85
+ rich_results_warning: "warning",
86
+ rich_results_pass: "info"
87
+ };
88
+ const KNOWN_PAGE_FETCH_STATES = /* @__PURE__ */ new Set([
89
+ "PAGE_FETCH_STATE_UNSPECIFIED",
90
+ "SUCCESSFUL",
91
+ "SOFT_404",
92
+ "BLOCKED_ROBOTS_TXT",
93
+ "NOT_FOUND",
94
+ "ACCESS_DENIED",
95
+ "SERVER_ERROR",
96
+ "REDIRECT_ERROR",
97
+ "ACCESS_FORBIDDEN",
98
+ "BLOCKED_4XX",
99
+ "INTERNAL_CRAWL_ERROR",
100
+ "INVALID_URL"
101
+ ]);
102
+ const KNOWN_COVERAGE_STATES = /* @__PURE__ */ new Set([
103
+ "Submitted and indexed",
104
+ "Indexed, not submitted in sitemap",
105
+ "Indexed, though blocked by robots.txt",
106
+ "Indexed; consider marking as canonical",
107
+ "URL is unknown to Google",
108
+ "Crawled - currently not indexed",
109
+ "Discovered - currently not indexed",
110
+ "Not found (404)",
111
+ "Soft 404",
112
+ "Server error (5xx)",
113
+ "Blocked due to access forbidden (403)",
114
+ "Blocked due to unauthorized request (401)",
115
+ "Blocked due to other 4xx issue",
116
+ "Redirect error",
117
+ "Blocked by robots.txt",
118
+ "Excluded by ‘noindex’ tag",
119
+ "Page with redirect",
120
+ "Alternate page with proper canonical tag",
121
+ "Duplicate without user-selected canonical",
122
+ "Duplicate, Google chose different canonical than user",
123
+ "Blocked by page removal tool"
124
+ ]);
125
+ function unmappedInspectionReasons(rows) {
126
+ const coverageStates = /* @__PURE__ */ new Set();
127
+ const pageFetchStates = /* @__PURE__ */ new Set();
128
+ for (const row of rows) {
129
+ const coverage = row.coverageState?.trim();
130
+ if (coverage && !KNOWN_COVERAGE_STATES.has(coverage)) coverageStates.add(coverage);
131
+ const fetchState = row.pageFetchState?.trim();
132
+ if (fetchState && !KNOWN_PAGE_FETCH_STATES.has(fetchState)) pageFetchStates.add(fetchState);
133
+ }
134
+ return {
135
+ coverageStates: [...coverageStates],
136
+ pageFetchStates: [...pageFetchStates]
137
+ };
138
+ }
139
+ export { INDEXING_ISSUE_FILTERS, INDEXING_ISSUE_LABELS, INDEXING_ISSUE_SEVERITY, KNOWN_COVERAGE_STATES, KNOWN_PAGE_FETCH_STATES, unmappedInspectionReasons };
@@ -0,0 +1,41 @@
1
+ interface GscPropertyCandidate {
2
+ siteUrl?: string | null;
3
+ permissionLevel?: string | null;
4
+ }
5
+ declare function isVerifiedGscProperty(property?: GscPropertyCandidate | null): boolean;
6
+ declare function isVerifiedGscPermission(level: string | null | undefined): boolean;
7
+ /**
8
+ * Does `propertyUrl` cover `targetDomain`? Matches sc-domain (exact or
9
+ * subdomain) and URL-prefix (host equality) without scheme/www noise.
10
+ */
11
+ declare function gscPropertyMatchesTarget(targetDomain: string, propertyUrl: string | null | undefined): boolean;
12
+ /**
13
+ * Convenience: match `siteUrl` against `gscSiteUrl` directly (extracts the
14
+ * hostname from `siteUrl` first).
15
+ */
16
+ declare function matchGscSite(siteUrl: string | null | undefined, gscSiteUrl: string | null | undefined): boolean;
17
+ /**
18
+ * Pick the best GSC property for a hostname from a candidate list. "Best":
19
+ * 1. Verified Domain property (widest + readable)
20
+ * 2. Verified URL-prefix property (narrower + readable)
21
+ * 3. Unverified Domain property (returned as a fallback so callers can
22
+ * surface the verification gap to the user)
23
+ * 4. Unverified URL-prefix property (same caveat)
24
+ *
25
+ * Without this ranking, naively picking the first match would register an
26
+ * unverified property and leave the site stuck with zero data.
27
+ */
28
+ declare function pickBestGscProperty<T extends GscPropertyCandidate>(origin: string, availableSites: readonly T[]): T | undefined;
29
+ /**
30
+ * Richer best-property selection that also returns the matched domain and
31
+ * URL candidates separately, so callers can show "we matched on X domain
32
+ * property and Y URL-prefix property" diagnostics.
33
+ */
34
+ declare function findBestGscProperty<T extends GscPropertyCandidate>(targetDomain: string, properties: readonly T[]): {
35
+ matchedSite: T | null;
36
+ domainProperty: T | null;
37
+ urlProperty: T | null;
38
+ };
39
+ declare function findExactGscProperty<T extends GscPropertyCandidate>(propertyUrl: string, properties: readonly T[]): T | null;
40
+ declare function formatGscPropertyCandidates(candidates: ReadonlyArray<GscPropertyCandidate | null | undefined>): string;
41
+ export { GscPropertyCandidate, findBestGscProperty, findExactGscProperty, formatGscPropertyCandidates, gscPropertyMatchesTarget, isVerifiedGscPermission, isVerifiedGscProperty, matchGscSite, pickBestGscProperty };
@@ -0,0 +1,74 @@
1
+ import { parseGscSiteUrl } from "./site-url.mjs";
2
+ const VERIFIED_PERMISSIONS = /* @__PURE__ */ new Set([
3
+ "siteOwner",
4
+ "siteFullUser",
5
+ "siteRestrictedUser"
6
+ ]);
7
+ function isVerifiedGscProperty(property) {
8
+ return !!property?.permissionLevel && VERIFIED_PERMISSIONS.has(property.permissionLevel);
9
+ }
10
+ function isVerifiedGscPermission(level) {
11
+ return !!level && VERIFIED_PERMISSIONS.has(level);
12
+ }
13
+ const stripWww = (d) => d.replace(/^www\./, "");
14
+ function gscPropertyMatchesTarget(targetDomain, propertyUrl) {
15
+ if (!propertyUrl) return false;
16
+ const cleanTarget = stripWww(targetDomain);
17
+ const parsed = parseGscSiteUrl(propertyUrl);
18
+ if (parsed.isDomain) return cleanTarget === parsed.hostname || cleanTarget.endsWith(`.${parsed.hostname}`);
19
+ const match = propertyUrl.match(/^https?:\/\/([^/]+)/);
20
+ return !!match && stripWww(match[1].toLowerCase()) === cleanTarget;
21
+ }
22
+ function matchGscSite(siteUrl, gscSiteUrl) {
23
+ if (!siteUrl || !gscSiteUrl) return false;
24
+ const getHostname = (url) => {
25
+ if (url.startsWith("sc-domain:")) return url.replace("sc-domain:", "");
26
+ try {
27
+ return new URL(url).hostname;
28
+ } catch {
29
+ return url;
30
+ }
31
+ };
32
+ return gscPropertyMatchesTarget(getHostname(siteUrl), gscSiteUrl);
33
+ }
34
+ function pickBestGscProperty(origin, availableSites) {
35
+ const matches = availableSites.filter((p) => matchGscSite(origin, p.siteUrl));
36
+ if (!matches.length) return void 0;
37
+ const isDomain = (p) => !!p.siteUrl?.startsWith("sc-domain:");
38
+ const isHttps = (p) => !!p.siteUrl?.startsWith("https://");
39
+ const pickTier = (pool) => pool.find(isDomain) ?? pool.find(isHttps) ?? pool[0];
40
+ const verified = matches.filter((p) => isVerifiedGscPermission(p.permissionLevel));
41
+ const pool = verified.length ? verified : matches;
42
+ const originHost = stripWww(parseGscSiteUrl(origin).hostname.toLowerCase());
43
+ const exact = pool.filter((property) => {
44
+ if (!property.siteUrl) return false;
45
+ return stripWww(parseGscSiteUrl(property.siteUrl).hostname.toLowerCase()) === originHost;
46
+ });
47
+ return pickTier(exact.length ? exact : pool);
48
+ }
49
+ function findBestGscProperty(targetDomain, properties) {
50
+ const cleanTarget = stripWww(targetDomain);
51
+ const domainProperty = properties.find((property) => {
52
+ if (!property.siteUrl) return false;
53
+ const parsed = parseGscSiteUrl(property.siteUrl);
54
+ if (!parsed.isDomain) return false;
55
+ return cleanTarget === parsed.hostname || cleanTarget.endsWith(`.${parsed.hostname}`);
56
+ });
57
+ const urlProperty = properties.find((property) => {
58
+ if (!property.siteUrl) return false;
59
+ const match = property.siteUrl.match(/^https?:\/\/([^/]+)/);
60
+ return match && stripWww(match[1].toLowerCase()) === cleanTarget;
61
+ });
62
+ return {
63
+ matchedSite: (isVerifiedGscProperty(domainProperty) ? domainProperty : isVerifiedGscProperty(urlProperty) ? urlProperty : domainProperty || urlProperty) ?? null,
64
+ domainProperty: domainProperty ?? null,
65
+ urlProperty: urlProperty ?? null
66
+ };
67
+ }
68
+ function findExactGscProperty(propertyUrl, properties) {
69
+ return properties.find((property) => property.siteUrl === propertyUrl) ?? null;
70
+ }
71
+ function formatGscPropertyCandidates(candidates) {
72
+ return candidates.filter((c) => !!c).map((property) => `${property.siteUrl} (${property.permissionLevel})`).join(", ");
73
+ }
74
+ export { findBestGscProperty, findExactGscProperty, formatGscPropertyCandidates, gscPropertyMatchesTarget, isVerifiedGscPermission, isVerifiedGscProperty, matchGscSite, pickBestGscProperty };
@@ -0,0 +1,2 @@
1
+ declare const URL_INSPECTION_EFFECTIVE_LIMIT = 1800;
2
+ export { URL_INSPECTION_EFFECTIVE_LIMIT };
@@ -0,0 +1,2 @@
1
+ const URL_INSPECTION_EFFECTIVE_LIMIT = 1800;
2
+ export { URL_INSPECTION_EFFECTIVE_LIMIT };
@@ -1,2 +1,20 @@
1
- import { Err, Ok, Result, err, isErr, isOk, ok, unwrapResult } from "../_chunks/result.mjs";
1
+ interface Ok<A> {
2
+ readonly ok: true;
3
+ readonly value: A;
4
+ }
5
+ interface Err<E> {
6
+ readonly ok: false;
7
+ readonly error: E;
8
+ }
9
+ type Result<A, E> = Ok<A> | Err<E>;
10
+ declare function ok<A>(value: A): Ok<A>;
11
+ declare function err<E>(error: E): Err<E>;
12
+ declare function isOk<A, E>(result: Result<A, E>): result is Ok<A>;
13
+ declare function isErr<A, E>(result: Result<A, E>): result is Err<E>;
14
+ /**
15
+ * Collapses a `Result` back into the throwing world: returns the value or throws
16
+ * via `toError`. Used by the throwing wrappers that sit over the `Result` core so
17
+ * existing call sites keep their `await`/`throw` ergonomics.
18
+ */
19
+ declare function unwrapResult<A, E>(result: Result<A, E>, toError: (error: E) => unknown): A;
2
20
  export { Err, Ok, Result, err, isErr, isOk, ok, unwrapResult };
@@ -0,0 +1,7 @@
1
+ type GoogleScopesInput = string | readonly string[] | null | undefined;
2
+ declare const GSC_READ_SCOPE: 'https://www.googleapis.com/auth/webmasters.readonly';
3
+ declare const GSC_WRITE_SCOPE: 'https://www.googleapis.com/auth/webmasters';
4
+ declare const GSC_INDEXING_SCOPE: 'https://www.googleapis.com/auth/indexing';
5
+ declare const GSC_SITE_VERIFICATION_SCOPE: 'https://www.googleapis.com/auth/siteverification';
6
+ declare function hasGoogleScope(scopes: GoogleScopesInput, scope: string): boolean;
7
+ export { GSC_INDEXING_SCOPE, GSC_READ_SCOPE, GSC_SITE_VERIFICATION_SCOPE, GSC_WRITE_SCOPE, hasGoogleScope };
@@ -0,0 +1,15 @@
1
+ const GSC_READ_SCOPE = "https://www.googleapis.com/auth/webmasters.readonly";
2
+ const GSC_WRITE_SCOPE = "https://www.googleapis.com/auth/webmasters";
3
+ const GSC_INDEXING_SCOPE = "https://www.googleapis.com/auth/indexing";
4
+ const GSC_SITE_VERIFICATION_SCOPE = "https://www.googleapis.com/auth/siteverification";
5
+ function parseGoogleScopes(scopes) {
6
+ if (!scopes) return [];
7
+ if (typeof scopes === "string") return scopes.split(/\s+/).map((scope) => scope.trim()).filter(Boolean);
8
+ return scopes.map((scope) => scope.trim()).filter(Boolean);
9
+ }
10
+ function hasGoogleScope(scopes, scope) {
11
+ const tokens = parseGoogleScopes(scopes);
12
+ const suffix = scope.replace("https://www.googleapis.com/auth/", "");
13
+ return tokens.includes(scope) || tokens.includes(suffix);
14
+ }
15
+ export { GSC_INDEXING_SCOPE, GSC_READ_SCOPE, GSC_SITE_VERIFICATION_SCOPE, GSC_WRITE_SCOPE, hasGoogleScope };
@@ -0,0 +1,5 @@
1
+ type GoogleScopesInput = string | readonly string[] | null | undefined;
2
+ declare function hasGscReadScope(scopes: GoogleScopesInput): boolean;
3
+ declare function hasGscWriteScope(scopes: GoogleScopesInput): boolean;
4
+ declare function hasIndexingScope(scopes: GoogleScopesInput): boolean;
5
+ export { hasGscReadScope, hasGscWriteScope, hasIndexingScope };
@@ -0,0 +1,11 @@
1
+ import { GSC_INDEXING_SCOPE, GSC_WRITE_SCOPE, hasGoogleScope } from "./scope-values.mjs";
2
+ function hasGscReadScope(scopes) {
3
+ return hasGoogleScope(scopes, "https://www.googleapis.com/auth/webmasters.readonly") || hasGoogleScope(scopes, "https://www.googleapis.com/auth/webmasters");
4
+ }
5
+ function hasGscWriteScope(scopes) {
6
+ return hasGoogleScope(scopes, GSC_WRITE_SCOPE);
7
+ }
8
+ function hasIndexingScope(scopes) {
9
+ return hasGoogleScope(scopes, GSC_INDEXING_SCOPE);
10
+ }
11
+ export { hasGscReadScope, hasGscWriteScope, hasIndexingScope };
@@ -0,0 +1,25 @@
1
+ interface ParsedGscSiteUrl {
2
+ /** Original, canonical GSC property URL. */
3
+ label: string;
4
+ /** Bare hostname, stripped of protocol / sc-domain prefix / path. */
5
+ hostname: string;
6
+ /** Human-friendly label: scheme stripped, trailing slash trimmed, path retained. */
7
+ displayLabel: string;
8
+ propertyType: 'domain' | 'url-prefix';
9
+ isDomain: boolean;
10
+ }
11
+ declare function parseGscSiteUrl(siteUrl: string): ParsedGscSiteUrl;
12
+ /**
13
+ * Comparison-canonical form of a GSC property URL. Strips `sc-domain:`,
14
+ * protocol, leading `www.`, trailing slash, and lowercases. Use when matching
15
+ * the same logical site across `https://www.example.com/` vs
16
+ * `sc-domain:example.com` vs bare hostnames — properties Google sometimes
17
+ * returns in different shapes for the same site.
18
+ */
19
+ declare function normalizeGscSiteUrl(siteUrl: string): string;
20
+ /**
21
+ * Normalize a user-input URL/hostname into a canonical registration target.
22
+ * Returns lowercase hostname stripped of protocol, or null if unparseable.
23
+ */
24
+ declare function normalizeRegistrationTarget(inputUrl: string): string | null;
25
+ export { ParsedGscSiteUrl, normalizeGscSiteUrl, normalizeRegistrationTarget, parseGscSiteUrl };
@@ -0,0 +1,30 @@
1
+ const SCHEME_RE = /^(sc-domain:|https?:\/\/)/;
2
+ function parseGscSiteUrl(siteUrl) {
3
+ const isDomain = siteUrl.startsWith("sc-domain:");
4
+ let hostname = siteUrl;
5
+ if (isDomain) hostname = siteUrl.slice(10);
6
+ else try {
7
+ hostname = new URL(siteUrl).hostname;
8
+ } catch {
9
+ hostname = siteUrl;
10
+ }
11
+ const displayLabel = siteUrl.replace(SCHEME_RE, "").replace(/\/$/, "");
12
+ return {
13
+ label: siteUrl,
14
+ hostname,
15
+ displayLabel,
16
+ propertyType: isDomain ? "domain" : "url-prefix",
17
+ isDomain
18
+ };
19
+ }
20
+ function normalizeGscSiteUrl(siteUrl) {
21
+ return siteUrl.replace(/^sc-domain:/, "").replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "").toLowerCase();
22
+ }
23
+ function normalizeRegistrationTarget(inputUrl) {
24
+ const trimmed = inputUrl.trim();
25
+ if (!trimmed) return null;
26
+ const inputParsed = parseGscSiteUrl(trimmed);
27
+ if (inputParsed.isDomain) return inputParsed.hostname.toLowerCase();
28
+ return trimmed.match(/^(?:https?:\/\/)?([^/]+)/)?.[1]?.toLowerCase() ?? null;
29
+ }
30
+ export { normalizeGscSiteUrl, normalizeRegistrationTarget, parseGscSiteUrl };