stitchkit 0.48.0 → 0.49.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 +1 -2
- 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/{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-44xysy8r.js → index-fjfzsq6y.js} +383 -26
- 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-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 +47 -3
- package/dist/node.d.ts +4 -3
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +109 -14
- 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/bun.d.ts +7 -8
- package/dist/server/bun.d.ts.map +1 -1
- package/dist/server/implement.d.ts +16 -6
- package/dist/server/implement.d.ts.map +1 -1
- package/dist/server/index.d.ts +4 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +132 -11
- package/dist/server/node.d.ts +12 -13
- package/dist/server/node.d.ts.map +1 -1
- package/dist/server/router.d.ts +4 -1
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/shutdown.d.ts +76 -0
- package/dist/server/shutdown.d.ts.map +1 -0
- package/dist/server/socket-io-config.d.ts +5 -1
- package/dist/server/socket-io-config.d.ts.map +1 -1
- package/dist/server/socket-io-node.d.ts +4 -1
- package/dist/server/socket-io-node.d.ts.map +1 -1
- package/dist/server/socket-io.d.ts +12 -4
- package/dist/server/socket-io.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 -428
- package/llms-full.txt +290 -43
- package/package.json +8 -3
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
isRecord,
|
|
3
|
+
resolveTraceContext
|
|
4
|
+
} from "./index-h05ygjqx.js";
|
|
3
5
|
|
|
4
6
|
// src/contract/errors.ts
|
|
5
7
|
var APP_ERROR_BRAND = Symbol.for("stitchkit.AppError");
|
|
@@ -72,48 +74,6 @@ function appError(code, message, details) {
|
|
|
72
74
|
// src/contract/define.ts
|
|
73
75
|
import { z } from "zod";
|
|
74
76
|
|
|
75
|
-
// src/internal/route-pattern.ts
|
|
76
|
-
var PARAM_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
77
|
-
function parseTrailingWildcard(path) {
|
|
78
|
-
const segments = path.split("/").filter(Boolean);
|
|
79
|
-
const paramNames = new Set;
|
|
80
|
-
let wildcard = null;
|
|
81
|
-
for (const [segmentIndex, segment] of segments.entries()) {
|
|
82
|
-
if (segment.startsWith(":")) {
|
|
83
|
-
const name2 = segment.slice(1);
|
|
84
|
-
if (!PARAM_IDENTIFIER.test(name2)) {
|
|
85
|
-
throw new Error(`Invalid route parameter name "${name2}" in path "${path}"`);
|
|
86
|
-
}
|
|
87
|
-
if (paramNames.has(name2)) {
|
|
88
|
-
throw new Error(`Duplicate route parameter name "${name2}" in path "${path}"`);
|
|
89
|
-
}
|
|
90
|
-
paramNames.add(name2);
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
if (!segment.startsWith("*")) {
|
|
94
|
-
if (segment.includes("*")) {
|
|
95
|
-
throw new Error(`Wildcard must occupy its own segment in path "${path}"`);
|
|
96
|
-
}
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
const name = segment.slice(1);
|
|
100
|
-
if (!PARAM_IDENTIFIER.test(name)) {
|
|
101
|
-
throw new Error(`Trailing wildcard in path "${path}" must be named, for example "/*filePath"`);
|
|
102
|
-
}
|
|
103
|
-
if (segmentIndex !== segments.length - 1) {
|
|
104
|
-
throw new Error(`Wildcard "*${name}" must be the final segment in path "${path}"`);
|
|
105
|
-
}
|
|
106
|
-
if (wildcard) {
|
|
107
|
-
throw new Error(`Path "${path}" contains more than one wildcard`);
|
|
108
|
-
}
|
|
109
|
-
if (paramNames.has(name)) {
|
|
110
|
-
throw new Error(`Duplicate route parameter name "${name}" in path "${path}"`);
|
|
111
|
-
}
|
|
112
|
-
wildcard = { name, segmentIndex };
|
|
113
|
-
}
|
|
114
|
-
return wildcard;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
77
|
// src/internal/safe-json.ts
|
|
118
78
|
function isUnsafeKey(key) {
|
|
119
79
|
return key === "__proto__";
|
|
@@ -132,20 +92,6 @@ function mergeMeta(contractMeta, endpointMeta) {
|
|
|
132
92
|
}
|
|
133
93
|
// src/contract/errors-factory.ts
|
|
134
94
|
import { z as z2 } from "zod";
|
|
135
|
-
|
|
136
|
-
// src/internal/typed.ts
|
|
137
|
-
function typedEntries(value) {
|
|
138
|
-
return Object.entries(value);
|
|
139
|
-
}
|
|
140
|
-
function isRecord(value) {
|
|
141
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
142
|
-
}
|
|
143
|
-
function callRuntimeHandler(handler, context) {
|
|
144
|
-
if (typeof handler !== "function") {
|
|
145
|
-
throw new TypeError("Runtime handler must be a function");
|
|
146
|
-
}
|
|
147
|
-
return Reflect.apply(handler, undefined, [context]);
|
|
148
|
-
}
|
|
149
95
|
// src/contract/pagination.ts
|
|
150
96
|
import { z as z3 } from "zod";
|
|
151
97
|
|
|
@@ -320,86 +266,6 @@ function parseQueryParams(url) {
|
|
|
320
266
|
return query;
|
|
321
267
|
}
|
|
322
268
|
|
|
323
|
-
// src/observability/trace.ts
|
|
324
|
-
var TRACEPARENT_RE = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})(.*)$/i;
|
|
325
|
-
function randomHex(bytes) {
|
|
326
|
-
const arr = new Uint8Array(bytes);
|
|
327
|
-
crypto.getRandomValues(arr);
|
|
328
|
-
let hex = "";
|
|
329
|
-
for (const byte of arr)
|
|
330
|
-
hex += byte.toString(16).padStart(2, "0");
|
|
331
|
-
return hex;
|
|
332
|
-
}
|
|
333
|
-
function createTraceContext() {
|
|
334
|
-
return { traceId: randomHex(16), spanId: randomHex(8) };
|
|
335
|
-
}
|
|
336
|
-
function parseTraceparent(header) {
|
|
337
|
-
if (!header)
|
|
338
|
-
return null;
|
|
339
|
-
const match = TRACEPARENT_RE.exec(header.trim());
|
|
340
|
-
if (!match?.[1] || !match[2] || !match[3] || !match[4] || match[5] === undefined)
|
|
341
|
-
return null;
|
|
342
|
-
const version = match[1].toLowerCase();
|
|
343
|
-
if (version === "ff")
|
|
344
|
-
return null;
|
|
345
|
-
const suffix = match[5];
|
|
346
|
-
if (version === "00" ? suffix !== "" : suffix !== "" && !/^(?:-[0-9a-f]{2,})+$/i.test(suffix)) {
|
|
347
|
-
return null;
|
|
348
|
-
}
|
|
349
|
-
const traceId = match[2].toLowerCase();
|
|
350
|
-
const parentSpanId = match[3].toLowerCase();
|
|
351
|
-
const traceFlags = match[4].toLowerCase();
|
|
352
|
-
if (/^0+$/.test(traceId) || /^0+$/.test(parentSpanId))
|
|
353
|
-
return null;
|
|
354
|
-
return { traceId, spanId: randomHex(8), parentSpanId, traceFlags };
|
|
355
|
-
}
|
|
356
|
-
function formatTraceparent(ctx) {
|
|
357
|
-
return `00-${ctx.traceId}-${ctx.spanId}-${ctx.traceFlags ?? "01"}`;
|
|
358
|
-
}
|
|
359
|
-
function resolveTraceContext(req) {
|
|
360
|
-
return parseTraceparent(req.headers.get("traceparent")) ?? createTraceContext();
|
|
361
|
-
}
|
|
362
|
-
function childSpan(parent) {
|
|
363
|
-
return {
|
|
364
|
-
traceId: parent.traceId,
|
|
365
|
-
spanId: randomHex(8),
|
|
366
|
-
parentSpanId: parent.spanId,
|
|
367
|
-
...parent.tracestate !== undefined && { tracestate: parent.tracestate },
|
|
368
|
-
...parent.baggage !== undefined && { baggage: parent.baggage },
|
|
369
|
-
...parent.traceFlags !== undefined && { traceFlags: parent.traceFlags }
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
var encoder = new TextEncoder;
|
|
373
|
-
function boundedPropagationValue(value, maxBytes, maxMembers) {
|
|
374
|
-
if (typeof value !== "string")
|
|
375
|
-
return;
|
|
376
|
-
const trimmed = value.trim();
|
|
377
|
-
if (!trimmed || encoder.encode(trimmed).byteLength > maxBytes)
|
|
378
|
-
return;
|
|
379
|
-
for (const character of trimmed) {
|
|
380
|
-
const code = character.charCodeAt(0);
|
|
381
|
-
if (code <= 31 || code === 127)
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
const members = trimmed.split(",");
|
|
385
|
-
if (members.length > maxMembers || members.some((member) => !member.trim())) {
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
return trimmed;
|
|
389
|
-
}
|
|
390
|
-
function resolvePropagationContext(metadata, fallback) {
|
|
391
|
-
const traceparent = metadata?.traceparent;
|
|
392
|
-
const parsed = typeof traceparent === "string" ? parseTraceparent(traceparent) : null;
|
|
393
|
-
const trace = traceparent === undefined ? fallback ?? createTraceContext() : parsed ?? createTraceContext();
|
|
394
|
-
const tracestate = parsed ? boundedPropagationValue(metadata?.tracestate, 512, 32) : undefined;
|
|
395
|
-
const baggage = boundedPropagationValue(metadata?.baggage, 8192, 180);
|
|
396
|
-
return {
|
|
397
|
-
...trace,
|
|
398
|
-
...tracestate !== undefined && { tracestate },
|
|
399
|
-
...baggage !== undefined && { baggage }
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
|
|
403
269
|
// src/observability/context.ts
|
|
404
270
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
405
271
|
var storage = new AsyncLocalStorage;
|
|
@@ -454,4 +320,4 @@ function wrapInRequestContext(handler, options = {}) {
|
|
|
454
320
|
};
|
|
455
321
|
}
|
|
456
322
|
|
|
457
|
-
export {
|
|
323
|
+
export { isUnsafeKey, safeJsonParse, mergeMeta, AppError, notFound, badRequest, unauthorized, forbidden, conflict, rateLimited, STITCH_ERROR_STATUS, isStitchErrorCode, appError, bytesToBase64Url, base64UrlToBytes, formatZodError, zodIssues, errorCode, recordedErrorMessage, normalizeError, validateDeclaredOutput, generateTraceId, resolveTraceId, resolveSocketIp, extractIp, getClientInfo, parseQueryParams, runWithRequestContext, getRequestContext, getTraceId, getUserId, setRequestUser, setRequestEndpoint, setRequestDimensions, setRequestError, wrapInRequestContext };
|
|
@@ -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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { type ClientConfig, type ClientContract, type ClientRegistryValue, type ContractClientConfig, contractEndpointMatchers, createClient, createClients, createScopedClients, createScopedUrlBuilders, createUrlBuilder, createUrlBuilders, type PathPrefixArgs, type RegistryScope, type ScopeClientConfigs, type ScopedClientRegistry, type ScopedUrlBuilderRegistry, type UrlBuilderConfig, } from './browser/client';
|
|
1
|
+
export { type ClientConfig, type ClientContract, type ClientFetch, type ClientRegistryValue, type ContractClientConfig, contractEndpointMatchers, createClient, createClients, createScopedClients, createScopedUrlBuilders, createUrlBuilder, createUrlBuilders, type PathPrefixArgs, type RegistryScope, type ScopeClientConfigs, type ScopedClientRegistry, type ScopedUrlBuilderRegistry, type UrlBuilderConfig, } from './browser/client';
|
|
2
2
|
export { ApiError, type ApiEvent, type ApiEventListener, type ConfiguredHttpClient, createHttpClient, type HeaderProvider, type HttpClient, type HttpClientConfig, type RequestOptions, type UnauthorizedMatcher, } from './browser/http';
|
|
3
3
|
export type { RealtimeClient, RealtimeClientOptions, SocketEventMap, SocketIOClient, SocketIOClientConfig, } from './browser/socket-io';
|
|
4
4
|
export { createRealtimeClient, createSocketIOClient } from './browser/socket-io';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,wBAAwB,EACxB,YAAY,EACZ,aAAa,EACb,mBAAmB,EACnB,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,QAAQ,EACR,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,cAAc,EACd,qBAAqB,EACrB,cAAc,EACd,cAAc,EACd,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AACjF,OAAO,EAAE,KAAK,eAAe,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAClE,cAAc,YAAY,CAAC;AAC3B,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAG7C,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,YAAY,GAClB,MAAM,uBAAuB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,oBAAoB,EAAE,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,wBAAwB,EACxB,YAAY,EACZ,aAAa,EACb,mBAAmB,EACnB,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,QAAQ,EACR,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,cAAc,EACd,qBAAqB,EACrB,cAAc,EACd,cAAc,EACd,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AACjF,OAAO,EAAE,KAAK,eAAe,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAClE,cAAc,YAAY,CAAC;AAC3B,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAG7C,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,YAAY,GAClB,MAAM,uBAAuB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,oBAAoB,EAAE,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC"}
|