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.
- package/dist/api/batch.d.mts +12 -0
- package/dist/api/batch.mjs +29 -0
- package/dist/api/indexing.d.mts +37 -0
- package/dist/api/indexing.mjs +25 -0
- package/dist/api/inspection.d.mts +89 -0
- package/dist/api/inspection.mjs +128 -0
- package/dist/api/oauth.d.mts +48 -0
- package/dist/api/oauth.mjs +154 -0
- package/dist/api/sites.d.mts +45 -0
- package/dist/api/sites.mjs +34 -0
- package/dist/api/verification.d.mts +43 -0
- package/dist/api/verification.mjs +89 -0
- package/dist/contracts.d.mts +46 -1
- package/dist/core/cli-format.mjs +9 -0
- package/dist/core/client.d.mts +122 -0
- package/dist/core/client.mjs +246 -0
- package/dist/core/errors.d.mts +66 -0
- package/dist/core/errors.mjs +207 -0
- package/dist/core/indexing-issues.d.mts +60 -0
- package/dist/core/indexing-issues.mjs +139 -0
- package/dist/core/property.d.mts +41 -0
- package/dist/core/property.mjs +74 -0
- package/dist/core/quota.d.mts +2 -0
- package/dist/core/quota.mjs +2 -0
- package/dist/core/result.d.mts +19 -1
- package/dist/core/scope-values.d.mts +7 -0
- package/dist/core/scope-values.mjs +15 -0
- package/dist/core/scopes.d.mts +5 -0
- package/dist/core/scopes.mjs +11 -0
- package/dist/core/site-url.d.mts +25 -0
- package/dist/core/site-url.mjs +30 -0
- package/dist/core/types.d.mts +201 -0
- package/dist/core/types.mjs +1 -0
- package/dist/dates.d.mts +2 -2
- package/dist/dates.mjs +2 -2
- package/dist/index.d.mts +20 -814
- package/dist/index.mjs +19 -1243
- package/dist/onboarding.d.mts +2 -0
- package/dist/onboarding.mjs +2 -0
- package/dist/{_chunks → query}/builder.d.mts +2 -2
- package/dist/query/builder.mjs +86 -0
- package/dist/query/columns.d.mts +15 -0
- package/dist/query/columns.mjs +23 -0
- package/dist/query/constants.d.mts +19 -0
- package/dist/query/constants.mjs +16 -0
- package/dist/query/errors.d.mts +85 -0
- package/dist/query/errors.mjs +141 -0
- package/dist/query/index.d.mts +9 -75
- package/dist/query/index.mjs +7 -230
- package/dist/query/operator-meta.mjs +41 -0
- package/dist/query/operators.d.mts +24 -0
- package/dist/query/operators.mjs +124 -0
- package/dist/query/plan.d.mts +92 -2
- package/dist/query/plan.mjs +3 -1
- package/dist/query/resolver.d.mts +40 -0
- package/dist/query/resolver.mjs +243 -0
- package/dist/{_chunks → query}/types.d.mts +3 -25
- package/dist/query/utils/countries.d.mts +7 -0
- package/dist/{_chunks/resolver.mjs → query/utils/countries.mjs} +1 -434
- package/dist/{_chunks → query/utils}/dayjs.mjs +2 -4
- package/dist/sitemap.d.mts +26 -0
- package/dist/sitemap.mjs +70 -0
- package/dist/url.d.mts +9 -0
- package/dist/url.mjs +6 -0
- package/package.json +2 -3
- package/dist/_chunks/contracts.d.mts +0 -47
- package/dist/_chunks/plan.d.mts +0 -175
- package/dist/_chunks/result.d.mts +0 -20
- /package/dist/{_chunks → core}/gsc-dates.d.mts +0 -0
- /package/dist/{_chunks → core}/gsc-dates.mjs +0 -0
- /package/dist/{_chunks → query/utils}/dayjs.d.mts +0 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Batch runner with optional concurrency, inter-call delay, and progress.
|
|
3
|
+
* Used by batchRequestIndexing / batchInspectUrls. Defaults to sequential
|
|
4
|
+
* (concurrency = 1) because the underlying APIs rate-limit aggressively;
|
|
5
|
+
* callers that know their quota headroom can opt into parallelism.
|
|
6
|
+
*/
|
|
7
|
+
declare function runSequentialBatch<I, R>(items: I[], operation: (item: I, index: number) => Promise<R>, options?: {
|
|
8
|
+
delayMs?: number;
|
|
9
|
+
concurrency?: number;
|
|
10
|
+
onProgress?: (result: R, index: number, total: number) => void;
|
|
11
|
+
}): Promise<R[]>;
|
|
12
|
+
export { runSequentialBatch };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
async function runSequentialBatch(items, operation, options = {}) {
|
|
2
|
+
const { delayMs = 0, concurrency = 1, onProgress } = options;
|
|
3
|
+
const results = Array.from({ length: items.length });
|
|
4
|
+
let completed = 0;
|
|
5
|
+
if (concurrency <= 1) {
|
|
6
|
+
for (let i = 0; i < items.length; i++) {
|
|
7
|
+
const result = await operation(items[i], i);
|
|
8
|
+
results[i] = result;
|
|
9
|
+
onProgress?.(result, i, items.length);
|
|
10
|
+
if (i < items.length - 1 && delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
|
|
11
|
+
}
|
|
12
|
+
return results;
|
|
13
|
+
}
|
|
14
|
+
const cursor = { i: 0 };
|
|
15
|
+
const worker = async () => {
|
|
16
|
+
while (true) {
|
|
17
|
+
const i = cursor.i++;
|
|
18
|
+
if (i >= items.length) return;
|
|
19
|
+
const result = await operation(items[i], i);
|
|
20
|
+
results[i] = result;
|
|
21
|
+
completed++;
|
|
22
|
+
onProgress?.(result, completed - 1, items.length);
|
|
23
|
+
if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, worker));
|
|
27
|
+
return results;
|
|
28
|
+
}
|
|
29
|
+
export { runSequentialBatch };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { UrlNotification } from "../core/types.mjs";
|
|
2
|
+
import { GoogleSearchConsoleClient } from "../core/client.mjs";
|
|
3
|
+
type IndexingNotificationType = 'URL_UPDATED' | 'URL_DELETED';
|
|
4
|
+
interface IndexingResult {
|
|
5
|
+
url: string;
|
|
6
|
+
type: IndexingNotificationType;
|
|
7
|
+
notifyTime?: string;
|
|
8
|
+
}
|
|
9
|
+
interface IndexingMetadata {
|
|
10
|
+
url: string;
|
|
11
|
+
latestUpdate?: UrlNotification;
|
|
12
|
+
latestRemove?: UrlNotification;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Request Google to index or remove a URL via the Indexing API.
|
|
16
|
+
* Note: The Indexing API officially supports only job posting and livestream content,
|
|
17
|
+
* but can be used for any URL with varying success.
|
|
18
|
+
*/
|
|
19
|
+
declare function requestIndexing(client: GoogleSearchConsoleClient, url: string, options?: {
|
|
20
|
+
type?: IndexingNotificationType;
|
|
21
|
+
}): Promise<IndexingResult>;
|
|
22
|
+
/**
|
|
23
|
+
* Get the indexing notification metadata for a URL.
|
|
24
|
+
* Returns when Google was last notified about updates/removals.
|
|
25
|
+
*/
|
|
26
|
+
declare function getIndexingMetadata(client: GoogleSearchConsoleClient, url: string): Promise<IndexingMetadata>;
|
|
27
|
+
/**
|
|
28
|
+
* Batch request indexing for multiple URLs with rate limiting.
|
|
29
|
+
* Returns results for each URL.
|
|
30
|
+
*/
|
|
31
|
+
declare function batchRequestIndexing(client: GoogleSearchConsoleClient, urls: string[], options?: {
|
|
32
|
+
type?: IndexingNotificationType;
|
|
33
|
+
delayMs?: number;
|
|
34
|
+
concurrency?: number;
|
|
35
|
+
onProgress?: (result: IndexingResult, index: number, total: number) => void;
|
|
36
|
+
}): Promise<IndexingResult[]>;
|
|
37
|
+
export { IndexingMetadata, IndexingNotificationType, IndexingResult, batchRequestIndexing, getIndexingMetadata, requestIndexing };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { runSequentialBatch } from "./batch.mjs";
|
|
2
|
+
async function requestIndexing(client, url, options = {}) {
|
|
3
|
+
const { type = "URL_UPDATED" } = options;
|
|
4
|
+
return client.indexing.publish(url, type).then((r) => ({
|
|
5
|
+
url,
|
|
6
|
+
type,
|
|
7
|
+
notifyTime: r.urlNotificationMetadata?.latestUpdate?.notifyTime || void 0
|
|
8
|
+
}));
|
|
9
|
+
}
|
|
10
|
+
async function getIndexingMetadata(client, url) {
|
|
11
|
+
return client.indexing.getMetadata(url).then((r) => ({
|
|
12
|
+
url,
|
|
13
|
+
latestUpdate: r.latestUpdate || void 0,
|
|
14
|
+
latestRemove: r.latestRemove || void 0
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
async function batchRequestIndexing(client, urls, options = {}) {
|
|
18
|
+
const { type = "URL_UPDATED", delayMs = 100, concurrency, onProgress } = options;
|
|
19
|
+
return runSequentialBatch(urls, (url) => requestIndexing(client, url, { type }), {
|
|
20
|
+
delayMs,
|
|
21
|
+
concurrency,
|
|
22
|
+
onProgress
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export { batchRequestIndexing, getIndexingMetadata, requestIndexing };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { UrlInspectionResult } from "../core/types.mjs";
|
|
2
|
+
import { CallOptions, GoogleSearchConsoleClient } from "../core/client.mjs";
|
|
3
|
+
interface InspectUrlResult {
|
|
4
|
+
url: string;
|
|
5
|
+
inspection?: UrlInspectionResult;
|
|
6
|
+
isIndexed: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Inspects a URL in Google Search Console to check its indexing status.
|
|
10
|
+
*/
|
|
11
|
+
declare function inspectUrl(client: GoogleSearchConsoleClient, siteUrl: string, inspectionUrl: string): Promise<{
|
|
12
|
+
inspection: UrlInspectionResult | undefined;
|
|
13
|
+
isIndexed: boolean;
|
|
14
|
+
}>;
|
|
15
|
+
/**
|
|
16
|
+
* Batch inspect multiple URLs with rate limiting.
|
|
17
|
+
* Returns inspection results for each URL.
|
|
18
|
+
*/
|
|
19
|
+
declare function batchInspectUrls(client: GoogleSearchConsoleClient, siteUrl: string, urls: string[], options?: {
|
|
20
|
+
delayMs?: number;
|
|
21
|
+
concurrency?: number;
|
|
22
|
+
onProgress?: (result: InspectUrlResult, index: number, total: number) => void;
|
|
23
|
+
}): Promise<InspectUrlResult[]>;
|
|
24
|
+
interface ParsedIndexingResult {
|
|
25
|
+
url: string;
|
|
26
|
+
verdict: string | null;
|
|
27
|
+
coverageState: string | null;
|
|
28
|
+
indexingState: string | null;
|
|
29
|
+
robotsTxtState: string | null;
|
|
30
|
+
pageFetchState: string | null;
|
|
31
|
+
lastCrawlTime: string | null;
|
|
32
|
+
crawlingUserAgent: string | null;
|
|
33
|
+
userCanonical: string | null;
|
|
34
|
+
googleCanonical: string | null;
|
|
35
|
+
sitemaps: string | null;
|
|
36
|
+
referringUrls: string | null;
|
|
37
|
+
mobileVerdict: string | null;
|
|
38
|
+
mobileIssues: string | null;
|
|
39
|
+
richResultsVerdict: string | null;
|
|
40
|
+
richResultsItems: string | null;
|
|
41
|
+
ampVerdict: string | null;
|
|
42
|
+
ampUrl: string | null;
|
|
43
|
+
ampIndexingState: string | null;
|
|
44
|
+
ampIndexStatusVerdict: string | null;
|
|
45
|
+
ampRobotsTxtState: string | null;
|
|
46
|
+
ampPageFetchState: string | null;
|
|
47
|
+
ampLastCrawlTime: string | null;
|
|
48
|
+
ampIssues: string | null;
|
|
49
|
+
inspectionResultLink: string | null;
|
|
50
|
+
}
|
|
51
|
+
declare function inspectUrlFlat(client: GoogleSearchConsoleClient, siteUrl: string, inspectionUrl: string, options?: CallOptions): Promise<ParsedIndexingResult>;
|
|
52
|
+
/** Maximum parallel URL Inspection requests started by the settled flat batch helper. */
|
|
53
|
+
declare const MAX_FLAT_INSPECTION_BATCH_CONCURRENCY = 10;
|
|
54
|
+
type InspectUrlFlatSettledResult = {
|
|
55
|
+
url: string;
|
|
56
|
+
status: 'fulfilled';
|
|
57
|
+
value: ParsedIndexingResult;
|
|
58
|
+
} | {
|
|
59
|
+
url: string;
|
|
60
|
+
status: 'rejected';
|
|
61
|
+
reason: unknown;
|
|
62
|
+
};
|
|
63
|
+
interface BatchInspectUrlsFlatSettledOptions extends CallOptions {
|
|
64
|
+
/** Delay after each request handled by a worker. Defaults to 200ms. */
|
|
65
|
+
delayMs?: number;
|
|
66
|
+
/** Number of workers. Defaults to 1 and is capped at {@link MAX_FLAT_INSPECTION_BATCH_CONCURRENCY}. */
|
|
67
|
+
concurrency?: number;
|
|
68
|
+
onProgress?: (result: InspectUrlFlatSettledResult, index: number, total: number) => void;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Inspect URLs into the flat storage shape without failing the whole batch
|
|
72
|
+
* when one URL fails. Results retain input order and include each URL so
|
|
73
|
+
* callers can persist successes and classify individual failures safely.
|
|
74
|
+
*/
|
|
75
|
+
declare function batchInspectUrlsFlatSettled(client: GoogleSearchConsoleClient, siteUrl: string, urls: readonly string[], options?: BatchInspectUrlsFlatSettledOptions): Promise<InspectUrlFlatSettledResult[]>;
|
|
76
|
+
type InspectionPriority = 'high' | 'medium' | 'low';
|
|
77
|
+
declare function getNextCheckPriority(result: Pick<ParsedIndexingResult, 'verdict'>): InspectionPriority;
|
|
78
|
+
/** Next-check unix seconds for a given priority. */
|
|
79
|
+
declare function getNextCheckAfter(priority: InspectionPriority): number;
|
|
80
|
+
declare function canUseUrlInspection(permissionLevel: string | null | undefined): boolean;
|
|
81
|
+
type IndexingIneligibleReason = 'missing_indexing_scope' | 'insufficient_gsc_permission';
|
|
82
|
+
interface IndexingEligibility {
|
|
83
|
+
indexingEligible: boolean;
|
|
84
|
+
indexingIneligibleReason?: IndexingIneligibleReason;
|
|
85
|
+
indexingPermissionLevel?: string | null;
|
|
86
|
+
grantedScopes?: string[];
|
|
87
|
+
}
|
|
88
|
+
declare function getIndexingEligibility(grantedScopes: string | null | undefined, permissionLevel: string | null | undefined): IndexingEligibility;
|
|
89
|
+
export { BatchInspectUrlsFlatSettledOptions, IndexingEligibility, IndexingIneligibleReason, InspectUrlFlatSettledResult, InspectUrlResult, InspectionPriority, MAX_FLAT_INSPECTION_BATCH_CONCURRENCY, ParsedIndexingResult, batchInspectUrls, batchInspectUrlsFlatSettled, canUseUrlInspection, getIndexingEligibility, getNextCheckAfter, getNextCheckPriority, inspectUrl, inspectUrlFlat };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { runSequentialBatch } from "./batch.mjs";
|
|
2
|
+
import { hasIndexingScope } from "../core/scopes.mjs";
|
|
3
|
+
async function inspectUrl(client, siteUrl, inspectionUrl) {
|
|
4
|
+
const inspection = (await client.inspect(siteUrl, inspectionUrl)).inspectionResult;
|
|
5
|
+
return {
|
|
6
|
+
inspection,
|
|
7
|
+
isIndexed: inspection?.indexStatusResult?.verdict === "PASS"
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
async function batchInspectUrls(client, siteUrl, urls, options = {}) {
|
|
11
|
+
const { delayMs = 200, concurrency, onProgress } = options;
|
|
12
|
+
return runSequentialBatch(urls, async (url) => {
|
|
13
|
+
const { inspection, isIndexed } = await inspectUrl(client, siteUrl, url);
|
|
14
|
+
return {
|
|
15
|
+
url,
|
|
16
|
+
inspection,
|
|
17
|
+
isIndexed
|
|
18
|
+
};
|
|
19
|
+
}, {
|
|
20
|
+
delayMs,
|
|
21
|
+
concurrency,
|
|
22
|
+
onProgress
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
async function inspectUrlFlat(client, siteUrl, inspectionUrl, options) {
|
|
26
|
+
const inspection = (await client.inspect(siteUrl, inspectionUrl, options)).inspectionResult;
|
|
27
|
+
const index = inspection?.indexStatusResult;
|
|
28
|
+
const mobile = inspection?.mobileUsabilityResult;
|
|
29
|
+
const rich = inspection?.richResultsResult;
|
|
30
|
+
const amp = inspection?.ampResult;
|
|
31
|
+
return {
|
|
32
|
+
url: inspectionUrl,
|
|
33
|
+
verdict: index?.verdict ?? null,
|
|
34
|
+
coverageState: index?.coverageState ?? null,
|
|
35
|
+
indexingState: index?.indexingState ?? null,
|
|
36
|
+
robotsTxtState: index?.robotsTxtState ?? null,
|
|
37
|
+
pageFetchState: index?.pageFetchState ?? null,
|
|
38
|
+
lastCrawlTime: index?.lastCrawlTime ?? null,
|
|
39
|
+
crawlingUserAgent: index?.crawledAs ?? null,
|
|
40
|
+
userCanonical: index?.userCanonical ?? null,
|
|
41
|
+
googleCanonical: index?.googleCanonical ?? null,
|
|
42
|
+
sitemaps: index?.sitemap?.length ? JSON.stringify(index.sitemap) : null,
|
|
43
|
+
referringUrls: index?.referringUrls?.length ? JSON.stringify(index.referringUrls) : null,
|
|
44
|
+
mobileVerdict: mobile?.verdict ?? null,
|
|
45
|
+
mobileIssues: mobile?.issues?.length ? JSON.stringify(mobile.issues) : null,
|
|
46
|
+
richResultsVerdict: rich?.verdict ?? null,
|
|
47
|
+
richResultsItems: rich?.detectedItems?.length ? JSON.stringify(rich.detectedItems) : null,
|
|
48
|
+
ampVerdict: amp?.verdict ?? null,
|
|
49
|
+
ampUrl: amp?.ampUrl ?? null,
|
|
50
|
+
ampIndexingState: amp?.indexingState ?? null,
|
|
51
|
+
ampIndexStatusVerdict: amp?.ampIndexStatusVerdict ?? null,
|
|
52
|
+
ampRobotsTxtState: amp?.robotsTxtState ?? null,
|
|
53
|
+
ampPageFetchState: amp?.pageFetchState ?? null,
|
|
54
|
+
ampLastCrawlTime: amp?.lastCrawlTime ?? null,
|
|
55
|
+
ampIssues: amp?.issues?.length ? JSON.stringify(amp.issues) : null,
|
|
56
|
+
inspectionResultLink: inspection?.inspectionResultLink ?? null
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const MAX_FLAT_INSPECTION_BATCH_CONCURRENCY = 10;
|
|
60
|
+
async function batchInspectUrlsFlatSettled(client, siteUrl, urls, options = {}) {
|
|
61
|
+
const requestedConcurrency = Math.trunc(options.concurrency ?? 1);
|
|
62
|
+
const concurrency = Number.isFinite(requestedConcurrency) ? Math.max(1, Math.min(requestedConcurrency, 10)) : 1;
|
|
63
|
+
return runSequentialBatch([...urls], async (url) => {
|
|
64
|
+
try {
|
|
65
|
+
return {
|
|
66
|
+
url,
|
|
67
|
+
status: "fulfilled",
|
|
68
|
+
value: await inspectUrlFlat(client, siteUrl, url, { signal: options.signal })
|
|
69
|
+
};
|
|
70
|
+
} catch (reason) {
|
|
71
|
+
return {
|
|
72
|
+
url,
|
|
73
|
+
status: "rejected",
|
|
74
|
+
reason
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}, {
|
|
78
|
+
delayMs: options.delayMs ?? 200,
|
|
79
|
+
concurrency,
|
|
80
|
+
onProgress: options.onProgress
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function getNextCheckPriority(result) {
|
|
84
|
+
if (!result.verdict || result.verdict === "VERDICT_UNSPECIFIED") return "medium";
|
|
85
|
+
if (result.verdict === "FAIL" || result.verdict === "PARTIAL" || result.verdict === "NEUTRAL") return "high";
|
|
86
|
+
if (result.verdict === "PASS") return "low";
|
|
87
|
+
return "medium";
|
|
88
|
+
}
|
|
89
|
+
function getNextCheckAfter(priority) {
|
|
90
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
91
|
+
switch (priority) {
|
|
92
|
+
case "high": return now + 7 * 86400;
|
|
93
|
+
case "medium": return now + 14 * 86400;
|
|
94
|
+
case "low": return now + 30 * 86400;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const INSPECTION_ALLOWED_PERMISSIONS = /* @__PURE__ */ new Set([
|
|
98
|
+
"siteOwner",
|
|
99
|
+
"siteFullUser",
|
|
100
|
+
"siteRestrictedUser"
|
|
101
|
+
]);
|
|
102
|
+
function canUseUrlInspection(permissionLevel) {
|
|
103
|
+
return !!permissionLevel && INSPECTION_ALLOWED_PERMISSIONS.has(permissionLevel);
|
|
104
|
+
}
|
|
105
|
+
function grantedScopeList(scopes) {
|
|
106
|
+
return scopes?.split(/\s+/).map((s) => s.trim()).filter(Boolean) ?? [];
|
|
107
|
+
}
|
|
108
|
+
function getIndexingEligibility(grantedScopes, permissionLevel) {
|
|
109
|
+
const scopes = grantedScopeList(grantedScopes);
|
|
110
|
+
if (!hasIndexingScope(grantedScopes)) return {
|
|
111
|
+
indexingEligible: false,
|
|
112
|
+
indexingIneligibleReason: "missing_indexing_scope",
|
|
113
|
+
indexingPermissionLevel: permissionLevel ?? null,
|
|
114
|
+
grantedScopes: scopes
|
|
115
|
+
};
|
|
116
|
+
if (!canUseUrlInspection(permissionLevel)) return {
|
|
117
|
+
indexingEligible: false,
|
|
118
|
+
indexingIneligibleReason: "insufficient_gsc_permission",
|
|
119
|
+
indexingPermissionLevel: permissionLevel ?? null,
|
|
120
|
+
grantedScopes: scopes
|
|
121
|
+
};
|
|
122
|
+
return {
|
|
123
|
+
indexingEligible: true,
|
|
124
|
+
indexingPermissionLevel: permissionLevel ?? null,
|
|
125
|
+
grantedScopes: scopes
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export { MAX_FLAT_INSPECTION_BATCH_CONCURRENCY, batchInspectUrls, batchInspectUrlsFlatSettled, canUseUrlInspection, getIndexingEligibility, getNextCheckAfter, getNextCheckPriority, inspectUrl, inspectUrlFlat };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Result } from "../core/result.mjs";
|
|
2
|
+
import { GscError } from "../core/errors.mjs";
|
|
3
|
+
interface OAuthTokens {
|
|
4
|
+
accessToken: string;
|
|
5
|
+
/** Unix seconds. */
|
|
6
|
+
expiresAt: number;
|
|
7
|
+
/** Space-delimited scopes Google granted for this token, when returned. */
|
|
8
|
+
scope?: string;
|
|
9
|
+
}
|
|
10
|
+
/** Normalized response from Google's access-token introspection endpoint. */
|
|
11
|
+
interface OAuthTokenInfo {
|
|
12
|
+
issuedTo?: string;
|
|
13
|
+
audience?: string;
|
|
14
|
+
authorizedParty?: string;
|
|
15
|
+
userId?: string;
|
|
16
|
+
subject?: string;
|
|
17
|
+
scope?: string;
|
|
18
|
+
/** Seconds remaining when Google inspected the token. */
|
|
19
|
+
expiresIn?: number;
|
|
20
|
+
/** Unix seconds. */
|
|
21
|
+
expiresAt?: number;
|
|
22
|
+
email?: string;
|
|
23
|
+
emailVerified?: boolean;
|
|
24
|
+
accessType?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Exchange a refresh token for an access token. Retries transient network
|
|
28
|
+
* failures; surfaces HTTP failures (invalid_grant, etc.) immediately as
|
|
29
|
+
* `GscApiError` so callers can mark the refresh token as bad.
|
|
30
|
+
*/
|
|
31
|
+
declare function refreshAccessToken(refreshToken: string, clientId: string, clientSecret: string): Promise<OAuthTokens>;
|
|
32
|
+
/**
|
|
33
|
+
* Exchange an authorization code (from the OAuth consent redirect) for
|
|
34
|
+
* access + refresh tokens. Same retry / surface model and `auth-expired` vs
|
|
35
|
+
* `transport` classification as {@link refreshAccessTokenResult}.
|
|
36
|
+
*/
|
|
37
|
+
declare function exchangeAuthCodeResult(code: string, clientId: string, clientSecret: string, redirectUri: string): Promise<Result<OAuthTokens & {
|
|
38
|
+
refreshToken?: string;
|
|
39
|
+
}, GscError>>;
|
|
40
|
+
/** Inspect an access token without throwing on expected OAuth failures. */
|
|
41
|
+
declare function introspectAccessTokenResult(accessToken: string): Promise<Result<OAuthTokenInfo, GscError>>;
|
|
42
|
+
/** Throwing convenience wrapper for {@link introspectAccessTokenResult}. */
|
|
43
|
+
declare function introspectAccessToken(accessToken: string): Promise<OAuthTokenInfo>;
|
|
44
|
+
/** Revoke an access or refresh token without throwing on expected OAuth failures. */
|
|
45
|
+
declare function revokeOAuthTokenResult(token: string): Promise<Result<void, GscError>>;
|
|
46
|
+
/** Throwing convenience wrapper for {@link revokeOAuthTokenResult}. */
|
|
47
|
+
declare function revokeOAuthToken(token: string): Promise<void>;
|
|
48
|
+
export { OAuthTokenInfo, OAuthTokens, exchangeAuthCodeResult, introspectAccessToken, introspectAccessTokenResult, refreshAccessToken, revokeOAuthToken, revokeOAuthTokenResult };
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { GscApiError, gscErrorToException, parseGoogleError } from "../core/errors.mjs";
|
|
2
|
+
import { err, ok, unwrapResult } from "../core/result.mjs";
|
|
3
|
+
const OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
4
|
+
const OAUTH_TOKEN_INFO_URL = "https://oauth2.googleapis.com/tokeninfo";
|
|
5
|
+
const OAUTH_REVOKE_URL = "https://oauth2.googleapis.com/revoke";
|
|
6
|
+
const OAUTH_TIMEOUT_MS = 8e3;
|
|
7
|
+
const OAUTH_MAX_ATTEMPTS = 3;
|
|
8
|
+
const OAUTH_BACKOFF_MS = [
|
|
9
|
+
0,
|
|
10
|
+
400,
|
|
11
|
+
1500
|
|
12
|
+
];
|
|
13
|
+
async function refreshAccessTokenResult(refreshToken, clientId, clientSecret) {
|
|
14
|
+
const result = await postOAuthTokenResult(new URLSearchParams({
|
|
15
|
+
client_id: clientId,
|
|
16
|
+
client_secret: clientSecret,
|
|
17
|
+
refresh_token: refreshToken,
|
|
18
|
+
grant_type: "refresh_token"
|
|
19
|
+
}), "refresh");
|
|
20
|
+
if (!result.ok) return result;
|
|
21
|
+
return ok({
|
|
22
|
+
accessToken: result.value.accessToken,
|
|
23
|
+
expiresAt: result.value.expiresAt,
|
|
24
|
+
scope: result.value.scope
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
async function refreshAccessToken(refreshToken, clientId, clientSecret) {
|
|
28
|
+
return unwrapResult(await refreshAccessTokenResult(refreshToken, clientId, clientSecret), oauthErrorToException);
|
|
29
|
+
}
|
|
30
|
+
async function exchangeAuthCodeResult(code, clientId, clientSecret, redirectUri) {
|
|
31
|
+
const result = await postOAuthTokenResult(new URLSearchParams({
|
|
32
|
+
client_id: clientId,
|
|
33
|
+
client_secret: clientSecret,
|
|
34
|
+
code,
|
|
35
|
+
redirect_uri: redirectUri,
|
|
36
|
+
grant_type: "authorization_code"
|
|
37
|
+
}), "exchange");
|
|
38
|
+
if (!result.ok) return result;
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
async function introspectAccessTokenResult(accessToken) {
|
|
42
|
+
const response = await requestOAuthResult(`${OAUTH_TOKEN_INFO_URL}?access_token=${encodeURIComponent(accessToken)}`, { method: "GET" }, "introspect");
|
|
43
|
+
if (!response.ok) return response;
|
|
44
|
+
const parsed = await readOAuthJsonResult(response.value, "introspect");
|
|
45
|
+
if (!parsed.ok) return parsed;
|
|
46
|
+
const data = parsed.value;
|
|
47
|
+
return ok({
|
|
48
|
+
issuedTo: optionalString(data.issued_to),
|
|
49
|
+
audience: optionalString(data.aud),
|
|
50
|
+
authorizedParty: optionalString(data.azp),
|
|
51
|
+
userId: optionalString(data.user_id),
|
|
52
|
+
subject: optionalString(data.sub),
|
|
53
|
+
scope: optionalString(data.scope),
|
|
54
|
+
expiresIn: optionalNumber(data.expires_in),
|
|
55
|
+
expiresAt: optionalNumber(data.exp),
|
|
56
|
+
email: optionalString(data.email),
|
|
57
|
+
emailVerified: optionalBoolean(data.email_verified),
|
|
58
|
+
accessType: optionalString(data.access_type)
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async function introspectAccessToken(accessToken) {
|
|
62
|
+
return unwrapResult(await introspectAccessTokenResult(accessToken), oauthErrorToException);
|
|
63
|
+
}
|
|
64
|
+
async function revokeOAuthTokenResult(token) {
|
|
65
|
+
const response = await requestOAuthResult(OAUTH_REVOKE_URL, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
68
|
+
body: new URLSearchParams({ token })
|
|
69
|
+
}, "revoke");
|
|
70
|
+
return response.ok ? ok(void 0) : response;
|
|
71
|
+
}
|
|
72
|
+
async function revokeOAuthToken(token) {
|
|
73
|
+
return unwrapResult(await revokeOAuthTokenResult(token), oauthErrorToException);
|
|
74
|
+
}
|
|
75
|
+
function oauthHttpError(op, info) {
|
|
76
|
+
const message = `Failed to ${op} token: ${info.message}`;
|
|
77
|
+
const cause = new GscApiError(message, info);
|
|
78
|
+
return info.reason === "invalid_grant" || info.reason === "invalid_token" || info.code === 400 || info.code === 401 || info.code === 403 ? {
|
|
79
|
+
kind: "auth-expired",
|
|
80
|
+
message,
|
|
81
|
+
cause
|
|
82
|
+
} : {
|
|
83
|
+
kind: "transport",
|
|
84
|
+
message,
|
|
85
|
+
status: info.code,
|
|
86
|
+
cause
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function oauthErrorToException(error) {
|
|
90
|
+
return error.cause instanceof Error ? error.cause : gscErrorToException(error);
|
|
91
|
+
}
|
|
92
|
+
async function postOAuthTokenResult(body, op) {
|
|
93
|
+
const response = await requestOAuthResult(OAUTH_TOKEN_URL, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
96
|
+
body
|
|
97
|
+
}, op);
|
|
98
|
+
if (!response.ok) return response;
|
|
99
|
+
const parsed = await readOAuthJsonResult(response.value, op);
|
|
100
|
+
if (!parsed.ok) return parsed;
|
|
101
|
+
const data = parsed.value;
|
|
102
|
+
return ok({
|
|
103
|
+
accessToken: data.access_token,
|
|
104
|
+
expiresAt: Math.floor(Date.now() / 1e3) + data.expires_in,
|
|
105
|
+
refreshToken: data.refresh_token,
|
|
106
|
+
scope: data.scope
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
async function requestOAuthResult(url, init, op) {
|
|
110
|
+
let lastError;
|
|
111
|
+
for (let attempt = 0; attempt < OAUTH_MAX_ATTEMPTS; attempt++) {
|
|
112
|
+
if (OAUTH_BACKOFF_MS[attempt]) await new Promise((r) => setTimeout(r, OAUTH_BACKOFF_MS[attempt]));
|
|
113
|
+
const res = await fetch(url, {
|
|
114
|
+
...init,
|
|
115
|
+
signal: AbortSignal.timeout(OAUTH_TIMEOUT_MS)
|
|
116
|
+
}).catch((error) => {
|
|
117
|
+
lastError = error;
|
|
118
|
+
return null;
|
|
119
|
+
});
|
|
120
|
+
if (!res) continue;
|
|
121
|
+
if (!res.ok) return err(oauthHttpError(op, parseGoogleError(await res.text(), res.status)));
|
|
122
|
+
return ok(res);
|
|
123
|
+
}
|
|
124
|
+
return err({
|
|
125
|
+
kind: "transport",
|
|
126
|
+
message: lastError instanceof Error ? lastError.message : `OAuth ${op} failed after ${OAUTH_MAX_ATTEMPTS} attempts`,
|
|
127
|
+
cause: lastError
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
async function readOAuthJsonResult(response, op) {
|
|
131
|
+
try {
|
|
132
|
+
return ok(await response.json());
|
|
133
|
+
} catch (cause) {
|
|
134
|
+
return err({
|
|
135
|
+
kind: "transport",
|
|
136
|
+
message: `Failed to parse ${op} token response`,
|
|
137
|
+
status: response.status,
|
|
138
|
+
cause
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function optionalString(value) {
|
|
143
|
+
return typeof value === "string" ? value : void 0;
|
|
144
|
+
}
|
|
145
|
+
function optionalNumber(value) {
|
|
146
|
+
const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
|
147
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
148
|
+
}
|
|
149
|
+
function optionalBoolean(value) {
|
|
150
|
+
if (typeof value === "boolean") return value;
|
|
151
|
+
if (value === "true") return true;
|
|
152
|
+
if (value === "false") return false;
|
|
153
|
+
}
|
|
154
|
+
export { exchangeAuthCodeResult, introspectAccessToken, introspectAccessTokenResult, refreshAccessToken, revokeOAuthToken, revokeOAuthTokenResult };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ApiSite, ApiSitemap, Site } from "../core/types.mjs";
|
|
2
|
+
import { GoogleSearchConsoleClient } from "../core/client.mjs";
|
|
3
|
+
interface FetchSitesWithSitemapsOptions {
|
|
4
|
+
/** Maximum concurrent sitemap-list requests. Defaults to 4. */
|
|
5
|
+
concurrency?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Fetches all sites the authenticated user has access to in Google Search Console.
|
|
9
|
+
*/
|
|
10
|
+
declare function fetchSites(client: GoogleSearchConsoleClient): Promise<ApiSite[]>;
|
|
11
|
+
/**
|
|
12
|
+
* Fetches all verified sites with their sitemaps from Google Search Console.
|
|
13
|
+
*/
|
|
14
|
+
declare function fetchSitesWithSitemaps(client: GoogleSearchConsoleClient, options?: FetchSitesWithSitemapsOptions): Promise<(Site & {
|
|
15
|
+
sitemaps: ApiSitemap[];
|
|
16
|
+
})[]>;
|
|
17
|
+
/**
|
|
18
|
+
* Fetches all sitemaps for a site.
|
|
19
|
+
*/
|
|
20
|
+
declare function fetchSitemaps(client: GoogleSearchConsoleClient, siteUrl: string): Promise<ApiSitemap[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Fetches a specific sitemap.
|
|
23
|
+
*/
|
|
24
|
+
declare function fetchSitemap(client: GoogleSearchConsoleClient, siteUrl: string, feedpath: string): Promise<ApiSitemap>;
|
|
25
|
+
/**
|
|
26
|
+
* Submits a sitemap to Google Search Console.
|
|
27
|
+
*/
|
|
28
|
+
declare function submitSitemap(client: GoogleSearchConsoleClient, siteUrl: string, feedpath: string): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Deletes a sitemap from Google Search Console.
|
|
31
|
+
*/
|
|
32
|
+
declare function deleteSitemap(client: GoogleSearchConsoleClient, siteUrl: string, feedpath: string): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Add a property to the user's Search Console account.
|
|
35
|
+
*
|
|
36
|
+
* Note: this only registers the property in an unverified state. Ownership
|
|
37
|
+
* must be proven via the Site Verification API (see `verifySite`) before any
|
|
38
|
+
* data is accessible.
|
|
39
|
+
*/
|
|
40
|
+
declare function addSite(client: GoogleSearchConsoleClient, siteUrl: string): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Remove a property from the user's Search Console account.
|
|
43
|
+
*/
|
|
44
|
+
declare function deleteSite(client: GoogleSearchConsoleClient, siteUrl: string): Promise<void>;
|
|
45
|
+
export { FetchSitesWithSitemapsOptions, addSite, deleteSite, deleteSitemap, fetchSitemap, fetchSitemaps, fetchSites, fetchSitesWithSitemaps, submitSitemap };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { runSequentialBatch } from "./batch.mjs";
|
|
2
|
+
async function fetchSites(client) {
|
|
3
|
+
return client.sites();
|
|
4
|
+
}
|
|
5
|
+
async function fetchSitesWithSitemaps(client, options = {}) {
|
|
6
|
+
const sites = (await client.sites()).filter((s) => !!s.siteUrl && s.permissionLevel !== "siteUnverifiedUser");
|
|
7
|
+
const requestedConcurrency = options.concurrency ?? 4;
|
|
8
|
+
return runSequentialBatch(sites, async (site) => {
|
|
9
|
+
const sitemaps = await client.sitemaps.list(site.siteUrl).catch(() => []);
|
|
10
|
+
return {
|
|
11
|
+
...site,
|
|
12
|
+
sitemaps
|
|
13
|
+
};
|
|
14
|
+
}, { concurrency: Number.isFinite(requestedConcurrency) ? Math.max(1, Math.floor(requestedConcurrency)) : 4 });
|
|
15
|
+
}
|
|
16
|
+
async function fetchSitemaps(client, siteUrl) {
|
|
17
|
+
return client.sitemaps.list(siteUrl);
|
|
18
|
+
}
|
|
19
|
+
async function fetchSitemap(client, siteUrl, feedpath) {
|
|
20
|
+
return client.sitemaps.get(siteUrl, feedpath);
|
|
21
|
+
}
|
|
22
|
+
async function submitSitemap(client, siteUrl, feedpath) {
|
|
23
|
+
return client.sitemaps.submit(siteUrl, feedpath);
|
|
24
|
+
}
|
|
25
|
+
async function deleteSitemap(client, siteUrl, feedpath) {
|
|
26
|
+
return client.sitemaps.delete(siteUrl, feedpath);
|
|
27
|
+
}
|
|
28
|
+
async function addSite(client, siteUrl) {
|
|
29
|
+
return client.sites.add(siteUrl);
|
|
30
|
+
}
|
|
31
|
+
async function deleteSite(client, siteUrl) {
|
|
32
|
+
return client.sites.delete(siteUrl);
|
|
33
|
+
}
|
|
34
|
+
export { addSite, deleteSite, deleteSitemap, fetchSitemap, fetchSitemaps, fetchSites, fetchSitesWithSitemaps, submitSitemap };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { GoogleSearchConsoleClient, VerificationMethod, VerificationSite, VerificationToken, VerificationWebResource } from "../core/client.mjs";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve a Search Console site URL (`https://example.com/` or
|
|
4
|
+
* `sc-domain:example.com`) to the Site Verification API's site shape.
|
|
5
|
+
*/
|
|
6
|
+
declare function siteUrlToVerificationSite(siteUrl: string): VerificationSite;
|
|
7
|
+
/** Resolve a Search Console property and method to Google's verification target. */
|
|
8
|
+
declare function resolveVerificationTarget(siteUrl: string, method?: VerificationMethod): {
|
|
9
|
+
site: VerificationSite;
|
|
10
|
+
method: VerificationMethod;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Methods valid for a given site shape. SITE properties can use META/FILE/
|
|
14
|
+
* ANALYTICS/TAG_MANAGER; INET_DOMAIN must use DNS_TXT or DNS_CNAME.
|
|
15
|
+
*/
|
|
16
|
+
declare function verificationMethodsFor(site: VerificationSite): VerificationMethod[];
|
|
17
|
+
/**
|
|
18
|
+
* Get the verification token Google expects to find on the site or DNS.
|
|
19
|
+
*/
|
|
20
|
+
declare function getVerificationToken(client: GoogleSearchConsoleClient, siteUrl: string, method: VerificationMethod): Promise<VerificationToken & {
|
|
21
|
+
site: VerificationSite;
|
|
22
|
+
}>;
|
|
23
|
+
/**
|
|
24
|
+
* Trigger Google to validate the placed token. Caller is responsible for
|
|
25
|
+
* having placed the token (HTML tag / file / DNS record) before calling.
|
|
26
|
+
*/
|
|
27
|
+
declare function verifySite(client: GoogleSearchConsoleClient, siteUrl: string, method: VerificationMethod): Promise<VerificationWebResource>;
|
|
28
|
+
/**
|
|
29
|
+
* List all verified WebResources for the authed user.
|
|
30
|
+
*/
|
|
31
|
+
declare function listVerifiedSites(client: GoogleSearchConsoleClient): Promise<VerificationWebResource[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Fetch a single verified WebResource by id.
|
|
34
|
+
*/
|
|
35
|
+
declare function getVerifiedSite(client: GoogleSearchConsoleClient, id: string): Promise<VerificationWebResource>;
|
|
36
|
+
/**
|
|
37
|
+
* Drop the calling user's verified ownership of a WebResource. The placed
|
|
38
|
+
* verification token (meta tag / file / DNS record) MUST be removed first,
|
|
39
|
+
* otherwise Google may auto-re-verify and the call will fail. Other owners
|
|
40
|
+
* on the property are unaffected.
|
|
41
|
+
*/
|
|
42
|
+
declare function unverifySite(client: GoogleSearchConsoleClient, id: string): Promise<void>;
|
|
43
|
+
export { getVerificationToken, getVerifiedSite, listVerifiedSites, resolveVerificationTarget, siteUrlToVerificationSite, unverifySite, verificationMethodsFor, verifySite };
|