@gscdump/sdk 1.4.0 → 1.4.2

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.
@@ -0,0 +1,427 @@
1
+ var GscdumpV1Error = class extends Error {
2
+ tag = "GscdumpV1Error";
3
+ code;
4
+ status;
5
+ requestId;
6
+ retryable;
7
+ details;
8
+ cause;
9
+ constructor(options) {
10
+ super(options.message);
11
+ this.name = "GscdumpV1Error";
12
+ this.code = options.code;
13
+ this.status = options.status;
14
+ this.requestId = options.requestId;
15
+ this.retryable = options.retryable;
16
+ this.details = options.details;
17
+ this.cause = options.cause;
18
+ }
19
+ };
20
+ function isGscdumpV1Error(error) {
21
+ return error instanceof GscdumpV1Error;
22
+ }
23
+ const DEFAULT_API_ROOT = "https://gscdump.com/api";
24
+ const API_PREFIX_RE = /^\/api(?=\/)/;
25
+ const TRAILING_SLASH_RE = /\/+$/;
26
+ const LEADING_SLASH_RE = /^\/+/;
27
+ function validationDetails(cause) {
28
+ if (typeof cause === "object" && cause !== null && "issues" in cause) return { issues: cause.issues };
29
+ return {};
30
+ }
31
+ function requestValidationError(operationId, location, cause) {
32
+ return new GscdumpV1Error({
33
+ code: "request_validation",
34
+ message: `${operationId}: invalid request ${location}`,
35
+ retryable: false,
36
+ details: {
37
+ location,
38
+ ...validationDetails(cause)
39
+ },
40
+ cause
41
+ });
42
+ }
43
+ function responseValidationError(operationId, status, requestId, cause) {
44
+ return new GscdumpV1Error({
45
+ code: "response_validation",
46
+ message: `${operationId}: response ${status} did not match the v1 contract`,
47
+ status,
48
+ requestId,
49
+ retryable: false,
50
+ details: validationDetails(cause),
51
+ cause
52
+ });
53
+ }
54
+ function clampInteger(value, fallback, min, max) {
55
+ if (value === void 0) return fallback;
56
+ if (!Number.isFinite(value)) throw new TypeError("Retry configuration values must be finite numbers.");
57
+ return Math.min(max, Math.max(min, Math.trunc(value)));
58
+ }
59
+ function resolveRetryOptions(options) {
60
+ const baseDelayMs = clampInteger(options?.baseDelayMs, 250, 0, 6e4);
61
+ const maxDelayMs = clampInteger(options?.maxDelayMs, 2e3, baseDelayMs, 3e5);
62
+ return {
63
+ maxAttempts: clampInteger(options?.maxAttempts, 3, 1, 5),
64
+ baseDelayMs,
65
+ maxDelayMs
66
+ };
67
+ }
68
+ function resolveApiUrl(apiRoot, contractPath) {
69
+ const relative = contractPath.replace(API_PREFIX_RE, "").replace(LEADING_SLASH_RE, "");
70
+ return `${apiRoot.replace(TRAILING_SLASH_RE, "")}/${relative}`;
71
+ }
72
+ function appendQueryValue(search, key, value) {
73
+ if (value === void 0) return;
74
+ if (Array.isArray(value)) {
75
+ for (const item of value) appendQueryValue(search, key, item);
76
+ return;
77
+ }
78
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
79
+ search.append(key, value === null ? "null" : String(value));
80
+ return;
81
+ }
82
+ search.append(key, JSON.stringify(value));
83
+ }
84
+ function appendQuery(url, query) {
85
+ if (query === void 0 || query === null) return url;
86
+ if (typeof query !== "object" || Array.isArray(query)) throw new TypeError("A query schema must produce an object.");
87
+ const search = new URLSearchParams();
88
+ for (const key of Object.keys(query).sort()) appendQueryValue(search, key, query[key]);
89
+ const serialized = search.toString();
90
+ return serialized ? `${url}?${serialized}` : url;
91
+ }
92
+ function assertAbsentLocation(operationId, location, value) {
93
+ if (value !== void 0) throw requestValidationError(operationId, location, /* @__PURE__ */ new TypeError(`${location} is not accepted by this operation.`));
94
+ }
95
+ function parseLocation(operation, location, value) {
96
+ const schema = operation.request[location];
97
+ if (schema === null) {
98
+ assertAbsentLocation(operation.id, location, value);
99
+ return;
100
+ }
101
+ try {
102
+ return schema.parse(location === "headers" && value === void 0 ? {} : value);
103
+ } catch (cause) {
104
+ throw requestValidationError(operation.id, location, cause);
105
+ }
106
+ }
107
+ function prepareRequest(buildOperationPath, surface, operation, input, options) {
108
+ let path;
109
+ if (operation.request.params === null) {
110
+ assertAbsentLocation(operation.id, "params", input.params);
111
+ path = buildOperationPath(surface, operation);
112
+ } else try {
113
+ path = buildOperationPath(surface, operation, input.params);
114
+ } catch (cause) {
115
+ throw requestValidationError(operation.id, "params", cause);
116
+ }
117
+ const inputHeaders = input.headers;
118
+ const headerRequestId = inputHeaders?.["x-request-id"];
119
+ if (options.requestId && headerRequestId && options.requestId !== headerRequestId) throw requestValidationError(operation.id, "headers", /* @__PURE__ */ new TypeError("requestId conflicts with headers.x-request-id."));
120
+ const requestHeaders = options.requestId ? {
121
+ ...inputHeaders ?? {},
122
+ "x-request-id": options.requestId
123
+ } : input.headers;
124
+ return {
125
+ path,
126
+ query: parseLocation(operation, "query", input.query),
127
+ headers: parseLocation(operation, "headers", requestHeaders),
128
+ body: parseLocation(operation, "body", input.body)
129
+ };
130
+ }
131
+ async function resolveCredential(resolver) {
132
+ let credential;
133
+ try {
134
+ credential = typeof resolver === "function" ? await resolver() : resolver;
135
+ } catch (cause) {
136
+ throw new GscdumpV1Error({
137
+ code: "credential_resolution",
138
+ message: "Could not resolve the v1 Bearer credential.",
139
+ retryable: false,
140
+ details: {},
141
+ cause
142
+ });
143
+ }
144
+ if (typeof credential !== "string" || !credential.trim()) throw new GscdumpV1Error({
145
+ code: "credential_resolution",
146
+ message: "The v1 Bearer credential cannot be empty.",
147
+ retryable: false,
148
+ details: {}
149
+ });
150
+ return credential;
151
+ }
152
+ async function resolveBaseHeaders(resolver) {
153
+ return typeof resolver === "function" ? await resolver() : resolver;
154
+ }
155
+ async function buildRequestHeaders(options, operation, prepared, executeOptions) {
156
+ try {
157
+ const [baseHeaders, credential] = await Promise.all([resolveBaseHeaders(options.headers), resolveCredential(options.credential)]);
158
+ const headers = new Headers(baseHeaders);
159
+ for (const [key, value] of Object.entries(prepared.headers)) if (value !== void 0) headers.set(key, String(value));
160
+ headers.delete("x-api-key");
161
+ headers.set("accept", "application/json");
162
+ headers.set("authorization", `Bearer ${credential}`);
163
+ if (executeOptions.idempotencyKey) headers.set("idempotency-key", executeOptions.idempotencyKey);
164
+ if (prepared.body !== void 0) headers.set("content-type", "application/json");
165
+ return headers;
166
+ } catch (cause) {
167
+ if (cause instanceof GscdumpV1Error) throw cause;
168
+ throw requestValidationError(operation.id, "headers", cause);
169
+ }
170
+ }
171
+ function isAbortError(error, signal) {
172
+ return signal?.aborted === true || typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
173
+ }
174
+ function abortedRequestError(operation, executeOptions, cause = executeOptions.signal?.reason) {
175
+ return new GscdumpV1Error({
176
+ code: "aborted",
177
+ message: `${operation.id}: request aborted`,
178
+ requestId: executeOptions.requestId,
179
+ retryable: false,
180
+ details: {},
181
+ cause
182
+ });
183
+ }
184
+ async function waitForRetry(ms, operation, executeOptions) {
185
+ try {
186
+ await sleep(ms, executeOptions.signal);
187
+ } catch (cause) {
188
+ if (isAbortError(cause, executeOptions.signal)) throw abortedRequestError(operation, executeOptions, cause);
189
+ throw cause;
190
+ }
191
+ }
192
+ function awaitWithAbort(promise, operation, executeOptions) {
193
+ const signal = executeOptions.signal;
194
+ if (!signal) return promise;
195
+ if (signal.aborted) return Promise.reject(abortedRequestError(operation, executeOptions));
196
+ return new Promise((resolve, reject) => {
197
+ function onAbort() {
198
+ reject(abortedRequestError(operation, executeOptions));
199
+ }
200
+ signal.addEventListener("abort", onAbort, { once: true });
201
+ promise.then((value) => {
202
+ signal.removeEventListener("abort", onAbort);
203
+ resolve(value);
204
+ }, (cause) => {
205
+ signal.removeEventListener("abort", onAbort);
206
+ reject(cause);
207
+ });
208
+ });
209
+ }
210
+ function parseRetryAfter(value, now) {
211
+ if (value === null) return void 0;
212
+ const seconds = Number(value);
213
+ if (Number.isFinite(seconds) && seconds >= 0) return Math.ceil(seconds * 1e3);
214
+ const date = Date.parse(value);
215
+ if (Number.isNaN(date)) return void 0;
216
+ return Math.max(0, date - now);
217
+ }
218
+ function retryDelay(attempt, retryAfter, options) {
219
+ const exponential = Math.min(options.maxDelayMs, options.baseDelayMs * 2 ** Math.max(0, attempt - 1));
220
+ return Math.max(exponential, parseRetryAfter(retryAfter, Date.now()) ?? 0);
221
+ }
222
+ function sleep(ms, signal) {
223
+ if (signal?.aborted) return Promise.reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError"));
224
+ return new Promise((resolve, reject) => {
225
+ const handle = setTimeout(() => {
226
+ signal?.removeEventListener("abort", onAbort);
227
+ resolve();
228
+ }, ms);
229
+ function onAbort() {
230
+ clearTimeout(handle);
231
+ reject(signal?.reason ?? new DOMException("The operation was aborted.", "AbortError"));
232
+ }
233
+ signal?.addEventListener("abort", onAbort, { once: true });
234
+ });
235
+ }
236
+ async function readJson(response) {
237
+ const text = await response.text();
238
+ if (!text) return null;
239
+ return JSON.parse(text);
240
+ }
241
+ function requestIdFrom(response) {
242
+ return response.headers.get("x-request-id") ?? void 0;
243
+ }
244
+ function operationLookup(protocol) {
245
+ const operations = /* @__PURE__ */ new Map();
246
+ for (const surface of Object.values(protocol.surfaces)) for (const operation of Object.values(surface.operations)) operations.set(operation.id, {
247
+ operation,
248
+ surface
249
+ });
250
+ return operations;
251
+ }
252
+ function createGscdumpV1Client(options) {
253
+ const retryOptions = resolveRetryOptions(options.retry);
254
+ const apiRoot = options.apiRoot ?? DEFAULT_API_ROOT;
255
+ const fetchImpl = options.fetch ?? globalThis.fetch;
256
+ let runtimePromise;
257
+ if (typeof fetchImpl !== "function") throw new TypeError("createGscdumpV1Client requires a fetch implementation in this runtime.");
258
+ function getRuntime() {
259
+ return runtimePromise ??= import("@gscdump/contracts/v1/http").then(({ buildHttpOperationPath: buildOperationPath, createGscdumpV1Protocol }) => {
260
+ const protocol = createGscdumpV1Protocol();
261
+ return {
262
+ buildOperationPath,
263
+ operations: operationLookup(protocol),
264
+ protocol
265
+ };
266
+ });
267
+ }
268
+ const execute = executeUntyped;
269
+ async function executeUntyped(operationId, input, executeOptions = {}) {
270
+ const { buildOperationPath, operations, protocol } = await getRuntime();
271
+ const entry = operations.get(operationId);
272
+ if (!entry) throw new GscdumpV1Error({
273
+ code: "request_validation",
274
+ message: `Unknown gscdump v1 operation: ${operationId}`,
275
+ retryable: false,
276
+ details: { operationId }
277
+ });
278
+ const { operation, surface } = entry;
279
+ if (typeof input !== "object" || input === null || Array.isArray(input)) throw requestValidationError(operation.id, "input", /* @__PURE__ */ new TypeError("input must be an object."));
280
+ const prepared = prepareRequest(buildOperationPath, surface, operation, input, executeOptions);
281
+ let url;
282
+ try {
283
+ url = appendQuery(resolveApiUrl(apiRoot, prepared.path), prepared.query);
284
+ } catch (cause) {
285
+ throw requestValidationError(operation.id, "query", cause);
286
+ }
287
+ const canRetry = operation.semantics.retry === "idempotent";
288
+ for (let attempt = 1; attempt <= retryOptions.maxAttempts; attempt++) {
289
+ if (executeOptions.signal?.aborted) throw abortedRequestError(operation, executeOptions);
290
+ const headers = await awaitWithAbort(buildRequestHeaders(options, operation, prepared, executeOptions), operation, executeOptions);
291
+ if (executeOptions.signal?.aborted) throw abortedRequestError(operation, executeOptions);
292
+ let response;
293
+ try {
294
+ response = await fetchImpl(url, {
295
+ method: operation.method,
296
+ headers,
297
+ body: prepared.body === void 0 ? void 0 : JSON.stringify(prepared.body),
298
+ signal: executeOptions.signal
299
+ });
300
+ } catch (cause) {
301
+ if (isAbortError(cause, executeOptions.signal)) throw abortedRequestError(operation, executeOptions, cause);
302
+ if (canRetry && attempt < retryOptions.maxAttempts) {
303
+ await waitForRetry(retryDelay(attempt, null, retryOptions), operation, executeOptions);
304
+ continue;
305
+ }
306
+ throw new GscdumpV1Error({
307
+ code: "network_error",
308
+ message: `${operation.id}: network request failed`,
309
+ requestId: executeOptions.requestId,
310
+ retryable: canRetry,
311
+ details: { attempts: attempt },
312
+ cause
313
+ });
314
+ }
315
+ const requestId = requestIdFrom(response) ?? executeOptions.requestId;
316
+ let payload;
317
+ try {
318
+ payload = await readJson(response);
319
+ } catch (cause) {
320
+ if (isAbortError(cause, executeOptions.signal)) throw abortedRequestError(operation, executeOptions, cause);
321
+ throw responseValidationError(operation.id, response.status, requestId, cause);
322
+ }
323
+ if (executeOptions.signal?.aborted) throw abortedRequestError(operation, executeOptions);
324
+ const responseContract = operation.responses[response.status];
325
+ if (responseContract) try {
326
+ return responseContract.client.parse(payload);
327
+ } catch (cause) {
328
+ throw responseValidationError(operation.id, response.status, requestId, cause);
329
+ }
330
+ if (response.ok) throw new GscdumpV1Error({
331
+ code: "protocol_error",
332
+ message: `${operation.id}: undeclared success status ${response.status}`,
333
+ status: response.status,
334
+ requestId,
335
+ retryable: false,
336
+ details: { status: response.status }
337
+ });
338
+ let envelope;
339
+ try {
340
+ envelope = protocol.schemas.errorEnvelope.client.parse(payload);
341
+ } catch (cause) {
342
+ throw responseValidationError(operation.id, response.status, requestId, cause);
343
+ }
344
+ if (!operation.errors.includes(envelope.error.code)) throw new GscdumpV1Error({
345
+ code: "protocol_error",
346
+ message: `${operation.id}: undeclared error code ${envelope.error.code}`,
347
+ status: response.status,
348
+ requestId: envelope.error.requestId,
349
+ retryable: false,
350
+ details: { code: envelope.error.code }
351
+ });
352
+ const apiError = new GscdumpV1Error({
353
+ code: envelope.error.code,
354
+ message: envelope.error.message,
355
+ status: response.status,
356
+ requestId: envelope.error.requestId,
357
+ retryable: envelope.error.retryable,
358
+ details: envelope.error.details ?? {}
359
+ });
360
+ if (canRetry && apiError.retryable && attempt < retryOptions.maxAttempts) {
361
+ await waitForRetry(retryDelay(attempt, response.headers.get("retry-after"), retryOptions), operation, executeOptions);
362
+ continue;
363
+ }
364
+ throw apiError;
365
+ }
366
+ throw new GscdumpV1Error({
367
+ code: "network_error",
368
+ message: `${operationId}: retry budget exhausted`,
369
+ retryable: false,
370
+ details: {}
371
+ });
372
+ }
373
+ return {
374
+ execute,
375
+ getUserLifecycle: (input, executeOptions) => execute("partner.users.lifecycle.get", input, executeOptions),
376
+ listAvailableSites: (input, executeOptions) => execute("partner.users.sites.available.list", input, executeOptions),
377
+ createSite: (input, executeOptions) => execute("partner.users.sites.create", input, executeOptions),
378
+ createUser: (input, executeOptions) => execute("partner.users.create", input, executeOptions),
379
+ updateUserTokens: (input, executeOptions) => execute("partner.users.tokens.update", input, executeOptions),
380
+ getSiteIndexing: (input, executeOptions) => execute("partner.sites.indexing.get", input, executeOptions),
381
+ listSiteIndexingUrls: (input, executeOptions) => execute("partner.sites.indexing.urls.list", input, executeOptions),
382
+ getSiteIndexingDiagnostics: (input, executeOptions) => execute("partner.sites.indexing.diagnostics.get", input, executeOptions),
383
+ getSiteSitemaps: (input, executeOptions) => execute("partner.sites.sitemaps.get", input, executeOptions),
384
+ getSiteSitemapChanges: (input, executeOptions) => execute("partner.sites.sitemaps.changes.get", input, executeOptions),
385
+ getSiteAnalysis: (input, executeOptions) => execute("partner.sites.analysis.get", input, executeOptions),
386
+ getSiteAnalysisBundle: (input, executeOptions) => execute("partner.sites.analysis.bundle.get", input, executeOptions),
387
+ deleteSite: (input, executeOptions) => execute("partner.sites.delete", input, executeOptions),
388
+ getCanonicalMismatches: (input, executeOptions) => execute("partner.sites.canonical.mismatches.get", input, executeOptions),
389
+ inspectSiteUrls: (input, executeOptions) => execute("partner.sites.indexing.inspect.create", input, executeOptions),
390
+ recoverSitePermission: (input, executeOptions) => execute("partner.sites.permission.recover", input, executeOptions),
391
+ queryKeywordSparklines: (input, executeOptions) => execute("partner.sites.keyword.sparklines.query", input, executeOptions),
392
+ getQueryTrend: (input, executeOptions) => execute("partner.sites.query.trend.get", input, executeOptions),
393
+ getPageTrend: (input, executeOptions) => execute("partner.sites.page.trend.get", input, executeOptions),
394
+ getContentVelocity: (input, executeOptions) => execute("partner.sites.content.velocity.get", input, executeOptions),
395
+ getCtrCurve: (input, executeOptions) => execute("partner.sites.ctr.curve.get", input, executeOptions),
396
+ getDarkTraffic: (input, executeOptions) => execute("partner.sites.dark.traffic.get", input, executeOptions),
397
+ getDeviceGap: (input, executeOptions) => execute("partner.sites.device.gap.get", input, executeOptions),
398
+ getKeywordBreadth: (input, executeOptions) => execute("partner.sites.keyword.breadth.get", input, executeOptions),
399
+ getPositionDistribution: (input, executeOptions) => execute("partner.sites.position.distribution.get", input, executeOptions),
400
+ getTopAssociation: (input, executeOptions) => execute("partner.sites.top.association.get", input, executeOptions),
401
+ getIndexPercent: (input, executeOptions) => execute("partner.sites.index.percent.get", input, executeOptions),
402
+ createSitemapAction: (input, executeOptions) => execute("partner.sites.sitemaps.action.create", input, executeOptions),
403
+ querySitemapMembership: (input, executeOptions) => execute("partner.sites.sitemaps.membership.query", input, executeOptions),
404
+ createTeam: (input, executeOptions) => execute("partner.teams.create", input, executeOptions),
405
+ renameTeam: (input, executeOptions) => execute("partner.teams.rename", input, executeOptions),
406
+ deleteTeam: (input, executeOptions) => execute("partner.teams.delete", input, executeOptions),
407
+ listTeamMembers: (input, executeOptions) => execute("partner.teams.members.list", input, executeOptions),
408
+ addTeamMember: (input, executeOptions) => execute("partner.teams.members.add", input, executeOptions),
409
+ updateTeamMemberRole: (input, executeOptions) => execute("partner.teams.members.role.update", input, executeOptions),
410
+ removeTeamMember: (input, executeOptions) => execute("partner.teams.members.remove", input, executeOptions),
411
+ updateSiteTeam: (input, executeOptions) => execute("partner.sites.team.update", input, executeOptions),
412
+ getTeamCatalog: (input, executeOptions) => execute("partner.teams.catalog.get", input, executeOptions),
413
+ bindTeamCatalog: (input, executeOptions) => execute("partner.teams.catalog.bind", input, executeOptions),
414
+ getSiteIntIdCrosswalk: (input, executeOptions) => execute("partner.users.sites.crosswalk.get", input, executeOptions),
415
+ deleteUser: (input, executeOptions) => execute("partner.users.delete", input, executeOptions),
416
+ createVerificationToken: (input, executeOptions) => execute("partner.users.verification.token.create", input, executeOptions),
417
+ addAndVerifySite: (input, executeOptions) => execute("partner.users.sites.verify.create", input, executeOptions),
418
+ queryCrossSource: (input, executeOptions) => execute("partner.sites.cross.source.query", input, executeOptions),
419
+ enrichKeywords: (input, executeOptions) => execute("partner.keywords.enrich.query", input, executeOptions),
420
+ queryAnalyticsRows: (input, executeOptions) => execute("analytics.rows.query", input, executeOptions),
421
+ queryAnalyticsReport: (input, executeOptions) => execute("analytics.reports.query", input, executeOptions),
422
+ queryAnalyticsReportDetail: (input, executeOptions) => execute("analytics.reports.detail.query", input, executeOptions),
423
+ getRealtimeStreamHead: (input = {}, executeOptions) => execute("realtime.stream.head.get", input, executeOptions),
424
+ createRealtimeTicket: (input, executeOptions) => execute("realtime.tickets.create", input, executeOptions)
425
+ };
426
+ }
427
+ export { GscdumpV1Error, createGscdumpV1Client, isGscdumpV1Error };