stitchkit 0.45.0 → 0.46.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 (100) hide show
  1. package/README.md +1 -1
  2. package/dist/browser/http.d.ts.map +1 -1
  3. package/dist/browser/socket-io.d.ts +3 -1
  4. package/dist/browser/socket-io.d.ts.map +1 -1
  5. package/dist/cli.js +3 -4
  6. package/dist/contract/errors.d.ts +1 -0
  7. package/dist/contract/errors.d.ts.map +1 -1
  8. package/dist/contract/index.js +2 -2
  9. package/dist/{index-yh0p5ddn.js → index-5s8b7z6q.js} +31 -11
  10. package/dist/{index-mvjyq9h9.js → index-62pqb23z.js} +309 -144
  11. package/dist/index-c40tkxcd.js +181 -0
  12. package/dist/{index-r6czv7yt.js → index-pwyedf7b.js} +4 -3
  13. package/dist/{index-6jypn22c.js → index-r1qp4rve.js} +9 -1
  14. package/dist/{index-s4qsmgwe.js → index-tss6bk5c.js} +4 -1
  15. package/dist/{index-36f1xkdb.js → index-w1s873ng.js} +2 -2
  16. package/dist/index-yxpe3phd.js +63 -0
  17. package/dist/{index-w0mec79h.js → index-zwqty9zf.js} +261 -281
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +129 -51
  21. package/dist/internal/errors.d.ts.map +1 -1
  22. package/dist/internal/secure-fetch.d.ts +23 -3
  23. package/dist/internal/secure-fetch.d.ts.map +1 -1
  24. package/dist/internal/typed.d.ts +7 -0
  25. package/dist/internal/typed.d.ts.map +1 -1
  26. package/dist/internal/within-dir.d.ts +6 -0
  27. package/dist/internal/within-dir.d.ts.map +1 -1
  28. package/dist/logger.d.ts +8 -0
  29. package/dist/logger.d.ts.map +1 -0
  30. package/dist/node.js +3 -4
  31. package/dist/observability/audit.d.ts.map +1 -1
  32. package/dist/observability/event.d.ts +7 -0
  33. package/dist/observability/event.d.ts.map +1 -1
  34. package/dist/observability/index.js +54 -125
  35. package/dist/observability/sanitize.d.ts +8 -1
  36. package/dist/observability/sanitize.d.ts.map +1 -1
  37. package/dist/observability/trace.d.ts +3 -1
  38. package/dist/observability/trace.d.ts.map +1 -1
  39. package/dist/react/cache-bridge.d.ts +4 -0
  40. package/dist/react/cache-bridge.d.ts.map +1 -1
  41. package/dist/react.js +46 -5
  42. package/dist/realtime/contract.d.ts +4 -1
  43. package/dist/realtime/contract.d.ts.map +1 -1
  44. package/dist/realtime/rejection.d.ts +10 -0
  45. package/dist/realtime/rejection.d.ts.map +1 -0
  46. package/dist/realtime/socket.d.ts +14 -1
  47. package/dist/realtime/socket.d.ts.map +1 -1
  48. package/dist/server/cache.d.ts +5 -1
  49. package/dist/server/cache.d.ts.map +1 -1
  50. package/dist/server/error-hook.d.ts +1 -0
  51. package/dist/server/error-hook.d.ts.map +1 -1
  52. package/dist/server/file.d.ts +6 -0
  53. package/dist/server/file.d.ts.map +1 -1
  54. package/dist/server/implement.d.ts.map +1 -1
  55. package/dist/server/index.d.ts +2 -3
  56. package/dist/server/index.d.ts.map +1 -1
  57. package/dist/server/index.js +108 -11
  58. package/dist/server/middleware/cors.d.ts.map +1 -1
  59. package/dist/server/openapi.d.ts.map +1 -1
  60. package/dist/server/realtime.d.ts +2 -0
  61. package/dist/server/realtime.d.ts.map +1 -1
  62. package/dist/server/router.d.ts +0 -8
  63. package/dist/server/router.d.ts.map +1 -1
  64. package/dist/server/stream.d.ts.map +1 -1
  65. package/dist/server/types.d.ts +2 -7
  66. package/dist/server/types.d.ts.map +1 -1
  67. package/dist/tools/cli-args.d.ts +11 -3
  68. package/dist/tools/cli-args.d.ts.map +1 -1
  69. package/dist/tools/cli.d.ts +5 -0
  70. package/dist/tools/cli.d.ts.map +1 -1
  71. package/dist/tools/coerce.d.ts.map +1 -1
  72. package/dist/tools/json-schema.d.ts.map +1 -1
  73. package/dist/tools/mcp-handler.d.ts +4 -3
  74. package/dist/tools/mcp-handler.d.ts.map +1 -1
  75. package/dist/tools/mcp-prepare.d.ts +95 -0
  76. package/dist/tools/mcp-prepare.d.ts.map +1 -0
  77. package/dist/tools/mcp-round.d.ts +4 -0
  78. package/dist/tools/mcp-round.d.ts.map +1 -1
  79. package/dist/tools/mcp-stdio.d.ts +2 -1
  80. package/dist/tools/mcp-stdio.d.ts.map +1 -1
  81. package/dist/tools/mcp.d.ts +2 -78
  82. package/dist/tools/mcp.d.ts.map +1 -1
  83. package/dist/tools/mount-download.d.ts +5 -0
  84. package/dist/tools/mount-download.d.ts.map +1 -1
  85. package/dist/tools/native-mcp.d.ts +1 -1
  86. package/dist/tools/native-mcp.d.ts.map +1 -1
  87. package/dist/tools/oauth-provider.d.ts +20 -1
  88. package/dist/tools/oauth-provider.d.ts.map +1 -1
  89. package/dist/tools/tool-logger.d.ts +1 -1
  90. package/dist/tools/tool-logger.d.ts.map +1 -1
  91. package/dist/tools/view-file.d.ts +9 -0
  92. package/dist/tools/view-file.d.ts.map +1 -1
  93. package/dist/tools.d.ts +2 -1
  94. package/dist/tools.d.ts.map +1 -1
  95. package/dist/tools.js +342 -214
  96. package/llms-full.txt +107 -19
  97. package/llms.txt +1 -1
  98. package/package.json +2 -2
  99. package/dist/index-frfyw9fa.js +0 -33
  100. package/dist/index-x3fcszf8.js +0 -8
