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.
- package/README.md +1 -1
- package/dist/browser/http.d.ts.map +1 -1
- package/dist/browser/socket-io.d.ts +3 -1
- 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-w0mec79h.js → index-zwqty9zf.js} +261 -281
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +129 -51
- 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.js +3 -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 +4 -1
- package/dist/realtime/contract.d.ts.map +1 -1
- package/dist/realtime/rejection.d.ts +10 -0
- package/dist/realtime/rejection.d.ts.map +1 -0
- package/dist/realtime/socket.d.ts +14 -1
- package/dist/realtime/socket.d.ts.map +1 -1
- 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 +2 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +108 -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 +2 -0
- package/dist/server/realtime.d.ts.map +1 -1
- 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 +107 -19
- 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,9 +986,25 @@ function createImplement() {
|
|
|
1056
986
|
return (contract, handlers) => implement(contract, handlers);
|
|
1057
987
|
}
|
|
1058
988
|
|
|
1059
|
-
// src/realtime/
|
|
989
|
+
// src/realtime/rejection.ts
|
|
1060
990
|
import { z } from "zod";
|
|
1061
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1119
|
-
|
|
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
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
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
|
-
|
|
1145
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1179
|
-
|
|
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,
|
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|