stitchkit 0.44.1 → 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 (104) 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 +16 -0
  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-4fyn9f09.js → index-zwqty9zf.js} +396 -246
  18. package/dist/index.d.ts +4 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +266 -17
  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.d.ts +1 -0
  31. package/dist/node.d.ts.map +1 -1
  32. package/dist/node.js +5 -4
  33. package/dist/observability/audit.d.ts.map +1 -1
  34. package/dist/observability/event.d.ts +7 -0
  35. package/dist/observability/event.d.ts.map +1 -1
  36. package/dist/observability/index.js +54 -125
  37. package/dist/observability/sanitize.d.ts +8 -1
  38. package/dist/observability/sanitize.d.ts.map +1 -1
  39. package/dist/observability/trace.d.ts +3 -1
  40. package/dist/observability/trace.d.ts.map +1 -1
  41. package/dist/react/cache-bridge.d.ts +4 -0
  42. package/dist/react/cache-bridge.d.ts.map +1 -1
  43. package/dist/react.js +46 -5
  44. package/dist/realtime/contract.d.ts +39 -0
  45. package/dist/realtime/contract.d.ts.map +1 -0
  46. package/dist/realtime/index.d.ts +3 -0
  47. package/dist/realtime/index.d.ts.map +1 -0
  48. package/dist/realtime/rejection.d.ts +10 -0
  49. package/dist/realtime/rejection.d.ts.map +1 -0
  50. package/dist/realtime/socket.d.ts +30 -0
  51. package/dist/realtime/socket.d.ts.map +1 -0
  52. package/dist/server/cache.d.ts +5 -1
  53. package/dist/server/cache.d.ts.map +1 -1
  54. package/dist/server/error-hook.d.ts +1 -0
  55. package/dist/server/error-hook.d.ts.map +1 -1
  56. package/dist/server/file.d.ts +6 -0
  57. package/dist/server/file.d.ts.map +1 -1
  58. package/dist/server/implement.d.ts.map +1 -1
  59. package/dist/server/index.d.ts +3 -3
  60. package/dist/server/index.d.ts.map +1 -1
  61. package/dist/server/index.js +110 -11
  62. package/dist/server/middleware/cors.d.ts.map +1 -1
  63. package/dist/server/openapi.d.ts.map +1 -1
  64. package/dist/server/realtime.d.ts +24 -0
  65. package/dist/server/realtime.d.ts.map +1 -0
  66. package/dist/server/router.d.ts +0 -8
  67. package/dist/server/router.d.ts.map +1 -1
  68. package/dist/server/stream.d.ts.map +1 -1
  69. package/dist/server/types.d.ts +2 -7
  70. package/dist/server/types.d.ts.map +1 -1
  71. package/dist/tools/cli-args.d.ts +11 -3
  72. package/dist/tools/cli-args.d.ts.map +1 -1
  73. package/dist/tools/cli.d.ts +5 -0
  74. package/dist/tools/cli.d.ts.map +1 -1
  75. package/dist/tools/coerce.d.ts.map +1 -1
  76. package/dist/tools/json-schema.d.ts.map +1 -1
  77. package/dist/tools/mcp-handler.d.ts +4 -3
  78. package/dist/tools/mcp-handler.d.ts.map +1 -1
  79. package/dist/tools/mcp-prepare.d.ts +95 -0
  80. package/dist/tools/mcp-prepare.d.ts.map +1 -0
  81. package/dist/tools/mcp-round.d.ts +4 -0
  82. package/dist/tools/mcp-round.d.ts.map +1 -1
  83. package/dist/tools/mcp-stdio.d.ts +2 -1
  84. package/dist/tools/mcp-stdio.d.ts.map +1 -1
  85. package/dist/tools/mcp.d.ts +2 -78
  86. package/dist/tools/mcp.d.ts.map +1 -1
  87. package/dist/tools/mount-download.d.ts +5 -0
  88. package/dist/tools/mount-download.d.ts.map +1 -1
  89. package/dist/tools/native-mcp.d.ts +1 -1
  90. package/dist/tools/native-mcp.d.ts.map +1 -1
  91. package/dist/tools/oauth-provider.d.ts +20 -1
  92. package/dist/tools/oauth-provider.d.ts.map +1 -1
  93. package/dist/tools/tool-logger.d.ts +1 -1
  94. package/dist/tools/tool-logger.d.ts.map +1 -1
  95. package/dist/tools/view-file.d.ts +9 -0
  96. package/dist/tools/view-file.d.ts.map +1 -1
  97. package/dist/tools.d.ts +2 -1
  98. package/dist/tools.d.ts.map +1 -1
  99. package/dist/tools.js +342 -214
  100. package/llms-full.txt +175 -44
  101. package/llms.txt +1 -1
  102. package/package.json +2 -2
  103. package/dist/index-frfyw9fa.js +0 -33
  104. 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,6 +986,226 @@ function createImplement() {
1056
986
  return (contract, handlers) => implement(contract, handlers);
1057
987
  }
