stitchkit 0.1.0 → 0.3.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 (84) hide show
  1. package/README.md +10 -7
  2. package/dist/browser/http.d.ts +5 -0
  3. package/dist/browser/http.d.ts.map +1 -1
  4. package/dist/contract/define.d.ts.map +1 -1
  5. package/dist/contract/errors.d.ts +4 -3
  6. package/dist/contract/errors.d.ts.map +1 -1
  7. package/dist/contract/index.js +1 -1
  8. package/dist/{index-gfzn1n4n.js → index-0t460v7k.js} +13 -5
  9. package/dist/{index-n7bmdwmz.js → index-4b1j82gp.js} +8 -4
  10. package/dist/{index-5sxnvwb1.js → index-4kz3aqpx.js} +2 -2
  11. package/dist/index-dpj1jcys.js +677 -0
  12. package/dist/{index-7wfkbvss.js → index-kckky6zw.js} +6 -2
  13. package/dist/index-kzfs85xp.js +9 -0
  14. package/dist/index-mwmpw6j1.js +60 -0
  15. package/dist/index-q6ja2qwq.js +7 -0
  16. package/dist/index.js +4 -3
  17. package/dist/internal/errors.d.ts +13 -1
  18. package/dist/internal/errors.d.ts.map +1 -1
  19. package/dist/internal/safe-json.d.ts +19 -0
  20. package/dist/internal/safe-json.d.ts.map +1 -0
  21. package/dist/internal/within-dir.d.ts +7 -0
  22. package/dist/internal/within-dir.d.ts.map +1 -0
  23. package/dist/node.d.ts +4 -0
  24. package/dist/node.d.ts.map +1 -0
  25. package/dist/node.js +29 -0
  26. package/dist/observability/context.d.ts +9 -1
  27. package/dist/observability/context.d.ts.map +1 -1
  28. package/dist/observability/index.js +35 -15
  29. package/dist/observability/sanitize.d.ts.map +1 -1
  30. package/dist/server/context.d.ts +3 -2
  31. package/dist/server/context.d.ts.map +1 -1
  32. package/dist/server/create.d.ts +3 -3
  33. package/dist/server/create.d.ts.map +1 -1
  34. package/dist/server/event-bus.d.ts +10 -1
  35. package/dist/server/event-bus.d.ts.map +1 -1
  36. package/dist/server/index.d.ts +2 -2
  37. package/dist/server/index.d.ts.map +1 -1
  38. package/dist/server/index.js +94 -606
  39. package/dist/server/logger.d.ts +5 -4
  40. package/dist/server/logger.d.ts.map +1 -1
  41. package/dist/server/middleware/auth.d.ts +24 -1
  42. package/dist/server/middleware/auth.d.ts.map +1 -1
  43. package/dist/server/middleware/cookies.d.ts.map +1 -1
  44. package/dist/server/middleware/cors.d.ts +7 -0
  45. package/dist/server/middleware/cors.d.ts.map +1 -1
  46. package/dist/server/multipart.d.ts +2 -1
  47. package/dist/server/multipart.d.ts.map +1 -1
  48. package/dist/server/node.d.ts +12 -0
  49. package/dist/server/node.d.ts.map +1 -0
  50. package/dist/server/rate-limit.d.ts +7 -2
  51. package/dist/server/rate-limit.d.ts.map +1 -1
  52. package/dist/server/request.d.ts +27 -4
  53. package/dist/server/request.d.ts.map +1 -1
  54. package/dist/server/router.d.ts +4 -2
  55. package/dist/server/router.d.ts.map +1 -1
  56. package/dist/server/stream.d.ts +3 -1
  57. package/dist/server/stream.d.ts.map +1 -1
  58. package/dist/server/types.d.ts +35 -20
  59. package/dist/server/types.d.ts.map +1 -1
  60. package/dist/tools/agent.d.ts +15 -2
  61. package/dist/tools/agent.d.ts.map +1 -1
  62. package/dist/tools/coerce.d.ts +12 -0
  63. package/dist/tools/coerce.d.ts.map +1 -0
  64. package/dist/tools/execute.d.ts +26 -2
  65. package/dist/tools/execute.d.ts.map +1 -1
  66. package/dist/tools/flatten.d.ts +15 -0
  67. package/dist/tools/flatten.d.ts.map +1 -0
  68. package/dist/tools/json-schema.d.ts +18 -0
  69. package/dist/tools/json-schema.d.ts.map +1 -0
  70. package/dist/tools/manifest.d.ts +17 -0
  71. package/dist/tools/manifest.d.ts.map +1 -0
  72. package/dist/tools/mcp-handler.d.ts.map +1 -1
  73. package/dist/tools/mcp.d.ts +53 -3
  74. package/dist/tools/mcp.d.ts.map +1 -1
  75. package/dist/tools/mount.d.ts +18 -6
  76. package/dist/tools/mount.d.ts.map +1 -1
  77. package/dist/tools/schema.d.ts +19 -2
  78. package/dist/tools/schema.d.ts.map +1 -1
  79. package/dist/tools/view-file.d.ts.map +1 -1
  80. package/dist/tools.d.ts +6 -2
  81. package/dist/tools.d.ts.map +1 -1
  82. package/dist/tools.js +415 -185
  83. package/package.json +15 -4
  84. package/dist/index-ke4mx4ea.js +0 -38
