@pikku/core 0.6.12 → 0.6.14
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/CHANGELOG.md +13 -0
- package/dist/channel/channel-handler.d.ts +2 -2
- package/dist/channel/channel-handler.js +4 -4
- package/dist/channel/channel-runner.d.ts +5 -4
- package/dist/channel/channel-runner.js +8 -14
- package/dist/channel/channel.types.d.ts +11 -18
- package/dist/channel/local/local-channel-handler.d.ts +1 -3
- package/dist/channel/local/local-channel-handler.js +0 -3
- package/dist/channel/local/local-channel-runner.js +49 -33
- package/dist/channel/pikku-abstract-channel-handler.d.ts +4 -7
- package/dist/channel/pikku-abstract-channel-handler.js +1 -5
- package/dist/channel/serverless/serverless-channel-runner.js +45 -38
- package/dist/handle-error.d.ts +14 -0
- package/dist/handle-error.js +53 -0
- package/dist/http/http-route-runner.d.ts +34 -11
- package/dist/http/http-route-runner.js +177 -106
- package/dist/http/http-routes.types.d.ts +15 -9
- package/dist/http/index.d.ts +0 -1
- package/dist/http/index.js +0 -1
- package/dist/http/pikku-http-abstract-request.d.ts +4 -1
- package/dist/http/pikku-http-abstract-request.js +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/middleware/auth-api-key.d.ts +9 -0
- package/dist/middleware/auth-api-key.js +23 -0
- package/dist/middleware/auth-cookie-middleware.d.ts +11 -0
- package/dist/middleware/auth-cookie-middleware.js +36 -0
- package/dist/middleware/auth-jwt-middleware.d.ts +10 -0
- package/dist/middleware/auth-jwt-middleware.js +32 -0
- package/dist/middleware/auth-query-middleware.d.ts +10 -0
- package/dist/middleware/auth-query-middleware.js +21 -0
- package/dist/middleware/index.d.ts +4 -0
- package/dist/middleware/index.js +4 -0
- package/dist/middleware-runner.d.ts +22 -0
- package/dist/middleware-runner.js +28 -0
- package/dist/scheduler/scheduler.types.d.ts +2 -0
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +3 -6
- package/dist/services/user-session-service.d.ts +20 -3
- package/dist/services/user-session-service.js +35 -1
- package/dist/types/core.types.d.ts +11 -15
- package/dist/types/functions.types.d.ts +4 -4
- package/lcov.info +1572 -1729
- package/package.json +1 -2
- package/src/channel/channel-handler.ts +6 -11
- package/src/channel/channel-runner.ts +13 -32
- package/src/channel/channel.types.ts +13 -39
- package/src/channel/local/local-channel-handler.ts +1 -7
- package/src/channel/local/local-channel-runner.test.ts +2 -5
- package/src/channel/local/local-channel-runner.ts +72 -54
- package/src/channel/pikku-abstract-channel-handler.test.ts +3 -28
- package/src/channel/pikku-abstract-channel-handler.ts +3 -9
- package/src/channel/serverless/serverless-channel-runner.ts +68 -56
- package/src/handle-error.ts +67 -0
- package/src/http/http-route-runner.test.ts +12 -45
- package/src/http/http-route-runner.ts +256 -182
- package/src/http/http-routes.types.ts +15 -10
- package/src/http/index.ts +0 -2
- package/src/http/pikku-http-abstract-request.ts +8 -1
- package/src/index.ts +2 -0
- package/src/middleware/auth-api-key.ts +32 -0
- package/src/middleware/auth-cookie-middleware.ts +54 -0
- package/src/middleware/auth-jwt-middleware.ts +47 -0
- package/src/middleware/auth-query-middleware.ts +33 -0
- package/src/middleware/index.ts +4 -0
- package/src/middleware-runner.ts +43 -0
- package/src/scheduler/scheduler.types.ts +2 -0
- package/src/schema.ts +4 -8
- package/src/services/user-session-service.ts +45 -3
- package/src/types/core.types.ts +19 -18
- package/src/types/functions.types.ts +11 -7
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/http/http-session-service.d.ts +0 -15
- package/dist/http/http-session-service.js +0 -1
- package/dist/http/pikku-http-session-service.d.ts +0 -45
- package/dist/http/pikku-http-session-service.js +0 -92
- package/src/http/http-session-service.ts +0 -19
- package/src/http/pikku-http-session-service.test.ts +0 -106
- package/src/http/pikku-http-session-service.ts +0 -135
|
@@ -1,43 +1,106 @@
|
|
|
1
|
-
import { getErrorResponse } from '../errors/error-handler.js';
|
|
2
1
|
import { verifyPermissions } from '../permissions.js';
|
|
3
2
|
import { match } from 'path-to-regexp';
|
|
4
3
|
import { PikkuHTTPAbstractRequest } from './pikku-http-abstract-request.js';
|
|
5
4
|
import { PikkuHTTPAbstractResponse } from './pikku-http-abstract-response.js';
|
|
6
|
-
import { ForbiddenError,
|
|
5
|
+
import { ForbiddenError, MissingSessionError, NotFoundError, } from '../errors/errors.js';
|
|
7
6
|
import crypto from 'crypto';
|
|
8
7
|
import { closeSessionServices } from '../utils.js';
|
|
9
|
-
import {
|
|
8
|
+
import { coerceQueryStringToArray, validateSchema } from '../schema.js';
|
|
9
|
+
import { LocalUserSessionService } from '../services/user-session-service.js';
|
|
10
|
+
import { runMiddleware } from '../middleware-runner.js';
|
|
11
|
+
import { handleError } from '../handle-error.js';
|
|
12
|
+
/**
|
|
13
|
+
* Initialize global state for HTTP routes and middleware if not already available
|
|
14
|
+
*/
|
|
10
15
|
if (!globalThis.pikku?.httpRoutes) {
|
|
11
16
|
globalThis.pikku = globalThis.pikku || {};
|
|
17
|
+
globalThis.pikku.httpMiddleware = [];
|
|
12
18
|
globalThis.pikku.httpRoutes = [];
|
|
13
19
|
globalThis.pikku.httpRoutesMeta = [];
|
|
14
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Get or set the global HTTP routes
|
|
23
|
+
*
|
|
24
|
+
* @param {CoreHTTPFunctionRoute<any, any, any>[]} [data] - Optional routes data to set
|
|
25
|
+
* @returns {CoreHTTPFunctionRoute<any, any, any>[]} Current routes
|
|
26
|
+
*/
|
|
15
27
|
const httpRoutes = (data) => {
|
|
16
28
|
if (data) {
|
|
17
29
|
globalThis.pikku.httpRoutes = data;
|
|
18
30
|
}
|
|
19
31
|
return globalThis.pikku.httpRoutes;
|
|
20
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* Get or set the global HTTP route metadata
|
|
35
|
+
*
|
|
36
|
+
* @param {HTTPRoutesMeta} [data] - Optional route metadata to set
|
|
37
|
+
* @returns {HTTPRoutesMeta} Current route metadata
|
|
38
|
+
*/
|
|
21
39
|
const httpRoutesMeta = (data) => {
|
|
22
40
|
if (data) {
|
|
23
41
|
globalThis.pikku.httpRoutesMeta = data;
|
|
24
42
|
}
|
|
25
43
|
return globalThis.pikku.httpRoutesMeta;
|
|
26
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* Get or set the global HTTP middleware
|
|
47
|
+
*
|
|
48
|
+
* @param {HTTPRouteMiddleware[]} [data] - Optional middleware to set
|
|
49
|
+
* @returns {HTTPRouteMiddleware[]} Current middleware
|
|
50
|
+
*/
|
|
51
|
+
const httpMiddleware = (data) => {
|
|
52
|
+
if (data) {
|
|
53
|
+
globalThis.pikku.httpMiddleware = data;
|
|
54
|
+
}
|
|
55
|
+
return globalThis.pikku.httpMiddleware;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Add middleware to a specific route or globally
|
|
59
|
+
*
|
|
60
|
+
* @param {APIMiddleware[] | string} routeOrMiddleware - Route pattern to match or middleware array
|
|
61
|
+
* @param {APIMiddleware} [middleware] - Middleware to add (required if first param is a string)
|
|
62
|
+
*/
|
|
63
|
+
export const addMiddleware = (routeOrMiddleware, middleware) => {
|
|
64
|
+
if (typeof routeOrMiddleware === 'string') {
|
|
65
|
+
globalThis.pikku.httpMiddleware.push({
|
|
66
|
+
route: routeOrMiddleware,
|
|
67
|
+
middleware: middleware,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
globalThis.pikku.httpMiddleware.push({
|
|
72
|
+
route: '*',
|
|
73
|
+
middleware: routeOrMiddleware,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Add a route to the global routes registry
|
|
79
|
+
*
|
|
80
|
+
* @param {CoreHTTPFunctionRoute} route - Route configuration to add
|
|
81
|
+
*/
|
|
27
82
|
export const addRoute = (route) => {
|
|
28
83
|
httpRoutes().push(route);
|
|
29
84
|
};
|
|
85
|
+
/**
|
|
86
|
+
* Remove all routes from the global registry
|
|
87
|
+
*/
|
|
30
88
|
export const clearRoutes = () => {
|
|
31
89
|
httpRoutes([]);
|
|
32
90
|
};
|
|
33
91
|
/**
|
|
92
|
+
* Set the HTTP routes metadata
|
|
93
|
+
*
|
|
94
|
+
* @param {HTTPRoutesMeta} routeMeta - Metadata for routes
|
|
34
95
|
* @ignore
|
|
35
96
|
*/
|
|
36
|
-
export const setHTTPRoutesMeta = (
|
|
37
|
-
httpRoutesMeta(
|
|
97
|
+
export const setHTTPRoutesMeta = (routeMeta) => {
|
|
98
|
+
httpRoutesMeta(routeMeta);
|
|
38
99
|
};
|
|
39
100
|
/**
|
|
40
|
-
* Returns all the registered routes and associated metadata
|
|
101
|
+
* Returns all the registered routes and associated metadata
|
|
102
|
+
*
|
|
103
|
+
* @returns {Object} Object containing routes and routesMeta
|
|
41
104
|
* @internal
|
|
42
105
|
*/
|
|
43
106
|
export const getRoutes = () => {
|
|
@@ -46,69 +109,51 @@ export const getRoutes = () => {
|
|
|
46
109
|
routesMeta: httpRoutesMeta(),
|
|
47
110
|
};
|
|
48
111
|
};
|
|
49
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Find a route that matches the given request type and path
|
|
114
|
+
*
|
|
115
|
+
* @param {string} requestType - HTTP method (GET, POST, etc.)
|
|
116
|
+
* @param {string} requestPath - URL path to match
|
|
117
|
+
* @returns {Object | undefined} Matching route information or undefined if no match
|
|
118
|
+
*/
|
|
119
|
+
const getMatchingRoute = (requestType, requestPath) => {
|
|
50
120
|
for (const route of httpRoutes()) {
|
|
51
|
-
//
|
|
52
|
-
// run against all routes if we want to return a 405 method.
|
|
53
|
-
// Probably want a cache to support.
|
|
121
|
+
// Skip routes that don't match the request method
|
|
54
122
|
if (route.method !== requestType.toLowerCase()) {
|
|
55
123
|
continue;
|
|
56
124
|
}
|
|
125
|
+
// Create path matcher function
|
|
57
126
|
const matchFunc = match(`/${route.route}`.replace(/^\/\//, '/'), {
|
|
58
127
|
decode: decodeURIComponent,
|
|
59
128
|
});
|
|
129
|
+
// Try to match the path
|
|
60
130
|
const matchedPath = matchFunc(requestPath.replace(/^\/\//, '/'));
|
|
61
131
|
if (matchedPath) {
|
|
62
|
-
//
|
|
132
|
+
// Get all middleware for this route
|
|
133
|
+
const globalMiddleware = httpMiddleware()
|
|
134
|
+
.filter((m) => m.route === '*' || new RegExp(m.route).test(route.route))
|
|
135
|
+
.map((m) => m.middleware)
|
|
136
|
+
.flat();
|
|
137
|
+
// Find schema for this route
|
|
63
138
|
const schemaName = httpRoutesMeta().find((routeMeta) => routeMeta.method === route.method && routeMeta.route === route.route)?.input;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return undefined;
|
|
72
|
-
};
|
|
73
|
-
export const getUserSession = async (httpSessionService, auth, request) => {
|
|
74
|
-
if (httpSessionService) {
|
|
75
|
-
return (await httpSessionService.getUserSession(auth, request));
|
|
76
|
-
}
|
|
77
|
-
else if (auth) {
|
|
78
|
-
throw new NotImplementedError('Session service not implemented');
|
|
79
|
-
}
|
|
80
|
-
return undefined;
|
|
81
|
-
};
|
|
82
|
-
export const loadUserSession = async (skipUserSession, requiresSession, http, matchedPath, route, logger, httpSessionService) => {
|
|
83
|
-
if (skipUserSession && requiresSession) {
|
|
84
|
-
throw new Error("Can't skip trying to get user session if auth is required");
|
|
85
|
-
}
|
|
86
|
-
if (skipUserSession === false) {
|
|
87
|
-
try {
|
|
88
|
-
if (http?.request) {
|
|
89
|
-
return await getUserSession(httpSessionService, requiresSession, http.request);
|
|
90
|
-
}
|
|
91
|
-
else if (requiresSession) {
|
|
92
|
-
logger.error({
|
|
93
|
-
action: 'Can only get user session with HTTP request',
|
|
94
|
-
path: matchedPath,
|
|
95
|
-
route,
|
|
96
|
-
});
|
|
97
|
-
throw new Error('Can only get user session with HTTP request');
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
catch (e) {
|
|
101
|
-
if (requiresSession) {
|
|
102
|
-
logger.info({
|
|
103
|
-
action: 'Rejecting route (invalid session)',
|
|
104
|
-
path: matchedPath,
|
|
105
|
-
});
|
|
106
|
-
throw e;
|
|
107
|
-
}
|
|
139
|
+
return {
|
|
140
|
+
matchedPath,
|
|
141
|
+
params: matchedPath.params,
|
|
142
|
+
route,
|
|
143
|
+
middleware: [...globalMiddleware, ...(route.middleware || [])],
|
|
144
|
+
schemaName,
|
|
145
|
+
};
|
|
108
146
|
}
|
|
109
147
|
}
|
|
110
148
|
return undefined;
|
|
111
149
|
};
|
|
150
|
+
/**
|
|
151
|
+
* Create an HTTP interaction object from request and response
|
|
152
|
+
*
|
|
153
|
+
* @param {PikkuRequest | undefined} request - The HTTP request object
|
|
154
|
+
* @param {PikkuResponse | undefined} response - The HTTP response object
|
|
155
|
+
* @returns {PikkuHTTP | undefined} HTTP interaction object or undefined
|
|
156
|
+
*/
|
|
112
157
|
export const createHTTPInteraction = (request, response) => {
|
|
113
158
|
let http = undefined;
|
|
114
159
|
if (request instanceof PikkuHTTPAbstractRequest ||
|
|
@@ -123,70 +168,55 @@ export const createHTTPInteraction = (request, response) => {
|
|
|
123
168
|
}
|
|
124
169
|
return http;
|
|
125
170
|
};
|
|
126
|
-
export const handleError = (e, http, trackerId, logger, logWarningsForStatusCodes, respondWith404, bubbleError) => {
|
|
127
|
-
if (e instanceof NotFoundError && !respondWith404) {
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
const errorResponse = getErrorResponse(e);
|
|
131
|
-
if (errorResponse != null) {
|
|
132
|
-
http?.response?.setStatus(errorResponse.status);
|
|
133
|
-
http?.response?.setJson({
|
|
134
|
-
message: errorResponse.message,
|
|
135
|
-
payload: e.payload,
|
|
136
|
-
traceId: trackerId,
|
|
137
|
-
});
|
|
138
|
-
if (logWarningsForStatusCodes.includes(errorResponse.status)) {
|
|
139
|
-
logger.warn(`Warning id: ${trackerId}`);
|
|
140
|
-
logger.warn(e);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
else {
|
|
144
|
-
logger.warn(`Error id: ${trackerId}`);
|
|
145
|
-
logger.error(e);
|
|
146
|
-
http?.response?.setStatus(500);
|
|
147
|
-
http?.response?.setJson({ errorId: trackerId });
|
|
148
|
-
}
|
|
149
|
-
if (e instanceof NotFoundError) {
|
|
150
|
-
http?.response?.end();
|
|
151
|
-
}
|
|
152
|
-
if (bubbleError) {
|
|
153
|
-
throw e;
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
171
|
/**
|
|
157
|
-
*
|
|
172
|
+
* Validate input data and execute route handler with appropriate middleware
|
|
173
|
+
*
|
|
174
|
+
* @param {Object} services - Available services
|
|
175
|
+
* @param {Object} matchedRoute - Information about the matched route
|
|
176
|
+
* @param {PikkuHTTP | undefined} http - HTTP interaction object
|
|
177
|
+
* @param {Object} options - Additional options
|
|
178
|
+
* @returns {Promise<any>} Result of the route handler
|
|
158
179
|
*/
|
|
159
|
-
|
|
180
|
+
const executeRouteWithMiddleware = async (services, matchedRoute, http, options) => {
|
|
181
|
+
const { matchedPath, params, route, middleware, schemaName } = matchedRoute;
|
|
182
|
+
const { singletonServices, userSessionService, createSessionServices, skipUserSession, } = services;
|
|
183
|
+
const { coerceToArray } = options;
|
|
184
|
+
const requiresSession = route.auth !== false;
|
|
160
185
|
let sessionServices;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
186
|
+
let result;
|
|
187
|
+
http?.request?.setParams(params);
|
|
188
|
+
singletonServices.logger.info(`Matched route: ${route.route} | method: ${route.method.toUpperCase()} | auth: ${requiresSession.toString()}`);
|
|
189
|
+
// Main route execution function
|
|
190
|
+
const runMain = async () => {
|
|
191
|
+
const session = userSessionService.get();
|
|
192
|
+
// Validate session was set if needed
|
|
193
|
+
if (skipUserSession && requiresSession) {
|
|
194
|
+
throw new Error("Can't skip trying to get user session if auth is required");
|
|
195
|
+
}
|
|
196
|
+
if (requiresSession && !session) {
|
|
166
197
|
singletonServices.logger.info({
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
apiType,
|
|
198
|
+
action: 'Rejecting route (invalid session)',
|
|
199
|
+
path: matchedPath,
|
|
170
200
|
});
|
|
171
|
-
throw new
|
|
201
|
+
throw new MissingSessionError();
|
|
172
202
|
}
|
|
173
|
-
|
|
174
|
-
const requiresSession = route.auth !== false;
|
|
175
|
-
http?.request?.setParams(params);
|
|
176
|
-
singletonServices.logger.info(`Matched route: ${route.route} | method: ${route.method.toUpperCase()} | auth: ${requiresSession.toString()}`);
|
|
177
|
-
const session = await loadUserSession(skipUserSession, requiresSession, http, matchedPath, route, singletonServices.logger, singletonServices.httpSessionService);
|
|
178
|
-
const data = await request.getData();
|
|
179
|
-
validateAndCoerce(singletonServices.logger, singletonServices.schemaService, schemaName, data, coerceToArray);
|
|
203
|
+
// Create session services
|
|
180
204
|
sessionServices = await createSessionServices(singletonServices, { http }, session);
|
|
181
205
|
const allServices = { ...singletonServices, ...sessionServices, http };
|
|
182
|
-
|
|
183
|
-
|
|
206
|
+
const data = await http?.request?.getData();
|
|
207
|
+
// Validate schema
|
|
208
|
+
validateSchema(singletonServices.logger, singletonServices.schemaService, schemaName, data);
|
|
209
|
+
if (coerceToArray && schemaName) {
|
|
210
|
+
coerceQueryStringToArray(schemaName, data);
|
|
184
211
|
}
|
|
212
|
+
// Run permission checks
|
|
185
213
|
const permissioned = await verifyPermissions(route.permissions, allServices, data, session);
|
|
186
214
|
if (permissioned === false) {
|
|
187
215
|
throw new ForbiddenError('Permission denied');
|
|
188
216
|
}
|
|
189
|
-
|
|
217
|
+
// Execute the route handler
|
|
218
|
+
result = await route.func(allServices, data, session);
|
|
219
|
+
// Set the response
|
|
190
220
|
if (route.returnsJSON === false) {
|
|
191
221
|
http?.response?.setResponse(result);
|
|
192
222
|
}
|
|
@@ -196,11 +226,52 @@ export const runHTTPRoute = async ({ singletonServices, request, response, creat
|
|
|
196
226
|
http?.response?.setStatus(200);
|
|
197
227
|
http?.response?.end();
|
|
198
228
|
return result;
|
|
229
|
+
};
|
|
230
|
+
await runMiddleware({ ...singletonServices, userSessionService }, { http }, middleware, runMain);
|
|
231
|
+
return sessionServices ? { result, sessionServices } : { result };
|
|
232
|
+
};
|
|
233
|
+
/**
|
|
234
|
+
* Run an HTTP route with the given parameters
|
|
235
|
+
*
|
|
236
|
+
* @param {Object} options - Options for running the route
|
|
237
|
+
* @returns {Promise<Out | void>} Result of the route handler
|
|
238
|
+
* @ignore
|
|
239
|
+
*/
|
|
240
|
+
export const runHTTPRoute = async ({ singletonServices, request, response, createSessionServices, route: apiRoute, method: apiType, skipUserSession = false, respondWith404 = true, logWarningsForStatusCodes = [], coerceToArray = false, bubbleErrors = false, }) => {
|
|
241
|
+
const trackerId = crypto.randomUUID().toString();
|
|
242
|
+
const userSessionService = new LocalUserSessionService();
|
|
243
|
+
let sessionServices;
|
|
244
|
+
let result;
|
|
245
|
+
// Create HTTP interaction object
|
|
246
|
+
const http = createHTTPInteraction(request, response);
|
|
247
|
+
// Find matching route
|
|
248
|
+
const matchedRoute = getMatchingRoute(apiType, apiRoute);
|
|
249
|
+
try {
|
|
250
|
+
// Handle route not found
|
|
251
|
+
if (!matchedRoute) {
|
|
252
|
+
singletonServices.logger.info({
|
|
253
|
+
message: 'Route not found',
|
|
254
|
+
apiRoute,
|
|
255
|
+
apiType,
|
|
256
|
+
});
|
|
257
|
+
throw new NotFoundError(`Route not found: ${apiRoute}`);
|
|
258
|
+
}
|
|
259
|
+
// Execute route with middleware
|
|
260
|
+
;
|
|
261
|
+
({ result, sessionServices } = await executeRouteWithMiddleware({
|
|
262
|
+
singletonServices,
|
|
263
|
+
userSessionService,
|
|
264
|
+
createSessionServices,
|
|
265
|
+
skipUserSession,
|
|
266
|
+
}, matchedRoute, http, { coerceToArray }));
|
|
267
|
+
return result;
|
|
199
268
|
}
|
|
200
269
|
catch (e) {
|
|
270
|
+
// Handle and possibly bubble the error
|
|
201
271
|
handleError(e, http, trackerId, singletonServices.logger, logWarningsForStatusCodes, respondWith404, bubbleErrors);
|
|
202
272
|
}
|
|
203
273
|
finally {
|
|
274
|
+
// Clean up session services
|
|
204
275
|
if (sessionServices) {
|
|
205
276
|
await closeSessionServices(singletonServices.logger, sessionServices);
|
|
206
277
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PikkuError } from '../errors/error-handler.js';
|
|
2
|
-
import { APIDocs, CoreServices, CoreSingletonServices, CoreUserSession, CreateSessionServices } from '../types/core.types.js';
|
|
2
|
+
import { APIDocs, CoreServices, CoreSingletonServices, CoreUserSession, CreateSessionServices, PikkuMiddleware } from '../types/core.types.js';
|
|
3
3
|
import { CoreAPIFunction, CoreAPIFunctionSessionless, CoreAPIPermission } from '../types/functions.types.js';
|
|
4
4
|
import { PikkuRequest } from '../pikku-request.js';
|
|
5
5
|
import { PikkuResponse } from '../pikku-response.js';
|
|
@@ -65,18 +65,22 @@ export type PikkuQuery<T = Record<string, string | undefined>> = Record<string,
|
|
|
65
65
|
* @template APIFunctionSessionless - The sessionless API function type, defaults to `CoreAPIFunctionSessionless`.
|
|
66
66
|
* @template APIPermission - The permission function type, defaults to `CoreAPIPermission`.
|
|
67
67
|
*/
|
|
68
|
-
export type CoreHTTPFunctionRoute<In, Out, R extends string, APIFunction = CoreAPIFunction<In, Out>, APIFunctionSessionless = CoreAPIFunctionSessionless<In, Out>, APIPermission = CoreAPIPermission<In
|
|
68
|
+
export type CoreHTTPFunctionRoute<In, Out, R extends string, APIFunction = CoreAPIFunction<In, Out>, APIFunctionSessionless = CoreAPIFunctionSessionless<In, Out>, APIPermission = CoreAPIPermission<In>, APIMiddleware = PikkuMiddleware> = (CoreHTTPFunction & {
|
|
69
69
|
route: R;
|
|
70
70
|
method: HTTPMethod;
|
|
71
71
|
func: APIFunction;
|
|
72
72
|
permissions?: Record<string, APIPermission[] | APIPermission>;
|
|
73
73
|
auth?: true;
|
|
74
|
+
tags?: string[];
|
|
75
|
+
middleware?: APIMiddleware[];
|
|
74
76
|
}) | (CoreHTTPFunction & {
|
|
75
77
|
route: R;
|
|
76
78
|
method: HTTPMethod;
|
|
77
79
|
func: APIFunctionSessionless;
|
|
78
80
|
permissions?: undefined;
|
|
79
81
|
auth?: false;
|
|
82
|
+
tags?: string[];
|
|
83
|
+
middleware?: APIMiddleware[];
|
|
80
84
|
}) | (CoreHTTPFunction & {
|
|
81
85
|
route: R;
|
|
82
86
|
method: 'post';
|
|
@@ -84,6 +88,8 @@ export type CoreHTTPFunctionRoute<In, Out, R extends string, APIFunction = CoreA
|
|
|
84
88
|
permissions?: Record<string, APIPermission[] | APIPermission>;
|
|
85
89
|
auth?: true;
|
|
86
90
|
query?: Array<keyof In>;
|
|
91
|
+
tags?: string[];
|
|
92
|
+
middleware?: APIMiddleware[];
|
|
87
93
|
}) | (CoreHTTPFunction & {
|
|
88
94
|
route: R;
|
|
89
95
|
method: 'post';
|
|
@@ -91,6 +97,8 @@ export type CoreHTTPFunctionRoute<In, Out, R extends string, APIFunction = CoreA
|
|
|
91
97
|
permissions?: undefined;
|
|
92
98
|
auth?: false;
|
|
93
99
|
query?: Array<keyof In>;
|
|
100
|
+
tags?: string[];
|
|
101
|
+
middleware?: APIMiddleware[];
|
|
94
102
|
});
|
|
95
103
|
/**
|
|
96
104
|
* Represents an array of core API routes.
|
|
@@ -116,12 +124,10 @@ export type HTTPRoutesMeta = Array<{
|
|
|
116
124
|
output: string | null;
|
|
117
125
|
inputTypes?: HTTPFunctionMetaInputTypes;
|
|
118
126
|
docs?: APIDocs;
|
|
127
|
+
tags?: string[];
|
|
119
128
|
}>;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
* @returns A promise that resolves if access is granted.
|
|
125
|
-
*/
|
|
126
|
-
export type enforceHTTPAccess = (route: CoreHTTPFunctionRoute<unknown, unknown, any>, session?: CoreUserSession) => Promise<void> | void;
|
|
129
|
+
export type HTTPRouteMiddleware = {
|
|
130
|
+
route: string;
|
|
131
|
+
middleware: PikkuMiddleware[];
|
|
132
|
+
};
|
|
127
133
|
export {};
|
package/dist/http/index.d.ts
CHANGED
package/dist/http/index.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { PikkuRequest } from '../pikku-request.js';
|
|
2
|
-
import { PikkuQuery } from './http-routes.types.js';
|
|
2
|
+
import { HTTPMethod, PikkuQuery } from './http-routes.types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Abstract class representing a pikku request.
|
|
5
5
|
* @template In - The type of the request body.
|
|
6
6
|
* @group RequestResponse
|
|
7
7
|
*/
|
|
8
8
|
export declare abstract class PikkuHTTPAbstractRequest<In = unknown> extends PikkuRequest<In> {
|
|
9
|
+
path: string;
|
|
10
|
+
method: HTTPMethod;
|
|
9
11
|
private params;
|
|
12
|
+
constructor(path: string, method: HTTPMethod);
|
|
10
13
|
/**
|
|
11
14
|
* Retrieves the request body.
|
|
12
15
|
* @returns A promise that resolves to the request body.
|
|
@@ -6,7 +6,14 @@ import { PikkuRequest } from '../pikku-request.js';
|
|
|
6
6
|
* @group RequestResponse
|
|
7
7
|
*/
|
|
8
8
|
export class PikkuHTTPAbstractRequest extends PikkuRequest {
|
|
9
|
+
path;
|
|
10
|
+
method;
|
|
9
11
|
params = {};
|
|
12
|
+
constructor(path, method) {
|
|
13
|
+
super();
|
|
14
|
+
this.path = path;
|
|
15
|
+
this.method = method;
|
|
16
|
+
}
|
|
10
17
|
/**
|
|
11
18
|
* Retrieves the raw request body as a Buffer.
|
|
12
19
|
* @returns A promise that resolves to the raw request body.
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export * from './services/index.js';
|
|
|
9
9
|
export * from './http/index.js';
|
|
10
10
|
export * from './channel/index.js';
|
|
11
11
|
export * from './scheduler/index.js';
|
|
12
|
+
export * from './errors/index.js';
|
|
13
|
+
export * from './middleware/index.js';
|
|
12
14
|
export { addRoute } from './http/http-route-runner.js';
|
|
13
15
|
export { addChannel } from './channel/channel-runner.js';
|
|
14
16
|
export { addScheduledTask } from './scheduler/scheduler-runner.js';
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,8 @@ export * from './services/index.js';
|
|
|
9
9
|
export * from './http/index.js';
|
|
10
10
|
export * from './channel/index.js';
|
|
11
11
|
export * from './scheduler/index.js';
|
|
12
|
+
export * from './errors/index.js';
|
|
13
|
+
export * from './middleware/index.js';
|
|
12
14
|
export { addRoute } from './http/http-route-runner.js';
|
|
13
15
|
export { addChannel } from './channel/channel-runner.js';
|
|
14
16
|
export { addScheduledTask } from './scheduler/scheduler-runner.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { PikkuMiddleware } from '../types/core.types.js';
|
|
2
|
+
/**
|
|
3
|
+
* API key middleware that retrieves a session from the 'x-api-key' header using a provided callback.
|
|
4
|
+
*
|
|
5
|
+
* @param options.getSessionForAPIKey - A function that returns a session when provided an API key.
|
|
6
|
+
*/
|
|
7
|
+
export declare const apiKeyMiddleware: (options?: {
|
|
8
|
+
getSessionForAPIKey?: (apiKey: string) => Promise<any>;
|
|
9
|
+
}) => PikkuMiddleware;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API key middleware that retrieves a session from the 'x-api-key' header using a provided callback.
|
|
3
|
+
*
|
|
4
|
+
* @param options.getSessionForAPIKey - A function that returns a session when provided an API key.
|
|
5
|
+
*/
|
|
6
|
+
export const apiKeyMiddleware = (options = {}) => {
|
|
7
|
+
const middleware = async ({ userSessionService }, { http }, next) => {
|
|
8
|
+
if (!http?.request || userSessionService.get()) {
|
|
9
|
+
return next();
|
|
10
|
+
}
|
|
11
|
+
if (options.getSessionForAPIKey) {
|
|
12
|
+
const apiKey = http.request.getHeader('x-api-key');
|
|
13
|
+
if (apiKey) {
|
|
14
|
+
const userSession = await options.getSessionForAPIKey(apiKey);
|
|
15
|
+
if (userSession) {
|
|
16
|
+
await userSessionService.set(userSession);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return next();
|
|
21
|
+
};
|
|
22
|
+
return middleware;
|
|
23
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { PikkuMiddleware } from '../types/core.types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cookie middleware that extracts a session from cookies.
|
|
4
|
+
*
|
|
5
|
+
* @param options.cookieNames - List of cookie names to check.
|
|
6
|
+
* @param options.getSessionForCookieValue - Function to retrieve a session using a cookie value.
|
|
7
|
+
*/
|
|
8
|
+
export declare const cookieMiddleware: (options?: {
|
|
9
|
+
cookieNames?: string[];
|
|
10
|
+
getSessionForCookieValue?: (cookieValue: string, cookieName: string) => Promise<any>;
|
|
11
|
+
}) => PikkuMiddleware;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cookie middleware that extracts a session from cookies.
|
|
3
|
+
*
|
|
4
|
+
* @param options.cookieNames - List of cookie names to check.
|
|
5
|
+
* @param options.getSessionForCookieValue - Function to retrieve a session using a cookie value.
|
|
6
|
+
*/
|
|
7
|
+
export const cookieMiddleware = (options = {}) => {
|
|
8
|
+
const middleware = async ({ userSessionService }, { http }, next) => {
|
|
9
|
+
if (!http?.request || userSessionService.get()) {
|
|
10
|
+
return next();
|
|
11
|
+
}
|
|
12
|
+
if (options.getSessionForCookieValue && options.cookieNames) {
|
|
13
|
+
const cookies = http.request.getCookies();
|
|
14
|
+
if (cookies) {
|
|
15
|
+
let cookieName;
|
|
16
|
+
for (const name of options.cookieNames) {
|
|
17
|
+
if (cookies[name]) {
|
|
18
|
+
cookieName = name;
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (cookieName) {
|
|
23
|
+
const cookieValue = cookies[cookieName];
|
|
24
|
+
if (cookieValue) {
|
|
25
|
+
const userSession = await options.getSessionForCookieValue(cookieValue, cookieName);
|
|
26
|
+
if (userSession) {
|
|
27
|
+
await userSessionService.set(userSession);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return next();
|
|
34
|
+
};
|
|
35
|
+
return middleware;
|
|
36
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { PikkuMiddleware } from '../types/core.types.js';
|
|
2
|
+
/**
|
|
3
|
+
* JWT middleware that extracts the Bearer token from the Authorization header,
|
|
4
|
+
* decodes it via the jwtService, and sets the session if found.
|
|
5
|
+
*
|
|
6
|
+
* @param options.debugJWTDecode - Optional flag for debugging the JWT decode process.
|
|
7
|
+
*/
|
|
8
|
+
export declare const jwtMiddleware: (options?: {
|
|
9
|
+
debugJWTDecode?: boolean;
|
|
10
|
+
}) => PikkuMiddleware;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { InvalidSessionError } from '../errors/errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* JWT middleware that extracts the Bearer token from the Authorization header,
|
|
4
|
+
* decodes it via the jwtService, and sets the session if found.
|
|
5
|
+
*
|
|
6
|
+
* @param options.debugJWTDecode - Optional flag for debugging the JWT decode process.
|
|
7
|
+
*/
|
|
8
|
+
export const jwtMiddleware = (options = {}) => {
|
|
9
|
+
const middleware = async ({ jwt, userSessionService }, { http }, next) => {
|
|
10
|
+
if (!jwt) {
|
|
11
|
+
throw new Error('JWT service not found');
|
|
12
|
+
}
|
|
13
|
+
// Skip if session already exists.
|
|
14
|
+
if (!http?.request || userSessionService.get()) {
|
|
15
|
+
return next();
|
|
16
|
+
}
|
|
17
|
+
const authHeader = http.request.getHeader('authorization') ||
|
|
18
|
+
http.request.getHeader('Authorization');
|
|
19
|
+
if (authHeader) {
|
|
20
|
+
const [scheme, token] = authHeader.split(' ');
|
|
21
|
+
if (scheme !== 'Bearer' || !token) {
|
|
22
|
+
throw new InvalidSessionError();
|
|
23
|
+
}
|
|
24
|
+
const userSession = await jwt.decode(token, undefined, options.debugJWTDecode);
|
|
25
|
+
if (userSession) {
|
|
26
|
+
await userSessionService.set(userSession);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return next();
|
|
30
|
+
};
|
|
31
|
+
return middleware;
|
|
32
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { PikkuQuery } from '../http/http-routes.types.js';
|
|
2
|
+
import { PikkuMiddleware } from '../types/core.types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Query middleware that retrieves a session using query parameters.
|
|
5
|
+
*
|
|
6
|
+
* @param options.getSessionForQueryValue - A function that returns a session based on query parameters.
|
|
7
|
+
*/
|
|
8
|
+
export declare const queryMiddleware: (options?: {
|
|
9
|
+
getSessionForQueryValue?: (query: PikkuQuery) => Promise<any>;
|
|
10
|
+
}) => PikkuMiddleware;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query middleware that retrieves a session using query parameters.
|
|
3
|
+
*
|
|
4
|
+
* @param options.getSessionForQueryValue - A function that returns a session based on query parameters.
|
|
5
|
+
*/
|
|
6
|
+
export const queryMiddleware = (options = {}) => {
|
|
7
|
+
const middleware = async ({ userSessionService }, { http }, next) => {
|
|
8
|
+
if (!http?.request || userSessionService.get()) {
|
|
9
|
+
return next();
|
|
10
|
+
}
|
|
11
|
+
if (options.getSessionForQueryValue) {
|
|
12
|
+
const query = http.request.getQuery();
|
|
13
|
+
const userSession = await options.getSessionForQueryValue(query);
|
|
14
|
+
if (userSession) {
|
|
15
|
+
await userSessionService.set(userSession);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return next();
|
|
19
|
+
};
|
|
20
|
+
return middleware;
|
|
21
|
+
};
|