@@ -2,14 +2,12 @@ import {
2
2
  assertCorsConfig,
3
3
  corsHeaders,
4
4
  corsPreflightResponse
5
- } from "./index-6jypn22c.js";
6
- import {
7
- isWithinDir
8
- } from "./index-x3fcszf8.js";
5
+ } from "./index-r1qp4rve.js";
9
6
  import {
10
7
  AppError,
11
8
  __require,
12
9
  badRequest,
10
+ callRuntimeHandler,
13
11
  errorCode,
14
12
  extractIp,
15
13
  getClientInfo,
@@ -29,8 +27,9 @@ import {
29
27
  setRequestEndpoint,
30
28
  setRequestError,
31
29
  typedEntries,
32
- validateDeclaredOutput
33
- } from "./index-yh0p5ddn.js";
30
+ validateDeclaredOutput,
31
+ zodIssues
32
+ } from "./index-5s8b7z6q.js";
34
33
 
35
34
  // src/server/multipart.ts
36
35
  var DEFAULT_MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
@@ -85,244 +84,6 @@ async function parseMultipart(req, fileField, fieldsSchema, maxBytes = DEFAULT_M
85
84
  return { file, fields: fieldsSchema ? fieldsSchema.parse(fields) : fields };
86
85
  }
87
86
 