@@ -1,10 +1,17 @@
1
1
  import {
2
2
  parseSSE,
3
3
  streamSSE
4
- } from "../index-n7bmdwmz.js";
4
+ } from "../index-4b1j82gp.js";
5
5
  import {
6
- normalizeError
7
- } from "../index-gfzn1n4n.js";
6
+ corsHeaders,
7
+ corsPreflightResponse,
8
+ createHandler,
9
+ createServer,
10
+ parseMultipart,
11
+ staticRoute
12
+ } from "../index-dpj1jcys.js";
13
+ import"../index-q6ja2qwq.js";
14
+ import"../index-0t460v7k.js";
8
15
  import {
9
16
  AppError,
10
17
  appError,
@@ -14,14 +21,21 @@ import {
14
21
  notFound,
15
22
  rateLimited,
16
23
  unauthorized
17
- } from "../index-7wfkbvss.js";
24
+ } from "../index-kckky6zw.js";
18
25
  import {
19
26
  extractIp,
20
27
  generateTraceId,
21
28
  getClientInfo,
22
- parseQueryParams,
29
+ resolveSocketIp,
23
30
  resolveTraceId
24
- } from "../index-ke4mx4ea.js";
31
+ } from "../index-mwmpw6j1.js";
32
+ import {
33
+ isUnsafeKey,
34
+ safeJsonParse
35
+ } from "../index-kzfs85xp.js";
36
+ import {
37
+ isRecord
38
+ } from "../index-809wc1tt.js";
25
39
  // src/server/swept-map.ts
26
40
  function createSweptMap(options) {
27
41
  const store = new Map;
@@ -79,579 +93,8 @@ function cacheHeaders(maxAge, scope = "public") {
79
93
  "Cache-Control": `${scope}, max-age=${maxAge}`
80
94
  };
81
95
  }
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
96
  // src/server/event-bus.ts
