stitchkit 0.47.0 → 0.48.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/README.md +9 -0
- package/dist/browser/client.d.ts +3 -0
- package/dist/browser/client.d.ts.map +1 -1
- package/dist/browser/http.d.ts +7 -4
- package/dist/browser/http.d.ts.map +1 -1
- package/dist/cli.js +3 -2
- package/dist/contract/define.d.ts +11 -3
- package/dist/contract/define.d.ts.map +1 -1
- package/dist/{index-nrytvb30.js → index-03j2t778.js} +5 -8
- package/dist/{index-kp8xamqp.js → index-0hj37z43.js} +4 -2
- package/dist/index-48ffdxgk.js +6 -0
- package/dist/index-h05ygjqx.js +149 -0
- package/dist/{index-ee621cmy.js → index-jewp9r0a.js} +5 -139
- package/dist/index-p9d4cxt5.js +436 -0
- package/dist/{index-44xysy8r.js → index-ts21eyz4.js} +141 -19
- package/dist/{index-8ekq6res.js → index-v58mwa19.js} +4 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -27
- package/dist/node.d.ts +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +8 -3
- package/dist/observability/audit.d.ts +4 -1
- package/dist/observability/audit.d.ts.map +1 -1
- package/dist/observability/index.d.ts +1 -0
- package/dist/observability/index.d.ts.map +1 -1
- package/dist/observability/index.js +150 -16
- package/dist/observability/status.d.ts +105 -0
- package/dist/observability/status.d.ts.map +1 -0
- package/dist/server/implement.d.ts +16 -6
- package/dist/server/implement.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +14 -6
- package/dist/server/router.d.ts +4 -1
- package/dist/server/router.d.ts.map +1 -1
- package/dist/testing.d.ts +34 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +36 -0
- package/dist/tools/mcp.d.ts +1 -0
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools.js +21 -430
- package/llms-full.txt +177 -22
- package/package.json +7 -3
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import {
|
|
2
|
+
inputIsQuery
|
|
3
|
+
} from "./index-48ffdxgk.js";
|
|
4
|
+
import {
|
|
5
|
+
isRecord,
|
|
6
|
+
mapObject,
|
|
7
|
+
parseTrailingWildcard,
|
|
8
|
+
typedEntries
|
|
9
|
+
} from "./index-h05ygjqx.js";
|
|
10
|
+
|
|
11
|
+
// src/browser/cancellation.ts
|
|
12
|
+
class RequestCancellationError extends Error {
|
|
13
|
+
cause;
|
|
14
|
+
constructor(cause) {
|
|
15
|
+
super(cause === "caller" ? "Request was aborted" : "Request timed out");
|
|
16
|
+
this.cause = cause;
|
|
17
|
+
this.name = "RequestCancellationError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function createRequestCancellation(caller, timeoutMs) {
|
|
21
|
+
if (!caller && timeoutMs === undefined) {
|
|
22
|
+
return { signal: undefined, run: (operation) => operation() };
|
|
23
|
+
}
|
|
24
|
+
const controller = new AbortController;
|
|
25
|
+
let cause;
|
|
26
|
+
let timer;
|
|
27
|
+
const abortFromCaller = () => {
|
|
28
|
+
if (cause)
|
|
29
|
+
return;
|
|
30
|
+
cause = "caller";
|
|
31
|
+
controller.abort(caller?.reason);
|
|
32
|
+
};
|
|
33
|
+
if (caller?.aborted)
|
|
34
|
+
abortFromCaller();
|
|
35
|
+
else
|
|
36
|
+
caller?.addEventListener("abort", abortFromCaller, { once: true });
|
|
37
|
+
if (timeoutMs !== undefined) {
|
|
38
|
+
timer = setTimeout(() => {
|
|
39
|
+
if (cause)
|
|
40
|
+
return;
|
|
41
|
+
cause = "timeout";
|
|
42
|
+
controller.abort(new DOMException("Request timed out", "TimeoutError"));
|
|
43
|
+
}, timeoutMs);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
signal: controller.signal,
|
|
47
|
+
async run(operation) {
|
|
48
|
+
try {
|
|
49
|
+
if (cause === "caller")
|
|
50
|
+
throw cancellationError(cause);
|
|
51
|
+
return await operation(controller.signal);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (cause)
|
|
54
|
+
throw cancellationError(cause);
|
|
55
|
+
throw error;
|
|
56
|
+
} finally {
|
|
57
|
+
if (timer !== undefined)
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
caller?.removeEventListener("abort", abortFromCaller);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function cancellationError(cause) {
|
|
65
|
+
return new RequestCancellationError(cause);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/browser/client-multipart.ts
|
|
69
|
+
function isFileDescriptor(value) {
|
|
70
|
+
return typeof value === "object" && value !== null && !(value instanceof Blob) && "uri" in value && typeof value.uri === "string" && "name" in value && typeof value.name === "string" && "type" in value && typeof value.type === "string";
|
|
71
|
+
}
|
|
72
|
+
function isMultipartFile(value) {
|
|
73
|
+
return value instanceof Blob || isFileDescriptor(value);
|
|
74
|
+
}
|
|
75
|
+
function appendMultipartFile(form, field, file) {
|
|
76
|
+
const sink = form;
|
|
77
|
+
sink.append(field, file);
|
|
78
|
+
}
|
|
79
|
+
function appendFormFields(formData, values, skipKeys) {
|
|
80
|
+
for (const [key, value] of Object.entries(values)) {
|
|
81
|
+
if (skipKeys.has(key) || value === undefined || value === null)
|
|
82
|
+
continue;
|
|
83
|
+
formData.append(key, typeof value === "string" ? value : JSON.stringify(value));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function buildMultipartForm(descriptor, values) {
|
|
87
|
+
const formData = new FormData;
|
|
88
|
+
const fileFields = new Set(Object.keys(descriptor.files));
|
|
89
|
+
for (const [field, policy] of Object.entries(descriptor.files)) {
|
|
90
|
+
const value = values[field];
|
|
91
|
+
if (value === undefined) {
|
|
92
|
+
if (policy.required !== false)
|
|
93
|
+
throw new Error(`Missing multipart file field: ${field}`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (policy.multiple === true) {
|
|
97
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
98
|
+
throw new Error(`Multipart file field "${field}" must be a non-empty array`);
|
|
99
|
+
}
|
|
100
|
+
for (const file of value) {
|
|
101
|
+
if (!isMultipartFile(file)) {
|
|
102
|
+
throw new Error(`Invalid multipart file field: ${field}`);
|
|
103
|
+
}
|
|
104
|
+
appendMultipartFile(formData, field, file);
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (!isMultipartFile(value))
|
|
109
|
+
throw new Error(`Invalid multipart file field: ${field}`);
|
|
110
|
+
appendMultipartFile(formData, field, value);
|
|
111
|
+
}
|
|
112
|
+
appendFormFields(formData, values, fileFields);
|
|
113
|
+
return formData;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/browser/client-url.ts
|
|
117
|
+
function isParamArray(value) {
|
|
118
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string" || typeof item === "number");
|
|
119
|
+
}
|
|
120
|
+
function collectQueryParams(args, endpoint) {
|
|
121
|
+
const params = {};
|
|
122
|
+
let hasParams = false;
|
|
123
|
+
for (const [key, value] of Object.entries(args)) {
|
|
124
|
+
if (value === undefined || value === null)
|
|
125
|
+
continue;
|
|
126
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || isParamArray(value)) {
|
|
127
|
+
params[key] = value;
|
|
128
|
+
hasParams = true;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const what = Array.isArray(value) ? "an array with non-primitive items" : typeof value === "object" ? "a nested object" : `a ${typeof value}`;
|
|
132
|
+
throw new Error(`${endpoint.method} ${endpoint.path}: input field "${key}" is ${what} — it cannot ` + "travel as a query parameter. GET / DELETE input must be flat (string / number / " + "boolean, or an array of string / number); flatten the field or move the " + "operation to a body verb (POST).");
|
|
133
|
+
}
|
|
134
|
+
return hasParams ? params : undefined;
|
|
135
|
+
}
|
|
136
|
+
function hasStringKeys(args, keys) {
|
|
137
|
+
for (const key of keys) {
|
|
138
|
+
if (typeof args[key] !== "string")
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
function resolvePathPrefix(config, args) {
|
|
144
|
+
if (!config?.pathPrefix)
|
|
145
|
+
return "";
|
|
146
|
+
if (typeof config.pathPrefix === "string")
|
|
147
|
+
return config.pathPrefix;
|
|
148
|
+
const keys = config.stripPrefixKeys ?? [];
|
|
149
|
+
if (!hasStringKeys(args, keys)) {
|
|
150
|
+
const missing = keys.find((key) => typeof args[key] !== "string");
|
|
151
|
+
throw new Error(`Missing path prefix key: ${missing}`);
|
|
152
|
+
}
|
|
153
|
+
return config.pathPrefix(args);
|
|
154
|
+
}
|
|
155
|
+
function extractParamNames(path) {
|
|
156
|
+
const matches = path.match(/:(\w+)/g);
|
|
157
|
+
const names = matches ? matches.map((match) => match.slice(1)) : [];
|
|
158
|
+
const wildcard = parseTrailingWildcard(path);
|
|
159
|
+
if (wildcard)
|
|
160
|
+
names.push(wildcard.name);
|
|
161
|
+
return names;
|
|
162
|
+
}
|
|
163
|
+
function fillPathParams(path, args) {
|
|
164
|
+
const wildcard = parseTrailingWildcard(path);
|
|
165
|
+
let filled = path.replace(/:(\w+)/g, (_, key) => {
|
|
166
|
+
const value = args[key];
|
|
167
|
+
if (value === undefined || value === null) {
|
|
168
|
+
throw new Error(`Missing path param: ${key}`);
|
|
169
|
+
}
|
|
170
|
+
return encodeURIComponent(String(value));
|
|
171
|
+
});
|
|
172
|
+
if (!wildcard)
|
|
173
|
+
return filled;
|
|
174
|
+
const wildcardValue = args[wildcard.name];
|
|
175
|
+
if (wildcardValue === undefined || wildcardValue === null) {
|
|
176
|
+
throw new Error(`Missing path param: ${wildcard.name}`);
|
|
177
|
+
}
|
|
178
|
+
const remainder = String(wildcardValue).split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
179
|
+
filled = `${filled.slice(0, -(wildcard.name.length + 1))}${remainder}`;
|
|
180
|
+
return filled;
|
|
181
|
+
}
|
|
182
|
+
function stripConsumedArgs(args, path, scopeKeys) {
|
|
183
|
+
const consumed = new Set(scopeKeys);
|
|
184
|
+
for (const name of extractParamNames(path))
|
|
185
|
+
consumed.add(name);
|
|
186
|
+
const remaining = {};
|
|
187
|
+
for (const [key, value] of Object.entries(args)) {
|
|
188
|
+
if (!consumed.has(key) && value !== undefined)
|
|
189
|
+
remaining[key] = value;
|
|
190
|
+
}
|
|
191
|
+
return remaining;
|
|
192
|
+
}
|
|
193
|
+
function appendQuery(relativeUrl, params) {
|
|
194
|
+
if (!params)
|
|
195
|
+
return relativeUrl;
|
|
196
|
+
const search = new URLSearchParams;
|
|
197
|
+
for (const [key, value] of Object.entries(params)) {
|
|
198
|
+
if (Array.isArray(value)) {
|
|
199
|
+
for (const item of value)
|
|
200
|
+
search.append(key, String(item));
|
|
201
|
+
} else {
|
|
202
|
+
search.set(key, String(value));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return search.size > 0 ? `${relativeUrl}?${search}` : relativeUrl;
|
|
206
|
+
}
|
|
207
|
+
function planClientRequest(endpoint, contractPrefix, args, config) {
|
|
208
|
+
let pathPrefix = resolvePathPrefix(config, args);
|
|
209
|
+
if (pathPrefix && !pathPrefix.endsWith("/"))
|
|
210
|
+
pathPrefix += "/";
|
|
211
|
+
if (pathPrefix.startsWith("/"))
|
|
212
|
+
pathPrefix = pathPrefix.slice(1);
|
|
213
|
+
const endpointPath = endpoint.path === "/" ? "" : endpoint.path;
|
|
214
|
+
let relativeUrl = fillPathParams(`${pathPrefix}${contractPrefix}${endpointPath}`, args);
|
|
215
|
+
if (relativeUrl.endsWith("/"))
|
|
216
|
+
relativeUrl = relativeUrl.slice(0, -1);
|
|
217
|
+
const remainingArgs = stripConsumedArgs(args, endpoint.path, config?.stripPrefixKeys ?? []);
|
|
218
|
+
if (inputIsQuery(endpoint.method)) {
|
|
219
|
+
relativeUrl = appendQuery(relativeUrl, collectQueryParams(remainingArgs, endpoint));
|
|
220
|
+
}
|
|
221
|
+
return { relativeUrl, remainingArgs };
|
|
222
|
+
}
|
|
223
|
+
function joinClientBaseUrl(baseUrl, relativeUrl) {
|
|
224
|
+
const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
225
|
+
const path = relativeUrl.startsWith("/") ? relativeUrl : `/${relativeUrl}`;
|
|
226
|
+
return `${base}${path}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/browser/http.ts
|
|
230
|
+
import ky, {
|
|
231
|
+
isHTTPError,
|
|
232
|
+
isNetworkError,
|
|
233
|
+
isTimeoutError
|
|
234
|
+
} from "ky";
|
|
235
|
+
|
|
236
|
+
// src/browser/request-id.ts
|
|
237
|
+
var REQUEST_ID_HEADER = "x-request-id";
|
|
238
|
+
function responseTraceId(response) {
|
|
239
|
+
return response?.headers.get(REQUEST_ID_HEADER) ?? undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/browser/http.ts
|
|
243
|
+
class ApiError extends Error {
|
|
244
|
+
code;
|
|
245
|
+
status;
|
|
246
|
+
details;
|
|
247
|
+
hint;
|
|
248
|
+
traceId;
|
|
249
|
+
constructor(code, status = 0, details, message, hint, traceId) {
|
|
250
|
+
super(message ?? `API Error: ${code}`);
|
|
251
|
+
this.code = code;
|
|
252
|
+
this.status = status;
|
|
253
|
+
this.details = details;
|
|
254
|
+
this.hint = hint;
|
|
255
|
+
this.traceId = traceId;
|
|
256
|
+
this.name = "ApiError";
|
|
257
|
+
}
|
|
258
|
+
static is(error) {
|
|
259
|
+
return error instanceof ApiError;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function parseApiErrorBody(body) {
|
|
263
|
+
if (!isRecord(body) || !isRecord(body.error))
|
|
264
|
+
return null;
|
|
265
|
+
const error = body.error;
|
|
266
|
+
if (typeof error.code !== "string")
|
|
267
|
+
return null;
|
|
268
|
+
return {
|
|
269
|
+
code: error.code,
|
|
270
|
+
message: typeof error.message === "string" ? error.message : undefined,
|
|
271
|
+
details: error.details,
|
|
272
|
+
hint: typeof error.hint === "string" ? error.hint : undefined
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/browser/client.ts
|
|
277
|
+
function withTimeout(options, endpoint) {
|
|
278
|
+
const responseType = endpoint.rawResponse ? "response" : endpoint.output ? undefined : "void";
|
|
279
|
+
if (endpoint.timeout === undefined && responseType === undefined)
|
|
280
|
+
return options;
|
|
281
|
+
return {
|
|
282
|
+
...options,
|
|
283
|
+
...endpoint.timeout !== undefined && { timeout: endpoint.timeout },
|
|
284
|
+
...responseType && { responseType }
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function withOutput(endpoint, result) {
|
|
288
|
+
if (endpoint.rawResponse)
|
|
289
|
+
return result;
|
|
290
|
+
const schema = endpoint.output;
|
|
291
|
+
return result.then((value) => {
|
|
292
|
+
if (!schema) {
|
|
293
|
+
if (value === undefined || value === null)
|
|
294
|
+
return;
|
|
295
|
+
throw new Error("Server returned data for an endpoint with no output contract");
|
|
296
|
+
}
|
|
297
|
+
if (value === undefined) {
|
|
298
|
+
throw new Error("Server returned no body for an endpoint with an output contract");
|
|
299
|
+
}
|
|
300
|
+
return schema.parse(value);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
function createClient(contract, configOrClient, contractConfig) {
|
|
304
|
+
const client = {};
|
|
305
|
+
const makeExecutor = isHttpAdapter(configOrClient) ? (endpoint) => createHttpExecutor(endpoint, contract.meta.prefix, configOrClient, contractConfig) : (endpoint) => createFetchExecutor(endpoint, contract.meta.prefix, configOrClient, contractConfig);
|
|
306
|
+
for (const [key, endpoint] of typedEntries(contract.endpoints)) {
|
|
307
|
+
if (endpoint.expose && !endpoint.expose.includes("HTTP"))
|
|
308
|
+
continue;
|
|
309
|
+
setClientMethod(client, key, createEndpointMethod(endpoint, makeExecutor(endpoint), contractConfig));
|
|
310
|
+
}
|
|
311
|
+
return client;
|
|
312
|
+
}
|
|
313
|
+
function createClients(contracts, configOrClient, contractConfig) {
|
|
314
|
+
return mapObject(contracts, (_key, contract) => createClient(contract, configOrClient, contractConfig));
|
|
315
|
+
}
|
|
316
|
+
function isHttpAdapter(value) {
|
|
317
|
+
return typeof value === "object" && "get" in value && typeof value.get === "function";
|
|
318
|
+
}
|
|
319
|
+
function setClientMethod(target, key, method) {
|
|
320
|
+
target[key] = method;
|
|
321
|
+
}
|
|
322
|
+
function createEndpointMethod(endpoint, execute, contractConfig) {
|
|
323
|
+
const hasScopedArguments = (contractConfig?.stripPrefixKeys?.length ?? 0) > 0;
|
|
324
|
+
if (endpointHasArguments(endpoint) || hasScopedArguments) {
|
|
325
|
+
const method2 = (requestArgs) => execute(readClientRequestArgs(requestArgs), undefined);
|
|
326
|
+
return Object.assign(method2, {
|
|
327
|
+
withOptions: (requestArgs, options) => execute(readClientRequestArgs(requestArgs), readClientRequestOptions(options))
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
const method = () => execute({}, undefined);
|
|
331
|
+
return Object.assign(method, {
|
|
332
|
+
withOptions: (options) => execute({}, readClientRequestOptions(options))
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
function createHttpExecutor(endpoint, prefix, client, config) {
|
|
336
|
+
const httpMethod = endpoint.method.toLowerCase();
|
|
337
|
+
return (requestArgs, options) => {
|
|
338
|
+
const plan = planClientRequest(endpoint, prefix, requestArgs, config);
|
|
339
|
+
if (endpoint.multipart) {
|
|
340
|
+
if (httpMethod === "get" || httpMethod === "head" || httpMethod === "delete") {
|
|
341
|
+
throw new Error(`Multipart endpoint ${endpoint.method} ${endpoint.path} must be POST / PUT / PATCH`);
|
|
342
|
+
}
|
|
343
|
+
const formData = buildMultipartForm(endpoint.multipart, plan.remainingArgs);
|
|
344
|
+
return withOutput(endpoint, client[httpMethod](plan.relativeUrl, formData, withTimeout(options, endpoint)));
|
|
345
|
+
}
|
|
346
|
+
if (httpMethod === "get" || httpMethod === "head") {
|
|
347
|
+
return withOutput(endpoint, client[httpMethod](plan.relativeUrl, withTimeout(options, endpoint)));
|
|
348
|
+
}
|
|
349
|
+
if (httpMethod === "delete") {
|
|
350
|
+
return withOutput(endpoint, client.delete(plan.relativeUrl, withTimeout(options, endpoint)));
|
|
351
|
+
}
|
|
352
|
+
return withOutput(endpoint, client[httpMethod](plan.relativeUrl, Object.keys(plan.remainingArgs).length > 0 ? plan.remainingArgs : undefined, withTimeout(options, endpoint)));
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function createFetchExecutor(endpoint, prefix, config, contractConfig) {
|
|
356
|
+
const executeFetch = config.fetch ?? globalThis.fetch;
|
|
357
|
+
return async (requestArgs, options) => {
|
|
358
|
+
const plan = planClientRequest(endpoint, prefix, requestArgs, contractConfig);
|
|
359
|
+
const url = joinClientBaseUrl(config.baseUrl, plan.relativeUrl);
|
|
360
|
+
const headers = {
|
|
361
|
+
Accept: "application/json",
|
|
362
|
+
...typeof config.headers === "function" ? config.headers() : config.headers
|
|
363
|
+
};
|
|
364
|
+
const cancellation = createRequestCancellation(options?.signal, endpoint.timeout ?? config.timeout ?? 30000);
|
|
365
|
+
const hasBody = endpoint.method !== "GET" && endpoint.method !== "HEAD" && endpoint.method !== "DELETE" && !endpoint.multipart && endpoint.input && Object.keys(plan.remainingArgs).length > 0;
|
|
366
|
+
if (hasBody)
|
|
367
|
+
headers["Content-Type"] = "application/json";
|
|
368
|
+
try {
|
|
369
|
+
return await cancellation.run(async (signal) => {
|
|
370
|
+
const body = endpoint.multipart ? buildMultipartForm(endpoint.multipart, plan.remainingArgs) : hasBody ? JSON.stringify(plan.remainingArgs) : undefined;
|
|
371
|
+
const res = await executeFetch(url, {
|
|
372
|
+
method: endpoint.method,
|
|
373
|
+
headers,
|
|
374
|
+
credentials: config.credentials,
|
|
375
|
+
signal,
|
|
376
|
+
...body !== undefined && { body }
|
|
377
|
+
});
|
|
378
|
+
if (!res.ok) {
|
|
379
|
+
await throwForErrorResponse(res, config, { error: res.statusText });
|
|
380
|
+
}
|
|
381
|
+
if (endpoint.rawResponse)
|
|
382
|
+
return res;
|
|
383
|
+
if (!endpoint.output) {
|
|
384
|
+
const text = await res.text();
|
|
385
|
+
if (text.length > 0) {
|
|
386
|
+
throw new Error("Server returned data for an endpoint with no output contract");
|
|
387
|
+
}
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
return endpoint.output.parse(await res.json());
|
|
391
|
+
});
|
|
392
|
+
} catch (error) {
|
|
393
|
+
if (error instanceof RequestCancellationError) {
|
|
394
|
+
throw new ApiError(error.cause === "caller" ? "REQUEST_ABORTED" : "REQUEST_TIMEOUT", 0, undefined, error.message);
|
|
395
|
+
}
|
|
396
|
+
if (ApiError.is(error))
|
|
397
|
+
throw error;
|
|
398
|
+
const message = error instanceof Error ? error.message : undefined;
|
|
399
|
+
throw new ApiError("UNKNOWN_ERROR", 0, message ? { message } : undefined, message);
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
function endpointHasArguments(endpoint) {
|
|
404
|
+
return Boolean(endpoint.params || endpoint.input || endpoint.multipart);
|
|
405
|
+
}
|
|
406
|
+
function readClientRequestArgs(requestArgs) {
|
|
407
|
+
if (requestArgs !== undefined && !isRecord(requestArgs)) {
|
|
408
|
+
throw new TypeError("Endpoint arguments must be an object");
|
|
409
|
+
}
|
|
410
|
+
return requestArgs ?? {};
|
|
411
|
+
}
|
|
412
|
+
function readClientRequestOptions(value) {
|
|
413
|
+
if (!isRecord(value))
|
|
414
|
+
throw new TypeError("Client request options must be an object");
|
|
415
|
+
const signal = value.signal;
|
|
416
|
+
if (signal === undefined)
|
|
417
|
+
return {};
|
|
418
|
+
if (!isAbortSignal(signal)) {
|
|
419
|
+
throw new TypeError("Client request signal must be an AbortSignal");
|
|
420
|
+
}
|
|
421
|
+
return { signal };
|
|
422
|
+
}
|
|
423
|
+
function isAbortSignal(value) {
|
|
424
|
+
return typeof value === "object" && value !== null && "aborted" in value && typeof value.aborted === "boolean" && "addEventListener" in value && typeof value.addEventListener === "function" && "removeEventListener" in value && typeof value.removeEventListener === "function";
|
|
425
|
+
}
|
|
426
|
+
async function throwForErrorResponse(res, config, fallbackBody) {
|
|
427
|
+
const body = await res.json().catch(() => fallbackBody);
|
|
428
|
+
config.onError?.(res.status, body);
|
|
429
|
+
const parsed = parseApiErrorBody(body);
|
|
430
|
+
if (parsed) {
|
|
431
|
+
throw new ApiError(parsed.code, res.status, parsed.details, parsed.message, parsed.hint, responseTraceId(res));
|
|
432
|
+
}
|
|
433
|
+
throw new ApiError("HTTP_ERROR", res.status, { body }, undefined, undefined, responseTraceId(res));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export { ApiError, createClient, createClients };
|
|
@@ -5,31 +5,33 @@ import {
|
|
|
5
5
|
} from "./index-r1qp4rve.js";
|
|
6
6
|
import {
|
|
7
7
|
AppError,
|
|
8
|
-
__require,
|
|
9
8
|
badRequest,
|
|
10
|
-
callRuntimeHandler,
|
|
11
9
|
errorCode,
|
|
12
10
|
extractIp,
|
|
13
11
|
getClientInfo,
|
|
14
12
|
getRequestContext,
|
|
15
|
-
isRecord,
|
|
16
13
|
isUnsafeKey,
|
|
17
14
|
mergeMeta,
|
|
18
15
|
normalizeError,
|
|
19
16
|
parseQueryParams,
|
|
20
|
-
parseTrailingWildcard,
|
|
21
17
|
recordedErrorMessage,
|
|
22
18
|
resolveSocketIp,
|
|
23
|
-
resolveTraceContext,
|
|
24
19
|
resolveTraceId,
|
|
25
20
|
runWithRequestContext,
|
|
26
21
|
safeJsonParse,
|
|
27
22
|
setRequestEndpoint,
|
|
28
23
|
setRequestError,
|
|
29
|
-
typedEntries,
|
|
30
24
|
validateDeclaredOutput,
|
|
31
25
|
zodIssues
|
|
32
|
-
} from "./index-
|
|
26
|
+
} from "./index-jewp9r0a.js";
|
|
27
|
+
import {
|
|
28
|
+
__require,
|
|
29
|
+
callRuntimeHandler,
|
|
30
|
+
isRecord,
|
|
31
|
+
parseTrailingWildcard,
|
|
32
|
+
resolveTraceContext,
|
|
33
|
+
typedEntries
|
|
34
|
+
} from "./index-h05ygjqx.js";
|
|
33
35
|
|
|
34
36
|
// src/server/multipart.ts
|
|
35
37
|
var DEFAULT_MAX_REQUEST_BYTES = 25 * 1024 * 1024;
|
|
@@ -803,6 +805,50 @@ function joinPath(...parts) {
|
|
|
803
805
|
const joined = parts.filter(Boolean).map((part) => part.replace(/^\/+|\/+$/g, "")).filter(Boolean).join("/");
|
|
804
806
|
return `/${joined}`;
|
|
805
807
|
}
|
|
808
|
+
function routeSegments(path) {
|
|
809
|
+
return path.split("/").filter(Boolean);
|
|
810
|
+
}
|
|
811
|
+
function rawRouteShape(path) {
|
|
812
|
+
parseTrailingWildcard(path);
|
|
813
|
+
const segments = routeSegments(path).map((segment) => {
|
|
814
|
+
if (segment.startsWith(":"))
|
|
815
|
+
return { kind: "param" };
|
|
816
|
+
if (segment.startsWith("*"))
|
|
817
|
+
return { kind: "wildcard" };
|
|
818
|
+
return { kind: "static", value: segment };
|
|
819
|
+
});
|
|
820
|
+
return {
|
|
821
|
+
segments,
|
|
822
|
+
signature: segments.map((segment) => segment.kind === "static" ? segment.value : `:${segment.kind}`).join("/"),
|
|
823
|
+
wildcard: segments.at(-1)?.kind === "wildcard"
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
function segmentCovers(earlier, later) {
|
|
827
|
+
if (earlier.kind === "param")
|
|
828
|
+
return later.kind !== "wildcard";
|
|
829
|
+
if (earlier.kind === "wildcard")
|
|
830
|
+
return true;
|
|
831
|
+
return later.kind === "static" && earlier.value === later.value;
|
|
832
|
+
}
|
|
833
|
+
function routeShapeCovers(earlier, later) {
|
|
834
|
+
const earlierPrefixLength = earlier.wildcard ? earlier.segments.length - 1 : earlier.segments.length;
|
|
835
|
+
const laterPrefixLength = later.wildcard ? later.segments.length - 1 : later.segments.length;
|
|
836
|
+
if (earlier.wildcard) {
|
|
837
|
+
if (earlierPrefixLength > laterPrefixLength)
|
|
838
|
+
return false;
|
|
839
|
+
} else {
|
|
840
|
+
if (later.wildcard || earlierPrefixLength !== laterPrefixLength)
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
for (let index = 0;index < earlierPrefixLength; index++) {
|
|
844
|
+
const earlierSegment = earlier.segments[index];
|
|
845
|
+
const laterSegment = later.segments[index];
|
|
846
|
+
if (!earlierSegment || !laterSegment || !segmentCovers(earlierSegment, laterSegment)) {
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return true;
|
|
851
|
+
}
|
|
806
852
|
function matchSegments(patternSegments, requestSegments) {
|
|
807
853
|
const wildcardSegment = patternSegments.at(-1);
|
|
808
854
|
const wildcardName = wildcardSegment?.startsWith("*") ? wildcardSegment.slice(1) : null;
|
|
@@ -844,7 +890,7 @@ function buildRouteMap(groups) {
|
|
|
844
890
|
continue;
|
|
845
891
|
const servicePath = joinPath("/", service.prefix, method.path === "/" ? "" : method.path);
|
|
846
892
|
const fullPath = prefix ? joinPath(prefix, servicePath) : servicePath;
|
|
847
|
-
const segments = fullPath
|
|
893
|
+
const segments = routeSegments(fullPath);
|
|
848
894
|
const entries = map.get(method.method) ?? [];
|
|
849
895
|
entries.push({ method, pattern: fullPath, segments, groupHooks: hooks });
|
|
850
896
|
map.set(method.method, entries);
|
|
@@ -867,7 +913,7 @@ function matchRoute(routeMap, httpMethod, pathname) {
|
|
|
867
913
|
const entries = routeMap.get(httpMethod);
|
|
868
914
|
if (!entries)
|
|
869
915
|
return null;
|
|
870
|
-
const requestSegments = pathname
|
|
916
|
+
const requestSegments = routeSegments(pathname);
|
|
871
917
|
for (const entry of entries) {
|
|
872
918
|
const pathParams = matchSegments(entry.segments, requestSegments);
|
|
873
919
|
if (pathParams) {
|
|
@@ -881,7 +927,7 @@ function matchRoute(routeMap, httpMethod, pathname) {
|
|
|
881
927
|
return null;
|
|
882
928
|
}
|
|
883
929
|
function allowedMethods(routeMap, pathname) {
|
|
884
|
-
const requestSegments = pathname
|
|
930
|
+
const requestSegments = routeSegments(pathname);
|
|
885
931
|
const methods = [];
|
|
886
932
|
for (const [method, entries] of routeMap) {
|
|
887
933
|
for (const entry of entries) {
|
|
@@ -939,16 +985,16 @@ function matchRawRoute(rawRoutes, httpMethod, pathname) {
|
|
|
939
985
|
if (route.method !== "ALL" && route.method !== httpMethod)
|
|
940
986
|
continue;
|
|
941
987
|
if (parseTrailingWildcard(route.path)) {
|
|
942
|
-
const routeSegs = route.path
|
|
943
|
-
const pathSegs = pathname
|
|
988
|
+
const routeSegs = routeSegments(route.path);
|
|
989
|
+
const pathSegs = routeSegments(pathname);
|
|
944
990
|
const params = matchSegments(routeSegs, pathSegs);
|
|
945
991
|
if (params)
|
|
946
992
|
return { route, params };
|
|
947
993
|
continue;
|
|
948
994
|
}
|
|
949
995
|
if (route.path.includes("/:")) {
|
|
950
|
-
const routeSegs = route.path
|
|
951
|
-
const pathSegs = pathname
|
|
996
|
+
const routeSegs = routeSegments(route.path);
|
|
997
|
+
const pathSegs = routeSegments(pathname);
|
|
952
998
|
const params = matchSegments(routeSegs, pathSegs);
|
|
953
999
|
if (params)
|
|
954
1000
|
return { route, params };
|
|
@@ -960,8 +1006,37 @@ function matchRawRoute(rawRoutes, httpMethod, pathname) {
|
|
|
960
1006
|
return null;
|
|
961
1007
|
}
|
|
962
1008
|
function validateRawRoutes(rawRoutes) {
|
|
963
|
-
|
|
964
|
-
|
|
1009
|
+
const routes = rawRoutes ?? [];
|
|
1010
|
+
const shapes = routes.map((route) => rawRouteShape(route.path));
|
|
1011
|
+
const conflicts = [];
|
|
1012
|
+
for (const [laterIndex, later] of routes.entries()) {
|
|
1013
|
+
const laterShape = shapes[laterIndex];
|
|
1014
|
+
if (!laterShape)
|
|
1015
|
+
continue;
|
|
1016
|
+
for (let earlierIndex = 0;earlierIndex < laterIndex; earlierIndex++) {
|
|
1017
|
+
const earlier = routes[earlierIndex];
|
|
1018
|
+
const earlierShape = shapes[earlierIndex];
|
|
1019
|
+
if (!earlier || !earlierShape)
|
|
1020
|
+
continue;
|
|
1021
|
+
if (earlier.method === later.method && earlier.path === later.path) {
|
|
1022
|
+
conflicts.push(`${later.method} ${later.path} duplicates earlier ${earlier.method} ${earlier.path}`);
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
if (earlier.method === later.method && earlierShape.signature === laterShape.signature) {
|
|
1026
|
+
conflicts.push(`${later.method} ${later.path} has the same parameter shape as earlier ${earlier.method} ${earlier.path}`);
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
const methodCovered = earlier.method === "ALL" || earlier.method === later.method;
|
|
1030
|
+
if (methodCovered && routeShapeCovers(earlierShape, laterShape)) {
|
|
1031
|
+
conflicts.push(`${later.method} ${later.path} is unreachable because earlier ${earlier.method} ${earlier.path} matches every request it could receive`);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
if (conflicts.length > 0) {
|
|
1036
|
+
throw new Error(`[stitchkit] conflicting raw routes:
|
|
1037
|
+
- ${conflicts.join(`
|
|
1038
|
+
- `)}`);
|
|
1039
|
+
}
|
|
965
1040
|
}
|
|
966
1041
|
|
|
967
1042
|
// src/server/create.ts
|
|
@@ -1345,11 +1420,11 @@ function defineMultipartStream(endpoint, config) {
|
|
|
1345
1420
|
};
|
|
1346
1421
|
}
|
|
1347
1422
|
var HTTP_ONLY = Object.freeze(["HTTP"]);
|
|
1348
|
-
function
|
|
1423
|
+
function bindContract(contract, handlers) {
|
|
1349
1424
|
const methods = {};
|
|
1350
1425
|
const groupScope = contract.meta.scope ?? "public";
|
|
1351
1426
|
for (const [key, endpoint] of typedEntries(contract.endpoints)) {
|
|
1352
|
-
const typedHandler = handlers[key];
|
|
1427
|
+
const typedHandler = handlers[String(key)];
|
|
1353
1428
|
const isStreaming = endpoint.multipart?.delivery === "stream";
|
|
1354
1429
|
if (!isStreaming && typeof typedHandler !== "function") {
|
|
1355
1430
|
throw new Error(`[stitchkit] implement: missing handler for "${contract.meta.prefix}.${String(key)}"`);
|
|
@@ -1398,9 +1473,56 @@ function implement(contract, handlers) {
|
|
|
1398
1473
|
methods
|
|
1399
1474
|
};
|
|
1400
1475
|
}
|
|
1476
|
+
function implement(contract, handlers) {
|
|
1477
|
+
return bindContract(contract, handlers);
|
|
1478
|
+
}
|
|
1401
1479
|
function createImplement() {
|
|
1402
1480
|
return (contract, handlers) => implement(contract, handlers);
|
|
1403
1481
|
}
|
|
1482
|
+
function isImplementationContract(value) {
|
|
1483
|
+
return isRecord(value) && isRecord(value.meta) && typeof value.meta.prefix === "string" && isRecord(value.endpoints);
|
|
1484
|
+
}
|
|
1485
|
+
function bindRegistry(contracts, handlers) {
|
|
1486
|
+
const contractKeys = Object.keys(contracts);
|
|
1487
|
+
const handlerKeys = Object.keys(handlers);
|
|
1488
|
+
const missing = contractKeys.filter((key) => !Object.hasOwn(handlers, key));
|
|
1489
|
+
const extra = handlerKeys.filter((key) => !Object.hasOwn(contracts, key));
|
|
1490
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
1491
|
+
throw new Error(`[stitchkit] implementRegistry: registry mismatch (missing: ${missing.join(", ") || "none"}; extra: ${extra.join(", ") || "none"})`);
|
|
1492
|
+
}
|
|
1493
|
+
const prefixes = new Map;
|
|
1494
|
+
const services = [];
|
|
1495
|
+
for (const [key, candidate] of Object.entries(contracts)) {
|
|
1496
|
+
if (!isImplementationContract(candidate)) {
|
|
1497
|
+
throw new TypeError(`[stitchkit] implementRegistry: registry entry "${key}" must be one contract; composed arrays and namespaces are not supported`);
|
|
1498
|
+
}
|
|
1499
|
+
const contract = candidate;
|
|
1500
|
+
const previousKey = prefixes.get(contract.meta.prefix);
|
|
1501
|
+
if (previousKey !== undefined) {
|
|
1502
|
+
throw new Error(`[stitchkit] implementRegistry: duplicate contract prefix "${contract.meta.prefix}" at "${previousKey}" and "${key}"`);
|
|
1503
|
+
}
|
|
1504
|
+
prefixes.set(contract.meta.prefix, key);
|
|
1505
|
+
const entryHandlers = handlers[key];
|
|
1506
|
+
if (!isRecord(entryHandlers)) {
|
|
1507
|
+
throw new TypeError(`[stitchkit] implementRegistry: handlers for "${key}" must be an object`);
|
|
1508
|
+
}
|
|
1509
|
+
const endpointKeys = Object.keys(contract.endpoints);
|
|
1510
|
+
const handlerEntryKeys = Object.keys(entryHandlers);
|
|
1511
|
+
const missingEndpoints = endpointKeys.filter((endpointKey) => !Object.hasOwn(entryHandlers, endpointKey));
|
|
1512
|
+
const extraEndpoints = handlerEntryKeys.filter((endpointKey) => !Object.hasOwn(contract.endpoints, endpointKey));
|
|
1513
|
+
if (missingEndpoints.length > 0 || extraEndpoints.length > 0) {
|
|
1514
|
+
throw new Error(`[stitchkit] implementRegistry: handlers for "${key}" mismatch (missing: ${missingEndpoints.join(", ") || "none"}; extra: ${extraEndpoints.join(", ") || "none"})`);
|
|
1515
|
+
}
|
|
1516
|
+
services.push(bindContract(contract, entryHandlers));
|
|
1517
|
+
}
|
|
1518
|
+
return services;
|
|
1519
|
+
}
|
|
1520
|
+
function implementRegistry(contracts, handlers) {
|
|
1521
|
+
return bindRegistry(contracts, handlers);
|
|
1522
|
+
}
|
|
1523
|
+
function createImplementRegistry() {
|
|
1524
|
+
return (contracts, handlers) => bindRegistry(contracts, handlers);
|
|
1525
|
+
}
|
|
1404
1526
|
|
|
1405
1527
|
// src/realtime/rejection.ts
|
|
1406
1528
|
import { z } from "zod";
|
|
@@ -1768,4 +1890,4 @@ function socketIoLane(websocket) {
|
|
|
1768
1890
|
});
|
|
1769
1891
|
}
|
|
1770
1892
|
|
|
1771
|
-
export { parseMultipart, createHandler, defineMultipartStream, implement, createImplement, bindRealtimeServer, webSocketLane, composeWebSocketHandlers, createSocketIOServer, socketIoLane };
|
|
1893
|
+
export { parseMultipart, createHandler, defineMultipartStream, implement, createImplement, implementRegistry, createImplementRegistry, bindRealtimeServer, webSocketLane, composeWebSocketHandlers, createSocketIOServer, socketIoLane };
|