stitchkit 0.2.0 → 0.4.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 +11 -10
- package/dist/browser/client.d.ts.map +1 -1
- package/dist/browser/http.d.ts +5 -0
- package/dist/browser/http.d.ts.map +1 -1
- package/dist/browser/socket-io.d.ts +17 -0
- package/dist/browser/socket-io.d.ts.map +1 -1
- package/dist/cli.d.ts +23 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +21 -0
- package/dist/contract/define.d.ts +35 -2
- package/dist/contract/define.d.ts.map +1 -1
- package/dist/contract/index.d.ts +1 -1
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +2 -1
- package/dist/index-0ed3bx43.js +33 -0
- package/dist/index-0ma1eqv4.js +1007 -0
- package/dist/index-1cf8jkhf.js +219 -0
- package/dist/index-37x76zdn.js +4 -0
- package/dist/{index-v2z2v3mq.js → index-3qmtdfpa.js} +304 -62
- package/dist/index-48ffdxgk.js +6 -0
- package/dist/{index-5sxnvwb1.js → index-78q1qm7v.js} +6 -3
- package/dist/{index-kckky6zw.js → index-afzt3nmx.js} +1 -1
- package/dist/{index-n7bmdwmz.js → index-d4rwrjbc.js} +8 -4
- package/dist/index-kzfs85xp.js +9 -0
- package/dist/index-mwmpw6j1.js +60 -0
- package/dist/{index-a35v22fh.js → index-vhdvv00d.js} +13 -5
- package/dist/index-x3fcszf8.js +8 -0
- package/dist/index.js +17 -4
- package/dist/internal/errors.d.ts +13 -1
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/http-input.d.ts +13 -0
- package/dist/internal/http-input.d.ts.map +1 -0
- package/dist/internal/safe-json.d.ts +19 -0
- package/dist/internal/safe-json.d.ts.map +1 -0
- package/dist/internal/secure-fetch.d.ts +27 -0
- package/dist/internal/secure-fetch.d.ts.map +1 -0
- package/dist/internal/within-dir.d.ts +7 -0
- package/dist/internal/within-dir.d.ts.map +1 -0
- package/dist/node.d.ts +10 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +41 -7
- package/dist/observability/audit.d.ts.map +1 -1
- package/dist/observability/context.d.ts +9 -1
- package/dist/observability/context.d.ts.map +1 -1
- package/dist/observability/event.d.ts +4 -0
- package/dist/observability/event.d.ts.map +1 -1
- package/dist/observability/index.js +38 -15
- package/dist/observability/sanitize.d.ts.map +1 -1
- package/dist/react.js +2 -0
- package/dist/server/context.d.ts +3 -2
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/create.d.ts.map +1 -1
- package/dist/server/event-bus.d.ts +10 -1
- package/dist/server/event-bus.d.ts.map +1 -1
- package/dist/server/implement.d.ts.map +1 -1
- package/dist/server/index.d.ts +6 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +203 -193
- package/dist/server/logger.d.ts +5 -4
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/middleware/auth.d.ts +41 -1
- package/dist/server/middleware/auth.d.ts.map +1 -1
- package/dist/server/middleware/cookies.d.ts.map +1 -1
- package/dist/server/middleware/cors.d.ts +7 -0
- package/dist/server/middleware/cors.d.ts.map +1 -1
- package/dist/server/middleware/pkce.d.ts +17 -0
- package/dist/server/middleware/pkce.d.ts.map +1 -0
- package/dist/server/multipart.d.ts +2 -1
- package/dist/server/multipart.d.ts.map +1 -1
- package/dist/server/node.d.ts +9 -0
- package/dist/server/node.d.ts.map +1 -1
- package/dist/server/openapi.d.ts +52 -0
- package/dist/server/openapi.d.ts.map +1 -0
- package/dist/server/rate-limit.d.ts +7 -2
- package/dist/server/rate-limit.d.ts.map +1 -1
- package/dist/server/request.d.ts +27 -4
- package/dist/server/request.d.ts.map +1 -1
- package/dist/server/router.d.ts +4 -2
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/socket-io.d.ts +60 -15
- package/dist/server/socket-io.d.ts.map +1 -1
- package/dist/server/stream.d.ts +3 -1
- package/dist/server/stream.d.ts.map +1 -1
- package/dist/server/types.d.ts +17 -1
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/websocket.d.ts +73 -0
- package/dist/server/websocket.d.ts.map +1 -0
- package/dist/tools/cli-args.d.ts +68 -0
- package/dist/tools/cli-args.d.ts.map +1 -0
- package/dist/tools/cli-format.d.ts +39 -0
- package/dist/tools/cli-format.d.ts.map +1 -0
- package/dist/tools/cli-wait.d.ts +45 -0
- package/dist/tools/cli-wait.d.ts.map +1 -0
- package/dist/tools/cli.d.ts +77 -0
- package/dist/tools/cli.d.ts.map +1 -0
- package/dist/tools/coerce.d.ts +8 -4
- package/dist/tools/coerce.d.ts.map +1 -1
- package/dist/tools/execute.d.ts +1 -1
- package/dist/tools/execute.d.ts.map +1 -1
- package/dist/tools/json-schema.d.ts +29 -4
- package/dist/tools/json-schema.d.ts.map +1 -1
- package/dist/tools/manifest.d.ts +4 -0
- package/dist/tools/manifest.d.ts.map +1 -1
- package/dist/tools/mcp-app.d.ts +59 -0
- package/dist/tools/mcp-app.d.ts.map +1 -0
- package/dist/tools/mcp-handler.d.ts +18 -0
- package/dist/tools/mcp-handler.d.ts.map +1 -1
- package/dist/tools/mcp.d.ts +9 -0
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/mount-download.d.ts +30 -0
- package/dist/tools/mount-download.d.ts.map +1 -0
- package/dist/tools/mount-upload.d.ts +19 -0
- package/dist/tools/mount-upload.d.ts.map +1 -0
- package/dist/tools/mount-wait.d.ts +36 -0
- package/dist/tools/mount-wait.d.ts.map +1 -0
- package/dist/tools/mount.d.ts +8 -4
- package/dist/tools/mount.d.ts.map +1 -1
- package/dist/tools/native-result.d.ts +17 -0
- package/dist/tools/native-result.d.ts.map +1 -0
- package/dist/tools/oauth-metadata.d.ts +28 -0
- package/dist/tools/oauth-metadata.d.ts.map +1 -0
- package/dist/tools/oauth-provider.d.ts +85 -0
- package/dist/tools/oauth-provider.d.ts.map +1 -0
- package/dist/tools/remote.d.ts +2 -2
- package/dist/tools/remote.d.ts.map +1 -1
- package/dist/tools/toolkit.d.ts +51 -0
- package/dist/tools/toolkit.d.ts.map +1 -0
- package/dist/tools/view-file.d.ts +12 -0
- package/dist/tools/view-file.d.ts.map +1 -1
- package/dist/tools/wait-core.d.ts +33 -0
- package/dist/tools/wait-core.d.ts.map +1 -0
- package/dist/tools.d.ts +13 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +637 -424
- package/package.json +22 -9
- package/dist/index-ke4mx4ea.js +0 -38
|
@@ -1,70 +1,121 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
} from "./index-
|
|
2
|
+
isWithinDir
|
|
3
|
+
} from "./index-x3fcszf8.js";
|
|
4
|
+
import {
|
|
5
|
+
normalizeError,
|
|
6
|
+
validateHandlerOutput
|
|
7
|
+
} from "./index-vhdvv00d.js";
|
|
4
8
|
import {
|
|
5
9
|
AppError,
|
|
6
10
|
badRequest
|
|
7
|
-
} from "./index-
|
|
11
|
+
} from "./index-afzt3nmx.js";
|
|
8
12
|
import {
|
|
9
13
|
extractIp,
|
|
10
14
|
getClientInfo,
|
|
11
15
|
parseQueryParams,
|
|
16
|
+
resolveSocketIp,
|
|
12
17
|
resolveTraceId
|
|
13
|
-
} from "./index-
|
|
18
|
+
} from "./index-mwmpw6j1.js";
|
|
19
|
+
import {
|
|
20
|
+
isUnsafeKey,
|
|
21
|
+
safeJsonParse
|
|
22
|
+
} from "./index-kzfs85xp.js";
|
|
23
|
+
import {
|
|
24
|
+
typedEntries
|
|
25
|
+
} from "./index-809wc1tt.js";
|
|
26
|
+
import {
|
|
27
|
+
__require
|
|
28
|
+
} from "./index-37x76zdn.js";
|
|
14
29
|
|
|
15
30
|
// src/server/multipart.ts
|
|
16
31
|
var DEFAULT_MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
|
|
17
|
-
async function
|
|
18
|
-
const
|
|
19
|
-
if (
|
|
20
|
-
|
|
32
|
+
async function readBodyCapped(req, maxBytes) {
|
|
33
|
+
const reader = req.body?.getReader();
|
|
34
|
+
if (!reader)
|
|
35
|
+
return new Uint8Array(0);
|
|
36
|
+
const chunks = [];
|
|
37
|
+
let total = 0;
|
|
38
|
+
try {
|
|
39
|
+
while (true) {
|
|
40
|
+
const { done, value } = await reader.read();
|
|
41
|
+
if (done)
|
|
42
|
+
break;
|
|
43
|
+
total += value.length;
|
|
44
|
+
if (total > maxBytes) {
|
|
45
|
+
await reader.cancel();
|
|
46
|
+
badRequest(`Upload exceeds the ${Math.round(maxBytes / 1024 / 1024)} MB limit`);
|
|
47
|
+
}
|
|
48
|
+
chunks.push(value);
|
|
49
|
+
}
|
|
50
|
+
} finally {
|
|
51
|
+
reader.releaseLock();
|
|
52
|
+
}
|
|
53
|
+
const buffer = new Uint8Array(total);
|
|
54
|
+
let offset = 0;
|
|
55
|
+
for (const chunk of chunks) {
|
|
56
|
+
buffer.set(chunk, offset);
|
|
57
|
+
offset += chunk.length;
|
|
21
58
|
}
|
|
22
|
-
|
|
59
|
+
return buffer;
|
|
60
|
+
}
|
|
61
|
+
async function parseMultipart(req, fileField, fieldsSchema, maxBytes = DEFAULT_MAX_UPLOAD_BYTES) {
|
|
62
|
+
const body = await readBodyCapped(req, maxBytes);
|
|
63
|
+
const contentType = req.headers.get("content-type") ?? "";
|
|
64
|
+
const formData = await new Response(body, {
|
|
65
|
+
headers: { "content-type": contentType }
|
|
66
|
+
}).formData();
|
|
23
67
|
const file = formData.get(fileField);
|
|
24
68
|
if (!file || !(file instanceof File)) {
|
|
25
69
|
badRequest(`Missing file field: ${fileField}`);
|
|
26
70
|
}
|
|
27
|
-
if (file.size > maxBytes) {
|
|
28
|
-
badRequest(`Upload exceeds the ${Math.round(maxBytes / 1024 / 1024)} MB limit`);
|
|
29
|
-
}
|
|
30
71
|
const fields = {};
|
|
31
72
|
for (const [key, value] of formData.entries()) {
|
|
32
73
|
if (key === fileField)
|
|
33
74
|
continue;
|
|
75
|
+
if (isUnsafeKey(key))
|
|
76
|
+
continue;
|
|
34
77
|
if (typeof value === "string") {
|
|
35
78
|
try {
|
|
36
|
-
fields[key] =
|
|
79
|
+
fields[key] = safeJsonParse(value);
|
|
37
80
|
} catch {
|
|
38
81
|
fields[key] = value;
|
|
39
82
|
}
|
|
40
83
|
}
|
|
41
84
|
}
|
|
42
|
-
|
|
43
|
-
return { file, fields: parsed };
|
|
85
|
+
return { file, fields: fieldsSchema ? fieldsSchema.parse(fields) : fields };
|
|
44
86
|
}
|
|
45
87
|
|
|
46
88
|
// src/server/middleware/cors.ts
|
|
47
|
-
function
|
|
48
|
-
|
|
89
|
+
function assertCorsConfig(config) {
|
|
90
|
+
if (config.credentials && (config.origin === undefined || config.origin === "*")) {
|
|
91
|
+
throw new Error("[stitchkit] cors: `credentials: true` cannot be combined with a wildcard origin. " + "Set `origin` to an explicit string or list.");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function resolveOrigin(config, requestOrigin) {
|
|
49
95
|
if (config.origin === undefined || config.origin === "*") {
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
96
|
+
return "*";
|
|
97
|
+
}
|
|
98
|
+
if (Array.isArray(config.origin)) {
|
|
99
|
+
if (!requestOrigin)
|
|
100
|
+
return;
|
|
101
|
+
const lower = requestOrigin.toLowerCase();
|
|
102
|
+
return config.origin.some((o) => o.toLowerCase() === lower) ? requestOrigin : undefined;
|
|
55
103
|
}
|
|
104
|
+
return config.origin;
|
|
105
|
+
}
|
|
106
|
+
function corsHeaders(config, requestOrigin) {
|
|
107
|
+
const allowOrigin = resolveOrigin(config, requestOrigin);
|
|
56
108
|
const headers = {
|
|
57
109
|
"Access-Control-Allow-Methods": config.methods ?? "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
58
110
|
"Access-Control-Allow-Headers": config.headers ?? "Content-Type, Authorization, X-Trace-Id"
|
|
59
111
|
};
|
|
60
112
|
if (allowOrigin !== undefined) {
|
|
61
113
|
headers["Access-Control-Allow-Origin"] = allowOrigin;
|
|
114
|
+
if (config.credentials) {
|
|
115
|
+
headers["Access-Control-Allow-Credentials"] = "true";
|
|
116
|
+
}
|
|
62
117
|
}
|
|
63
|
-
if (config.
|
|
64
|
-
headers["Access-Control-Allow-Credentials"] = "true";
|
|
65
|
-
}
|
|
66
|
-
const variesByOrigin = Array.isArray(config.origin) || (config.origin === undefined || config.origin === "*") && Boolean(config.credentials);
|
|
67
|
-
if (variesByOrigin) {
|
|
118
|
+
if (Array.isArray(config.origin)) {
|
|
68
119
|
headers.Vary = "Origin";
|
|
69
120
|
}
|
|
70
121
|
return headers;
|
|
@@ -77,7 +128,8 @@ function corsPreflightResponse(config, req) {
|
|
|
77
128
|
}
|
|
78
129
|
|
|
79
130
|
// src/server/router.ts
|
|
80
|
-
import {
|
|
131
|
+
import { readFile, stat } from "node:fs/promises";
|
|
132
|
+
import { extname, resolve } from "node:path";
|
|
81
133
|
function joinPath(...parts) {
|
|
82
134
|
const joined = parts.filter(Boolean).map((part) => part.replace(/^\/+|\/+$/g, "")).filter(Boolean).join("/");
|
|
83
135
|
return `/${joined}`;
|
|
@@ -195,25 +247,51 @@ function matchRawRoute(rawRoutes, httpMethod, pathname) {
|
|
|
195
247
|
}
|
|
196
248
|
return null;
|
|
197
249
|
}
|
|
250
|
+
var STATIC_MIME = {
|
|
251
|
+
".html": "text/html; charset=utf-8",
|
|
252
|
+
".css": "text/css; charset=utf-8",
|
|
253
|
+
".js": "text/javascript; charset=utf-8",
|
|
254
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
255
|
+
".json": "application/json; charset=utf-8",
|
|
256
|
+
".svg": "image/svg+xml",
|
|
257
|
+
".png": "image/png",
|
|
258
|
+
".jpg": "image/jpeg",
|
|
259
|
+
".jpeg": "image/jpeg",
|
|
260
|
+
".gif": "image/gif",
|
|
261
|
+
".webp": "image/webp",
|
|
262
|
+
".ico": "image/x-icon",
|
|
263
|
+
".woff2": "font/woff2",
|
|
264
|
+
".txt": "text/plain; charset=utf-8"
|
|
265
|
+
};
|
|
198
266
|
function staticRoute(prefix, dir) {
|
|
199
267
|
const cleanPrefix = prefix.replace(/\/+$/, "");
|
|
200
|
-
const
|
|
268
|
+
const root = resolve(dir.replace(/\/+$/, ""));
|
|
201
269
|
return {
|
|
202
270
|
method: "GET",
|
|
203
271
|
path: `${cleanPrefix}/*`,
|
|
204
272
|
handler: async (req) => {
|
|
205
273
|
const pathname = new URL(req.url).pathname;
|
|
206
|
-
|
|
207
|
-
|
|
274
|
+
let rel;
|
|
275
|
+
try {
|
|
276
|
+
rel = decodeURIComponent(pathname.slice(cleanPrefix.length)).replace(/^\/+/, "");
|
|
277
|
+
} catch {
|
|
278
|
+
return new Response("Bad request", { status: 400 });
|
|
279
|
+
}
|
|
208
280
|
const target = resolve(root, rel);
|
|
209
|
-
if (
|
|
281
|
+
if (!isWithinDir(root, target)) {
|
|
210
282
|
return new Response("Forbidden", { status: 403 });
|
|
211
283
|
}
|
|
212
|
-
const
|
|
213
|
-
if (!
|
|
284
|
+
const info = await stat(target).catch(() => null);
|
|
285
|
+
if (!info?.isFile()) {
|
|
214
286
|
return new Response("Not found", { status: 404 });
|
|
215
287
|
}
|
|
216
|
-
|
|
288
|
+
const body = await readFile(target);
|
|
289
|
+
return new Response(body, {
|
|
290
|
+
headers: {
|
|
291
|
+
"Content-Type": STATIC_MIME[extname(target).toLowerCase()] ?? "application/octet-stream",
|
|
292
|
+
"X-Content-Type-Options": "nosniff"
|
|
293
|
+
}
|
|
294
|
+
});
|
|
217
295
|
}
|
|
218
296
|
};
|
|
219
297
|
}
|
|
@@ -235,13 +313,16 @@ async function readJsonBody(req) {
|
|
|
235
313
|
const text = await req.text();
|
|
236
314
|
if (text.trim() === "")
|
|
237
315
|
return {};
|
|
316
|
+
if (!req.headers.get("content-type")?.includes("application/json")) {
|
|
317
|
+
badRequest("Request body must be application/json");
|
|
318
|
+
}
|
|
238
319
|
try {
|
|
239
|
-
return
|
|
320
|
+
return safeJsonParse(text);
|
|
240
321
|
} catch {
|
|
241
322
|
badRequest("Invalid JSON body");
|
|
242
323
|
}
|
|
243
324
|
}
|
|
244
|
-
async function buildContext(req, url, method, pathParams, traceId) {
|
|
325
|
+
async function buildContext(req, url, method, pathParams, traceId, clientIp) {
|
|
245
326
|
const parsedParams = method.paramsSchema ? method.paramsSchema.parse(pathParams) : undefined;
|
|
246
327
|
let parsedInput;
|
|
247
328
|
let file;
|
|
@@ -265,7 +346,7 @@ async function buildContext(req, url, method, pathParams, traceId) {
|
|
|
265
346
|
}
|
|
266
347
|
const safePathParams = {};
|
|
267
348
|
for (const [k, v] of Object.entries(pathParams)) {
|
|
268
|
-
if (!RESERVED_KEYS.has(k))
|
|
349
|
+
if (!RESERVED_KEYS.has(k) && !isUnsafeKey(k))
|
|
269
350
|
safePathParams[k] = v;
|
|
270
351
|
}
|
|
271
352
|
return {
|
|
@@ -278,10 +359,10 @@ async function buildContext(req, url, method, pathParams, traceId) {
|
|
|
278
359
|
headers: req.headers,
|
|
279
360
|
...safePathParams,
|
|
280
361
|
traceId,
|
|
281
|
-
...getClientInfo(req)
|
|
362
|
+
...getClientInfo(req, clientIp)
|
|
282
363
|
};
|
|
283
364
|
}
|
|
284
|
-
function buildErrorContext(req, url, traceId) {
|
|
365
|
+
function buildErrorContext(req, url, traceId, clientIp) {
|
|
285
366
|
return {
|
|
286
367
|
params: undefined,
|
|
287
368
|
input: undefined,
|
|
@@ -290,7 +371,7 @@ function buildErrorContext(req, url, traceId) {
|
|
|
290
371
|
url,
|
|
291
372
|
headers: req.headers,
|
|
292
373
|
traceId,
|
|
293
|
-
...getClientInfo(req)
|
|
374
|
+
...getClientInfo(req, clientIp)
|
|
294
375
|
};
|
|
295
376
|
}
|
|
296
377
|
|
|
@@ -326,7 +407,10 @@ function timestamp() {
|
|
|
326
407
|
return `${t}.${now.getMilliseconds().toString().padStart(3, "0")}`;
|
|
327
408
|
}
|
|
328
409
|
function elapsedMs(startTime) {
|
|
329
|
-
return
|
|
410
|
+
return performance.now() - startTime;
|
|
411
|
+
}
|
|
412
|
+
function safePath(pathname) {
|
|
413
|
+
return pathname.replace(/[-]/g, "");
|
|
330
414
|
}
|
|
331
415
|
function levelForStatus(status) {
|
|
332
416
|
if (status >= 500)
|
|
@@ -372,15 +456,15 @@ function shouldLog(pathname, method) {
|
|
|
372
456
|
return false;
|
|
373
457
|
return !SKIP_PREFIXES.some((prefix) => pathname.startsWith(prefix));
|
|
374
458
|
}
|
|
375
|
-
function logIncoming(req, pathname, traceId) {
|
|
376
|
-
const log = { traceId, startTime:
|
|
459
|
+
function logIncoming(req, pathname, traceId, ipAddress) {
|
|
460
|
+
const log = { traceId, startTime: performance.now() };
|
|
377
461
|
if (!isProd) {
|
|
378
462
|
const mc = METHOD_COLOR[req.method] ?? c.dim;
|
|
379
|
-
console.log(`${c.gray}[${timestamp()}]${c.reset} ${mc}${req.method}${c.reset} ${c.dim}${traceId}${c.reset} ${c.cyan}→${c.reset} ${pathname} ${ipLabel(
|
|
463
|
+
console.log(`${c.gray}[${timestamp()}]${c.reset} ${mc}${req.method}${c.reset} ${c.dim}${traceId}${c.reset} ${c.cyan}→${c.reset} ${safePath(pathname)} ${ipLabel(ipAddress ?? "")}`);
|
|
380
464
|
}
|
|
381
465
|
return log;
|
|
382
466
|
}
|
|
383
|
-
function logOutgoing(req, pathname, status, log) {
|
|
467
|
+
function logOutgoing(req, pathname, status, log, ipAddress) {
|
|
384
468
|
const ms = elapsedMs(log.startTime);
|
|
385
469
|
if (isProd) {
|
|
386
470
|
console.log(JSON.stringify({
|
|
@@ -388,42 +472,45 @@ function logOutgoing(req, pathname, status, log) {
|
|
|
388
472
|
level: levelForStatus(status),
|
|
389
473
|
msg: `${req.method} ${pathname} ${status}`,
|
|
390
474
|
...buildLogFields(req.method, pathname, status, Math.round(ms), log.traceId),
|
|
391
|
-
ip:
|
|
475
|
+
ip: ipAddress
|
|
392
476
|
}));
|
|
393
477
|
return;
|
|
394
478
|
}
|
|
395
479
|
const mc = METHOD_COLOR[req.method] ?? c.dim;
|
|
396
|
-
console.log(`${c.gray}[${timestamp()}]${c.reset} ${mc}${req.method}${c.reset} ${c.dim}${log.traceId}${c.reset} ${c.cyan}←${c.reset} ${pathname} ${statusColor(status)}${status}${c.reset} ${durationColor(ms)}${formatMs(ms)}${c.reset} ${ipLabel(
|
|
480
|
+
console.log(`${c.gray}[${timestamp()}]${c.reset} ${mc}${req.method}${c.reset} ${c.dim}${log.traceId}${c.reset} ${c.cyan}←${c.reset} ${safePath(pathname)} ${statusColor(status)}${status}${c.reset} ${durationColor(ms)}${formatMs(ms)}${c.reset} ${ipLabel(ipAddress ?? "")}`);
|
|
397
481
|
}
|
|
398
482
|
|
|
399
483
|
// src/server/create.ts
|
|
400
484
|
function createHandler(config) {
|
|
401
|
-
const { cors, hooks, logging = false } = config;
|
|
485
|
+
const { cors, hooks, logging = false, trustProxy = false } = config;
|
|
486
|
+
if (cors)
|
|
487
|
+
assertCorsConfig(cors);
|
|
402
488
|
const customLogger = typeof logging === "object" ? logging : null;
|
|
403
489
|
const useDefaultLog = logging === true;
|
|
404
490
|
const resolveId = config.traceId ?? resolveTraceId;
|
|
405
491
|
const routeMap = buildRouteMap(normalizeGroups(config));
|
|
406
492
|
validateRoutes(routeMap);
|
|
407
|
-
async function dispatch(req, url, traceId, server) {
|
|
493
|
+
async function dispatch(req, url, traceId, server, clientIp) {
|
|
408
494
|
const shouldLogRequest = logging && shouldLog(url.pathname, req.method);
|
|
495
|
+
const ipAddress = extractIp(req, clientIp) || undefined;
|
|
409
496
|
let reqLog;
|
|
410
497
|
if (shouldLogRequest && useDefaultLog) {
|
|
411
|
-
reqLog = logIncoming(req, url.pathname, traceId);
|
|
498
|
+
reqLog = logIncoming(req, url.pathname, traceId, ipAddress);
|
|
412
499
|
}
|
|
413
500
|
if (shouldLogRequest && customLogger) {
|
|
414
501
|
customLogger.debug(`${req.method} ${url.pathname}`, {
|
|
415
502
|
traceId,
|
|
416
503
|
method: req.method,
|
|
417
504
|
path: url.pathname,
|
|
418
|
-
ip:
|
|
505
|
+
ip: ipAddress
|
|
419
506
|
});
|
|
420
|
-
reqLog = { traceId, startTime:
|
|
507
|
+
reqLog = { traceId, startTime: performance.now() };
|
|
421
508
|
}
|
|
422
509
|
const logDone = (status) => {
|
|
423
510
|
if (!reqLog)
|
|
424
511
|
return;
|
|
425
512
|
if (useDefaultLog)
|
|
426
|
-
logOutgoing(req, url.pathname, status, reqLog);
|
|
513
|
+
logOutgoing(req, url.pathname, status, reqLog, ipAddress);
|
|
427
514
|
if (customLogger) {
|
|
428
515
|
const durationMs = Math.round(elapsedMs(reqLog.startTime));
|
|
429
516
|
const level = levelForStatus(status);
|
|
@@ -433,7 +520,7 @@ function createHandler(config) {
|
|
|
433
520
|
const respondError = async (err, errCtx, endpoint) => {
|
|
434
521
|
if (hooks?.onError) {
|
|
435
522
|
try {
|
|
436
|
-
const response = await hooks.onError(errCtx ?? buildErrorContext(req, url, traceId), err, endpoint);
|
|
523
|
+
const response = await hooks.onError(errCtx ?? buildErrorContext(req, url, traceId, clientIp), err, endpoint);
|
|
437
524
|
if (response instanceof Response) {
|
|
438
525
|
const withCors = applyCors(response, cors, req);
|
|
439
526
|
logDone(withCors.status);
|
|
@@ -463,7 +550,8 @@ function createHandler(config) {
|
|
|
463
550
|
try {
|
|
464
551
|
const res = await rawMatch.route.handler(req, {
|
|
465
552
|
params: rawMatch.params,
|
|
466
|
-
server
|
|
553
|
+
server,
|
|
554
|
+
ipAddress
|
|
467
555
|
});
|
|
468
556
|
const withCors = applyCors(res, cors, req);
|
|
469
557
|
logDone(withCors.status);
|
|
@@ -486,7 +574,7 @@ function createHandler(config) {
|
|
|
486
574
|
const { method, pathParams, groupHooks } = match;
|
|
487
575
|
let ctx;
|
|
488
576
|
try {
|
|
489
|
-
ctx = await buildContext(req, url, method, pathParams, traceId);
|
|
577
|
+
ctx = await buildContext(req, url, method, pathParams, traceId, clientIp);
|
|
490
578
|
if (hooks?.beforeHandle) {
|
|
491
579
|
await hooks.beforeHandle(ctx, method);
|
|
492
580
|
}
|
|
@@ -505,7 +593,11 @@ function createHandler(config) {
|
|
|
505
593
|
result = transformed;
|
|
506
594
|
}
|
|
507
595
|
if (method.outputSchema) {
|
|
508
|
-
|
|
596
|
+
const checked = validateHandlerOutput(method.outputSchema, result);
|
|
597
|
+
if (!checked.ok) {
|
|
598
|
+
throw new AppError("INTERNAL_SERVER_ERROR", checked.message, 500);
|
|
599
|
+
}
|
|
600
|
+
result = checked.data;
|
|
509
601
|
}
|
|
510
602
|
if (result === undefined || result === null) {
|
|
511
603
|
logDone(204);
|
|
@@ -520,10 +612,14 @@ function createHandler(config) {
|
|
|
520
612
|
return async (req, server) => {
|
|
521
613
|
const url = new URL(req.url, "http://localhost");
|
|
522
614
|
const traceId = resolveId(req);
|
|
523
|
-
const
|
|
524
|
-
|
|
615
|
+
const clientIp = {
|
|
616
|
+
trustProxy,
|
|
617
|
+
socketIp: resolveSocketIp(req, server)
|
|
618
|
+
};
|
|
619
|
+
const response = await dispatch(req, url, traceId, server, clientIp);
|
|
620
|
+
try {
|
|
525
621
|
response.headers.set("x-request-id", traceId);
|
|
526
|
-
}
|
|
622
|
+
} catch {}
|
|
527
623
|
return response;
|
|
528
624
|
};
|
|
529
625
|
}
|
|
@@ -584,4 +680,150 @@ function json(data, status, cors, req) {
|
|
|
584
680
|
return Response.json(data, { status, headers: corsHeaders2(cors, req) });
|
|
585
681
|
}
|
|
586
682
|
|
|
587
|
-
|
|
683
|
+
// src/server/implement.ts
|
|
684
|
+
function implement(contract, handlers) {
|
|
685
|
+
const methods = {};
|
|
686
|
+
const groupScope = contract.meta.scope ?? "public";
|
|
687
|
+
for (const [key, endpoint] of typedEntries(contract.endpoints)) {
|
|
688
|
+
const typedHandler = handlers[key];
|
|
689
|
+
methods[String(key)] = {
|
|
690
|
+
method: endpoint.method,
|
|
691
|
+
path: endpoint.path,
|
|
692
|
+
desc: endpoint.desc,
|
|
693
|
+
toolName: "toolName" in endpoint ? endpoint.toolName : undefined,
|
|
694
|
+
expose: endpoint.expose,
|
|
695
|
+
scope: endpoint.scope ?? groupScope,
|
|
696
|
+
paramsSchema: endpoint.params,
|
|
697
|
+
inputSchema: endpoint.input,
|
|
698
|
+
outputSchema: endpoint.output,
|
|
699
|
+
multipart: endpoint.multipart,
|
|
700
|
+
ui: "ui" in endpoint ? endpoint.ui : undefined,
|
|
701
|
+
annotations: "annotations" in endpoint ? endpoint.annotations : undefined,
|
|
702
|
+
handler: (ctx) => typedHandler(ctx)
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
return {
|
|
706
|
+
name: contract.meta.prefix,
|
|
707
|
+
prefix: contract.meta.prefix,
|
|
708
|
+
scope: groupScope,
|
|
709
|
+
methods
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
function createImplement() {
|
|
713
|
+
return (contract, handlers) => implement(contract, handlers);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// src/server/websocket.ts
|
|
717
|
+
function webSocketLane(lane) {
|
|
718
|
+
const { match, handlers } = lane;
|
|
719
|
+
return {
|
|
720
|
+
match,
|
|
721
|
+
handlers: {
|
|
722
|
+
message(ws, message) {
|
|
723
|
+
if (match(ws))
|
|
724
|
+
handlers.message(ws, message);
|
|
725
|
+
},
|
|
726
|
+
open(ws) {
|
|
727
|
+
if (match(ws))
|
|
728
|
+
handlers.open?.(ws);
|
|
729
|
+
},
|
|
730
|
+
close(ws, code, reason) {
|
|
731
|
+
if (match(ws))
|
|
732
|
+
handlers.close?.(ws, code, reason);
|
|
733
|
+
},
|
|
734
|
+
drain(ws) {
|
|
735
|
+
if (match(ws))
|
|
736
|
+
handlers.drain?.(ws);
|
|
737
|
+
},
|
|
738
|
+
ping(ws, data) {
|
|
739
|
+
if (match(ws))
|
|
740
|
+
handlers.ping?.(ws, data);
|
|
741
|
+
},
|
|
742
|
+
pong(ws, data) {
|
|
743
|
+
if (match(ws))
|
|
744
|
+
handlers.pong?.(ws, data);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
function composeWebSocketHandlers(lanes, config) {
|
|
750
|
+
const pick = (ws) => lanes.find((lane) => lane.match(ws));
|
|
751
|
+
return {
|
|
752
|
+
...config,
|
|
753
|
+
message(ws, message) {
|
|
754
|
+
pick(ws)?.handlers.message(ws, message);
|
|
755
|
+
},
|
|
756
|
+
open(ws) {
|
|
757
|
+
pick(ws)?.handlers.open?.(ws);
|
|
758
|
+
},
|
|
759
|
+
close(ws, code, reason) {
|
|
760
|
+
pick(ws)?.handlers.close?.(ws, code, reason);
|
|
761
|
+
},
|
|
762
|
+
drain(ws) {
|
|
763
|
+
pick(ws)?.handlers.drain?.(ws);
|
|
764
|
+
},
|
|
765
|
+
ping(ws, data) {
|
|
766
|
+
pick(ws)?.handlers.ping?.(ws, data);
|
|
767
|
+
},
|
|
768
|
+
pong(ws, data) {
|
|
769
|
+
pick(ws)?.handlers.pong?.(ws, data);
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// src/server/socket-io.ts
|
|
775
|
+
var noop = () => {};
|
|
776
|
+
async function createSocketIOServer(config) {
|
|
777
|
+
const path = config.path ?? "/socket.io/";
|
|
778
|
+
const onBun = "Bun" in globalThis;
|
|
779
|
+
const transports = config.transports ?? (onBun ? ["websocket", "polling"] : ["websocket"]);
|
|
780
|
+
const { Server } = await import("socket.io");
|
|
781
|
+
const io = new Server({
|
|
782
|
+
path,
|
|
783
|
+
cors: {
|
|
784
|
+
origin: config.cors.origin,
|
|
785
|
+
credentials: config.cors.credentials ?? true,
|
|
786
|
+
methods: ["GET", "POST"]
|
|
787
|
+
},
|
|
788
|
+
transports,
|
|
789
|
+
pingTimeout: config.pingTimeout ?? 20000,
|
|
790
|
+
pingInterval: config.pingInterval ?? 1e4
|
|
791
|
+
});
|
|
792
|
+
if (onBun) {
|
|
793
|
+
const { Server: Engine } = await import("@socket.io/bun-engine");
|
|
794
|
+
const engine = new Engine({ path });
|
|
795
|
+
io.bind(engine);
|
|
796
|
+
const { websocket } = engine.handler();
|
|
797
|
+
const route = {
|
|
798
|
+
method: "ALL",
|
|
799
|
+
path: `${path.replace(/\/+$/, "")}/*`,
|
|
800
|
+
handler: (req, ctx) => {
|
|
801
|
+
if (!ctx.server) {
|
|
802
|
+
throw new Error("[stitchkit] createSocketIOServer route needs a running Bun server — mount it via createServer.");
|
|
803
|
+
}
|
|
804
|
+
return engine.handleRequest(req, ctx.server);
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
return { io, websocket, route, attach: noop };
|
|
808
|
+
}
|
|
809
|
+
return {
|
|
810
|
+
io,
|
|
811
|
+
attach: (server) => io.attach(server),
|
|
812
|
+
websocket: { open: noop, message: noop, close: noop, maxPayloadLength: 0 },
|
|
813
|
+
route: {
|
|
814
|
+
method: "ALL",
|
|
815
|
+
path: `${path.replace(/\/+$/, "")}/*`,
|
|
816
|
+
handler: () => {
|
|
817
|
+
throw new Error("[stitchkit] On Node, Socket.IO attaches via serveNode({ socket }) — this route is not mounted.");
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
function socketIoLane(websocket) {
|
|
823
|
+
return webSocketLane({
|
|
824
|
+
match: (_ws) => true,
|
|
825
|
+
handlers: websocket
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
export { parseMultipart, corsHeaders, corsPreflightResponse, staticRoute, createHandler, createServer, implement, createImplement, webSocketLane, composeWebSocketHandlers, createSocketIOServer, socketIoLane };
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
inputIsQuery
|
|
3
|
+
} from "./index-48ffdxgk.js";
|
|
1
4
|
import {
|
|
2
5
|
isRecord,
|
|
3
6
|
mapObject,
|
|
@@ -40,7 +43,7 @@ function createHttpClient(config) {
|
|
|
40
43
|
let ssrCookies = null;
|
|
41
44
|
let isLoggedOut = false;
|
|
42
45
|
const listeners = new Set;
|
|
43
|
-
const authEndpoints = config.authEndpoints ?? ["auth/"];
|
|
46
|
+
const authEndpoints = config.authEndpoints ?? ["/auth/"];
|
|
44
47
|
const parseError = config.parseError ?? parseApiErrorBody;
|
|
45
48
|
function emit(event) {
|
|
46
49
|
for (const fn of listeners) {
|
|
@@ -76,7 +79,7 @@ function createHttpClient(config) {
|
|
|
76
79
|
async ({ request: request2, response }) => {
|
|
77
80
|
if (response.status === 401) {
|
|
78
81
|
const url = new URL(request2.url).pathname;
|
|
79
|
-
if (!isLoggedOut && !authEndpoints.some((path) => url.
|
|
82
|
+
if (!isLoggedOut && !authEndpoints.some((path) => url.startsWith(path))) {
|
|
80
83
|
isLoggedOut = true;
|
|
81
84
|
emit({ type: "unauthorized" });
|
|
82
85
|
}
|
|
@@ -261,7 +264,7 @@ function createFetchMethod(endpoint, prefix, config) {
|
|
|
261
264
|
Accept: "application/json",
|
|
262
265
|
...typeof config.headers === "function" ? config.headers() : config.headers
|
|
263
266
|
};
|
|
264
|
-
const isQuery = endpoint.method
|
|
267
|
+
const isQuery = inputIsQuery(endpoint.method);
|
|
265
268
|
const hasBody = !isQuery && !endpoint.multipart && endpoint.input && args;
|
|
266
269
|
if (isQuery && args) {
|
|
267
270
|
const remaining = stripParams(args, endpoint.path);
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeError
|
|
3
|
+
} from "./index-vhdvv00d.js";
|
|
4
|
+
|
|
1
5
|
// src/server/stream.ts
|
|
2
6
|
function streamSSE(generator) {
|
|
3
7
|
const encoder = new TextEncoder;
|
|
@@ -15,8 +19,8 @@ function streamSSE(generator) {
|
|
|
15
19
|
`));
|
|
16
20
|
controller.close();
|
|
17
21
|
} catch (err) {
|
|
18
|
-
const
|
|
19
|
-
controller.enqueue(encoder.encode(`data: ${JSON.stringify(
|
|
22
|
+
const envelope = normalizeError(err).toJSON();
|
|
23
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(envelope)}
|
|
20
24
|
|
|
21
25
|
`));
|
|
22
26
|
controller.close();
|
|
@@ -48,9 +52,9 @@ async function* parseSSE(response, options) {
|
|
|
48
52
|
buffer = lines.pop() ?? "";
|
|
49
53
|
for (const rawLine of lines) {
|
|
50
54
|
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
51
|
-
if (!line.startsWith("data:
|
|
55
|
+
if (!line.startsWith("data:"))
|
|
52
56
|
continue;
|
|
53
|
-
const data = line.slice(
|
|
57
|
+
const data = line.slice(5).replace(/^ /, "");
|
|
54
58
|
if (data === "[DONE]")
|
|
55
59
|
return;
|
|
56
60
|
try {
|