@pikku/core 0.9.11 → 0.10.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.
Files changed (124) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/errors/errors.d.ts +2 -0
  3. package/dist/errors/errors.js +6 -0
  4. package/dist/function/function-runner.d.ts +15 -9
  5. package/dist/function/function-runner.js +55 -37
  6. package/dist/function/functions.types.d.ts +67 -1
  7. package/dist/function/functions.types.js +43 -1
  8. package/dist/index.d.ts +2 -6
  9. package/dist/index.js +2 -6
  10. package/dist/middleware/auth-apikey.d.ts +19 -11
  11. package/dist/middleware/auth-apikey.js +33 -33
  12. package/dist/middleware/auth-bearer.d.ts +34 -7
  13. package/dist/middleware/auth-bearer.js +53 -36
  14. package/dist/middleware/auth-cookie.d.ts +30 -12
  15. package/dist/middleware/auth-cookie.js +51 -43
  16. package/dist/middleware/timeout.d.ts +6 -2
  17. package/dist/middleware/timeout.js +7 -9
  18. package/dist/middleware-runner.d.ts +46 -43
  19. package/dist/middleware-runner.js +100 -69
  20. package/dist/permissions.d.ts +82 -20
  21. package/dist/permissions.js +176 -62
  22. package/dist/pikku-state.d.ts +46 -1
  23. package/dist/pikku-state.js +16 -1
  24. package/dist/services/content-service.d.ts +2 -3
  25. package/dist/services/index.d.ts +0 -1
  26. package/dist/services/index.js +0 -1
  27. package/dist/services/local-content.d.ts +2 -4
  28. package/dist/types/core.types.d.ts +110 -4
  29. package/dist/types/core.types.js +44 -1
  30. package/dist/utils.d.ts +1 -1
  31. package/dist/wirings/channel/channel-handler.d.ts +2 -2
  32. package/dist/wirings/channel/channel-handler.js +16 -12
  33. package/dist/wirings/channel/channel-runner.d.ts +2 -2
  34. package/dist/wirings/channel/channel-runner.js +6 -24
  35. package/dist/wirings/channel/channel.types.d.ts +15 -14
  36. package/dist/wirings/channel/local/local-channel-runner.js +12 -12
  37. package/dist/wirings/channel/serverless/serverless-channel-runner.js +7 -8
  38. package/dist/wirings/cli/channel/cli-channel-runner.d.ts +12 -0
  39. package/dist/wirings/cli/channel/cli-channel-runner.js +83 -0
  40. package/dist/wirings/cli/channel/index.d.ts +1 -0
  41. package/dist/wirings/cli/channel/index.js +1 -0
  42. package/dist/wirings/cli/cli-runner.d.ts +42 -0
  43. package/dist/wirings/cli/cli-runner.js +352 -0
  44. package/dist/wirings/cli/cli.types.d.ts +204 -0
  45. package/dist/wirings/cli/cli.types.js +1 -0
  46. package/dist/wirings/cli/command-parser.d.ts +19 -0
  47. package/dist/wirings/cli/command-parser.js +403 -0
  48. package/dist/wirings/cli/index.d.ts +5 -0
  49. package/dist/wirings/cli/index.js +5 -0
  50. package/dist/wirings/http/http-runner.d.ts +59 -10
  51. package/dist/wirings/http/http-runner.js +138 -131
  52. package/dist/wirings/http/http.types.d.ts +11 -10
  53. package/dist/wirings/http/index.d.ts +1 -1
  54. package/dist/wirings/http/index.js +1 -1
  55. package/dist/wirings/http/pikku-fetch-http-response.js +3 -1
  56. package/dist/wirings/http/routers/http-router.d.ts +0 -2
  57. package/dist/wirings/http/routers/path-to-regex.d.ts +1 -1
  58. package/dist/wirings/http/routers/path-to-regex.js +5 -34
  59. package/dist/wirings/mcp/mcp-endpoint-registry.d.ts +1 -1
  60. package/dist/wirings/mcp/mcp-runner.d.ts +4 -5
  61. package/dist/wirings/mcp/mcp-runner.js +32 -34
  62. package/dist/wirings/mcp/mcp.types.d.ts +38 -14
  63. package/dist/wirings/queue/queue-runner.d.ts +2 -2
  64. package/dist/wirings/queue/queue-runner.js +25 -27
  65. package/dist/wirings/queue/queue.types.d.ts +6 -5
  66. package/dist/wirings/rpc/index.d.ts +1 -1
  67. package/dist/wirings/rpc/index.js +1 -1
  68. package/dist/wirings/rpc/rpc-runner.d.ts +6 -18
  69. package/dist/wirings/rpc/rpc-runner.js +13 -8
  70. package/dist/wirings/scheduler/scheduler-runner.d.ts +2 -2
  71. package/dist/wirings/scheduler/scheduler-runner.js +38 -35
  72. package/dist/wirings/scheduler/scheduler.types.d.ts +5 -4
  73. package/package.json +9 -7
  74. package/src/errors/errors.ts +6 -0
  75. package/src/factory-functions.test.ts +60 -1
  76. package/src/function/function-runner.test.ts +81 -26
  77. package/src/function/function-runner.ts +89 -57
  78. package/src/function/functions.types.ts +85 -2
  79. package/src/index.ts +6 -19
  80. package/src/middleware/auth-apikey.ts +42 -57
  81. package/src/middleware/auth-bearer.ts +66 -49
  82. package/src/middleware/auth-cookie.ts +63 -82
  83. package/src/middleware/timeout.ts +10 -14
  84. package/src/middleware-runner.test.ts +24 -16
  85. package/src/middleware-runner.ts +115 -87
  86. package/src/permissions.test.ts +26 -27
  87. package/src/permissions.ts +216 -104
  88. package/src/pikku-state.ts +79 -2
  89. package/src/services/content-service.ts +5 -4
  90. package/src/services/index.ts +0 -1
  91. package/src/services/local-content.ts +9 -4
  92. package/src/types/core.types.ts +143 -5
  93. package/src/utils.ts +1 -1
  94. package/src/wirings/channel/channel-handler.ts +18 -21
  95. package/src/wirings/channel/channel-runner.ts +19 -32
  96. package/src/wirings/channel/channel.types.ts +29 -16
  97. package/src/wirings/channel/local/local-channel-runner.ts +25 -15
  98. package/src/wirings/channel/serverless/serverless-channel-runner.ts +15 -17
  99. package/src/wirings/cli/channel/cli-channel-runner.ts +123 -0
  100. package/src/wirings/cli/channel/index.ts +1 -0
  101. package/src/wirings/cli/cli-runner.test.ts +403 -0
  102. package/src/wirings/cli/cli-runner.ts +539 -0
  103. package/src/wirings/cli/cli.types.ts +350 -0
  104. package/src/wirings/cli/command-parser.test.ts +445 -0
  105. package/src/wirings/cli/command-parser.ts +504 -0
  106. package/src/wirings/cli/index.ts +13 -0
  107. package/src/wirings/http/http-runner.test.ts +8 -7
  108. package/src/wirings/http/http-runner.ts +165 -158
  109. package/src/wirings/http/http.types.ts +58 -11
  110. package/src/wirings/http/index.ts +7 -1
  111. package/src/wirings/http/pikku-fetch-http-response.ts +3 -1
  112. package/src/wirings/http/routers/http-router.ts +0 -2
  113. package/src/wirings/http/routers/path-to-regex.test.ts +4 -5
  114. package/src/wirings/http/routers/path-to-regex.ts +6 -43
  115. package/src/wirings/mcp/mcp-endpoint-registry.ts +1 -1
  116. package/src/wirings/mcp/mcp-runner.ts +58 -55
  117. package/src/wirings/mcp/mcp.types.ts +65 -14
  118. package/src/wirings/queue/queue-runner.ts +44 -47
  119. package/src/wirings/queue/queue.types.ts +10 -6
  120. package/src/wirings/rpc/index.ts +1 -1
  121. package/src/wirings/rpc/rpc-runner.ts +27 -9
  122. package/src/wirings/scheduler/scheduler-runner.ts +58 -56
  123. package/src/wirings/scheduler/scheduler.types.ts +10 -3
  124. package/tsconfig.tsbuildinfo +1 -1