88
- // src/server/router.ts
89
- import { readFile, stat } from "node:fs/promises";
90
- import { resolve } from "node:path";
91
-
92
- // src/server/mime.ts
93
- import { extname } from "node:path";
94
- var MIME_BY_EXT = {
95
- ".html": "text/html; charset=utf-8",
96
- ".css": "text/css; charset=utf-8",
97
- ".js": "text/javascript; charset=utf-8",
98
- ".mjs": "text/javascript; charset=utf-8",
99
- ".json": "application/json; charset=utf-8",
100
- ".svg": "image/svg+xml",
101
- ".png": "image/png",
102
- ".jpg": "image/jpeg",
103
- ".jpeg": "image/jpeg",
104
- ".gif": "image/gif",
105
- ".webp": "image/webp",
106
- ".avif": "image/avif",
107
- ".ico": "image/x-icon",
108
- ".woff2": "font/woff2",
109
- ".txt": "text/plain; charset=utf-8",
110
- ".wasm": "application/wasm",
111
- ".mp4": "video/mp4",
112
- ".webm": "video/webm",
113
- ".mov": "video/quicktime",
114
- ".ogv": "video/ogg",
115
- ".mp3": "audio/mpeg",
116
- ".m4a": "audio/mp4",
117
- ".wav": "audio/wav",
118
- ".flac": "audio/flac",
119
- ".oga": "audio/ogg",
120
- ".ogg": "audio/ogg",
121
- ".pdf": "application/pdf",
122
- ".zip": "application/zip"
123
- };
124
- function mimeForPath(path) {
125
- return MIME_BY_EXT[extname(path).toLowerCase()] ?? "application/octet-stream";
126
- }
127
-
128
- // src/server/router.ts
129
- function joinPath(...parts) {
130
- const joined = parts.filter(Boolean).map((part) => part.replace(/^\/+|\/+$/g, "")).filter(Boolean).join("/");
131
- return `/${joined}`;
132
- }
133
- function matchSegments(patternSegments, requestSegments) {
134
- const wildcardSegment = patternSegments.at(-1);
135
- const wildcardName = wildcardSegment?.startsWith("*") ? wildcardSegment.slice(1) : null;
136
- const prefixLength = wildcardName ? patternSegments.length - 1 : patternSegments.length;
137
- if (wildcardName) {
138
- if (requestSegments.length < prefixLength)
139
- return null;
140
- } else if (patternSegments.length !== requestSegments.length) {
141
- return null;
142
- }
143
- const params = {};
144
- for (const [i, pattern] of patternSegments.slice(0, prefixLength).entries()) {
145
- const actual = requestSegments[i];
146
- if (actual === undefined)
147
- return null;
148
- if (pattern.startsWith(":")) {
149
- params[pattern.slice(1)] = decodeURIComponent(actual);
150
- } else if (pattern !== actual) {
151
- return null;
152
- }
153
- }
154
- if (wildcardName) {
155
- params[wildcardName] = requestSegments.slice(prefixLength).map(decodeURIComponent).join("/");
156
- }
157
- return params;
158
- }
159
- function segmentRank(segment) {
160
- if (segment?.startsWith("*"))
161
- return 2;
162
- if (segment?.startsWith(":"))
163
- return 1;
164
- return 0;
165
- }
166
- function buildRouteMap(groups) {
167
- const map = new Map;
168
- for (const { prefix, service, hooks } of groups) {
169
- for (const [, method] of Object.entries(service.methods)) {
170
- if (method.expose && !method.expose.includes("HTTP"))
171
- continue;
172
- const servicePath = joinPath("/", service.prefix, method.path === "/" ? "" : method.path);
173
- const fullPath = prefix ? joinPath(prefix, servicePath) : servicePath;
174
- const segments = fullPath.split("/").filter(Boolean);
175
- const entries = map.get(method.method) ?? [];
176
- entries.push({ method, pattern: fullPath, segments, groupHooks: hooks });
177
- map.set(method.method, entries);
178
- }
179
- }
180
- for (const [, entries] of map) {
181
- entries.sort((a, b) => {
182
- const len = Math.min(a.segments.length, b.segments.length);
183
- for (let i = 0;i < len; i++) {
184
- const rankDifference = segmentRank(a.segments[i]) - segmentRank(b.segments[i]);
185
- if (rankDifference !== 0)
186
- return rankDifference;
187
- }
188
- return a.segments.length - b.segments.length;
189
- });
190
- }
191
- return map;
192
- }
193
- function matchRoute(routeMap, httpMethod, pathname) {
194
- const entries = routeMap.get(httpMethod);
195
- if (!entries)
196
- return null;
197
- const requestSegments = pathname.split("/").filter(Boolean);
198
- for (const entry of entries) {
199
- const pathParams = matchSegments(entry.segments, requestSegments);
200
- if (pathParams) {
201
- return {
202
- method: entry.method,
203
- pathParams,
204
- groupHooks: entry.groupHooks
205
- };
206
- }
207
- }
208
- return null;
209
- }
210
- function allowedMethods(routeMap, pathname) {
211
- const requestSegments = pathname.split("/").filter(Boolean);
212
- const methods = [];
213
- for (const [method, entries] of routeMap) {
214
- for (const entry of entries) {
215
- if (matchSegments(entry.segments, requestSegments)) {
216
- methods.push(method);
217
- break;
218
- }
219
- }
220
- }
221
- return methods;
222
- }
223
- function validateRoutes(routeMap) {
224
- for (const [method, entries] of routeMap) {
225
- const seen = new Map;
226
- for (const entry of entries) {
227
- parseTrailingWildcard(entry.pattern);
228
- const normalized = entry.segments.map((s) => s.startsWith(":") ? ":param" : s.startsWith("*") ? "*wildcard" : s).join("/");
229
- const key = `${method} /${normalized}`;
230
- const existing = seen.get(key);
231
- if (existing) {
232
- throw new Error(`Duplicate route: ${method} ${entry.pattern} conflicts with ${existing}`);
233
- }
234
- seen.set(key, entry.pattern);
235
- }
236
- }
237
- }
238
- function findShadowedRoutes(routeMap, rawRoutes) {
239
- if (!rawRoutes || rawRoutes.length === 0)
240
- return [];
241
- const shadowed = [];
242
- for (const [httpMethod, entries] of routeMap) {
243
- for (const entry of entries) {
244
- const probe = `/${entry.segments.map((segment) => {
245
- if (segment.startsWith(":"))
246
- return "__param__";
247
- if (segment.startsWith("*"))
248
- return "__wildcard__";
249
- return segment;
250
- }).join("/")}`;
251
- const match = matchRawRoute(rawRoutes, httpMethod, probe);
252
- if (!match)
253
- continue;
254
- shadowed.push({
255
- pattern: `${httpMethod} ${entry.pattern}`,
256
- endpoint: `${entry.method.serviceName}.${entry.method.key}`,
257
- rawRoute: `${match.route.method} ${match.route.path}`,
258
- scope: entry.method.scope
259
- });
260
- }
261
- }
262
- return shadowed;
263
- }
264
- function matchRawRoute(rawRoutes, httpMethod, pathname) {
265
- for (const route of rawRoutes) {
266
- if (route.method !== "ALL" && route.method !== httpMethod)
267
- continue;
268
- if (parseTrailingWildcard(route.path)) {
269
- const routeSegs = route.path.split("/").filter(Boolean);
270
- const pathSegs = pathname.split("/").filter(Boolean);
271
- const params = matchSegments(routeSegs, pathSegs);
272
- if (params)
273
- return { route, params };
274
- continue;
275
- }
276
- if (route.path.includes("/:")) {
277
- const routeSegs = route.path.split("/").filter(Boolean);
278
- const pathSegs = pathname.split("/").filter(Boolean);
279
- const params = matchSegments(routeSegs, pathSegs);
280
- if (params)
281
- return { route, params };
282
- continue;
283
- }
284
- if (route.path === pathname)
285
- return { route, params: {} };
286
- }
287
- return null;
288
- }
289
- function validateRawRoutes(rawRoutes) {
290
- for (const route of rawRoutes ?? [])
291
- parseTrailingWildcard(route.path);
292
- }
293
- function staticRoute(prefix, dir) {
294
- const cleanPrefix = prefix.replace(/\/+$/, "");
295
- const root = resolve(dir.replace(/\/+$/, ""));
296
- return {
297
- method: "GET",
298
- path: `${cleanPrefix}/*filePath`,
299
- handler: async (req) => {
300
- const pathname = new URL(req.url).pathname;
301
- let rel;
302
- try {
303
- rel = decodeURIComponent(pathname.slice(cleanPrefix.length)).replace(/^\/+/, "");
304
- } catch {
305
- return new Response("Bad request", { status: 400 });
306
- }
307
- const target = resolve(root, rel);
308
- if (!isWithinDir(root, target)) {
309
- return new Response("Forbidden", { status: 403 });
310
- }
311
- const info = await stat(target).catch(() => null);
312
- if (!info?.isFile()) {
313
- return new Response("Not found", { status: 404 });
314
- }
315
- const body = await readFile(target);
316
- return new Response(body, {
317
- headers: {
318
- "Content-Type": mimeForPath(target),
319
- "X-Content-Type-Options": "nosniff"
320
- }
321
- });
322
- }
323
- };
324
- }
325
-
326
87
  // src/server/request-body.ts
