@pikku/core 0.9.11 → 0.9.12-next.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 (99) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/function/function-runner.d.ts +13 -8
  3. package/dist/function/function-runner.js +49 -37
  4. package/dist/index.d.ts +3 -1
  5. package/dist/index.js +3 -1
  6. package/dist/middleware-runner.d.ts +61 -40
  7. package/dist/middleware-runner.js +118 -66
  8. package/dist/permissions.js +1 -0
  9. package/dist/pikku-state.d.ts +26 -1
  10. package/dist/pikku-state.js +10 -1
  11. package/dist/services/content-service.d.ts +2 -3
  12. package/dist/services/index.d.ts +0 -1
  13. package/dist/services/index.js +0 -1
  14. package/dist/services/local-content.d.ts +2 -4
  15. package/dist/types/core.types.d.ts +51 -2
  16. package/dist/types/core.types.js +21 -0
  17. package/dist/wirings/channel/channel-handler.d.ts +2 -2
  18. package/dist/wirings/channel/channel-handler.js +12 -9
  19. package/dist/wirings/channel/channel-runner.d.ts +0 -2
  20. package/dist/wirings/channel/channel-runner.js +2 -3
  21. package/dist/wirings/channel/channel.types.d.ts +5 -3
  22. package/dist/wirings/channel/local/local-channel-runner.js +12 -12
  23. package/dist/wirings/channel/serverless/serverless-channel-runner.js +7 -8
  24. package/dist/wirings/cli/channel/cli-channel-runner.d.ts +12 -0
  25. package/dist/wirings/cli/channel/cli-channel-runner.js +80 -0
  26. package/dist/wirings/cli/channel/index.d.ts +1 -0
  27. package/dist/wirings/cli/channel/index.js +1 -0
  28. package/dist/wirings/cli/cli-runner.d.ts +32 -0
  29. package/dist/wirings/cli/cli-runner.js +328 -0
  30. package/dist/wirings/cli/cli.types.d.ts +177 -0
  31. package/dist/wirings/cli/cli.types.js +1 -0
  32. package/dist/wirings/cli/command-parser.d.ts +19 -0
  33. package/dist/wirings/cli/command-parser.js +373 -0
  34. package/dist/wirings/cli/index.d.ts +5 -0
  35. package/dist/wirings/cli/index.js +5 -0
  36. package/dist/wirings/http/http-runner.d.ts +29 -10
  37. package/dist/wirings/http/http-runner.js +103 -131
  38. package/dist/wirings/http/http.types.d.ts +10 -10
  39. package/dist/wirings/http/index.d.ts +1 -1
  40. package/dist/wirings/http/index.js +1 -1
  41. package/dist/wirings/http/pikku-fetch-http-response.js +3 -1
  42. package/dist/wirings/http/routers/http-router.d.ts +0 -2
  43. package/dist/wirings/http/routers/path-to-regex.d.ts +1 -1
  44. package/dist/wirings/http/routers/path-to-regex.js +5 -34
  45. package/dist/wirings/mcp/mcp-runner.d.ts +4 -5
  46. package/dist/wirings/mcp/mcp-runner.js +30 -34
  47. package/dist/wirings/mcp/mcp.types.d.ts +11 -8
  48. package/dist/wirings/queue/queue-runner.d.ts +2 -2
  49. package/dist/wirings/queue/queue-runner.js +25 -27
  50. package/dist/wirings/queue/queue.types.d.ts +6 -5
  51. package/dist/wirings/rpc/index.d.ts +1 -1
  52. package/dist/wirings/rpc/index.js +1 -1
  53. package/dist/wirings/rpc/rpc-runner.d.ts +6 -18
  54. package/dist/wirings/rpc/rpc-runner.js +13 -8
  55. package/dist/wirings/scheduler/scheduler-runner.d.ts +2 -2
  56. package/dist/wirings/scheduler/scheduler-runner.js +37 -35
  57. package/dist/wirings/scheduler/scheduler.types.d.ts +4 -3
  58. package/package.json +4 -3
  59. package/src/function/function-runner.test.ts +73 -23
  60. package/src/function/function-runner.ts +74 -55
  61. package/src/index.ts +8 -1
  62. package/src/middleware-runner.test.ts +24 -16
  63. package/src/middleware-runner.ts +136 -83
  64. package/src/permissions.ts +1 -0
  65. package/src/pikku-state.ts +47 -2
  66. package/src/services/content-service.ts +5 -4
  67. package/src/services/index.ts +0 -1
  68. package/src/services/local-content.ts +9 -4
  69. package/src/types/core.types.ts +74 -3
  70. package/src/wirings/channel/channel-handler.ts +9 -13
  71. package/src/wirings/channel/channel-runner.ts +2 -6
  72. package/src/wirings/channel/channel.types.ts +5 -1
  73. package/src/wirings/channel/local/local-channel-runner.ts +25 -15
  74. package/src/wirings/channel/serverless/serverless-channel-runner.ts +15 -17
  75. package/src/wirings/cli/channel/cli-channel-runner.ts +118 -0
  76. package/src/wirings/cli/channel/index.ts +1 -0
  77. package/src/wirings/cli/cli-runner.test.ts +382 -0
  78. package/src/wirings/cli/cli-runner.ts +503 -0
  79. package/src/wirings/cli/cli.types.ts +320 -0
  80. package/src/wirings/cli/command-parser.test.ts +440 -0
  81. package/src/wirings/cli/command-parser.ts +470 -0
  82. package/src/wirings/cli/index.ts +12 -0
  83. package/src/wirings/http/http-runner.test.ts +8 -7
  84. package/src/wirings/http/http-runner.ts +126 -159
  85. package/src/wirings/http/http.types.ts +56 -11
  86. package/src/wirings/http/index.ts +1 -1
  87. package/src/wirings/http/pikku-fetch-http-response.ts +3 -1
  88. package/src/wirings/http/routers/http-router.ts +0 -2
  89. package/src/wirings/http/routers/path-to-regex.test.ts +4 -5
  90. package/src/wirings/http/routers/path-to-regex.ts +6 -43
  91. package/src/wirings/mcp/mcp-runner.ts +56 -55
  92. package/src/wirings/mcp/mcp.types.ts +23 -8
  93. package/src/wirings/queue/queue-runner.ts +44 -47
  94. package/src/wirings/queue/queue.types.ts +10 -6
  95. package/src/wirings/rpc/index.ts +1 -1
  96. package/src/wirings/rpc/rpc-runner.ts +27 -9
  97. package/src/wirings/scheduler/scheduler-runner.ts +57 -56
  98. package/src/wirings/scheduler/scheduler.types.ts +9 -2
  99. package/tsconfig.tsbuildinfo +1 -1
