stitchkit 0.2.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 (63) hide show
  1. package/dist/browser/http.d.ts +5 -0
  2. package/dist/browser/http.d.ts.map +1 -1
  3. package/dist/{index-a35v22fh.js → index-0t460v7k.js} +12 -4
  4. package/dist/{index-n7bmdwmz.js → index-4b1j82gp.js} +8 -4
  5. package/dist/{index-5sxnvwb1.js → index-4kz3aqpx.js} +2 -2
  6. package/dist/{index-v2z2v3mq.js → index-dpj1jcys.js} +150 -60
  7. package/dist/index-kzfs85xp.js +9 -0
  8. package/dist/index-mwmpw6j1.js +60 -0
  9. package/dist/index-q6ja2qwq.js +7 -0
  10. package/dist/index.js +4 -3
  11. package/dist/internal/errors.d.ts +13 -1
  12. package/dist/internal/errors.d.ts.map +1 -1
  13. package/dist/internal/safe-json.d.ts +19 -0
  14. package/dist/internal/safe-json.d.ts.map +1 -0
  15. package/dist/internal/within-dir.d.ts +7 -0
  16. package/dist/internal/within-dir.d.ts.map +1 -0
  17. package/dist/node.js +6 -3
  18. package/dist/observability/context.d.ts +9 -1
  19. package/dist/observability/context.d.ts.map +1 -1
  20. package/dist/observability/index.js +35 -15
  21. package/dist/observability/sanitize.d.ts.map +1 -1
  22. package/dist/server/context.d.ts +3 -2
  23. package/dist/server/context.d.ts.map +1 -1
  24. package/dist/server/create.d.ts.map +1 -1
  25. package/dist/server/event-bus.d.ts +10 -1
  26. package/dist/server/event-bus.d.ts.map +1 -1
  27. package/dist/server/index.d.ts +1 -1
  28. package/dist/server/index.d.ts.map +1 -1
  29. package/dist/server/index.js +86 -32
  30. package/dist/server/logger.d.ts +5 -4
  31. package/dist/server/logger.d.ts.map +1 -1
  32. package/dist/server/middleware/auth.d.ts +24 -1
  33. package/dist/server/middleware/auth.d.ts.map +1 -1
  34. package/dist/server/middleware/cookies.d.ts.map +1 -1
  35. package/dist/server/middleware/cors.d.ts +7 -0
  36. package/dist/server/middleware/cors.d.ts.map +1 -1
  37. package/dist/server/multipart.d.ts +2 -1
  38. package/dist/server/multipart.d.ts.map +1 -1
  39. package/dist/server/rate-limit.d.ts +7 -2
  40. package/dist/server/rate-limit.d.ts.map +1 -1
  41. package/dist/server/request.d.ts +27 -4
  42. package/dist/server/request.d.ts.map +1 -1
  43. package/dist/server/router.d.ts +4 -2
  44. package/dist/server/router.d.ts.map +1 -1
  45. package/dist/server/stream.d.ts +3 -1
  46. package/dist/server/stream.d.ts.map +1 -1
  47. package/dist/server/types.d.ts +12 -0
  48. package/dist/server/types.d.ts.map +1 -1
  49. package/dist/tools/coerce.d.ts +8 -4
  50. package/dist/tools/coerce.d.ts.map +1 -1
  51. package/dist/tools/execute.d.ts +1 -1
  52. package/dist/tools/execute.d.ts.map +1 -1
  53. package/dist/tools/manifest.d.ts +4 -0
  54. package/dist/tools/manifest.d.ts.map +1 -1
  55. package/dist/tools/mcp-handler.d.ts.map +1 -1
  56. package/dist/tools/mount.d.ts +2 -2
  57. package/dist/tools/mount.d.ts.map +1 -1
  58. package/dist/tools/view-file.d.ts.map +1 -1
  59. package/dist/tools.d.ts +1 -1
  60. package/dist/tools.d.ts.map +1 -1
  61. package/dist/tools.js +140 -93
  62. package/package.json +1 -1
  63. package/dist/index-ke4mx4ea.js +0 -38
package/dist/tools.js CHANGED
@@ -1,41 +1,77 @@
1
1
  import {
2
2
  createClient
3
- } from "./index-5sxnvwb1.js";
3
+ } from "./index-4kz3aqpx.js";
4
4
  import {
5
- isRecord
6
- } from "./index-809wc1tt.js";
5
+ isWithinDir
6
+ } from "./index-q6ja2qwq.js";
7
7
  import {
8
8
  formatZodError,
9
- normalizeError
10
- } from "./index-a35v22fh.js";
9
+ normalizeError,
10
+ validateHandlerOutput
11
+ } from "./index-0t460v7k.js";
11
12
  import"./index-kckky6zw.js";
