@pikku/core 0.6.13 → 0.6.15
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/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- 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/index.ts +1 -0
- 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,4 +1,3 @@
|
|
|
1
|
-
import { getErrorResponse } from '../errors/error-handler.js'
|
|
2
1
|
import { verifyPermissions } from '../permissions.js'
|
|
3
2
|
import {
|
|
4
3
|
CoreHTTPFunctionRoute,
|
|
@@ -6,31 +5,42 @@ import {
|
|
|
6
5
|
RunRouteOptions,
|
|
7
6
|
RunRouteParams,
|
|
8
7
|
PikkuHTTP,
|
|
8
|
+
HTTPRouteMiddleware,
|
|
9
9
|
} from './http-routes.types.js'
|
|
10
|
-
import {
|
|
10
|
+
import { SessionServices } from '../types/core.types.js'
|
|
11
11
|
import { match } from 'path-to-regexp'
|
|
12
12
|
import { PikkuHTTPAbstractRequest } from './pikku-http-abstract-request.js'
|
|
13
13
|
import { PikkuHTTPAbstractResponse } from './pikku-http-abstract-response.js'
|
|
14
|
-
import { Logger, SchemaService } from '../services/index.js'
|
|
15
14
|
import {
|
|
16
15
|
ForbiddenError,
|
|
16
|
+
MissingSessionError,
|
|
17
17
|
NotFoundError,
|
|
18
|
-
NotImplementedError,
|
|
19
18
|
} from '../errors/errors.js'
|
|
20
19
|
import crypto from 'crypto'
|
|
21
20
|
import { closeSessionServices } from '../utils.js'
|
|
22
|
-
import { CoreAPIChannel } from '../channel/channel.types.js'
|
|
23
21
|
import { PikkuRequest } from '../pikku-request.js'
|
|
24
22
|
import { PikkuResponse } from '../pikku-response.js'
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
23
|
+
import { coerceQueryStringToArray, validateSchema } from '../schema.js'
|
|
24
|
+
import { LocalUserSessionService } from '../services/user-session-service.js'
|
|
25
|
+
import { runMiddleware } from '../middleware-runner.js'
|
|
26
|
+
import { handleError } from '../handle-error.js'
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Initialize global state for HTTP routes and middleware if not already available
|
|
30
|
+
*/
|
|
28
31
|
if (!globalThis.pikku?.httpRoutes) {
|
|
29
32
|
globalThis.pikku = globalThis.pikku || {}
|
|
33
|
+
globalThis.pikku.httpMiddleware = []
|
|
30
34
|
globalThis.pikku.httpRoutes = []
|
|
31
35
|
globalThis.pikku.httpRoutesMeta = []
|
|
32
36
|
}
|
|
33
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Get or set the global HTTP routes
|
|
40
|
+
*
|
|
41
|
+
* @param {CoreHTTPFunctionRoute<any, any, any>[]} [data] - Optional routes data to set
|
|
42
|
+
* @returns {CoreHTTPFunctionRoute<any, any, any>[]} Current routes
|
|
43
|
+
*/
|
|
34
44
|
const httpRoutes = (
|
|
35
45
|
data?: CoreHTTPFunctionRoute<any, any, any>[]
|
|
36
46
|
): CoreHTTPFunctionRoute<any, any, any>[] => {
|
|
@@ -40,6 +50,12 @@ const httpRoutes = (
|
|
|
40
50
|
return globalThis.pikku.httpRoutes
|
|
41
51
|
}
|
|
42
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Get or set the global HTTP route metadata
|
|
55
|
+
*
|
|
56
|
+
* @param {HTTPRoutesMeta} [data] - Optional route metadata to set
|
|
57
|
+
* @returns {HTTPRoutesMeta} Current route metadata
|
|
58
|
+
*/
|
|
43
59
|
const httpRoutesMeta = (data?: HTTPRoutesMeta): HTTPRoutesMeta => {
|
|
44
60
|
if (data) {
|
|
45
61
|
globalThis.pikku.httpRoutesMeta = data
|
|
@@ -47,6 +63,49 @@ const httpRoutesMeta = (data?: HTTPRoutesMeta): HTTPRoutesMeta => {
|
|
|
47
63
|
return globalThis.pikku.httpRoutesMeta
|
|
48
64
|
}
|
|
49
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Get or set the global HTTP middleware
|
|
68
|
+
*
|
|
69
|
+
* @param {HTTPRouteMiddleware[]} [data] - Optional middleware to set
|
|
70
|
+
* @returns {HTTPRouteMiddleware[]} Current middleware
|
|
71
|
+
*/
|
|
72
|
+
const httpMiddleware = (
|
|
73
|
+
data?: HTTPRouteMiddleware[]
|
|
74
|
+
): HTTPRouteMiddleware[] => {
|
|
75
|
+
if (data) {
|
|
76
|
+
globalThis.pikku.httpMiddleware = data
|
|
77
|
+
}
|
|
78
|
+
return globalThis.pikku.httpMiddleware
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Add middleware to a specific route or globally
|
|
83
|
+
*
|
|
84
|
+
* @param {APIMiddleware[] | string} routeOrMiddleware - Route pattern to match or middleware array
|
|
85
|
+
* @param {APIMiddleware} [middleware] - Middleware to add (required if first param is a string)
|
|
86
|
+
*/
|
|
87
|
+
export const addMiddleware = <APIMiddleware>(
|
|
88
|
+
routeOrMiddleware: APIMiddleware[] | string,
|
|
89
|
+
middleware?: APIMiddleware
|
|
90
|
+
) => {
|
|
91
|
+
if (typeof routeOrMiddleware === 'string') {
|
|
92
|
+
globalThis.pikku.httpMiddleware.push({
|
|
93
|
+
route: routeOrMiddleware,
|
|
94
|
+
middleware: middleware!,
|
|
95
|
+
})
|
|
96
|
+
} else {
|
|
97
|
+
globalThis.pikku.httpMiddleware.push({
|
|
98
|
+
route: '*',
|
|
99
|
+
middleware: routeOrMiddleware,
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Add a route to the global routes registry
|
|
106
|
+
*
|
|
107
|
+
* @param {CoreHTTPFunctionRoute} route - Route configuration to add
|
|
108
|
+
*/
|
|
50
109
|
export const addRoute = <
|
|
51
110
|
In,
|
|
52
111
|
Out,
|
|
@@ -54,6 +113,7 @@ export const addRoute = <
|
|
|
54
113
|
APIFunction,
|
|
55
114
|
APIFunctionSessionless,
|
|
56
115
|
APIPermission,
|
|
116
|
+
APIMiddleware,
|
|
57
117
|
>(
|
|
58
118
|
route: CoreHTTPFunctionRoute<
|
|
59
119
|
In,
|
|
@@ -61,25 +121,34 @@ export const addRoute = <
|
|
|
61
121
|
Route,
|
|
62
122
|
APIFunction,
|
|
63
123
|
APIFunctionSessionless,
|
|
64
|
-
APIPermission
|
|
124
|
+
APIPermission,
|
|
125
|
+
APIMiddleware
|
|
65
126
|
>
|
|
66
127
|
) => {
|
|
67
128
|
httpRoutes().push(route as any)
|
|
68
129
|
}
|
|
69
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Remove all routes from the global registry
|
|
133
|
+
*/
|
|
70
134
|
export const clearRoutes = () => {
|
|
71
135
|
httpRoutes([])
|
|
72
136
|
}
|
|
73
137
|
|
|
74
138
|
/**
|
|
139
|
+
* Set the HTTP routes metadata
|
|
140
|
+
*
|
|
141
|
+
* @param {HTTPRoutesMeta} routeMeta - Metadata for routes
|
|
75
142
|
* @ignore
|
|
76
143
|
*/
|
|
77
|
-
export const setHTTPRoutesMeta = (
|
|
78
|
-
httpRoutesMeta(
|
|
144
|
+
export const setHTTPRoutesMeta = (routeMeta: HTTPRoutesMeta) => {
|
|
145
|
+
httpRoutesMeta(routeMeta)
|
|
79
146
|
}
|
|
80
147
|
|
|
81
148
|
/**
|
|
82
|
-
* Returns all the registered routes and associated metadata
|
|
149
|
+
* Returns all the registered routes and associated metadata
|
|
150
|
+
*
|
|
151
|
+
* @returns {Object} Object containing routes and routesMeta
|
|
83
152
|
* @internal
|
|
84
153
|
*/
|
|
85
154
|
export const getRoutes = () => {
|
|
@@ -89,105 +158,66 @@ export const getRoutes = () => {
|
|
|
89
158
|
}
|
|
90
159
|
}
|
|
91
160
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
161
|
+
/**
|
|
162
|
+
* Find a route that matches the given request type and path
|
|
163
|
+
*
|
|
164
|
+
* @param {string} requestType - HTTP method (GET, POST, etc.)
|
|
165
|
+
* @param {string} requestPath - URL path to match
|
|
166
|
+
* @returns {Object | undefined} Matching route information or undefined if no match
|
|
167
|
+
*/
|
|
168
|
+
const getMatchingRoute = (requestType: string, requestPath: string) => {
|
|
97
169
|
for (const route of httpRoutes()) {
|
|
98
|
-
//
|
|
99
|
-
// run against all routes if we want to return a 405 method.
|
|
100
|
-
// Probably want a cache to support.
|
|
170
|
+
// Skip routes that don't match the request method
|
|
101
171
|
if (route.method !== requestType.toLowerCase()) {
|
|
102
172
|
continue
|
|
103
173
|
}
|
|
174
|
+
|
|
175
|
+
// Create path matcher function
|
|
104
176
|
const matchFunc = match(`/${route.route}`.replace(/^\/\//, '/'), {
|
|
105
177
|
decode: decodeURIComponent,
|
|
106
178
|
})
|
|
179
|
+
|
|
180
|
+
// Try to match the path
|
|
107
181
|
const matchedPath = matchFunc(requestPath.replace(/^\/\//, '/'))
|
|
108
182
|
|
|
109
183
|
if (matchedPath) {
|
|
110
|
-
//
|
|
184
|
+
// Get all middleware for this route
|
|
185
|
+
const globalMiddleware = httpMiddleware()
|
|
186
|
+
.filter((m) => m.route === '*' || new RegExp(m.route).test(route.route))
|
|
187
|
+
.map((m) => m.middleware)
|
|
188
|
+
.flat()
|
|
189
|
+
|
|
190
|
+
// Find schema for this route
|
|
111
191
|
const schemaName = httpRoutesMeta().find(
|
|
112
192
|
(routeMeta) =>
|
|
113
193
|
routeMeta.method === route.method && routeMeta.route === route.route
|
|
114
194
|
)?.input
|
|
115
|
-
if (schemaName && schemaService) {
|
|
116
|
-
const schema = getSchema(schemaName)
|
|
117
|
-
schemaService.compileSchema(schemaName, schema)
|
|
118
|
-
}
|
|
119
|
-
return { matchedPath, params: matchedPath.params, route, schemaName }
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return undefined
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export const getUserSession = async <UserSession extends CoreUserSession>(
|
|
126
|
-
httpSessionService: HTTPSessionService<UserSession> | undefined,
|
|
127
|
-
auth: boolean,
|
|
128
|
-
request: PikkuHTTPAbstractRequest
|
|
129
|
-
): Promise<CoreUserSession | undefined> => {
|
|
130
|
-
if (httpSessionService) {
|
|
131
|
-
return (await httpSessionService.getUserSession(
|
|
132
|
-
auth,
|
|
133
|
-
request
|
|
134
|
-
)) as UserSession
|
|
135
|
-
} else if (auth) {
|
|
136
|
-
throw new NotImplementedError('Session service not implemented')
|
|
137
|
-
}
|
|
138
|
-
return undefined
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export const loadUserSession = async (
|
|
142
|
-
skipUserSession: boolean,
|
|
143
|
-
requiresSession: boolean,
|
|
144
|
-
http: PikkuHTTP | undefined,
|
|
145
|
-
matchedPath: any,
|
|
146
|
-
route:
|
|
147
|
-
| CoreHTTPFunctionRoute<unknown, unknown, any>
|
|
148
|
-
| CoreAPIChannel<unknown, any>,
|
|
149
|
-
logger: Logger,
|
|
150
|
-
httpSessionService: HTTPSessionService | undefined
|
|
151
|
-
) => {
|
|
152
|
-
if (skipUserSession && requiresSession) {
|
|
153
|
-
throw new Error("Can't skip trying to get user session if auth is required")
|
|
154
|
-
}
|
|
155
195
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
http.request
|
|
163
|
-
)
|
|
164
|
-
} else if (requiresSession) {
|
|
165
|
-
logger.error({
|
|
166
|
-
action: 'Can only get user session with HTTP request',
|
|
167
|
-
path: matchedPath,
|
|
168
|
-
route,
|
|
169
|
-
})
|
|
170
|
-
throw new Error('Can only get user session with HTTP request')
|
|
171
|
-
}
|
|
172
|
-
} catch (e: any) {
|
|
173
|
-
if (requiresSession) {
|
|
174
|
-
logger.info({
|
|
175
|
-
action: 'Rejecting route (invalid session)',
|
|
176
|
-
path: matchedPath,
|
|
177
|
-
})
|
|
178
|
-
throw e
|
|
196
|
+
return {
|
|
197
|
+
matchedPath,
|
|
198
|
+
params: matchedPath.params,
|
|
199
|
+
route,
|
|
200
|
+
middleware: [...globalMiddleware, ...(route.middleware || [])],
|
|
201
|
+
schemaName,
|
|
179
202
|
}
|
|
180
203
|
}
|
|
181
204
|
}
|
|
182
|
-
|
|
183
205
|
return undefined
|
|
184
206
|
}
|
|
185
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Create an HTTP interaction object from request and response
|
|
210
|
+
*
|
|
211
|
+
* @param {PikkuRequest | undefined} request - The HTTP request object
|
|
212
|
+
* @param {PikkuResponse | undefined} response - The HTTP response object
|
|
213
|
+
* @returns {PikkuHTTP | undefined} HTTP interaction object or undefined
|
|
214
|
+
*/
|
|
186
215
|
export const createHTTPInteraction = (
|
|
187
216
|
request: PikkuRequest | undefined,
|
|
188
217
|
response: PikkuResponse | undefined
|
|
189
|
-
) => {
|
|
218
|
+
): PikkuHTTP | undefined => {
|
|
190
219
|
let http: PikkuHTTP | undefined = undefined
|
|
220
|
+
|
|
191
221
|
if (
|
|
192
222
|
request instanceof PikkuHTTPAbstractRequest ||
|
|
193
223
|
response instanceof PikkuHTTPAbstractResponse
|
|
@@ -200,52 +230,141 @@ export const createHTTPInteraction = (
|
|
|
200
230
|
http.response = response
|
|
201
231
|
}
|
|
202
232
|
}
|
|
233
|
+
|
|
203
234
|
return http
|
|
204
235
|
}
|
|
205
236
|
|
|
206
|
-
|
|
207
|
-
|
|
237
|
+
/**
|
|
238
|
+
* Validate input data and execute route handler with appropriate middleware
|
|
239
|
+
*
|
|
240
|
+
* @param {Object} services - Available services
|
|
241
|
+
* @param {Object} matchedRoute - Information about the matched route
|
|
242
|
+
* @param {PikkuHTTP | undefined} http - HTTP interaction object
|
|
243
|
+
* @param {Object} options - Additional options
|
|
244
|
+
* @returns {Promise<any>} Result of the route handler
|
|
245
|
+
*/
|
|
246
|
+
const executeRouteWithMiddleware = async (
|
|
247
|
+
services: {
|
|
248
|
+
singletonServices: any
|
|
249
|
+
userSessionService: any
|
|
250
|
+
createSessionServices: Function
|
|
251
|
+
skipUserSession: boolean
|
|
252
|
+
},
|
|
253
|
+
matchedRoute: {
|
|
254
|
+
matchedPath: any
|
|
255
|
+
params: any
|
|
256
|
+
route: CoreHTTPFunctionRoute<any, any, any>
|
|
257
|
+
middleware: any[]
|
|
258
|
+
schemaName?: string | null
|
|
259
|
+
},
|
|
208
260
|
http: PikkuHTTP | undefined,
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
logWarningsForStatusCodes: number[],
|
|
212
|
-
respondWith404: boolean,
|
|
213
|
-
bubbleError: boolean
|
|
214
|
-
) => {
|
|
215
|
-
if (e instanceof NotFoundError && !respondWith404) {
|
|
216
|
-
return
|
|
261
|
+
options: {
|
|
262
|
+
coerceToArray: boolean
|
|
217
263
|
}
|
|
264
|
+
) => {
|
|
265
|
+
const { matchedPath, params, route, middleware, schemaName } = matchedRoute
|
|
266
|
+
const {
|
|
267
|
+
singletonServices,
|
|
268
|
+
userSessionService,
|
|
269
|
+
createSessionServices,
|
|
270
|
+
skipUserSession,
|
|
271
|
+
} = services
|
|
272
|
+
const { coerceToArray } = options
|
|
273
|
+
|
|
274
|
+
const requiresSession = route.auth !== false
|
|
275
|
+
let sessionServices: any
|
|
276
|
+
let result: any
|
|
277
|
+
|
|
278
|
+
http?.request?.setParams(params)
|
|
279
|
+
|
|
280
|
+
singletonServices.logger.info(
|
|
281
|
+
`Matched route: ${route.route} | method: ${route.method.toUpperCase()} | auth: ${requiresSession.toString()}`
|
|
282
|
+
)
|
|
218
283
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
http?.response?.setJson({
|
|
223
|
-
message: errorResponse.message,
|
|
224
|
-
payload: (e as any).payload,
|
|
225
|
-
traceId: trackerId,
|
|
226
|
-
})
|
|
284
|
+
// Main route execution function
|
|
285
|
+
const runMain = async () => {
|
|
286
|
+
const session = userSessionService.get()
|
|
227
287
|
|
|
228
|
-
if
|
|
229
|
-
|
|
230
|
-
|
|
288
|
+
// Validate session was set if needed
|
|
289
|
+
if (skipUserSession && requiresSession) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
"Can't skip trying to get user session if auth is required"
|
|
292
|
+
)
|
|
231
293
|
}
|
|
232
|
-
} else {
|
|
233
|
-
logger.warn(`Error id: ${trackerId}`)
|
|
234
|
-
logger.error(e)
|
|
235
|
-
http?.response?.setStatus(500)
|
|
236
|
-
http?.response?.setJson({ errorId: trackerId })
|
|
237
|
-
}
|
|
238
294
|
|
|
239
|
-
|
|
295
|
+
if (requiresSession && !session) {
|
|
296
|
+
singletonServices.logger.info({
|
|
297
|
+
action: 'Rejecting route (invalid session)',
|
|
298
|
+
path: matchedPath,
|
|
299
|
+
})
|
|
300
|
+
throw new MissingSessionError()
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Create session services
|
|
304
|
+
sessionServices = await createSessionServices(
|
|
305
|
+
singletonServices,
|
|
306
|
+
{ http },
|
|
307
|
+
session
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
const allServices = { ...singletonServices, ...sessionServices, http }
|
|
311
|
+
const data = await http?.request?.getData()
|
|
312
|
+
|
|
313
|
+
// Validate schema
|
|
314
|
+
validateSchema(
|
|
315
|
+
singletonServices.logger,
|
|
316
|
+
singletonServices.schemaService,
|
|
317
|
+
schemaName,
|
|
318
|
+
data
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
if (coerceToArray && schemaName) {
|
|
322
|
+
coerceQueryStringToArray(schemaName, data)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Run permission checks
|
|
326
|
+
const permissioned = await verifyPermissions(
|
|
327
|
+
route.permissions,
|
|
328
|
+
allServices,
|
|
329
|
+
data,
|
|
330
|
+
session
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
if (permissioned === false) {
|
|
334
|
+
throw new ForbiddenError('Permission denied')
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Execute the route handler
|
|
338
|
+
result = await route.func(allServices, data, session!)
|
|
339
|
+
|
|
340
|
+
// Set the response
|
|
341
|
+
if (route.returnsJSON === false) {
|
|
342
|
+
http?.response?.setResponse(result)
|
|
343
|
+
} else {
|
|
344
|
+
http?.response?.setJson(result)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
http?.response?.setStatus(200)
|
|
240
348
|
http?.response?.end()
|
|
241
|
-
}
|
|
242
349
|
|
|
243
|
-
|
|
244
|
-
throw e
|
|
350
|
+
return result
|
|
245
351
|
}
|
|
352
|
+
|
|
353
|
+
await runMiddleware(
|
|
354
|
+
{ ...singletonServices, userSessionService },
|
|
355
|
+
{ http },
|
|
356
|
+
middleware,
|
|
357
|
+
runMain
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
return sessionServices ? { result, sessionServices } : { result }
|
|
246
361
|
}
|
|
247
362
|
|
|
248
363
|
/**
|
|
364
|
+
* Run an HTTP route with the given parameters
|
|
365
|
+
*
|
|
366
|
+
* @param {Object} options - Options for running the route
|
|
367
|
+
* @returns {Promise<Out | void>} Result of the route handler
|
|
249
368
|
* @ignore
|
|
250
369
|
*/
|
|
251
370
|
export const runHTTPRoute = async <In, Out>({
|
|
@@ -263,18 +382,20 @@ export const runHTTPRoute = async <In, Out>({
|
|
|
263
382
|
}: Pick<CoreHTTPFunctionRoute<unknown, unknown, any>, 'route' | 'method'> &
|
|
264
383
|
RunRouteOptions &
|
|
265
384
|
RunRouteParams<In>): Promise<Out | void> => {
|
|
266
|
-
let sessionServices: SessionServices<typeof singletonServices> | undefined
|
|
267
385
|
const trackerId: string = crypto.randomUUID().toString()
|
|
268
386
|
|
|
387
|
+
const userSessionService = new LocalUserSessionService()
|
|
388
|
+
let sessionServices: SessionServices<typeof singletonServices> | undefined
|
|
389
|
+
let result: Out
|
|
390
|
+
|
|
391
|
+
// Create HTTP interaction object
|
|
269
392
|
const http = createHTTPInteraction(request, response)
|
|
270
393
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
apiType,
|
|
274
|
-
apiRoute
|
|
275
|
-
)
|
|
394
|
+
// Find matching route
|
|
395
|
+
const matchedRoute = getMatchingRoute(apiType, apiRoute)
|
|
276
396
|
|
|
277
397
|
try {
|
|
398
|
+
// Handle route not found
|
|
278
399
|
if (!matchedRoute) {
|
|
279
400
|
singletonServices.logger.info({
|
|
280
401
|
message: 'Route not found',
|
|
@@ -284,70 +405,22 @@ export const runHTTPRoute = async <In, Out>({
|
|
|
284
405
|
throw new NotFoundError(`Route not found: ${apiRoute}`)
|
|
285
406
|
}
|
|
286
407
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
skipUserSession,
|
|
297
|
-
requiresSession,
|
|
408
|
+
// Execute route with middleware
|
|
409
|
+
;({ result, sessionServices } = await executeRouteWithMiddleware(
|
|
410
|
+
{
|
|
411
|
+
singletonServices,
|
|
412
|
+
userSessionService,
|
|
413
|
+
createSessionServices,
|
|
414
|
+
skipUserSession,
|
|
415
|
+
},
|
|
416
|
+
matchedRoute,
|
|
298
417
|
http,
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
singletonServices.logger,
|
|
302
|
-
singletonServices.httpSessionService
|
|
303
|
-
)
|
|
304
|
-
const data = await request.getData()
|
|
305
|
-
|
|
306
|
-
validateAndCoerce(
|
|
307
|
-
singletonServices.logger,
|
|
308
|
-
singletonServices.schemaService,
|
|
309
|
-
schemaName,
|
|
310
|
-
data,
|
|
311
|
-
coerceToArray
|
|
312
|
-
)
|
|
313
|
-
|
|
314
|
-
sessionServices = await createSessionServices(
|
|
315
|
-
singletonServices,
|
|
316
|
-
{ http },
|
|
317
|
-
session
|
|
318
|
-
)
|
|
319
|
-
const allServices = { ...singletonServices, ...sessionServices, http }
|
|
320
|
-
|
|
321
|
-
if (singletonServices.enforceHTTPAccess) {
|
|
322
|
-
await singletonServices.enforceHTTPAccess(route, session)
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const permissioned = await verifyPermissions(
|
|
326
|
-
route.permissions,
|
|
327
|
-
allServices,
|
|
328
|
-
data,
|
|
329
|
-
session
|
|
330
|
-
)
|
|
331
|
-
if (permissioned === false) {
|
|
332
|
-
throw new ForbiddenError('Permission denied')
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const result: any = (await route.func(
|
|
336
|
-
allServices,
|
|
337
|
-
data,
|
|
338
|
-
session!
|
|
339
|
-
)) as unknown as Out
|
|
340
|
-
|
|
341
|
-
if (route.returnsJSON === false) {
|
|
342
|
-
http?.response?.setResponse(result)
|
|
343
|
-
} else {
|
|
344
|
-
http?.response?.setJson(result)
|
|
345
|
-
}
|
|
346
|
-
http?.response?.setStatus(200)
|
|
347
|
-
http?.response?.end()
|
|
418
|
+
{ coerceToArray }
|
|
419
|
+
))
|
|
348
420
|
|
|
349
421
|
return result
|
|
350
422
|
} catch (e: any) {
|
|
423
|
+
// Handle and possibly bubble the error
|
|
351
424
|
handleError(
|
|
352
425
|
e,
|
|
353
426
|
http,
|
|
@@ -358,6 +431,7 @@ export const runHTTPRoute = async <In, Out>({
|
|
|
358
431
|
bubbleErrors
|
|
359
432
|
)
|
|
360
433
|
} finally {
|
|
434
|
+
// Clean up session services
|
|
361
435
|
if (sessionServices) {
|
|
362
436
|
await closeSessionServices(singletonServices.logger, sessionServices)
|
|
363
437
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
CoreSingletonServices,
|
|
6
6
|
CoreUserSession,
|
|
7
7
|
CreateSessionServices,
|
|
8
|
+
PikkuMiddleware,
|
|
8
9
|
} from '../types/core.types.js'
|
|
9
10
|
import {
|
|
10
11
|
CoreAPIFunction,
|
|
@@ -115,6 +116,7 @@ export type CoreHTTPFunctionRoute<
|
|
|
115
116
|
APIFunction = CoreAPIFunction<In, Out>,
|
|
116
117
|
APIFunctionSessionless = CoreAPIFunctionSessionless<In, Out>,
|
|
117
118
|
APIPermission = CoreAPIPermission<In>,
|
|
119
|
+
APIMiddleware = PikkuMiddleware,
|
|
118
120
|
> =
|
|
119
121
|
| (CoreHTTPFunction & {
|
|
120
122
|
route: R
|
|
@@ -122,6 +124,8 @@ export type CoreHTTPFunctionRoute<
|
|
|
122
124
|
func: APIFunction
|
|
123
125
|
permissions?: Record<string, APIPermission[] | APIPermission>
|
|
124
126
|
auth?: true
|
|
127
|
+
tags?: string[]
|
|
128
|
+
middleware?: APIMiddleware[]
|
|
125
129
|
})
|
|
126
130
|
| (CoreHTTPFunction & {
|
|
127
131
|
route: R
|
|
@@ -129,6 +133,8 @@ export type CoreHTTPFunctionRoute<
|
|
|
129
133
|
func: APIFunctionSessionless
|
|
130
134
|
permissions?: undefined
|
|
131
135
|
auth?: false
|
|
136
|
+
tags?: string[]
|
|
137
|
+
middleware?: APIMiddleware[]
|
|
132
138
|
})
|
|
133
139
|
| (CoreHTTPFunction & {
|
|
134
140
|
route: R
|
|
@@ -137,6 +143,8 @@ export type CoreHTTPFunctionRoute<
|
|
|
137
143
|
permissions?: Record<string, APIPermission[] | APIPermission>
|
|
138
144
|
auth?: true
|
|
139
145
|
query?: Array<keyof In>
|
|
146
|
+
tags?: string[]
|
|
147
|
+
middleware?: APIMiddleware[]
|
|
140
148
|
})
|
|
141
149
|
| (CoreHTTPFunction & {
|
|
142
150
|
route: R
|
|
@@ -145,6 +153,8 @@ export type CoreHTTPFunctionRoute<
|
|
|
145
153
|
permissions?: undefined
|
|
146
154
|
auth?: false
|
|
147
155
|
query?: Array<keyof In>
|
|
156
|
+
tags?: string[]
|
|
157
|
+
middleware?: APIMiddleware[]
|
|
148
158
|
})
|
|
149
159
|
|
|
150
160
|
/**
|
|
@@ -175,15 +185,10 @@ export type HTTPRoutesMeta = Array<{
|
|
|
175
185
|
output: string | null
|
|
176
186
|
inputTypes?: HTTPFunctionMetaInputTypes
|
|
177
187
|
docs?: APIDocs
|
|
188
|
+
tags?: string[]
|
|
178
189
|
}>
|
|
179
190
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
* @returns A promise that resolves if access is granted.
|
|
185
|
-
*/
|
|
186
|
-
export type enforceHTTPAccess = (
|
|
187
|
-
route: CoreHTTPFunctionRoute<unknown, unknown, any>,
|
|
188
|
-
session?: CoreUserSession
|
|
189
|
-
) => Promise<void> | void
|
|
191
|
+
export type HTTPRouteMiddleware = {
|
|
192
|
+
route: string
|
|
193
|
+
middleware: PikkuMiddleware[]
|
|
194
|
+
}
|
package/src/http/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parse as parseCookie } from 'cookie'
|
|
2
2
|
import { PikkuRequest } from '../pikku-request.js'
|
|
3
|
-
import { PikkuQuery } from './http-routes.types.js'
|
|
3
|
+
import { HTTPMethod, PikkuQuery } from './http-routes.types.js'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Abstract class representing a pikku request.
|
|
@@ -12,6 +12,13 @@ export abstract class PikkuHTTPAbstractRequest<
|
|
|
12
12
|
> extends PikkuRequest<In> {
|
|
13
13
|
private params: Partial<Record<string, string | string[]>> = {}
|
|
14
14
|
|
|
15
|
+
constructor(
|
|
16
|
+
public path: string,
|
|
17
|
+
public method: HTTPMethod
|
|
18
|
+
) {
|
|
19
|
+
super()
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
/**
|
|
16
23
|
* Retrieves the request body.
|
|
17
24
|
* @returns A promise that resolves to the request body.
|
package/src/index.ts
CHANGED
|
@@ -10,7 +10,9 @@ export * from './http/index.js'
|
|
|
10
10
|
export * from './channel/index.js'
|
|
11
11
|
export * from './scheduler/index.js'
|
|
12
12
|
export * from './errors/index.js'
|
|
13
|
+
export * from './middleware/index.js'
|
|
13
14
|
|
|
15
|
+
export { runMiddleware } from './middleware-runner.js'
|
|
14
16
|
export { addRoute } from './http/http-route-runner.js'
|
|
15
17
|
export { addChannel } from './channel/channel-runner.js'
|
|
16
18
|
export { addScheduledTask } from './scheduler/scheduler-runner.js'
|