stitchkit 0.0.1 → 0.1.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.
Files changed (113) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +373 -4
  3. package/dist/browser/client.d.ts +30 -0
  4. package/dist/browser/client.d.ts.map +1 -0
  5. package/dist/browser/http.d.ts +77 -0
  6. package/dist/browser/http.d.ts.map +1 -0
  7. package/dist/browser/socket-io.d.ts +69 -0
  8. package/dist/browser/socket-io.d.ts.map +1 -0
  9. package/dist/browser/stream.d.ts +2 -0
  10. package/dist/browser/stream.d.ts.map +1 -0
  11. package/dist/contract/define.d.ts +94 -0
  12. package/dist/contract/define.d.ts.map +1 -0
  13. package/dist/contract/errors.d.ts +46 -0
  14. package/dist/contract/errors.d.ts.map +1 -0
  15. package/dist/contract/index.d.ts +4 -0
  16. package/dist/contract/index.d.ts.map +1 -0
  17. package/dist/contract/index.js +26 -0
  18. package/dist/contract/pagination.d.ts +19 -0
  19. package/dist/contract/pagination.d.ts.map +1 -0
  20. package/dist/index-5sxnvwb1.js +368 -0
  21. package/dist/index-7wfkbvss.js +92 -0
  22. package/dist/index-809wc1tt.js +18 -0
  23. package/dist/index-gfzn1n4n.js +29 -0
  24. package/dist/index-ke4mx4ea.js +38 -0
  25. package/dist/index-n7bmdwmz.js +68 -0
  26. package/dist/index.d.ts +7 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +111 -0
  29. package/dist/internal/errors.d.ts +5 -0
  30. package/dist/internal/errors.d.ts.map +1 -0
  31. package/dist/internal/typed.d.ts +9 -0
  32. package/dist/internal/typed.d.ts.map +1 -0
  33. package/dist/observability/audit.d.ts +32 -0
  34. package/dist/observability/audit.d.ts.map +1 -0
  35. package/dist/observability/context.d.ts +60 -0
  36. package/dist/observability/context.d.ts.map +1 -0
  37. package/dist/observability/event.d.ts +49 -0
  38. package/dist/observability/event.d.ts.map +1 -0
  39. package/dist/observability/index.d.ts +14 -0
  40. package/dist/observability/index.d.ts.map +1 -0
  41. package/dist/observability/index.js +266 -0
  42. package/dist/observability/sanitize.d.ts +42 -0
  43. package/dist/observability/sanitize.d.ts.map +1 -0
  44. package/dist/observability/trace.d.ts +42 -0
  45. package/dist/observability/trace.d.ts.map +1 -0
  46. package/dist/react/cache-bridge.d.ts +53 -0
  47. package/dist/react/cache-bridge.d.ts.map +1 -0
  48. package/dist/react/cursor-query.d.ts +37 -0
  49. package/dist/react/cursor-query.d.ts.map +1 -0
  50. package/dist/react.d.ts +3 -0
  51. package/dist/react.d.ts.map +1 -0
  52. package/dist/react.js +52 -0
  53. package/dist/server/cache.d.ts +15 -0
  54. package/dist/server/cache.d.ts.map +1 -0
  55. package/dist/server/context.d.ts +9 -0
  56. package/dist/server/context.d.ts.map +1 -0
  57. package/dist/server/create.d.ts +4 -0
  58. package/dist/server/create.d.ts.map +1 -0
  59. package/dist/server/event-bus.d.ts +24 -0
  60. package/dist/server/event-bus.d.ts.map +1 -0
  61. package/dist/server/implement.d.ts +16 -0
  62. package/dist/server/implement.d.ts.map +1 -0
  63. package/dist/server/index.d.ts +17 -0
  64. package/dist/server/index.d.ts.map +1 -0
  65. package/dist/server/index.js +965 -0
  66. package/dist/server/logger.d.ts +21 -0
  67. package/dist/server/logger.d.ts.map +1 -0
  68. package/dist/server/middleware/auth.d.ts +50 -0
  69. package/dist/server/middleware/auth.d.ts.map +1 -0
  70. package/dist/server/middleware/cookies.d.ts +34 -0
  71. package/dist/server/middleware/cookies.d.ts.map +1 -0
  72. package/dist/server/middleware/cors.d.ts +9 -0
  73. package/dist/server/middleware/cors.d.ts.map +1 -0
  74. package/dist/server/multipart.d.ts +14 -0
  75. package/dist/server/multipart.d.ts.map +1 -0
  76. package/dist/server/rate-limit.d.ts +17 -0
  77. package/dist/server/rate-limit.d.ts.map +1 -0
  78. package/dist/server/request.d.ts +22 -0
  79. package/dist/server/request.d.ts.map +1 -0
  80. package/dist/server/router.d.ts +42 -0
  81. package/dist/server/router.d.ts.map +1 -0
  82. package/dist/server/socket-io.d.ts +42 -0
  83. package/dist/server/socket-io.d.ts.map +1 -0
  84. package/dist/server/stream.d.ts +18 -0
  85. package/dist/server/stream.d.ts.map +1 -0
  86. package/dist/server/swept-map.d.ts +22 -0
  87. package/dist/server/swept-map.d.ts.map +1 -0
  88. package/dist/server/types.d.ts +124 -0
  89. package/dist/server/types.d.ts.map +1 -0
  90. package/dist/tools/agent.d.ts +14 -0
  91. package/dist/tools/agent.d.ts.map +1 -0
  92. package/dist/tools/execute.d.ts +21 -0
  93. package/dist/tools/execute.d.ts.map +1 -0
  94. package/dist/tools/mcp-handler.d.ts +21 -0
  95. package/dist/tools/mcp-handler.d.ts.map +1 -0
  96. package/dist/tools/mcp-stdio.d.ts +25 -0
  97. package/dist/tools/mcp-stdio.d.ts.map +1 -0
  98. package/dist/tools/mcp.d.ts +42 -0
  99. package/dist/tools/mcp.d.ts.map +1 -0
  100. package/dist/tools/mount.d.ts +61 -0
  101. package/dist/tools/mount.d.ts.map +1 -0
  102. package/dist/tools/names.d.ts +2 -0
  103. package/dist/tools/names.d.ts.map +1 -0
  104. package/dist/tools/remote.d.ts +22 -0
  105. package/dist/tools/remote.d.ts.map +1 -0
  106. package/dist/tools/schema.d.ts +4 -0
  107. package/dist/tools/schema.d.ts.map +1 -0
  108. package/dist/tools/view-file.d.ts +57 -0
  109. package/dist/tools/view-file.d.ts.map +1 -0
  110. package/dist/tools.d.ts +8 -0
  111. package/dist/tools.d.ts.map +1 -0
  112. package/dist/tools.js +654 -0
  113. package/package.json +136 -4