@@ -8,23 +8,27 @@ import {
8
8
  HTTPWiringMeta,
9
9
  HTTPMethod,
10
10
  } from './http.types.js'
11
+ import {
12
+ CorePikkuFunction,
13
+ CorePikkuFunctionSessionless,
14
+ CorePikkuPermission,
15
+ CorePermissionGroup,
16
+ } from '../../function/functions.types.js'
11
17
  import {
12
18
  CoreUserSession,
13
19
  CorePikkuMiddleware,
20
+ CorePikkuMiddlewareGroup,
14
21
  SessionServices,
15
22
  PikkuWiringTypes,
23
+ PikkuInteraction,
16
24
  } from '../../types/core.types.js'
17
- import { MissingSessionError, NotFoundError } from '../../errors/errors.js'
25
+ import { NotFoundError } from '../../errors/errors.js'
18
26
  import {
19
27
  closeSessionServices,
20
28
  createWeakUID,
21
29
  isSerializable,
22
30
  } from '../../utils.js'
23
- import {
24
- PikkuUserSessionService,
25
- UserSessionService,
26
- } from '../../services/user-session-service.js'
27
- import { combineMiddleware, runMiddleware } from '../../middleware-runner.js'
31
+ import { PikkuUserSessionService } from '../../services/user-session-service.js'
28
32
  import { handleHTTPError } from '../../handle-error.js'