@@ -8,23 +8,26 @@ import {
8
8
  HTTPWiringMeta,
9
9
  HTTPMethod,
10
10
  } from './http.types.js'
11
+ import {
12
+ CorePikkuFunction,
13
+ CorePikkuFunctionSessionless,
14
+ CorePikkuPermission,
15
+ } from '../../function/functions.types.js'
11
16
  import {
12
17
  CoreUserSession,
13
18
  CorePikkuMiddleware,
19
+ CorePikkuMiddlewareGroup,
14
20
  SessionServices,
15
21
  PikkuWiringTypes,
22
+ PikkuInteraction,
16
23
  } from '../../types/core.types.js'
17
- import { MissingSessionError, NotFoundError } from '../../errors/errors.js'
24
+ import { NotFoundError } from '../../errors/errors.js'
18
25
  import {
19
26
  closeSessionServices,
20
27
  createWeakUID,
21
28
  isSerializable,
22
29
  } from '../../utils.js'
23
- import {
24
- PikkuUserSessionService,
25
- UserSessionService,
26
- } from '../../services/user-session-service.js'
27
- import { combineMiddleware, runMiddleware } from '../../middleware-runner.js'
30
+ import { PikkuUserSessionService } from '../../services/user-session-service.js'
28
31
  import { handleHTTPError } from '../../handle-error.js'
29
32
  import { pikkuState } from '../../pikku-state.js'
30
33
  import { PikkuFetchHTTPResponse } from './pikku-fetch-http-response.js'
@@ -35,36 +38,41 @@ import { rpcService } from '../rpc/rpc-runner.js'
35
38
  import { httpRouter } from './routers/http-router.js'
36
39
 