13
+ import {
14
+ isUnsafeKey,
15
+ safeJsonParse
16
+ } from "./index-kzfs85xp.js";
17
+ import {
18
+ isRecord
19
+ } from "./index-809wc1tt.js";
12
20
 
13
21
  // src/tools/agent.ts
14
22
  import { tool, zodSchema } from "ai";
15
23
 
16
- // src/tools/schema.ts
24
+ // src/tools/coerce.ts
17
25
  import { z } from "zod";
26
+ function needsJsonCoercion(field) {
27
+ if (field instanceof z.ZodArray || field instanceof z.ZodObject)
28
+ return true;
29
+ if (field instanceof z.ZodOptional || field instanceof z.ZodNullable || field instanceof z.ZodDefault) {
30
+ return needsJsonCoercion(field.unwrap());
31
+ }
32
+ return false;
33
+ }
34
+ function coerceJsonArgs(args, schema) {
35
+ if (!(schema instanceof z.ZodObject))
36
+ return args;
37
+ const shape = schema.shape;
38
+ const out = {};
39
+ for (const [key, value] of Object.entries(args)) {
40
+ const field = shape[key];
41
+ if (field && needsJsonCoercion(field) && typeof value === "string") {
42
+ try {
43
+ out[key] = safeJsonParse(value);
44
+ continue;
45
+ } catch {}
46
+ }
47
+ out[key] = value;
48
+ }
49
+ return out;
50
+ }
51
+
52
+ // src/tools/schema.ts
53
+ import { z as z2 } from "zod";
18
54
  function objectShapeKeys(schema) {
19
- return schema instanceof z.ZodObject ? Object.keys(schema.shape) : [];
55
+ return schema instanceof z2.ZodObject ? Object.keys(schema.shape) : [];
20
56
  }