29
33
  import { pikkuState } from '../../pikku-state.js'
30
34
  import { PikkuFetchHTTPResponse } from './pikku-fetch-http-response.js'
@@ -35,36 +39,79 @@ import { rpcService } from '../rpc/rpc-runner.js'
35
39
  import { httpRouter } from './routers/http-router.js'
36
40
 
37
41
  /**
38
- * Registers middleware either globally or for a specific route.
42
+ * Registers HTTP middleware for a specific route pattern.
43
+ *
44
+ * This function registers middleware at runtime that will be applied to
45
+ * HTTP routes matching the specified pattern.
39
46
  *
40
- * When a string route pattern is provided along with middleware, the middleware
41
- * is applied only to that route. Otherwise, if an array is provided, it is treated
42
- * as global middleware (applied to all routes).
47
+ * For tree-shaking benefits, wrap in a factory function:
48
+ * `export const x = () => addHTTPMiddleware('pattern', [...])`
43
49
  *
44
50
  * @template PikkuMiddleware The middleware type.
45
- * @param {PikkuMiddleware[] | string} routeOrMiddleware - Either a global middleware array or a route pattern string.
46
- * @param {PikkuMiddleware[]} [middleware] - The middleware array to apply when a route pattern is specified.
51
+ * @param {string} pattern - Route pattern (e.g., '*' for all routes, '/api/*' for specific routes).
52
+ * @param {CorePikkuMiddlewareGroup} middleware - Array of middleware for this route pattern.
53
+ *
54
+ * @returns {CorePikkuMiddlewareGroup} The middleware array (for chaining/wrapping).
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * // Recommended: tree-shakeable
59
+ * export const httpGlobal = () => addHTTPMiddleware('*', [
60
+ * corsMiddleware,
61
+ * loggingMiddleware
62
+ * ])
63
+ *
64
+ * // Also works: no tree-shaking
65
+ * export const apiMiddleware = addHTTPMiddleware('/api/*', [
66
+ * authMiddleware
67
+ * ])
68
+ * ```
47
69
  */
48
70
  export const addHTTPMiddleware = <PikkuMiddleware extends CorePikkuMiddleware>(
49
- routeOrMiddleware: PikkuMiddleware[] | string,
50
- middleware?: PikkuMiddleware[]
51
- ) => {
52
- const middlewareStore = pikkuState('http', 'middleware')
53
- let route = '*'
54
-
55
- if (typeof routeOrMiddleware === 'string') {
56
- route = routeOrMiddleware
57
- middleware = middleware!
58
- } else {
59
- middleware = routeOrMiddleware
60
- }
71
+ pattern: string,
72
+ middleware: CorePikkuMiddlewareGroup
73
+ ): CorePikkuMiddlewareGroup => {
74
+ const httpGroups = pikkuState('middleware', 'httpGroup')
75
+ httpGroups[pattern] = middleware
76
+ return middleware
77
+ }
61
78
 
62
- const currentMiddleware = middlewareStore.get(route)
63
- if (currentMiddleware) {
64
- middlewareStore.set(route, [...currentMiddleware, ...middleware])
65
- } else {
66
- middlewareStore.set(route, middleware)
67
- }
79
+ /**
80
+ * Registers HTTP permissions for a specific route pattern.
81
+ *
82
+ * This function registers permissions at runtime that will be applied to
83
+ * HTTP routes matching the specified pattern.
84
+ *
85
+ * For tree-shaking benefits, wrap in a factory function:
86
+ * `export const x = () => addHTTPPermission('pattern', [...])`
87
+ *
88
+ * @template PikkuPermission The permission type.
89
+ * @param {string} pattern - Route pattern (e.g., '*' for all routes, '/api/*' for specific routes).
90
+ * @param {CorePermissionGroup | CorePikkuPermission[]} permissions - Permissions for this route pattern.
91
+ *
92
+ * @returns {CorePermissionGroup | CorePikkuPermission[]} The permissions (for chaining/wrapping).
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * // Recommended: tree-shakeable
97
+ * export const httpGlobalPermissions = () => addHTTPPermission('*', [
98
+ * authenticatedPermission,
99
+ * rateLimitPermission
100
+ * ])
101
+ *
102
+ * // Also works: no tree-shaking
103
+ * export const apiPermissions = addHTTPPermission('/api/*', [
104
+ * adminPermission
105
+ * ])
106
+ * ```
107
+ */
108
+ export const addHTTPPermission = <PikkuPermission extends CorePikkuPermission>(
109
+ pattern: string,
110
+ permissions: CorePermissionGroup | CorePikkuPermission[]
111
+ ): CorePermissionGroup | CorePikkuPermission[] => {
112
+ const httpGroups = pikkuState('permissions', 'httpGroup')
113
+ httpGroups[pattern] = permissions
114
+ return permissions
68
115
  }
