@gscdump/contracts 0.37.6 → 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.
- package/README.md +27 -0
- package/dist/_chunks/endpoints.d.mts +4 -4
- package/dist/_chunks/onboarding.mjs +130 -0
- package/dist/_chunks/schemas.d.mts +11 -11
- package/dist/_chunks/schemas.mjs +2 -130
- package/dist/archetypes.d.mts +1 -1
- package/dist/index.mjs +2 -1
- package/dist/partner.mjs +2 -1
- package/dist/v1/index.d.mts +2825 -0
- package/dist/v1/index.mjs +1440 -0
- package/package.json +8 -2
|
@@ -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 };
|