1058
988
 
989
+ // src/realtime/rejection.ts
990
+ import { z } from "zod";
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
1008
+ function method(target, name) {
1009
+ const value = Reflect.get(target, name);
1010
+ if (typeof value !== "function") {
1011
+ throw new Error(`Realtime target does not implement ${name}()`);
1012
+ }
1013
+ return (...args) => Reflect.apply(value, target, args);
1014
+ }
1015
+ function splitWireArguments(definition, args) {
1016
+ if (!definition.ack)
1017
+ return { values: args };
1018
+ const acknowledgement = args.at(-1);
1019
+ if (typeof acknowledgement !== "function")
1020
+ return { values: args };
1021
+ return {
1022
+ values: args.slice(0, -1),
1023
+ acknowledgement: (...callbackArgs) => Reflect.apply(acknowledgement, undefined, callbackArgs)
1024
+ };
1025
+ }
1026
+ function eventDefinition(registry, event, direction) {
1027
+ const definition = registry[event];
1028
+ if (!definition) {
1029
+ throw realtimeContractViolation({
1030
+ event,
1031
+ direction,
1032
+ phase: "arguments",
1033
+ reason: "unknown-event",
1034
+ fault: "local"
1035
+ }).error;
1036
+ }
1037
+ return definition;
1038
+ }
1039
+ function reportRejected(rejected, hook, logger) {
1040
+ if (!hook) {
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);
1046
+ return;
1047
+ }
1048
+ Promise.resolve(hook(rejected)).catch((error) => {
1049
+ console.error("[stitchkit] realtime rejection hook failed", error);
1050
+ });
1051
+ }
1052
+ function createValidatedRealtimeSocket({
1053
+ target,
1054
+ inbound,
1055
+ outbound,
1056
+ inboundDirection,
1057
+ outboundDirection,
1058
+ onRejected,
1059
+ logger,
1060
+ subscribe
1061
+ }) {
1062
+ const emitTarget = method(target, "emit");
1063
+ return {
1064
+ on: (event, handler) => {
1065
+ const definition = eventDefinition(inbound, event, inboundDirection);
1066
+ const wrapped = (...wireArgs) => {
1067
+ const { values, acknowledgement } = splitWireArguments(definition, wireArgs);
1068
+ const parsed = definition.args.safeParse(values);
1069
+ if (!parsed.success) {
1070
+ reportRejected(realtimeContractViolation({
1071
+ event,
1072
+ direction: inboundDirection,
1073
+ phase: "arguments",
1074
+ reason: "invalid-arguments",
1075
+ fault: "peer",
1076
+ cause: parsed.error
1077
+ }), onRejected, logger);
1078
+ return;
1079
+ }
1080
+ if (definition.ack && !acknowledgement) {
1081
+ reportRejected(realtimeContractViolation({
1082
+ event,
1083
+ direction: inboundDirection,
1084
+ phase: "acknowledgement",
1085
+ reason: "missing-acknowledgement",
1086
+ fault: "peer"
1087
+ }), onRejected, logger);
1088
+ return;
1089
+ }
1090
+ const applicationArgs = [...parsed.data];
1091
+ if (definition.ack) {
1092
+ applicationArgs.push((value) => {
1093
+ const ack = definition.ack?.safeParse(value);
1094
+ if (!ack?.success) {
1095
+ if (ack) {
1096
+ reportRejected(realtimeContractViolation({
1097
+ event,
1098
+ direction: outboundDirection,
1099
+ phase: "acknowledgement",
1100
+ reason: "invalid-acknowledgement-value",
1101
+ fault: "local",
1102
+ cause: ack.error
1103
+ }), onRejected, logger);
1104
+ }
1105
+ return;
1106
+ }
1107
+ acknowledgement?.(ack.data);
1108
+ });
1109
+ }
1110
+ Reflect.apply(handler, undefined, applicationArgs);
1111
+ };
1112
+ if (subscribe)
1113
+ return subscribe(event, wrapped);
1114
+ const onTarget = method(target, "on");
1115
+ const offTarget = method(target, "off");
1116
+ onTarget(event, wrapped);
1117
+ return () => {
1118
+ offTarget(event, wrapped);
1119
+ };
1120
+ },
1121
+ emit: (event, ...args) => {
1122
+ const definition = eventDefinition(outbound, event, outboundDirection);
1123
+ const { values, acknowledgement } = splitWireArguments(definition, args);
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
+ }
1135
+ if (definition.ack && !acknowledgement) {
1136
+ throw realtimeContractViolation({
1137
+ event,
1138
+ direction: outboundDirection,
1139
+ phase: "acknowledgement",
1140
+ reason: "missing-acknowledgement",
1141
+ fault: "local"
1142
+ }).error;
1143
+ }
1144
+ const wireArgs = [...parsed.data];
1145
+ if (definition.ack && acknowledgement) {
1146
+ wireArgs.push((value) => {
1147
+ const ack = definition.ack?.safeParse(value);
1148
+ if (!ack?.success) {
1149
+ if (ack) {
1150
+ reportRejected(realtimeContractViolation({
1151
+ event,
1152
+ direction: inboundDirection,
1153
+ phase: "acknowledgement",
1154
+ reason: "invalid-acknowledgement-value",
1155
+ fault: "peer",
1156
+ cause: ack.error
1157
+ }), onRejected, logger);
1158
+ }
1159
+ return;
1160
+ }
1161
+ acknowledgement(ack.data);
1162
+ });
1163
+ }
1164
+ emitTarget(event, ...wireArgs);
1165
+ }
1166
+ };
1167
+ }
1168
+
1169
+ // src/server/realtime.ts
1170
+ function bindRealtimeServer(contract, handle, options = {}) {
1171
+ const outbound = (target) => createValidatedRealtimeSocket({
1172
+ target,
1173
+ inbound: {},
1174
+ outbound: contract.serverToClient,
1175
+ inboundDirection: "server-inbound",
1176
+ outboundDirection: "server-outbound",
1177
+ onRejected: options.onRejected,
1178
+ logger: options.logger
1179
+ });
1180
+ const broadcast = outbound(handle.io);
1181
+ return {
1182
+ onConnection: (handler) => {
1183
+ const listener = (raw) => {
1184
+ const events = createValidatedRealtimeSocket({
1185
+ target: raw,
1186
+ inbound: contract.clientToServer,
1187
+ outbound: contract.serverToClient,
1188
+ inboundDirection: "server-inbound",
1189
+ outboundDirection: "server-outbound",
1190
+ onRejected: options.onRejected,
1191
+ logger: options.logger
1192
+ });
1193
+ handler({
1194
+ raw,
1195
+ events,
1196
+ to: (room) => outbound(raw.to(room))
1197
+ });
1198
+ };
1199
+ handle.io.on("connection", listener);
1200
+ return () => {
1201
+ handle.io.off("connection", listener);
1202
+ };
1203
+ },
1204
+ emit: broadcast.emit,
1205
+ to: (room) => outbound(handle.io.to(room))
1206
+ };
1207
+ }
1208
+
1059
1209
  // src/server/websocket.ts
