@gscdump/contracts 1.3.2 → 1.4.1
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/dist/analytics.d.mts +5 -3
- package/dist/analytics.mjs +3 -3
- package/dist/archetypes.d.mts +1 -1
- package/dist/{_chunks/endpoints.d.mts → endpoints.d.mts} +204 -204
- package/dist/file-resolution.d.mts +155 -0
- package/dist/index.d.mts +6 -3
- package/dist/index.mjs +4 -2
- package/dist/onboarding.d.mts +113 -0
- package/dist/onboarding.mjs +122 -0
- package/dist/partner.d.mts +6 -4
- package/dist/partner.mjs +5 -3
- package/dist/routes.d.mts +88 -0
- package/dist/{_chunks/schemas.d.mts → schemas.d.mts} +2556 -2643
- package/dist/{_chunks/schemas.mjs → schemas.mjs} +3 -137
- package/dist/{_chunks/types.d.mts → types.d.mts} +3 -266
- package/dist/v1/documents.d.mts +4 -0
- package/dist/v1/documents.mjs +227 -0
- package/dist/v1/http.d.mts +94 -0
- package/dist/v1/http.mjs +162 -0
- package/dist/v1/index.d.mts +4 -12440
- package/dist/v1/index.mjs +4 -5107
- package/dist/v1/operations.d.mts +11699 -0
- package/dist/v1/operations.mjs +4282 -0
- package/dist/v1/realtime.d.mts +650 -0
- package/dist/v1/realtime.mjs +447 -0
- package/dist/webhook-constants.mjs +16 -0
- package/package.json +2 -2
- package/dist/{_chunks/endpoints.mjs → endpoints.mjs} +1 -1
- /package/dist/{_chunks/routes.mjs → routes.mjs} +0 -0
- /package/dist/{_chunks/webhook-constants.d.mts → webhook-constants.d.mts} +0 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { createGscdumpV1Protocol } from "./operations.mjs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
function compareStrings(left, right) {
|
|
4
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
5
|
+
}
|
|
6
|
+
function orderedJson(value) {
|
|
7
|
+
if (Array.isArray(value)) return value.map(orderedJson);
|
|
8
|
+
if (value === null || typeof value !== "object") return value;
|
|
9
|
+
return Object.fromEntries(Object.entries(value).sort(([left], [right]) => compareStrings(left, right)).map(([key, item]) => [key, orderedJson(item)]));
|
|
10
|
+
}
|
|
11
|
+
function serializeContractDocument(document) {
|
|
12
|
+
return `${JSON.stringify(orderedJson(document), null, 2)}\n`;
|
|
13
|
+
}
|
|
14
|
+
function jsonSchema(schema) {
|
|
15
|
+
const { $schema: _schema, ...body } = z.toJSONSchema(schema);
|
|
16
|
+
return orderedJson(body);
|
|
17
|
+
}
|
|
18
|
+
function rewriteDefinitionRefs(value, references) {
|
|
19
|
+
if (Array.isArray(value)) return value.map((item) => rewriteDefinitionRefs(item, references));
|
|
20
|
+
if (value === null || typeof value !== "object") return value;
|
|
21
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => {
|
|
22
|
+
if (key === "$ref" && typeof item === "string" && item.startsWith("#/$defs/")) {
|
|
23
|
+
const name = item.slice(8);
|
|
24
|
+
const reference = references[name];
|
|
25
|
+
if (!reference) throw new TypeError(`JSON Schema references an unknown definition: ${name}`);
|
|
26
|
+
return [key, reference];
|
|
27
|
+
}
|
|
28
|
+
return [key, rewriteDefinitionRefs(item, references)];
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
function registerComponentSchema(schema, prefix, components) {
|
|
32
|
+
const { $defs, ...body } = jsonSchema(schema);
|
|
33
|
+
if (!$defs) return body;
|
|
34
|
+
const definitions = $defs;
|
|
35
|
+
const safePrefix = prefix.replace(/[^\w.-]/g, "_");
|
|
36
|
+
const references = Object.fromEntries(Object.keys(definitions).map((name) => {
|
|
37
|
+
return [name, `#/components/schemas/${safePrefix}.${name}`];
|
|
38
|
+
}));
|
|
39
|
+
for (const [name, definition] of Object.entries(definitions)) {
|
|
40
|
+
const componentName = `${safePrefix}.${name}`;
|
|
41
|
+
if (componentName in components) throw new TypeError(`Duplicate generated component schema: ${componentName}`);
|
|
42
|
+
components[componentName] = rewriteDefinitionRefs(definition, references);
|
|
43
|
+
}
|
|
44
|
+
return rewriteDefinitionRefs(body, references);
|
|
45
|
+
}
|
|
46
|
+
function objectSchemaProperties(schema) {
|
|
47
|
+
const document = jsonSchema(schema);
|
|
48
|
+
return {
|
|
49
|
+
properties: document.properties ?? {},
|
|
50
|
+
required: new Set(document.required ?? [])
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function requestParameters(operation) {
|
|
54
|
+
const parameters = [];
|
|
55
|
+
if (operation.request.params) {
|
|
56
|
+
const { properties } = objectSchemaProperties(operation.request.params);
|
|
57
|
+
for (const name of operation.path.matchAll(/\{([^{}]+)\}/g)) parameters.push({
|
|
58
|
+
name: name[1],
|
|
59
|
+
in: "path",
|
|
60
|
+
required: true,
|
|
61
|
+
schema: properties[name[1]] ?? {}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
for (const location of ["query", "headers"]) {
|
|
65
|
+
const schema = operation.request[location];
|
|
66
|
+
if (!schema) continue;
|
|
67
|
+
const { properties, required } = objectSchemaProperties(schema);
|
|
68
|
+
for (const [name, property] of Object.entries(properties)) parameters.push({
|
|
69
|
+
name,
|
|
70
|
+
in: location === "headers" ? "header" : "query",
|
|
71
|
+
required: required.has(name),
|
|
72
|
+
schema: property
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return parameters;
|
|
76
|
+
}
|
|
77
|
+
function operationDocument(operation, components) {
|
|
78
|
+
const errorComponentName = `${operation.id}.error`;
|
|
79
|
+
if (errorComponentName in components) throw new TypeError(`Duplicate generated component schema: ${errorComponentName}`);
|
|
80
|
+
components[errorComponentName] = registerComponentSchema(operation.errorResponse.producer, errorComponentName, components);
|
|
81
|
+
const errorSchema = { $ref: `#/components/schemas/${errorComponentName}` };
|
|
82
|
+
const responses = Object.fromEntries(Object.entries(operation.responses).sort(([left], [right]) => Number(left) - Number(right)).map(([status, response]) => [status, {
|
|
83
|
+
description: status.startsWith("2") ? "Successful response" : "Documented response",
|
|
84
|
+
content: { "application/json": { schema: registerComponentSchema(response.producer, `${operation.id}.response.${status}`, components) } }
|
|
85
|
+
}]));
|
|
86
|
+
responses.default = {
|
|
87
|
+
description: "Stable error envelope",
|
|
88
|
+
content: { "application/json": { schema: errorSchema } }
|
|
89
|
+
};
|
|
90
|
+
responses["4XX"] = {
|
|
91
|
+
description: "Declared client error",
|
|
92
|
+
content: { "application/json": { schema: errorSchema } }
|
|
93
|
+
};
|
|
94
|
+
responses["5XX"] = {
|
|
95
|
+
description: "Safe internal or contract error",
|
|
96
|
+
content: { "application/json": { schema: errorSchema } }
|
|
97
|
+
};
|
|
98
|
+
const document = {
|
|
99
|
+
"operationId": operation.id,
|
|
100
|
+
"summary": operation.docs.summary,
|
|
101
|
+
"description": operation.docs.description,
|
|
102
|
+
"tags": operation.docs.tags,
|
|
103
|
+
"security": operation.auth.credentials.map((credential) => ({ [credential]: [] })),
|
|
104
|
+
"parameters": requestParameters(operation),
|
|
105
|
+
responses,
|
|
106
|
+
"x-gscdump-errors": operation.errors,
|
|
107
|
+
"x-gscdump-examples": operation.docs.examples,
|
|
108
|
+
"x-gscdump-lifecycle": operation.lifecycle,
|
|
109
|
+
"x-gscdump-resources": operation.resources,
|
|
110
|
+
"x-gscdump-scopes": operation.auth.scopes,
|
|
111
|
+
"x-gscdump-ownership": operation.auth.ownership,
|
|
112
|
+
"x-gscdump-semantics": operation.semantics,
|
|
113
|
+
"x-gscdump-visibility": operation.visibility
|
|
114
|
+
};
|
|
115
|
+
if (operation.request.body) document.requestBody = {
|
|
116
|
+
required: true,
|
|
117
|
+
content: { "application/json": { schema: registerComponentSchema(operation.request.body, `${operation.id}.request.body`, components) } }
|
|
118
|
+
};
|
|
119
|
+
return document;
|
|
120
|
+
}
|
|
121
|
+
function openApiDocument(surface) {
|
|
122
|
+
const paths = {};
|
|
123
|
+
const schemas = {};
|
|
124
|
+
for (const operation of Object.values(surface.operations).sort((left, right) => {
|
|
125
|
+
return compareStrings(`${left.path}\0${left.method}\0${left.id}`, `${right.path}\0${right.method}\0${right.id}`);
|
|
126
|
+
})) {
|
|
127
|
+
const path = `${surface.prefix}${operation.path}`;
|
|
128
|
+
paths[path] ??= {};
|
|
129
|
+
paths[path][operation.method.toLowerCase()] = operationDocument(operation, schemas);
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
openapi: "3.1.0",
|
|
133
|
+
jsonSchemaDialect: "https://json-schema.org/draft/2020-12/schema",
|
|
134
|
+
info: {
|
|
135
|
+
title: `gscdump ${surface.name} API`,
|
|
136
|
+
version: surface.version,
|
|
137
|
+
license: {
|
|
138
|
+
name: "MIT",
|
|
139
|
+
identifier: "MIT"
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
servers: [{ url: "https://gscdump.com" }],
|
|
143
|
+
paths,
|
|
144
|
+
components: {
|
|
145
|
+
schemas,
|
|
146
|
+
securitySchemes: {
|
|
147
|
+
user_key: {
|
|
148
|
+
type: "http",
|
|
149
|
+
scheme: "bearer",
|
|
150
|
+
bearerFormat: "gscdump user key"
|
|
151
|
+
},
|
|
152
|
+
partner_key: {
|
|
153
|
+
type: "http",
|
|
154
|
+
scheme: "bearer",
|
|
155
|
+
bearerFormat: "gscdump partner key"
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function asyncApiDocument(protocol) {
|
|
162
|
+
const schemas = {};
|
|
163
|
+
const clientFrameSchema = registerComponentSchema(protocol.schemas.clientFrame, "clientFrame", schemas);
|
|
164
|
+
const serverFrameSchema = registerComponentSchema(protocol.schemas.serverFrame, "serverFrame", schemas);
|
|
165
|
+
return {
|
|
166
|
+
"asyncapi": "3.1.0",
|
|
167
|
+
"info": {
|
|
168
|
+
title: "gscdump realtime API",
|
|
169
|
+
version: "1.0"
|
|
170
|
+
},
|
|
171
|
+
"servers": { production: {
|
|
172
|
+
"host": "gscdump.com",
|
|
173
|
+
"pathname": "/ws/v1",
|
|
174
|
+
"protocol": "wss",
|
|
175
|
+
"x-websocket-subprotocol": protocol.constants.realtimeSubprotocol
|
|
176
|
+
} },
|
|
177
|
+
"channels": { notifications: {
|
|
178
|
+
address: "/",
|
|
179
|
+
messages: {
|
|
180
|
+
clientFrame: { $ref: "#/components/messages/clientFrame" },
|
|
181
|
+
serverFrame: { $ref: "#/components/messages/serverFrame" }
|
|
182
|
+
}
|
|
183
|
+
} },
|
|
184
|
+
"operations": {
|
|
185
|
+
receiveServerFrame: {
|
|
186
|
+
action: "receive",
|
|
187
|
+
channel: { $ref: "#/channels/notifications" },
|
|
188
|
+
messages: [{ $ref: "#/channels/notifications/messages/serverFrame" }]
|
|
189
|
+
},
|
|
190
|
+
sendClientFrame: {
|
|
191
|
+
action: "send",
|
|
192
|
+
channel: { $ref: "#/channels/notifications" },
|
|
193
|
+
messages: [{ $ref: "#/channels/notifications/messages/clientFrame" }]
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
"components": {
|
|
197
|
+
messages: {
|
|
198
|
+
clientFrame: {
|
|
199
|
+
name: "clientFrame",
|
|
200
|
+
contentType: "application/json",
|
|
201
|
+
payload: clientFrameSchema
|
|
202
|
+
},
|
|
203
|
+
serverFrame: {
|
|
204
|
+
name: "serverFrame",
|
|
205
|
+
contentType: "application/json",
|
|
206
|
+
payload: serverFrameSchema
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
schemas
|
|
210
|
+
},
|
|
211
|
+
"x-heartbeat-text-frames": {
|
|
212
|
+
ping: protocol.constants.ping,
|
|
213
|
+
pong: protocol.constants.pong
|
|
214
|
+
},
|
|
215
|
+
"x-gscdump-event-semantics": protocol.constants.eventSemantics
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function createGscdumpV1Documents() {
|
|
219
|
+
const protocol = createGscdumpV1Protocol();
|
|
220
|
+
return {
|
|
221
|
+
"openapi.partner.v1.json": openApiDocument(protocol.surfaces.partner),
|
|
222
|
+
"openapi.analytics.v1.json": openApiDocument(protocol.surfaces.analytics),
|
|
223
|
+
"openapi.realtime.v1.json": openApiDocument(protocol.surfaces.realtime),
|
|
224
|
+
"asyncapi.realtime.v1.json": asyncApiDocument(protocol)
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
export { createGscdumpV1Documents, serializeContractDocument };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { ZodRawShape, ZodTypeAny, z } from "zod";
|
|
2
|
+
declare const HTTP_V1_SURFACES: readonly ['partner', 'analytics', 'realtime'];
|
|
3
|
+
declare const HTTP_V1_METHODS: readonly ['DELETE', 'GET', 'PATCH', 'POST'];
|
|
4
|
+
declare const HTTP_V1_CREDENTIALS: readonly ['user_key', 'partner_key'];
|
|
5
|
+
declare const HTTP_V1_SCOPES: readonly ["users:read", "users:write", "sites:read", "sites:write", "analytics:read", "analytics:execute", "indexing:read", "indexing:write", "sitemaps:read", "sitemaps:write", "settings:read", "settings:write", "realtime:connect", "teams:read", "teams:write"];
|
|
6
|
+
declare const HTTP_V1_CREDENTIAL_SCOPES: {
|
|
7
|
+
readonly user_key: readonly ["users:read", "users:write", "sites:read", "sites:write", "analytics:read", "analytics:execute", "indexing:read", "indexing:write", "sitemaps:read", "sitemaps:write", "settings:read", "settings:write", "realtime:connect"];
|
|
8
|
+
readonly partner_key: readonly ["users:read", "users:write", "sites:read", "sites:write", "analytics:read", "analytics:execute", "indexing:read", "indexing:write", "sitemaps:read", "sitemaps:write", "settings:read", "settings:write", "realtime:connect", "teams:read", "teams:write"];
|
|
9
|
+
};
|
|
10
|
+
declare const HTTP_V1_ERROR_CODES: readonly ['invalid_request', 'unauthorized', 'forbidden', 'user_not_found', 'site_not_found', 'rate_limited', 'realtime_unavailable', 'internal_error', 'contract_violation'];
|
|
11
|
+
type HttpV1SurfaceName = typeof HTTP_V1_SURFACES[number];
|
|
12
|
+
type HttpV1Method = typeof HTTP_V1_METHODS[number];
|
|
13
|
+
type HttpV1Credential = typeof HTTP_V1_CREDENTIALS[number];
|
|
14
|
+
type HttpV1Scope = typeof HTTP_V1_SCOPES[number];
|
|
15
|
+
type HttpV1ErrorCode = typeof HTTP_V1_ERROR_CODES[number];
|
|
16
|
+
type HttpV1Visibility = 'internal' | 'public';
|
|
17
|
+
type HttpV1OwnershipRule = 'self' | 'linked_user' | 'authorized_site' | 'partner_tenant' | 'principal_stream';
|
|
18
|
+
type HttpV1ResourceType = 'partner.user' | 'partner.team' | 'user.sites' | 'site.registration' | 'site.lifecycle' | 'site.analytics' | 'site.sitemaps' | 'site.indexing' | 'site.auth';
|
|
19
|
+
interface CompatibleResponseSchema<TProducer extends ZodTypeAny = ZodTypeAny, TClient extends ZodTypeAny = ZodTypeAny> {
|
|
20
|
+
producer: TProducer;
|
|
21
|
+
client: TClient;
|
|
22
|
+
}
|
|
23
|
+
interface HttpV1RequestSchemas {
|
|
24
|
+
params: ZodTypeAny | null;
|
|
25
|
+
query: ZodTypeAny | null;
|
|
26
|
+
headers: ZodTypeAny | null;
|
|
27
|
+
body: ZodTypeAny | null;
|
|
28
|
+
}
|
|
29
|
+
interface HttpV1Semantics {
|
|
30
|
+
kind: 'mutation' | 'query';
|
|
31
|
+
sideEffects: 'none' | 'state';
|
|
32
|
+
idempotent: boolean;
|
|
33
|
+
retry: 'idempotent' | 'never';
|
|
34
|
+
readConsistency: 'primary' | 'replica-ok' | null;
|
|
35
|
+
}
|
|
36
|
+
interface HttpV1ResourceReference {
|
|
37
|
+
type: HttpV1ResourceType;
|
|
38
|
+
idFrom: `params.${string}` | 'principal.id';
|
|
39
|
+
}
|
|
40
|
+
interface HttpV1OperationDefinition {
|
|
41
|
+
id: string;
|
|
42
|
+
method: HttpV1Method;
|
|
43
|
+
path: `/${string}`;
|
|
44
|
+
visibility: HttpV1Visibility;
|
|
45
|
+
semantics: HttpV1Semantics;
|
|
46
|
+
auth: {
|
|
47
|
+
credentials: readonly HttpV1Credential[];
|
|
48
|
+
scopes: readonly HttpV1Scope[];
|
|
49
|
+
ownership: readonly {
|
|
50
|
+
credential: HttpV1Credential;
|
|
51
|
+
rule: HttpV1OwnershipRule;
|
|
52
|
+
}[];
|
|
53
|
+
};
|
|
54
|
+
request: HttpV1RequestSchemas;
|
|
55
|
+
responses: Readonly<Record<number, CompatibleResponseSchema>>;
|
|
56
|
+
errors: readonly HttpV1ErrorCode[];
|
|
57
|
+
errorResponse: CompatibleResponseSchema;
|
|
58
|
+
resources: {
|
|
59
|
+
reads: readonly HttpV1ResourceReference[];
|
|
60
|
+
changes: readonly HttpV1ResourceReference[];
|
|
61
|
+
};
|
|
62
|
+
lifecycle: {
|
|
63
|
+
introduced: `${number}.${number}.${number}`;
|
|
64
|
+
deprecated?: `${number}.${number}.${number}`;
|
|
65
|
+
sunset?: string;
|
|
66
|
+
};
|
|
67
|
+
docs: {
|
|
68
|
+
summary: string;
|
|
69
|
+
description: string;
|
|
70
|
+
tags: readonly string[];
|
|
71
|
+
examples: {
|
|
72
|
+
request: unknown;
|
|
73
|
+
response: unknown;
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
interface HttpV1Surface<TOperations extends Readonly<Record<string, HttpV1OperationDefinition>> = Readonly<Record<string, HttpV1OperationDefinition>>> {
|
|
78
|
+
name: HttpV1SurfaceName;
|
|
79
|
+
prefix: `/api/${HttpV1SurfaceName}/v1`;
|
|
80
|
+
version: '1.0';
|
|
81
|
+
operations: TOperations;
|
|
82
|
+
}
|
|
83
|
+
declare function defineHttpOperation<const TOperation extends HttpV1OperationDefinition>(operation: TOperation): TOperation;
|
|
84
|
+
declare function defineHttpSurface<const TOperations extends Readonly<Record<string, HttpV1OperationDefinition>>>(surface: HttpV1Surface<TOperations>): HttpV1Surface<TOperations>;
|
|
85
|
+
declare function defineResponseObject<const TProducerShape extends ZodRawShape, const TClientShape extends ZodRawShape = TProducerShape>(producerShape: TProducerShape, clientShape?: TClientShape): CompatibleResponseSchema<z.ZodObject<TProducerShape>, z.ZodObject<TClientShape>>;
|
|
86
|
+
declare function defineSuccessResponse<const TDataProducer extends ZodTypeAny, const TDataClient extends ZodTypeAny, const TMetaProducer extends ZodTypeAny, const TMetaClient extends ZodTypeAny>(data: CompatibleResponseSchema<TDataProducer, TDataClient>, meta: CompatibleResponseSchema<TMetaProducer, TMetaClient>): CompatibleResponseSchema<z.ZodObject<{
|
|
87
|
+
data: TDataProducer;
|
|
88
|
+
meta: TMetaProducer;
|
|
89
|
+
}>, z.ZodObject<{
|
|
90
|
+
data: TDataClient;
|
|
91
|
+
meta: TMetaClient;
|
|
92
|
+
}>>;
|
|
93
|
+
declare function buildHttpOperationPath(surface: HttpV1Surface, operation: HttpV1OperationDefinition, params?: unknown): string;
|
|
94
|
+
export { CompatibleResponseSchema, HTTP_V1_CREDENTIALS, HTTP_V1_CREDENTIAL_SCOPES, HTTP_V1_ERROR_CODES, HTTP_V1_METHODS, HTTP_V1_SCOPES, HTTP_V1_SURFACES, HttpV1Credential, HttpV1ErrorCode, HttpV1Method, HttpV1OperationDefinition, HttpV1OwnershipRule, HttpV1RequestSchemas, HttpV1ResourceReference, HttpV1ResourceType, HttpV1Scope, HttpV1Semantics, HttpV1Surface, HttpV1SurfaceName, HttpV1Visibility, buildHttpOperationPath, defineHttpOperation, defineHttpSurface, defineResponseObject, defineSuccessResponse };
|
package/dist/v1/http.mjs
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const HTTP_V1_SURFACES = [
|
|
3
|
+
"partner",
|
|
4
|
+
"analytics",
|
|
5
|
+
"realtime"
|
|
6
|
+
];
|
|
7
|
+
const HTTP_V1_METHODS = [
|
|
8
|
+
"DELETE",
|
|
9
|
+
"GET",
|
|
10
|
+
"PATCH",
|
|
11
|
+
"POST"
|
|
12
|
+
];
|
|
13
|
+
const HTTP_V1_CREDENTIALS = ["user_key", "partner_key"];
|
|
14
|
+
const HTTP_V1_USER_KEY_SCOPES = [
|
|
15
|
+
"users:read",
|
|
16
|
+
"users:write",
|
|
17
|
+
"sites:read",
|
|
18
|
+
"sites:write",
|
|
19
|
+
"analytics:read",
|
|
20
|
+
"analytics:execute",
|
|
21
|
+
"indexing:read",
|
|
22
|
+
"indexing:write",
|
|
23
|
+
"sitemaps:read",
|
|
24
|
+
"sitemaps:write",
|
|
25
|
+
"settings:read",
|
|
26
|
+
"settings:write",
|
|
27
|
+
"realtime:connect"
|
|
28
|
+
];
|
|
29
|
+
const HTTP_V1_SCOPES = [
|
|
30
|
+
...HTTP_V1_USER_KEY_SCOPES,
|
|
31
|
+
"teams:read",
|
|
32
|
+
"teams:write"
|
|
33
|
+
];
|
|
34
|
+
const HTTP_V1_CREDENTIAL_SCOPES = {
|
|
35
|
+
user_key: HTTP_V1_USER_KEY_SCOPES,
|
|
36
|
+
partner_key: HTTP_V1_SCOPES
|
|
37
|
+
};
|
|
38
|
+
const HTTP_V1_ERROR_CODES = [
|
|
39
|
+
"invalid_request",
|
|
40
|
+
"unauthorized",
|
|
41
|
+
"forbidden",
|
|
42
|
+
"user_not_found",
|
|
43
|
+
"site_not_found",
|
|
44
|
+
"rate_limited",
|
|
45
|
+
"realtime_unavailable",
|
|
46
|
+
"internal_error",
|
|
47
|
+
"contract_violation"
|
|
48
|
+
];
|
|
49
|
+
function pathParameterNames(path) {
|
|
50
|
+
return [...path.matchAll(/\{([^{}]+)\}/g)].map((match) => match[1]);
|
|
51
|
+
}
|
|
52
|
+
function isSafePathTemplate(path) {
|
|
53
|
+
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);
|
|
54
|
+
}
|
|
55
|
+
function objectSchemaKeys(schema) {
|
|
56
|
+
return schema instanceof z.ZodObject ? Object.keys(schema.shape) : null;
|
|
57
|
+
}
|
|
58
|
+
function assertRequestLocations(request, operationId) {
|
|
59
|
+
for (const location of [
|
|
60
|
+
"params",
|
|
61
|
+
"query",
|
|
62
|
+
"headers",
|
|
63
|
+
"body"
|
|
64
|
+
]) if (!(location in request) || request[location] === void 0) throw new TypeError(`${operationId}: request.${location} must be a schema or null`);
|
|
65
|
+
}
|
|
66
|
+
function assertPathContract(operation) {
|
|
67
|
+
const names = pathParameterNames(operation.path);
|
|
68
|
+
if (new Set(names).size !== names.length) throw new TypeError(`${operation.id}: path parameters must be unique`);
|
|
69
|
+
if (names.length === 0) {
|
|
70
|
+
if (operation.request.params !== null) throw new TypeError(`${operation.id}: params must be null when the path has no parameters`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (operation.request.params === null) throw new TypeError(`${operation.id}: path parameters require a params schema`);
|
|
74
|
+
const schemaKeys = objectSchemaKeys(operation.request.params);
|
|
75
|
+
if (!schemaKeys) throw new TypeError(`${operation.id}: params must be an object schema`);
|
|
76
|
+
if (names.join("\0") !== schemaKeys.join("\0")) throw new TypeError(`${operation.id}: params schema keys (${schemaKeys.join(", ")}) must match path order (${names.join(", ")})`);
|
|
77
|
+
}
|
|
78
|
+
function assertOperation(operation) {
|
|
79
|
+
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`);
|
|
80
|
+
if (!isSafePathTemplate(operation.path)) throw new TypeError(`${operation.id}: path must be a safe literal surface-relative template`);
|
|
81
|
+
assertRequestLocations(operation.request, operation.id);
|
|
82
|
+
assertPathContract(operation);
|
|
83
|
+
if (operation.semantics.kind === "query") {
|
|
84
|
+
if (operation.semantics.sideEffects !== "none") throw new TypeError(`${operation.id}: query operations cannot declare state side effects`);
|
|
85
|
+
if (!operation.semantics.idempotent) throw new TypeError(`${operation.id}: query operations must be idempotent`);
|
|
86
|
+
if (operation.semantics.readConsistency === null) throw new TypeError(`${operation.id}: query operations must declare read consistency`);
|
|
87
|
+
} else {
|
|
88
|
+
if (operation.semantics.sideEffects !== "state") throw new TypeError(`${operation.id}: mutation operations must declare state side effects`);
|
|
89
|
+
if (operation.semantics.readConsistency !== null) throw new TypeError(`${operation.id}: mutations cannot declare read consistency`);
|
|
90
|
+
}
|
|
91
|
+
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`);
|
|
92
|
+
if (operation.semantics.idempotent && operation.semantics.retry !== "idempotent") throw new TypeError(`${operation.id}: idempotent operations must use the idempotent retry policy`);
|
|
93
|
+
if (!operation.semantics.idempotent && operation.semantics.retry !== "never") throw new TypeError(`${operation.id}: non-idempotent operations cannot be retried automatically`);
|
|
94
|
+
if (operation.auth.credentials.length === 0 || operation.auth.scopes.length === 0) throw new TypeError(`${operation.id}: auth credentials and scopes are required`);
|
|
95
|
+
for (const credential of operation.auth.credentials) {
|
|
96
|
+
const availableScopes = HTTP_V1_CREDENTIAL_SCOPES[credential];
|
|
97
|
+
const unavailableScope = operation.auth.scopes.find((scope) => !availableScopes.includes(scope));
|
|
98
|
+
if (unavailableScope) throw new TypeError(`${operation.id}: ${credential} does not grant the ${unavailableScope} scope`);
|
|
99
|
+
}
|
|
100
|
+
const ownershipCredentials = operation.auth.ownership.map((ownership) => ownership.credential);
|
|
101
|
+
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`);
|
|
102
|
+
if (operation.errors.length === 0) throw new TypeError(`${operation.id}: declared stable errors are required`);
|
|
103
|
+
if (!operation.errorResponse?.producer || !operation.errorResponse.client) throw new TypeError(`${operation.id}: an operation-specific error response schema is required`);
|
|
104
|
+
if (Object.keys(operation.responses).length === 0) throw new TypeError(`${operation.id}: at least one response is required`);
|
|
105
|
+
const pathParameters = new Set(pathParameterNames(operation.path));
|
|
106
|
+
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`);
|
|
107
|
+
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`);
|
|
108
|
+
}
|
|
109
|
+
function defineHttpOperation(operation) {
|
|
110
|
+
assertOperation(operation);
|
|
111
|
+
return operation;
|
|
112
|
+
}
|
|
113
|
+
function defineHttpSurface(surface) {
|
|
114
|
+
if (surface.prefix !== `/api/${surface.name}/v1`) throw new TypeError(`${surface.name}: prefix must retain the surface and v1 major`);
|
|
115
|
+
if (surface.version !== "1.0") throw new TypeError(`${surface.name}: wire version must be 1.0`);
|
|
116
|
+
const ids = /* @__PURE__ */ new Set();
|
|
117
|
+
const routes = /* @__PURE__ */ new Set();
|
|
118
|
+
for (const [key, operation] of Object.entries(surface.operations)) {
|
|
119
|
+
assertOperation(operation);
|
|
120
|
+
if (!operation.id.startsWith(`${surface.name}.`)) throw new TypeError(`${surface.name}: operation ID ${operation.id} must use the surface namespace`);
|
|
121
|
+
if (ids.has(operation.id)) throw new TypeError(`${surface.name}: duplicate operation ID ${operation.id}`);
|
|
122
|
+
ids.add(operation.id);
|
|
123
|
+
const route = `${operation.method} ${operation.path}`;
|
|
124
|
+
if (routes.has(route)) throw new TypeError(`${surface.name}: duplicate method/path ${route}`);
|
|
125
|
+
routes.add(route);
|
|
126
|
+
if (key.length === 0) throw new TypeError(`${surface.name}: operation registry keys cannot be empty`);
|
|
127
|
+
}
|
|
128
|
+
return surface;
|
|
129
|
+
}
|
|
130
|
+
function defineResponseObject(producerShape, clientShape) {
|
|
131
|
+
return {
|
|
132
|
+
producer: z.strictObject(producerShape),
|
|
133
|
+
client: z.looseObject(clientShape ?? producerShape)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function defineSuccessResponse(data, meta) {
|
|
137
|
+
return defineResponseObject({
|
|
138
|
+
data: data.producer,
|
|
139
|
+
meta: meta.producer
|
|
140
|
+
}, {
|
|
141
|
+
data: data.client,
|
|
142
|
+
meta: meta.client
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function buildHttpOperationPath(surface, operation, params) {
|
|
146
|
+
if (!operation.id.startsWith(`${surface.name}.`)) throw new TypeError(`${operation.id}: operation does not belong to the ${surface.name} surface`);
|
|
147
|
+
if (pathParameterNames(operation.path).length === 0) {
|
|
148
|
+
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`);
|
|
149
|
+
return `${surface.prefix}${operation.path}`;
|
|
150
|
+
}
|
|
151
|
+
if (operation.request.params === null) throw new TypeError(`${operation.id}: operation has an invalid path contract`);
|
|
152
|
+
const parsed = operation.request.params.parse(params);
|
|
153
|
+
const relativePath = operation.path.replace(/\{([^{}]+)\}/g, (_match, name) => {
|
|
154
|
+
const value = parsed[name];
|
|
155
|
+
if (typeof value !== "string" && typeof value !== "number") throw new TypeError(`${operation.id}: path parameter ${name} must serialize as a string or number`);
|
|
156
|
+
const serialized = String(value);
|
|
157
|
+
if (serialized.length === 0 || serialized === "." || serialized === "..") throw new TypeError(`${operation.id}: path parameter ${name} cannot serialize as an empty or dot segment`);
|
|
158
|
+
return encodeURIComponent(serialized);
|
|
159
|
+
});
|
|
160
|
+
return `${surface.prefix}${relativePath}`;
|
|
161
|
+
}
|
|
162
|
+
export { HTTP_V1_CREDENTIALS, HTTP_V1_CREDENTIAL_SCOPES, HTTP_V1_ERROR_CODES, HTTP_V1_METHODS, HTTP_V1_SCOPES, HTTP_V1_SURFACES, buildHttpOperationPath, defineHttpOperation, defineHttpSurface, defineResponseObject, defineSuccessResponse };
|