@gscdump/sdk 1.0.1 → 1.0.3
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 +7 -1
- package/dist/_chunks/errors.d.mts +22 -0
- package/dist/_chunks/request.d.mts +25 -0
- package/dist/_chunks/request.mjs +84 -0
- package/dist/_chunks/search-console-signals.mjs +9 -0
- package/dist/analytics-client.d.mts +53 -0
- package/dist/analytics-client.mjs +128 -0
- package/dist/analyzer-defs.d.mts +73 -0
- package/dist/analyzer-defs.mjs +4 -0
- package/dist/anonymization.d.mts +6 -0
- package/dist/anonymization.mjs +12 -0
- package/dist/archetype-compile.d.mts +48 -0
- package/dist/archetype-compile.mjs +124 -0
- package/dist/client.d.mts +122 -0
- package/dist/client.mjs +365 -0
- package/dist/country-names.d.mts +2 -0
- package/dist/country-names.mjs +56 -0
- package/dist/cwv-thresholds.d.mts +13 -0
- package/dist/cwv-thresholds.mjs +23 -0
- package/dist/errors.d.mts +2 -0
- package/dist/errors.mjs +47 -0
- package/dist/gsc-console-url.d.mts +12 -0
- package/dist/gsc-console-url.mjs +15 -0
- package/dist/gsc-constants.d.mts +17 -0
- package/dist/gsc-constants.mjs +2 -0
- package/dist/gsc-error.d.mts +12 -0
- package/dist/gsc-error.mjs +42 -0
- package/dist/gsc-period-presets.d.mts +31 -0
- package/dist/gsc-period-presets.mjs +115 -0
- package/dist/gsc-rows.d.mts +54 -0
- package/dist/gsc-rows.mjs +48 -0
- package/dist/hosted-query.d.mts +31 -0
- package/dist/hosted-query.mjs +105 -0
- package/dist/index.d.mts +21 -852
- package/dist/index.mjs +21 -2275
- package/dist/indexing-issues.d.mts +29 -0
- package/dist/indexing-issues.mjs +168 -0
- package/dist/lifecycle.d.mts +14 -0
- package/dist/lifecycle.mjs +84 -0
- package/dist/period.d.mts +49 -0
- package/dist/period.mjs +138 -0
- package/dist/search-console-stage.d.mts +102 -0
- package/dist/search-console-stage.mjs +292 -0
- package/dist/site-baseline.d.mts +69 -0
- package/dist/site-baseline.mjs +112 -0
- package/dist/site-triage.d.mts +86 -0
- package/dist/site-triage.mjs +268 -0
- package/dist/v1/index.d.mts +1 -1
- package/dist/v1/index.mjs +2 -2
- package/dist/webhook.d.mts +29 -0
- package/dist/webhook.mjs +118 -0
- package/package.json +110 -5
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { countSearchConsoleIssues, formatSearchConsoleCount } from "./_chunks/search-console-signals.mjs";
|
|
2
|
+
import { normalizeSiteType } from "./site-baseline.mjs";
|
|
3
|
+
const NASCENT_IMPRESSIONS_28D = 1e3;
|
|
4
|
+
const ESTABLISHED_IMPRESSIONS_28D = 2e4;
|
|
5
|
+
const LIFETIME_FLOOR = 500;
|
|
6
|
+
const GROWTH_CLICKS_PCT = 10;
|
|
7
|
+
const GROWTH_IMPRESSIONS_PCT = 20;
|
|
8
|
+
const DECLINE_CLICKS_PCT = -10;
|
|
9
|
+
const MIN_DECLINE_PRIOR_CLICKS = 50;
|
|
10
|
+
const FADED_LIVENESS = .2;
|
|
11
|
+
const DECAY_CTR = .005;
|
|
12
|
+
const HARD_BLOCK_FLOOR = 10;
|
|
13
|
+
const HARD_BLOCK_SHARE = .03;
|
|
14
|
+
const REJECT_SHARE = .3;
|
|
15
|
+
const REJECT_MIN = 50;
|
|
16
|
+
function reachLivenessRatio(daily) {
|
|
17
|
+
if (!daily || daily.length < 14) return null;
|
|
18
|
+
const series = daily.map((d) => d.impressions);
|
|
19
|
+
while (series.length && series[series.length - 1] === 0) series.pop();
|
|
20
|
+
if (series.length < 14) return null;
|
|
21
|
+
const rolling7 = (end) => {
|
|
22
|
+
let sum = 0;
|
|
23
|
+
for (let i = Math.max(0, end - 6); i <= end; i++) sum += series[i];
|
|
24
|
+
return sum;
|
|
25
|
+
};
|
|
26
|
+
const latestWeek = rolling7(series.length - 1);
|
|
27
|
+
let peakWeek = 0;
|
|
28
|
+
for (let i = 6; i < series.length; i++) peakWeek = Math.max(peakWeek, rolling7(i));
|
|
29
|
+
return peakWeek > 0 ? latestWeek / peakWeek : null;
|
|
30
|
+
}
|
|
31
|
+
function clamp01(n) {
|
|
32
|
+
if (!Number.isFinite(n)) return 0;
|
|
33
|
+
return Math.min(1, Math.max(0, n));
|
|
34
|
+
}
|
|
35
|
+
function pctStr(n) {
|
|
36
|
+
return `${n >= 0 ? "+" : ""}${Math.round(n)}%`;
|
|
37
|
+
}
|
|
38
|
+
function advance(nextStage, metric, value, target, gapLabel) {
|
|
39
|
+
return {
|
|
40
|
+
nextStage,
|
|
41
|
+
metric,
|
|
42
|
+
value,
|
|
43
|
+
target,
|
|
44
|
+
pct: clamp01(value / target),
|
|
45
|
+
gapLabel,
|
|
46
|
+
direction: "advance"
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function escapeToward(nextStage, metric, value, target, gapLabel) {
|
|
50
|
+
return {
|
|
51
|
+
nextStage,
|
|
52
|
+
metric,
|
|
53
|
+
value,
|
|
54
|
+
target,
|
|
55
|
+
pct: clamp01(value / target),
|
|
56
|
+
gapLabel,
|
|
57
|
+
direction: "escape"
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function escapeReduce(nextStage, metric, value, target, gapLabel) {
|
|
61
|
+
return {
|
|
62
|
+
nextStage,
|
|
63
|
+
metric,
|
|
64
|
+
value,
|
|
65
|
+
target,
|
|
66
|
+
pct: target <= 0 ? value <= 0 ? 1 : 0 : clamp01(2 - value / target),
|
|
67
|
+
gapLabel,
|
|
68
|
+
direction: "escape"
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function sustain(metric, value, gapLabel) {
|
|
72
|
+
return {
|
|
73
|
+
nextStage: null,
|
|
74
|
+
metric,
|
|
75
|
+
value,
|
|
76
|
+
target: value,
|
|
77
|
+
pct: 1,
|
|
78
|
+
gapLabel,
|
|
79
|
+
direction: "sustain"
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const HEALTH_COPY = {
|
|
83
|
+
healthy: {
|
|
84
|
+
summary: "Google can crawl and index the pages that should be indexed.",
|
|
85
|
+
primaryAction: "No indexing cleanup needed — focus on reach."
|
|
86
|
+
},
|
|
87
|
+
crawl_faults: {
|
|
88
|
+
summary: "Real access faults (server errors / broken links) are capping otherwise-indexable pages.",
|
|
89
|
+
primaryAction: "Fix the 5xx and broken URLs; return 410/404 for pages you retire on purpose."
|
|
90
|
+
},
|
|
91
|
+
quality_rejection: {
|
|
92
|
+
summary: "Google is crawling pages and refusing to index them — a soft quality signal it never reports explicitly.",
|
|
93
|
+
primaryAction: "Consolidate or improve the rejected pages, or noindex the thin/low-value set."
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
function isIntentionalRetirementSite(input, notFound, serverError) {
|
|
97
|
+
const type = normalizeSiteType(input.siteType);
|
|
98
|
+
const typeMatches = type === "agency" || type === "portfolio" || type === "other";
|
|
99
|
+
const fingerprint = notFound > 50 && serverError <= Math.max(5, notFound * .1);
|
|
100
|
+
return typeMatches && fingerprint;
|
|
101
|
+
}
|
|
102
|
+
function classifyHealthStage(input) {
|
|
103
|
+
const issues = input.issues ?? [];
|
|
104
|
+
const totalUrls = input.totalUrls ?? 0;
|
|
105
|
+
const noindex = countSearchConsoleIssues(issues, "noindex");
|
|
106
|
+
const notFound = countSearchConsoleIssues(issues, "not_found");
|
|
107
|
+
const softFound = countSearchConsoleIssues(issues, "soft_404");
|
|
108
|
+
const serverError = countSearchConsoleIssues(issues, "server_error", "blocked_robots", "access_denied", "forbidden");
|
|
109
|
+
const crawledNotIndexed = countSearchConsoleIssues(issues, "crawled_not_indexed");
|
|
110
|
+
const intentionalDead = isIntentionalRetirementSite(input, notFound, serverError) ? notFound : 0;
|
|
111
|
+
const indexableUrls = Math.max(1, totalUrls - noindex - intentionalDead);
|
|
112
|
+
const hardBlocks = serverError + (input.crawlAuditBlockerCount ?? 0);
|
|
113
|
+
const faultTarget = Math.max(HARD_BLOCK_FLOOR, indexableUrls * HARD_BLOCK_SHARE);
|
|
114
|
+
if (hardBlocks > faultTarget) {
|
|
115
|
+
const toFix = Math.ceil(hardBlocks - faultTarget);
|
|
116
|
+
return {
|
|
117
|
+
stage: "crawl_faults",
|
|
118
|
+
...HEALTH_COPY.crawl_faults,
|
|
119
|
+
evidence: [{
|
|
120
|
+
label: "Access faults (5xx / broken)",
|
|
121
|
+
value: formatSearchConsoleCount(hardBlocks)
|
|
122
|
+
}],
|
|
123
|
+
progression: escapeReduce("healthy", "access faults", hardBlocks, faultTarget, `${formatSearchConsoleCount(hardBlocks)} faults — fix ~${formatSearchConsoleCount(toFix)} to clear the gate`)
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const rejectPool = crawledNotIndexed + softFound;
|
|
127
|
+
if (totalUrls > 0 && rejectPool > REJECT_MIN && rejectPool / totalUrls >= REJECT_SHARE) {
|
|
128
|
+
const share = rejectPool / totalUrls;
|
|
129
|
+
const toClear = Math.max(0, Math.ceil(rejectPool - REJECT_SHARE * totalUrls));
|
|
130
|
+
return {
|
|
131
|
+
stage: "quality_rejection",
|
|
132
|
+
...HEALTH_COPY.quality_rejection,
|
|
133
|
+
evidence: [{
|
|
134
|
+
label: "Crawled, then refused",
|
|
135
|
+
value: formatSearchConsoleCount(rejectPool)
|
|
136
|
+
}, {
|
|
137
|
+
label: "Share of known URLs",
|
|
138
|
+
value: `${(share * 100).toFixed(0)}%`
|
|
139
|
+
}],
|
|
140
|
+
progression: escapeReduce("healthy", "crawled-rejected share", share, REJECT_SHARE, `${(share * 100).toFixed(0)}% rejected — improve/consolidate ~${formatSearchConsoleCount(toClear)} pages to clear`)
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
stage: "healthy",
|
|
145
|
+
...HEALTH_COPY.healthy,
|
|
146
|
+
evidence: [],
|
|
147
|
+
progression: sustain("indexing health", 1, "Indexable pages are getting indexed — no gate blocking reach.")
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const REACH_COPY = {
|
|
151
|
+
waiting_for_data: {
|
|
152
|
+
summary: "Too new to diagnose — not enough search history yet.",
|
|
153
|
+
primaryAction: "Submit a clean sitemap, add internal links, and give it time."
|
|
154
|
+
},
|
|
155
|
+
emerging: {
|
|
156
|
+
summary: "Early but climbing — real impressions are starting to land.",
|
|
157
|
+
primaryAction: "Keep publishing on the themes already gaining impressions."
|
|
158
|
+
},
|
|
159
|
+
growing: {
|
|
160
|
+
summary: "Discoverable, indexed-enough, and trending up. The next work is expansion, not cleanup.",
|
|
161
|
+
primaryAction: "Expand: striking-distance pages, content gaps, authority."
|
|
162
|
+
},
|
|
163
|
+
plateaued: {
|
|
164
|
+
summary: "Established but flat — visibility is steady, not compounding.",
|
|
165
|
+
primaryAction: "Refresh top pages and open a new content cluster to restart growth."
|
|
166
|
+
},
|
|
167
|
+
declining: {
|
|
168
|
+
summary: "Established visibility is genuinely falling versus the previous period.",
|
|
169
|
+
primaryAction: "Investigate the losing pages, recent releases, and competitor moves before expanding."
|
|
170
|
+
},
|
|
171
|
+
faded: {
|
|
172
|
+
summary: "The site had real reach that has collapsed — it spiked and is now near-invisible in search.",
|
|
173
|
+
primaryAction: "Diagnose the drop (quality, deindexing, lost rankings) — the 90-day total hides it; look at the recent week."
|
|
174
|
+
},
|
|
175
|
+
decayed: {
|
|
176
|
+
summary: "Still shows for old terms but earns almost no clicks — the content has aged out of relevance.",
|
|
177
|
+
primaryAction: "Refresh the decayed top pages, or mark the site low-priority if it is no longer maintained."
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
function reach(stage, evidence, progression) {
|
|
181
|
+
return {
|
|
182
|
+
stage,
|
|
183
|
+
...REACH_COPY[stage],
|
|
184
|
+
evidence,
|
|
185
|
+
progression
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function classifyReachStage(input) {
|
|
189
|
+
const imp28d = input.impressions28d ?? 0;
|
|
190
|
+
const imp12m = input.impressions12m ?? null;
|
|
191
|
+
const clicks28d = input.clicks28d ?? null;
|
|
192
|
+
const clicks90dPct = input.clicksPct90d ?? null;
|
|
193
|
+
const clicks28dPct = input.clicksPct28d ?? null;
|
|
194
|
+
const priorClicks = input.clicksPrior28d ?? null;
|
|
195
|
+
const imp90dPct = input.impressionsPct90d ?? null;
|
|
196
|
+
const posDelta = input.positionDelta90d ?? null;
|
|
197
|
+
const liveness = input.livenessRatio ?? null;
|
|
198
|
+
if (imp12m != null && imp12m < LIFETIME_FLOOR) return reach("waiting_for_data", [{
|
|
199
|
+
label: "Impressions (12m)",
|
|
200
|
+
value: formatSearchConsoleCount(imp12m)
|
|
201
|
+
}], advance("emerging", "lifetime impressions", imp12m, LIFETIME_FLOOR, `${formatSearchConsoleCount(imp12m)} of ${formatSearchConsoleCount(LIFETIME_FLOOR)} lifetime impressions — collecting data, keep indexing`));
|
|
202
|
+
const hadRealReach = (imp12m ?? imp28d) > LIFETIME_FLOOR;
|
|
203
|
+
const isGrowing = clicks90dPct != null && clicks90dPct > GROWTH_CLICKS_PCT || imp90dPct != null && imp90dPct > GROWTH_IMPRESSIONS_PCT && posDelta != null && posDelta < 0;
|
|
204
|
+
if (hadRealReach && liveness != null && liveness < FADED_LIVENESS && !isGrowing) return reach("faded", [{
|
|
205
|
+
label: "Recent week vs peak",
|
|
206
|
+
value: `${(liveness * 100).toFixed(0)}%`
|
|
207
|
+
}], escapeToward("growing", "recent week vs peak (liveness)", liveness, .5, `recent week is ${(liveness * 100).toFixed(0)}% of peak — revive toward ~50%`));
|
|
208
|
+
if (clicks90dPct != null && clicks90dPct < DECLINE_CLICKS_PCT && clicks28dPct != null && clicks28dPct < DECLINE_CLICKS_PCT && priorClicks != null && priorClicks >= MIN_DECLINE_PRIOR_CLICKS) return reach("declining", [{
|
|
209
|
+
label: "Clicks 90d",
|
|
210
|
+
value: pctStr(clicks90dPct)
|
|
211
|
+
}, {
|
|
212
|
+
label: "Clicks 28d",
|
|
213
|
+
value: pctStr(clicks28dPct)
|
|
214
|
+
}], {
|
|
215
|
+
nextStage: "plateaued",
|
|
216
|
+
metric: "90d clicks change",
|
|
217
|
+
value: clicks90dPct,
|
|
218
|
+
target: DECLINE_CLICKS_PCT,
|
|
219
|
+
pct: clamp01(2 - Math.abs(clicks90dPct) / Math.abs(DECLINE_CLICKS_PCT)),
|
|
220
|
+
gapLabel: `down ${pctStr(clicks90dPct)} (90d) — recover clicks above ${pctStr(DECLINE_CLICKS_PCT)} to exit`,
|
|
221
|
+
direction: "escape"
|
|
222
|
+
});
|
|
223
|
+
if (isGrowing) {
|
|
224
|
+
const clickGrowth = clicks90dPct != null && clicks90dPct > GROWTH_CLICKS_PCT;
|
|
225
|
+
const growthPct = clickGrowth ? clicks90dPct : imp90dPct ?? clicks90dPct ?? 0;
|
|
226
|
+
const growthMetric = clickGrowth ? "90d clicks growth" : "90d impressions growth";
|
|
227
|
+
const growthLabel = clickGrowth ? `${pctStr(clicks90dPct)} 90d clicks — comfortably growing; defend & expand` : `${pctStr(growthPct)} 90d impressions — comfortably growing; defend & expand`;
|
|
228
|
+
return reach("growing", [...clicks90dPct != null ? [{
|
|
229
|
+
label: "Clicks 90d",
|
|
230
|
+
value: pctStr(clicks90dPct)
|
|
231
|
+
}] : [], ...imp90dPct != null ? [{
|
|
232
|
+
label: "Impressions 90d",
|
|
233
|
+
value: pctStr(imp90dPct)
|
|
234
|
+
}] : []], sustain(growthMetric, growthPct, growthLabel));
|
|
235
|
+
}
|
|
236
|
+
const ctr = clicks28d != null && imp28d > 0 ? clicks28d / imp28d : null;
|
|
237
|
+
if (hadRealReach && imp28d >= NASCENT_IMPRESSIONS_28D && ctr != null && ctr < DECAY_CTR) return reach("decayed", [{
|
|
238
|
+
label: "Impressions (28d)",
|
|
239
|
+
value: formatSearchConsoleCount(imp28d)
|
|
240
|
+
}, {
|
|
241
|
+
label: "Clicks (28d)",
|
|
242
|
+
value: formatSearchConsoleCount(clicks28d ?? 0)
|
|
243
|
+
}], escapeToward("growing", "CTR (clicks/impressions)", ctr, DECAY_CTR, `${(ctr * 100).toFixed(2)}% CTR on ${formatSearchConsoleCount(imp28d)} impressions — refresh content toward ~${(DECAY_CTR * 100).toFixed(1)}%`));
|
|
244
|
+
const growthVal = clicks90dPct ?? 0;
|
|
245
|
+
const growthGap = Math.max(0, GROWTH_CLICKS_PCT - growthVal);
|
|
246
|
+
const growthProgression = (stage) => advance("growing", "90d clicks growth", growthVal, GROWTH_CLICKS_PCT, clicks90dPct != null ? `${pctStr(growthVal)} 90d clicks — need ${pctStr(growthGap)} more to clear the +${GROWTH_CLICKS_PCT}% growth bar` : `${stage === "plateaued" ? "flat" : "rising"} — reach +${GROWTH_CLICKS_PCT}% 90d clicks growth to break into growing`);
|
|
247
|
+
if (imp28d >= ESTABLISHED_IMPRESSIONS_28D) return reach("plateaued", [{
|
|
248
|
+
label: "Impressions (28d)",
|
|
249
|
+
value: formatSearchConsoleCount(imp28d)
|
|
250
|
+
}], growthProgression("plateaued"));
|
|
251
|
+
if (imp28d < NASCENT_IMPRESSIONS_28D) return reach("emerging", [{
|
|
252
|
+
label: "Impressions (28d)",
|
|
253
|
+
value: formatSearchConsoleCount(imp28d)
|
|
254
|
+
}], growthProgression("emerging"));
|
|
255
|
+
return reach("plateaued", [{
|
|
256
|
+
label: "Impressions (28d)",
|
|
257
|
+
value: formatSearchConsoleCount(imp28d)
|
|
258
|
+
}], growthProgression("plateaued"));
|
|
259
|
+
}
|
|
260
|
+
function classifySiteTriage(input) {
|
|
261
|
+
const health = classifyHealthStage(input);
|
|
262
|
+
return {
|
|
263
|
+
reach: classifyReachStage(input),
|
|
264
|
+
health,
|
|
265
|
+
headline: health.stage === "healthy" ? "reach" : "health"
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
export { classifyHealthStage, classifyReachStage, classifySiteTriage, reachLivenessRatio };
|
package/dist/v1/index.d.mts
CHANGED
|
@@ -92,7 +92,7 @@ interface GscdumpV1Client {
|
|
|
92
92
|
/** Create one framework-neutral client whose behavior is driven by the v1 registry. */
|
|
93
93
|
declare function createGscdumpV1Client(options: CreateGscdumpV1ClientOptions): GscdumpV1Client;
|
|
94
94
|
type MaybePromise<T> = T | Promise<T>;
|
|
95
|
-
declare const GSCDUMP_REALTIME_V1_SDK_VERSION: "1.0.
|
|
95
|
+
declare const GSCDUMP_REALTIME_V1_SDK_VERSION: "1.0.2";
|
|
96
96
|
type GscdumpRealtimeV1TransportState = 'idle' | 'ticketing' | 'connecting' | 'handshaking' | 'replaying' | 'live' | 'waiting' | 'stopped' | 'terminal';
|
|
97
97
|
type GscdumpRealtimeV1Freshness = 'unknown' | 'stale' | 'applying' | 'fresh' | 'resyncing' | 'degraded';
|
|
98
98
|
type GscdumpRealtimeV1ErrorCode = 'cursor_store_failed' | 'effect_failed' | 'heartbeat_stale' | 'integration_failed' | 'protocol_error' | 'resync_failed' | 'runtime_unavailable' | 'socket_error' | 'ticket_invalid' | 'ticket_provider_failed' | 'upgrade_rejected';
|
package/dist/v1/index.mjs
CHANGED
|
@@ -395,7 +395,7 @@ function utf8Size(value) {
|
|
|
395
395
|
}
|
|
396
396
|
return bytes;
|
|
397
397
|
}
|
|
398
|
-
const GSCDUMP_REALTIME_V1_SDK_VERSION = "1.0.
|
|
398
|
+
const GSCDUMP_REALTIME_V1_SDK_VERSION = "1.0.2";
|
|
399
399
|
var GscdumpRealtimeV1Error = class extends Error {
|
|
400
400
|
tag = "GscdumpRealtimeV1Error";
|
|
401
401
|
code;
|
|
@@ -482,7 +482,7 @@ function createGscdumpRealtimeV1Client(options) {
|
|
|
482
482
|
const protocol = createGscdumpV1Protocol();
|
|
483
483
|
const runtime = options.runtime ?? defaultRuntime();
|
|
484
484
|
const cursorStore = options.cursorStore ?? createMemoryCursorStore();
|
|
485
|
-
const sdkVersion = options.sdkVersion ?? "1.0.
|
|
485
|
+
const sdkVersion = options.sdkVersion ?? "1.0.2";
|
|
486
486
|
if (!sdkVersion) throw new TypeError("sdkVersion cannot be empty.");
|
|
487
487
|
let running = false;
|
|
488
488
|
let epoch = 0;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { PartnerApiError } from "./_chunks/errors.mjs";
|
|
2
|
+
import { Result } from "gscdump/result";
|
|
3
|
+
import { CANONICAL_WEBHOOK_EVENTS, CreateWebhookEnvelopeOptions, PartnerWebhookHeaders, VALID_WEBHOOK_EVENTS, WEBHOOK_CONTRACT_VERSION, WEBHOOK_CONTRACT_VERSION_HEADER, WEBHOOK_DELIVERY_HEADER, WEBHOOK_EVENT_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, WebhookEnvelope } from "@gscdump/contracts";
|
|
4
|
+
declare function generateWebhookSecret(): string;
|
|
5
|
+
declare function generateWebhookDeliveryId(): string;
|
|
6
|
+
declare function shouldQueueWebhook(webhookEvents: string | string[] | null | undefined, eventType: string): boolean;
|
|
7
|
+
declare function createWebhookEnvelope<TData extends Record<string, unknown>>(options: CreateWebhookEnvelopeOptions<TData>): WebhookEnvelope<TData>;
|
|
8
|
+
declare function signWebhookPayload(payload: string | object, secret: string): Promise<string>;
|
|
9
|
+
declare function verifyWebhookSignature(payload: string | object, signature: string | null | undefined, secret: string): Promise<boolean>;
|
|
10
|
+
/**
|
|
11
|
+
* Errors-as-values core for {@link parseWebhookPayload}: a failed HMAC signature
|
|
12
|
+
* check is a caller-actionable `auth` (401) failure at the webhook boundary, so
|
|
13
|
+
* it is returned as a modelled `PartnerApiError` rather than only thrown. A
|
|
14
|
+
* malformed envelope (schema parse) is a defect and keeps propagating.
|
|
15
|
+
*/
|
|
16
|
+
declare function parseWebhookPayloadResult<TData extends Record<string, unknown> = Record<string, unknown>>(payload: string | object, options?: {
|
|
17
|
+
secret?: string;
|
|
18
|
+
signature?: string | null;
|
|
19
|
+
headers?: PartnerWebhookHeaders | Headers;
|
|
20
|
+
validateSignature?: boolean;
|
|
21
|
+
}): Promise<Result<WebhookEnvelope<TData>, PartnerApiError>>;
|
|
22
|
+
declare function parseWebhookPayload<TData extends Record<string, unknown> = Record<string, unknown>>(payload: string | object, options?: {
|
|
23
|
+
secret?: string;
|
|
24
|
+
signature?: string | null;
|
|
25
|
+
headers?: PartnerWebhookHeaders | Headers;
|
|
26
|
+
validateSignature?: boolean;
|
|
27
|
+
}): Promise<WebhookEnvelope<TData>>;
|
|
28
|
+
declare function readWebhookHeaders(headers: Headers | PartnerWebhookHeaders | null | undefined): Required<PartnerWebhookHeaders>;
|
|
29
|
+
export { CANONICAL_WEBHOOK_EVENTS, VALID_WEBHOOK_EVENTS, WEBHOOK_CONTRACT_VERSION, WEBHOOK_CONTRACT_VERSION_HEADER, WEBHOOK_DELIVERY_HEADER, WEBHOOK_EVENT_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, createWebhookEnvelope, generateWebhookDeliveryId, generateWebhookSecret, parseWebhookPayload, parseWebhookPayloadResult, readWebhookHeaders, shouldQueueWebhook, signWebhookPayload, verifyWebhookSignature };
|
package/dist/webhook.mjs
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { PartnerApiError, partnerErrorToException } from "./errors.mjs";
|
|
2
|
+
import { err, ok, unwrapResult } from "gscdump/result";
|
|
3
|
+
import { CANONICAL_WEBHOOK_EVENTS, VALID_WEBHOOK_EVENTS, WEBHOOK_CONTRACT_VERSION, WEBHOOK_CONTRACT_VERSION as WEBHOOK_CONTRACT_VERSION$1, WEBHOOK_CONTRACT_VERSION_HEADER, WEBHOOK_CONTRACT_VERSION_HEADER as WEBHOOK_CONTRACT_VERSION_HEADER$1, WEBHOOK_DELIVERY_HEADER, WEBHOOK_DELIVERY_HEADER as WEBHOOK_DELIVERY_HEADER$1, WEBHOOK_EVENT_HEADER, WEBHOOK_EVENT_HEADER as WEBHOOK_EVENT_HEADER$1, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_SIGNATURE_HEADER as WEBHOOK_SIGNATURE_HEADER$1, WEBHOOK_TIMESTAMP_HEADER, WEBHOOK_TIMESTAMP_HEADER as WEBHOOK_TIMESTAMP_HEADER$1, partnerWebhookEnvelopeSchema } from "@gscdump/contracts";
|
|
4
|
+
const CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
5
|
+
const SECRET_LENGTH = 32;
|
|
6
|
+
const encoder = new TextEncoder();
|
|
7
|
+
function randomString(length) {
|
|
8
|
+
const bytes = crypto.getRandomValues(new Uint8Array(length));
|
|
9
|
+
let value = "";
|
|
10
|
+
for (let i = 0; i < length; i++) value += CHARSET[bytes[i] % 62];
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function toPayloadString(payload) {
|
|
14
|
+
return typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
15
|
+
}
|
|
16
|
+
function bytesToHex(bytes) {
|
|
17
|
+
return Array.from(new Uint8Array(bytes)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
18
|
+
}
|
|
19
|
+
function hexToBytes(hex) {
|
|
20
|
+
if (!/^[\da-f]+$/i.test(hex) || hex.length % 2 !== 0) return null;
|
|
21
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
22
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
23
|
+
return bytes;
|
|
24
|
+
}
|
|
25
|
+
function constantTimeEqual(a, b) {
|
|
26
|
+
if (a.length !== b.length) return false;
|
|
27
|
+
let diff = 0;
|
|
28
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
29
|
+
return diff === 0;
|
|
30
|
+
}
|
|
31
|
+
function signatureHex(signature) {
|
|
32
|
+
const trimmed = signature.trim();
|
|
33
|
+
return trimmed.startsWith("sha256=") ? trimmed.slice(7) : null;
|
|
34
|
+
}
|
|
35
|
+
async function hmacSha256(payload, secret) {
|
|
36
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
37
|
+
name: "HMAC",
|
|
38
|
+
hash: "SHA-256"
|
|
39
|
+
}, false, ["sign"]);
|
|
40
|
+
return crypto.subtle.sign("HMAC", key, encoder.encode(payload));
|
|
41
|
+
}
|
|
42
|
+
function generateWebhookSecret() {
|
|
43
|
+
return `whsec_${randomString(SECRET_LENGTH)}`;
|
|
44
|
+
}
|
|
45
|
+
function generateWebhookDeliveryId() {
|
|
46
|
+
return `whd_${crypto.randomUUID()}`;
|
|
47
|
+
}
|
|
48
|
+
function shouldQueueWebhook(webhookEvents, eventType) {
|
|
49
|
+
if (!webhookEvents) return false;
|
|
50
|
+
return (Array.isArray(webhookEvents) ? webhookEvents : JSON.parse(webhookEvents)).includes(eventType);
|
|
51
|
+
}
|
|
52
|
+
function createWebhookEnvelope(options) {
|
|
53
|
+
const occurredAt = options.occurredAt instanceof Date ? options.occurredAt.toISOString() : options.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
54
|
+
return {
|
|
55
|
+
contractVersion: options.contractVersion ?? WEBHOOK_CONTRACT_VERSION$1,
|
|
56
|
+
deliveryId: options.deliveryId ?? generateWebhookDeliveryId(),
|
|
57
|
+
event: options.event,
|
|
58
|
+
partnerId: options.partnerId,
|
|
59
|
+
userId: options.userId,
|
|
60
|
+
siteId: options.siteId,
|
|
61
|
+
externalUserId: options.externalUserId ?? null,
|
|
62
|
+
externalSiteId: options.externalSiteId ?? null,
|
|
63
|
+
lifecycleRevision: options.lifecycleRevision,
|
|
64
|
+
occurredAt,
|
|
65
|
+
data: options.data
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async function signWebhookPayload(payload, secret) {
|
|
69
|
+
return `sha256=${bytesToHex(await hmacSha256(toPayloadString(payload), secret))}`;
|
|
70
|
+
}
|
|
71
|
+
async function verifyWebhookSignature(payload, signature, secret) {
|
|
72
|
+
if (!signature) return false;
|
|
73
|
+
const hex = signatureHex(signature);
|
|
74
|
+
if (!hex) return false;
|
|
75
|
+
const received = hexToBytes(hex);
|
|
76
|
+
if (!received) return false;
|
|
77
|
+
return constantTimeEqual(new Uint8Array(await hmacSha256(toPayloadString(payload), secret)), received);
|
|
78
|
+
}
|
|
79
|
+
async function parseWebhookPayloadResult(payload, options = {}) {
|
|
80
|
+
const payloadString = toPayloadString(payload);
|
|
81
|
+
const signature = options.signature ?? readWebhookHeaders(options.headers).signature;
|
|
82
|
+
if (options.secret && (options.validateSignature ?? true)) {
|
|
83
|
+
if (!await verifyWebhookSignature(payloadString, signature, options.secret)) return err(new PartnerApiError({
|
|
84
|
+
kind: "auth",
|
|
85
|
+
statusCode: 401,
|
|
86
|
+
message: "Invalid webhook signature"
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
const parsed = typeof payload === "string" ? JSON.parse(payload) : payload;
|
|
90
|
+
return ok(partnerWebhookEnvelopeSchema.parse(parsed));
|
|
91
|
+
}
|
|
92
|
+
async function parseWebhookPayload(payload, options = {}) {
|
|
93
|
+
return unwrapResult(await parseWebhookPayloadResult(payload, options), partnerErrorToException);
|
|
94
|
+
}
|
|
95
|
+
function readWebhookHeaders(headers) {
|
|
96
|
+
if (!headers) return {
|
|
97
|
+
event: null,
|
|
98
|
+
delivery: null,
|
|
99
|
+
contractVersion: null,
|
|
100
|
+
timestamp: null,
|
|
101
|
+
signature: null
|
|
102
|
+
};
|
|
103
|
+
if (headers instanceof Headers) return {
|
|
104
|
+
event: headers.get(WEBHOOK_EVENT_HEADER$1),
|
|
105
|
+
delivery: headers.get(WEBHOOK_DELIVERY_HEADER$1),
|
|
106
|
+
contractVersion: headers.get(WEBHOOK_CONTRACT_VERSION_HEADER$1),
|
|
107
|
+
timestamp: headers.get(WEBHOOK_TIMESTAMP_HEADER$1),
|
|
108
|
+
signature: headers.get(WEBHOOK_SIGNATURE_HEADER$1)
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
event: headers.event ?? null,
|
|
112
|
+
delivery: headers.delivery ?? null,
|
|
113
|
+
contractVersion: headers.contractVersion ?? null,
|
|
114
|
+
timestamp: headers.timestamp ?? null,
|
|
115
|
+
signature: headers.signature ?? null
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export { CANONICAL_WEBHOOK_EVENTS, VALID_WEBHOOK_EVENTS, WEBHOOK_CONTRACT_VERSION, WEBHOOK_CONTRACT_VERSION_HEADER, WEBHOOK_DELIVERY_HEADER, WEBHOOK_EVENT_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, createWebhookEnvelope, generateWebhookDeliveryId, generateWebhookSecret, parseWebhookPayload, parseWebhookPayloadResult, readWebhookHeaders, shouldQueueWebhook, signWebhookPayload, verifyWebhookSignature };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gscdump/sdk",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.3",
|
|
5
5
|
"description": "Consumer SDK for hosted gscdump.com integrations.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -26,6 +26,111 @@
|
|
|
26
26
|
"import": "./dist/index.mjs",
|
|
27
27
|
"default": "./dist/index.mjs"
|
|
28
28
|
},
|
|
29
|
+
"./analytics": {
|
|
30
|
+
"types": "./dist/analytics-client.d.mts",
|
|
31
|
+
"import": "./dist/analytics-client.mjs",
|
|
32
|
+
"default": "./dist/analytics-client.mjs"
|
|
33
|
+
},
|
|
34
|
+
"./analyzers": {
|
|
35
|
+
"types": "./dist/analyzer-defs.d.mts",
|
|
36
|
+
"import": "./dist/analyzer-defs.mjs",
|
|
37
|
+
"default": "./dist/analyzer-defs.mjs"
|
|
38
|
+
},
|
|
39
|
+
"./anonymization": {
|
|
40
|
+
"types": "./dist/anonymization.d.mts",
|
|
41
|
+
"import": "./dist/anonymization.mjs",
|
|
42
|
+
"default": "./dist/anonymization.mjs"
|
|
43
|
+
},
|
|
44
|
+
"./archetype": {
|
|
45
|
+
"types": "./dist/archetype-compile.d.mts",
|
|
46
|
+
"import": "./dist/archetype-compile.mjs",
|
|
47
|
+
"default": "./dist/archetype-compile.mjs"
|
|
48
|
+
},
|
|
49
|
+
"./partner": {
|
|
50
|
+
"types": "./dist/client.d.mts",
|
|
51
|
+
"import": "./dist/client.mjs",
|
|
52
|
+
"default": "./dist/client.mjs"
|
|
53
|
+
},
|
|
54
|
+
"./country": {
|
|
55
|
+
"types": "./dist/country-names.d.mts",
|
|
56
|
+
"import": "./dist/country-names.mjs",
|
|
57
|
+
"default": "./dist/country-names.mjs"
|
|
58
|
+
},
|
|
59
|
+
"./cwv": {
|
|
60
|
+
"types": "./dist/cwv-thresholds.d.mts",
|
|
61
|
+
"import": "./dist/cwv-thresholds.mjs",
|
|
62
|
+
"default": "./dist/cwv-thresholds.mjs"
|
|
63
|
+
},
|
|
64
|
+
"./partner-errors": {
|
|
65
|
+
"types": "./dist/errors.d.mts",
|
|
66
|
+
"import": "./dist/errors.mjs",
|
|
67
|
+
"default": "./dist/errors.mjs"
|
|
68
|
+
},
|
|
69
|
+
"./gsc-console-url": {
|
|
70
|
+
"types": "./dist/gsc-console-url.d.mts",
|
|
71
|
+
"import": "./dist/gsc-console-url.mjs",
|
|
72
|
+
"default": "./dist/gsc-console-url.mjs"
|
|
73
|
+
},
|
|
74
|
+
"./gsc-constants": {
|
|
75
|
+
"types": "./dist/gsc-constants.d.mts",
|
|
76
|
+
"import": "./dist/gsc-constants.mjs",
|
|
77
|
+
"default": "./dist/gsc-constants.mjs"
|
|
78
|
+
},
|
|
79
|
+
"./gsc-error": {
|
|
80
|
+
"types": "./dist/gsc-error.d.mts",
|
|
81
|
+
"import": "./dist/gsc-error.mjs",
|
|
82
|
+
"default": "./dist/gsc-error.mjs"
|
|
83
|
+
},
|
|
84
|
+
"./period-presets": {
|
|
85
|
+
"types": "./dist/gsc-period-presets.d.mts",
|
|
86
|
+
"import": "./dist/gsc-period-presets.mjs",
|
|
87
|
+
"default": "./dist/gsc-period-presets.mjs"
|
|
88
|
+
},
|
|
89
|
+
"./rows": {
|
|
90
|
+
"types": "./dist/gsc-rows.d.mts",
|
|
91
|
+
"import": "./dist/gsc-rows.mjs",
|
|
92
|
+
"default": "./dist/gsc-rows.mjs"
|
|
93
|
+
},
|
|
94
|
+
"./hosted-query": {
|
|
95
|
+
"types": "./dist/hosted-query.d.mts",
|
|
96
|
+
"import": "./dist/hosted-query.mjs",
|
|
97
|
+
"default": "./dist/hosted-query.mjs"
|
|
98
|
+
},
|
|
99
|
+
"./indexing-issues": {
|
|
100
|
+
"types": "./dist/indexing-issues.d.mts",
|
|
101
|
+
"import": "./dist/indexing-issues.mjs",
|
|
102
|
+
"default": "./dist/indexing-issues.mjs"
|
|
103
|
+
},
|
|
104
|
+
"./lifecycle": {
|
|
105
|
+
"types": "./dist/lifecycle.d.mts",
|
|
106
|
+
"import": "./dist/lifecycle.mjs",
|
|
107
|
+
"default": "./dist/lifecycle.mjs"
|
|
108
|
+
},
|
|
109
|
+
"./period": {
|
|
110
|
+
"types": "./dist/period.d.mts",
|
|
111
|
+
"import": "./dist/period.mjs",
|
|
112
|
+
"default": "./dist/period.mjs"
|
|
113
|
+
},
|
|
114
|
+
"./search-console-stage": {
|
|
115
|
+
"types": "./dist/search-console-stage.d.mts",
|
|
116
|
+
"import": "./dist/search-console-stage.mjs",
|
|
117
|
+
"default": "./dist/search-console-stage.mjs"
|
|
118
|
+
},
|
|
119
|
+
"./site-baseline": {
|
|
120
|
+
"types": "./dist/site-baseline.d.mts",
|
|
121
|
+
"import": "./dist/site-baseline.mjs",
|
|
122
|
+
"default": "./dist/site-baseline.mjs"
|
|
123
|
+
},
|
|
124
|
+
"./site-triage": {
|
|
125
|
+
"types": "./dist/site-triage.d.mts",
|
|
126
|
+
"import": "./dist/site-triage.mjs",
|
|
127
|
+
"default": "./dist/site-triage.mjs"
|
|
128
|
+
},
|
|
129
|
+
"./webhook": {
|
|
130
|
+
"types": "./dist/webhook.d.mts",
|
|
131
|
+
"import": "./dist/webhook.mjs",
|
|
132
|
+
"default": "./dist/webhook.mjs"
|
|
133
|
+
},
|
|
29
134
|
"./v1": {
|
|
30
135
|
"types": "./dist/v1/index.d.mts",
|
|
31
136
|
"import": "./dist/v1/index.mjs",
|
|
@@ -44,10 +149,10 @@
|
|
|
44
149
|
"date-fns": "^4.4.0",
|
|
45
150
|
"ofetch": "^1.5.1",
|
|
46
151
|
"zod": "^4.4.3",
|
|
47
|
-
"@gscdump/
|
|
48
|
-
"@gscdump/engine": "^1.0.
|
|
49
|
-
"gscdump": "^1.0.
|
|
50
|
-
"
|
|
152
|
+
"@gscdump/analysis": "^1.0.3",
|
|
153
|
+
"@gscdump/engine": "^1.0.3",
|
|
154
|
+
"@gscdump/contracts": "^1.0.3",
|
|
155
|
+
"gscdump": "^1.0.3"
|
|
51
156
|
},
|
|
52
157
|
"devDependencies": {
|
|
53
158
|
"typescript": "^6.0.3",
|