37
40
  /**
38
- * Registers middleware either globally or for a specific route.
41
+ * Registers HTTP middleware for a specific route pattern.
42
+ *
43
+ * This function registers middleware at runtime that will be applied to
44
+ * HTTP routes matching the specified pattern.
39
45
  *
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).
46
+ * For tree-shaking benefits, wrap in a factory function:
47
+ * `export const x = () => addHTTPMiddleware('pattern', [...])`
43
48
  *
44
49
  * @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.
50
+ * @param {string} pattern - Route pattern (e.g., '*' for all routes, '/api/*' for specific routes).
51
+ * @param {CorePikkuMiddlewareGroup} middleware - Array of middleware for this route pattern.
52
+ *
53
+ * @returns {CorePikkuMiddlewareGroup} The middleware array (for chaining/wrapping).
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * // Recommended: tree-shakeable
58
+ * export const httpGlobal = () => addHTTPMiddleware('*', [
59
+ * corsMiddleware,
60
+ * loggingMiddleware
61
+ * ])
62
+ *
63
+ * // Also works: no tree-shaking
64
+ * export const apiMiddleware = addHTTPMiddleware('/api/*', [
65
+ * authMiddleware
66
+ * ])
67
+ * ```
47
68
  */
48
69
  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
- }
61
-
62
- const currentMiddleware = middlewareStore.get(route)
63
- if (currentMiddleware) {
64
- middlewareStore.set(route, [...currentMiddleware, ...middleware])
65
- } else {
66
- middlewareStore.set(route, middleware)
67
- }
70
+ pattern: string,
71
+ middleware: CorePikkuMiddlewareGroup
72
+ ): CorePikkuMiddlewareGroup => {
73
+ const httpGroups = pikkuState('middleware', 'httpGroup')
74
+ httpGroups[pattern] = middleware
75
+ return middleware
68
76
  }
69
77
 
70
78
  /**
@@ -86,10 +94,14 @@ export const wireHTTP = <
86
94
  In,
87
95
  Out,
88
96
  Route extends string,
89
- PikkuFunction,
90
- PikkuFunctionSessionless,
91
- PikkuPermissionGroup,
92
- PikkuMiddleware,
97
+ PikkuFunction extends CorePikkuFunction<In, Out> = CorePikkuFunction<In, Out>,
98
+ PikkuFunctionSessionless extends CorePikkuFunctionSessionless<
99
+ In,
100
+ Out
101
+ > = CorePikkuFunctionSessionless<In, Out>,
102
+ PikkuPermissionGroup extends
103
+ CorePikkuPermission<In> = CorePikkuPermission<In>,
104
+ PikkuMiddleware extends CorePikkuMiddleware = CorePikkuMiddleware,
93
105
  >(
94
106
  httpWiring: CoreHTTPFunctionWiring<
95
107
  In,
@@ -106,7 +118,7 @@ export const wireHTTP = <
106
118
  if (!routeMeta) {
107
119
  throw new Error('Route metadata not found')
108
120
  }
109
- addFunction(routeMeta.pikkuFuncName, httpWiring.func as any)
121
+ addFunction(routeMeta.pikkuFuncName, httpWiring.func)
110
122
  const routes = pikkuState('http', 'routes')
111
123
  if (!routes.has(httpWiring.method)) {
112
124
  routes.set(httpWiring.method, new Map())
@@ -146,8 +158,6 @@ const getMatchingRoute = (requestType: string, requestPath: string) => {
146
158
  params: matchedPath.params,
147
159
  route,
148
160
  permissions: route.permissions,
149
- httpMiddleware: matchedPath.middleware,
150
- middleware: route.middleware,
151
161
  meta: meta!,
152
162
  }
153
163
  }
@@ -183,16 +193,7 @@ export const createHTTPInteraction = (
183
193
  }
184
194
 
185
195
  /**
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.
196
+ * Validates the input data and executes the route handler
196
197
  *
197
198
  * This function performs these steps:
198
199
  * 1. Sets URL parameters on the request.
@@ -205,16 +206,15 @@ export const createHTTPInteraction = (
205
206
  * 8. Sends the appropriate response.
206
207
  *
207
208
  * @param {Object} services - A collection of shared services and utilities.
208
- * @param {Object} matchedRoute - Contains route details, URL parameters, middleware, and optional schema.
209
+ * @param {Object} matchedRoute - Contains route details, URL parameters, and optional schema.
209
210
  * @param {PikkuHTTP | undefined} http - The HTTP interaction object.
210
211
  * @param {Object} options - Options for route execution (e.g., whether to coerce query strings to arrays).
211
212
  * @returns {Promise<any>} An object containing the route handler result and session services (if any).
212
213
  * @throws Throws errors like MissingSessionError or ForbiddenError on validation failures.
213
214
  */
