stitchkit 0.7.0 → 0.8.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 +20 -10
- package/dist/cli.js +3 -7
- package/dist/contract/index.d.ts +1 -1
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +5 -2
- package/dist/contract/pagination.d.ts +11 -0
- package/dist/contract/pagination.d.ts.map +1 -1
- package/dist/{index-za2p453b.js → index-031q8xmx.js} +1 -1
- package/dist/{index-hty3443r.js → index-9zrq8x5z.js} +9 -6
- package/dist/{index-9bk6r5ff.js → index-eq29zkrx.js} +26 -1
- package/dist/{index-p8byjw3e.js → index-f39j6twc.js} +4 -8
- package/dist/index-jgpsd7dy.js +105 -0
- package/dist/{index-mwmpw6j1.js → index-p9m9c0jw.js} +2 -4
- package/dist/{index-8hsj2qm9.js → index-svvpy5bz.js} +18 -19
- package/dist/index-tm7dqzxc.js +20 -0
- package/dist/index.js +434 -14
- package/dist/node.js +4 -8
- package/dist/observability/index.js +4 -7
- package/dist/react.js +0 -2
- package/dist/server/index.js +89 -34
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/types.d.ts +10 -2
- package/dist/server/types.d.ts.map +1 -1
- package/dist/tools/mcp-app.d.ts.map +1 -1
- package/dist/tools.js +253 -21
- package/llms-full.txt +3025 -0
- package/llms.txt +23 -0
- package/package.json +8 -4
- package/dist/index-37x76zdn.js +0 -4
- package/dist/index-4843j09b.js +0 -37
- package/dist/index-48ffdxgk.js +0 -6
- package/dist/index-809wc1tt.js +0 -18
- package/dist/index-kdkvp26v.js +0 -380
- package/dist/index-kzfs85xp.js +0 -9
- package/dist/index-mahbaen9.js +0 -72
- package/dist/index-x3fcszf8.js +0 -8
package/llms.txt
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# stitchkit
|
|
2
|
+
|
|
3
|
+
> Contract-first backend framework for Bun and Node. One `defineContract()` becomes an HTTP API, MCP tools, AI-agent tools, a CLI and a fully-typed client — one source of truth, no drift.
|
|
4
|
+
|
|
5
|
+
Build with stitchkit: define a contract once, then `implement` it and serve it (`createServer` on Bun, `serveNode` on Node ≥ 22). The same contract drives MCP / agent tools and a typed client, so the transports cannot diverge. The pages below are the full guide; `llms-full.txt` (in this package) inlines all of them for offline reading.
|
|
6
|
+
|
|
7
|
+
## Guide
|
|
8
|
+
- [Getting started](https://github.com/max-listov/stitchkit/blob/master/docs/guide/getting-started.md): install, entrypoints, and a first contract → server → client app
|
|
9
|
+
- [Contracts](https://github.com/max-listov/stitchkit/blob/master/docs/guide/contracts.md): every endpoint field — method, path, params/input/output, scope, expose, meta, multipart
|
|
10
|
+
- [HTTP server](https://github.com/max-listov/stitchkit/blob/master/docs/guide/server.md): createServer/createHandler, implement, lifecycle hooks, raw routes + helpers, scopePrefixes, serveFile, primitives
|
|
11
|
+
- [Typed client](https://github.com/max-listov/stitchkit/blob/master/docs/guide/client.md): createClient/createHttpClient, the typed call surface, scoped clients, SSE
|
|
12
|
+
- [MCP & agents](https://github.com/max-listov/stitchkit/blob/master/docs/guide/mcp-and-agents.md): contracts as MCP tools (createMcpHandler) and AI-agent tools (mountAgent); tool lifecycle, extend, identity
|
|
13
|
+
- [CLI](https://github.com/max-listov/stitchkit/blob/master/docs/guide/cli.md): contracts as a command-line program
|
|
14
|
+
- [Realtime](https://github.com/max-listov/stitchkit/blob/master/docs/guide/realtime.md): Socket.IO server/client wrappers, handshake auth, the cache bridge, a raw WebSocket lane
|
|
15
|
+
- [Auth & errors](https://github.com/max-listov/stitchkit/blob/master/docs/guide/auth-and-errors.md): scopes, createAuthHook, JWT/cookies, the AppError model, the stitch error-code registry
|
|
16
|
+
- [Observability](https://github.com/max-listov/stitchkit/blob/master/docs/guide/observability.md): request and tool-call logging via hooks, W3C trace context, createAuditHook
|
|
17
|
+
- [Testing & deployment](https://github.com/max-listov/stitchkit/blob/master/docs/guide/testing-and-deployment.md): in-process testing; deploying on Bun and on Node (serveNode)
|
|
18
|
+
- [Multi-tenant](https://github.com/max-listov/stitchkit/blob/master/docs/guide/multi-tenant.md): a /tenants/:id/… scenario end-to-end — scopePrefixes, scoped client, extend
|
|
19
|
+
- [Upgrading](https://github.com/max-listov/stitchkit/blob/master/docs/guide/upgrading.md): moving a project across stitchkit versions; how breaking changes are marked
|
|
20
|
+
|
|
21
|
+
## Reference
|
|
22
|
+
- [API reference](https://github.com/max-listov/stitchkit/blob/master/docs/api/reference.md): every public export, grouped by entrypoint, each linked to the guide
|
|
23
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stitchkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Contract-first backend framework — one defineContract() into an HTTP API, MCP tools, AI-agent tools and a typed client. Bun and Node.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
"types": "./dist/index.d.ts",
|
|
39
39
|
"files": [
|
|
40
40
|
"dist",
|
|
41
|
+
"llms.txt",
|
|
42
|
+
"llms-full.txt",
|
|
41
43
|
"README.md",
|
|
42
44
|
"LICENSE"
|
|
43
45
|
],
|
|
@@ -80,11 +82,13 @@
|
|
|
80
82
|
},
|
|
81
83
|
"scripts": {
|
|
82
84
|
"check": "bun x tsc --noEmit",
|
|
83
|
-
"build:
|
|
85
|
+
"build:browser": "bun build src/index.ts src/react.ts src/contract/index.ts --outdir dist --target node --packages external --splitting --root src",
|
|
86
|
+
"build:server": "bun build src/server/index.ts src/node.ts src/tools.ts src/cli.ts src/observability/index.ts --outdir dist --target node --packages external --splitting --root src",
|
|
87
|
+
"build:js": "bun run build:browser && bun run build:server",
|
|
84
88
|
"build:types": "bun x tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
85
|
-
"build": "rm -rf dist && bun run build:js && bun run build:types",
|
|
89
|
+
"build": "rm -rf dist && bun run build:js && bun run build:types && bun scripts/check-browser-clean.mjs",
|
|
86
90
|
"dev": "bun run build:js -- --watch",
|
|
87
|
-
"prepublishOnly": "cp ../../README.md ./README.md && bun run build",
|
|
91
|
+
"prepublishOnly": "cp ../../README.md ./README.md && bun ../../scripts/gen-llms.ts && bun run build",
|
|
88
92
|
"test": "bun test",
|
|
89
93
|
"smoke:node": "node scripts/node-smoke.mjs"
|
|
90
94
|
},
|
package/dist/index-37x76zdn.js
DELETED
package/dist/index-4843j09b.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
AppError
|
|
3
|
-
} from "./index-9bk6r5ff.js";
|
|
4
|
-
|
|
5
|
-
// src/internal/errors.ts
|
|
6
|
-
import { z } from "zod";
|
|
7
|
-
function formatZodError(error) {
|
|
8
|
-
const issues = error.issues.slice(0, 5);
|
|
9
|
-
const lines = issues.map((issue) => {
|
|
10
|
-
const path = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
11
|
-
return `${path}: ${issue.message}`;
|
|
12
|
-
});
|
|
13
|
-
const suffix = error.issues.length > 5 ? `
|
|
14
|
-
...and ${error.issues.length - 5} more issues` : "";
|
|
15
|
-
return lines.join(`
|
|
16
|
-
`) + suffix;
|
|
17
|
-
}
|
|
18
|
-
function normalizeError(err) {
|
|
19
|
-
if (AppError.is(err))
|
|
20
|
-
return err;
|
|
21
|
-
if (err instanceof z.ZodError) {
|
|
22
|
-
return new AppError("VALIDATION_ERROR", formatZodError(err), 400);
|
|
23
|
-
}
|
|
24
|
-
console.error("[stitchkit] unhandled error:", err);
|
|
25
|
-
return new AppError("INTERNAL_SERVER_ERROR", "Internal server error", 500);
|
|
26
|
-
}
|
|
27
|
-
function validateHandlerOutput(schema, data) {
|
|
28
|
-
const parsed = schema.safeParse(data);
|
|
29
|
-
if (parsed.success)
|
|
30
|
-
return { ok: true, data: parsed.data };
|
|
31
|
-
return {
|
|
32
|
-
ok: false,
|
|
33
|
-
message: `Handler output does not match the contract: ${formatZodError(parsed.error)}`
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export { formatZodError, normalizeError, validateHandlerOutput };
|
package/dist/index-48ffdxgk.js
DELETED
package/dist/index-809wc1tt.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// src/internal/typed.ts
|
|
2
|
-
function typedEntries(value) {
|
|
3
|
-
return Object.entries(value);
|
|
4
|
-
}
|
|
5
|
-
function isRecord(value) {
|
|
6
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
-
}
|
|
8
|
-
function mapObject(source, mapper) {
|
|
9
|
-
const result = {};
|
|
10
|
-
for (const [key, value] of typedEntries(source)) {
|
|
11
|
-
const mapped = mapper(key, value);
|
|
12
|
-
if (mapped !== undefined)
|
|
13
|
-
result[key] = mapped;
|
|
14
|
-
}
|
|
15
|
-
return result;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export { typedEntries, isRecord, mapObject };
|
package/dist/index-kdkvp26v.js
DELETED
|
@@ -1,380 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
inputIsQuery
|
|
3
|
-
} from "./index-48ffdxgk.js";
|
|
4
|
-
import {
|
|
5
|
-
isRecord,
|
|
6
|
-
mapObject,
|
|
7
|
-
typedEntries
|
|
8
|
-
} from "./index-809wc1tt.js";
|
|
9
|
-
|
|
10
|
-
// src/browser/http.ts
|
|
11
|
-
import ky, { isHTTPError } from "ky";
|
|
12
|
-
class ApiError extends Error {
|
|
13
|
-
code;
|
|
14
|
-
status;
|
|
15
|
-
details;
|
|
16
|
-
hint;
|
|
17
|
-
constructor(code, status = 0, details, message, hint) {
|
|
18
|
-
super(message ?? `API Error: ${code}`);
|
|
19
|
-
this.code = code;
|
|
20
|
-
this.status = status;
|
|
21
|
-
this.details = details;
|
|
22
|
-
this.hint = hint;
|
|
23
|
-
this.name = "ApiError";
|
|
24
|
-
}
|
|
25
|
-
static is(error) {
|
|
26
|
-
return error instanceof ApiError;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
function parseApiErrorBody(body) {
|
|
30
|
-
if (!isRecord(body) || !isRecord(body.error))
|
|
31
|
-
return null;
|
|
32
|
-
const error = body.error;
|
|
33
|
-
if (typeof error.code !== "string")
|
|
34
|
-
return null;
|
|
35
|
-
return {
|
|
36
|
-
code: error.code,
|
|
37
|
-
message: typeof error.message === "string" ? error.message : undefined,
|
|
38
|
-
details: error.details,
|
|
39
|
-
hint: typeof error.hint === "string" ? error.hint : undefined
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
function createHttpClient(config) {
|
|
43
|
-
let ssrCookies = null;
|
|
44
|
-
let isLoggedOut = false;
|
|
45
|
-
const listeners = new Set;
|
|
46
|
-
const authEndpoints = config.authEndpoints ?? ["/auth/"];
|
|
47
|
-
const parseError = config.parseError ?? parseApiErrorBody;
|
|
48
|
-
function emit(event) {
|
|
49
|
-
for (const fn of listeners) {
|
|
50
|
-
try {
|
|
51
|
-
fn(event);
|
|
52
|
-
} catch {}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
const client = ky.create({
|
|
56
|
-
prefix: config.baseUrl,
|
|
57
|
-
credentials: config.credentials ?? "include",
|
|
58
|
-
timeout: config.timeout ?? 30000,
|
|
59
|
-
retry: {
|
|
60
|
-
limit: config.retry?.limit ?? 2,
|
|
61
|
-
methods: config.retry?.methods ?? ["get"],
|
|
62
|
-
statusCodes: config.retry?.statusCodes ?? []
|
|
63
|
-
},
|
|
64
|
-
hooks: {
|
|
65
|
-
beforeRequest: [
|
|
66
|
-
({ request: request2 }) => {
|
|
67
|
-
if (ssrCookies) {
|
|
68
|
-
request2.headers.set("Cookie", ssrCookies);
|
|
69
|
-
}
|
|
70
|
-
const extra = typeof config.headers === "function" ? config.headers() : config.headers;
|
|
71
|
-
if (extra) {
|
|
72
|
-
for (const [key, value] of Object.entries(extra)) {
|
|
73
|
-
request2.headers.set(key, value);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
],
|
|
78
|
-
afterResponse: [
|
|
79
|
-
async ({ request: request2, response }) => {
|
|
80
|
-
if (response.status === 401) {
|
|
81
|
-
const url = new URL(request2.url).pathname;
|
|
82
|
-
if (!isLoggedOut && !authEndpoints.some((path) => url.startsWith(path))) {
|
|
83
|
-
isLoggedOut = true;
|
|
84
|
-
emit({ type: "unauthorized" });
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (!response.ok) {
|
|
88
|
-
const body = await response.clone().json().catch(() => null);
|
|
89
|
-
if (body) {
|
|
90
|
-
const parsed = parseError(body);
|
|
91
|
-
if (parsed) {
|
|
92
|
-
throw new ApiError(parsed.code, response.status, parsed.details, parsed.message, parsed.hint);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
]
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
async function request(method, url, data, options = {}) {
|
|
101
|
-
const kyOptions = {
|
|
102
|
-
timeout: options.timeout
|
|
103
|
-
};
|
|
104
|
-
if (options.params) {
|
|
105
|
-
const searchParams = new URLSearchParams;
|
|
106
|
-
for (const [key, value] of Object.entries(options.params)) {
|
|
107
|
-
if (value === undefined)
|
|
108
|
-
continue;
|
|
109
|
-
if (Array.isArray(value)) {
|
|
110
|
-
for (const item of value)
|
|
111
|
-
searchParams.append(key, String(item));
|
|
112
|
-
} else {
|
|
113
|
-
searchParams.set(key, String(value));
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (searchParams.size > 0) {
|
|
117
|
-
kyOptions.searchParams = searchParams;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
if (data instanceof FormData) {
|
|
121
|
-
kyOptions.body = data;
|
|
122
|
-
} else if (data !== undefined) {
|
|
123
|
-
kyOptions.json = data;
|
|
124
|
-
}
|
|
125
|
-
try {
|
|
126
|
-
if (options.responseType === "blob") {
|
|
127
|
-
return client[method](url, kyOptions).blob();
|
|
128
|
-
}
|
|
129
|
-
const response = await client[method](url, kyOptions);
|
|
130
|
-
if (response.status === 204 || response.headers.get("content-length") === "0") {
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
return response.json();
|
|
134
|
-
} catch (error) {
|
|
135
|
-
if (ApiError.is(error))
|
|
136
|
-
throw error;
|
|
137
|
-
const isAbort = error instanceof Error && error.name === "AbortError";
|
|
138
|
-
if (!isAbort) {
|
|
139
|
-
emit({ type: "network_error" });
|
|
140
|
-
}
|
|
141
|
-
const status = isAbort ? 0 : isHTTPError(error) ? error.response.status : 0;
|
|
142
|
-
const msg = error instanceof Error ? error.message : undefined;
|
|
143
|
-
throw new ApiError("UNKNOWN_ERROR", status, msg ? { message: msg } : undefined);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return {
|
|
147
|
-
get: (url, options) => request("get", url, undefined, options),
|
|
148
|
-
post: (url, data, options) => request("post", url, data, options),
|
|
149
|
-
put: (url, data, options) => request("put", url, data, options),
|
|
150
|
-
patch: (url, data, options) => request("patch", url, data, options),
|
|
151
|
-
delete: (url, options) => request("delete", url, undefined, options),
|
|
152
|
-
setServerContext(cookies) {
|
|
153
|
-
ssrCookies = cookies;
|
|
154
|
-
},
|
|
155
|
-
subscribe(listener) {
|
|
156
|
-
listeners.add(listener);
|
|
157
|
-
return () => listeners.delete(listener);
|
|
158
|
-
},
|
|
159
|
-
logout() {
|
|
160
|
-
isLoggedOut = true;
|
|
161
|
-
emit({ type: "logout" });
|
|
162
|
-
},
|
|
163
|
-
resetLogoutState() {
|
|
164
|
-
isLoggedOut = false;
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// src/browser/client.ts
|
|
170
|
-
function withTimeout(options, timeout) {
|
|
171
|
-
if (timeout === undefined)
|
|
172
|
-
return options;
|
|
173
|
-
return { ...options, timeout };
|
|
174
|
-
}
|
|
175
|
-
function isParamArray(value) {
|
|
176
|
-
return Array.isArray(value) && value.every((v) => typeof v === "string" || typeof v === "number");
|
|
177
|
-
}
|
|
178
|
-
function collectQueryParams(args, skipKeys) {
|
|
179
|
-
const params = {};
|
|
180
|
-
let hasParams = false;
|
|
181
|
-
for (const [key, value] of Object.entries(args)) {
|
|
182
|
-
if (skipKeys.has(key) || value === undefined || value === null)
|
|
183
|
-
continue;
|
|
184
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || isParamArray(value)) {
|
|
185
|
-
params[key] = value;
|
|
186
|
-
hasParams = true;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
return hasParams ? params : undefined;
|
|
190
|
-
}
|
|
191
|
-
function createClient(contract, configOrClient, contractConfig) {
|
|
192
|
-
const client = {};
|
|
193
|
-
const makeMethod = isHttpAdapter(configOrClient) ? (endpoint) => createHttpMethod(endpoint, contract.meta.prefix, configOrClient, contractConfig) : (endpoint) => createFetchMethod(endpoint, contract.meta.prefix, configOrClient, contractConfig);
|
|
194
|
-
for (const [key, endpoint] of typedEntries(contract.endpoints)) {
|
|
195
|
-
if (endpoint.expose && !endpoint.expose.includes("HTTP"))
|
|
196
|
-
continue;
|
|
197
|
-
setClientMethod(client, key, makeMethod(endpoint));
|
|
198
|
-
}
|
|
199
|
-
return client;
|
|
200
|
-
}
|
|
201
|
-
function createClients(contracts, http) {
|
|
202
|
-
return mapObject(contracts, (_key, contract) => createClient(contract, http));
|
|
203
|
-
}
|
|
204
|
-
function isHttpAdapter(value) {
|
|
205
|
-
return typeof value === "object" && "get" in value && typeof value.get === "function";
|
|
206
|
-
}
|
|
207
|
-
function setClientMethod(target, key, method) {
|
|
208
|
-
target[key] = method;
|
|
209
|
-
}
|
|
210
|
-
function createHttpMethod(endpoint, prefix, client, config) {
|
|
211
|
-
const httpMethod = endpoint.method.toLowerCase();
|
|
212
|
-
const isGet = httpMethod === "get";
|
|
213
|
-
const paramNames = extractParamNames(endpoint.path);
|
|
214
|
-
const prefixKeys = new Set([...config?.stripPrefixKeys ?? [], ...paramNames]);
|
|
215
|
-
return (...args) => {
|
|
216
|
-
const firstArg = args[0] ?? {};
|
|
217
|
-
let pathPrefixStr = "";
|
|
218
|
-
if (config?.pathPrefix) {
|
|
219
|
-
pathPrefixStr = typeof config.pathPrefix === "function" ? config.pathPrefix(firstArg) : config.pathPrefix;
|
|
220
|
-
if (pathPrefixStr && !pathPrefixStr.endsWith("/"))
|
|
221
|
-
pathPrefixStr += "/";
|
|
222
|
-
}
|
|
223
|
-
let url = `${pathPrefixStr}${prefix}${endpoint.path}`;
|
|
224
|
-
for (const name of paramNames) {
|
|
225
|
-
const value = firstArg[name];
|
|
226
|
-
if (value === undefined || value === null) {
|
|
227
|
-
throw new Error(`Missing path param: ${name}`);
|
|
228
|
-
}
|
|
229
|
-
url = url.replace(`:${name}`, encodeURIComponent(String(value)));
|
|
230
|
-
}
|
|
231
|
-
if (url.endsWith("/"))
|
|
232
|
-
url = url.slice(0, -1);
|
|
233
|
-
if (endpoint.multipart) {
|
|
234
|
-
const file = firstArg[endpoint.multipart];
|
|
235
|
-
if (!(file instanceof Blob)) {
|
|
236
|
-
throw new Error(`Missing multipart file field: ${endpoint.multipart}`);
|
|
237
|
-
}
|
|
238
|
-
const formData = new FormData;
|
|
239
|
-
formData.append(endpoint.multipart, file);
|
|
240
|
-
appendFormFields(formData, firstArg, new Set([...prefixKeys, endpoint.multipart]));
|
|
241
|
-
return client.post(url, formData, withTimeout(undefined, endpoint.timeout));
|
|
242
|
-
}
|
|
243
|
-
if (isGet) {
|
|
244
|
-
const params = collectQueryParams(firstArg, prefixKeys);
|
|
245
|
-
return client.get(url, withTimeout(params ? { params } : undefined, endpoint.timeout));
|
|
246
|
-
}
|
|
247
|
-
if (httpMethod === "delete") {
|
|
248
|
-
const params = collectQueryParams(firstArg, prefixKeys);
|
|
249
|
-
return client.delete(url, withTimeout(params ? { params } : undefined, endpoint.timeout));
|
|
250
|
-
}
|
|
251
|
-
const payload = {};
|
|
252
|
-
for (const [key, value] of Object.entries(firstArg)) {
|
|
253
|
-
if (!prefixKeys.has(key) && value !== undefined) {
|
|
254
|
-
payload[key] = value;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
return client[httpMethod](url, Object.keys(payload).length > 0 ? payload : undefined, withTimeout(undefined, endpoint.timeout));
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
function createFetchMethod(endpoint, prefix, config, contractConfig) {
|
|
261
|
-
const prefixKeys = new Set(contractConfig?.stripPrefixKeys ?? []);
|
|
262
|
-
return async (args) => {
|
|
263
|
-
let pathPrefixStr = "";
|
|
264
|
-
if (contractConfig?.pathPrefix) {
|
|
265
|
-
pathPrefixStr = typeof contractConfig.pathPrefix === "function" ? contractConfig.pathPrefix(args ?? {}) : contractConfig.pathPrefix;
|
|
266
|
-
if (pathPrefixStr && !pathPrefixStr.endsWith("/"))
|
|
267
|
-
pathPrefixStr += "/";
|
|
268
|
-
}
|
|
269
|
-
let url = buildFetchUrl(config.baseUrl, prefix, endpoint.path, args, pathPrefixStr);
|
|
270
|
-
const headers = {
|
|
271
|
-
Accept: "application/json",
|
|
272
|
-
...typeof config.headers === "function" ? config.headers() : config.headers
|
|
273
|
-
};
|
|
274
|
-
const isQuery = inputIsQuery(endpoint.method);
|
|
275
|
-
const hasBody = !isQuery && !endpoint.multipart && endpoint.input && args;
|
|
276
|
-
if (isQuery && args) {
|
|
277
|
-
const remaining = stripParams(args, endpoint.path, prefixKeys);
|
|
278
|
-
const searchParams = new URLSearchParams;
|
|
279
|
-
for (const [k, v] of Object.entries(remaining)) {
|
|
280
|
-
if (v === undefined || v === null)
|
|
281
|
-
continue;
|
|
282
|
-
if (isParamArray(v)) {
|
|
283
|
-
for (const item of v)
|
|
284
|
-
searchParams.append(k, String(item));
|
|
285
|
-
} else if (typeof v !== "object") {
|
|
286
|
-
searchParams.set(k, String(v));
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
if (searchParams.size > 0)
|
|
290
|
-
url += `?${searchParams}`;
|
|
291
|
-
}
|
|
292
|
-
if (hasBody)
|
|
293
|
-
headers["Content-Type"] = "application/json";
|
|
294
|
-
if (endpoint.multipart && args) {
|
|
295
|
-
const file = args[endpoint.multipart];
|
|
296
|
-
if (!(file instanceof Blob)) {
|
|
297
|
-
throw new Error(`Missing multipart file field: ${endpoint.multipart}`);
|
|
298
|
-
}
|
|
299
|
-
const formData = new FormData;
|
|
300
|
-
formData.append(endpoint.multipart, file);
|
|
301
|
-
appendFormFields(formData, stripParams(args, endpoint.path, prefixKeys), new Set([endpoint.multipart]));
|
|
302
|
-
const res2 = await fetch(url, {
|
|
303
|
-
method: endpoint.method,
|
|
304
|
-
headers,
|
|
305
|
-
credentials: config.credentials,
|
|
306
|
-
body: formData
|
|
307
|
-
});
|
|
308
|
-
if (!res2.ok) {
|
|
309
|
-
await throwForErrorResponse(res2, config, null);
|
|
310
|
-
}
|
|
311
|
-
if (res2.status === 204)
|
|
312
|
-
return;
|
|
313
|
-
const json2 = await res2.json();
|
|
314
|
-
return endpoint.output ? endpoint.output.parse(json2) : json2;
|
|
315
|
-
}
|
|
316
|
-
const res = await fetch(url, {
|
|
317
|
-
method: endpoint.method,
|
|
318
|
-
headers,
|
|
319
|
-
credentials: config.credentials,
|
|
320
|
-
...hasBody && {
|
|
321
|
-
body: JSON.stringify(stripParams(hasBody, endpoint.path, prefixKeys))
|
|
322
|
-
}
|
|
323
|
-
});
|
|
324
|
-
if (!res.ok) {
|
|
325
|
-
await throwForErrorResponse(res, config, { error: res.statusText });
|
|
326
|
-
}
|
|
327
|
-
if (res.status === 204)
|
|
328
|
-
return;
|
|
329
|
-
const json = await res.json();
|
|
330
|
-
return endpoint.output ? endpoint.output.parse(json) : json;
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
function appendFormFields(formData, values, skipKeys) {
|
|
334
|
-
for (const [key, value] of Object.entries(values)) {
|
|
335
|
-
if (skipKeys.has(key) || value === undefined || value === null)
|
|
336
|
-
continue;
|
|
337
|
-
formData.append(key, typeof value === "string" ? value : JSON.stringify(value));
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
async function throwForErrorResponse(res, config, fallbackBody) {
|
|
341
|
-
const body = await res.json().catch(() => fallbackBody);
|
|
342
|
-
config.onError?.(res.status, body);
|
|
343
|
-
const parsed = parseApiErrorBody(body);
|
|
344
|
-
if (parsed) {
|
|
345
|
-
throw new ApiError(parsed.code, res.status, parsed.details, parsed.message, parsed.hint);
|
|
346
|
-
}
|
|
347
|
-
throw new ApiError("HTTP_ERROR", res.status, { body });
|
|
348
|
-
}
|
|
349
|
-
function extractParamNames(path) {
|
|
350
|
-
const matches = path.match(/:(\w+)/g);
|
|
351
|
-
return matches ? matches.map((m) => m.slice(1)) : [];
|
|
352
|
-
}
|
|
353
|
-
function buildFetchUrl(baseUrl, prefix, path, args, pathPrefix = "") {
|
|
354
|
-
let fullPath = `/${pathPrefix}${prefix}${path === "/" ? "" : path}`;
|
|
355
|
-
if (args) {
|
|
356
|
-
fullPath = fullPath.replace(/:(\w+)/g, (_, key) => {
|
|
357
|
-
const val = args[key];
|
|
358
|
-
if (val === undefined || val === null) {
|
|
359
|
-
throw new Error(`Missing path param: ${key}`);
|
|
360
|
-
}
|
|
361
|
-
return encodeURIComponent(String(val));
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
return `${baseUrl}${fullPath}`;
|
|
365
|
-
}
|
|
366
|
-
function stripParams(args, path, extra) {
|
|
367
|
-
const skip = new Set(extra);
|
|
368
|
-
for (const match of path.matchAll(/:(\w+)/g)) {
|
|
369
|
-
if (match[1])
|
|
370
|
-
skip.add(match[1]);
|
|
371
|
-
}
|
|
372
|
-
const result = {};
|
|
373
|
-
for (const [k, v] of Object.entries(args)) {
|
|
374
|
-
if (!skip.has(k))
|
|
375
|
-
result[k] = v;
|
|
376
|
-
}
|
|
377
|
-
return result;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
export { ApiError, createHttpClient, createClient, createClients };
|
package/dist/index-kzfs85xp.js
DELETED
package/dist/index-mahbaen9.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
normalizeError
|
|
3
|
-
} from "./index-4843j09b.js";
|
|
4
|
-
|
|
5
|
-
// src/server/stream.ts
|
|
6
|
-
function streamSSE(generator) {
|
|
7
|
-
const encoder = new TextEncoder;
|
|
8
|
-
const stream = new ReadableStream({
|
|
9
|
-
async start(controller) {
|
|
10
|
-
try {
|
|
11
|
-
for await (const chunk of generator) {
|
|
12
|
-
const data = JSON.stringify(chunk);
|
|
13
|
-
controller.enqueue(encoder.encode(`data: ${data}
|
|
14
|
-
|
|
15
|
-
`));
|
|
16
|
-
}
|
|
17
|
-
controller.enqueue(encoder.encode(`data: [DONE]
|
|
18
|
-
|
|
19
|
-
`));
|
|
20
|
-
controller.close();
|
|
21
|
-
} catch (err) {
|
|
22
|
-
const envelope = normalizeError(err).toJSON();
|
|
23
|
-
controller.enqueue(encoder.encode(`data: ${JSON.stringify(envelope)}
|
|
24
|
-
|
|
25
|
-
`));
|
|
26
|
-
controller.close();
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
return new Response(stream, {
|
|
31
|
-
headers: {
|
|
32
|
-
"Content-Type": "text/event-stream",
|
|
33
|
-
"Cache-Control": "no-cache",
|
|
34
|
-
Connection: "keep-alive"
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
async function* parseSSE(response, options) {
|
|
39
|
-
const reader = response.body?.getReader();
|
|
40
|
-
if (!reader)
|
|
41
|
-
return;
|
|
42
|
-
const decoder = new TextDecoder;
|
|
43
|
-
let buffer = "";
|
|
44
|
-
try {
|
|
45
|
-
while (true) {
|
|
46
|
-
const { done, value } = await reader.read();
|
|
47
|
-
if (done)
|
|
48
|
-
break;
|
|
49
|
-
buffer += decoder.decode(value, { stream: true });
|
|
50
|
-
const lines = buffer.split(`
|
|
51
|
-
`);
|
|
52
|
-
buffer = lines.pop() ?? "";
|
|
53
|
-
for (const rawLine of lines) {
|
|
54
|
-
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
55
|
-
if (!line.startsWith("data:"))
|
|
56
|
-
continue;
|
|
57
|
-
const data = line.slice(5).replace(/^ /, "");
|
|
58
|
-
if (data === "[DONE]")
|
|
59
|
-
return;
|
|
60
|
-
try {
|
|
61
|
-
yield JSON.parse(data);
|
|
62
|
-
} catch (err) {
|
|
63
|
-
options?.onParseError?.(data, err instanceof Error ? err : new Error(String(err)));
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
} finally {
|
|
68
|
-
reader.releaseLock();
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export { streamSSE, parseSSE };
|
package/dist/index-x3fcszf8.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
// src/internal/within-dir.ts
|
|
2
|
-
import { sep } from "node:path";
|
|
3
|
-
function isWithinDir(root, target) {
|
|
4
|
-
const base = root.endsWith(sep) ? root.slice(0, -sep.length) : root;
|
|
5
|
-
return target === root || target === base || target.startsWith(base + sep);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export { isWithinDir };
|