stitchkit 0.3.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/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-dpj1jcys.js → index-3qmtdfpa.js} +156 -4
- package/dist/index-48ffdxgk.js +6 -0
- package/dist/{index-4kz3aqpx.js → index-78q1qm7v.js} +4 -1
- package/dist/{index-kckky6zw.js → index-afzt3nmx.js} +1 -1
- package/dist/{index-4b1j82gp.js → index-d4rwrjbc.js} +1 -1
- package/dist/{index-0t460v7k.js → index-vhdvv00d.js} +1 -1
- package/dist/index-x3fcszf8.js +8 -0
- package/dist/index.js +16 -4
- package/dist/internal/http-input.d.ts +13 -0
- package/dist/internal/http-input.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.map +1 -1
- package/dist/node.d.ts +10 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +38 -7
- package/dist/observability/audit.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 +3 -0
- package/dist/react.js +2 -0
- package/dist/server/implement.d.ts.map +1 -1
- package/dist/server/index.d.ts +5 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +172 -216
- package/dist/server/middleware/auth.d.ts +17 -0
- package/dist/server/middleware/auth.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/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/socket-io.d.ts +60 -15
- package/dist/server/socket-io.d.ts.map +1 -1
- package/dist/server/types.d.ts +5 -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.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/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 +6 -2
- 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 +12 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +587 -421
- package/package.json +22 -9
- package/dist/index-q6ja2qwq.js +0 -7
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forbidden,
|
|
3
|
+
unauthorized
|
|
4
|
+
} from "./index-afzt3nmx.js";
|
|
5
|
+
import {
|
|
6
|
+
isUnsafeKey,
|
|
7
|
+
safeJsonParse
|
|
8
|
+
} from "./index-kzfs85xp.js";
|
|
9
|
+
import {
|
|
10
|
+
isRecord
|
|
11
|
+
} from "./index-809wc1tt.js";
|
|
12
|
+
|
|
13
|
+
// src/server/middleware/cookies.ts
|
|
14
|
+
function parseCookies(header) {
|
|
15
|
+
if (!header)
|
|
16
|
+
return {};
|
|
17
|
+
const cookies = {};
|
|
18
|
+
for (const pair of header.split(";")) {
|
|
19
|
+
const eq = pair.indexOf("=");
|
|
20
|
+
if (eq === -1)
|
|
21
|
+
continue;
|
|
22
|
+
const key = pair.slice(0, eq).trim();
|
|
23
|
+
if (!key || isUnsafeKey(key))
|
|
24
|
+
continue;
|
|
25
|
+
const raw = pair.slice(eq + 1).trim();
|
|
26
|
+
try {
|
|
27
|
+
cookies[key] = decodeURIComponent(raw);
|
|
28
|
+
} catch {
|
|
29
|
+
cookies[key] = raw;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return cookies;
|
|
33
|
+
}
|
|
34
|
+
function serializeCookie(name, value, opts = {}) {
|
|
35
|
+
let cookie = `${name}=${encodeURIComponent(value)}`;
|
|
36
|
+
if (opts.maxAge !== undefined)
|
|
37
|
+
cookie += `; Max-Age=${opts.maxAge}`;
|
|
38
|
+
if (opts.path)
|
|
39
|
+
cookie += `; Path=${opts.path}`;
|
|
40
|
+
if (opts.domain)
|
|
41
|
+
cookie += `; Domain=${opts.domain}`;
|
|
42
|
+
if (opts.secure)
|
|
43
|
+
cookie += "; Secure";
|
|
44
|
+
if (opts.httpOnly)
|
|
45
|
+
cookie += "; HttpOnly";
|
|
46
|
+
if (opts.sameSite)
|
|
47
|
+
cookie += `; SameSite=${opts.sameSite}`;
|
|
48
|
+
return cookie;
|
|
49
|
+
}
|
|
50
|
+
function defineCookie(config) {
|
|
51
|
+
const { name, ...options } = config;
|
|
52
|
+
return {
|
|
53
|
+
get(req) {
|
|
54
|
+
return parseCookies(req.headers.get("cookie"))[name];
|
|
55
|
+
},
|
|
56
|
+
set(value) {
|
|
57
|
+
return serializeCookie(name, value, options);
|
|
58
|
+
},
|
|
59
|
+
clear() {
|
|
60
|
+
return serializeCookie(name, "", { ...options, maxAge: 0 });
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/server/middleware/auth.ts
|
|
66
|
+
var MAX_TOKEN_BYTES = 8192;
|
|
67
|
+
function decodeBase64Url(segment) {
|
|
68
|
+
if (!/^[A-Za-z0-9_-]*$/.test(segment) || segment.length % 4 === 1) {
|
|
69
|
+
throw new Error("invalid base64url segment");
|
|
70
|
+
}
|
|
71
|
+
const b64 = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
72
|
+
const padded = b64.padEnd(Math.ceil(b64.length / 4) * 4, "=");
|
|
73
|
+
return Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
|
|
74
|
+
}
|
|
75
|
+
async function verifyJwt(token, secret, options = {}) {
|
|
76
|
+
if (!secret)
|
|
77
|
+
throw new Error("verifyJwt: a non-empty secret is required");
|
|
78
|
+
if (token.length > MAX_TOKEN_BYTES)
|
|
79
|
+
throw unauthorized("Token too large");
|
|
80
|
+
const [headerB64, payloadB64, signatureB64] = token.split(".");
|
|
81
|
+
if (!headerB64 || !payloadB64 || !signatureB64)
|
|
82
|
+
throw unauthorized("Invalid token format");
|
|
83
|
+
let header;
|
|
84
|
+
let payloadRaw;
|
|
85
|
+
let signature;
|
|
86
|
+
try {
|
|
87
|
+
const decoder = new TextDecoder;
|
|
88
|
+
header = JSON.parse(decoder.decode(decodeBase64Url(headerB64)));
|
|
89
|
+
payloadRaw = safeJsonParse(decoder.decode(decodeBase64Url(payloadB64)));
|
|
90
|
+
signature = decodeBase64Url(signatureB64);
|
|
91
|
+
} catch {
|
|
92
|
+
throw unauthorized("Malformed token");
|
|
93
|
+
}
|
|
94
|
+
if (!isRecord(payloadRaw))
|
|
95
|
+
throw unauthorized("Malformed token");
|
|
96
|
+
const payload = payloadRaw;
|
|
97
|
+
if (header.alg !== "HS256")
|
|
98
|
+
throw unauthorized("Unsupported token algorithm");
|
|
99
|
+
const encoder = new TextEncoder;
|
|
100
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
|
|
101
|
+
const data = encoder.encode(`${headerB64}.${payloadB64}`);
|
|
102
|
+
const valid = await crypto.subtle.verify("HMAC", key, signature, data);
|
|
103
|
+
if (!valid)
|
|
104
|
+
throw unauthorized("Invalid token signature");
|
|
105
|
+
const leeway = options.leewaySeconds ?? 60;
|
|
106
|
+
const now = Date.now() / 1000;
|
|
107
|
+
if ("exp" in payload) {
|
|
108
|
+
if (typeof payload.exp !== "number")
|
|
109
|
+
throw unauthorized("Malformed token");
|
|
110
|
+
if (payload.exp < now - leeway)
|
|
111
|
+
throw unauthorized("Token expired");
|
|
112
|
+
}
|
|
113
|
+
if ("nbf" in payload) {
|
|
114
|
+
if (typeof payload.nbf !== "number")
|
|
115
|
+
throw unauthorized("Malformed token");
|
|
116
|
+
if (payload.nbf > now + leeway)
|
|
117
|
+
throw unauthorized("Token not yet valid");
|
|
118
|
+
}
|
|
119
|
+
if (options.issuer !== undefined && payload.iss !== options.issuer) {
|
|
120
|
+
throw unauthorized("Token issuer mismatch");
|
|
121
|
+
}
|
|
122
|
+
if (options.audience !== undefined) {
|
|
123
|
+
const aud = payload.aud;
|
|
124
|
+
const ok = Array.isArray(aud) ? aud.includes(options.audience) : aud === options.audience;
|
|
125
|
+
if (!ok)
|
|
126
|
+
throw unauthorized("Token audience mismatch");
|
|
127
|
+
}
|
|
128
|
+
return payload;
|
|
129
|
+
}
|
|
130
|
+
function encodeBase64Url(bytes) {
|
|
131
|
+
let binary = "";
|
|
132
|
+
for (const byte of bytes)
|
|
133
|
+
binary += String.fromCharCode(byte);
|
|
134
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
135
|
+
}
|
|
136
|
+
async function signJwt(payload, secret, options = {}) {
|
|
137
|
+
if (!secret)
|
|
138
|
+
throw new Error("signJwt: a non-empty secret is required");
|
|
139
|
+
const now = Math.floor(Date.now() / 1000);
|
|
140
|
+
const claims = {
|
|
141
|
+
...payload,
|
|
142
|
+
iat: now,
|
|
143
|
+
...options.expiresInSec !== undefined && { exp: now + options.expiresInSec },
|
|
144
|
+
...options.issuer !== undefined && { iss: options.issuer },
|
|
145
|
+
...options.audience !== undefined && { aud: options.audience },
|
|
146
|
+
...options.subject !== undefined && { sub: options.subject }
|
|
147
|
+
};
|
|
148
|
+
const encoder = new TextEncoder;
|
|
149
|
+
const headerB64 = encodeBase64Url(encoder.encode(JSON.stringify({ alg: "HS256", typ: "JWT" })));
|
|
150
|
+
const payloadB64 = encodeBase64Url(encoder.encode(JSON.stringify(claims)));
|
|
151
|
+
const signingInput = `${headerB64}.${payloadB64}`;
|
|
152
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
153
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(signingInput));
|
|
154
|
+
const signatureB64 = encodeBase64Url(new Uint8Array(signature));
|
|
155
|
+
return `${signingInput}.${signatureB64}`;
|
|
156
|
+
}
|
|
157
|
+
function extractToken(req, cookieName) {
|
|
158
|
+
const authHeader = req.headers.get("authorization");
|
|
159
|
+
if (authHeader?.startsWith("Bearer "))
|
|
160
|
+
return authHeader.slice(7);
|
|
161
|
+
if (cookieName) {
|
|
162
|
+
const cookies = parseCookies(req.headers.get("cookie"));
|
|
163
|
+
return cookies[cookieName] ?? null;
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
function createAuthHook(config) {
|
|
168
|
+
const onAnonymous = config.onAnonymous ?? (() => unauthorized());
|
|
169
|
+
const onForbidden = config.onForbidden ?? (() => forbidden());
|
|
170
|
+
return async (ctx, endpoint) => {
|
|
171
|
+
const identity = ctx.source === "http" ? await config.resolve(ctx) : await config.resolveFromContext?.(ctx) ?? null;
|
|
172
|
+
config.inject?.(ctx, identity);
|
|
173
|
+
const scope = endpoint.scope ?? config.defaultScope;
|
|
174
|
+
if (!scope)
|
|
175
|
+
return;
|
|
176
|
+
const rule = config.rules[scope];
|
|
177
|
+
if (!rule) {
|
|
178
|
+
throw new Error(`[stitchkit] auth: no rule for scope "${scope}"`);
|
|
179
|
+
}
|
|
180
|
+
if (rule === "public")
|
|
181
|
+
return;
|
|
182
|
+
if (!identity) {
|
|
183
|
+
onAnonymous();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (rule === "authenticated")
|
|
187
|
+
return;
|
|
188
|
+
if (!await rule(identity, ctx))
|
|
189
|
+
onForbidden();
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function createBearerResolver(config) {
|
|
193
|
+
return async (req) => {
|
|
194
|
+
const token = extractToken(req);
|
|
195
|
+
if (!token)
|
|
196
|
+
return null;
|
|
197
|
+
return config.lookup(token, req);
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/server/middleware/pkce.ts
|
|
202
|
+
function base64UrlEncode(bytes) {
|
|
203
|
+
let binary = "";
|
|
204
|
+
for (const byte of bytes)
|
|
205
|
+
binary += String.fromCharCode(byte);
|
|
206
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
207
|
+
}
|
|
208
|
+
async function deriveCodeChallenge(verifier) {
|
|
209
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
210
|
+
return base64UrlEncode(new Uint8Array(digest));
|
|
211
|
+
}
|
|
212
|
+
async function verifyPkce(verifier, challenge) {
|
|
213
|
+
if (!verifier || !challenge)
|
|
214
|
+
return false;
|
|
215
|
+
const derived = await deriveCodeChallenge(verifier);
|
|
216
|
+
return derived === challenge;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export { parseCookies, serializeCookie, defineCookie, verifyJwt, signJwt, extractToken, createAuthHook, createBearerResolver, deriveCodeChallenge, verifyPkce };
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isWithinDir
|
|
3
|
-
} from "./index-
|
|
3
|
+
} from "./index-x3fcszf8.js";
|
|
4
4
|
import {
|
|
5
5
|
normalizeError,
|
|
6
6
|
validateHandlerOutput
|
|
7
|
-
} from "./index-
|
|
7
|
+
} from "./index-vhdvv00d.js";
|
|
8
8
|
import {
|
|
9
9
|
AppError,
|
|
10
10
|
badRequest
|
|
11
|
-
} from "./index-
|
|
11
|
+
} from "./index-afzt3nmx.js";
|
|
12
12
|
import {
|
|
13
13
|
extractIp,
|
|
14
14
|
getClientInfo,
|
|
@@ -20,6 +20,12 @@ import {
|
|
|
20
20
|
isUnsafeKey,
|
|
21
21
|
safeJsonParse
|
|
22
22
|
} from "./index-kzfs85xp.js";
|
|
23
|
+
import {
|
|
24
|
+
typedEntries
|
|
25
|
+
} from "./index-809wc1tt.js";
|
|
26
|
+
import {
|
|
27
|
+
__require
|
|
28
|
+
} from "./index-37x76zdn.js";
|
|
23
29
|
|
|
24
30
|
// src/server/multipart.ts
|
|
25
31
|
var DEFAULT_MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
|
|
@@ -674,4 +680,150 @@ function json(data, status, cors, req) {
|
|
|
674
680
|
return Response.json(data, { status, headers: corsHeaders2(cors, req) });
|
|
675
681
|
}
|
|
676
682
|
|
|
677
|
-
|
|
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,
|
|
@@ -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);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// src/internal/within-dir.ts
|
|
2
|
+
import { sep } from "node:path";
|
|
3
|
+
function isWithinDir(root, target) {
|
|
4
|
+
const base = root.endsWith(sep) ? root.slice(0, -sep.length) : root;
|
|
5
|
+
return target === root || target === base || target.startsWith(base + sep);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export { isWithinDir };
|
package/dist/index.js
CHANGED
|
@@ -3,11 +3,12 @@ import {
|
|
|
3
3
|
createClient,
|
|
4
4
|
createClients,
|
|
5
5
|
createHttpClient
|
|
6
|
-
} from "./index-
|
|
6
|
+
} from "./index-78q1qm7v.js";
|
|
7
7
|
import {
|
|
8
8
|
parseSSE
|
|
9
|
-
} from "./index-
|
|
10
|
-
import"./index-
|
|
9
|
+
} from "./index-d4rwrjbc.js";
|
|
10
|
+
import"./index-48ffdxgk.js";
|
|
11
|
+
import"./index-vhdvv00d.js";
|
|
11
12
|
import {
|
|
12
13
|
ALL_TRANSPORTS,
|
|
13
14
|
AppError,
|
|
@@ -20,10 +21,18 @@ import {
|
|
|
20
21
|
paginatedSchema,
|
|
21
22
|
rateLimited,
|
|
22
23
|
unauthorized
|
|
23
|
-
} from "./index-
|
|
24
|
+
} from "./index-afzt3nmx.js";
|
|
24
25
|
import"./index-809wc1tt.js";
|
|
26
|
+
import"./index-37x76zdn.js";
|
|
25
27
|
// src/browser/socket-io.ts
|
|
26
28
|
import { io } from "socket.io-client";
|
|
29
|
+
function toIoAuth(auth) {
|
|
30
|
+
if (typeof auth !== "function")
|
|
31
|
+
return auth;
|
|
32
|
+
return (cb) => {
|
|
33
|
+
Promise.resolve().then(auth).then(cb, () => cb({}));
|
|
34
|
+
};
|
|
35
|
+
}
|
|
27
36
|
function createSocketIOClient(config) {
|
|
28
37
|
let socket = null;
|
|
29
38
|
const connectionListeners = new Set;
|
|
@@ -42,6 +51,9 @@ function createSocketIOClient(config) {
|
|
|
42
51
|
socket = io(config.url, {
|
|
43
52
|
path: config.path ?? "/socket.io/",
|
|
44
53
|
withCredentials: config.withCredentials ?? true,
|
|
54
|
+
auth: toIoAuth(config.auth),
|
|
55
|
+
...config.query && { query: config.query },
|
|
56
|
+
...config.extraHeaders && { extraHeaders: config.extraHeaders },
|
|
45
57
|
transports: config.transports ?? ["websocket", "polling"],
|
|
46
58
|
autoConnect: false,
|
|
47
59
|
reconnection: true,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a method's input travels over HTTP — the single source of truth for the
|
|
3
|
+
* convention shared by the typed client (what it SENDS) and the OpenAPI
|
|
4
|
+
* generator (what it DOCUMENTS), so the two cannot drift. GET and DELETE carry
|
|
5
|
+
* their input as query parameters; the body verbs (POST / PUT / PATCH) carry it
|
|
6
|
+
* as a request body.
|
|
7
|
+
*
|
|
8
|
+
* The server runtime is a tolerant superset of this: it additionally accepts a
|
|
9
|
+
* DELETE *body* when the request carries an `application/json` content-type. The
|
|
10
|
+
* canonical form a client uses and the spec documents is the query form.
|
|
11
|
+
*/
|
|
12
|
+
export declare function inputIsQuery(method: string): boolean;
|
|
13
|
+
//# sourceMappingURL=http-input.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-input.d.ts","sourceRoot":"","sources":["../../src/internal/http-input.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEpD"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** True for a loopback / private / link-local / ULA / CGNAT IP literal. */
|
|
2
|
+
export declare function isPrivateIp(ip: string): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Refuse a URL that is not `http(s)`, has no host, or whose host is — or
|
|
5
|
+
* resolves to — a private/internal address. Called on the initial URL and on
|
|
6
|
+
* every redirect hop, so a public host cannot `302` to an internal address or
|
|
7
|
+
* to a `file:` / `gopher:` / `data:` scheme.
|
|
8
|
+
*/
|
|
9
|
+
export declare function assertPublicUrl(url: URL): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Fetch a URL with the SSRF guard enforced on the initial URL and on every
|
|
12
|
+
* redirect hop — a public host can otherwise `302` to an internal address or a
|
|
13
|
+
* non-http(s) scheme.
|
|
14
|
+
*
|
|
15
|
+
* Note: the DNS resolution here and the resolution `fetch` performs are
|
|
16
|
+
* separate, so a hostile resolver retains a narrow rebinding window. The
|
|
17
|
+
* per-hop re-check is the larger and fully-closed hole; a static private DNS
|
|
18
|
+
* record is rejected outright.
|
|
19
|
+
*/
|
|
20
|
+
export declare function fetchGuarded(start: URL, allowPrivate: boolean): Promise<Response>;
|
|
21
|
+
/**
|
|
22
|
+
* Read a response body into a buffer, aborting (and returning `null`) if it
|
|
23
|
+
* exceeds `max` bytes — the size cap that keeps a hostile or unbounded stream
|
|
24
|
+
* from OOM-ing the process. A missing body yields an empty buffer.
|
|
25
|
+
*/
|
|
26
|
+
export declare function readCapped(res: Response, max: number): Promise<Buffer | null>;
|
|
27
|
+
//# sourceMappingURL=secure-fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secure-fetch.d.ts","sourceRoot":"","sources":["../../src/internal/secure-fetch.ts"],"names":[],"mappings":"AA8CA,2EAA2E;AAC3E,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAkC/C;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CA0B7D;AAED;;;;;;;;;GASG;AACH,wBAAsB,YAAY,CAAC,KAAK,EAAE,GAAG,EAAE,YAAY,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAavF;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAoBnF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"within-dir.d.ts","sourceRoot":"","sources":["../../src/internal/within-dir.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"within-dir.d.ts","sourceRoot":"","sources":["../../src/internal/within-dir.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAQjE"}
|
package/dist/node.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `stitchkit/node` — the Node entrypoint. Everything a Node app needs without
|
|
3
|
+
* touching the Bun-named `createServer`: the Fetch-clean `createHandler`, the
|
|
4
|
+
* srvx-backed `serveNode`, `implement`, the error helpers and (lazily-loaded,
|
|
5
|
+
* Node-safe) `createSocketIOServer`.
|
|
6
|
+
*/
|
|
7
|
+
export { AppError, appError, badRequest, conflict, forbidden, notFound, rateLimited, unauthorized, } from './contract';
|
|
1
8
|
export { createHandler } from './server/create';
|
|
9
|
+
export { createImplement, implement } from './server/implement';
|
|
2
10
|
export { type NodeServerConfig, type NodeServerHandle, serveNode } from './server/node';
|
|
3
|
-
export type
|
|
11
|
+
export { createSocketIOServer, type SocketIOServerConfig, type SocketIOServerHandle, } from './server/socket-io';
|
|
12
|
+
export type { HandlerConfig, RawRoute, RawRouteContext, ServiceDef, } from './server/types';
|
|
4
13
|
//# sourceMappingURL=node.d.ts.map
|
package/dist/node.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AACxF,YAAY,
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,WAAW,EACX,YAAY,GACb,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AACxF,OAAO,EACL,oBAAoB,EACpB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,GAC1B,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,aAAa,EACb,QAAQ,EACR,eAAe,EACf,UAAU,GACX,MAAM,gBAAgB,CAAC"}
|
package/dist/node.js
CHANGED
|
@@ -1,19 +1,39 @@
|
|
|
1
1
|
import {
|
|
2
|
-
createHandler
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
createHandler,
|
|
3
|
+
createImplement,
|
|
4
|
+
createSocketIOServer,
|
|
5
|
+
implement
|
|
6
|
+
} from "./index-3qmtdfpa.js";
|
|
7
|
+
import"./index-x3fcszf8.js";
|
|
8
|
+
import"./index-vhdvv00d.js";
|
|
9
|
+
import {
|
|
10
|
+
AppError,
|
|
11
|
+
appError,
|
|
12
|
+
badRequest,
|
|
13
|
+
conflict,
|
|
14
|
+
forbidden,
|
|
15
|
+
notFound,
|
|
16
|
+
rateLimited,
|
|
17
|
+
unauthorized
|
|
18
|
+
} from "./index-afzt3nmx.js";
|
|
7
19
|
import"./index-mwmpw6j1.js";
|
|
8
20
|
import"./index-kzfs85xp.js";
|
|
9
21
|
import"./index-809wc1tt.js";
|
|
22
|
+
import"./index-37x76zdn.js";
|
|
10
23
|
// src/server/node.ts
|
|
11
24
|
import { serve } from "srvx";
|
|
12
25
|
async function serveNode(config) {
|
|
13
|
-
const { port = 3000, hostname, ...handlerConfig } = config;
|
|
26
|
+
const { port = 3000, hostname, socket, ...handlerConfig } = config;
|
|
14
27
|
const handler = createHandler(handlerConfig);
|
|
15
28
|
const server = serve({ port, hostname, fetch: handler });
|
|
16
29
|
await server.ready();
|
|
30
|
+
if (socket) {
|
|
31
|
+
const nodeServer = server.node?.server;
|
|
32
|
+
if (!nodeServer || !("maxRequestsPerSocket" in nodeServer)) {
|
|
33
|
+
throw new Error("[stitchkit] serveNode: expected a node:http.Server for Socket.IO (none / http2).");
|
|
34
|
+
}
|
|
35
|
+
socket.attach(nodeServer);
|
|
36
|
+
}
|
|
17
37
|
const listenUrl = server.url ?? `http://${hostname ?? "localhost"}:${port}`;
|
|
18
38
|
const resolvedPort = Number(new URL(listenUrl).port) || port;
|
|
19
39
|
const resolvedHost = hostname ?? "localhost";
|
|
@@ -24,6 +44,17 @@ async function serveNode(config) {
|
|
|
24
44
|
};
|
|
25
45
|
}
|
|
26
46
|
export {
|
|
47
|
+
unauthorized,
|
|
27
48
|
serveNode,
|
|
28
|
-
|
|
49
|
+
rateLimited,
|
|
50
|
+
notFound,
|
|
51
|
+
implement,
|
|
52
|
+
forbidden,
|
|
53
|
+
createSocketIOServer,
|
|
54
|
+
createImplement,
|
|
55
|
+
createHandler,
|
|
56
|
+
conflict,
|
|
57
|
+
badRequest,
|
|
58
|
+
appError,
|
|
59
|
+
AppError
|
|
29
60
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/observability/audit.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,kBAAkB,CAAC;AAElE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAe,KAAK,eAAe,EAAmB,MAAM,YAAY,CAAC;AAGhF,oCAAoC;AACpC,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,KAAK,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,iFAAiF;IACjF,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC;IAC1C,yEAAyE;IACzE,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B;AAED,qEAAqE;AACrE,MAAM,WAAW,SAAS;IACxB;;;;OAIG;IACH,IAAI,EAAE,CAAC,CAAC,EACN,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,KACpD,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpD;;;OAGG;IACH,QAAQ,EAAE,aAAa,CAAC;CACzB;AAmBD,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/observability/audit.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,kBAAkB,CAAC;AAElE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAe,KAAK,eAAe,EAAmB,MAAM,YAAY,CAAC;AAGhF,oCAAoC;AACpC,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,KAAK,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,iFAAiF;IACjF,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC;IAC1C,yEAAyE;IACzE,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B;AAED,qEAAqE;AACrE,MAAM,WAAW,SAAS;IACxB;;;;OAIG;IACH,IAAI,EAAE,CAAC,CAAC,EACN,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,KACpD,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpD;;;OAGG;IACH,QAAQ,EAAE,aAAa,CAAC;CACzB;AAmBD,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,CA+F9D"}
|
|
@@ -39,6 +39,10 @@ export interface RequestEvent {
|
|
|
39
39
|
responseBytes: number;
|
|
40
40
|
/** Resolved user id, when authenticated. */
|
|
41
41
|
userId?: string;
|
|
42
|
+
/** How the caller authenticated, when the app records it on the context. */
|
|
43
|
+
authMethod?: string;
|
|
44
|
+
/** Client id the call was made under (e.g. an OAuth client), when recorded. */
|
|
45
|
+
clientId?: string;
|
|
42
46
|
/** Client IP. */
|
|
43
47
|
ipAddress?: string;
|
|
44
48
|
/** Client user-agent. */
|