69
116
 
70
117
  /**
@@ -86,10 +133,14 @@ export const wireHTTP = <
86
133
  In,
87
134
  Out,
88
135
  Route extends string,
89
- PikkuFunction,
90
- PikkuFunctionSessionless,
91
- PikkuPermissionGroup,
92
- PikkuMiddleware,
136
+ PikkuFunction extends CorePikkuFunction<In, Out> = CorePikkuFunction<In, Out>,
137
+ PikkuFunctionSessionless extends CorePikkuFunctionSessionless<
138
+ In,
139
+ Out
140
+ > = CorePikkuFunctionSessionless<In, Out>,
141
+ PikkuPermissionGroup extends
142
+ CorePikkuPermission<In> = CorePikkuPermission<In>,
143
+ PikkuMiddleware extends CorePikkuMiddleware = CorePikkuMiddleware,
93
144
  >(
94
145
  httpWiring: CoreHTTPFunctionWiring<
95
146
  In,
@@ -106,7 +157,7 @@ export const wireHTTP = <
106
157
  if (!routeMeta) {
107
158
  throw new Error('Route metadata not found')
108
159
  }
109
- addFunction(routeMeta.pikkuFuncName, httpWiring.func as any)
160
+ addFunction(routeMeta.pikkuFuncName, httpWiring.func)
110
161
  const routes = pikkuState('http', 'routes')
111
162
  if (!routes.has(httpWiring.method)) {
112
163
  routes.set(httpWiring.method, new Map())
@@ -146,8 +197,6 @@ const getMatchingRoute = (requestType: string, requestPath: string) => {
146
197
  params: matchedPath.params,
147
198
  route,
148
199
  permissions: route.permissions,
149
- httpMiddleware: matchedPath.middleware,
150
- middleware: route.middleware,
151
200
  meta: meta!,
152
201
  }
153
202
  }
@@ -183,16 +232,7 @@ export const createHTTPInteraction = (
183
232
  }
184
233
 
185
234
  /**
186
- * Validates the input data and executes the route handler with associated middleware.
187
- *
188
- * NOTE: HTTP wiring handles middleware differently from other wirings (RPC, MCP, Queue, etc.)
189
- * because HTTP needs to:
190
- * 1. Check session early for performance (before expensive body parsing)
191
- * 2. Handle HTTP-specific concerns (headers, cookies, SSE setup)
192
- * 3. Process middleware that may set up authentication/session state
193
- *
194
- * Other wirings (RPC/MCP/Queue/Scheduler) simply pass middleware/permissions/auth
195
- * directly to runPikkuFunc without processing them.
235
+ * Validates the input data and executes the route handler
196
236
  *
197
237
  * This function performs these steps:
198
238
  * 1. Sets URL parameters on the request.
@@ -205,16 +245,15 @@ export const createHTTPInteraction = (
205
245
  * 8. Sends the appropriate response.
206
246
  *
207
247
  * @param {Object} services - A collection of shared services and utilities.
208
- * @param {Object} matchedRoute - Contains route details, URL parameters, middleware, and optional schema.
248
+ * @param {Object} matchedRoute - Contains route details, URL parameters, and optional schema.
209
249
  * @param {PikkuHTTP | undefined} http - The HTTP interaction object.
210
250
  * @param {Object} options - Options for route execution (e.g., whether to coerce query strings to arrays).
211
251
  * @returns {Promise<any>} An object containing the route handler result and session services (if any).
212
252
  * @throws Throws errors like MissingSessionError or ForbiddenError on validation failures.
213
253
  */