@@ -0,0 +1,965 @@
1
+ import {
2
+ parseSSE,
3
+ streamSSE
4
+ } from "../index-n7bmdwmz.js";
5
+ import {
6
+ normalizeError
7
+ } from "../index-gfzn1n4n.js";
8
+ import {
9
+ AppError,
10
+ appError,
11
+ badRequest,
12
+ conflict,
13
+ forbidden,
14
+ notFound,
15
+ rateLimited,
16
+ unauthorized
17
+ } from "../index-7wfkbvss.js";
18
+ import {
19
+ extractIp,
20
+ generateTraceId,
21
+ getClientInfo,
22
+ parseQueryParams,
23
+ resolveTraceId
24
+ } from "../index-ke4mx4ea.js";
25
+ // src/server/swept-map.ts
26
+ function createSweptMap(options) {
27
+ const store = new Map;
28
+ const timer = setInterval(() => {
29
+ const now = Date.now();
30
+ for (const [key, value] of store) {
31
+ if (options.isExpired(value, now))
32
+ store.delete(key);
33
+ }
34
+ }, options.intervalMs);
35
+ timer.unref();
36
+ return {
37
+ store,
38
+ destroy() {
39
+ clearInterval(timer);
40
+ store.clear();
41
+ }
42
+ };
43
+ }
44
+
45
+ // src/server/cache.ts
46
+ function createCache() {
47
+ const { store, destroy } = createSweptMap({
48
+ intervalMs: 30000,
49
+ isExpired: (entry, now) => entry.expiresAt < now
50
+ });
51
+ return {
52
+ destroy,
53
+ get(key) {
54
+ const entry = store.get(key);
55
+ if (!entry)
56
+ return null;
57
+ if (entry.expiresAt < Date.now()) {
58
+ store.delete(key);
59
+ return null;
60
+ }
61
+ return entry.data;
62
+ },
63
+ set(key, data, maxAgeSeconds) {
64
+ store.set(key, { data, expiresAt: Date.now() + maxAgeSeconds * 1000 });
65
+ },
66
+ invalidate(prefix) {
67
+ for (const key of store.keys()) {
68
+ if (key.startsWith(prefix))
69
+ store.delete(key);
70
+ }
71
+ },
72
+ clear() {
73
+ store.clear();
74
+ }
75
+ };
76
+ }
77
+ function cacheHeaders(maxAge, scope = "public") {
78
+ return {
79
+ "Cache-Control": `${scope}, max-age=${maxAge}`
80
+ };
81
+ }
82
+ // src/server/multipart.ts
83
+ var DEFAULT_MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
84
+ async function parseMultipart(req, fileField, fieldsSchema, maxBytes = DEFAULT_MAX_UPLOAD_BYTES) {
85
+ const declared = Number(req.headers.get("content-length") ?? 0);
86
+ if (declared > maxBytes) {
87
+ badRequest(`Upload exceeds the ${Math.round(maxBytes / 1024 / 1024)} MB limit`);
88
+ }
89
+ const formData = await req.formData();
90
+ const file = formData.get(fileField);
91
+ if (!file || !(file instanceof File)) {
92
+ badRequest(`Missing file field: ${fileField}`);
93
+ }
94
+ if (file.size > maxBytes) {
95
+ badRequest(`Upload exceeds the ${Math.round(maxBytes / 1024 / 1024)} MB limit`);
96
+ }
97
+ const fields = {};
98
+ for (const [key, value] of formData.entries()) {
99
+ if (key === fileField)
100
+ continue;
101
+ if (typeof value === "string") {
102
+ try {
103
+ fields[key] = JSON.parse(value);
104
+ } catch {
105
+ fields[key] = value;
106
+ }
107
+ }
108
+ }
109
+ const parsed = fieldsSchema ? fieldsSchema.parse(fields) : fields;
110
+ return { file, fields: parsed };
111
+ }
112
+
113
+ // src/server/context.ts
114
+ var RESERVED_KEYS = new Set([
115
+ "params",
116
+ "input",
117
+ "source",
118
+ "req",
119
+ "url",
120
+ "headers",
121
+ "traceId",
122
+ "spanId",
123
+ "ipAddress",
124
+ "userAgent"
125
+ ]);
126
+ async function readJsonBody(req) {
127
+ const text = await req.text();
128
+ if (text.trim() === "")
129
+ return {};
130
+ try {
131
+ return JSON.parse(text);
132
+ } catch {
133
+ badRequest("Invalid JSON body");
134
+ }
135
+ }
136
+ async function buildContext(req, url, method, pathParams, traceId) {
137
+ const parsedParams = method.paramsSchema ? method.paramsSchema.parse(pathParams) : undefined;
138
+ let parsedInput;
139
+ let file;
140
+ if (method.multipart) {
141
+ const multipart = await parseMultipart(req, method.multipart, method.inputSchema);
142
+ parsedInput = multipart.fields;
143
+ file = multipart.file;
144
+ } else if (method.inputSchema) {
145
+ if (req.method === "GET") {
146
+ parsedInput = method.inputSchema.parse(parseQueryParams(url));
147
+ } else if (req.method === "DELETE") {
148
+ const ct = req.headers.get("content-type");
149
+ if (ct?.includes("application/json")) {
150
+ parsedInput = method.inputSchema.parse(await readJsonBody(req));
151
+ } else {
152
+ parsedInput = method.inputSchema.parse(parseQueryParams(url));
153
+ }
154
+ } else {
155
+ parsedInput = method.inputSchema.parse(await readJsonBody(req));
156
+ }
157
+ }
158
+ const safePathParams = {};
159
+ for (const [k, v] of Object.entries(pathParams)) {
160
+ if (!RESERVED_KEYS.has(k))
161
+ safePathParams[k] = v;
162
+ }
163
+ return {
164
+ params: parsedParams,
165
+ input: parsedInput,
166
+ ...file && { file },
167
+ source: "http",
168
+ req,
169
+ url,
170
+ headers: req.headers,
171
+ ...safePathParams,
172
+ traceId,
173
+ ...getClientInfo(req)
174
+ };
175
+ }
176
+ function buildErrorContext(req, url, traceId) {
177
+ return {
178
+ params: undefined,
179
+ input: undefined,
180
+ source: "http",
181
+ req,
182
+ url,
183
+ headers: req.headers,
184
+ traceId,
185
+ ...getClientInfo(req)
186
+ };
187
+ }
188
+
189
+ // src/server/logger.ts
190
+ var c = {
191
+ reset: "\x1B[0m",
192
+ dim: "\x1B[2m",
193
+ red: "\x1B[31m",
194
+ green: "\x1B[32m",
195
+ yellow: "\x1B[33m",
196
+ blue: "\x1B[34m",
197
+ magenta: "\x1B[35m",
198
+ cyan: "\x1B[36m",
199
+ gray: "\x1B[90m"
200
+ };
201
+ var METHOD_COLOR = {
202
+ GET: c.green,
203
+ POST: c.yellow,
204
+ PUT: c.blue,
205
+ PATCH: c.cyan,
206
+ DELETE: c.red,
207
+ OPTIONS: c.gray
208
+ };
209
+ var isProd = false;
210
+ function timestamp() {
211
+ const now = new Date;
212
+ const t = now.toLocaleTimeString("en-US", {
213
+ hour12: false,
214
+ hour: "2-digit",
215
+ minute: "2-digit",
216
+ second: "2-digit"
217
+ });
218
+ return `${t}.${now.getMilliseconds().toString().padStart(3, "0")}`;
219
+ }
220
+ function elapsedMs(startTime) {
221
+ return Number(process.hrtime.bigint() - startTime) / 1e6;
222
+ }
223
+ function levelForStatus(status) {
224
+ if (status >= 500)
225
+ return "error";
226
+ if (status >= 400)
227
+ return "warn";
228
+ return "info";
229
+ }
230
+ function buildLogFields(method, path, status, durationMs, traceId) {
231
+ return { traceId, method, path, status, durationMs };
232
+ }
233
+ function formatMs(ms) {
234
+ if (ms >= 1000)
235
+ return `${(ms / 1000).toFixed(2)}s`;
236
+ if (ms >= 1)
237
+ return `${Math.round(ms)}ms`;
238
+ return `${Math.round(ms * 1000)}µs`;
239
+ }
240
+ function durationColor(ms) {
241
+ if (ms >= 1000)
242
+ return c.red;
243
+ if (ms > 300)
244
+ return c.yellow;
245
+ return c.green;
246
+ }
247
+ function statusColor(status) {
248
+ if (status >= 500)
249
+ return c.red;
250
+ if (status >= 400)
251
+ return c.yellow;
252
+ if (status >= 300)
253
+ return c.cyan;
254
+ return c.green;
255
+ }
256
+ function ipLabel(ip) {
257
+ if (!ip || ip === "::1" || ip === "127.0.0.1")
258
+ return `${c.magenta}local${c.reset}`;
259
+ return `${c.dim}${ip}${c.reset}`;
260
+ }
261
+ var SKIP_PREFIXES = ["/_bun/", "/_bundle", "/favicon"];
262
+ function shouldLog(pathname, method) {
263
+ if (method === "OPTIONS")
264
+ return false;
265
+ return !SKIP_PREFIXES.some((prefix) => pathname.startsWith(prefix));
266
+ }
267
+ function logIncoming(req, pathname, traceId) {
268
+ const log = { traceId, startTime: process.hrtime.bigint() };
269
+ if (!isProd) {
270
+ const mc = METHOD_COLOR[req.method] ?? c.dim;
271
+ 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))}`);
272
+ }
273
+ return log;
274
+ }
275
+ function logOutgoing(req, pathname, status, log) {
276
+ const ms = elapsedMs(log.startTime);
277
+ if (isProd) {
278
+ console.log(JSON.stringify({
279
+ ts: new Date().toISOString(),
280
+ level: levelForStatus(status),
281
+ msg: `${req.method} ${pathname} ${status}`,
282
+ ...buildLogFields(req.method, pathname, status, Math.round(ms), log.traceId),
283
+ ip: extractIp(req) || undefined
284
+ }));
285
+ return;
286
+ }
287
+ const mc = METHOD_COLOR[req.method] ?? c.dim;
288
+ 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))}`);
289
+ }
290
+
291
+ // src/server/middleware/cors.ts
292
+ function corsHeaders(config, requestOrigin) {
293
+ let allowOrigin;
294
+ if (config.origin === undefined || config.origin === "*") {
295
+ allowOrigin = config.credentials ? requestOrigin ?? undefined : "*";
296
+ } else if (Array.isArray(config.origin)) {
297
+ allowOrigin = requestOrigin && config.origin.includes(requestOrigin) ? requestOrigin : undefined;
298
+ } else {
299
+ allowOrigin = config.origin;
300
+ }
301
+ const headers = {
302
+ "Access-Control-Allow-Methods": config.methods ?? "GET, POST, PUT, PATCH, DELETE, OPTIONS",
303
+ "Access-Control-Allow-Headers": config.headers ?? "Content-Type, Authorization, X-Trace-Id"
304
+ };
305
+ if (allowOrigin !== undefined) {
306
+ headers["Access-Control-Allow-Origin"] = allowOrigin;
307
+ }
308
+ if (config.credentials) {
309
+ headers["Access-Control-Allow-Credentials"] = "true";
310
+ }
311
+ const variesByOrigin = Array.isArray(config.origin) || (config.origin === undefined || config.origin === "*") && Boolean(config.credentials);
312
+ if (variesByOrigin) {
313
+ headers.Vary = "Origin";
314
+ }
315
+ return headers;
316
+ }
317
+ function corsPreflightResponse(config, req) {
318
+ return new Response(null, {
319
+ status: 204,
320
+ headers: corsHeaders(config, req.headers.get("origin"))
321
+ });
322
+ }
323
+
324
+ // src/server/router.ts
325
+ import { resolve, sep } from "node:path";
326
+ function joinPath(...parts) {
327
+ const joined = parts.filter(Boolean).map((part) => part.replace(/^\/+|\/+$/g, "")).filter(Boolean).join("/");
328
+ return `/${joined}`;
329
+ }
330
+ function matchSegments(patternSegments, requestSegments) {
331
+ if (patternSegments.length !== requestSegments.length)
332
+ return null;
333
+ const params = {};
334
+ for (const [i, pattern] of patternSegments.entries()) {
335
+ const actual = requestSegments[i];
336
+ if (actual === undefined)
337
+ return null;
338
+ if (pattern.startsWith(":")) {
339
+ params[pattern.slice(1)] = decodeURIComponent(actual);
340
+ } else if (pattern !== actual) {
341
+ return null;
342
+ }
343
+ }
344
+ return params;
345
+ }
346
+ function buildRouteMap(groups) {
347
+ const map = new Map;
348
+ for (const { prefix, service, hooks } of groups) {
349
+ for (const [, method] of Object.entries(service.methods)) {
350
+ if (method.expose && !method.expose.includes("HTTP"))
351
+ continue;
352
+ const servicePath = joinPath("/", service.prefix, method.path === "/" ? "" : method.path);
353
+ const fullPath = prefix ? joinPath(prefix, servicePath) : servicePath;
354
+ const segments = fullPath.split("/").filter(Boolean);
355
+ const entries = map.get(method.method) ?? [];
356
+ entries.push({ method, service, pattern: fullPath, segments, groupHooks: hooks });
357
+ map.set(method.method, entries);
358
+ }
359
+ }
360
+ for (const [, entries] of map) {
361
+ entries.sort((a, b) => {
362
+ const len = Math.min(a.segments.length, b.segments.length);
363
+ for (let i = 0;i < len; i++) {
364
+ const aIsParam = a.segments[i]?.startsWith(":");
365
+ const bIsParam = b.segments[i]?.startsWith(":");
366
+ if (aIsParam !== bIsParam)
367
+ return aIsParam ? 1 : -1;
368
+ }
369
+ return a.segments.length - b.segments.length;
370
+ });
371
+ }
372
+ return map;
373
+ }
374
+ function matchRoute(routeMap, httpMethod, pathname) {
375
+ const entries = routeMap.get(httpMethod);
376
+ if (!entries)
377
+ return null;
378
+ const requestSegments = pathname.split("/").filter(Boolean);
379
+ for (const entry of entries) {
380
+ const pathParams = matchSegments(entry.segments, requestSegments);
381
+ if (pathParams) {
382
+ return {
383
+ method: entry.method,
384
+ service: entry.service,
385
+ pathParams,
386
+ groupHooks: entry.groupHooks
387
+ };
388
+ }
389
+ }
390
+ return null;
391
+ }
392
+ function allowedMethods(routeMap, pathname) {
393
+ const requestSegments = pathname.split("/").filter(Boolean);
394
+ const methods = [];
395
+ for (const [method, entries] of routeMap) {
396
+ for (const entry of entries) {
397
+ if (matchSegments(entry.segments, requestSegments)) {
398
+ methods.push(method);
399
+ break;
400
+ }
401
+ }
402
+ }
403
+ return methods;
404
+ }
405
+ function validateRoutes(routeMap) {
406
+ for (const [method, entries] of routeMap) {
407
+ const seen = new Map;
408
+ for (const entry of entries) {
409
+ const normalized = entry.segments.map((s) => s.startsWith(":") ? ":param" : s).join("/");
410
+ const key = `${method} /${normalized}`;
411
+ const existing = seen.get(key);
412
+ if (existing) {
413
+ throw new Error(`Duplicate route: ${method} ${entry.pattern} conflicts with ${existing}`);
414
+ }
415
+ seen.set(key, entry.pattern);
416
+ }
417
+ }
418
+ }
419
+ function matchRawRoute(rawRoutes, httpMethod, pathname) {
420
+ for (const route of rawRoutes) {
421
+ if (route.method !== "ALL" && route.method !== httpMethod)
422
+ continue;
423
+ if (route.path.endsWith("/*")) {
424
+ const prefix = route.path.slice(0, -2);
425
+ if (pathname === prefix || pathname.startsWith(`${prefix}/`)) {
426
+ return { route, params: {} };
427
+ }
428
+ continue;
429
+ }
430
+ if (route.path.includes("/:")) {
431
+ const routeSegs = route.path.split("/").filter(Boolean);
432
+ const pathSegs = pathname.split("/").filter(Boolean);
433
+ const params = matchSegments(routeSegs, pathSegs);
434
+ if (params)
435
+ return { route, params };
436
+ continue;
437
+ }
438
+ if (route.path === pathname)
439
+ return { route, params: {} };
440
+ }
441
+ return null;
442
+ }
443
+ function staticRoute(prefix, dir) {
444
+ const cleanPrefix = prefix.replace(/\/+$/, "");
445
+ const cleanDir = dir.replace(/\/+$/, "");
446
+ return {
447
+ method: "GET",
448
+ path: `${cleanPrefix}/*`,
449
+ handler: async (req) => {
450
+ const pathname = new URL(req.url).pathname;
451
+ const rel = pathname.slice(cleanPrefix.length).replace(/^\/+/, "");
452
+ const root = resolve(cleanDir);
453
+ const target = resolve(root, rel);
454
+ if (target !== root && !target.startsWith(root + sep)) {
455
+ return new Response("Forbidden", { status: 403 });
456
+ }
457
+ const file = Bun.file(target);
458
+ if (!await file.exists()) {
459
+ return new Response("Not found", { status: 404 });
460
+ }
461
+ return new Response(file);
462
+ }
463
+ };
464
+ }
465
+
466
+ // src/server/create.ts
467
+ function createHandler(config) {
468
+ const { cors, hooks, logging = false } = config;
469
+ const customLogger = typeof logging === "object" ? logging : null;
470
+ const useDefaultLog = logging === true;
471
+ const resolveId = config.traceId ?? resolveTraceId;
472
+ const routeMap = buildRouteMap(normalizeGroups(config));
473
+ validateRoutes(routeMap);
474
+ async function dispatch(req, url, traceId, server) {
475
+ const shouldLogRequest = logging && shouldLog(url.pathname, req.method);
476
+ let reqLog;
477
+ if (shouldLogRequest && useDefaultLog) {
478
+ reqLog = logIncoming(req, url.pathname, traceId);
479
+ }
480
+ if (shouldLogRequest && customLogger) {
481
+ customLogger.debug(`${req.method} ${url.pathname}`, {
482
+ traceId,
483
+ method: req.method,
484
+ path: url.pathname,
485
+ ip: extractIp(req) || undefined
486
+ });
487
+ reqLog = { traceId, startTime: process.hrtime.bigint() };
488
+ }
489
+ const logDone = (status) => {
490
+ if (!reqLog)
491
+ return;
492
+ if (useDefaultLog)
493
+ logOutgoing(req, url.pathname, status, reqLog);
494
+ if (customLogger) {
495
+ const durationMs = Math.round(elapsedMs(reqLog.startTime));
496
+ const level = levelForStatus(status);
497
+ customLogger[level](`${req.method} ${url.pathname} ${status} ${durationMs}ms`, buildLogFields(req.method, url.pathname, status, durationMs, reqLog.traceId));
498
+ }
499
+ };
500
+ const respondError = async (err, errCtx, endpoint) => {
501
+ if (hooks?.onError) {
502
+ try {
503
+ const response = await hooks.onError(errCtx ?? buildErrorContext(req, url, traceId), err, endpoint);
504
+ if (response instanceof Response) {
505
+ const withCors = applyCors(response, cors, req);
506
+ logDone(withCors.status);
507
+ return withCors;
508
+ }
509
+ } catch {}
510
+ }
511
+ const appErr = normalizeError(err);
512
+ logDone(appErr.status);
513
+ return json(appErr.toJSON(), appErr.status, cors, req);
514
+ };
515
+ if (cors && req.method === "OPTIONS") {
516
+ const res = corsPreflightResponse(cors, req);
517
+ logDone(204);
518
+ return res;
519
+ }
520
+ if (hooks?.onRequest) {
521
+ const earlyResponse = await hooks.onRequest(req);
522
+ if (earlyResponse instanceof Response) {
523
+ logDone(earlyResponse.status);
524
+ return earlyResponse;
525
+ }
526
+ }
527
+ if (config.rawRoutes) {
528
+ const rawMatch = matchRawRoute(config.rawRoutes, req.method, url.pathname);
529
+ if (rawMatch) {
530
+ try {
531
+ const res = await rawMatch.route.handler(req, {
532
+ params: rawMatch.params,
533
+ server
534
+ });
535
+ const withCors = applyCors(res, cors, req);
536
+ logDone(withCors.status);
537
+ return withCors;
538
+ } catch (err) {
539
+ return respondError(err);
540
+ }
541
+ }
542
+ }
543
+ const match = matchRoute(routeMap, req.method, url.pathname);
544
+ if (!match) {
545
+ const allow = allowedMethods(routeMap, url.pathname);
546
+ if (allow.length > 0) {
547
+ const res = await respondError(new AppError("METHOD_NOT_ALLOWED", `Method ${req.method} not allowed`, 405));
548
+ res.headers.set("Allow", allow.join(", "));
549
+ return res;
550
+ }
551
+ return respondError(new AppError("NOT_FOUND", "Not found", 404));
552
+ }
553
+ const { method, pathParams, groupHooks } = match;
554
+ let ctx;
555
+ try {
556
+ ctx = await buildContext(req, url, method, pathParams, traceId);
557
+ if (hooks?.beforeHandle) {
558
+ await hooks.beforeHandle(ctx, method);
559
+ }
560
+ if (groupHooks?.beforeHandle) {
561
+ await groupHooks.beforeHandle(ctx, method);
562
+ }
563
+ let result = await method.handler(ctx);
564
+ if (groupHooks?.afterHandle) {
565
+ const transformed = await groupHooks.afterHandle(ctx, result, method);
566
+ if (transformed !== undefined)
567
+ result = transformed;
568
+ }
569
+ if (hooks?.afterHandle) {
570
+ const transformed = await hooks.afterHandle(ctx, result, method);
571
+ if (transformed !== undefined)
572
+ result = transformed;
573
+ }
574
+ if (method.outputSchema) {
575
+ result = method.outputSchema.parse(result);
576
+ }
577
+ if (result === undefined || result === null) {
578
+ logDone(204);
579
+ return new Response(null, { status: 204, headers: corsHeaders2(cors, req) });
580
+ }
581
+ logDone(200);
582
+ return json(result, 200, cors, req);
583
+ } catch (err) {
584
+ return respondError(err, ctx, method);
585
+ }
586
+ }
587
+ return async (req, server) => {
588
+ const url = new URL(req.url);
589
+ const traceId = resolveId(req);
590
+ const response = await dispatch(req, url, traceId, server);
591
+ if (!response.headers.has("x-request-id")) {
592
+ response.headers.set("x-request-id", traceId);
593
+ }
594
+ return response;
595
+ };
596
+ }
597
+ function createServer(config) {
598
+ const { routes, websocket, development, bun: bunExtra, port = 3000, hostname } = config;
599
+ const fetch = createHandler(config);
600
+ return websocket ? Bun.serve({
601
+ ...bunExtra,
602
+ ...routes && { routes },
603
+ ...development && { development },
604
+ port,
605
+ hostname,
606
+ websocket,
607
+ fetch
608
+ }) : Bun.serve({
609
+ ...bunExtra,
610
+ ...development && { development },
611
+ port,
612
+ hostname,
613
+ fetch
614
+ });
615
+ }
616
+ function normalizeGroups(config) {
617
+ const result = [];
618
+ if (config.services) {
619
+ for (const service of config.services) {
620
+ result.push({ prefix: "", service });
621
+ }
622
+ }
623
+ if (config.groups) {
624
+ for (const group of config.groups) {
625
+ for (const service of group.services) {
626
+ result.push({ prefix: group.pathPrefix ?? "", service, hooks: group.hooks });
627
+ }
628
+ }
629
+ }
630
+ return result;
631
+ }
632
+ function corsHeaders2(cors, req) {
633
+ if (!cors)
634
+ return {};
635
+ return corsHeaders(cors, req.headers.get("origin"));
636
+ }
637
+ function applyCors(res, cors, req) {
638
+ const extra = corsHeaders2(cors, req);
639
+ if (Object.keys(extra).length === 0)
640
+ return res;
641
+ const headers = new Headers(res.headers);
642
+ for (const [key, value] of Object.entries(extra))
643
+ headers.set(key, value);
644
+ return new Response(res.body, {
645
+ status: res.status,
646
+ statusText: res.statusText,
647
+ headers
648
+ });
649
+ }
650
+ function json(data, status, cors, req) {
651
+ return Response.json(data, { status, headers: corsHeaders2(cors, req) });
652
+ }
653
+ // src/server/event-bus.ts
654
+ function createEventBus() {
655
+ const subscriptions = new Map;
656
+ function add(event, fn, once) {
657
+ let set = subscriptions.get(event);
658
+ if (!set) {
659
+ set = new Set;
660
+ subscriptions.set(event, set);
661
+ }
662
+ const sub = { fn, once };
663
+ set.add(sub);
664
+ return () => {
665
+ set.delete(sub);
666
+ };
667
+ }
668
+ return {
669
+ emit(event, data) {
670
+ const set = subscriptions.get(event);
671
+ if (!set)
672
+ return;
673
+ for (const sub of [...set]) {
674
+ if (sub.once)
675
+ set.delete(sub);
676
+ try {
677
+ const result = sub.fn(data);
678
+ if (result instanceof Promise)
679
+ result.catch(() => {
680
+ return;
681
+ });
682
+ } catch {}
683
+ }
684
+ },
685
+ on(event, handler) {
686
+ return add(event, handler, false);
687
+ },
688
+ once(event, handler) {
689
+ return add(event, handler, true);
690
+ },
691
+ off(event, handler) {
692
+ const set = subscriptions.get(event);
693
+ if (!set)
694
+ return;
695
+ for (const sub of set) {
696
+ if (sub.fn === handler)
697
+ set.delete(sub);
698
+ }
699
+ },
700
+ clear() {
701
+ subscriptions.clear();
702
+ }
703
+ };
704
+ }
705
+ // src/server/implement.ts
706
+ function implement(contract, handlers) {
707
+ const methods = {};
708
+ const groupScope = contract.meta.scope ?? "public";
709
+ for (const [key, endpoint] of Object.entries(contract.endpoints)) {
710
+ const typedHandler = handlers[key];
711
+ methods[key] = {
712
+ method: endpoint.method,
713
+ path: endpoint.path,
714
+ desc: endpoint.desc,
715
+ toolName: "toolName" in endpoint ? endpoint.toolName : undefined,
716
+ expose: endpoint.expose,
717
+ scope: endpoint.scope ?? groupScope,
718
+ paramsSchema: endpoint.params,
719
+ inputSchema: endpoint.input,
720
+ outputSchema: endpoint.output,
721
+ multipart: endpoint.multipart,
722
+ handler: (ctx) => typedHandler(ctx)
723
+ };
724
+ }
725
+ return {
726
+ name: contract.meta.prefix,
727
+ prefix: contract.meta.prefix,
728
+ scope: groupScope,
729
+ methods
730
+ };
731
+ }
732
+ function createImplement() {
733
+ return (contract, handlers) => implement(contract, handlers);
734
+ }
735
+ // src/server/middleware/cookies.ts
736
+ function parseCookies(header) {
737
+ if (!header)
738
+ return {};
739
+ const cookies = {};
740
+ for (const pair of header.split(";")) {
741
+ const [key, ...rest] = pair.split("=");
742
+ if (!key)
743
+ continue;
744
+ const raw = rest.join("=").trim();
745
+ try {
746
+ cookies[key.trim()] = decodeURIComponent(raw);
747
+ } catch {
748
+ cookies[key.trim()] = raw;
749
+ }
750
+ }
751
+ return cookies;
752
+ }
753
+ function serializeCookie(name, value, opts = {}) {
754
+ let cookie = `${name}=${encodeURIComponent(value)}`;
755
+ if (opts.maxAge !== undefined)
756
+ cookie += `; Max-Age=${opts.maxAge}`;
757
+ if (opts.path)
758
+ cookie += `; Path=${opts.path}`;
759
+ if (opts.domain)
760
+ cookie += `; Domain=${opts.domain}`;
761
+ if (opts.secure)
762
+ cookie += "; Secure";
763
+ if (opts.httpOnly)
764
+ cookie += "; HttpOnly";
765
+ if (opts.sameSite)
766
+ cookie += `; SameSite=${opts.sameSite}`;
767
+ return cookie;
768
+ }
769
+ function defineCookie(config) {
770
+ const { name, ...options } = config;
771
+ return {
772
+ get(req) {
773
+ return parseCookies(req.headers.get("cookie"))[name];
774
+ },
775
+ set(value) {
776
+ return serializeCookie(name, value, options);
777
+ },
778
+ clear() {
779
+ return serializeCookie(name, "", { ...options, maxAge: 0 });
780
+ }
781
+ };
782
+ }
783
+
784
+ // src/server/middleware/auth.ts
785
+ function decodeBase64Url(segment) {
786
+ const b64 = segment.replace(/-/g, "+").replace(/_/g, "/");
787
+ const padded = b64.padEnd(Math.ceil(b64.length / 4) * 4, "=");
788
+ return Uint8Array.from(atob(padded), (c2) => c2.charCodeAt(0));
789
+ }
790
+ async function verifyJwt(token, secret) {
791
+ const [headerB64, payloadB64, signatureB64] = token.split(".");
792
+ if (!headerB64 || !payloadB64 || !signatureB64)
793
+ throw unauthorized("Invalid token format");
794
+ let header;
795
+ let payload;
796
+ let signature;
797
+ try {
798
+ const decoder = new TextDecoder;
799
+ header = JSON.parse(decoder.decode(decodeBase64Url(headerB64)));
800
+ payload = JSON.parse(decoder.decode(decodeBase64Url(payloadB64)));
801
+ signature = decodeBase64Url(signatureB64);
802
+ } catch {
803
+ throw unauthorized("Malformed token");
804
+ }
805
+ if (header.alg !== "HS256")
806
+ throw unauthorized("Unsupported token algorithm");
807
+ const encoder = new TextEncoder;
808
+ const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
809
+ const data = encoder.encode(`${headerB64}.${payloadB64}`);
810
+ const valid = await crypto.subtle.verify("HMAC", key, signature, data);
811
+ if (!valid)
812
+ throw unauthorized("Invalid token signature");
813
+ const now = Date.now() / 1000;
814
+ if (typeof payload.exp === "number" && payload.exp < now) {
815
+ throw unauthorized("Token expired");
816
+ }
817
+ if (typeof payload.nbf === "number" && payload.nbf > now) {
818
+ throw unauthorized("Token not yet valid");
819
+ }
820
+ return payload;
821
+ }
822
+ function extractToken(req, cookieName) {
823
+ const authHeader = req.headers.get("authorization");
824
+ if (authHeader?.startsWith("Bearer "))
825
+ return authHeader.slice(7);
826
+ if (cookieName) {
827
+ const cookies = parseCookies(req.headers.get("cookie"));
828
+ return cookies[cookieName] ?? null;
829
+ }
830
+ return null;
831
+ }
832
+ function createAuthHook(config) {
833
+ const onAnonymous = config.onAnonymous ?? (() => unauthorized());
834
+ const onForbidden = config.onForbidden ?? (() => forbidden());
835
+ return async (ctx, endpoint) => {
836
+ if (!(ctx.req instanceof Request))
837
+ return;
838
+ const identity = await config.resolve(ctx);
839
+ config.inject?.(ctx, identity);
840
+ const scope = endpoint.scope ?? config.defaultScope;
841
+ if (!scope)
842
+ return;
843
+ const rule = config.rules[scope];
844
+ if (!rule || rule === "public")
845
+ return;
846
+ if (!identity) {
847
+ onAnonymous();
848
+ return;
849
+ }
850
+ if (rule === "authenticated")
851
+ return;
852
+ if (!await rule(identity, ctx))
853
+ onForbidden();
854
+ };
855
+ }
856
+ function createBearerResolver(config) {
857
+ return async (req) => {
858
+ const token = extractToken(req);
859
+ if (!token)
860
+ return null;
861
+ return config.lookup(token, req);
862
+ };
863
+ }
864
+ // src/server/rate-limit.ts
865
+ function refillBucket(entry, config, now) {
866
+ const elapsed = (now - entry.lastRefill) / 1000;
867
+ entry.tokens = Math.min(config.max, entry.tokens + elapsed / config.window * config.max);
868
+ entry.lastRefill = now;
869
+ }
870
+ function createRateLimiter() {
871
+ const { store: buckets, destroy } = createSweptMap({
872
+ intervalMs: 60000,
873
+ isExpired: (entry, now) => now - entry.lastRefill > 300000
874
+ });
875
+ return {
876
+ destroy,
877
+ check(key, config) {
878
+ const now = Date.now();
879
+ let entry = buckets.get(key);
880
+ if (!entry) {
881
+ entry = { tokens: config.max, lastRefill: now };
882
+ buckets.set(key, entry);
883
+ }
884
+ refillBucket(entry, config, now);
885
+ if (entry.tokens < 1)
886
+ return false;
887
+ entry.tokens -= 1;
888
+ return true;
889
+ },
890
+ remaining(key, config) {
891
+ const entry = buckets.get(key);
892
+ if (!entry)
893
+ return config.max;
894
+ refillBucket(entry, config, Date.now());
895
+ return Math.floor(entry.tokens);
896
+ }
897
+ };
898
+ }
899
+ // src/server/socket-io.ts
900
+ import { Server as BunEngine } from "@socket.io/bun-engine";
901
+ import { Server as SocketIOServer } from "socket.io";
902
+ function createSocketIOServer(config) {
903
+ const path = config.path ?? "/socket.io/";
904
+ const io = new SocketIOServer({
905
+ path,
906
+ cors: {
907
+ origin: config.cors.origin,
908
+ credentials: config.cors.credentials ?? true,
909
+ methods: ["GET", "POST"]
910
+ },
911
+ transports: config.transports ?? ["websocket", "polling"],
912
+ pingTimeout: config.pingTimeout ?? 20000,
913
+ pingInterval: config.pingInterval ?? 1e4
914
+ });
915
+ const engine = new BunEngine({ path });
916
+ io.bind(engine);
917
+ const { websocket } = engine.handler();
918
+ const route = {
919
+ method: "ALL",
920
+ path: `${path.replace(/\/+$/, "")}/*`,
921
+ handler: (req, ctx) => {
922
+ if (!ctx.server) {
923
+ throw new Error("[stitchkit] createSocketIOServer route needs a running Bun server — mount it via createServer.");
924
+ }
925
+ return engine.handleRequest(req, ctx.server);
926
+ }
927
+ };
928
+ return { io, websocket, route };
929
+ }
930
+ export {
931
+ verifyJwt,
932
+ unauthorized,
933
+ streamSSE,
934
+ staticRoute,
935
+ serializeCookie,
936
+ resolveTraceId,
937
+ rateLimited,
938
+ parseSSE,
939
+ parseMultipart,
940
+ parseCookies,
941
+ notFound,
942
+ implement,
943
+ getClientInfo,
944
+ generateTraceId,
945
+ forbidden,
946
+ extractToken,
947
+ extractIp,
948
+ defineCookie,
949
+ createSocketIOServer,
950
+ createServer,
951
+ createRateLimiter,
952
+ createImplement,
953
+ createHandler,
954
+ createEventBus,
955
+ createCache,
956
+ createBearerResolver,
957
+ createAuthHook,
958
+ corsPreflightResponse,
959
+ corsHeaders,
960
+ conflict,
961
+ cacheHeaders,
962
+ badRequest,
963
+ appError,
964
+ AppError
965
+ };