654
- function createEventBus() {
97
+ function createEventBus(options = {}) {
655
98
  const subscriptions = new Map;
656
99
  function add(event, fn, once) {
657
100
  let set = subscriptions.get(event);
@@ -675,11 +118,12 @@ function createEventBus() {
675
118
  set.delete(sub);
676
119
  try {
677
120
  const result = sub.fn(data);
678
- if (result instanceof Promise)
679
- result.catch(() => {
680
- return;
681
- });
682
- } catch {}
121
+ if (result instanceof Promise) {
122
+ result.catch((err) => options.onListenerError?.(err, event));
123
+ }
124
+ } catch (err) {
125
+ options.onListenerError?.(err, event);
126
+ }
683
127
  }
684
128
  },
685
129
  on(event, handler) {
@@ -738,14 +182,17 @@ function parseCookies(header) {
738
182
  return {};
739
183
  const cookies = {};
740
184
  for (const pair of header.split(";")) {
741
- const [key, ...rest] = pair.split("=");
742
- if (!key)
185
+ const eq = pair.indexOf("=");
186
+ if (eq === -1)
743
187
  continue;
744
- const raw = rest.join("=").trim();
188
+ const key = pair.slice(0, eq).trim();
189
+ if (!key || isUnsafeKey(key))
190
+ continue;
191
+ const raw = pair.slice(eq + 1).trim();
745
192
  try {
746
- cookies[key.trim()] = decodeURIComponent(raw);
193
+ cookies[key] = decodeURIComponent(raw);
747
194
  } catch {
748
- cookies[key.trim()] = raw;
195
+ cookies[key] = raw;
749
196
  }
750
197
  }
751
198
  return cookies;
@@ -782,26 +229,37 @@ function defineCookie(config) {
782
229
  }
783
230
 
784
231
  // src/server/middleware/auth.ts
232
+ var MAX_TOKEN_BYTES = 8192;
785
233
  function decodeBase64Url(segment) {
234
+ if (!/^[A-Za-z0-9_-]*$/.test(segment) || segment.length % 4 === 1) {
235
+ throw new Error("invalid base64url segment");
236
+ }
786
237
  const b64 = segment.replace(/-/g, "+").replace(/_/g, "/");
787
238
  const padded = b64.padEnd(Math.ceil(b64.length / 4) * 4, "=");
788
- return Uint8Array.from(atob(padded), (c2) => c2.charCodeAt(0));
239
+ return Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
789
240
  }
790
- async function verifyJwt(token, secret) {
241
+ async function verifyJwt(token, secret, options = {}) {
242
+ if (!secret)
243
+ throw new Error("verifyJwt: a non-empty secret is required");
244
+ if (token.length > MAX_TOKEN_BYTES)
245
+ throw unauthorized("Token too large");
791
246
  const [headerB64, payloadB64, signatureB64] = token.split(".");
792
247
  if (!headerB64 || !payloadB64 || !signatureB64)
793
248
  throw unauthorized("Invalid token format");
794
249
  let header;
795
- let payload;
250
+ let payloadRaw;
796
251
  let signature;
797
252
  try {
798
253
  const decoder = new TextDecoder;
799
254
  header = JSON.parse(decoder.decode(decodeBase64Url(headerB64)));
800
- payload = JSON.parse(decoder.decode(decodeBase64Url(payloadB64)));
255
+ payloadRaw = safeJsonParse(decoder.decode(decodeBase64Url(payloadB64)));
801
256
  signature = decodeBase64Url(signatureB64);
802
257
  } catch {
803
258
  throw unauthorized("Malformed token");
804
259
  }
260
+ if (!isRecord(payloadRaw))
261
+ throw unauthorized("Malformed token");
262
+ const payload = payloadRaw;
805
263
  if (header.alg !== "HS256")
806
264
  throw unauthorized("Unsupported token algorithm");
807
265
  const encoder = new TextEncoder;
@@ -810,12 +268,28 @@ async function verifyJwt(token, secret) {
810
268
  const valid = await crypto.subtle.verify("HMAC", key, signature, data);
811
269
  if (!valid)
812
270
  throw unauthorized("Invalid token signature");
271
+ const leeway = options.leewaySeconds ?? 60;
813
272
  const now = Date.now() / 1000;
814
- if (typeof payload.exp === "number" && payload.exp < now) {
815
- throw unauthorized("Token expired");
273
+ if ("exp" in payload) {
274
+ if (typeof payload.exp !== "number")
275
+ throw unauthorized("Malformed token");
276
+ if (payload.exp < now - leeway)
277
+ throw unauthorized("Token expired");
278
+ }
279
+ if ("nbf" in payload) {
280
+ if (typeof payload.nbf !== "number")
281
+ throw unauthorized("Malformed token");
282
+ if (payload.nbf > now + leeway)
283
+ throw unauthorized("Token not yet valid");
284
+ }
285
+ if (options.issuer !== undefined && payload.iss !== options.issuer) {
286
+ throw unauthorized("Token issuer mismatch");
816
287
  }
817
- if (typeof payload.nbf === "number" && payload.nbf > now) {
818
- throw unauthorized("Token not yet valid");
288
+ if (options.audience !== undefined) {
289
+ const aud = payload.aud;
290
+ const ok = Array.isArray(aud) ? aud.includes(options.audience) : aud === options.audience;
291
+ if (!ok)
292
+ throw unauthorized("Token audience mismatch");
819
293
  }
820
294
  return payload;
821
295
  }
@@ -833,15 +307,16 @@ function createAuthHook(config) {
833
307
  const onAnonymous = config.onAnonymous ?? (() => unauthorized());
834
308
  const onForbidden = config.onForbidden ?? (() => forbidden());
835
309
  return async (ctx, endpoint) => {
836
- if (!(ctx.req instanceof Request))
837
- return;
838
- const identity = await config.resolve(ctx);
310
+ const identity = ctx.source === "http" ? await config.resolve(ctx) : await config.resolveFromContext?.(ctx) ?? null;
839
311
  config.inject?.(ctx, identity);
840
312
  const scope = endpoint.scope ?? config.defaultScope;
841
313
  if (!scope)
842
314
  return;
843
315
  const rule = config.rules[scope];
844
- if (!rule || rule === "public")
316
+ if (!rule) {
317
+ throw new Error(`[stitchkit] auth: no rule for scope "${scope}"`);
318
+ }
319
+ if (rule === "public")
845
320
  return;
846
321
  if (!identity) {
847
322
  onAnonymous();
@@ -867,6 +342,7 @@ function refillBucket(entry, config, now) {
867
342
  entry.tokens = Math.min(config.max, entry.tokens + elapsed / config.window * config.max);
868
343
  entry.lastRefill = now;
869
344
  }
345
+ var MAX_BUCKETS = 1e5;
870
346
  function createRateLimiter() {
871
347
  const { store: buckets, destroy } = createSweptMap({
872
348
  intervalMs: 60000,
@@ -876,15 +352,26 @@ function createRateLimiter() {
876
352
  destroy,
877
353
  check(key, config) {
878
354
  const now = Date.now();
879
- let entry = buckets.get(key);
880
- if (!entry) {
881
- entry = { tokens: config.max, lastRefill: now };
355
+ const entry = buckets.get(key);
356
+ if (entry) {
357
+ buckets.delete(key);
882
358
  buckets.set(key, entry);
359
+ refillBucket(entry, config, now);
360
+ if (entry.tokens < 1)
361
+ return false;
362
+ entry.tokens -= 1;
363
+ return true;
364
+ }
365
+ if (buckets.size >= MAX_BUCKETS) {
366
+ const lru = buckets.keys().next().value;
367
+ if (lru !== undefined)
368
+ buckets.delete(lru);
883
369
  }
884
- refillBucket(entry, config, now);
885
- if (entry.tokens < 1)
370
+ const fresh = { tokens: config.max, lastRefill: now };
371
+ buckets.set(key, fresh);
372
+ if (fresh.tokens < 1)
886
373
  return false;
887
- entry.tokens -= 1;
374
+ fresh.tokens -= 1;
888
375
  return true;
889
376
  },
890
377
  remaining(key, config) {
@@ -934,6 +421,7 @@ export {
934
421
  staticRoute,
935
422
  serializeCookie,
936
423
  resolveTraceId,
424
+ resolveSocketIp,
937
425
  rateLimited,
938
426
  parseSSE,
939
427
  parseMultipart,