214
- const executeRouteWithMiddleware = async (
254
+ const executeRoute = async (
215
255
  services: {
216
256
  singletonServices: any
217
- userSession: UserSessionService<CoreUserSession>
218
257
  createSessionServices: Function
219
258
  skipUserSession: boolean
220
259
  requestId: string
@@ -223,8 +262,6 @@ const executeRouteWithMiddleware = async (
223
262
  matchedPath: any
224
263
  params: any
225
264
  route: CoreHTTPFunctionWiring<any, any, any>
226
- httpMiddleware: CorePikkuMiddleware[] | undefined
227
- middleware: CorePikkuMiddleware[] | undefined
228
265
  meta: HTTPWiringMeta
229
266
  },
230
267
  http: PikkuHTTP,
@@ -232,11 +269,10 @@ const executeRouteWithMiddleware = async (
232
269
  coerceDataFromSchema: boolean
233
270
  }
234
271
  ) => {
235
- const { matchedPath, params, route, httpMiddleware, middleware, meta } =
236
- matchedRoute
272
+ const userSession = new PikkuUserSessionService<CoreUserSession>()
273
+ const { params, route, meta } = matchedRoute
237
274
  const {
238
275
  singletonServices,
239
- userSession,
240
276
  createSessionServices,
241
277
  skipUserSession,
242
278
  requestId,
@@ -253,116 +289,96 @@ const executeRouteWithMiddleware = async (
253
289
  `Matched route: ${route.route} | method: ${route.method.toUpperCase()} | auth: ${requiresSession.toString()}`
254
290
  )
255
291
 
256
- // Main route execution logic wrapped for middleware handling
257
- const runMain = async () => {
258
- const session = userSession.get()
292
+ // Ensure session is available when required
293
+ if (skipUserSession && requiresSession) {
294
+ throw new Error("Can't skip trying to get user session if auth is required")
295
+ }
259
296
 
260
- // Ensure session is available when required
261
- if (skipUserSession && requiresSession) {
262
- throw new Error(
263
- "Can't skip trying to get user session if auth is required"
264
- )
265
- }
297
+ const data = () => http.request!.data()
298
+ let channel: PikkuChannel<unknown, unknown> | undefined
266
299
 
267
- if (requiresSession && !session) {
268
- singletonServices.logger.info({
269
- action: 'Rejecting route (invalid session)',
270
- path: matchedPath,
271
- })
272
- throw new MissingSessionError()
300
+ if (matchedRoute.route.sse) {
301
+ const response = http?.response
302
+ if (!response) {
303
+ throw new Error('SSE requires a valid HTTP response object')
304
+ }
305
+ if (!response.setMode) {
306
+ throw new Error('Response object does not support SSE mode')
273
307
  }
308
+ response.setMode('stream')
309
+ response.header('Content-Type', 'text/event-stream')
310
+ response.header('Cache-Control', 'no-cache')
311
+ response.header('Connection', 'keep-alive')
312
+ response.header('Transfer-Encoding', 'chunked')
313
+ channel = {
314
+ channelId: requestId,
315
+ openingData: await data(),
316
+ send: (data: any) => {
317
+ response.arrayBuffer(isSerializable(data) ? JSON.stringify(data) : data)
318
+ },
319
+ close: () => {
320
+ channel!.state = 'closed'
321
+ response.close?.()
322
+ },
323
+ state: 'open',
324
+ }
325
+ }
274
326
 
275
- const data = await http.request!.data()
327
+ const interaction: PikkuInteraction = { http, channel }
276
328
 
277
- const getAllServices = async () => {
278
- let channel: PikkuChannel<unknown, unknown> | undefined
279
- if (matchedRoute.route.sse) {
280
- const response = http?.response
281
- if (!response) {
282
- throw new Error('SSE requires a valid HTTP response object')
283
- }
284
- if (!response.setMode) {
285
- throw new Error('Response object does not support SSE mode')
286
- }
287
- response.setMode('stream')
288
- response.header('Content-Type', 'text/event-stream')
289
- response.header('Cache-Control', 'no-cache')
290
- response.header('Connection', 'keep-alive')
291
- response.header('Transfer-Encoding', 'chunked')
292
- channel = {
293
- channelId: requestId,
294
- openingData: data,
295
- send: (data: any) => {
296
- response.arrayBuffer(
297
- isSerializable(data) ? JSON.stringify(data) : data
298
- )
299
- },
300
- close: () => {
301
- channel!.state = 'closed'
302
- response.close?.()
303
- },
304
- state: 'open',
305
- }
306
- }
329
+ const getAllServices = async (session?: CoreUserSession) => {
330
+ let channel: PikkuChannel<unknown, unknown> | undefined
307
331
 
308
- // Create session-specific services for handling the request
309
- sessionServices = await createSessionServices(
310
- { ...singletonServices, userSession, channel },
311
- { http },
312
- session
313
- )
332
+ // Create session-specific services for handling the request
333
+ sessionServices = await createSessionServices(
334
+ { ...singletonServices, userSession, channel },
335
+ { http },
336
+ session
337
+ )
314
338
 
315
- return rpcService.injectRPCService({
339
+ return rpcService.injectRPCService(
340
+ {
316
341
  ...singletonServices,
317
342
  ...sessionServices,
318
343
  http,
319
344
  userSession,
320
345
  channel,
321
- })
322
- }
323
-
324
- result = await runPikkuFunc(
325
- PikkuWiringTypes.http,
326
- `${meta.method}:${meta.route}`,
327
- meta.pikkuFuncName,
328
- {
329
- getAllServices,
330
- session,
331
- data,
332
- permissions: route.permissions,
333
- coerceDataFromSchema: options.coerceDataFromSchema,
334
- tags: route.tags,
335
- }
346
+ },
347
+ interaction,
348
+ route.auth
336
349
  )
350
+ }
337
351
 
338
- // Respond with either a binary or JSON response based on configuration
339
- if (route.returnsJSON === false) {
340
- http?.response?.arrayBuffer(result)
341
- } else {
342
- http?.response?.json(result)
352
+ result = await runPikkuFunc(
353
+ PikkuWiringTypes.http,
354
+ `${meta.method}:${meta.route}`,
355
+ meta.pikkuFuncName,
356
+ {
357
+ singletonServices,
358
+ getAllServices,
359
+ auth: route.auth !== false,
360
+ userSession,
361
+ data,
362
+ inheritedMiddleware: meta.middleware,
363
+ wireMiddleware: route.middleware,
364
+ inheritedPermissions: meta.permissions,
365
+ wirePermissions: route.permissions,
366
+ coerceDataFromSchema: options.coerceDataFromSchema,
367
+ tags: route.tags,
368
+ interaction,
343
369
  }
370
+ )
344
371
 
345
- http?.response?.status(200)
346
- // TODO: Evaluate if the response stream should be explicitly ended.
347
- // http?.response?.end()
372
+ // Respond with either a binary or JSON response based on configuration
373
+ if (route.returnsJSON === false) {
374
+ http?.response?.arrayBuffer(result)
375
+ } else {
376
+ http?.response?.json(result)
348
377
  }
349
378
 
350
- // Get function config for middleware and tags
351
- const funcConfig = pikkuState('function', 'functions').get(meta.pikkuFuncName)
352
-
353
- // Execute middleware, then run the main logic
354
- await runMiddleware(
355
- { ...singletonServices, userSession },
356
- { http },
357
- combineMiddleware(PikkuWiringTypes.http, `${meta.method}:${meta.route}`, {
358
- httpMiddleware,
359
- wiringMiddleware: middleware,
360
- wiringTags: route.tags,
361
- funcMiddleware: funcConfig?.middleware,
362
- funcTags: funcConfig?.tags,
363
- }),
364
- runMain
365
- )
379
+ http?.response?.status(200)
380
+ // TODO: Evaluate if the response stream should be explicitly ended.
381
+ // http?.response?.end()
366
382
 
367
383
  return sessionServices ? { result, sessionServices } : { result }
368
384
  }
@@ -442,14 +458,12 @@ export const fetchData = async <In, Out>(
442
458
  coerceDataFromSchema = true,
443
459
  bubbleErrors = false,
444
460
  generateRequestId,
445
- ignoreMiddleware = false,
446
461
  }: RunHTTPWiringOptions & RunHTTPWiringParams
447
462
  ): Promise<Out | void> => {
448
463
  const requestId =
449
464
  (request as any).getHeader?.('x-request-id') ||
450
465
  generateRequestId?.() ||
451
466
  createWeakUID()
452
- const userSession = new PikkuUserSessionService()
453
467
  let sessionServices: SessionServices<typeof singletonServices> | undefined
454
468
  let result: Out
455
469
 
@@ -475,21 +489,14 @@ export const fetchData = async <In, Out>(
475
489
  }
476
490
 
477
491
  // Execute the matched route along with its middleware and session management
478
- ;({ result, sessionServices } = await executeRouteWithMiddleware(
492
+ ;({ result, sessionServices } = await executeRoute(
479
493
  {
480
494
  singletonServices,
481
- userSession,
482
495
  createSessionServices,
483
496
  skipUserSession,
484
497
  requestId,
485
498
  },
486
- ignoreMiddleware
487
- ? {
488
- ...matchedRoute,
489
- middleware: undefined,
490
- httpMiddleware: undefined,
491
- }
492
- : matchedRoute,
499
+ matchedRoute,
493
500
  http!,
494
501
  { coerceDataFromSchema }
495
502
  ))
@@ -7,12 +7,15 @@ import type {
7
7
  CoreUserSession,
8
8
  CreateSessionServices,
9
9
  CorePikkuMiddleware,
10
+ MiddlewareMetadata,
11
+ PermissionMetadata,
10
12
  } from '../../types/core.types.js'
11
13
  import type {
12
14
  CorePikkuFunction,
13
15
  CorePikkuFunctionSessionless,
14
16
  CorePikkuPermission,
15
17
  CorePermissionGroup,
18
+ CorePikkuFunctionConfig,
16
19
  } from '../../function/functions.types.js'
17
20
 
18
21
  type ExtractHTTPWiringParams<S extends string> =
@@ -39,7 +42,6 @@ export type RunHTTPWiringOptions = Partial<{
39
42
  coerceDataFromSchema: boolean
40
43
  bubbleErrors: boolean
41
44
  generateRequestId: () => string
42
- ignoreMiddleware: boolean
43
45
  }>
44
46
 
45
47
  export type RunHTTPWiringParams = {
@@ -117,15 +119,38 @@ export type CoreHTTPFunctionWiring<
117
119
  In,
118
120
  Out,
119
121
  R extends string,
120
- PikkuFunction = CorePikkuFunction<In, Out>,
121
- PikkuFunctionSessionless = CorePikkuFunctionSessionless<In, Out>,
122
- PikkuPermission = CorePikkuPermission<In>,
123
- PikkuMiddleware = CorePikkuMiddleware,
122
+ PikkuFunction extends CorePikkuFunction<
123
+ In,
124
+ Out,
125
+ any,
126
+ any,
127
+ any
128
+ > = CorePikkuFunction<In, Out>,
129
+ PikkuFunctionSessionless extends CorePikkuFunctionSessionless<
130
+ In,
131
+ Out,
132
+ any,
133
+ any,
134
+ any
135
+ > = CorePikkuFunctionSessionless<In, Out>,
136
+ PikkuPermission extends CorePikkuPermission<
137
+ In,
138
+ any,
139
+ any
140
+ > = CorePikkuPermission<In, any, any>,
141
+ PikkuMiddleware extends CorePikkuMiddleware<
142
+ any,
143
+ any
144
+ > = CorePikkuMiddleware<any>,
124
145
  > =
125
146
  | (CoreHTTPFunction & {
126
147
  route: R
127
148
  method: HTTPMethod
128
- func: PikkuFunction
149
+ func: CorePikkuFunctionConfig<
150
+ PikkuFunction,
151
+ PikkuPermission,
152
+ PikkuMiddleware
153
+ >
129
154
  permissions?: CorePermissionGroup<PikkuPermission>
130
155
  auth?: true
131
156
  tags?: string[]
@@ -135,7 +160,11 @@ export type CoreHTTPFunctionWiring<
135
160
  | (CoreHTTPFunction & {
136
161
  route: R
137
162
  method: HTTPMethod
138
- func: PikkuFunctionSessionless
163
+ func: CorePikkuFunctionConfig<
164
+ PikkuFunctionSessionless,
165
+ PikkuPermission,
166
+ PikkuMiddleware
167
+ >
139
168
  permissions?: undefined
140
169
  auth?: false
141
170
  tags?: string[]
@@ -145,7 +174,11 @@ export type CoreHTTPFunctionWiring<
145
174
  | (CoreHTTPFunction & {
146
175
  route: R
147
176
  method: 'get'
148
- func: PikkuFunction
177
+ func: CorePikkuFunctionConfig<
178
+ PikkuFunction,
179
+ PikkuPermission,
180
+ PikkuMiddleware
181
+ >
149
182
  permissions?: CorePermissionGroup<PikkuPermission>
150
183
  auth?: true
151
184
  sse?: boolean
@@ -155,7 +188,11 @@ export type CoreHTTPFunctionWiring<
155
188
  | (CoreHTTPFunction & {
156
189
  route: R
157
190
  method: 'get'
158
- func: PikkuFunctionSessionless
191
+ func: CorePikkuFunctionConfig<
192
+ PikkuFunctionSessionless,
193
+ PikkuPermission,
194
+ PikkuMiddleware
195
+ >
159
196
  permissions?: undefined
160
197
  auth?: false
161
198
  sse?: boolean
@@ -165,7 +202,11 @@ export type CoreHTTPFunctionWiring<
165
202
  | (CoreHTTPFunction & {
166
203
  route: R
167
204
  method: 'post'
168
- func: PikkuFunction
205
+ func: CorePikkuFunctionConfig<
206
+ PikkuFunction,
207
+ PikkuPermission,
208
+ PikkuMiddleware
209
+ >
169
210
  permissions?: CorePermissionGroup<PikkuPermission>
170
211
  auth?: true
171
212
  query?: Array<keyof In>
@@ -176,7 +217,11 @@ export type CoreHTTPFunctionWiring<
176
217
  | (CoreHTTPFunction & {
177
218
  route: R
178
219
  method: 'post'
179
- func: PikkuFunctionSessionless
220
+ func: CorePikkuFunctionConfig<
221
+ PikkuFunctionSessionless,
222
+ PikkuPermission,
223
+ PikkuMiddleware
224
+ >
180
225
  permissions?: undefined
181
226
  auth?: false
182
227
  query?: Array<keyof In>
@@ -207,6 +252,8 @@ export type HTTPWiringMeta = {
207
252
  docs?: PikkuDocs
208
253
  tags?: string[]
209
254
  sse?: true
255
+ middleware?: MiddlewareMetadata[] // Pre-resolved middleware chain (global + route + tag + explicit)
256
+ permissions?: PermissionMetadata[] // Pre-resolved permission chain (global + route + tag + explicit)
210
257
  }
211
258
  export type HTTPWiringsMeta = Record<HTTPMethod, Record<string, HTTPWiringMeta>>
212
259
 
@@ -2,6 +2,12 @@ export * from './pikku-fetch-http-request.js'
2
2
  export * from './pikku-fetch-http-response.js'
3
3
  export * from './log-http-routes.js'
4
4
 
5
- export { fetch, fetchData, wireHTTP } from './http-runner.js'
5
+ export {
6
+ fetch,
7
+ fetchData,
8
+ wireHTTP,
9
+ addHTTPMiddleware,
10
+ addHTTPPermission,
11
+ } from './http-runner.js'
6
12
 
7
13
  export type * from './http.types.js'
@@ -107,7 +107,9 @@ export class PikkuFetchHTTPResponse implements PikkuHTTPResponse {
107
107
  const cookieHeader = Array.from(this.#cookies.entries()).map(
108
108
  ([name, { value, flags }]) => serializeCookie(name, value, flags)
109
109
  )
110
- this.#headers.set('Set-Cookie', cookieHeader.join(', '))
110
+ // Multiple Set-Cookie headers must be appended separately, not joined with commas
111
+ // per HTTP specification (RFC 6265)
112
+ cookieHeader.forEach((cookie) => this.#headers.append('Set-Cookie', cookie))
111
113
  return new Response(this.#body, {
112
114
  ...args,
113
115
  status: this.#statusCode,
@@ -1,11 +1,9 @@
1
- import { CorePikkuMiddleware } from '../../../types/core.types.js'
2
1
  import { HTTPMethod } from '../http.types.js'
3
2
  import { PathToRegexRouter } from './path-to-regex.js'
4
3
 
5
4
  export type MatchResult = {
6
5
  route: string
7
6
  params: Record<string, any>
8
- middleware?: CorePikkuMiddleware[]
9
7
  } | null
10
8
 
11
9
  export interface Router {
@@ -46,7 +46,6 @@ describe('PathToRegexRouter', () => {
46
46
  assert.ok(staticResult, 'Static route should match')
47
47
  assert.strictEqual(staticResult.route, '/static')
48
48
  assert.deepStrictEqual(staticResult.params, {})
49
- assert.strictEqual(staticResult.middleware?.length, 0)
50
49
 
51
50
  const aboutResult = router.match('get', '/api/about')
52
51
  assert.ok(aboutResult, 'Static route /api/about should match')
@@ -229,7 +228,7 @@ describe('PathToRegexRouter', () => {
229
228
  })
230
229
 
231
230
  describe('Middleware Integration', () => {
232
- test('should include global and route-specific middleware', () => {
231
+ test('should match route without returning middleware', () => {
233
232
  const globalMiddleware = [async () => {}]
234
233
  const routeMiddleware = [async () => {}]
235
234
 
@@ -252,9 +251,9 @@ describe('PathToRegexRouter', () => {
252
251
 
253
252
  const result = router.match('get', '/api/test')
254
253
  assert.ok(result, 'Route should match')
255
- assert.strictEqual(result.middleware?.length, 2) // global + route-specific
256
- assert.strictEqual(result.middleware?.[0], globalMiddleware[0])
257
- assert.strictEqual(result.middleware?.[1], routeMiddleware[0])
254
+ // Router only returns route and params, middleware is handled separately
255
+ assert.strictEqual(result.route, '/api/test')
256
+ assert.deepStrictEqual(result.params, {})
258
257
  })
259
258
  })
260
259