1060
1210
  function webSocketLane(lane) {
1061
1211
  const { match, handlers } = lane;
@@ -1202,4 +1352,4 @@ function socketIoLane(websocket) {
1202
1352
  });
1203
1353
  }
1204
1354
 
1205
- export { parseMultipart, mimeForPath, staticRoute, createHandler, implement, createImplement, webSocketLane, composeWebSocketHandlers, createSocketIOServer, socketIoLane };
1355
+ export { parseMultipart, createHandler, implement, createImplement, bindRealtimeServer, webSocketLane, composeWebSocketHandlers, createSocketIOServer, socketIoLane };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  export { type ClientConfig, type ClientContract, type ClientRegistryValue, type ContractClientConfig, contractEndpointMatchers, createClient, createClients, createScopedClients, createScopedUrlBuilders, createUrlBuilder, createUrlBuilders, type PathPrefixArgs, type RegistryScope, type ScopeClientConfigs, type ScopedClientRegistry, type ScopedUrlBuilderRegistry, type UrlBuilderConfig, } from './browser/client';
2
2
  export { ApiError, type ApiEvent, type ApiEventListener, type ConfiguredHttpClient, createHttpClient, type HeaderProvider, type HttpClient, type HttpClientConfig, type RequestOptions, type UnauthorizedMatcher, } from './browser/http';
3
- export type { SocketEventMap, SocketIOClient, SocketIOClientConfig, } from './browser/socket-io';
4
- export { createSocketIOClient } from './browser/socket-io';
3
+ export type { RealtimeClient, RealtimeClientOptions, SocketEventMap, SocketIOClient, SocketIOClientConfig, } from './browser/socket-io';
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';
9
+ export * from './realtime';
8
10
  export { createRetainedTopics, type RetainedTopics } from './retained';
9
11
  //# sourceMappingURL=index.d.ts.map