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.
- package/README.md +1 -1
- package/dist/browser/http.d.ts.map +1 -1
- package/dist/browser/socket-io.d.ts +16 -0
- package/dist/browser/socket-io.d.ts.map +1 -1
- package/dist/cli.js +3 -4
- package/dist/contract/errors.d.ts +1 -0
- package/dist/contract/errors.d.ts.map +1 -1
- package/dist/contract/index.js +2 -2
- package/dist/{index-yh0p5ddn.js → index-5s8b7z6q.js} +31 -11
- package/dist/{index-mvjyq9h9.js → index-62pqb23z.js} +309 -144
- package/dist/index-c40tkxcd.js +181 -0
- package/dist/{index-r6czv7yt.js → index-pwyedf7b.js} +4 -3
- package/dist/{index-6jypn22c.js → index-r1qp4rve.js} +9 -1
- package/dist/{index-s4qsmgwe.js → index-tss6bk5c.js} +4 -1
- package/dist/{index-36f1xkdb.js → index-w1s873ng.js} +2 -2
- package/dist/index-yxpe3phd.js +63 -0
- package/dist/{index-4fyn9f09.js → index-zwqty9zf.js} +396 -246
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +266 -17
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/secure-fetch.d.ts +23 -3
- package/dist/internal/secure-fetch.d.ts.map +1 -1
- package/dist/internal/typed.d.ts +7 -0
- package/dist/internal/typed.d.ts.map +1 -1
- package/dist/internal/within-dir.d.ts +6 -0
- package/dist/internal/within-dir.d.ts.map +1 -1
- package/dist/logger.d.ts +8 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/node.d.ts +1 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +5 -4
- package/dist/observability/audit.d.ts.map +1 -1
- package/dist/observability/event.d.ts +7 -0
- package/dist/observability/event.d.ts.map +1 -1
- package/dist/observability/index.js +54 -125
- package/dist/observability/sanitize.d.ts +8 -1
- package/dist/observability/sanitize.d.ts.map +1 -1
- package/dist/observability/trace.d.ts +3 -1
- package/dist/observability/trace.d.ts.map +1 -1
- package/dist/react/cache-bridge.d.ts +4 -0
- package/dist/react/cache-bridge.d.ts.map +1 -1
- package/dist/react.js +46 -5
- package/dist/realtime/contract.d.ts +39 -0
- package/dist/realtime/contract.d.ts.map +1 -0
- package/dist/realtime/index.d.ts +3 -0
- package/dist/realtime/index.d.ts.map +1 -0
- package/dist/realtime/rejection.d.ts +10 -0
- package/dist/realtime/rejection.d.ts.map +1 -0
- package/dist/realtime/socket.d.ts +30 -0
- package/dist/realtime/socket.d.ts.map +1 -0
- package/dist/server/cache.d.ts +5 -1
- package/dist/server/cache.d.ts.map +1 -1
- package/dist/server/error-hook.d.ts +1 -0
- package/dist/server/error-hook.d.ts.map +1 -1
- package/dist/server/file.d.ts +6 -0
- package/dist/server/file.d.ts.map +1 -1
- package/dist/server/implement.d.ts.map +1 -1
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +110 -11
- package/dist/server/middleware/cors.d.ts.map +1 -1
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/realtime.d.ts +24 -0
- package/dist/server/realtime.d.ts.map +1 -0
- package/dist/server/router.d.ts +0 -8
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/stream.d.ts.map +1 -1
- package/dist/server/types.d.ts +2 -7
- package/dist/server/types.d.ts.map +1 -1
- package/dist/tools/cli-args.d.ts +11 -3
- package/dist/tools/cli-args.d.ts.map +1 -1
- package/dist/tools/cli.d.ts +5 -0
- package/dist/tools/cli.d.ts.map +1 -1
- package/dist/tools/coerce.d.ts.map +1 -1
- package/dist/tools/json-schema.d.ts.map +1 -1
- package/dist/tools/mcp-handler.d.ts +4 -3
- package/dist/tools/mcp-handler.d.ts.map +1 -1
- package/dist/tools/mcp-prepare.d.ts +95 -0
- package/dist/tools/mcp-prepare.d.ts.map +1 -0
- package/dist/tools/mcp-round.d.ts +4 -0
- package/dist/tools/mcp-round.d.ts.map +1 -1
- package/dist/tools/mcp-stdio.d.ts +2 -1
- package/dist/tools/mcp-stdio.d.ts.map +1 -1
- package/dist/tools/mcp.d.ts +2 -78
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/mount-download.d.ts +5 -0
- package/dist/tools/mount-download.d.ts.map +1 -1
- package/dist/tools/native-mcp.d.ts +1 -1
- package/dist/tools/native-mcp.d.ts.map +1 -1
- package/dist/tools/oauth-provider.d.ts +20 -1
- package/dist/tools/oauth-provider.d.ts.map +1 -1
- package/dist/tools/tool-logger.d.ts +1 -1
- package/dist/tools/tool-logger.d.ts.map +1 -1
- package/dist/tools/view-file.d.ts +9 -0
- package/dist/tools/view-file.d.ts.map +1 -1
- package/dist/tools.d.ts +2 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +342 -214
- package/llms-full.txt +175 -44
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/dist/index-frfyw9fa.js +0 -33
- package/dist/index-x3fcszf8.js +0 -8
|
@@ -2,14 +2,12 @@ import {
|
|
|
2
2
|
assertCorsConfig,
|
|
3
3
|
corsHeaders,
|
|
4
4
|
corsPreflightResponse
|
|
5
|
-
} from "./index-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|