214
- const executeRouteWithMiddleware = async (
215
+ const executeRoute = async (
215
216
  services: {
216
217
  singletonServices: any
217
- userSession: UserSessionService<CoreUserSession>
218
218
  createSessionServices: Function
219
219
  skipUserSession: boolean
220
220
  requestId: string
@@ -223,8 +223,6 @@ const executeRouteWithMiddleware = async (
223
223
  matchedPath: any
224
224
  params: any
225
225
  route: CoreHTTPFunctionWiring<any, any, any>
226
- httpMiddleware: CorePikkuMiddleware[] | undefined
227
- middleware: CorePikkuMiddleware[] | undefined
228
226
  meta: HTTPWiringMeta
229
227
  },
230
228
  http: PikkuHTTP,
@@ -232,11 +230,10 @@ const executeRouteWithMiddleware = async (
232
230
  coerceDataFromSchema: boolean
233
231
  }
234
232
  ) => {
235
- const { matchedPath, params, route, httpMiddleware, middleware, meta } =
236
- matchedRoute
233
+ const userSession = new PikkuUserSessionService<CoreUserSession>()
234
+ const { params, route, meta } = matchedRoute
237
235
  const {
238
236
  singletonServices,
239
- userSession,
240
237
  createSessionServices,
241
238
  skipUserSession,
242
239
  requestId,
@@ -253,116 +250,95 @@ const executeRouteWithMiddleware = async (
253
250
  `Matched route: ${route.route} | method: ${route.method.toUpperCase()} | auth: ${requiresSession.toString()}`
254
251
  )
255
252
 
256
- // Main route execution logic wrapped for middleware handling
257
- const runMain = async () => {
258
- const session = userSession.get()
253
+ // Ensure session is available when required
254
+ if (skipUserSession && requiresSession) {
255
+ throw new Error("Can't skip trying to get user session if auth is required")
256
+ }
259
257
 
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
- }
258
+ const data = () => http.request!.data()
259
+ let channel: PikkuChannel<unknown, unknown> | undefined
266
260
 
267
- if (requiresSession && !session) {
268
- singletonServices.logger.info({
269
- action: 'Rejecting route (invalid session)',
270
- path: matchedPath,
271
- })
272
- throw new MissingSessionError()
261
+ if (matchedRoute.route.sse) {
262
+ const response = http?.response
263
+ if (!response) {
264
+ throw new Error('SSE requires a valid HTTP response object')
273
265
  }
266
+ if (!response.setMode) {
267
+ throw new Error('Response object does not support SSE mode')
268
+ }
269
+ response.setMode('stream')
270
+ response.header('Content-Type', 'text/event-stream')
271
+ response.header('Cache-Control', 'no-cache')
272
+ response.header('Connection', 'keep-alive')
273
+ response.header('Transfer-Encoding', 'chunked')
274
+ channel = {
275
+ channelId: requestId,
276
+ openingData: await data(),
277
+ send: (data: any) => {
278
+ response.arrayBuffer(isSerializable(data) ? JSON.stringify(data) : data)
279
+ },
280
+ close: () => {
281
+ channel!.state = 'closed'
282
+ response.close?.()
283
+ },
284
+ state: 'open',
285
+ }
286
+ }
274
287
 
275
- const data = await http.request!.data()
288
+ const interaction: PikkuInteraction = { http, channel }
276
289
 
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
- }
290
+ const getAllServices = async (session?: CoreUserSession) => {
291
+ let channel: PikkuChannel<unknown, unknown> | undefined
307
292
 
308
- // Create session-specific services for handling the request
309
- sessionServices = await createSessionServices(
310
- { ...singletonServices, userSession, channel },
311
- { http },
312
- session
313
- )
293
+ // Create session-specific services for handling the request
294
+ sessionServices = await createSessionServices(
295
+ { ...singletonServices, userSession, channel },
296
+ { http },
297
+ session
298
+ )
314
299
 
315
- return rpcService.injectRPCService({
300
+ return rpcService.injectRPCService(
301
+ {
316
302
  ...singletonServices,
317
303
  ...sessionServices,
318
304
  http,
319
305
  userSession,
320
306
  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
- }
307
+ },
308
+ interaction,
309
+ route.auth
336
310
  )
311
+ }
337
312
 
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)
313
+ result = await runPikkuFunc(
314
+ PikkuWiringTypes.http,
315
+ `${meta.method}:${meta.route}`,
316
+ meta.pikkuFuncName,
317
+ {
318
+ singletonServices,
319
+ getAllServices,
320
+ auth: route.auth !== false,
321
+ userSession,
322
+ data,
323
+ permissions: route.permissions,
324
+ inheritedMiddleware: meta.middleware,
325
+ wireMiddleware: route.middleware,
326
+ coerceDataFromSchema: options.coerceDataFromSchema,
327
+ tags: route.tags,
328
+ interaction,
343
329
  }
330
+ )
344
331
 
345
- http?.response?.status(200)
346
- // TODO: Evaluate if the response stream should be explicitly ended.
347
- // http?.response?.end()
332
+ // Respond with either a binary or JSON response based on configuration
333
+ if (route.returnsJSON === false) {
334
+ http?.response?.arrayBuffer(result)
335
+ } else {
336
+ http?.response?.json(result)
348
337
  }
349
338
 
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
- )
339
+ http?.response?.status(200)
340
+ // TODO: Evaluate if the response stream should be explicitly ended.
341
+ // http?.response?.end()
366
342
 
367
343
  return sessionServices ? { result, sessionServices } : { result }
368
344
  }
@@ -442,14 +418,12 @@ export const fetchData = async <In, Out>(
442
418
  coerceDataFromSchema = true,
443
419
  bubbleErrors = false,
444
420
  generateRequestId,
445
- ignoreMiddleware = false,
446
421
  }: RunHTTPWiringOptions & RunHTTPWiringParams
447
422
  ): Promise<Out | void> => {
448
423
  const requestId =
449
424
  (request as any).getHeader?.('x-request-id') ||
450
425
  generateRequestId?.() ||
451
426
  createWeakUID()
452
- const userSession = new PikkuUserSessionService()
453
427
  let sessionServices: SessionServices<typeof singletonServices> | undefined
454
428
  let result: Out
455
429
 
@@ -475,21 +449,14 @@ export const fetchData = async <In, Out>(
475
449
  }
476
450
 
477
451
  // Execute the matched route along with its middleware and session management
478
- ;({ result, sessionServices } = await executeRouteWithMiddleware(
452
+ ;({ result, sessionServices } = await executeRoute(
479
453
  {
480
454
  singletonServices,
481
- userSession,
482
455
  createSessionServices,
483
456
  skipUserSession,
484
457
  requestId,
485
458
  },
486
- ignoreMiddleware
487
- ? {
488
- ...matchedRoute,
489
- middleware: undefined,
490
- httpMiddleware: undefined,
491
- }
492
- : matchedRoute,
459
+ matchedRoute,
493
460
  http!,
494
461
  { coerceDataFromSchema }
495
462
  ))
@@ -7,12 +7,14 @@ import type {
7
7
  CoreUserSession,
8
8
  CreateSessionServices,
9
9
  CorePikkuMiddleware,
10
+ MiddlewareMetadata,
10
11
  } from '../../types/core.types.js'
11
12
  import type {
12
13
  CorePikkuFunction,
13
14
  CorePikkuFunctionSessionless,
14
15
  CorePikkuPermission,
15
16
  CorePermissionGroup,
17
+ CorePikkuFunctionConfig,
16
18
  } from '../../function/functions.types.js'
17
19
 
18
20
  type ExtractHTTPWiringParams<S extends string> =
@@ -39,7 +41,6 @@ export type RunHTTPWiringOptions = Partial<{
39
41
  coerceDataFromSchema: boolean
40
42
  bubbleErrors: boolean
41
43
  generateRequestId: () => string
42
- ignoreMiddleware: boolean
43
44
  }>
44
45
 
45
46
  export type RunHTTPWiringParams = {
@@ -117,15 +118,38 @@ export type CoreHTTPFunctionWiring<
117
118
  In,
118
119
  Out,
119
120
  R extends string,
120
- PikkuFunction = CorePikkuFunction<In, Out>,
121
- PikkuFunctionSessionless = CorePikkuFunctionSessionless<In, Out>,
122
- PikkuPermission = CorePikkuPermission<In>,
123
- PikkuMiddleware = CorePikkuMiddleware,
121
+ PikkuFunction extends CorePikkuFunction<
122
+ In,
123
+ Out,
124
+ any,
125
+ any,
126
+ any
127
+ > = CorePikkuFunction<In, Out>,
128
+ PikkuFunctionSessionless extends CorePikkuFunctionSessionless<
129
+ In,
130
+ Out,
131
+ any,
132
+ any,
133
+ any
134
+ > = CorePikkuFunctionSessionless<In, Out>,
135
+ PikkuPermission extends CorePikkuPermission<
136
+ In,
137
+ any,
138
+ any
139
+ > = CorePikkuPermission<In, any, any>,
140
+ PikkuMiddleware extends CorePikkuMiddleware<
141
+ any,
142
+ any
143
+ > = CorePikkuMiddleware<any>,
124
144
  > =
125
145
  | (CoreHTTPFunction & {
126
146
  route: R
127
147
  method: HTTPMethod
128
- func: PikkuFunction
148
+ func: CorePikkuFunctionConfig<
149
+ PikkuFunction,
150
+ PikkuPermission,
151
+ PikkuMiddleware
152
+ >
129
153
  permissions?: CorePermissionGroup<PikkuPermission>
130
154
  auth?: true
131
155
  tags?: string[]
@@ -135,7 +159,11 @@ export type CoreHTTPFunctionWiring<
135
159
  | (CoreHTTPFunction & {
136
160
  route: R
137
161
  method: HTTPMethod
138
- func: PikkuFunctionSessionless
162
+ func: CorePikkuFunctionConfig<
163
+ PikkuFunctionSessionless,
164
+ PikkuPermission,
165
+ PikkuMiddleware
166
+ >
139
167
  permissions?: undefined
140
168
  auth?: false
141
169
  tags?: string[]
@@ -145,7 +173,11 @@ export type CoreHTTPFunctionWiring<
145
173
  | (CoreHTTPFunction & {
146
174
  route: R
147
175
  method: 'get'
148
- func: PikkuFunction
176
+ func: CorePikkuFunctionConfig<
177
+ PikkuFunction,
178
+ PikkuPermission,
179
+ PikkuMiddleware
180
+ >
149
181
  permissions?: CorePermissionGroup<PikkuPermission>
150
182
  auth?: true
151
183
  sse?: boolean
@@ -155,7 +187,11 @@ export type CoreHTTPFunctionWiring<
155
187
  | (CoreHTTPFunction & {
156
188
  route: R
157
189
  method: 'get'
158
- func: PikkuFunctionSessionless
190
+ func: CorePikkuFunctionConfig<
191
+ PikkuFunctionSessionless,
192
+ PikkuPermission,
193
+ PikkuMiddleware
194
+ >
159
195
  permissions?: undefined
160
196
  auth?: false
161
197
  sse?: boolean
@@ -165,7 +201,11 @@ export type CoreHTTPFunctionWiring<
165
201
  | (CoreHTTPFunction & {
166
202
  route: R
167
203
  method: 'post'
168
- func: PikkuFunction
204
+ func: CorePikkuFunctionConfig<
205
+ PikkuFunction,
206
+ PikkuPermission,
207
+ PikkuMiddleware
208
+ >
169
209
  permissions?: CorePermissionGroup<PikkuPermission>
170
210
  auth?: true
171
211
  query?: Array<keyof In>
@@ -176,7 +216,11 @@ export type CoreHTTPFunctionWiring<
176
216
  | (CoreHTTPFunction & {
177
217
  route: R
178
218
  method: 'post'
179
- func: PikkuFunctionSessionless
219
+ func: CorePikkuFunctionConfig<
220
+ PikkuFunctionSessionless,
221
+ PikkuPermission,
222
+ PikkuMiddleware
223
+ >
180
224
  permissions?: undefined
181
225
  auth?: false
182
226
  query?: Array<keyof In>
@@ -207,6 +251,7 @@ export type HTTPWiringMeta = {
207
251
  docs?: PikkuDocs
208
252
  tags?: string[]
209
253
  sse?: true
254
+ middleware?: MiddlewareMetadata[] // Pre-resolved middleware chain (global + route + tag + explicit + efunc)
210
255
  }
211
256
  export type HTTPWiringsMeta = Record<HTTPMethod, Record<string, HTTPWiringMeta>>
212
257
 
@@ -2,6 +2,6 @@ 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 { fetch, fetchData, wireHTTP, addHTTPMiddleware } from './http-runner.js'
6
6
 
7
7
  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