pagesight 0.16.0 → 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.
- package/README.md +17 -83
- package/docs/credentials.md +54 -0
- package/docs/diagnostics.md +85 -0
- package/docs/snapshots.md +123 -0
- package/docs/usage.md +130 -0
- package/package.json +26 -29
- package/src/api/bing.ts +83 -0
- package/src/api/compare-snapshots.ts +377 -0
- package/src/api/discover.ts +39 -0
- package/src/api/doctor.ts +35 -0
- package/src/api/evidence-schema.ts +78 -0
- package/src/api/evidence.ts +99 -0
- package/src/api/execute.ts +147 -0
- package/src/api/index.ts +5 -0
- package/src/api/reports.ts +90 -0
- package/src/api/schema.ts +207 -0
- package/src/api/snapshot.ts +183 -0
- package/src/api/ui-findings.ts +68 -0
- package/src/cli.ts +182 -0
- package/src/http.ts +39 -0
- package/src/index.ts +8 -25
- package/src/mcp-server.ts +27 -0
- package/src/mcp.ts +5 -0
- package/src/providers/bing.ts +48 -0
- package/src/{lib → providers}/crux.ts +4 -9
- package/src/providers/ga.ts +114 -0
- package/src/providers/google-tokens.ts +86 -0
- package/src/providers/gsc-auth.ts +93 -0
- package/src/{lib → providers}/gsc.ts +27 -24
- package/src/{lib/psi.ts → providers/pagespeed.ts} +3 -14
- package/src/shared/dates.ts +32 -0
- package/src/shared/http.ts +63 -0
- package/src/tools/ai.ts +19 -27
- package/src/tools/audit.ts +26 -97
- package/src/tools/observe.ts +28 -0
- package/src/tools/page/analyze.ts +194 -0
- package/src/tools/page/batch.ts +163 -0
- package/src/tools/page/contrast.ts +128 -0
- package/src/tools/page/links.ts +200 -0
- package/src/tools/page/metadata.ts +225 -0
- package/src/tools/page/structured-data.ts +288 -0
- package/src/tools/page/tool.ts +48 -0
- package/src/tools/search/actions.ts +56 -0
- package/src/tools/search/analytics.ts +266 -0
- package/src/tools/search/coverage.ts +247 -0
- package/src/tools/search/gaps.ts +129 -0
- package/src/tools/search/inspection.ts +160 -0
- package/src/tools/search/result.ts +3 -0
- package/src/tools/search/sample.ts +110 -0
- package/src/tools/search/schema.ts +62 -0
- package/src/{lib/sitemap.ts → tools/search/sitemap-sampling.ts} +1 -45
- package/src/tools/search/sites.ts +86 -0
- package/src/tools/search/tool.ts +11 -0
- package/src/tools/setup.ts +59 -20
- package/src/tools/speed/analyze.ts +191 -0
- package/src/tools/speed/batch.ts +265 -0
- package/src/tools/speed/crux.ts +176 -0
- package/src/tools/speed/pagespeed.ts +273 -0
- package/src/tools/speed/schema.ts +39 -0
- package/src/tools/speed/tool.ts +11 -0
- package/src/web/fetch.ts +31 -0
- package/src/web/images.ts +62 -0
- package/src/web/page-observation.ts +69 -0
- package/src/{lib → web}/robots.ts +20 -12
- package/src/web/sitemap-inventory.ts +64 -0
- package/src/web/sitemap-parser.ts +59 -0
- package/src/lib/auth.ts +0 -187
- package/src/tools/page.ts +0 -1241
- package/src/tools/search.ts +0 -852
- package/src/tools/speed.ts +0 -956
package/src/lib/auth.ts
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
|
|
3
|
-
const SCOPES = ["https://www.googleapis.com/auth/webmasters.readonly"];
|
|
4
|
-
const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
5
|
-
|
|
6
|
-
interface TokenResponse {
|
|
7
|
-
access_token: string;
|
|
8
|
-
expires_in: number;
|
|
9
|
-
token_type: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
let cachedToken: { token: string; expiresAt: number } | null = null;
|
|
13
|
-
|
|
14
|
-
// --- Service Account Auth ---
|
|
15
|
-
|
|
16
|
-
async function getServiceAccountToken(keyPath: string): Promise<string> {
|
|
17
|
-
const keyFile = JSON.parse(await Bun.file(keyPath).text());
|
|
18
|
-
if (!keyFile.client_email || !keyFile.private_key) {
|
|
19
|
-
throw new Error("Service account key file missing client_email or private_key");
|
|
20
|
-
}
|
|
21
|
-
const now = Math.floor(Date.now() / 1000);
|
|
22
|
-
|
|
23
|
-
const header = toBase64Url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
|
|
24
|
-
const payload = toBase64Url(
|
|
25
|
-
JSON.stringify({
|
|
26
|
-
iss: keyFile.client_email,
|
|
27
|
-
scope: SCOPES.join(" "),
|
|
28
|
-
aud: TOKEN_URL,
|
|
29
|
-
iat: now,
|
|
30
|
-
exp: now + 3600,
|
|
31
|
-
}),
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
const signingInput = `${header}.${payload}`;
|
|
35
|
-
const key = await crypto.subtle.importKey(
|
|
36
|
-
"pkcs8",
|
|
37
|
-
pemToBuffer(keyFile.private_key),
|
|
38
|
-
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
39
|
-
false,
|
|
40
|
-
["sign"],
|
|
41
|
-
);
|
|
42
|
-
const signature = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", key, new TextEncoder().encode(signingInput));
|
|
43
|
-
const jwt = `${signingInput}.${bufferToBase64Url(signature)}`;
|
|
44
|
-
|
|
45
|
-
const res = await fetch(TOKEN_URL, {
|
|
46
|
-
method: "POST",
|
|
47
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
48
|
-
body: `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${jwt}`,
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
if (!res.ok) {
|
|
52
|
-
const err = await res.text();
|
|
53
|
-
throw new Error(`Service account token exchange failed: ${err}`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const data: TokenResponse = await res.json();
|
|
57
|
-
return data.access_token;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function pemToBuffer(pem: string): ArrayBuffer {
|
|
61
|
-
const b64 = pem
|
|
62
|
-
.replace(/-----BEGIN PRIVATE KEY-----/g, "")
|
|
63
|
-
.replace(/-----END PRIVATE KEY-----/g, "")
|
|
64
|
-
.replace(/\s/g, "");
|
|
65
|
-
const binary = atob(b64);
|
|
66
|
-
const buf = new Uint8Array(binary.length);
|
|
67
|
-
for (let i = 0; i < binary.length; i++) buf[i] = binary.charCodeAt(i);
|
|
68
|
-
return buf.buffer;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function toBase64Url(input: string): string {
|
|
72
|
-
return btoa(input).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function bufferToBase64Url(buf: ArrayBuffer): string {
|
|
76
|
-
const bytes = new Uint8Array(buf);
|
|
77
|
-
let binary = "";
|
|
78
|
-
for (const b of bytes) binary += String.fromCharCode(b);
|
|
79
|
-
return toBase64Url(binary);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// --- OAuth Refresh Token Auth ---
|
|
83
|
-
|
|
84
|
-
async function getOAuthToken(clientId: string, clientSecret: string, refreshToken: string): Promise<string> {
|
|
85
|
-
const res = await fetch(TOKEN_URL, {
|
|
86
|
-
method: "POST",
|
|
87
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
88
|
-
body: new URLSearchParams({
|
|
89
|
-
grant_type: "refresh_token",
|
|
90
|
-
client_id: clientId,
|
|
91
|
-
client_secret: clientSecret,
|
|
92
|
-
refresh_token: refreshToken,
|
|
93
|
-
}).toString(),
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
if (!res.ok) {
|
|
97
|
-
const err = await res.text();
|
|
98
|
-
throw new Error(`OAuth token refresh failed: ${err}`);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const data: TokenResponse = await res.json();
|
|
102
|
-
return data.access_token;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// --- Public API ---
|
|
106
|
-
|
|
107
|
-
export async function getAccessToken(): Promise<string> {
|
|
108
|
-
// Return cached token if still valid
|
|
109
|
-
if (cachedToken && Date.now() < cachedToken.expiresAt) {
|
|
110
|
-
return cachedToken.token;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Try service account first
|
|
114
|
-
const saKeyPath = process.env.GSC_SERVICE_ACCOUNT_KEY;
|
|
115
|
-
if (saKeyPath && existsSync(saKeyPath)) {
|
|
116
|
-
const token = await getServiceAccountToken(saKeyPath);
|
|
117
|
-
cachedToken = { token, expiresAt: Date.now() + 3500_000 }; // ~58 min
|
|
118
|
-
return token;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Try OAuth refresh token
|
|
122
|
-
const clientId = process.env.GSC_CLIENT_ID;
|
|
123
|
-
const clientSecret = process.env.GSC_CLIENT_SECRET;
|
|
124
|
-
const refreshToken = process.env.GSC_REFRESH_TOKEN;
|
|
125
|
-
|
|
126
|
-
if (clientId && clientSecret && refreshToken) {
|
|
127
|
-
const token = await getOAuthToken(clientId, clientSecret, refreshToken);
|
|
128
|
-
cachedToken = { token, expiresAt: Date.now() + 3500_000 };
|
|
129
|
-
return token;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
throw new Error(
|
|
133
|
-
"No GSC credentials configured. Set either:\n" +
|
|
134
|
-
" - GSC_SERVICE_ACCOUNT_KEY (path to service account JSON)\n" +
|
|
135
|
-
" - GSC_CLIENT_ID + GSC_CLIENT_SECRET + GSC_REFRESH_TOKEN (OAuth 2.0)",
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function clearTokenCache(): void {
|
|
140
|
-
cachedToken = null;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export function getAuthMethod(): string {
|
|
144
|
-
if (process.env.GSC_SERVICE_ACCOUNT_KEY) return "service_account";
|
|
145
|
-
if (process.env.GSC_REFRESH_TOKEN) return "oauth";
|
|
146
|
-
return "none";
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// --- OAuth Setup Helper ---
|
|
150
|
-
|
|
151
|
-
export function getOAuthSetupUrl(clientId: string): string {
|
|
152
|
-
const params = new URLSearchParams({
|
|
153
|
-
client_id: clientId,
|
|
154
|
-
redirect_uri: "http://localhost",
|
|
155
|
-
response_type: "code",
|
|
156
|
-
scope: SCOPES.join(" "),
|
|
157
|
-
access_type: "offline",
|
|
158
|
-
prompt: "consent",
|
|
159
|
-
});
|
|
160
|
-
return `https://accounts.google.com/o/oauth2/auth?${params}`;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export async function exchangeCodeForToken(
|
|
164
|
-
clientId: string,
|
|
165
|
-
clientSecret: string,
|
|
166
|
-
code: string,
|
|
167
|
-
): Promise<{ refreshToken: string; accessToken: string }> {
|
|
168
|
-
const res = await fetch(TOKEN_URL, {
|
|
169
|
-
method: "POST",
|
|
170
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
171
|
-
body: new URLSearchParams({
|
|
172
|
-
grant_type: "authorization_code",
|
|
173
|
-
client_id: clientId,
|
|
174
|
-
client_secret: clientSecret,
|
|
175
|
-
code,
|
|
176
|
-
redirect_uri: "http://localhost",
|
|
177
|
-
}).toString(),
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
if (!res.ok) {
|
|
181
|
-
const err = await res.text();
|
|
182
|
-
throw new Error(`Code exchange failed: ${err}`);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const data = (await res.json()) as Record<string, string>;
|
|
186
|
-
return { refreshToken: data.refresh_token, accessToken: data.access_token };
|
|
187
|
-
}
|