327
88
  function assertJsonBodyLimit(maxBytes, owner) {
328
89
  if (maxBytes !== undefined && (!Number.isSafeInteger(maxBytes) || maxBytes <= 0)) {
@@ -662,6 +423,172 @@ function applyResponseMetadata(target, metadata, endpointIdentity) {
662
423
  }
663
424
  }
664
425
 
426
+ // src/server/router.ts
427
+ function joinPath(...parts) {
428
+ const joined = parts.filter(Boolean).map((part) => part.replace(/^\/+|\/+$/g, "")).filter(Boolean).join("/");
429
+ return `/${joined}`;
430
+ }
431
+ function matchSegments(patternSegments, requestSegments) {
432
+ const wildcardSegment = patternSegments.at(-1);
433
+ const wildcardName = wildcardSegment?.startsWith("*") ? wildcardSegment.slice(1) : null;
434
+ const prefixLength = wildcardName ? patternSegments.length - 1 : patternSegments.length;
435
+ if (wildcardName) {
436
+ if (requestSegments.length < prefixLength)
437
+ return null;
438
+ } else if (patternSegments.length !== requestSegments.length) {
439
+ return null;
440
+ }
441
+ const params = {};
442
+ for (const [i, pattern] of patternSegments.slice(0, prefixLength).entries()) {
443
+ const actual = requestSegments[i];
444
+ if (actual === undefined)
445
+ return null;
446
+ if (pattern.startsWith(":")) {
447
+ params[pattern.slice(1)] = decodeURIComponent(actual);
448
+ } else if (pattern !== actual) {
449
+ return null;
450
+ }
451
+ }
452
+ if (wildcardName) {
453
+ params[wildcardName] = requestSegments.slice(prefixLength).map(decodeURIComponent).join("/");
454
+ }
455
+ return params;
456
+ }
457
+ function segmentRank(segment) {
458
+ if (segment?.startsWith("*"))
459
+ return 2;
460
+ if (segment?.startsWith(":"))
461
+ return 1;
462
+ return 0;
463
+ }
464
+ function buildRouteMap(groups) {
465
+ const map = new Map;
466
+ for (const { prefix, service, hooks } of groups) {
467
+ for (const [, method] of Object.entries(service.methods)) {
468
+ if (method.expose && !method.expose.includes("HTTP"))
469
+ continue;
470
+ const servicePath = joinPath("/", service.prefix, method.path === "/" ? "" : method.path);
471
+ const fullPath = prefix ? joinPath(prefix, servicePath) : servicePath;
472
+ const segments = fullPath.split("/").filter(Boolean);
473
+ const entries = map.get(method.method) ?? [];
474
+ entries.push({ method, pattern: fullPath, segments, groupHooks: hooks });
475
+ map.set(method.method, entries);
476
+ }
477
+ }
478
+ for (const [, entries] of map) {
479
+ entries.sort((a, b) => {
480
+ const len = Math.min(a.segments.length, b.segments.length);
481
+ for (let i = 0;i < len; i++) {
482
+ const rankDifference = segmentRank(a.segments[i]) - segmentRank(b.segments[i]);
483
+ if (rankDifference !== 0)
484
+ return rankDifference;
485
+ }
486
+ return a.segments.length - b.segments.length;
487
+ });
488
+ }
489
+ return map;
490
+ }
491
+ function matchRoute(routeMap, httpMethod, pathname) {
492
+ const entries = routeMap.get(httpMethod);
493
+ if (!entries)
494
+ return null;
495
+ const requestSegments = pathname.split("/").filter(Boolean);
496
+ for (const entry of entries) {
497
+ const pathParams = matchSegments(entry.segments, requestSegments);
498
+ if (pathParams) {
499
+ return {
500
+ method: entry.method,
501
+ pathParams,
502
+ groupHooks: entry.groupHooks
503
+ };
504
+ }
505
+ }
506
+ return null;
507
+ }
508
+ function allowedMethods(routeMap, pathname) {
509
+ const requestSegments = pathname.split("/").filter(Boolean);
510
+ const methods = [];
511
+ for (const [method, entries] of routeMap) {
512
+ for (const entry of entries) {
513
+ if (matchSegments(entry.segments, requestSegments)) {
514
+ methods.push(method);
515
+ break;
516
+ }
517
+ }
518
+ }
519
+ return methods;
520
+ }
521
+ function validateRoutes(routeMap) {
522
+ for (const [method, entries] of routeMap) {
523
+ const seen = new Map;
524
+ for (const entry of entries) {
525
+ parseTrailingWildcard(entry.pattern);
526
+ const normalized = entry.segments.map((s) => s.startsWith(":") ? ":param" : s.startsWith("*") ? "*wildcard" : s).join("/");
527
+ const key = `${method} /${normalized}`;
528
+ const existing = seen.get(key);
529
+ if (existing) {
530
+ throw new Error(`Duplicate route: ${method} ${entry.pattern} conflicts with ${existing}`);
531
+ }
532
+ seen.set(key, entry.pattern);
533
+ }
534
+ }
535
+ }
536
+ function findShadowedRoutes(routeMap, rawRoutes) {
537
+ if (!rawRoutes || rawRoutes.length === 0)
538
+ return [];
539
+ const shadowed = [];
540
+ for (const [httpMethod, entries] of routeMap) {
541
+ for (const entry of entries) {
542
+ const probe = `/${entry.segments.map((segment) => {
543
+ if (segment.startsWith(":"))
544
+ return "__param__";
545
+ if (segment.startsWith("*"))
546
+ return "__wildcard__";
547
+ return segment;
548
+ }).join("/")}`;
549
+ const match = matchRawRoute(rawRoutes, httpMethod, probe);
550
+ if (!match)
551
+ continue;
552
+ shadowed.push({
553
+ pattern: `${httpMethod} ${entry.pattern}`,
554
+ endpoint: `${entry.method.serviceName}.${entry.method.key}`,
555
+ rawRoute: `${match.route.method} ${match.route.path}`,
556
+ scope: entry.method.scope
557
+ });
558
+ }
559
+ }
560
+ return shadowed;
561
+ }
562
+ function matchRawRoute(rawRoutes, httpMethod, pathname) {
563
+ for (const route of rawRoutes) {
564
+ if (route.method !== "ALL" && route.method !== httpMethod)
565
+ continue;
566
+ if (parseTrailingWildcard(route.path)) {
567
+ const routeSegs = route.path.split("/").filter(Boolean);
568
+ const pathSegs = pathname.split("/").filter(Boolean);
569
+ const params = matchSegments(routeSegs, pathSegs);
570
+ if (params)
571
+ return { route, params };
572
+ continue;
573
+ }
574
+ if (route.path.includes("/:")) {
575
+ const routeSegs = route.path.split("/").filter(Boolean);
576
+ const pathSegs = pathname.split("/").filter(Boolean);
577
+ const params = matchSegments(routeSegs, pathSegs);
578
+ if (params)
579
+ return { route, params };
580
+ continue;
581
+ }
582
+ if (route.path === pathname)
583
+ return { route, params: {} };
584
+ }
585
+ return null;
586
+ }
587
+ function validateRawRoutes(rawRoutes) {
588
+ for (const route of rawRoutes ?? [])
589
+ parseTrailingWildcard(route.path);
590
+ }
591
+
665
592
  // src/server/create.ts
666
593
  var BODY_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
667
594
  function createHandler(config) {
@@ -1018,6 +945,9 @@ function implement(contract, handlers) {
1018
945
  const groupScope = contract.meta.scope ?? "public";
1019
946
  for (const [key, endpoint] of typedEntries(contract.endpoints)) {
1020
947
  const typedHandler = handlers[key];
948
+ if (typeof typedHandler !== "function") {
949
+ throw new Error(`[stitchkit] implement: missing handler for "${contract.meta.prefix}.${String(key)}"`);
950
+ }
1021
951
  methods[String(key)] = {
1022
952
  method: endpoint.method,
1023
953
  path: endpoint.path,
@@ -1042,7 +972,7 @@ function implement(contract, handlers) {
1042
972
  rawBody: endpoint.rawBody,
1043
973
  responseMeta: endpoint.responseMeta,
1044
974
  contentType: "contentType" in endpoint ? endpoint.contentType : undefined,
1045
- handler: (ctx) => typedHandler(ctx)
975
+ handler: (ctx) => callRuntimeHandler(typedHandler, ctx)
1046
976
  };
1047
977
  }
1048
978
  return {
@@ -1056,9 +986,25 @@ function createImplement() {
1056
986
  return (contract, handlers) => implement(contract, handlers);
1057
987
  }
1058
988
 
1059
- // src/realtime/socket.ts
989
+ // src/realtime/rejection.ts
1060
990
  import { z } from "zod";
1061
- var AcknowledgementCallbackSchema = z.function();
991
+ function realtimeContractViolation(options) {
992
+ const issues = options.cause instanceof z.ZodError ? zodIssues(options.cause) : undefined;
993
+ const reason = options.reason.replaceAll("-", " ");
994
+ const error = new AppError("REALTIME_CONTRACT_VIOLATION", `Realtime event "${options.event}" (${options.direction}, ${options.phase}): ${reason}`, 500, {
995
+ event: options.event,
996
+ direction: options.direction,
997
+ phase: options.phase,
998
+ reason: options.reason,
999
+ fault: options.fault,
1000
+ ...issues !== undefined && { issues }
1001
+ });
1002
+ if (options.cause !== undefined)
1003
+ error.cause = options.cause;
1004
+ return { ...options, error };
1005
+ }
1006
+
1007
+ // src/realtime/socket.ts
1062
1008
  function method(target, name) {
1063
1009
  const value = Reflect.get(target, name);
1064
1010
  if (typeof value !== "function") {
@@ -1077,15 +1023,26 @@ function splitWireArguments(definition, args) {
1077
1023
  acknowledgement: (...callbackArgs) => Reflect.apply(acknowledgement, undefined, callbackArgs)
1078
1024
  };
1079
1025
  }
1080
- function eventDefinition(registry, event) {
1026
+ function eventDefinition(registry, event, direction) {
1081
1027
  const definition = registry[event];
1082
- if (!definition)
1083
- throw new Error(`Unknown realtime event "${event}"`);
1028
+ if (!definition) {
1029
+ throw realtimeContractViolation({
1030
+ event,
1031
+ direction,
1032
+ phase: "arguments",
1033
+ reason: "unknown-event",
1034
+ fault: "local"
1035
+ }).error;
1036
+ }
1084
1037
  return definition;
1085
1038
  }
1086
- function reportRejected(rejected, hook) {
1039
+ function reportRejected(rejected, hook, logger) {
1087
1040
  if (!hook) {
1088
- console.error(`[stitchkit] rejected realtime ${rejected.direction} event "${rejected.event}" (${rejected.phase})`, rejected.error);
1041
+ if (logger) {
1042
+ logger.warn(rejected.error.message, rejected.error.details);
1043
+ return;
1044
+ }
1045
+ console.warn(`[stitchkit] rejected realtime ${rejected.direction} event "${rejected.event}" (${rejected.phase})`, rejected.error);
1089
1046
  return;
1090
1047
  }
1091
1048
  Promise.resolve(hook(rejected)).catch((error) => {
@@ -1099,36 +1056,35 @@ function createValidatedRealtimeSocket({
1099
1056
  inboundDirection,
1100
1057
  outboundDirection,
1101
1058
  onRejected,
1059
+ logger,
1102
1060
  subscribe
1103
1061
  }) {
1104
- const onTarget = method(target, "on");
1105
- const offTarget = subscribe ? undefined : method(target, "off");
1106
1062
  const emitTarget = method(target, "emit");
1107
1063
  return {
1108
1064
  on: (event, handler) => {
1109
- const definition = eventDefinition(inbound, event);
1065
+ const definition = eventDefinition(inbound, event, inboundDirection);
1110
1066
  const wrapped = (...wireArgs) => {
1111
1067
  const { values, acknowledgement } = splitWireArguments(definition, wireArgs);
1112
1068
  const parsed = definition.args.safeParse(values);
1113
1069
  if (!parsed.success) {
1114
- reportRejected({
1070
+ reportRejected(realtimeContractViolation({
1115
1071
  event,
1116
1072
  direction: inboundDirection,
1117
1073
  phase: "arguments",
1118
- error: parsed.error
1119
- }, onRejected);
1074
+ reason: "invalid-arguments",
1075
+ fault: "peer",
1076
+ cause: parsed.error
1077
+ }), onRejected, logger);
1120
1078
  return;
1121
1079
  }
1122
1080
  if (definition.ack && !acknowledgement) {
1123
- const callback = AcknowledgementCallbackSchema.safeParse(undefined);
1124
- if (!callback.success) {
1125
- reportRejected({
1126
- event,
1127
- direction: inboundDirection,
1128
- phase: "acknowledgement",
1129
- error: callback.error
1130
- }, onRejected);
1131
- }
1081
+ reportRejected(realtimeContractViolation({
1082
+ event,
1083
+ direction: inboundDirection,
1084
+ phase: "acknowledgement",
1085
+ reason: "missing-acknowledgement",
1086
+ fault: "peer"
1087
+ }), onRejected, logger);
1132
1088
  return;
1133
1089
  }
1134
1090
  const applicationArgs = [...parsed.data];
@@ -1137,12 +1093,14 @@ function createValidatedRealtimeSocket({
1137
1093
  const ack = definition.ack?.safeParse(value);
1138
1094
  if (!ack?.success) {
1139
1095
  if (ack) {
1140
- reportRejected({
1096
+ reportRejected(realtimeContractViolation({
1141
1097
  event,
1142
1098
  direction: outboundDirection,
1143
1099
  phase: "acknowledgement",
1144
- error: ack.error
1145
- }, onRejected);
1100
+ reason: "invalid-acknowledgement-value",
1101
+ fault: "local",
1102
+ cause: ack.error
1103
+ }), onRejected, logger);
1146
1104
  }
1147
1105
  return;
1148
1106
  }
@@ -1153,30 +1111,50 @@ function createValidatedRealtimeSocket({
1153
1111
  };
1154
1112
  if (subscribe)
1155
1113
  return subscribe(event, wrapped);
1114
+ const onTarget = method(target, "on");
1115
+ const offTarget = method(target, "off");
1156
1116
  onTarget(event, wrapped);
1157
1117
  return () => {
1158
- offTarget?.(event, wrapped);
1118
+ offTarget(event, wrapped);
1159
1119
  };
1160
1120
  },
1161
1121
  emit: (event, ...args) => {
1162
- const definition = eventDefinition(outbound, event);
1122
+ const definition = eventDefinition(outbound, event, outboundDirection);
1163
1123
  const { values, acknowledgement } = splitWireArguments(definition, args);
1164
- const parsed = definition.args.parse(values);
1124
+ const parsed = definition.args.safeParse(values);
1125
+ if (!parsed.success) {
1126
+ throw realtimeContractViolation({
1127
+ event,
1128
+ direction: outboundDirection,
1129
+ phase: "arguments",
1130
+ reason: "invalid-arguments",
1131
+ fault: "local",
1132
+ cause: parsed.error
1133
+ }).error;
1134
+ }
1165
1135
  if (definition.ack && !acknowledgement) {
1166
- AcknowledgementCallbackSchema.parse(undefined);
1136
+ throw realtimeContractViolation({
1137
+ event,
1138
+ direction: outboundDirection,
1139
+ phase: "acknowledgement",
1140
+ reason: "missing-acknowledgement",
1141
+ fault: "local"
1142
+ }).error;
1167
1143
  }
1168
- const wireArgs = [...parsed];
1144
+ const wireArgs = [...parsed.data];
1169
1145
  if (definition.ack && acknowledgement) {
1170
1146
  wireArgs.push((value) => {
1171
1147
  const ack = definition.ack?.safeParse(value);
1172
1148
  if (!ack?.success) {
1173
1149
  if (ack) {
1174
- reportRejected({
1150
+ reportRejected(realtimeContractViolation({
1175
1151
  event,
1176
1152
  direction: inboundDirection,
1177
1153
  phase: "acknowledgement",
1178
- error: ack.error
1179
- }, onRejected);
1154
+ reason: "invalid-acknowledgement-value",
1155
+ fault: "peer",
1156
+ cause: ack.error
1157
+ }), onRejected, logger);
1180
1158
  }
1181
1159
  return;
1182
1160
  }
@@ -1196,7 +1174,8 @@ function bindRealtimeServer(contract, handle, options = {}) {
1196
1174
  outbound: contract.serverToClient,
1197
1175
  inboundDirection: "server-inbound",
1198
1176
  outboundDirection: "server-outbound",
1199
- onRejected: options.onRejected
1177
+ onRejected: options.onRejected,
1178
+ logger: options.logger
1200
1179
  });
1201
1180
  const broadcast = outbound(handle.io);
1202
1181
  return {
@@ -1208,7 +1187,8 @@ function bindRealtimeServer(contract, handle, options = {}) {
1208
1187
  outbound: contract.serverToClient,
1209
1188
  inboundDirection: "server-inbound",
1210
1189
  outboundDirection: "server-outbound",
1211
- onRejected: options.onRejected
1190
+ onRejected: options.onRejected,
1191
+ logger: options.logger
1212
1192
  });
1213
1193
  handler({
1214
1194
  raw,
@@ -1372,4 +1352,4 @@ function socketIoLane(websocket) {
1372
1352
  });
1373
1353
  }
1374
1354
 
1375
- export { parseMultipart, mimeForPath, staticRoute, createHandler, implement, createImplement, bindRealtimeServer, webSocketLane, composeWebSocketHandlers, createSocketIOServer, socketIoLane };
1355
+ export { parseMultipart, createHandler, implement, createImplement, bindRealtimeServer, webSocketLane, composeWebSocketHandlers, createSocketIOServer, socketIoLane };
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export type { RealtimeClient, RealtimeClientOptions, SocketEventMap, SocketIOCli
4
4
  export { createRealtimeClient, createSocketIOClient } from './browser/socket-io';
5
5
  export { type ParseSSEOptions, parseSSE } from './browser/stream';
6
6
  export * from './contract';
7
+ export type { StitchLogger } from './logger';
7
8
  export { childSpan, createTraceContext, formatTraceparent, parseTraceparent, type TraceContext, } from './observability/trace';
8
9
  export * from './realtime';
9
10
  export { createRetainedTopics, type RetainedTopics } from './retained';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,wBAAwB,EACxB,YAAY,EACZ,aAAa,EACb,mBAAmB,EACnB,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,QAAQ,EACR,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,cAAc,EACd,qBAAqB,EACrB,cAAc,EACd,cAAc,EACd,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AACjF,OAAO,EAAE,KAAK,eAAe,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAClE,cAAc,YAAY,CAAC;AAG3B,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,YAAY,GAClB,MAAM,uBAAuB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,oBAAoB,EAAE,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,wBAAwB,EACxB,YAAY,EACZ,aAAa,EACb,mBAAmB,EACnB,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,QAAQ,EACR,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,cAAc,EACd,qBAAqB,EACrB,cAAc,EACd,cAAc,EACd,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AACjF,OAAO,EAAE,KAAK,eAAe,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAClE,cAAc,YAAY,CAAC;AAC3B,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAG7C,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,YAAY,GAClB,MAAM,uBAAuB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,oBAAoB,EAAE,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC"}