@forklaunch/core 0.10.4 → 0.11.1
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/lib/http/index.d.mts +118 -121
- package/lib/http/index.d.ts +118 -121
- package/lib/http/index.js +180 -99
- package/lib/http/index.js.map +1 -1
- package/lib/http/index.mjs +141 -61
- package/lib/http/index.mjs.map +1 -1
- package/package.json +19 -19
package/lib/http/index.mjs
CHANGED
@@ -66,7 +66,47 @@ function isTypedHandler(maybeTypedHandler) {
|
|
66
66
|
}
|
67
67
|
|
68
68
|
// src/http/middleware/request/auth.middleware.ts
|
69
|
+
import { isNever } from "@forklaunch/common";
|
69
70
|
import { jwtVerify } from "jose";
|
71
|
+
|
72
|
+
// src/http/discriminateAuthMethod.ts
|
73
|
+
function discriminateAuthMethod(auth) {
|
74
|
+
if ("basic" in auth) {
|
75
|
+
return {
|
76
|
+
type: "basic",
|
77
|
+
auth: {
|
78
|
+
decodeResource: auth.decodeResource,
|
79
|
+
login: auth.basic.login
|
80
|
+
}
|
81
|
+
};
|
82
|
+
} else if ("jwt" in auth) {
|
83
|
+
return {
|
84
|
+
type: "jwt",
|
85
|
+
auth: {
|
86
|
+
decodeResource: auth.decodeResource
|
87
|
+
}
|
88
|
+
};
|
89
|
+
} else {
|
90
|
+
return {
|
91
|
+
type: "jwt",
|
92
|
+
auth: {
|
93
|
+
decodeResource: auth.decodeResource
|
94
|
+
}
|
95
|
+
};
|
96
|
+
}
|
97
|
+
}
|
98
|
+
|
99
|
+
// src/http/guards/hasPermissionChecks.ts
|
100
|
+
function hasPermissionChecks(maybePermissionedAuth) {
|
101
|
+
return typeof maybePermissionedAuth === "object" && maybePermissionedAuth !== null && "mapPermissions" in maybePermissionedAuth && ("allowedPermissions" in maybePermissionedAuth || "forbiddenPermissions" in maybePermissionedAuth);
|
102
|
+
}
|
103
|
+
|
104
|
+
// src/http/guards/hasRoleChecks.ts
|
105
|
+
function hasRoleChecks(maybeRoledAuth) {
|
106
|
+
return typeof maybeRoledAuth === "object" && maybeRoledAuth !== null && "mapRoles" in maybeRoledAuth && ("allowedRoles" in maybeRoledAuth || "forbiddenRoles" in maybeRoledAuth);
|
107
|
+
}
|
108
|
+
|
109
|
+
// src/http/middleware/request/auth.middleware.ts
|
70
110
|
var invalidAuthorizationTokenFormat = [
|
71
111
|
401,
|
72
112
|
"Invalid Authorization token format."
|
@@ -97,51 +137,52 @@ async function checkAuthorizationToken(authorizationMethod, authorizationToken,
|
|
97
137
|
}
|
98
138
|
const [tokenPrefix, token] = authorizationToken.split(" ");
|
99
139
|
let resourceId;
|
100
|
-
|
140
|
+
const { type, auth } = discriminateAuthMethod(authorizationMethod);
|
141
|
+
switch (type) {
|
101
142
|
case "jwt": {
|
102
|
-
if (tokenPrefix !== "Bearer") {
|
143
|
+
if (tokenPrefix !== (authorizationMethod.tokenPrefix ?? "Bearer")) {
|
103
144
|
return invalidAuthorizationTokenFormat;
|
104
145
|
}
|
105
146
|
try {
|
106
|
-
const decodedJwt = await jwtVerify(
|
147
|
+
const decodedJwt = await auth?.decodeResource?.(token) ?? (await jwtVerify(
|
107
148
|
token,
|
108
|
-
new TextEncoder().encode(
|
109
|
-
|
110
|
-
|
111
|
-
)
|
112
|
-
);
|
113
|
-
if (!decodedJwt.payload.sub) {
|
149
|
+
new TextEncoder().encode(process.env.JWT_SECRET)
|
150
|
+
)).payload;
|
151
|
+
if (!decodedJwt) {
|
114
152
|
return invalidAuthorizationSubject;
|
115
153
|
}
|
116
|
-
resourceId = decodedJwt
|
154
|
+
resourceId = decodedJwt;
|
117
155
|
} catch (error) {
|
118
|
-
req
|
156
|
+
req?.openTelemetryCollector.error(error);
|
119
157
|
return invalidAuthorizationToken;
|
120
158
|
}
|
121
159
|
break;
|
122
160
|
}
|
123
161
|
case "basic": {
|
124
|
-
if (
|
125
|
-
return invalidAuthorizationTokenFormat;
|
126
|
-
}
|
127
|
-
const [username, password] = Buffer.from(token, "base64").toString("utf-8").split(":");
|
128
|
-
if (!username || !password) {
|
162
|
+
if (tokenPrefix !== (authorizationMethod.tokenPrefix ?? "Basic")) {
|
129
163
|
return invalidAuthorizationTokenFormat;
|
130
164
|
}
|
131
|
-
if (
|
132
|
-
|
165
|
+
if (auth.decodeResource) {
|
166
|
+
resourceId = await auth.decodeResource(token);
|
167
|
+
} else {
|
168
|
+
const [username, password] = Buffer.from(token, "base64").toString("utf-8").split(":");
|
169
|
+
if (!username || !password) {
|
170
|
+
return invalidAuthorizationTokenFormat;
|
171
|
+
}
|
172
|
+
if (!auth.login(username, password)) {
|
173
|
+
return invalidAuthorizationLogin;
|
174
|
+
}
|
175
|
+
resourceId = {
|
176
|
+
sub: username
|
177
|
+
};
|
133
178
|
}
|
134
|
-
resourceId = username;
|
135
179
|
break;
|
136
180
|
}
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
}
|
141
|
-
resourceId = authorizationMethod.decodeResource(token);
|
142
|
-
break;
|
181
|
+
default:
|
182
|
+
isNever(type);
|
183
|
+
return [401, "Invalid Authorization method."];
|
143
184
|
}
|
144
|
-
if (authorizationMethod
|
185
|
+
if (hasPermissionChecks(authorizationMethod)) {
|
145
186
|
if (!authorizationMethod.mapPermissions) {
|
146
187
|
return [500, "No permission mapping function provided."];
|
147
188
|
}
|
@@ -149,49 +190,49 @@ async function checkAuthorizationToken(authorizationMethod, authorizationToken,
|
|
149
190
|
resourceId,
|
150
191
|
req
|
151
192
|
);
|
152
|
-
if (authorizationMethod.allowedPermissions) {
|
193
|
+
if ("allowedPermissions" in authorizationMethod && authorizationMethod.allowedPermissions) {
|
153
194
|
if (resourcePermissions.intersection(authorizationMethod.allowedPermissions).size === 0) {
|
154
195
|
return invalidAuthorizationTokenPermissions;
|
155
196
|
}
|
156
197
|
}
|
157
|
-
if (authorizationMethod.forbiddenPermissions) {
|
198
|
+
if ("forbiddenPermissions" in authorizationMethod && authorizationMethod.forbiddenPermissions) {
|
158
199
|
if (resourcePermissions.intersection(
|
159
200
|
authorizationMethod.forbiddenPermissions
|
160
201
|
).size !== 0) {
|
161
202
|
return invalidAuthorizationTokenPermissions;
|
162
203
|
}
|
163
204
|
}
|
164
|
-
}
|
165
|
-
if (authorizationMethod.allowedRoles || authorizationMethod.forbiddenRoles) {
|
205
|
+
} else if (hasRoleChecks(authorizationMethod)) {
|
166
206
|
if (!authorizationMethod.mapRoles) {
|
167
207
|
return [500, "No role mapping function provided."];
|
168
208
|
}
|
169
209
|
const resourceRoles = await authorizationMethod.mapRoles(resourceId, req);
|
170
|
-
if (authorizationMethod.allowedRoles) {
|
210
|
+
if ("allowedRoles" in authorizationMethod && authorizationMethod.allowedRoles) {
|
171
211
|
if (resourceRoles.intersection(authorizationMethod.allowedRoles).size === 0) {
|
172
212
|
return invalidAuthorizationTokenRoles;
|
173
213
|
}
|
174
214
|
}
|
175
|
-
if (authorizationMethod.forbiddenRoles) {
|
215
|
+
if ("forbiddenRoles" in authorizationMethod && authorizationMethod.forbiddenRoles) {
|
176
216
|
if (resourceRoles.intersection(authorizationMethod.forbiddenRoles).size !== 0) {
|
177
217
|
return invalidAuthorizationTokenRoles;
|
178
218
|
}
|
179
219
|
}
|
220
|
+
} else {
|
221
|
+
return [401, "Invalid Authorization method."];
|
180
222
|
}
|
181
|
-
return [401, "Invalid Authorization method."];
|
182
223
|
}
|
183
224
|
async function parseRequestAuth(req, res, next) {
|
184
225
|
const auth = req.contractDetails.auth;
|
185
226
|
if (auth) {
|
186
227
|
const [error, message] = await checkAuthorizationToken(
|
187
228
|
auth,
|
188
|
-
req.headers[
|
229
|
+
req.headers[auth.headerName ?? "Authorization"] || req.headers[auth.headerName ?? "authorization"],
|
189
230
|
req
|
190
231
|
) ?? [];
|
191
232
|
if (error != null) {
|
192
233
|
res.type("text/plain");
|
193
234
|
res.status(error).send(message);
|
194
|
-
|
235
|
+
return;
|
195
236
|
}
|
196
237
|
}
|
197
238
|
next?.();
|
@@ -262,7 +303,7 @@ function isForklaunchRequest(request) {
|
|
262
303
|
}
|
263
304
|
|
264
305
|
// src/http/telemetry/pinoLogger.ts
|
265
|
-
import { isNever, safeStringify } from "@forklaunch/common";
|
306
|
+
import { isNever as isNever2, safeStringify } from "@forklaunch/common";
|
266
307
|
import { trace as trace2 } from "@opentelemetry/api";
|
267
308
|
import { logs } from "@opentelemetry/api-logs";
|
268
309
|
import pino from "pino";
|
@@ -294,7 +335,7 @@ function mapSeverity(level) {
|
|
294
335
|
case "fatal":
|
295
336
|
return 21;
|
296
337
|
default:
|
297
|
-
|
338
|
+
isNever2(level);
|
298
339
|
return 0;
|
299
340
|
}
|
300
341
|
}
|
@@ -540,7 +581,18 @@ function parse(req, res, next) {
|
|
540
581
|
enumerable: true,
|
541
582
|
configurable: false
|
542
583
|
});
|
543
|
-
|
584
|
+
const parsedHeaders = parsedRequest.value.headers ?? {};
|
585
|
+
req.headers = Object.keys(req.headers).reduce(
|
586
|
+
(acc, key) => {
|
587
|
+
if (parsedHeaders?.[key]) {
|
588
|
+
acc[key] = parsedHeaders[key];
|
589
|
+
} else {
|
590
|
+
acc[key] = req.headers[key];
|
591
|
+
}
|
592
|
+
return acc;
|
593
|
+
},
|
594
|
+
{}
|
595
|
+
);
|
544
596
|
}
|
545
597
|
if (!parsedRequest.ok) {
|
546
598
|
switch (req.contractDetails.options?.requestValidation) {
|
@@ -1457,6 +1509,11 @@ var trace3 = (_schemaValidator, path, contractDetails, ...handlers) => {
|
|
1457
1509
|
return typedHandler(_schemaValidator, path, "trace", contractDetails, ...handlers);
|
1458
1510
|
};
|
1459
1511
|
|
1512
|
+
// src/http/handlers/typedAuthHandler.ts
|
1513
|
+
function typedAuthHandler(_schemaValidator, _contractDetails, authHandler) {
|
1514
|
+
return authHandler;
|
1515
|
+
}
|
1516
|
+
|
1460
1517
|
// src/http/httpStatusCodes.ts
|
1461
1518
|
var HTTPStatuses = {
|
1462
1519
|
/**
|
@@ -2441,8 +2498,8 @@ var getCodeForStatus = (status) => {
|
|
2441
2498
|
var httpStatusCodes_default = HTTPStatuses;
|
2442
2499
|
|
2443
2500
|
// src/http/mcpGenerator/mcpGenerator.ts
|
2444
|
-
import { isNever as
|
2445
|
-
import { ZodSchemaValidator } from "@forklaunch/validator/zod";
|
2501
|
+
import { isNever as isNever3, isRecord, safeStringify as safeStringify2 } from "@forklaunch/common";
|
2502
|
+
import { string, ZodSchemaValidator } from "@forklaunch/validator/zod";
|
2446
2503
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
2447
2504
|
|
2448
2505
|
// src/http/router/unpackRouters.ts
|
@@ -2499,14 +2556,15 @@ function generateMcpServer(schemaValidator, protocol, host, port, version, route
|
|
2499
2556
|
...route.contractDetails.params ? { params: schemaValidator.schemify(route.contractDetails.params) } : {},
|
2500
2557
|
...route.contractDetails.query ? { query: schemaValidator.schemify(route.contractDetails.query) } : {},
|
2501
2558
|
...route.contractDetails.requestHeaders ? {
|
2502
|
-
headers: schemaValidator.schemify(
|
2503
|
-
route.contractDetails.requestHeaders
|
2504
|
-
|
2559
|
+
headers: schemaValidator.schemify({
|
2560
|
+
...route.contractDetails.requestHeaders,
|
2561
|
+
...route.contractDetails.auth ? {
|
2562
|
+
[route.contractDetails.auth.headerName ?? "authorization"]: string.startsWith(
|
2563
|
+
route.contractDetails.auth.tokenPrefix ?? ("basic" in route.contractDetails.auth ? "Basic " : "Bearer ")
|
2564
|
+
)
|
2565
|
+
} : {}
|
2566
|
+
})
|
2505
2567
|
} : {}
|
2506
|
-
// TODO: support auth
|
2507
|
-
// ...(route.contractDetails.auth
|
2508
|
-
// ? { auth: route.contractDetails.auth }
|
2509
|
-
// : {})
|
2510
2568
|
};
|
2511
2569
|
mcpServer.tool(
|
2512
2570
|
route.contractDetails.name,
|
@@ -2568,7 +2626,7 @@ function generateMcpServer(schemaValidator, protocol, host, port, version, route
|
|
2568
2626
|
break;
|
2569
2627
|
}
|
2570
2628
|
default: {
|
2571
|
-
|
2629
|
+
isNever3(discriminatedBody.parserType);
|
2572
2630
|
parsedBody = safeStringify2(body);
|
2573
2631
|
break;
|
2574
2632
|
}
|
@@ -2718,7 +2776,7 @@ ${parseErrors.join("\n\n")}`
|
|
2718
2776
|
// src/http/middleware/response/enrichExpressLikeSend.middleware.ts
|
2719
2777
|
import {
|
2720
2778
|
isAsyncGenerator,
|
2721
|
-
isNever as
|
2779
|
+
isNever as isNever4,
|
2722
2780
|
isNodeJsWriteableStream,
|
2723
2781
|
isRecord as isRecord2,
|
2724
2782
|
readableStreamToAsyncIterable,
|
@@ -2855,7 +2913,7 @@ ${res.locals.errorMessage}`;
|
|
2855
2913
|
res.bodyData = data;
|
2856
2914
|
break;
|
2857
2915
|
default:
|
2858
|
-
|
2916
|
+
isNever4(parserType);
|
2859
2917
|
res.bodyData = data;
|
2860
2918
|
break;
|
2861
2919
|
}
|
@@ -2901,7 +2959,7 @@ function transformBasePath(basePath) {
|
|
2901
2959
|
}
|
2902
2960
|
return `/${basePath}`;
|
2903
2961
|
}
|
2904
|
-
function generateOpenApiDocument(protocol, host, port, tags, paths, otherServers) {
|
2962
|
+
function generateOpenApiDocument(protocol, host, port, tags, paths, securitySchemes, otherServers) {
|
2905
2963
|
return {
|
2906
2964
|
openapi: "3.1.0",
|
2907
2965
|
info: {
|
@@ -2909,13 +2967,7 @@ function generateOpenApiDocument(protocol, host, port, tags, paths, otherServers
|
|
2909
2967
|
version: process.env.VERSION || "1.0.0"
|
2910
2968
|
},
|
2911
2969
|
components: {
|
2912
|
-
securitySchemes
|
2913
|
-
bearer: {
|
2914
|
-
type: "http",
|
2915
|
-
scheme: "bearer",
|
2916
|
-
bearerFormat: "JWT"
|
2917
|
-
}
|
2918
|
-
}
|
2970
|
+
securitySchemes
|
2919
2971
|
},
|
2920
2972
|
tags,
|
2921
2973
|
servers: [
|
@@ -2947,6 +2999,7 @@ function contentResolver(schemaValidator, body, contentType) {
|
|
2947
2999
|
function generateSwaggerDocument(schemaValidator, protocol, host, port, routers, otherServers) {
|
2948
3000
|
const tags = [];
|
2949
3001
|
const paths = {};
|
3002
|
+
const securitySchemes = {};
|
2950
3003
|
unpackRouters(routers).forEach(({ fullPath, router, sdkPath }) => {
|
2951
3004
|
const controllerName = transformBasePath(fullPath);
|
2952
3005
|
tags.push({
|
@@ -3043,14 +3096,39 @@ function generateSwaggerDocument(schemaValidator, protocol, host, port, routers,
|
|
3043
3096
|
description: httpStatusCodes_default[403],
|
3044
3097
|
content: contentResolver(schemaValidator, schemaValidator.string)
|
3045
3098
|
};
|
3046
|
-
if (route.contractDetails.auth
|
3099
|
+
if ("basic" in route.contractDetails.auth) {
|
3100
|
+
operationObject.security = [
|
3101
|
+
{
|
3102
|
+
basic: Array.from(
|
3103
|
+
"allowedPermissions" in route.contractDetails.auth ? route.contractDetails.auth.allowedPermissions?.values() || [] : []
|
3104
|
+
)
|
3105
|
+
}
|
3106
|
+
];
|
3107
|
+
securitySchemes["basic"] = {
|
3108
|
+
type: "http",
|
3109
|
+
scheme: "basic"
|
3110
|
+
};
|
3111
|
+
} else if (route.contractDetails.auth) {
|
3047
3112
|
operationObject.security = [
|
3048
3113
|
{
|
3049
|
-
bearer: Array.from(
|
3050
|
-
route.contractDetails.auth.allowedPermissions?.values() || []
|
3114
|
+
[route.contractDetails.auth.headerName !== "Authorization" ? "bearer" : "apiKey"]: Array.from(
|
3115
|
+
"allowedPermissions" in route.contractDetails.auth ? route.contractDetails.auth.allowedPermissions?.values() || [] : []
|
3051
3116
|
)
|
3052
3117
|
}
|
3053
3118
|
];
|
3119
|
+
if (route.contractDetails.auth.headerName && route.contractDetails.auth.headerName !== "Authorization") {
|
3120
|
+
securitySchemes[route.contractDetails.auth.headerName] = {
|
3121
|
+
type: "apiKey",
|
3122
|
+
in: "header",
|
3123
|
+
name: route.contractDetails.auth.headerName
|
3124
|
+
};
|
3125
|
+
} else {
|
3126
|
+
securitySchemes["Authorization"] = {
|
3127
|
+
type: "http",
|
3128
|
+
scheme: "bearer",
|
3129
|
+
bearerFormat: "JWT"
|
3130
|
+
};
|
3131
|
+
}
|
3054
3132
|
}
|
3055
3133
|
}
|
3056
3134
|
if (route.method !== "middleware") {
|
@@ -3064,6 +3142,7 @@ function generateSwaggerDocument(schemaValidator, protocol, host, port, routers,
|
|
3064
3142
|
port,
|
3065
3143
|
tags,
|
3066
3144
|
paths,
|
3145
|
+
securitySchemes,
|
3067
3146
|
otherServers
|
3068
3147
|
);
|
3069
3148
|
}
|
@@ -3129,6 +3208,7 @@ export {
|
|
3129
3208
|
put,
|
3130
3209
|
recordMetric,
|
3131
3210
|
trace3 as trace,
|
3211
|
+
typedAuthHandler,
|
3132
3212
|
typedHandler
|
3133
3213
|
};
|
3134
3214
|
//# sourceMappingURL=index.mjs.map
|