@gscdump/contracts 0.37.7 → 0.38.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.
@@ -0,0 +1,1440 @@
1
+ import { accountNextActions, accountStatuses, analyticsNextActions, analyticsStatuses, indexingNextActions, indexingStatuses, lifecycleErrorCodes, propertyNextActions, propertyStatuses, querySourceModes, sitemapNextActions, sitemapStatuses } from "../_chunks/onboarding.mjs";
2
+ import { z } from "zod";
3
+ const HTTP_V1_SURFACES = [
4
+ "partner",
5
+ "analytics",
6
+ "realtime"
7
+ ];
8
+ const HTTP_V1_METHODS = [
9
+ "DELETE",
10
+ "GET",
11
+ "PATCH",
12
+ "POST"
13
+ ];
14
+ const HTTP_V1_CREDENTIALS = ["user_key", "partner_key"];
15
+ const HTTP_V1_USER_KEY_SCOPES = [
16
+ "users:read",
17
+ "users:write",
18
+ "sites:read",
19
+ "sites:write",
20
+ "analytics:read",
21
+ "analytics:execute",
22
+ "indexing:read",
23
+ "indexing:write",
24
+ "sitemaps:read",
25
+ "sitemaps:write",
26
+ "settings:read",
27
+ "settings:write",
28
+ "realtime:connect"
29
+ ];
30
+ const HTTP_V1_SCOPES = [
31
+ ...HTTP_V1_USER_KEY_SCOPES,
32
+ "teams:read",
33
+ "teams:write"
34
+ ];
35
+ const HTTP_V1_CREDENTIAL_SCOPES = {
36
+ user_key: HTTP_V1_USER_KEY_SCOPES,
37
+ partner_key: HTTP_V1_SCOPES
38
+ };
39
+ const HTTP_V1_ERROR_CODES = [
40
+ "invalid_request",
41
+ "unauthorized",
42
+ "forbidden",
43
+ "user_not_found",
44
+ "site_not_found",
45
+ "rate_limited",
46
+ "realtime_unavailable",
47
+ "internal_error",
48
+ "contract_violation"
49
+ ];
50
+ function pathParameterNames(path) {
51
+ return [...path.matchAll(/\{([^{}]+)\}/g)].map((match) => match[1]);
52
+ }
53
+ function isSafePathTemplate(path) {
54
+ return /^\/(?:[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?|\{[a-z][A-Za-z0-9]*\})(?:\/(?:[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?|\{[a-z][A-Za-z0-9]*\}))*$/.test(path);
55
+ }
56
+ function objectSchemaKeys(schema) {
57
+ return schema instanceof z.ZodObject ? Object.keys(schema.shape) : null;
58
+ }
59
+ function assertRequestLocations(request, operationId) {
60
+ for (const location of [
61
+ "params",
62
+ "query",
63
+ "headers",
64
+ "body"
65
+ ]) if (!(location in request) || request[location] === void 0) throw new TypeError(`${operationId}: request.${location} must be a schema or null`);
66
+ }
67
+ function assertPathContract(operation) {
68
+ const names = pathParameterNames(operation.path);
69
+ if (new Set(names).size !== names.length) throw new TypeError(`${operation.id}: path parameters must be unique`);
70
+ if (names.length === 0) {
71
+ if (operation.request.params !== null) throw new TypeError(`${operation.id}: params must be null when the path has no parameters`);
72
+ return;
73
+ }
74
+ if (operation.request.params === null) throw new TypeError(`${operation.id}: path parameters require a params schema`);
75
+ const schemaKeys = objectSchemaKeys(operation.request.params);
76
+ if (!schemaKeys) throw new TypeError(`${operation.id}: params must be an object schema`);
77
+ if (names.join("\0") !== schemaKeys.join("\0")) throw new TypeError(`${operation.id}: params schema keys (${schemaKeys.join(", ")}) must match path order (${names.join(", ")})`);
78
+ }
79
+ function assertOperation(operation) {
80
+ if (!/^[a-z][a-z0-9]*(?:\.[a-z][a-z0-9]*)+$/.test(operation.id)) throw new TypeError(`${operation.id}: operation ID must be a stable dotted lowercase identifier`);
81
+ if (!isSafePathTemplate(operation.path)) throw new TypeError(`${operation.id}: path must be a safe literal surface-relative template`);
82
+ assertRequestLocations(operation.request, operation.id);
83
+ assertPathContract(operation);
84
+ if (operation.semantics.kind === "query") {
85
+ if (operation.semantics.sideEffects !== "none") throw new TypeError(`${operation.id}: query operations cannot declare state side effects`);
86
+ if (!operation.semantics.idempotent) throw new TypeError(`${operation.id}: query operations must be idempotent`);
87
+ if (operation.semantics.readConsistency === null) throw new TypeError(`${operation.id}: query operations must declare read consistency`);
88
+ } else {
89
+ if (operation.semantics.sideEffects !== "state") throw new TypeError(`${operation.id}: mutation operations must declare state side effects`);
90
+ if (operation.semantics.readConsistency !== null) throw new TypeError(`${operation.id}: mutations cannot declare read consistency`);
91
+ }
92
+ if (operation.auth.credentials.includes("user_key") && operation.semantics.kind === "query" && operation.semantics.readConsistency !== "primary") throw new TypeError(`${operation.id}: user_key queries must use primary consistency`);
93
+ if (operation.semantics.idempotent && operation.semantics.retry !== "idempotent") throw new TypeError(`${operation.id}: idempotent operations must use the idempotent retry policy`);
94
+ if (!operation.semantics.idempotent && operation.semantics.retry !== "never") throw new TypeError(`${operation.id}: non-idempotent operations cannot be retried automatically`);
95
+ if (operation.auth.credentials.length === 0 || operation.auth.scopes.length === 0) throw new TypeError(`${operation.id}: auth credentials and scopes are required`);
96
+ for (const credential of operation.auth.credentials) {
97
+ const availableScopes = HTTP_V1_CREDENTIAL_SCOPES[credential];
98
+ const unavailableScope = operation.auth.scopes.find((scope) => !availableScopes.includes(scope));
99
+ if (unavailableScope) throw new TypeError(`${operation.id}: ${credential} does not grant the ${unavailableScope} scope`);
100
+ }
101
+ const ownershipCredentials = operation.auth.ownership.map((ownership) => ownership.credential);
102
+ if (ownershipCredentials.length !== operation.auth.credentials.length || new Set(ownershipCredentials).size !== ownershipCredentials.length || operation.auth.credentials.some((credential) => !ownershipCredentials.includes(credential))) throw new TypeError(`${operation.id}: every credential requires exactly one ownership rule`);
103
+ if (operation.errors.length === 0) throw new TypeError(`${operation.id}: declared stable errors are required`);
104
+ if (!operation.errorResponse?.producer || !operation.errorResponse.client) throw new TypeError(`${operation.id}: an operation-specific error response schema is required`);
105
+ if (Object.keys(operation.responses).length === 0) throw new TypeError(`${operation.id}: at least one response is required`);
106
+ const pathParameters = new Set(pathParameterNames(operation.path));
107
+ for (const reference of [...operation.resources.reads, ...operation.resources.changes]) if (reference.idFrom.startsWith("params.") && !pathParameters.has(reference.idFrom.slice(7))) throw new TypeError(`${operation.id}: resource reference ${reference.idFrom} is not a declared path parameter`);
108
+ if (operation.docs.summary.length === 0 || operation.docs.description.length === 0 || operation.docs.tags.length === 0) throw new TypeError(`${operation.id}: public documentation metadata is incomplete`);
109
+ }
110
+ function defineHttpOperation(operation) {
111
+ assertOperation(operation);
112
+ return operation;
113
+ }
114
+ function defineHttpSurface(surface) {
115
+ if (surface.prefix !== `/api/${surface.name}/v1`) throw new TypeError(`${surface.name}: prefix must retain the surface and v1 major`);
116
+ if (surface.version !== "1.0") throw new TypeError(`${surface.name}: wire version must be 1.0`);
117
+ const ids = /* @__PURE__ */ new Set();
118
+ const routes = /* @__PURE__ */ new Set();
119
+ for (const [key, operation] of Object.entries(surface.operations)) {
120
+ assertOperation(operation);
121
+ if (!operation.id.startsWith(`${surface.name}.`)) throw new TypeError(`${surface.name}: operation ID ${operation.id} must use the surface namespace`);
122
+ if (ids.has(operation.id)) throw new TypeError(`${surface.name}: duplicate operation ID ${operation.id}`);
123
+ ids.add(operation.id);
124
+ const route = `${operation.method} ${operation.path}`;
125
+ if (routes.has(route)) throw new TypeError(`${surface.name}: duplicate method/path ${route}`);
126
+ routes.add(route);
127
+ if (key.length === 0) throw new TypeError(`${surface.name}: operation registry keys cannot be empty`);
128
+ }
129
+ return surface;
130
+ }
131
+ function defineResponseObject(producerShape, clientShape) {
132
+ return {
133
+ producer: z.strictObject(producerShape),
134
+ client: z.looseObject(clientShape ?? producerShape)
135
+ };
136
+ }
137
+ function defineSuccessResponse(data, meta) {
138
+ return defineResponseObject({
139
+ data: data.producer,
140
+ meta: meta.producer
141
+ }, {
142
+ data: data.client,
143
+ meta: meta.client
144
+ });
145
+ }
146
+ function buildHttpOperationPath(surface, operation, params) {
147
+ if (!operation.id.startsWith(`${surface.name}.`)) throw new TypeError(`${operation.id}: operation does not belong to the ${surface.name} surface`);
148
+ if (pathParameterNames(operation.path).length === 0) {
149
+ if (params !== void 0 && params !== null && (typeof params !== "object" || Object.keys(params).length > 0)) throw new TypeError(`${operation.id}: this operation has no path parameters`);
150
+ return `${surface.prefix}${operation.path}`;
151
+ }
152
+ if (operation.request.params === null) throw new TypeError(`${operation.id}: operation has an invalid path contract`);
153
+ const parsed = operation.request.params.parse(params);
154
+ const relativePath = operation.path.replace(/\{([^{}]+)\}/g, (_match, name) => {
155
+ const value = parsed[name];
156
+ if (typeof value !== "string" && typeof value !== "number") throw new TypeError(`${operation.id}: path parameter ${name} must serialize as a string or number`);
157
+ const serialized = String(value);
158
+ if (serialized.length === 0 || serialized === "." || serialized === "..") throw new TypeError(`${operation.id}: path parameter ${name} cannot serialize as an empty or dot segment`);
159
+ return encodeURIComponent(serialized);
160
+ });
161
+ return `${surface.prefix}${relativePath}`;
162
+ }
163
+ const GSCDUMP_REALTIME_PROTOCOL_VERSION = 1;
164
+ const GSCDUMP_REALTIME_SUBPROTOCOL = "gscdump.v1";
165
+ const GSCDUMP_REALTIME_TICKET_PREFIX = "gscdump.ticket.v1";
166
+ const GSCDUMP_REALTIME_TICKET_ISSUER = "https://gscdump.com";
167
+ const GSCDUMP_REALTIME_TICKET_AUDIENCE = "https://gscdump.com/ws/v1";
168
+ const GSCDUMP_REALTIME_PING = "ping";
169
+ const GSCDUMP_REALTIME_PONG = "pong";
170
+ const GSCDUMP_REALTIME_TICKET_TTL_SECONDS = 60;
171
+ const GSCDUMP_REALTIME_MAX_CONNECTION_SECONDS = 900;
172
+ const GSCDUMP_REALTIME_TICKET_POLICY = {
173
+ algorithm: "HMAC-SHA-256",
174
+ minimumSigningKeyBytes: 32,
175
+ jtiRandomBits: 128,
176
+ maxVerificationKeys: 2,
177
+ futureIssuedAtToleranceMs: 5e3,
178
+ expiryToleranceMs: 0,
179
+ rotationOverlapMs: 9e4,
180
+ maxBytes: 2048,
181
+ singleUse: true
182
+ };
183
+ const GSCDUMP_REALTIME_CONNECTION_POLICY = {
184
+ helloDeadlineMs: 1e4,
185
+ maxLifetimeMs: 9e5,
186
+ connectionRefreshBeforeExpiryMs: 3e4,
187
+ heartbeatIntervalMs: 25e3,
188
+ staleAfterMs: 75e3,
189
+ reconnectBaseDelayMs: 1e3,
190
+ reconnectMaxDelayMs: 3e4,
191
+ reconnectJitter: "equal"
192
+ };
193
+ const GSCDUMP_REALTIME_REPLAY_POLICY = {
194
+ maxAgeMs: 864e5,
195
+ maxEventsPerStream: 1e4
196
+ };
197
+ const GSCDUMP_REALTIME_LIMITS = {
198
+ clientFrameMaxBytes: 16384,
199
+ durableEventMaxBytes: 65536,
200
+ ephemeralEventMaxBytes: 16384,
201
+ outboundFrameMaxBytes: 262144,
202
+ batchMaxEvents: 20,
203
+ batchMaxBytes: 262144,
204
+ connectionAttachmentMaxBytes: 2048
205
+ };
206
+ const GSCDUMP_REALTIME_ACK_POLICY = {
207
+ softLagEvents: 100,
208
+ softLagBytes: 1048576,
209
+ hardLagEvents: 500,
210
+ hardLagBytes: 4194304,
211
+ hardLagCloseCode: 1013,
212
+ effectRetryBeforeUnsafeResync: 3
213
+ };
214
+ const GSCDUMP_REALTIME_CLOSE_CODES = {
215
+ normal: 1e3,
216
+ protocolError: 1002,
217
+ policyViolation: 1008,
218
+ internalError: 1011,
219
+ serviceRestart: 1012,
220
+ overloadedOrAckLag: 1013,
221
+ maximumLifetime: 4001
222
+ };
223
+ const REALTIME_V1_RESOURCE_TYPES = [
224
+ "partner.user",
225
+ "partner.team",
226
+ "user.sites",
227
+ "site.registration",
228
+ "site.lifecycle",
229
+ "site.analytics",
230
+ "site.sitemaps",
231
+ "site.indexing",
232
+ "site.auth"
233
+ ];
234
+ const REALTIME_V1_EVENT_NAMES = [
235
+ "user.lifecycle.changed",
236
+ "site.lifecycle.changed",
237
+ "site.lifecycle.progress",
238
+ "site.analytics.ready",
239
+ "site.sitemaps.ready",
240
+ "site.indexing.ready",
241
+ "site.auth.failed",
242
+ "site.lifecycle.failed",
243
+ "site.registration.changed"
244
+ ];
245
+ const REALTIME_V1_EVENT_SEMANTICS = {
246
+ "user.lifecycle.changed": {
247
+ delivery: "durable",
248
+ baseChanges: ["partner.user", "user.sites"]
249
+ },
250
+ "site.lifecycle.changed": {
251
+ delivery: "durable",
252
+ baseChanges: ["site.lifecycle"]
253
+ },
254
+ "site.lifecycle.progress": {
255
+ delivery: "ephemeral",
256
+ baseChanges: []
257
+ },
258
+ "site.analytics.ready": {
259
+ delivery: "durable",
260
+ baseChanges: ["site.analytics", "site.lifecycle"]
261
+ },
262
+ "site.sitemaps.ready": {
263
+ delivery: "durable",
264
+ baseChanges: ["site.sitemaps", "site.lifecycle"]
265
+ },
266
+ "site.indexing.ready": {
267
+ delivery: "durable",
268
+ baseChanges: ["site.indexing", "site.lifecycle"]
269
+ },
270
+ "site.auth.failed": {
271
+ delivery: "durable",
272
+ baseChanges: ["site.auth", "site.lifecycle"]
273
+ },
274
+ "site.lifecycle.failed": {
275
+ delivery: "durable",
276
+ baseChanges: ["site.lifecycle"]
277
+ },
278
+ "site.registration.changed": {
279
+ delivery: "durable",
280
+ baseChanges: ["site.registration", "user.sites"]
281
+ }
282
+ };
283
+ function createRealtimeV1Schemas() {
284
+ const sequence = z.string().regex(/^(0|[1-9]\d*)$/);
285
+ const opaquePublicId = z.string().regex(/^[\w-]+$/);
286
+ const publicUserId = z.string().regex(/^u_[\w-]+$/);
287
+ const publicPartnerId = z.string().regex(/^p_[\w-]+$/);
288
+ const publicTeamId = z.string().regex(/^t_[\w-]+$/);
289
+ const publicSiteId = z.string().regex(/^s_[\w-]+$/);
290
+ const publicPrincipalId = z.union([publicUserId, publicPartnerId]);
291
+ const publicResourceId = z.union([
292
+ publicUserId,
293
+ publicPartnerId,
294
+ publicTeamId,
295
+ publicSiteId
296
+ ]);
297
+ const publicRequestId = z.string().regex(/^req_[\w-]+$/);
298
+ const publicEventId = z.string().regex(/^evt_[\w-]+$/);
299
+ const userStreamId = z.string().regex(/^user:u_[\w-]+$/);
300
+ const partnerStreamId = z.string().regex(/^partner:p_[\w-]+$/);
301
+ const streamId = z.union([userStreamId, partnerStreamId]);
302
+ const origin = z.url().refine((value) => {
303
+ const parsed = new URL(value);
304
+ return (parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.origin === value;
305
+ }, "origin must be an exact HTTP(S) origin without a path, query, or fragment.");
306
+ const cursor = z.strictObject({
307
+ streamId,
308
+ sequence
309
+ });
310
+ const streamHead = z.strictObject({
311
+ streamId,
312
+ sequence
313
+ });
314
+ const ticketRequest = z.strictObject({ origin: origin.optional() });
315
+ const ticketClaims = z.strictObject({
316
+ v: z.literal(1),
317
+ iss: z.literal(GSCDUMP_REALTIME_TICKET_ISSUER),
318
+ aud: z.literal(GSCDUMP_REALTIME_TICKET_AUDIENCE),
319
+ kid: z.string().regex(/^[\w-]{1,128}$/),
320
+ jti: z.string().regex(/^[\w-]{22,}$/),
321
+ iat: z.number().int().nonnegative(),
322
+ exp: z.number().int().positive(),
323
+ connectionExpiresAt: z.number().int().positive(),
324
+ principalClass: z.enum(["user_key", "partner_key"]),
325
+ principalPublicId: publicPrincipalId,
326
+ streamId,
327
+ origin: origin.nullable()
328
+ }).superRefine((claims, context) => {
329
+ if (claims.exp <= claims.iat || claims.exp - claims.iat > 60) context.addIssue({
330
+ code: "custom",
331
+ message: `exp must be after iat and no more than 60 seconds later.`,
332
+ path: ["exp"]
333
+ });
334
+ if (claims.connectionExpiresAt <= claims.iat || claims.connectionExpiresAt - claims.iat > 900) context.addIssue({
335
+ code: "custom",
336
+ message: `connectionExpiresAt must be after iat and no more than 900 seconds later.`,
337
+ path: ["connectionExpiresAt"]
338
+ });
339
+ if (claims.connectionExpiresAt < claims.exp) context.addIssue({
340
+ code: "custom",
341
+ message: "connectionExpiresAt cannot be earlier than exp.",
342
+ path: ["connectionExpiresAt"]
343
+ });
344
+ const expectedStream = claims.principalClass === "user_key" ? `user:${claims.principalPublicId}` : `partner:${claims.principalPublicId}`;
345
+ if (claims.streamId !== expectedStream) context.addIssue({
346
+ code: "custom",
347
+ message: "streamId must be the exact stream inferred from principalClass and principalPublicId.",
348
+ path: ["streamId"]
349
+ });
350
+ if (claims.principalClass === "user_key" && !claims.principalPublicId.startsWith("u_")) context.addIssue({
351
+ code: "custom",
352
+ message: "A user principal requires a user public ID.",
353
+ path: ["principalPublicId"]
354
+ });
355
+ if (claims.principalClass === "partner_key" && !claims.principalPublicId.startsWith("p_")) context.addIssue({
356
+ code: "custom",
357
+ message: "A partner principal requires a partner public ID.",
358
+ path: ["principalPublicId"]
359
+ });
360
+ });
361
+ const knownResourceType = z.enum(REALTIME_V1_RESOURCE_TYPES);
362
+ const resourceChange = z.strictObject({
363
+ type: z.string().regex(/^[a-z][a-z0-9]*(?:\.[a-z][a-z0-9]*)+$/),
364
+ id: opaquePublicId,
365
+ kind: z.enum([
366
+ "created",
367
+ "updated",
368
+ "deleted"
369
+ ])
370
+ }).superRefine((change, context) => {
371
+ const prefix = change.type === "partner.user" || change.type === "user.sites" ? "u_" : change.type === "partner.team" ? "t_" : REALTIME_V1_RESOURCE_TYPES.includes(change.type) && change.type.startsWith("site.") ? "s_" : null;
372
+ if (prefix && !change.id.startsWith(prefix)) context.addIssue({
373
+ code: "custom",
374
+ message: `${change.type} requires an opaque ${prefix} public ID.`,
375
+ path: ["id"]
376
+ });
377
+ });
378
+ const subject = z.discriminatedUnion("type", [
379
+ z.strictObject({
380
+ type: z.literal("partner"),
381
+ id: publicPartnerId
382
+ }),
383
+ z.strictObject({
384
+ type: z.literal("team"),
385
+ id: publicTeamId
386
+ }),
387
+ z.strictObject({
388
+ type: z.literal("user"),
389
+ id: publicUserId
390
+ }),
391
+ z.strictObject({
392
+ type: z.literal("site"),
393
+ id: publicSiteId
394
+ })
395
+ ]);
396
+ const eventBase = {
397
+ type: z.literal("event"),
398
+ protocolVersion: z.literal(1),
399
+ eventVersion: z.number().int().positive(),
400
+ id: publicEventId,
401
+ name: z.string().regex(/^[a-z][a-z0-9]*(?:\.[a-z][a-z0-9]*)+$/),
402
+ subject,
403
+ changes: z.array(resourceChange),
404
+ occurredAt: z.iso.datetime(),
405
+ correlationId: publicRequestId.nullable(),
406
+ data: z.json()
407
+ };
408
+ const durableEvent = z.strictObject({
409
+ ...eventBase,
410
+ cursor,
411
+ changes: z.array(resourceChange).min(1),
412
+ delivery: z.literal("durable")
413
+ }).superRefine((event, context) => {
414
+ const semantics = REALTIME_V1_EVENT_SEMANTICS[event.name];
415
+ if (semantics?.delivery === "ephemeral") context.addIssue({
416
+ code: "custom",
417
+ message: `${event.name} must use ephemeral delivery.`,
418
+ path: ["delivery"]
419
+ });
420
+ if (semantics) {
421
+ for (const requiredType of semantics.baseChanges) if (!event.changes.some((change) => change.type === requiredType)) context.addIssue({
422
+ code: "custom",
423
+ message: `${event.name} requires a ${requiredType} base change.`,
424
+ path: ["changes"]
425
+ });
426
+ }
427
+ });
428
+ const ephemeralEvent = z.strictObject({
429
+ ...eventBase,
430
+ cursor: z.null(),
431
+ changes: z.array(resourceChange).max(0),
432
+ delivery: z.literal("ephemeral")
433
+ }).superRefine((event, context) => {
434
+ if (REALTIME_V1_EVENT_SEMANTICS[event.name]?.delivery === "durable") context.addIssue({
435
+ code: "custom",
436
+ message: `${event.name} must use durable delivery.`,
437
+ path: ["delivery"]
438
+ });
439
+ });
440
+ const event = z.union([durableEvent, ephemeralEvent]);
441
+ const helloFrame = z.strictObject({
442
+ type: z.literal("hello"),
443
+ protocolVersion: z.literal(1),
444
+ sdkVersion: z.string().min(1),
445
+ resume: cursor.nullable()
446
+ });
447
+ const ackFrame = z.strictObject({
448
+ type: z.literal("ack"),
449
+ cursor
450
+ });
451
+ const readyFrame = z.strictObject({
452
+ type: z.literal("ready"),
453
+ protocolVersion: z.literal(1),
454
+ connectionId: z.string().regex(/^conn_[\w-]+$/),
455
+ streamId,
456
+ replayFloor: streamHead,
457
+ head: streamHead,
458
+ limits: z.strictObject({
459
+ maxBatchEvents: z.number().int().positive().max(GSCDUMP_REALTIME_LIMITS.batchMaxEvents),
460
+ maxUnackedEvents: z.number().int().positive().max(GSCDUMP_REALTIME_ACK_POLICY.hardLagEvents)
461
+ }),
462
+ heartbeat: z.strictObject({
463
+ ping: z.literal(GSCDUMP_REALTIME_PING),
464
+ pong: z.literal(GSCDUMP_REALTIME_PONG)
465
+ }),
466
+ expiresAt: z.iso.datetime()
467
+ }).superRefine((frame, context) => {
468
+ if (frame.head.streamId !== frame.streamId || frame.replayFloor.streamId !== frame.streamId) context.addIssue({
469
+ code: "custom",
470
+ message: "ready head and replayFloor must match streamId."
471
+ });
472
+ if (BigInt(frame.replayFloor.sequence) > BigInt(frame.head.sequence)) context.addIssue({
473
+ code: "custom",
474
+ message: "replayFloor cannot be ahead of head.",
475
+ path: ["replayFloor"]
476
+ });
477
+ });
478
+ const replayBeginFrame = z.strictObject({
479
+ type: z.literal("replay.begin"),
480
+ after: cursor.nullable(),
481
+ through: cursor
482
+ }).superRefine((frame, context) => {
483
+ if (!frame.after) return;
484
+ if (frame.after.streamId !== frame.through.streamId) context.addIssue({
485
+ code: "custom",
486
+ message: "Replay cursors must belong to one stream.",
487
+ path: ["through"]
488
+ });
489
+ if (BigInt(frame.after.sequence) > BigInt(frame.through.sequence)) context.addIssue({
490
+ code: "custom",
491
+ message: "Replay through cannot be behind after.",
492
+ path: ["through"]
493
+ });
494
+ });
495
+ const replayEndFrame = z.strictObject({
496
+ type: z.literal("replay.end"),
497
+ through: cursor
498
+ });
499
+ const batchFrame = z.strictObject({
500
+ type: z.literal("batch"),
501
+ events: z.array(durableEvent).min(1).max(GSCDUMP_REALTIME_LIMITS.batchMaxEvents)
502
+ }).superRefine((frame, context) => {
503
+ const first = frame.events[0];
504
+ if (!first) return;
505
+ for (const [index, current] of frame.events.entries()) {
506
+ if (current.cursor.streamId !== first.cursor.streamId) context.addIssue({
507
+ code: "custom",
508
+ message: "A batch must contain events from one exact stream.",
509
+ path: [
510
+ "events",
511
+ index,
512
+ "cursor",
513
+ "streamId"
514
+ ]
515
+ });
516
+ const previous = frame.events[index - 1];
517
+ if (previous && BigInt(current.cursor.sequence) !== BigInt(previous.cursor.sequence) + 1n) context.addIssue({
518
+ code: "custom",
519
+ message: "Batch cursors must be contiguous and ordered.",
520
+ path: [
521
+ "events",
522
+ index,
523
+ "cursor",
524
+ "sequence"
525
+ ]
526
+ });
527
+ }
528
+ });
529
+ const resyncRequiredFrame = z.strictObject({
530
+ type: z.literal("resync.required"),
531
+ reason: z.enum([
532
+ "cursor_expired",
533
+ "retention_gap",
534
+ "sequence_gap",
535
+ "stream_mismatch"
536
+ ]),
537
+ scope: z.strictObject({
538
+ type: z.literal("stream"),
539
+ streamId
540
+ }),
541
+ resumeAfter: cursor
542
+ }).superRefine((frame, context) => {
543
+ if (frame.scope.streamId !== frame.resumeAfter.streamId) context.addIssue({
544
+ code: "custom",
545
+ message: "resync.required scope and resumeAfter must identify one exact stream.",
546
+ path: ["resumeAfter", "streamId"]
547
+ });
548
+ });
549
+ const realtimeErrorFrame = z.strictObject({
550
+ type: z.literal("error"),
551
+ error: z.strictObject({
552
+ code: z.enum([
553
+ "invalid_frame",
554
+ "protocol_mismatch",
555
+ "policy_violation",
556
+ "overloaded",
557
+ "internal_error"
558
+ ]),
559
+ message: z.string().min(1),
560
+ retryable: z.boolean(),
561
+ retryAfter: z.number().int().nonnegative().optional()
562
+ })
563
+ });
564
+ return {
565
+ opaquePublicId,
566
+ userStreamId,
567
+ partnerStreamId,
568
+ publicUserId,
569
+ publicPartnerId,
570
+ publicTeamId,
571
+ publicSiteId,
572
+ publicPrincipalId,
573
+ publicResourceId,
574
+ publicRequestId,
575
+ publicEventId,
576
+ sequence,
577
+ streamId,
578
+ cursor,
579
+ streamHead,
580
+ ticketRequest,
581
+ ticketClaims,
582
+ knownResourceType,
583
+ resourceChange,
584
+ subject,
585
+ durableEvent,
586
+ ephemeralEvent,
587
+ event,
588
+ helloFrame,
589
+ ackFrame,
590
+ readyFrame,
591
+ replayBeginFrame,
592
+ replayEndFrame,
593
+ batchFrame,
594
+ resyncRequiredFrame,
595
+ realtimeErrorFrame,
596
+ clientFrame: z.union([helloFrame, ackFrame]),
597
+ serverFrame: z.union([
598
+ readyFrame,
599
+ replayBeginFrame,
600
+ replayEndFrame,
601
+ event,
602
+ batchFrame,
603
+ resyncRequiredFrame,
604
+ realtimeErrorFrame
605
+ ])
606
+ };
607
+ }
608
+ const GSCDUMP_HTTP_V1_VERSION = "1.0";
609
+ function errorEnvelopeSchemas(codes, publicRequestId) {
610
+ const errorDetails = z.record(z.string(), z.json());
611
+ const producer = z.strictObject({
612
+ code: z.enum(codes),
613
+ message: z.string().min(1),
614
+ requestId: publicRequestId,
615
+ retryable: z.boolean(),
616
+ details: errorDetails
617
+ });
618
+ const client = z.looseObject({
619
+ code: z.enum(codes),
620
+ message: z.string().min(1),
621
+ requestId: publicRequestId,
622
+ retryable: z.boolean(),
623
+ details: errorDetails
624
+ });
625
+ return defineResponseObject({ error: producer }, { error: client });
626
+ }
627
+ function lifecycleResponseSchemas(ids) {
628
+ const progress = defineResponseObject({
629
+ completed: z.number().int().nonnegative(),
630
+ failed: z.number().int().nonnegative(),
631
+ total: z.number().int().nonnegative(),
632
+ percent: z.number().min(0).max(100)
633
+ });
634
+ const latestError = defineResponseObject({
635
+ code: z.enum(lifecycleErrorCodes),
636
+ message: z.string(),
637
+ retryable: z.boolean()
638
+ });
639
+ const account = defineResponseObject({
640
+ status: z.enum(accountStatuses),
641
+ grantedScopes: z.array(z.string()),
642
+ missingScopes: z.array(z.string()),
643
+ nextAction: z.enum(accountNextActions)
644
+ });
645
+ const property = defineResponseObject({
646
+ status: z.enum(propertyStatuses),
647
+ nextAction: z.enum(propertyNextActions)
648
+ });
649
+ const syncedRange = defineResponseObject({
650
+ oldest: z.iso.date().nullable(),
651
+ newest: z.iso.date().nullable()
652
+ });
653
+ const analytics = defineResponseObject({
654
+ status: z.enum(analyticsStatuses),
655
+ progress: progress.producer,
656
+ queryable: z.boolean(),
657
+ sourceMode: z.enum(querySourceModes),
658
+ syncedRange: syncedRange.producer,
659
+ nextAction: z.enum(analyticsNextActions)
660
+ }, {
661
+ status: z.enum(analyticsStatuses),
662
+ progress: progress.client,
663
+ queryable: z.boolean(),
664
+ sourceMode: z.enum(querySourceModes),
665
+ syncedRange: syncedRange.client,
666
+ nextAction: z.enum(analyticsNextActions)
667
+ });
668
+ const sitemaps = defineResponseObject({
669
+ status: z.enum(sitemapStatuses),
670
+ discoveredCount: z.number().int().nonnegative(),
671
+ nextAction: z.enum(sitemapNextActions)
672
+ });
673
+ const indexing = defineResponseObject({
674
+ status: z.enum(indexingStatuses),
675
+ eligible: z.boolean(),
676
+ reason: z.string().nullable(),
677
+ progress: progress.producer,
678
+ nextAction: z.enum(indexingNextActions)
679
+ }, {
680
+ status: z.enum(indexingStatuses),
681
+ eligible: z.boolean(),
682
+ reason: z.string().nullable(),
683
+ progress: progress.client,
684
+ nextAction: z.enum(indexingNextActions)
685
+ });
686
+ const site = defineResponseObject({
687
+ siteId: ids.publicSiteId,
688
+ externalSiteId: z.string().nullable(),
689
+ requestedUrl: z.string().min(1),
690
+ gscPropertyUrl: z.string().nullable(),
691
+ permissionLevel: z.string().nullable(),
692
+ property: property.producer,
693
+ analytics: analytics.producer,
694
+ sitemaps: sitemaps.producer,
695
+ indexing: indexing.producer,
696
+ latestError: latestError.producer.nullable(),
697
+ updatedAt: z.iso.datetime()
698
+ }, {
699
+ siteId: ids.publicSiteId,
700
+ externalSiteId: z.string().nullable(),
701
+ requestedUrl: z.string().min(1),
702
+ gscPropertyUrl: z.string().nullable(),
703
+ permissionLevel: z.string().nullable(),
704
+ property: property.client,
705
+ analytics: analytics.client,
706
+ sitemaps: sitemaps.client,
707
+ indexing: indexing.client,
708
+ latestError: latestError.client.nullable(),
709
+ updatedAt: z.iso.datetime()
710
+ });
711
+ return defineResponseObject({
712
+ userId: ids.publicUserId,
713
+ partnerId: ids.publicPartnerId.nullable(),
714
+ currentTeamId: ids.publicTeamId.nullable(),
715
+ account: account.producer,
716
+ sites: z.array(site.producer)
717
+ }, {
718
+ userId: ids.publicUserId,
719
+ partnerId: ids.publicPartnerId.nullable(),
720
+ currentTeamId: ids.publicTeamId.nullable(),
721
+ account: account.client,
722
+ sites: z.array(site.client)
723
+ });
724
+ }
725
+ function createGscdumpV1Protocol() {
726
+ const realtimeSchemas = createRealtimeV1Schemas();
727
+ const surfaceSchema = z.enum([
728
+ "partner",
729
+ "analytics",
730
+ "realtime"
731
+ ]);
732
+ const responseMeta = defineResponseObject({
733
+ requestId: realtimeSchemas.publicRequestId,
734
+ surface: surfaceSchema,
735
+ version: z.literal("1.0")
736
+ });
737
+ const partnerResponseMeta = defineResponseObject({
738
+ requestId: realtimeSchemas.publicRequestId,
739
+ surface: z.literal("partner"),
740
+ version: z.literal("1.0")
741
+ });
742
+ const realtimeResponseMeta = defineResponseObject({
743
+ requestId: realtimeSchemas.publicRequestId,
744
+ surface: z.literal("realtime"),
745
+ version: z.literal("1.0")
746
+ });
747
+ const requestMetadata = z.strictObject({
748
+ requestId: realtimeSchemas.publicRequestId,
749
+ surface: surfaceSchema,
750
+ version: z.literal("1.0")
751
+ });
752
+ const requestHeaders = z.strictObject({ "x-request-id": realtimeSchemas.publicRequestId.optional() });
753
+ const errorEnvelope = errorEnvelopeSchemas(HTTP_V1_ERROR_CODES, realtimeSchemas.publicRequestId);
754
+ const lifecycleResponse = defineSuccessResponse(lifecycleResponseSchemas(realtimeSchemas), partnerResponseMeta);
755
+ const dimensions = [
756
+ "page",
757
+ "query",
758
+ "queryCanonical",
759
+ "country",
760
+ "device",
761
+ "date",
762
+ "searchAppearance",
763
+ "hour"
764
+ ];
765
+ const metrics = [
766
+ "clicks",
767
+ "impressions",
768
+ "ctr",
769
+ "position"
770
+ ];
771
+ const filterDimension = z.enum([
772
+ ...dimensions,
773
+ ...metrics,
774
+ "searchType"
775
+ ]);
776
+ const normalizedFilterLeaf = z.union([z.strictObject({
777
+ dimension: filterDimension,
778
+ operator: z.enum([
779
+ "equals",
780
+ "notEquals",
781
+ "contains",
782
+ "notContains",
783
+ "includingRegex",
784
+ "excludingRegex",
785
+ "gte",
786
+ "gt",
787
+ "lte",
788
+ "lt",
789
+ "metricGte",
790
+ "metricGt",
791
+ "metricLte",
792
+ "metricLt",
793
+ "topLevel"
794
+ ]),
795
+ expression: z.string()
796
+ }), z.strictObject({
797
+ dimension: filterDimension,
798
+ operator: z.enum(["between", "metricBetween"]),
799
+ expression: z.string(),
800
+ expression2: z.string()
801
+ })]);
802
+ const normalizedFilter = z.lazy(() => z.union([z.strictObject({
803
+ _filters: z.array(normalizedFilterLeaf).min(1),
804
+ _nestedGroups: z.array(normalizedFilter).optional(),
805
+ _groupType: z.enum(["and", "or"]).optional()
806
+ }), z.strictObject({
807
+ _filters: z.array(normalizedFilterLeaf).max(0),
808
+ _nestedGroups: z.array(normalizedFilter).min(1),
809
+ _groupType: z.enum(["and", "or"]).optional()
810
+ })]));
811
+ const analyticsRowsRequest = z.strictObject({
812
+ dimensions: z.array(z.enum(dimensions)).min(1),
813
+ metrics: z.array(z.enum(metrics)).optional(),
814
+ filter: normalizedFilter.optional(),
815
+ prefilter: normalizedFilter.optional(),
816
+ orderBy: z.strictObject({
817
+ column: z.enum([...metrics, "date"]),
818
+ dir: z.enum(["asc", "desc"])
819
+ }).optional(),
820
+ rowLimit: z.number().int().positive().max(25e3).optional(),
821
+ startRow: z.number().int().nonnegative().optional(),
822
+ dataState: z.enum([
823
+ "final",
824
+ "all",
825
+ "hourly_all"
826
+ ]).optional(),
827
+ aggregationType: z.enum([
828
+ "auto",
829
+ "byPage",
830
+ "byProperty",
831
+ "byNewsShowcasePanel"
832
+ ]).optional(),
833
+ searchType: z.enum([
834
+ "web",
835
+ "image",
836
+ "video",
837
+ "news",
838
+ "discover",
839
+ "googleNews"
840
+ ]).optional()
841
+ });
842
+ const analyticsRowsResponse = defineSuccessResponse(defineResponseObject({ rows: z.array(z.record(z.string(), z.union([
843
+ z.string(),
844
+ z.number(),
845
+ z.boolean(),
846
+ z.null()
847
+ ]))) }), defineResponseObject({
848
+ requestId: realtimeSchemas.publicRequestId,
849
+ surface: z.literal("analytics"),
850
+ version: z.literal("1.0"),
851
+ sourceName: z.string().min(1),
852
+ sourceKind: z.enum(["row", "sql"]),
853
+ queryMs: z.number().nonnegative()
854
+ }));
855
+ const responseStreamHead = defineResponseObject({
856
+ streamId: realtimeSchemas.streamId,
857
+ sequence: realtimeSchemas.sequence
858
+ });
859
+ const headResponse = defineSuccessResponse(defineResponseObject({ head: responseStreamHead.producer }, { head: responseStreamHead.client }), realtimeResponseMeta);
860
+ const ticketDataProducerShape = {
861
+ socketUrl: z.url().refine((value) => value.startsWith("wss://"), "socketUrl must use wss."),
862
+ protocol: z.literal(GSCDUMP_REALTIME_SUBPROTOCOL),
863
+ protocolVersion: z.literal(1),
864
+ ticket: z.string().max(GSCDUMP_REALTIME_TICKET_POLICY.maxBytes).regex(/^gscdump\.ticket\.v1\.[\w-]+\.[\w-]+$/),
865
+ expiresAt: z.iso.datetime(),
866
+ head: responseStreamHead.producer,
867
+ maxConnectionSeconds: z.literal(900)
868
+ };
869
+ const ticketResponse = defineSuccessResponse(defineResponseObject(ticketDataProducerShape, {
870
+ ...ticketDataProducerShape,
871
+ head: responseStreamHead.client
872
+ }), realtimeResponseMeta);
873
+ const partnerUserLifecycleErrors = [
874
+ "invalid_request",
875
+ "unauthorized",
876
+ "forbidden",
877
+ "user_not_found",
878
+ "rate_limited",
879
+ "internal_error",
880
+ "contract_violation"
881
+ ];
882
+ const analyticsRowsErrors = [
883
+ "invalid_request",
884
+ "unauthorized",
885
+ "forbidden",
886
+ "site_not_found",
887
+ "rate_limited",
888
+ "internal_error",
889
+ "contract_violation"
890
+ ];
891
+ const realtimeErrors = [
892
+ "invalid_request",
893
+ "unauthorized",
894
+ "forbidden",
895
+ "rate_limited",
896
+ "realtime_unavailable",
897
+ "internal_error",
898
+ "contract_violation"
899
+ ];
900
+ const partner = defineHttpSurface({
901
+ name: "partner",
902
+ prefix: "/api/partner/v1",
903
+ version: "1.0",
904
+ operations: { getUserLifecycle: defineHttpOperation({
905
+ id: "partner.users.lifecycle.get",
906
+ method: "GET",
907
+ path: "/users/{userId}/lifecycle",
908
+ visibility: "public",
909
+ semantics: {
910
+ kind: "query",
911
+ sideEffects: "none",
912
+ idempotent: true,
913
+ retry: "idempotent",
914
+ readConsistency: "primary"
915
+ },
916
+ auth: {
917
+ credentials: ["user_key", "partner_key"],
918
+ scopes: ["users:read"],
919
+ ownership: [{
920
+ credential: "user_key",
921
+ rule: "self"
922
+ }, {
923
+ credential: "partner_key",
924
+ rule: "linked_user"
925
+ }]
926
+ },
927
+ request: {
928
+ params: z.strictObject({ userId: realtimeSchemas.publicUserId }),
929
+ query: null,
930
+ headers: requestHeaders,
931
+ body: null
932
+ },
933
+ responses: { 200: lifecycleResponse },
934
+ errors: partnerUserLifecycleErrors,
935
+ errorResponse: errorEnvelopeSchemas(partnerUserLifecycleErrors, realtimeSchemas.publicRequestId),
936
+ resources: {
937
+ reads: [{
938
+ type: "partner.user",
939
+ idFrom: "params.userId"
940
+ }, {
941
+ type: "user.sites",
942
+ idFrom: "params.userId"
943
+ }],
944
+ changes: []
945
+ },
946
+ lifecycle: { introduced: "1.0.0" },
947
+ docs: {
948
+ summary: "Get user lifecycle",
949
+ description: "Returns the authoritative lifecycle state for one visible user and their sites.",
950
+ tags: ["Users"],
951
+ examples: {
952
+ request: { params: { userId: "u_01" } },
953
+ response: {
954
+ data: {
955
+ userId: "u_01",
956
+ partnerId: "p_01",
957
+ currentTeamId: null,
958
+ account: {
959
+ status: "ready",
960
+ grantedScopes: ["https://www.googleapis.com/auth/webmasters.readonly"],
961
+ missingScopes: [],
962
+ nextAction: "none"
963
+ },
964
+ sites: []
965
+ },
966
+ meta: {
967
+ requestId: "req_01",
968
+ surface: "partner",
969
+ version: "1.0"
970
+ }
971
+ }
972
+ }
973
+ }
974
+ }) }
975
+ });
976
+ const analytics = defineHttpSurface({
977
+ name: "analytics",
978
+ prefix: "/api/analytics/v1",
979
+ version: "1.0",
980
+ operations: { queryRows: defineHttpOperation({
981
+ id: "analytics.rows.query",
982
+ method: "POST",
983
+ path: "/sites/{siteId}/rows",
984
+ visibility: "public",
985
+ semantics: {
986
+ kind: "query",
987
+ sideEffects: "none",
988
+ idempotent: true,
989
+ retry: "idempotent",
990
+ readConsistency: "primary"
991
+ },
992
+ auth: {
993
+ credentials: ["user_key", "partner_key"],
994
+ scopes: ["analytics:execute"],
995
+ ownership: [{
996
+ credential: "user_key",
997
+ rule: "authorized_site"
998
+ }, {
999
+ credential: "partner_key",
1000
+ rule: "authorized_site"
1001
+ }]
1002
+ },
1003
+ request: {
1004
+ params: z.strictObject({ siteId: realtimeSchemas.publicSiteId }),
1005
+ query: null,
1006
+ headers: requestHeaders,
1007
+ body: analyticsRowsRequest
1008
+ },
1009
+ responses: { 200: analyticsRowsResponse },
1010
+ errors: analyticsRowsErrors,
1011
+ errorResponse: errorEnvelopeSchemas(analyticsRowsErrors, realtimeSchemas.publicRequestId),
1012
+ resources: {
1013
+ reads: [{
1014
+ type: "site.analytics",
1015
+ idFrom: "params.siteId"
1016
+ }],
1017
+ changes: []
1018
+ },
1019
+ lifecycle: { introduced: "1.0.0" },
1020
+ docs: {
1021
+ summary: "Query analytics rows",
1022
+ description: "Executes an idempotent primary-consistent analytics row query.",
1023
+ tags: ["Analytics rows"],
1024
+ examples: {
1025
+ request: {
1026
+ params: { siteId: "s_01" },
1027
+ body: {
1028
+ dimensions: ["query"],
1029
+ metrics: ["clicks", "impressions"],
1030
+ rowLimit: 100
1031
+ }
1032
+ },
1033
+ response: {
1034
+ data: { rows: [] },
1035
+ meta: {
1036
+ requestId: "req_01",
1037
+ surface: "analytics",
1038
+ version: "1.0",
1039
+ sourceName: "primary",
1040
+ sourceKind: "sql",
1041
+ queryMs: 12
1042
+ }
1043
+ }
1044
+ }
1045
+ }
1046
+ }) }
1047
+ });
1048
+ const realtime = defineHttpSurface({
1049
+ name: "realtime",
1050
+ prefix: "/api/realtime/v1",
1051
+ version: "1.0",
1052
+ operations: {
1053
+ getStreamHead: defineHttpOperation({
1054
+ id: "realtime.stream.head.get",
1055
+ method: "GET",
1056
+ path: "/stream/head",
1057
+ visibility: "public",
1058
+ semantics: {
1059
+ kind: "query",
1060
+ sideEffects: "none",
1061
+ idempotent: true,
1062
+ retry: "idempotent",
1063
+ readConsistency: "primary"
1064
+ },
1065
+ auth: {
1066
+ credentials: ["user_key", "partner_key"],
1067
+ scopes: ["realtime:connect"],
1068
+ ownership: [{
1069
+ credential: "user_key",
1070
+ rule: "principal_stream"
1071
+ }, {
1072
+ credential: "partner_key",
1073
+ rule: "principal_stream"
1074
+ }]
1075
+ },
1076
+ request: {
1077
+ params: null,
1078
+ query: null,
1079
+ headers: requestHeaders,
1080
+ body: null
1081
+ },
1082
+ responses: { 200: headResponse },
1083
+ errors: realtimeErrors,
1084
+ errorResponse: errorEnvelopeSchemas(realtimeErrors, realtimeSchemas.publicRequestId),
1085
+ resources: {
1086
+ reads: [],
1087
+ changes: []
1088
+ },
1089
+ lifecycle: { introduced: "1.0.0" },
1090
+ docs: {
1091
+ summary: "Get the inferred stream head",
1092
+ description: "Returns the current durable head for the credential's one exact stream.",
1093
+ tags: ["Realtime"],
1094
+ examples: {
1095
+ request: {},
1096
+ response: {
1097
+ data: { head: {
1098
+ streamId: "user:u_01",
1099
+ sequence: "142"
1100
+ } },
1101
+ meta: {
1102
+ requestId: "req_01",
1103
+ surface: "realtime",
1104
+ version: "1.0"
1105
+ }
1106
+ }
1107
+ }
1108
+ }
1109
+ }),
1110
+ createTicket: defineHttpOperation({
1111
+ id: "realtime.tickets.create",
1112
+ method: "POST",
1113
+ path: "/tickets",
1114
+ visibility: "public",
1115
+ semantics: {
1116
+ kind: "mutation",
1117
+ sideEffects: "state",
1118
+ idempotent: false,
1119
+ retry: "never",
1120
+ readConsistency: null
1121
+ },
1122
+ auth: {
1123
+ credentials: ["user_key", "partner_key"],
1124
+ scopes: ["realtime:connect"],
1125
+ ownership: [{
1126
+ credential: "user_key",
1127
+ rule: "principal_stream"
1128
+ }, {
1129
+ credential: "partner_key",
1130
+ rule: "principal_stream"
1131
+ }]
1132
+ },
1133
+ request: {
1134
+ params: null,
1135
+ query: null,
1136
+ headers: requestHeaders,
1137
+ body: realtimeSchemas.ticketRequest
1138
+ },
1139
+ responses: { 201: ticketResponse },
1140
+ errors: realtimeErrors,
1141
+ errorResponse: errorEnvelopeSchemas(realtimeErrors, realtimeSchemas.publicRequestId),
1142
+ resources: {
1143
+ reads: [],
1144
+ changes: []
1145
+ },
1146
+ lifecycle: { introduced: "1.0.0" },
1147
+ docs: {
1148
+ summary: "Create a realtime ticket",
1149
+ description: "Creates a short-lived single-use ticket for the credential's inferred stream.",
1150
+ tags: ["Realtime"],
1151
+ examples: {
1152
+ request: { body: { origin: "https://nuxtseo.com" } },
1153
+ response: {
1154
+ data: {
1155
+ socketUrl: "wss://gscdump.com/ws/v1",
1156
+ protocol: GSCDUMP_REALTIME_SUBPROTOCOL,
1157
+ protocolVersion: 1,
1158
+ ticket: "gscdump.ticket.v1.cGF5bG9hZA.c2lnbmF0dXJl",
1159
+ expiresAt: "2026-07-14T08:01:00.000Z",
1160
+ head: {
1161
+ streamId: "user:u_01",
1162
+ sequence: "142"
1163
+ },
1164
+ maxConnectionSeconds: 900
1165
+ },
1166
+ meta: {
1167
+ requestId: "req_01",
1168
+ surface: "realtime",
1169
+ version: "1.0"
1170
+ }
1171
+ }
1172
+ }
1173
+ }
1174
+ })
1175
+ }
1176
+ });
1177
+ return {
1178
+ constants: {
1179
+ httpVersion: "1.0",
1180
+ realtimeProtocolVersion: 1,
1181
+ realtimeSubprotocol: GSCDUMP_REALTIME_SUBPROTOCOL,
1182
+ realtimeTicketPrefix: GSCDUMP_REALTIME_TICKET_PREFIX,
1183
+ realtimeTicketIssuer: GSCDUMP_REALTIME_TICKET_ISSUER,
1184
+ realtimeTicketAudience: GSCDUMP_REALTIME_TICKET_AUDIENCE,
1185
+ ping: GSCDUMP_REALTIME_PING,
1186
+ pong: GSCDUMP_REALTIME_PONG,
1187
+ ticketTtlSeconds: 60,
1188
+ maxConnectionSeconds: 900,
1189
+ ticketPolicy: GSCDUMP_REALTIME_TICKET_POLICY,
1190
+ connectionPolicy: GSCDUMP_REALTIME_CONNECTION_POLICY,
1191
+ replayPolicy: GSCDUMP_REALTIME_REPLAY_POLICY,
1192
+ limits: GSCDUMP_REALTIME_LIMITS,
1193
+ ackPolicy: GSCDUMP_REALTIME_ACK_POLICY,
1194
+ closeCodes: GSCDUMP_REALTIME_CLOSE_CODES,
1195
+ eventSemantics: REALTIME_V1_EVENT_SEMANTICS
1196
+ },
1197
+ schemas: {
1198
+ requestMetadata,
1199
+ requestHeaders,
1200
+ responseMeta,
1201
+ errorEnvelope,
1202
+ analyticsRowsRequest,
1203
+ analyticsRowsResponse,
1204
+ lifecycleResponse,
1205
+ streamHeadResponse: headResponse,
1206
+ ticketResponse,
1207
+ ...realtimeSchemas
1208
+ },
1209
+ surfaces: {
1210
+ partner,
1211
+ analytics,
1212
+ realtime
1213
+ }
1214
+ };
1215
+ }
1216
+ function compareStrings(left, right) {
1217
+ return left < right ? -1 : left > right ? 1 : 0;
1218
+ }
1219
+ function orderedJson(value) {
1220
+ if (Array.isArray(value)) return value.map(orderedJson);
1221
+ if (value === null || typeof value !== "object") return value;
1222
+ return Object.fromEntries(Object.entries(value).sort(([left], [right]) => compareStrings(left, right)).map(([key, item]) => [key, orderedJson(item)]));
1223
+ }
1224
+ function serializeContractDocument(document) {
1225
+ return `${JSON.stringify(orderedJson(document), null, 2)}\n`;
1226
+ }
1227
+ function jsonSchema(schema) {
1228
+ const { $schema: _schema, ...body } = z.toJSONSchema(schema);
1229
+ return orderedJson(body);
1230
+ }
1231
+ function rewriteDefinitionRefs(value, references) {
1232
+ if (Array.isArray(value)) return value.map((item) => rewriteDefinitionRefs(item, references));
1233
+ if (value === null || typeof value !== "object") return value;
1234
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => {
1235
+ if (key === "$ref" && typeof item === "string" && item.startsWith("#/$defs/")) {
1236
+ const name = item.slice(8);
1237
+ const reference = references[name];
1238
+ if (!reference) throw new TypeError(`JSON Schema references an unknown definition: ${name}`);
1239
+ return [key, reference];
1240
+ }
1241
+ return [key, rewriteDefinitionRefs(item, references)];
1242
+ }));
1243
+ }
1244
+ function registerComponentSchema(schema, prefix, components) {
1245
+ const { $defs, ...body } = jsonSchema(schema);
1246
+ if (!$defs) return body;
1247
+ const definitions = $defs;
1248
+ const safePrefix = prefix.replace(/[^\w.-]/g, "_");
1249
+ const references = Object.fromEntries(Object.keys(definitions).map((name) => {
1250
+ return [name, `#/components/schemas/${safePrefix}.${name}`];
1251
+ }));
1252
+ for (const [name, definition] of Object.entries(definitions)) {
1253
+ const componentName = `${safePrefix}.${name}`;
1254
+ if (componentName in components) throw new TypeError(`Duplicate generated component schema: ${componentName}`);
1255
+ components[componentName] = rewriteDefinitionRefs(definition, references);
1256
+ }
1257
+ return rewriteDefinitionRefs(body, references);
1258
+ }
1259
+ function objectSchemaProperties(schema) {
1260
+ const document = jsonSchema(schema);
1261
+ return {
1262
+ properties: document.properties ?? {},
1263
+ required: new Set(document.required ?? [])
1264
+ };
1265
+ }
1266
+ function requestParameters(operation) {
1267
+ const parameters = [];
1268
+ if (operation.request.params) {
1269
+ const { properties } = objectSchemaProperties(operation.request.params);
1270
+ for (const name of operation.path.matchAll(/\{([^{}]+)\}/g)) parameters.push({
1271
+ name: name[1],
1272
+ in: "path",
1273
+ required: true,
1274
+ schema: properties[name[1]] ?? {}
1275
+ });
1276
+ }
1277
+ for (const location of ["query", "headers"]) {
1278
+ const schema = operation.request[location];
1279
+ if (!schema) continue;
1280
+ const { properties, required } = objectSchemaProperties(schema);
1281
+ for (const [name, property] of Object.entries(properties)) parameters.push({
1282
+ name,
1283
+ in: location === "headers" ? "header" : "query",
1284
+ required: required.has(name),
1285
+ schema: property
1286
+ });
1287
+ }
1288
+ return parameters;
1289
+ }
1290
+ function operationDocument(operation, components) {
1291
+ const errorComponentName = `${operation.id}.error`;
1292
+ if (errorComponentName in components) throw new TypeError(`Duplicate generated component schema: ${errorComponentName}`);
1293
+ components[errorComponentName] = registerComponentSchema(operation.errorResponse.producer, errorComponentName, components);
1294
+ const errorSchema = { $ref: `#/components/schemas/${errorComponentName}` };
1295
+ const responses = Object.fromEntries(Object.entries(operation.responses).sort(([left], [right]) => Number(left) - Number(right)).map(([status, response]) => [status, {
1296
+ description: status.startsWith("2") ? "Successful response" : "Documented response",
1297
+ content: { "application/json": { schema: registerComponentSchema(response.producer, `${operation.id}.response.${status}`, components) } }
1298
+ }]));
1299
+ responses.default = {
1300
+ description: "Stable error envelope",
1301
+ content: { "application/json": { schema: errorSchema } }
1302
+ };
1303
+ responses["4XX"] = {
1304
+ description: "Declared client error",
1305
+ content: { "application/json": { schema: errorSchema } }
1306
+ };
1307
+ responses["5XX"] = {
1308
+ description: "Safe internal or contract error",
1309
+ content: { "application/json": { schema: errorSchema } }
1310
+ };
1311
+ const document = {
1312
+ "operationId": operation.id,
1313
+ "summary": operation.docs.summary,
1314
+ "description": operation.docs.description,
1315
+ "tags": operation.docs.tags,
1316
+ "security": operation.auth.credentials.map((credential) => ({ [credential]: [] })),
1317
+ "parameters": requestParameters(operation),
1318
+ responses,
1319
+ "x-gscdump-errors": operation.errors,
1320
+ "x-gscdump-examples": operation.docs.examples,
1321
+ "x-gscdump-lifecycle": operation.lifecycle,
1322
+ "x-gscdump-resources": operation.resources,
1323
+ "x-gscdump-scopes": operation.auth.scopes,
1324
+ "x-gscdump-ownership": operation.auth.ownership,
1325
+ "x-gscdump-semantics": operation.semantics,
1326
+ "x-gscdump-visibility": operation.visibility
1327
+ };
1328
+ if (operation.request.body) document.requestBody = {
1329
+ required: true,
1330
+ content: { "application/json": { schema: registerComponentSchema(operation.request.body, `${operation.id}.request.body`, components) } }
1331
+ };
1332
+ return document;
1333
+ }
1334
+ function openApiDocument(surface) {
1335
+ const paths = {};
1336
+ const schemas = {};
1337
+ for (const operation of Object.values(surface.operations).sort((left, right) => {
1338
+ return compareStrings(`${left.path}\0${left.method}\0${left.id}`, `${right.path}\0${right.method}\0${right.id}`);
1339
+ })) {
1340
+ const path = `${surface.prefix}${operation.path}`;
1341
+ paths[path] ??= {};
1342
+ paths[path][operation.method.toLowerCase()] = operationDocument(operation, schemas);
1343
+ }
1344
+ return {
1345
+ openapi: "3.1.0",
1346
+ jsonSchemaDialect: "https://json-schema.org/draft/2020-12/schema",
1347
+ info: {
1348
+ title: `gscdump ${surface.name} API`,
1349
+ version: surface.version,
1350
+ license: {
1351
+ name: "MIT",
1352
+ identifier: "MIT"
1353
+ }
1354
+ },
1355
+ servers: [{ url: "https://gscdump.com" }],
1356
+ paths,
1357
+ components: {
1358
+ schemas,
1359
+ securitySchemes: {
1360
+ user_key: {
1361
+ type: "http",
1362
+ scheme: "bearer",
1363
+ bearerFormat: "gscdump user key"
1364
+ },
1365
+ partner_key: {
1366
+ type: "http",
1367
+ scheme: "bearer",
1368
+ bearerFormat: "gscdump partner key"
1369
+ }
1370
+ }
1371
+ }
1372
+ };
1373
+ }
1374
+ function asyncApiDocument(protocol) {
1375
+ const schemas = {};
1376
+ const clientFrameSchema = registerComponentSchema(protocol.schemas.clientFrame, "clientFrame", schemas);
1377
+ const serverFrameSchema = registerComponentSchema(protocol.schemas.serverFrame, "serverFrame", schemas);
1378
+ return {
1379
+ "asyncapi": "3.1.0",
1380
+ "info": {
1381
+ title: "gscdump realtime API",
1382
+ version: "1.0"
1383
+ },
1384
+ "servers": { production: {
1385
+ "host": "gscdump.com",
1386
+ "pathname": "/ws/v1",
1387
+ "protocol": "wss",
1388
+ "x-websocket-subprotocol": protocol.constants.realtimeSubprotocol
1389
+ } },
1390
+ "channels": { notifications: {
1391
+ address: "/",
1392
+ messages: {
1393
+ clientFrame: { $ref: "#/components/messages/clientFrame" },
1394
+ serverFrame: { $ref: "#/components/messages/serverFrame" }
1395
+ }
1396
+ } },
1397
+ "operations": {
1398
+ receiveServerFrame: {
1399
+ action: "receive",
1400
+ channel: { $ref: "#/channels/notifications" },
1401
+ messages: [{ $ref: "#/channels/notifications/messages/serverFrame" }]
1402
+ },
1403
+ sendClientFrame: {
1404
+ action: "send",
1405
+ channel: { $ref: "#/channels/notifications" },
1406
+ messages: [{ $ref: "#/channels/notifications/messages/clientFrame" }]
1407
+ }
1408
+ },
1409
+ "components": {
1410
+ messages: {
1411
+ clientFrame: {
1412
+ name: "clientFrame",
1413
+ contentType: "application/json",
1414
+ payload: clientFrameSchema
1415
+ },
1416
+ serverFrame: {
1417
+ name: "serverFrame",
1418
+ contentType: "application/json",
1419
+ payload: serverFrameSchema
1420
+ }
1421
+ },
1422
+ schemas
1423
+ },
1424
+ "x-heartbeat-text-frames": {
1425
+ ping: protocol.constants.ping,
1426
+ pong: protocol.constants.pong
1427
+ },
1428
+ "x-gscdump-event-semantics": protocol.constants.eventSemantics
1429
+ };
1430
+ }
1431
+ function createGscdumpV1Documents() {
1432
+ const protocol = createGscdumpV1Protocol();
1433
+ return {
1434
+ "openapi.partner.v1.json": openApiDocument(protocol.surfaces.partner),
1435
+ "openapi.analytics.v1.json": openApiDocument(protocol.surfaces.analytics),
1436
+ "openapi.realtime.v1.json": openApiDocument(protocol.surfaces.realtime),
1437
+ "asyncapi.realtime.v1.json": asyncApiDocument(protocol)
1438
+ };
1439
+ }
1440
+ export { GSCDUMP_HTTP_V1_VERSION, GSCDUMP_REALTIME_ACK_POLICY, GSCDUMP_REALTIME_CLOSE_CODES, GSCDUMP_REALTIME_CONNECTION_POLICY, GSCDUMP_REALTIME_LIMITS, GSCDUMP_REALTIME_MAX_CONNECTION_SECONDS, GSCDUMP_REALTIME_PING, GSCDUMP_REALTIME_PONG, GSCDUMP_REALTIME_PROTOCOL_VERSION, GSCDUMP_REALTIME_REPLAY_POLICY, GSCDUMP_REALTIME_SUBPROTOCOL, GSCDUMP_REALTIME_TICKET_AUDIENCE, GSCDUMP_REALTIME_TICKET_ISSUER, GSCDUMP_REALTIME_TICKET_POLICY, GSCDUMP_REALTIME_TICKET_PREFIX, GSCDUMP_REALTIME_TICKET_TTL_SECONDS, HTTP_V1_CREDENTIALS, HTTP_V1_CREDENTIAL_SCOPES, HTTP_V1_ERROR_CODES, HTTP_V1_METHODS, HTTP_V1_SCOPES, HTTP_V1_SURFACES, REALTIME_V1_EVENT_NAMES, REALTIME_V1_EVENT_SEMANTICS, REALTIME_V1_RESOURCE_TYPES, buildHttpOperationPath, createGscdumpV1Documents, createGscdumpV1Protocol, createRealtimeV1Schemas, defineHttpOperation, defineHttpSurface, defineResponseObject, defineSuccessResponse, serializeContractDocument };