21
57
  function mergeSchemas(paramsSchema, inputSchema) {
22
- if (paramsSchema && !(paramsSchema instanceof z.ZodObject)) {
58
+ if (paramsSchema && !(paramsSchema instanceof z2.ZodObject)) {
23
59
  throw new Error("Tool params schema must be a z.object()");
24
60
  }
25
- const paramsObject = paramsSchema instanceof z.ZodObject ? paramsSchema : undefined;
61
+ const paramsObject = paramsSchema instanceof z2.ZodObject ? paramsSchema : undefined;
26
62
  if (!inputSchema) {
27
- return paramsObject ?? z.object({});
63
+ return paramsObject ?? z2.object({});
28
64
  }
29
- if (inputSchema instanceof z.ZodObject) {
65
+ if (inputSchema instanceof z2.ZodObject) {
30
66
  if (paramsObject) {
31
67
  const conflicts = Object.keys(paramsObject.shape).filter((key) => (key in inputSchema.shape));
32
68
  if (conflicts.length > 0) {
33
69
  throw new Error(`Schema merge conflict: ${conflicts.join(", ")} appear in both params and input`);
34
70
  }
35
71
  }
36
- return z.object({ ...paramsObject?.shape ?? {}, ...inputSchema.shape });
72
+ return z2.object({ ...paramsObject?.shape ?? {}, ...inputSchema.shape });
37
73
  }
38
- return paramsObject ? z.intersection(paramsObject, inputSchema) : inputSchema;
74
+ return paramsObject ? z2.intersection(paramsObject, inputSchema) : inputSchema;
39
75
  }
40
76
 
41
77
  // src/tools/execute.ts
@@ -48,24 +84,34 @@ function toolResultFromError(err) {
48
84
  ...appErr.hint && { hint: appErr.hint }
49
85
  };
50
86
  }
51
- async function executeToolMethod(method, toolName, rawArgs, context, hooks, lifecycle) {
87
+ async function executeToolMethod(method, toolName, rawArgs, context, hooks, lifecycle, coerceJson = false) {
52
88
  const startedAt = Date.now();
53
89
  const finish = async (result) => {
54
90
  await hooks?.afterToolCall?.(toolName, rawArgs, result, Date.now() - startedAt, context);
55
91
  return result;
56
92
  };
57
93
  if (hooks?.beforeToolCall) {
58
- await hooks.beforeToolCall(toolName, rawArgs, context);
94
+ try {
95
+ await hooks.beforeToolCall(toolName, rawArgs, context);
96
+ } catch (err) {
97
+ return finish(toolResultFromError(err));
98
+ }
59
99
  }
60
100
  const paramKeys = new Set(objectShapeKeys(method.paramsSchema));
61
- const paramArgs = {};
62
- const inputArgs = {};
101
+ let paramArgs = {};
102
+ let inputArgs = {};
63
103
  for (const [key, value] of Object.entries(rawArgs)) {
104
+ if (isUnsafeKey(key))
105
+ continue;
64
106
  if (paramKeys.has(key))
65
107
  paramArgs[key] = value;
66
108
  else
67
109
  inputArgs[key] = value;
68
110
  }
111
+ if (coerceJson) {
112
+ paramArgs = coerceJsonArgs(paramArgs, method.paramsSchema);
113
+ inputArgs = coerceJsonArgs(inputArgs, method.inputSchema);
114
+ }
69
115
  let params;
70
116
  if (method.paramsSchema) {
71
117
  const result = method.paramsSchema.safeParse(paramArgs);
@@ -102,19 +148,17 @@ async function executeToolMethod(method, toolName, rawArgs, context, hooks, life
102
148
  data = transformed;
103
149
  }
104
150
  if (method.outputSchema) {
105
- const parsed = method.outputSchema.safeParse(data);
106
- if (!parsed.success) {
151
+ const checked = validateHandlerOutput(method.outputSchema, data);
152
+ if (!checked.ok) {
107
153
  return finish({
108
154
  ok: false,
109
155
  code: "INTERNAL_SERVER_ERROR",
110
- details: {
111
- message: `Handler output does not match the contract: ${formatZodError(parsed.error)}`
112
- }
156
+ details: { message: checked.message }
113
157
  });
114
158
  }
115
- data = parsed.data;
159
+ data = checked.data;
116
160
  }
117
- const output = data === undefined || data === null ? { status: "ok" } : data;
161
+ const output = (data === undefined || data === null) && !method.outputSchema ? { status: "ok" } : data;
118
162
  return finish({ ok: true, data: output });
119
163
  } catch (err) {
120
164
  return finish(toolResultFromError(err));
@@ -124,50 +168,6 @@ async function executeToolMethod(method, toolName, rawArgs, context, hooks, life
124
168
  // src/tools/mount.ts
125
169
  import { z as z4 } from "zod";
126
170
 
127
- // src/tools/coerce.ts
128
- import { z as z2 } from "zod";
129
- function needsJsonCoercion(field) {
130
- if (field instanceof z2.ZodArray || field instanceof z2.ZodObject)
131
- return true;
132
- if (field instanceof z2.ZodOptional || field instanceof z2.ZodNullable || field instanceof z2.ZodDefault) {
133
- return needsJsonCoercion(field.unwrap());
134
- }
135
- return false;
136
- }
137
- var jsonCoerce = (val) => {
138
- if (typeof val !== "string")
139
- return val;
140
- try {
141
- return JSON.parse(val);
142
- } catch {
143
- return val;
144
- }
145
- };
146
- function withJsonCoercion(schema) {
147
- const shape = schema.shape;
148
- const coerced = {};
149
- for (const key of Object.keys(shape)) {
150
- const field = shape[key];
151
- if (!field || !needsJsonCoercion(field)) {
152
- if (field)
153
- coerced[key] = field;
154
- continue;
155
- }
156
- let inner = field;
157
- const wrappers = [];
158
- while (inner instanceof z2.ZodOptional || inner instanceof z2.ZodNullable) {
159
- wrappers.push(inner instanceof z2.ZodOptional ? "optional" : "nullable");
160
- inner = inner.unwrap();
161
- }
162
- let result = z2.preprocess(jsonCoerce, inner);
163
- for (const wrapper of wrappers.reverse()) {
164
- result = wrapper === "optional" ? z2.optional(result) : z2.nullable(result);
165
- }
166
- coerced[key] = result;
167
- }
168
- return z2.object(coerced);
169
- }
170
-
171
171
  // src/tools/flatten.ts
172
172
  import { z as z3 } from "zod";
173
173
  function flattenDiscriminatedUnion(union) {
@@ -209,7 +209,7 @@ function flattenDiscriminatedUnion(union) {
209
209
  for (const [key, field] of Object.entries(fieldSchemas)) {
210
210
  const variants = fieldToVariants.get(key) ?? [];
211
211
  const hint = `Required if ${discriminator} = ${variants.join(" | ")}`;
212
- shape[key] = field instanceof z3.ZodType ? field.describe(hint) : field;
212
+ shape[key] = field.describe(hint);
213
213
  }
214
214
  return z3.object(shape);
215
215
  }
@@ -262,7 +262,7 @@ function applyExtend(base, extra) {
262
262
  return z4.intersection(z4.object(extra), base);
263
263
  }
264
264
  function collectTools(service, transport, config = {}) {
265
- const { extend, coerceJsonArgs = true, flattenUnionInput = false } = config;
265
+ const { extend, flattenUnionInput = false } = config;
266
266
  const tools = [];
267
267
  for (const [methodName, method] of Object.entries(service.methods)) {
268
268
  if (method.expose && !method.expose.includes(transport))
@@ -274,9 +274,6 @@ function collectTools(service, transport, config = {}) {
274
274
  if (flattenUnionInput && baseSchema instanceof z4.ZodDiscriminatedUnion) {
275
275
  baseSchema = flattenDiscriminatedUnion(baseSchema);
276
276
  }
277
- if (coerceJsonArgs && baseSchema instanceof z4.ZodObject) {
278
- baseSchema = withJsonCoercion(baseSchema);
279
- }
280
277
  const shouldExtend = !!extend && (!extend.filter || extend.filter(service, method));
281
278
  const schema = shouldExtend && extend ? applyExtend(baseSchema, extend.schema) : baseSchema;
282
279
  tools.push({ method, name, schema, shouldExtend });
@@ -291,7 +288,7 @@ function createToolRunner(config) {
291
288
  extraContext = await config.extend.resolve(rawArgs);
292
289
  }
293
290
  const cleanArgs = extendKeys ? Object.fromEntries(Object.entries(rawArgs).filter(([key]) => !extendKeys.has(key))) : rawArgs;
294
- return executeToolMethod(tool.method, tool.name, cleanArgs, { ...config.context, ...extraContext, source: config.source }, config.hooks, config.lifecycle);
291
+ return executeToolMethod(tool.method, tool.name, cleanArgs, { ...config.context, ...extraContext, source: config.source }, config.hooks, config.lifecycle, config.coerceJsonArgs ?? true);
295
292
  };
296
293
  }
297
294
  function formatToolError(result, toolName, errorHint) {
@@ -321,12 +318,12 @@ function mountAgent(services, config = {}) {
321
318
  context: config.context,
322
319
  hooks: config.hooks,
323
320
  lifecycle: config.lifecycle,
324
- errorHint: config.errorHint
321
+ errorHint: config.errorHint,
322
+ coerceJsonArgs: config.coerceJsonArgs
325
323
  });
326
324
  for (const service of serviceList) {
327
325
  for (const mountable of collectTools(service, "AGENT", {
328
326
  extend: config.extend,
329
- coerceJsonArgs: config.coerceJsonArgs,
330
327
  flattenUnionInput: config.flattenUnionInput
331
328
  })) {
332
329
  if (mountable.name in tools) {
@@ -364,11 +361,15 @@ function toJsonSchema(schema, io) {
364
361
 
365
362
  // src/tools/manifest.ts
366
363
  function buildToolManifest(tools) {
367
- return tools.map((t) => ({
368
- name: t.name,
369
- description: t.method.desc,
370
- inputSchema: toJsonSchema(t.schema, "input")
371
- }));
364
+ return tools.map((t) => {
365
+ let inputSchema;
366
+ try {
367
+ inputSchema = toJsonSchema(t.schema, "input");
368
+ } catch {
369
+ inputSchema = {};
370
+ }
371
+ return { name: t.name, description: t.method.desc, inputSchema };
372
+ });
372
373
  }
373
374
  // src/tools/mcp.ts
374
375
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -468,14 +469,14 @@ function mountMcp(mcpServer, services, config = {}) {
468
469
  context: config.context,
469
470
  hooks: config.hooks,
470
471
  lifecycle: config.lifecycle,
471
- errorHint: config.errorHint
472
+ errorHint: config.errorHint,
473
+ coerceJsonArgs: config.coerceJsonArgs
472
474
  });
473
475
  const seen = new Set;
474
476
  const failures = [];
475
477
  for (const service of serviceList) {
476
478
  for (const mountable of collectTools(service, "MCP", {
477
479
  extend: config.extend,
478
- coerceJsonArgs: config.coerceJsonArgs,
479
480
  flattenUnionInput: config.flattenUnionInput
480
481
  })) {
481
482
  const prepared = prepareMcpTool(mountable, policy, config.logger, failures, seen);
@@ -521,6 +522,9 @@ import {
521
522
  } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
522
523
  var EVENT_TTL_MS = 10 * 60 * 1000;
523
524
  var SESSION_TTL_MS = 30 * 60 * 1000;
525
+ var SWEEP_INTERVAL_MS = 60 * 1000;
526
+ var MAX_EVENTS = 1e4;
527
+ var MAX_SESSIONS = 1000;
524
528
 
525
529
  class InMemoryEventStore {
526
530
  events = new Map;
@@ -528,6 +532,11 @@ class InMemoryEventStore {
528
532
  async storeEvent(streamId, message) {
529
533
  const eventId = String(++this.counter);
530
534
  this.events.set(eventId, { streamId, message, timestamp: Date.now() });
535
+ if (this.events.size > MAX_EVENTS) {
536
+ const oldest = this.events.keys().next().value;
537
+ if (oldest !== undefined)
538
+ this.events.delete(oldest);
539
+ }
531
540
  return eventId;
532
541
  }
533
542
  async getStreamIdForEventId(eventId) {
@@ -562,18 +571,21 @@ function createMcpHandler(config) {
562
571
  }
563
572
  const eventStore = new InMemoryEventStore;
564
573
  const sessions = new Map;
574
+ const closeTransport = (transport) => {
575
+ transport.close().catch((err) => {
576
+ console.error("[stitchkit] MCP transport close failed:", err);
577
+ });
578
+ };
565
579
  setInterval(() => {
566
580
  eventStore.cleanup();
567
581
  const cutoff = Date.now() - SESSION_TTL_MS;
568
582
  for (const [id, session] of sessions) {
569
583
  if (session.lastSeen < cutoff) {
570
584
  sessions.delete(id);
571
- session.transport.close().catch(() => {
572
- return;
573
- });
585
+ closeTransport(session.transport);
574
586
  }
575
587
  }
576
- }, EVENT_TTL_MS).unref();
588
+ }, SWEEP_INTERVAL_MS).unref();
577
589
  return async (req) => {
578
590
  const auth = await config.auth(req);
579
591
  if (!auth) {
@@ -591,6 +603,22 @@ function createMcpHandler(config) {
591
603
  }
592
604
  return existing.transport.handleRequest(req);
593
605
  }
606
+ if (sessions.size >= MAX_SESSIONS) {
607
+ let oldestId;
608
+ let oldestSeen = Number.POSITIVE_INFINITY;
609
+ for (const [id, session] of sessions) {
610
+ if (session.lastSeen < oldestSeen) {
611
+ oldestSeen = session.lastSeen;
612
+ oldestId = id;
613
+ }
614
+ }
615
+ if (oldestId !== undefined) {
616
+ const evicted = sessions.get(oldestId);
617
+ sessions.delete(oldestId);
618
+ if (evicted)
619
+ closeTransport(evicted.transport);
620
+ }
621
+ }
594
622
  const newSessionId = randomUUID();
595
623
  const transport = new WebStandardStreamableHTTPServerTransport({
596
624
  sessionIdGenerator: () => newSessionId,
@@ -658,8 +686,10 @@ function implementRemote(contract, http, options) {
658
686
  import { lookup } from "node:dns/promises";
659
687
  import { readFile, stat } from "node:fs/promises";
660
688
  import { isIP } from "node:net";
661
- import { extname, resolve, sep } from "node:path";
689
+ import { extname, resolve } from "node:path";
662
690
  import { z as z7 } from "zod";
691
+ var NUMERIC_HOST = /^(0x[0-9a-f]+|[0-9.]+)$/i;
692
+ var MAX_REDIRECTS = 5;
663
693
  var MAX_INLINE_BYTES = 20 * 1024 * 1024;
664
694
  var EXT_MIME = {
665
695
  ".png": "image/png",
@@ -702,6 +732,9 @@ async function assertPublicUrl(url) {
702
732
  throw new Error("refusing to fetch a private address");
703
733
  return;
704
734
  }
735
+ if (NUMERIC_HOST.test(host)) {
736
+ throw new Error("refusing to fetch a non-canonical numeric host");
737
+ }
705
738
  if (host === "localhost" || host.endsWith(".local") || host.endsWith(".internal")) {
706
739
  throw new Error("refusing to fetch an internal host");
707
740
  }
@@ -712,6 +745,22 @@ async function assertPublicUrl(url) {
712
745
  }
713
746
  }
714
747
  }
748
+ async function fetchGuarded(start, allowPrivate) {
749
+ let url = start;
750
+ for (let hop = 0;hop <= MAX_REDIRECTS; hop++) {
751
+ if (!allowPrivate)
752
+ await assertPublicUrl(url);
753
+ const res = await fetch(url, { redirect: "manual" });
754
+ if (res.status < 300 || res.status >= 400)
755
+ return res;
756
+ const location = res.headers.get("location");
757
+ if (!location)
758
+ return res;
759
+ await res.body?.cancel();
760
+ url = new URL(location, url);
761
+ }
762
+ throw new Error("too many redirects");
763
+ }
715
764
  async function readCapped(res, max) {
716
765
  const reader = res.body?.getReader();
717
766
  if (!reader)
@@ -739,9 +788,7 @@ async function fetchSource(pathOrUrl, options) {
739
788
  const extMime = EXT_MIME[extname(pathOrUrl).toLowerCase()];
740
789
  if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) {
741
790
  const url = new URL(pathOrUrl);
742
- if (!options.allowPrivateHosts)
743
- await assertPublicUrl(url);
744
- const res = await fetch(url);
791
+ const res = await fetchGuarded(url, options.allowPrivateHosts ?? false);
745
792
  if (!res.ok)
746
793
  throw new Error(`HTTP ${res.status}`);
747
794
  const headerMime = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() ?? "";
@@ -759,7 +806,7 @@ async function fetchSource(pathOrUrl, options) {
759
806
  }
760
807
  const root = resolve(options.baseDir);
761
808
  const target = resolve(root, pathOrUrl);
762
- if (target !== root && !target.startsWith(root + sep)) {
809
+ if (!isWithinDir(root, target)) {
763
810
  throw new Error("path escapes the allowed directory");
764
811
  }
765
812
  const info = await stat(target).catch(() => null);
@@ -821,7 +868,6 @@ function mountViewFile(server, options = {}) {
821
868
  });
822
869
  }
823
870
  export {
824
- withJsonCoercion,
825
871
  validateMcpSchemas,
826
872
  resolveMedia,
827
873
  mountViewFile,
@@ -832,6 +878,7 @@ export {
832
878
  createStdioMcpServer,
833
879
  createMcpHandler,
834
880
  collectTools,
881
+ coerceJsonArgs,
835
882
  buildToolManifest,
836
883
  buildMcpServer
837
884
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Contract-first backend framework — one defineContract() into an HTTP API, MCP tools, AI-agent tools and a typed client. Bun and Node.",
5
5
  "keywords": [
6
6
  "bun",
@@ -1,38 +0,0 @@
1
- // src/server/request.ts
2
- function generateTraceId() {
3
- return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
4
- }
5
- function resolveTraceId(req) {
6
- const header = req.headers.get("x-request-id") ?? req.headers.get("x-trace-id");
7
- const trimmed = header?.trim();
8
- if (trimmed && trimmed.length <= 128 && /^[\w.-]+$/.test(trimmed)) {
9
- return trimmed;
10
- }
11
- return generateTraceId();
12
- }
13
- function extractIp(req) {
14
- const forwarded = req.headers.get("x-forwarded-for");
15
- if (forwarded)
16
- return (forwarded.split(",")[0] ?? "").trim().replace("::ffff:", "");
17
- const realIp = req.headers.get("x-real-ip");
18
- if (realIp)
19
- return realIp.trim().replace("::ffff:", "");
20
- return "";
21
- }
22
- function getClientInfo(req) {
23
- return {
24
- ipAddress: extractIp(req) || undefined,
25
- userAgent: req.headers.get("user-agent") ?? undefined
26
- };
27
- }
28
- function parseQueryParams(url) {
29
- const query = {};
30
- for (const key of new Set(url.searchParams.keys())) {
31
- const values = url.searchParams.getAll(key);
32
- const [first] = values;
33
- query[key] = values.length === 1 && first !== undefined ? first : values;
34
- }
35
- return query;
36
- }
37
-
38
- export { generateTraceId, resolveTraceId, extractIp, getClientInfo, parseQueryParams };