stitchkit 0.1.0 → 0.2.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 +10 -7
- package/dist/contract/define.d.ts.map +1 -1
- package/dist/contract/errors.d.ts +4 -3
- package/dist/contract/errors.d.ts.map +1 -1
- package/dist/contract/index.js +1 -1
- package/dist/{index-gfzn1n4n.js → index-a35v22fh.js} +1 -1
- package/dist/{index-7wfkbvss.js → index-kckky6zw.js} +6 -2
- package/dist/index-v2z2v3mq.js +587 -0
- package/dist/index.js +2 -2
- package/dist/node.d.ts +4 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +26 -0
- package/dist/server/create.d.ts +3 -3
- package/dist/server/create.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 +10 -576
- package/dist/server/node.d.ts +12 -0
- package/dist/server/node.d.ts.map +1 -0
- package/dist/server/types.d.ts +24 -21
- package/dist/server/types.d.ts.map +1 -1
- package/dist/tools/agent.d.ts +15 -2
- package/dist/tools/agent.d.ts.map +1 -1
- package/dist/tools/coerce.d.ts +8 -0
- package/dist/tools/coerce.d.ts.map +1 -0
- package/dist/tools/execute.d.ts +26 -2
- package/dist/tools/execute.d.ts.map +1 -1
- package/dist/tools/flatten.d.ts +15 -0
- package/dist/tools/flatten.d.ts.map +1 -0
- package/dist/tools/json-schema.d.ts +18 -0
- package/dist/tools/json-schema.d.ts.map +1 -0
- package/dist/tools/manifest.d.ts +13 -0
- package/dist/tools/manifest.d.ts.map +1 -0
- package/dist/tools/mcp-handler.d.ts.map +1 -1
- package/dist/tools/mcp.d.ts +53 -3
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/mount.d.ts +18 -6
- package/dist/tools/mount.d.ts.map +1 -1
- package/dist/tools/schema.d.ts +19 -2
- package/dist/tools/schema.d.ts.map +1 -1
- package/dist/tools.d.ts +6 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +337 -154
- package/package.json +15 -4
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeError
|
|
3
|
+
} from "./index-a35v22fh.js";
|
|
4
|
+
import {
|
|
5
|
+
AppError,
|
|
6
|
+
badRequest
|
|
7
|
+
} from "./index-kckky6zw.js";
|
|
8
|
+
import {
|
|
9
|
+
extractIp,
|
|
10
|
+
getClientInfo,
|
|
11
|
+
parseQueryParams,
|
|
12
|
+
resolveTraceId
|
|
13
|
+
} from "./index-ke4mx4ea.js";
|
|
14
|
+
|
|
15
|
+
// src/server/multipart.ts
|
|
16
|
+
var DEFAULT_MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
|
|
17
|
+
async function parseMultipart(req, fileField, fieldsSchema, maxBytes = DEFAULT_MAX_UPLOAD_BYTES) {
|
|
18
|
+
const declared = Number(req.headers.get("content-length") ?? 0);
|
|
19
|
+
if (declared > maxBytes) {
|
|
20
|
+
badRequest(`Upload exceeds the ${Math.round(maxBytes / 1024 / 1024)} MB limit`);
|
|
21
|
+
}
|
|
22
|
+
const formData = await req.formData();
|
|
23
|
+
const file = formData.get(fileField);
|
|
24
|
+
if (!file || !(file instanceof File)) {
|
|
25
|
+
badRequest(`Missing file field: ${fileField}`);
|
|
26
|
+
}
|
|
27
|
+
if (file.size > maxBytes) {
|
|
28
|
+
badRequest(`Upload exceeds the ${Math.round(maxBytes / 1024 / 1024)} MB limit`);
|
|
29
|
+
}
|
|
30
|
+
const fields = {};
|
|
31
|
+
for (const [key, value] of formData.entries()) {
|
|
32
|
+
if (key === fileField)
|
|
33
|
+
continue;
|
|
34
|
+
if (typeof value === "string") {
|
|
35
|
+
try {
|
|
36
|
+
fields[key] = JSON.parse(value);
|
|
37
|
+
} catch {
|
|
38
|
+
fields[key] = value;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const parsed = fieldsSchema ? fieldsSchema.parse(fields) : fields;
|
|
43
|
+
return { file, fields: parsed };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/server/middleware/cors.ts
|
|
47
|
+
function corsHeaders(config, requestOrigin) {
|
|
48
|
+
let allowOrigin;
|
|
49
|
+
if (config.origin === undefined || config.origin === "*") {
|
|
50
|
+
allowOrigin = config.credentials ? requestOrigin ?? undefined : "*";
|
|
51
|
+
} else if (Array.isArray(config.origin)) {
|
|
52
|
+
allowOrigin = requestOrigin && config.origin.includes(requestOrigin) ? requestOrigin : undefined;
|
|
53
|
+
} else {
|
|
54
|
+
allowOrigin = config.origin;
|
|
55
|
+
}
|
|
56
|
+
const headers = {
|
|
57
|
+
"Access-Control-Allow-Methods": config.methods ?? "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
58
|
+
"Access-Control-Allow-Headers": config.headers ?? "Content-Type, Authorization, X-Trace-Id"
|
|
59
|
+
};
|
|
60
|
+
if (allowOrigin !== undefined) {
|
|
61
|
+
headers["Access-Control-Allow-Origin"] = allowOrigin;
|
|
62
|
+
}
|
|
63
|
+
if (config.credentials) {
|
|
64
|
+
headers["Access-Control-Allow-Credentials"] = "true";
|
|
65
|
+
}
|
|
66
|
+
const variesByOrigin = Array.isArray(config.origin) || (config.origin === undefined || config.origin === "*") && Boolean(config.credentials);
|
|
67
|
+
if (variesByOrigin) {
|
|
68
|
+
headers.Vary = "Origin";
|
|
69
|
+
}
|
|
70
|
+
return headers;
|
|
71
|
+
}
|
|
72
|
+
function corsPreflightResponse(config, req) {
|
|
73
|
+
return new Response(null, {
|
|
74
|
+
status: 204,
|
|
75
|
+
headers: corsHeaders(config, req.headers.get("origin"))
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/server/router.ts
|
|
80
|
+
import { resolve, sep } from "node:path";
|
|
81
|
+
function joinPath(...parts) {
|
|
82
|
+
const joined = parts.filter(Boolean).map((part) => part.replace(/^\/+|\/+$/g, "")).filter(Boolean).join("/");
|
|
83
|
+
return `/${joined}`;
|
|
84
|
+
}
|
|
85
|
+
function matchSegments(patternSegments, requestSegments) {
|
|
86
|
+
if (patternSegments.length !== requestSegments.length)
|
|
87
|
+
return null;
|
|
88
|
+
const params = {};
|
|
89
|
+
for (const [i, pattern] of patternSegments.entries()) {
|
|
90
|
+
const actual = requestSegments[i];
|
|
91
|
+
if (actual === undefined)
|
|
92
|
+
return null;
|
|
93
|
+
if (pattern.startsWith(":")) {
|
|
94
|
+
params[pattern.slice(1)] = decodeURIComponent(actual);
|
|
95
|
+
} else if (pattern !== actual) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return params;
|
|
100
|
+
}
|
|
101
|
+
function buildRouteMap(groups) {
|
|
102
|
+
const map = new Map;
|
|
103
|
+
for (const { prefix, service, hooks } of groups) {
|
|
104
|
+
for (const [, method] of Object.entries(service.methods)) {
|
|
105
|
+
if (method.expose && !method.expose.includes("HTTP"))
|
|
106
|
+
continue;
|
|
107
|
+
const servicePath = joinPath("/", service.prefix, method.path === "/" ? "" : method.path);
|
|
108
|
+
const fullPath = prefix ? joinPath(prefix, servicePath) : servicePath;
|
|
109
|
+
const segments = fullPath.split("/").filter(Boolean);
|
|
110
|
+
const entries = map.get(method.method) ?? [];
|
|
111
|
+
entries.push({ method, service, pattern: fullPath, segments, groupHooks: hooks });
|
|
112
|
+
map.set(method.method, entries);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const [, entries] of map) {
|
|
116
|
+
entries.sort((a, b) => {
|
|
117
|
+
const len = Math.min(a.segments.length, b.segments.length);
|
|
118
|
+
for (let i = 0;i < len; i++) {
|
|
119
|
+
const aIsParam = a.segments[i]?.startsWith(":");
|
|
120
|
+
const bIsParam = b.segments[i]?.startsWith(":");
|
|
121
|
+
if (aIsParam !== bIsParam)
|
|
122
|
+
return aIsParam ? 1 : -1;
|
|
123
|
+
}
|
|
124
|
+
return a.segments.length - b.segments.length;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return map;
|
|
128
|
+
}
|
|
129
|
+
function matchRoute(routeMap, httpMethod, pathname) {
|
|
130
|
+
const entries = routeMap.get(httpMethod);
|
|
131
|
+
if (!entries)
|
|
132
|
+
return null;
|
|
133
|
+
const requestSegments = pathname.split("/").filter(Boolean);
|
|
134
|
+
for (const entry of entries) {
|
|
135
|
+
const pathParams = matchSegments(entry.segments, requestSegments);
|
|
136
|
+
if (pathParams) {
|
|
137
|
+
return {
|
|
138
|
+
method: entry.method,
|
|
139
|
+
service: entry.service,
|
|
140
|
+
pathParams,
|
|
141
|
+
groupHooks: entry.groupHooks
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
function allowedMethods(routeMap, pathname) {
|
|
148
|
+
const requestSegments = pathname.split("/").filter(Boolean);
|
|
149
|
+
const methods = [];
|
|
150
|
+
for (const [method, entries] of routeMap) {
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
if (matchSegments(entry.segments, requestSegments)) {
|
|
153
|
+
methods.push(method);
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return methods;
|
|
159
|
+
}
|
|
160
|
+
function validateRoutes(routeMap) {
|
|
161
|
+
for (const [method, entries] of routeMap) {
|
|
162
|
+
const seen = new Map;
|
|
163
|
+
for (const entry of entries) {
|
|
164
|
+
const normalized = entry.segments.map((s) => s.startsWith(":") ? ":param" : s).join("/");
|
|
165
|
+
const key = `${method} /${normalized}`;
|
|
166
|
+
const existing = seen.get(key);
|
|
167
|
+
if (existing) {
|
|
168
|
+
throw new Error(`Duplicate route: ${method} ${entry.pattern} conflicts with ${existing}`);
|
|
169
|
+
}
|
|
170
|
+
seen.set(key, entry.pattern);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function matchRawRoute(rawRoutes, httpMethod, pathname) {
|
|
175
|
+
for (const route of rawRoutes) {
|
|
176
|
+
if (route.method !== "ALL" && route.method !== httpMethod)
|
|
177
|
+
continue;
|
|
178
|
+
if (route.path.endsWith("/*")) {
|
|
179
|
+
const prefix = route.path.slice(0, -2);
|
|
180
|
+
if (pathname === prefix || pathname.startsWith(`${prefix}/`)) {
|
|
181
|
+
return { route, params: {} };
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (route.path.includes("/:")) {
|
|
186
|
+
const routeSegs = route.path.split("/").filter(Boolean);
|
|
187
|
+
const pathSegs = pathname.split("/").filter(Boolean);
|
|
188
|
+
const params = matchSegments(routeSegs, pathSegs);
|
|
189
|
+
if (params)
|
|
190
|
+
return { route, params };
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (route.path === pathname)
|
|
194
|
+
return { route, params: {} };
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
function staticRoute(prefix, dir) {
|
|
199
|
+
const cleanPrefix = prefix.replace(/\/+$/, "");
|
|
200
|
+
const cleanDir = dir.replace(/\/+$/, "");
|
|
201
|
+
return {
|
|
202
|
+
method: "GET",
|
|
203
|
+
path: `${cleanPrefix}/*`,
|
|
204
|
+
handler: async (req) => {
|
|
205
|
+
const pathname = new URL(req.url).pathname;
|
|
206
|
+
const rel = pathname.slice(cleanPrefix.length).replace(/^\/+/, "");
|
|
207
|
+
const root = resolve(cleanDir);
|
|
208
|
+
const target = resolve(root, rel);
|
|
209
|
+
if (target !== root && !target.startsWith(root + sep)) {
|
|
210
|
+
return new Response("Forbidden", { status: 403 });
|
|
211
|
+
}
|
|
212
|
+
const file = Bun.file(target);
|
|
213
|
+
if (!await file.exists()) {
|
|
214
|
+
return new Response("Not found", { status: 404 });
|
|
215
|
+
}
|
|
216
|
+
return new Response(file);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/server/context.ts
|
|
222
|
+
var RESERVED_KEYS = new Set([
|
|
223
|
+
"params",
|
|
224
|
+
"input",
|
|
225
|
+
"source",
|
|
226
|
+
"req",
|
|
227
|
+
"url",
|
|
228
|
+
"headers",
|
|
229
|
+
"traceId",
|
|
230
|
+
"spanId",
|
|
231
|
+
"ipAddress",
|
|
232
|
+
"userAgent"
|
|
233
|
+
]);
|
|
234
|
+
async function readJsonBody(req) {
|
|
235
|
+
const text = await req.text();
|
|
236
|
+
if (text.trim() === "")
|
|
237
|
+
return {};
|
|
238
|
+
try {
|
|
239
|
+
return JSON.parse(text);
|
|
240
|
+
} catch {
|
|
241
|
+
badRequest("Invalid JSON body");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async function buildContext(req, url, method, pathParams, traceId) {
|
|
245
|
+
const parsedParams = method.paramsSchema ? method.paramsSchema.parse(pathParams) : undefined;
|
|
246
|
+
let parsedInput;
|
|
247
|
+
let file;
|
|
248
|
+
if (method.multipart) {
|
|
249
|
+
const multipart = await parseMultipart(req, method.multipart, method.inputSchema);
|
|
250
|
+
parsedInput = multipart.fields;
|
|
251
|
+
file = multipart.file;
|
|
252
|
+
} else if (method.inputSchema) {
|
|
253
|
+
if (req.method === "GET") {
|
|
254
|
+
parsedInput = method.inputSchema.parse(parseQueryParams(url));
|
|
255
|
+
} else if (req.method === "DELETE") {
|
|
256
|
+
const ct = req.headers.get("content-type");
|
|
257
|
+
if (ct?.includes("application/json")) {
|
|
258
|
+
parsedInput = method.inputSchema.parse(await readJsonBody(req));
|
|
259
|
+
} else {
|
|
260
|
+
parsedInput = method.inputSchema.parse(parseQueryParams(url));
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
parsedInput = method.inputSchema.parse(await readJsonBody(req));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const safePathParams = {};
|
|
267
|
+
for (const [k, v] of Object.entries(pathParams)) {
|
|
268
|
+
if (!RESERVED_KEYS.has(k))
|
|
269
|
+
safePathParams[k] = v;
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
params: parsedParams,
|
|
273
|
+
input: parsedInput,
|
|
274
|
+
...file && { file },
|
|
275
|
+
source: "http",
|
|
276
|
+
req,
|
|
277
|
+
url,
|
|
278
|
+
headers: req.headers,
|
|
279
|
+
...safePathParams,
|
|
280
|
+
traceId,
|
|
281
|
+
...getClientInfo(req)
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function buildErrorContext(req, url, traceId) {
|
|
285
|
+
return {
|
|
286
|
+
params: undefined,
|
|
287
|
+
input: undefined,
|
|
288
|
+
source: "http",
|
|
289
|
+
req,
|
|
290
|
+
url,
|
|
291
|
+
headers: req.headers,
|
|
292
|
+
traceId,
|
|
293
|
+
...getClientInfo(req)
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/server/logger.ts
|
|
298
|
+
var c = {
|
|
299
|
+
reset: "\x1B[0m",
|
|
300
|
+
dim: "\x1B[2m",
|
|
301
|
+
red: "\x1B[31m",
|
|
302
|
+
green: "\x1B[32m",
|
|
303
|
+
yellow: "\x1B[33m",
|
|
304
|
+
blue: "\x1B[34m",
|
|
305
|
+
magenta: "\x1B[35m",
|
|
306
|
+
cyan: "\x1B[36m",
|
|
307
|
+
gray: "\x1B[90m"
|
|
308
|
+
};
|
|
309
|
+
var METHOD_COLOR = {
|
|
310
|
+
GET: c.green,
|
|
311
|
+
POST: c.yellow,
|
|
312
|
+
PUT: c.blue,
|
|
313
|
+
PATCH: c.cyan,
|
|
314
|
+
DELETE: c.red,
|
|
315
|
+
OPTIONS: c.gray
|
|
316
|
+
};
|
|
317
|
+
var isProd = false;
|
|
318
|
+
function timestamp() {
|
|
319
|
+
const now = new Date;
|
|
320
|
+
const t = now.toLocaleTimeString("en-US", {
|
|
321
|
+
hour12: false,
|
|
322
|
+
hour: "2-digit",
|
|
323
|
+
minute: "2-digit",
|
|
324
|
+
second: "2-digit"
|
|
325
|
+
});
|
|
326
|
+
return `${t}.${now.getMilliseconds().toString().padStart(3, "0")}`;
|
|
327
|
+
}
|
|
328
|
+
function elapsedMs(startTime) {
|
|
329
|
+
return Number(process.hrtime.bigint() - startTime) / 1e6;
|
|
330
|
+
}
|
|
331
|
+
function levelForStatus(status) {
|
|
332
|
+
if (status >= 500)
|
|
333
|
+
return "error";
|
|
334
|
+
if (status >= 400)
|
|
335
|
+
return "warn";
|
|
336
|
+
return "info";
|
|
337
|
+
}
|
|
338
|
+
function buildLogFields(method, path, status, durationMs, traceId) {
|
|
339
|
+
return { traceId, method, path, status, durationMs };
|
|
340
|
+
}
|
|
341
|
+
function formatMs(ms) {
|
|
342
|
+
if (ms >= 1000)
|
|
343
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
344
|
+
if (ms >= 1)
|
|
345
|
+
return `${Math.round(ms)}ms`;
|
|
346
|
+
return `${Math.round(ms * 1000)}µs`;
|
|
347
|
+
}
|
|
348
|
+
function durationColor(ms) {
|
|
349
|
+
if (ms >= 1000)
|
|
350
|
+
return c.red;
|
|
351
|
+
if (ms > 300)
|
|
352
|
+
return c.yellow;
|
|
353
|
+
return c.green;
|
|
354
|
+
}
|
|
355
|
+
function statusColor(status) {
|
|
356
|
+
if (status >= 500)
|
|
357
|
+
return c.red;
|
|
358
|
+
if (status >= 400)
|
|
359
|
+
return c.yellow;
|
|
360
|
+
if (status >= 300)
|
|
361
|
+
return c.cyan;
|
|
362
|
+
return c.green;
|
|
363
|
+
}
|
|
364
|
+
function ipLabel(ip) {
|
|
365
|
+
if (!ip || ip === "::1" || ip === "127.0.0.1")
|
|
366
|
+
return `${c.magenta}local${c.reset}`;
|
|
367
|
+
return `${c.dim}${ip}${c.reset}`;
|
|
368
|
+
}
|
|
369
|
+
var SKIP_PREFIXES = ["/_bun/", "/_bundle", "/favicon"];
|
|
370
|
+
function shouldLog(pathname, method) {
|
|
371
|
+
if (method === "OPTIONS")
|
|
372
|
+
return false;
|
|
373
|
+
return !SKIP_PREFIXES.some((prefix) => pathname.startsWith(prefix));
|
|
374
|
+
}
|
|
375
|
+
function logIncoming(req, pathname, traceId) {
|
|
376
|
+
const log = { traceId, startTime: process.hrtime.bigint() };
|
|
377
|
+
if (!isProd) {
|
|
378
|
+
const mc = METHOD_COLOR[req.method] ?? c.dim;
|
|
379
|
+
console.log(`${c.gray}[${timestamp()}]${c.reset} ${mc}${req.method}${c.reset} ${c.dim}${traceId}${c.reset} ${c.cyan}→${c.reset} ${pathname} ${ipLabel(extractIp(req))}`);
|
|
380
|
+
}
|
|
381
|
+
return log;
|
|
382
|
+
}
|
|
383
|
+
function logOutgoing(req, pathname, status, log) {
|
|
384
|
+
const ms = elapsedMs(log.startTime);
|
|
385
|
+
if (isProd) {
|
|
386
|
+
console.log(JSON.stringify({
|
|
387
|
+
ts: new Date().toISOString(),
|
|
388
|
+
level: levelForStatus(status),
|
|
389
|
+
msg: `${req.method} ${pathname} ${status}`,
|
|
390
|
+
...buildLogFields(req.method, pathname, status, Math.round(ms), log.traceId),
|
|
391
|
+
ip: extractIp(req) || undefined
|
|
392
|
+
}));
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const mc = METHOD_COLOR[req.method] ?? c.dim;
|
|
396
|
+
console.log(`${c.gray}[${timestamp()}]${c.reset} ${mc}${req.method}${c.reset} ${c.dim}${log.traceId}${c.reset} ${c.cyan}←${c.reset} ${pathname} ${statusColor(status)}${status}${c.reset} ${durationColor(ms)}${formatMs(ms)}${c.reset} ${ipLabel(extractIp(req))}`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// src/server/create.ts
|
|
400
|
+
function createHandler(config) {
|
|
401
|
+
const { cors, hooks, logging = false } = config;
|
|
402
|
+
const customLogger = typeof logging === "object" ? logging : null;
|
|
403
|
+
const useDefaultLog = logging === true;
|
|
404
|
+
const resolveId = config.traceId ?? resolveTraceId;
|
|
405
|
+
const routeMap = buildRouteMap(normalizeGroups(config));
|
|
406
|
+
validateRoutes(routeMap);
|
|
407
|
+
async function dispatch(req, url, traceId, server) {
|
|
408
|
+
const shouldLogRequest = logging && shouldLog(url.pathname, req.method);
|
|
409
|
+
let reqLog;
|
|
410
|
+
if (shouldLogRequest && useDefaultLog) {
|
|
411
|
+
reqLog = logIncoming(req, url.pathname, traceId);
|
|
412
|
+
}
|
|
413
|
+
if (shouldLogRequest && customLogger) {
|
|
414
|
+
customLogger.debug(`${req.method} ${url.pathname}`, {
|
|
415
|
+
traceId,
|
|
416
|
+
method: req.method,
|
|
417
|
+
path: url.pathname,
|
|
418
|
+
ip: extractIp(req) || undefined
|
|
419
|
+
});
|
|
420
|
+
reqLog = { traceId, startTime: process.hrtime.bigint() };
|
|
421
|
+
}
|
|
422
|
+
const logDone = (status) => {
|
|
423
|
+
if (!reqLog)
|
|
424
|
+
return;
|
|
425
|
+
if (useDefaultLog)
|
|
426
|
+
logOutgoing(req, url.pathname, status, reqLog);
|
|
427
|
+
if (customLogger) {
|
|
428
|
+
const durationMs = Math.round(elapsedMs(reqLog.startTime));
|
|
429
|
+
const level = levelForStatus(status);
|
|
430
|
+
customLogger[level](`${req.method} ${url.pathname} ${status} ${durationMs}ms`, buildLogFields(req.method, url.pathname, status, durationMs, reqLog.traceId));
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
const respondError = async (err, errCtx, endpoint) => {
|
|
434
|
+
if (hooks?.onError) {
|
|
435
|
+
try {
|
|
436
|
+
const response = await hooks.onError(errCtx ?? buildErrorContext(req, url, traceId), err, endpoint);
|
|
437
|
+
if (response instanceof Response) {
|
|
438
|
+
const withCors = applyCors(response, cors, req);
|
|
439
|
+
logDone(withCors.status);
|
|
440
|
+
return withCors;
|
|
441
|
+
}
|
|
442
|
+
} catch {}
|
|
443
|
+
}
|
|
444
|
+
const appErr = normalizeError(err);
|
|
445
|
+
logDone(appErr.status);
|
|
446
|
+
return json(appErr.toJSON(), appErr.status, cors, req);
|
|
447
|
+
};
|
|
448
|
+
if (cors && req.method === "OPTIONS") {
|
|
449
|
+
const res = corsPreflightResponse(cors, req);
|
|
450
|
+
logDone(204);
|
|
451
|
+
return res;
|
|
452
|
+
}
|
|
453
|
+
if (hooks?.onRequest) {
|
|
454
|
+
const earlyResponse = await hooks.onRequest(req);
|
|
455
|
+
if (earlyResponse instanceof Response) {
|
|
456
|
+
logDone(earlyResponse.status);
|
|
457
|
+
return earlyResponse;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (config.rawRoutes) {
|
|
461
|
+
const rawMatch = matchRawRoute(config.rawRoutes, req.method, url.pathname);
|
|
462
|
+
if (rawMatch) {
|
|
463
|
+
try {
|
|
464
|
+
const res = await rawMatch.route.handler(req, {
|
|
465
|
+
params: rawMatch.params,
|
|
466
|
+
server
|
|
467
|
+
});
|
|
468
|
+
const withCors = applyCors(res, cors, req);
|
|
469
|
+
logDone(withCors.status);
|
|
470
|
+
return withCors;
|
|
471
|
+
} catch (err) {
|
|
472
|
+
return respondError(err);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const match = matchRoute(routeMap, req.method, url.pathname);
|
|
477
|
+
if (!match) {
|
|
478
|
+
const allow = allowedMethods(routeMap, url.pathname);
|
|
479
|
+
if (allow.length > 0) {
|
|
480
|
+
const res = await respondError(new AppError("METHOD_NOT_ALLOWED", `Method ${req.method} not allowed`, 405));
|
|
481
|
+
res.headers.set("Allow", allow.join(", "));
|
|
482
|
+
return res;
|
|
483
|
+
}
|
|
484
|
+
return respondError(new AppError("NOT_FOUND", "Not found", 404));
|
|
485
|
+
}
|
|
486
|
+
const { method, pathParams, groupHooks } = match;
|
|
487
|
+
let ctx;
|
|
488
|
+
try {
|
|
489
|
+
ctx = await buildContext(req, url, method, pathParams, traceId);
|
|
490
|
+
if (hooks?.beforeHandle) {
|
|
491
|
+
await hooks.beforeHandle(ctx, method);
|
|
492
|
+
}
|
|
493
|
+
if (groupHooks?.beforeHandle) {
|
|
494
|
+
await groupHooks.beforeHandle(ctx, method);
|
|
495
|
+
}
|
|
496
|
+
let result = await method.handler(ctx);
|
|
497
|
+
if (groupHooks?.afterHandle) {
|
|
498
|
+
const transformed = await groupHooks.afterHandle(ctx, result, method);
|
|
499
|
+
if (transformed !== undefined)
|
|
500
|
+
result = transformed;
|
|
501
|
+
}
|
|
502
|
+
if (hooks?.afterHandle) {
|
|
503
|
+
const transformed = await hooks.afterHandle(ctx, result, method);
|
|
504
|
+
if (transformed !== undefined)
|
|
505
|
+
result = transformed;
|
|
506
|
+
}
|
|
507
|
+
if (method.outputSchema) {
|
|
508
|
+
result = method.outputSchema.parse(result);
|
|
509
|
+
}
|
|
510
|
+
if (result === undefined || result === null) {
|
|
511
|
+
logDone(204);
|
|
512
|
+
return new Response(null, { status: 204, headers: corsHeaders2(cors, req) });
|
|
513
|
+
}
|
|
514
|
+
logDone(200);
|
|
515
|
+
return json(result, 200, cors, req);
|
|
516
|
+
} catch (err) {
|
|
517
|
+
return respondError(err, ctx, method);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return async (req, server) => {
|
|
521
|
+
const url = new URL(req.url, "http://localhost");
|
|
522
|
+
const traceId = resolveId(req);
|
|
523
|
+
const response = await dispatch(req, url, traceId, server);
|
|
524
|
+
if (!response.headers.has("x-request-id")) {
|
|
525
|
+
response.headers.set("x-request-id", traceId);
|
|
526
|
+
}
|
|
527
|
+
return response;
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
function createServer(config) {
|
|
531
|
+
const { routes, websocket, development, bun: bunExtra, port = 3000, hostname } = config;
|
|
532
|
+
const fetch = createHandler(config);
|
|
533
|
+
return websocket ? Bun.serve({
|
|
534
|
+
...bunExtra,
|
|
535
|
+
...routes && { routes },
|
|
536
|
+
...development && { development },
|
|
537
|
+
port,
|
|
538
|
+
hostname,
|
|
539
|
+
websocket,
|
|
540
|
+
fetch
|
|
541
|
+
}) : Bun.serve({
|
|
542
|
+
...bunExtra,
|
|
543
|
+
...development && { development },
|
|
544
|
+
port,
|
|
545
|
+
hostname,
|
|
546
|
+
fetch
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
function normalizeGroups(config) {
|
|
550
|
+
const result = [];
|
|
551
|
+
if (config.services) {
|
|
552
|
+
for (const service of config.services) {
|
|
553
|
+
result.push({ prefix: "", service });
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (config.groups) {
|
|
557
|
+
for (const group of config.groups) {
|
|
558
|
+
for (const service of group.services) {
|
|
559
|
+
result.push({ prefix: group.pathPrefix ?? "", service, hooks: group.hooks });
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return result;
|
|
564
|
+
}
|
|
565
|
+
function corsHeaders2(cors, req) {
|
|
566
|
+
if (!cors)
|
|
567
|
+
return {};
|
|
568
|
+
return corsHeaders(cors, req.headers.get("origin"));
|
|
569
|
+
}
|
|
570
|
+
function applyCors(res, cors, req) {
|
|
571
|
+
const extra = corsHeaders2(cors, req);
|
|
572
|
+
if (Object.keys(extra).length === 0)
|
|
573
|
+
return res;
|
|
574
|
+
const headers = new Headers(res.headers);
|
|
575
|
+
for (const [key, value] of Object.entries(extra))
|
|
576
|
+
headers.set(key, value);
|
|
577
|
+
return new Response(res.body, {
|
|
578
|
+
status: res.status,
|
|
579
|
+
statusText: res.statusText,
|
|
580
|
+
headers
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
function json(data, status, cors, req) {
|
|
584
|
+
return Response.json(data, { status, headers: corsHeaders2(cors, req) });
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export { parseMultipart, corsHeaders, corsPreflightResponse, staticRoute, createHandler, createServer };
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import {
|
|
8
8
|
parseSSE
|
|
9
9
|
} from "./index-n7bmdwmz.js";
|
|
10
|
+
import"./index-809wc1tt.js";
|
|
10
11
|
import {
|
|
11
12
|
ALL_TRANSPORTS,
|
|
12
13
|
AppError,
|
|
@@ -19,8 +20,7 @@ import {
|
|
|
19
20
|
paginatedSchema,
|
|
20
21
|
rateLimited,
|
|
21
22
|
unauthorized
|
|
22
|
-
} from "./index-
|
|
23
|
-
import"./index-809wc1tt.js";
|
|
23
|
+
} from "./index-kckky6zw.js";
|
|
24
24
|
// src/browser/socket-io.ts
|
|
25
25
|
import { io } from "socket.io-client";
|
|
26
26
|
function createSocketIOClient(config) {
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AACxF,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createHandler
|
|
3
|
+
} from "./index-v2z2v3mq.js";
|
|
4
|
+
import"./index-a35v22fh.js";
|
|
5
|
+
import"./index-kckky6zw.js";
|
|
6
|
+
import"./index-ke4mx4ea.js";
|
|
7
|
+
// src/server/node.ts
|
|
8
|
+
import { serve } from "srvx";
|
|
9
|
+
async function serveNode(config) {
|
|
10
|
+
const { port = 3000, hostname, ...handlerConfig } = config;
|
|
11
|
+
const handler = createHandler(handlerConfig);
|
|
12
|
+
const server = serve({ port, hostname, fetch: handler });
|
|
13
|
+
await server.ready();
|
|
14
|
+
const listenUrl = server.url ?? `http://${hostname ?? "localhost"}:${port}`;
|
|
15
|
+
const resolvedPort = Number(new URL(listenUrl).port) || port;
|
|
16
|
+
const resolvedHost = hostname ?? "localhost";
|
|
17
|
+
return {
|
|
18
|
+
url: `http://${resolvedHost}:${resolvedPort}`,
|
|
19
|
+
port: resolvedPort,
|
|
20
|
+
close: (closeActive) => server.close(closeActive)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
serveNode,
|
|
25
|
+
createHandler
|
|
26
|
+
};
|
package/dist/server/create.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function createHandler(config:
|
|
3
|
-
export declare function createServer(config:
|
|
1
|
+
import type { BunServerConfig, HandlerConfig } from './types';
|
|
2
|
+
export declare function createHandler(config: HandlerConfig): (req: Request) => Promise<Response>;
|
|
3
|
+
export declare function createServer(config: BunServerConfig): Bun.Server<unknown>;
|
|
4
4
|
//# sourceMappingURL=create.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/server/create.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/server/create.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAEV,eAAe,EACf,aAAa,EAGd,MAAM,SAAS,CAAC;AAEjB,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAiLxF;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,uBAsBnD"}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -13,5 +13,5 @@ export { staticRoute } from './router';
|
|
|
13
13
|
export type { SocketIOServerConfig, SocketIOServerHandle } from './socket-io';
|
|
14
14
|
export { createSocketIOServer } from './socket-io';
|
|
15
15
|
export { type ParseSSEOptions, parseSSE, streamSSE } from './stream';
|
|
16
|
-
export type { BunServer, Handlers, LifecycleHooks, MethodDef, RawRoute, RawRouteContext, RouteGroup,
|
|
16
|
+
export type { BunServer, BunServerConfig, HandlerConfig, Handlers, LifecycleHooks, MethodDef, RawRoute, RawRouteContext, RouteGroup, ServerPassthrough, ServiceDef, StitchLogger, } from './types';
|
|
17
17
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,WAAW,EACX,YAAY,GACb,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,oBAAoB,EACzB,cAAc,EACd,oBAAoB,EACpB,YAAY,EACZ,KAAK,UAAU,EACf,SAAS,GACV,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,YAAY,EACZ,YAAY,EACZ,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,KAAK,UAAU,EACf,WAAW,EACX,qBAAqB,GACtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,iBAAiB,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EACL,SAAS,EACT,eAAe,EACf,aAAa,EACb,cAAc,GACf,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,YAAY,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,KAAK,eAAe,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrE,YAAY,EACV,SAAS,EACT,QAAQ,EACR,cAAc,EACd,SAAS,EACT,QAAQ,EACR,eAAe,EACf,UAAU,EACV,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,WAAW,EACX,YAAY,GACb,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,oBAAoB,EACzB,cAAc,EACd,oBAAoB,EACpB,YAAY,EACZ,KAAK,UAAU,EACf,SAAS,GACV,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,YAAY,EACZ,YAAY,EACZ,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,KAAK,UAAU,EACf,WAAW,EACX,qBAAqB,GACtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,iBAAiB,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EACL,SAAS,EACT,eAAe,EACf,aAAa,EACb,cAAc,GACf,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,YAAY,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,KAAK,eAAe,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrE,YAAY,EACV,SAAS,EACT,eAAe,EACf,aAAa,EACb,QAAQ,EACR,cAAc,EACd,SAAS,EACT,QAAQ,EACR,eAAe,EACf,UAAU,EACV,iBAAiB,EACjB,UAAU,EACV,YAAY,GACb,MAAM,SAAS,CAAC"}
|