@pikku/core 0.9.2 → 0.9.4

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 (53) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/function/function-runner.d.ts +4 -3
  3. package/dist/function/function-runner.js +20 -7
  4. package/dist/function/functions.types.d.ts +3 -2
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.js +2 -1
  7. package/dist/middleware-runner.d.ts +62 -0
  8. package/dist/middleware-runner.js +91 -2
  9. package/dist/permissions.d.ts +40 -0
  10. package/dist/permissions.js +62 -0
  11. package/dist/pikku-state.d.ts +3 -2
  12. package/dist/pikku-state.js +2 -0
  13. package/dist/types/core.types.d.ts +23 -11
  14. package/dist/wirings/channel/channel-handler.js +1 -0
  15. package/dist/wirings/channel/local/local-channel-runner.js +23 -15
  16. package/dist/wirings/channel/serverless/serverless-channel-runner.js +2 -2
  17. package/dist/wirings/http/http-runner.js +14 -6
  18. package/dist/wirings/http/http.types.d.ts +1 -0
  19. package/dist/wirings/mcp/mcp-runner.js +25 -23
  20. package/dist/wirings/mcp/mcp.types.d.ts +7 -3
  21. package/dist/wirings/queue/index.d.ts +1 -1
  22. package/dist/wirings/queue/index.js +1 -1
  23. package/dist/wirings/queue/queue-runner.d.ts +15 -1
  24. package/dist/wirings/queue/queue-runner.js +65 -19
  25. package/dist/wirings/queue/queue.types.d.ts +19 -2
  26. package/dist/wirings/rpc/rpc-runner.d.ts +5 -2
  27. package/dist/wirings/rpc/rpc-runner.js +15 -3
  28. package/dist/wirings/rpc/rpc-types.d.ts +1 -1
  29. package/dist/wirings/scheduler/scheduler-runner.js +50 -27
  30. package/dist/wirings/scheduler/scheduler.types.d.ts +17 -2
  31. package/package.json +1 -1
  32. package/src/function/function-runner.ts +34 -7
  33. package/src/function/functions.types.ts +3 -1
  34. package/src/index.ts +6 -1
  35. package/src/middleware-runner.ts +110 -2
  36. package/src/permissions.ts +71 -0
  37. package/src/pikku-state.ts +5 -2
  38. package/src/types/core.types.ts +30 -16
  39. package/src/wirings/channel/channel-handler.ts +1 -0
  40. package/src/wirings/channel/local/local-channel-runner.ts +33 -16
  41. package/src/wirings/channel/serverless/serverless-channel-runner.ts +5 -2
  42. package/src/wirings/http/http-runner.ts +14 -6
  43. package/src/wirings/http/http.types.ts +1 -0
  44. package/src/wirings/mcp/mcp-runner.ts +35 -26
  45. package/src/wirings/mcp/mcp.types.ts +7 -0
  46. package/src/wirings/queue/index.ts +2 -0
  47. package/src/wirings/queue/queue-runner.ts +83 -22
  48. package/src/wirings/queue/queue.types.ts +20 -1
  49. package/src/wirings/rpc/rpc-runner.ts +16 -4
  50. package/src/wirings/rpc/rpc-types.ts +1 -1
  51. package/src/wirings/scheduler/scheduler-runner.ts +74 -35
  52. package/src/wirings/scheduler/scheduler.types.ts +22 -1
  53. package/tsconfig.tsbuildinfo +1 -1
@@ -1,6 +1,6 @@
1
1
  import { ForbiddenError } from '../errors/errors.js'
2
2
  import { runMiddleware } from '../middleware-runner.js'
3
- import { verifyPermissions } from '../permissions.js'
3
+ import { verifyPermissions, getPermissionsForTags } from '../permissions.js'
4
4
  import { pikkuState } from '../pikku-state.js'
5
5
  import { coerceTopLevelDataFromSchema, validateSchema } from '../schema.js'
6
6
  import {
@@ -10,13 +10,21 @@ import {
10
10
  } from '../types/core.types.js'
11
11
  import {
12
12
  CorePermissionGroup,
13
+ CorePikkuFunction,
13
14
  CorePikkuFunctionConfig,
15
+ CorePikkuFunctionSessionless,
14
16
  } from './functions.types.js'
15
17
 
16
18
  export const addFunction = (
17
19
  funcName: string,
18
- funcConfig: CorePikkuFunctionConfig<any, any>
20
+ funcConfig:
21
+ | CorePikkuFunctionConfig<any, any>
22
+ | CorePikkuFunctionSessionless<any, any>
23
+ | CorePikkuFunction<any, any>
19
24
  ) => {
25
+ if (funcConfig instanceof Function) {
26
+ funcConfig = { func: funcConfig }
27
+ }
20
28
  pikkuState('function', 'functions').set(funcName, funcConfig)
21
29
  }
22
30
 
@@ -42,6 +50,7 @@ export const runPikkuFunc = async <In = any, Out = any>(
42
50
  permissions: transportPermissions,
43
51
  middleware: transportMiddleware,
44
52
  coerceDataFromSchema,
53
+ tags = [],
45
54
  }: {
46
55
  getAllServices: () => Promise<CoreServices> | CoreServices
47
56
  data: In
@@ -49,6 +58,7 @@ export const runPikkuFunc = async <In = any, Out = any>(
49
58
  permissions?: CorePermissionGroup
50
59
  middleware?: CorePikkuMiddleware[]
51
60
  coerceDataFromSchema?: boolean
61
+ tags?: string[]
52
62
  }
53
63
  ): Promise<Out> => {
54
64
  const funcConfig = pikkuState('function', 'functions').get(funcName)
@@ -68,23 +78,39 @@ export const runPikkuFunc = async <In = any, Out = any>(
68
78
 
69
79
  const allServices = await getAllServices()
70
80
 
71
- const schemaName = funcMeta.schemaName
72
- if (schemaName) {
81
+ const inputSchemaName = funcMeta.inputSchemaName
82
+ if (inputSchemaName) {
73
83
  // Validate request data against the defined schema, if any
74
84
  await validateSchema(
75
85
  allServices.logger,
76
86
  allServices.schema,
77
- schemaName,
87
+ inputSchemaName,
78
88
  data
79
89
  )
80
90
  // Coerce (top level) query string parameters or date objects if specified by the schema
81
91
  if (coerceDataFromSchema) {
82
- coerceTopLevelDataFromSchema(schemaName, data)
92
+ coerceTopLevelDataFromSchema(inputSchemaName, data)
83
93
  }
84
94
  }
85
95
 
86
96
  let permissioned = true
87
- if (funcConfig.permissions) {
97
+
98
+ // Check tagged permissions first - ALL must pass
99
+ const taggedPermissions = getPermissionsForTags([
100
+ ...tags,
101
+ ...(funcConfig.tags || []),
102
+ ])
103
+ if (taggedPermissions.length > 0) {
104
+ const taggedResults = await Promise.all(
105
+ taggedPermissions.map((permission) =>
106
+ permission(allServices, data, session)
107
+ )
108
+ )
109
+ permissioned = taggedResults.every((result) => result)
110
+ }
111
+
112
+ // Only check function permissions if tagged permissions passed
113
+ if (permissioned && funcConfig.permissions) {
88
114
  permissioned = await verifyPermissions(
89
115
  funcConfig.permissions,
90
116
  allServices,
@@ -92,6 +118,7 @@ export const runPikkuFunc = async <In = any, Out = any>(
92
118
  session
93
119
  )
94
120
  }
121
+
95
122
  if (!permissioned && transportPermissions) {
96
123
  permissioned = await verifyPermissions(
97
124
  transportPermissions,
@@ -87,12 +87,14 @@ export type CorePikkuFunctionConfig<
87
87
  any,
88
88
  any
89
89
  > = CorePikkuPermission<any>,
90
+ PikkuMiddleware extends CorePikkuMiddleware<any> = CorePikkuMiddleware<any>,
90
91
  > = {
91
92
  name?: string
92
93
  expose?: boolean
93
94
  func: PikkuFunction
94
95
  auth?: boolean
95
96
  permissions?: CorePermissionGroup<PikkuPermission>
96
- middleware?: CorePikkuMiddleware[]
97
+ middleware?: PikkuMiddleware[]
98
+ tags?: string[]
97
99
  docs?: PikkuDocs
98
100
  }
package/src/index.ts CHANGED
@@ -21,7 +21,12 @@ export * from './middleware/index.js'
21
21
  export * from './time-utils.js'
22
22
  export * from './utils.js'
23
23
  export { pikkuState } from './pikku-state.js'
24
- export { runMiddleware } from './middleware-runner.js'
24
+ export {
25
+ runMiddleware,
26
+ addMiddleware,
27
+ addMiddlewareForTags,
28
+ } from './middleware-runner.js'
29
+ export { addPermission, getPermissionsForTags } from './permissions.js'
25
30
  export { wireHTTP, addHTTPMiddleware } from './wirings/http/http-runner.js'
26
31
  export { wireChannel } from './wirings/channel/channel-runner.js'
27
32
  export { wireScheduler } from './wirings/scheduler/scheduler-runner.js'
@@ -4,6 +4,7 @@ import {
4
4
  PikkuInteraction,
5
5
  CorePikkuMiddleware,
6
6
  } from './types/core.types.js'
7
+ import { pikkuState } from './pikku-state.js'
7
8
 
8
9
  /**
9
10
  * Runs a chain of middleware functions in sequence before executing the main function.
@@ -30,10 +31,13 @@ export const runMiddleware = async <Middleware extends CorePikkuMiddleware>(
30
31
  middlewares: Middleware[],
31
32
  main?: () => Promise<unknown>
32
33
  ): Promise<unknown> => {
34
+ // Deduplicate middleware using Set to avoid running the same middleware multiple times
35
+ const uniqueMiddleware = Array.from(new Set(middlewares))
36
+
33
37
  let result: any
34
38
  const dispatch = async (index: number): Promise<any> => {
35
- if (middlewares && index < middlewares.length) {
36
- return await middlewares[index]!(services as any, interaction, () =>
39
+ if (uniqueMiddleware && index < uniqueMiddleware.length) {
40
+ return await uniqueMiddleware[index]!(services as any, interaction, () =>
37
41
  dispatch(index + 1)
38
42
  )
39
43
  } else if (main) {
@@ -43,3 +47,107 @@ export const runMiddleware = async <Middleware extends CorePikkuMiddleware>(
43
47
  await dispatch(0)
44
48
  return result
45
49
  }
50
+
51
+ /**
52
+ * Adds global middleware for a specific tag.
53
+ *
54
+ * This function allows you to register middleware that will be applied to
55
+ * any wiring (HTTP, Channel, Queue, Scheduler, MCP) that includes the matching tag.
56
+ *
57
+ * @template PikkuMiddleware The middleware type.
58
+ * @param {string} tag - The tag that the middleware should apply to.
59
+ * @param {PikkuMiddleware[]} middleware - The middleware array to apply for the specified tag.
60
+ *
61
+ * @throws {Error} If middleware for the tag already exists.
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * // Add admin middleware for admin endpoints
66
+ * addMiddleware('admin', [adminMiddleware])
67
+ *
68
+ * // Add authentication middleware for auth endpoints
69
+ * addMiddleware('auth', [authMiddleware])
70
+ *
71
+ * // Add logging middleware for all API endpoints
72
+ * addMiddleware('api', [loggingMiddleware])
73
+ * ```
74
+ */
75
+ export const addMiddleware = <PikkuMiddleware extends CorePikkuMiddleware>(
76
+ tag: string,
77
+ middleware: PikkuMiddleware[]
78
+ ) => {
79
+ const middlewareStore = pikkuState('misc', 'middleware')
80
+
81
+ // Check if tag already exists
82
+ if (middlewareStore[tag]) {
83
+ throw new Error(
84
+ `Middleware for tag '${tag}' already exists. Use a different tag or remove the existing middleware first.`
85
+ )
86
+ }
87
+
88
+ middlewareStore[tag] = middleware as CorePikkuMiddleware[]
89
+ }
90
+
91
+ /**
92
+ * Retrieves middleware for a given set of tags.
93
+ *
94
+ * This function looks up all middleware registered for any of the provided tags
95
+ * and returns them as a flattened array.
96
+ *
97
+ * @param {string[]} tags - Array of tags to look up middleware for.
98
+ * @returns {CorePikkuMiddleware[]} Array of middleware functions that apply to the given tags.
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * // Get all middleware for tags 'api' and 'auth'
103
+ * const middleware = getMiddlewareForTags(['api', 'auth'])
104
+ * ```
105
+ */
106
+ export const getMiddlewareForTags = (
107
+ tags?: string[]
108
+ ): CorePikkuMiddleware[] => {
109
+ if (!tags || tags.length === 0) {
110
+ return []
111
+ }
112
+
113
+ const middlewareStore = pikkuState('misc', 'middleware')
114
+ const applicableMiddleware: CorePikkuMiddleware[] = []
115
+
116
+ // Collect middleware for all matching tags
117
+ for (const tag of tags) {
118
+ const tagMiddleware = middlewareStore[tag]
119
+ if (tagMiddleware) {
120
+ applicableMiddleware.push(...tagMiddleware)
121
+ }
122
+ }
123
+
124
+ return applicableMiddleware
125
+ }
126
+
127
+ /**
128
+ * Combines tag-based middleware with wiring-specific middleware.
129
+ *
130
+ * This helper function gets middleware for tags and combines it with any
131
+ * wiring-specific middleware, avoiding the need for manual spreading.
132
+ *
133
+ * @param {CorePikkuMiddleware[] | undefined} wiringMiddleware - Wiring-specific middleware.
134
+ * @param {string[] | undefined} tags - Array of tags to look up middleware for.
135
+ * @returns {CorePikkuMiddleware[]} Combined array of tag-based and wiring-specific middleware.
136
+ *
137
+ * @example
138
+ * ```typescript
139
+ * // Instead of:
140
+ * const taggedMiddleware = getMiddlewareForTags(tags)
141
+ * const combined = [...taggedMiddleware, ...(middleware || [])]
142
+ *
143
+ * // Use:
144
+ * const combined = addMiddlewareForTags(middleware, tags)
145
+ * ```
146
+ */
147
+ export const addMiddlewareForTags = (
148
+ wiringMiddleware?: CorePikkuMiddleware[],
149
+ tags?: string[]
150
+ ): CorePikkuMiddleware[] => {
151
+ const taggedMiddleware = getMiddlewareForTags(tags)
152
+ return [...taggedMiddleware, ...(wiringMiddleware || [])]
153
+ }
@@ -1,5 +1,6 @@
1
1
  import { CoreServices, CoreUserSession } from './types/core.types.js'
2
2
  import { CorePermissionGroup } from './function/functions.types.js'
3
+ import { pikkuState } from './pikku-state.js'
3
4
 
4
5
  /**
5
6
  * This function validates permissions by iterating over permission groups and executing the corresponding permission functions. If all functions in at least one group return true, the permission is considered valid.
@@ -41,3 +42,73 @@ export const verifyPermissions = async (
41
42
  }
42
43
  return false
43
44
  }
45
+
46
+ /**
47
+ * Adds global permissions for a specific tag.
48
+ *
49
+ * This function allows you to register permissions that will be applied to
50
+ * any wiring (HTTP, Channel, Queue, Scheduler, MCP) that includes the matching tag.
51
+ *
52
+ * @param {string} tag - The tag that the permissions should apply to.
53
+ * @param {any[]} permissions - The permissions array to apply for the specified tag.
54
+ *
55
+ * @throws {Error} If permissions for the tag already exist.
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * // Add admin permissions for admin endpoints
60
+ * addPermission('admin', [adminPermission])
61
+ *
62
+ * // Add authentication permissions for auth endpoints
63
+ * addPermission('auth', [authPermission])
64
+ *
65
+ * // Add read permissions for all API endpoints
66
+ * addPermission('api', [readPermission])
67
+ * ```
68
+ */
69
+ export const addPermission = (tag: string, permissions: any[]) => {
70
+ const permissionsStore = pikkuState('misc', 'permissions')
71
+
72
+ // Check if tag already exists
73
+ if (permissionsStore[tag]) {
74
+ throw new Error(
75
+ `Permissions for tag '${tag}' already exist. Use a different tag or remove the existing permissions first.`
76
+ )
77
+ }
78
+
79
+ permissionsStore[tag] = permissions
80
+ }
81
+
82
+ /**
83
+ * Retrieves permissions for a given set of tags.
84
+ *
85
+ * This function looks up all permissions registered for any of the provided tags
86
+ * and returns them as a flattened array.
87
+ *
88
+ * @param {string[]} tags - Array of tags to look up permissions for.
89
+ * @returns {any[]} Array of permission functions that apply to the given tags.
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * // Get all permissions for tags 'api' and 'auth'
94
+ * const permissions = getPermissionsForTags(['api', 'auth'])
95
+ * ```
96
+ */
97
+ export const getPermissionsForTags = (tags?: string[]): any[] => {
98
+ if (!tags || tags.length === 0) {
99
+ return []
100
+ }
101
+
102
+ const permissionsStore = pikkuState('misc', 'permissions')
103
+ const applicablePermissions: any[] = []
104
+
105
+ // Collect permissions for all matching tags
106
+ for (const tag of new Set(tags)) {
107
+ const tagPermissions = permissionsStore[tag]
108
+ if (tagPermissions) {
109
+ applicablePermissions.push(...tagPermissions)
110
+ }
111
+ }
112
+
113
+ return applicablePermissions
114
+ }
@@ -11,7 +11,6 @@ import {
11
11
  } from './wirings/scheduler/scheduler.types.js'
12
12
  import { ErrorDetails, PikkuError } from './errors/error-handler.js'
13
13
  import { CorePikkuFunctionConfig } from './function/functions.types.js'
14
- import { RPCMeta } from './wirings/rpc/rpc-types.js'
15
14
  import {
16
15
  queueWorkersMeta,
17
16
  CoreQueueWorker,
@@ -31,7 +30,7 @@ interface PikkuState {
31
30
  functions: Map<string, CorePikkuFunctionConfig<any, any>>
32
31
  }
33
32
  rpc: {
34
- meta: Record<string, RPCMeta>
33
+ meta: Record<string, string>
35
34
  files: Map<
36
35
  string,
37
36
  {
@@ -68,6 +67,8 @@ interface PikkuState {
68
67
  misc: {
69
68
  errors: Map<PikkuError, ErrorDetails>
70
69
  schemas: Map<string, any>
70
+ middleware: Record<string, CorePikkuMiddleware[]>
71
+ permissions: Record<string, any[]>
71
72
  }
72
73
  }
73
74
 
@@ -109,6 +110,8 @@ export const resetPikkuState = () => {
109
110
  misc: {
110
111
  errors: globalThis.pikkuState?.misc?.errors || new Map(),
111
112
  schemas: globalThis.pikkuState?.misc?.schema || new Map(),
113
+ middleware: globalThis.pikkuState?.misc?.middleware || {},
114
+ permissions: globalThis.pikkuState?.misc?.permissions || {},
112
115
  },
113
116
  } as PikkuState
114
117
  }
@@ -6,6 +6,8 @@ import { UserSessionService } from '../services/user-session-service.js'
6
6
  import { PikkuChannel } from '../wirings/channel/channel.types.js'
7
7
  import { PikkuRPC } from '../wirings/rpc/rpc-types.js'
8
8
  import { PikkuMCP } from '../wirings/mcp/mcp.types.js'
9
+ import { PikkuScheduledTask } from '../wirings/scheduler/scheduler.types.js'
10
+ import { PikkuQueue } from '../wirings/queue/queue.types.js'
9
11
 
10
12
  export enum PikkuWiringTypes {
11
13
  http = 'http',
@@ -21,20 +23,28 @@ export interface FunctionServicesMeta {
21
23
  services: string[]
22
24
  }
23
25
 
24
- export type FunctionsMeta = Record<
25
- string,
26
- {
27
- pikkuFuncName: string
28
- name?: string
26
+ export type FunctionRuntimeMeta = {
27
+ pikkuFuncName: string
28
+ inputSchemaName: string | null
29
+ outputSchemaName: string | null
30
+ expose?: boolean
31
+ }
32
+
33
+ export type FunctionMeta = FunctionRuntimeMeta &
34
+ Partial<{
35
+ name: string
29
36
  services: FunctionServicesMeta
30
- schemaName: string | null
31
37
  inputs: string[] | null
32
38
  outputs: string[] | null
33
- expose?: boolean
34
- tags?: string[]
35
- docs?: PikkuDocs
36
- }
37
- >
39
+ tags: string[]
40
+ docs: PikkuDocs
41
+ isDirectFunction: boolean // true if it's pikkuFunc(fn), false if it's pikkuFunc({ func: fn })
42
+ }>
43
+
44
+ export type FunctionsRuntimeMeta = Record<string, FunctionRuntimeMeta>
45
+ export type FunctionsMeta = Record<string, FunctionMeta>
46
+
47
+ // Optimized runtime metadata with only essential fields
38
48
 
39
49
  export type MakeRequired<T, K extends keyof T> = Omit<T, K> &
40
50
  Required<Pick<T, K>>
@@ -104,11 +114,15 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
104
114
  /**
105
115
  * Represents different forms of interaction within Pikku and the outside world.
106
116
  */
107
- export interface PikkuInteraction {
108
- http?: PikkuHTTP
109
- mcp?: PikkuMCP
110
- rpc?: PikkuRPC
111
- }
117
+ export type PikkuInteraction = Partial<{
118
+ http: PikkuHTTP
119
+ mcp: PikkuMCP
120
+ rpc: PikkuRPC
121
+ channel: PikkuChannel<unknown, unknown>
122
+ scheduledTask: PikkuScheduledTask
123
+ queue: PikkuQueue
124
+ s
125
+ }>
112
126
 
113
127
  /**
114
128
  * A function that can wrap an interaction and be called before or after
@@ -107,6 +107,7 @@ export const processMessageHandlers = (
107
107
  session,
108
108
  permissions,
109
109
  middleware,
110
+ tags: channelConfig.tags,
110
111
  })
111
112
  }
112
113
 
@@ -10,7 +10,10 @@ import {
10
10
  import { PikkuLocalChannelHandler } from './local-channel-handler.js'
11
11
  import { SessionServices } from '../../../types/core.types.js'
12
12
  import { handleHTTPError } from '../../../handle-error.js'
13
- import { runMiddleware } from '../../../middleware-runner.js'
13
+ import {
14
+ addMiddlewareForTags,
15
+ runMiddleware,
16
+ } from '../../../middleware-runner.js'
14
17
  import { PikkuUserSessionService } from '../../../services/user-session-service.js'
15
18
  import { PikkuHTTP } from '../../http/http.types.js'
16
19
  import { runPikkuFuncDirectly } from '../../../function/function-runner.js'
@@ -42,21 +45,35 @@ export const runLocalChannel = async ({
42
45
  route = http?.request?.path()
43
46
  }
44
47
 
48
+ let openingData, channelConfig, meta
49
+ try {
50
+ ;({ openingData, channelConfig, meta } = await openChannel({
51
+ channelId,
52
+ createSessionServices,
53
+ respondWith404,
54
+ request,
55
+ response,
56
+ route,
57
+ singletonServices,
58
+ skipUserSession,
59
+ coerceDataFromSchema,
60
+ userSession,
61
+ }))
62
+ } catch (e) {
63
+ handleHTTPError(
64
+ e,
65
+ http,
66
+ channelId,
67
+ singletonServices.logger,
68
+ logWarningsForStatusCodes,
69
+ respondWith404,
70
+ bubbleErrors
71
+ )
72
+ return
73
+ }
74
+
45
75
  const main = async () => {
46
76
  try {
47
- const { openingData, channelConfig, meta } = await openChannel({
48
- channelId,
49
- createSessionServices,
50
- respondWith404,
51
- request,
52
- response,
53
- route,
54
- singletonServices,
55
- skipUserSession,
56
- coerceDataFromSchema,
57
- userSession,
58
- })
59
-
60
77
  channelHandler = new PikkuLocalChannelHandler(
61
78
  channelId,
62
79
  channelConfig.name,
@@ -67,7 +84,7 @@ export const runLocalChannel = async ({
67
84
  if (createSessionServices) {
68
85
  sessionServices = await createSessionServices(
69
86
  singletonServices,
70
- { http },
87
+ { http, channel },
71
88
  session
72
89
  )
73
90
  }
@@ -132,7 +149,7 @@ export const runLocalChannel = async ({
132
149
  userSession,
133
150
  },
134
151
  { http },
135
- route.middleware || [],
152
+ addMiddlewareForTags(channelConfig.middleware, channelConfig.tags),
136
153
  main
137
154
  )
138
155
 
@@ -12,7 +12,10 @@ import { createHTTPInteraction } from '../../http/http-runner.js'
12
12
  import { ChannelStore } from '../channel-store.js'
13
13
  import { handleHTTPError } from '../../../handle-error.js'
14
14
  import { PikkuUserSessionService } from '../../../services/user-session-service.js'
15
- import { runMiddleware } from '../../../middleware-runner.js'
15
+ import {
16
+ addMiddlewareForTags,
17
+ runMiddleware,
18
+ } from '../../../middleware-runner.js'
16
19
  import { pikkuState } from '../../../pikku-state.js'
17
20
  import { PikkuFetchHTTPRequest } from '../../http/pikku-fetch-http-request.js'
18
21
  import { PikkuHTTP } from '../../http/http.types.js'
@@ -146,7 +149,7 @@ export const runChannelConnect = async ({
146
149
  userSession,
147
150
  },
148
151
  { http },
149
- channelConfig.middleware || [],
152
+ addMiddlewareForTags(channelConfig.middleware, channelConfig.tags),
150
153
  main
151
154
  )
152
155
  }
@@ -24,7 +24,7 @@ import {
24
24
  PikkuUserSessionService,
25
25
  UserSessionService,
26
26
  } from '../../services/user-session-service.js'
27
- import { runMiddleware } from '../../middleware-runner.js'
27
+ import { addMiddlewareForTags, runMiddleware } from '../../middleware-runner.js'
28
28
  import { handleHTTPError } from '../../handle-error.js'
29
29
  import { pikkuState } from '../../pikku-state.js'
30
30
  import { PikkuFetchHTTPResponse } from './pikku-fetch-http-response.js'
@@ -120,9 +120,7 @@ export const wireHTTP = <
120
120
  if (!routeMeta) {
121
121
  throw new Error('Route metadata not found')
122
122
  }
123
- addFunction(routeMeta.pikkuFuncName, {
124
- func: httpWiring.func,
125
- })
123
+ addFunction(routeMeta.pikkuFuncName, httpWiring.func as any)
126
124
  const routes = pikkuState('http', 'routes')
127
125
  if (!routes.has(httpWiring.method)) {
128
126
  routes.set(httpWiring.method, new Map())
@@ -215,7 +213,16 @@ export const createHTTPInteraction = (
215
213
  /**
216
214
  * Validates the input data and executes the route handler with associated middleware.
217
215
  *
218
- * This function is the central execution point for a route. It performs these steps:
216
+ * NOTE: HTTP wiring handles middleware differently from other wirings (RPC, MCP, Queue, etc.)
217
+ * because HTTP needs to:
218
+ * 1. Check session early for performance (before expensive body parsing)
219
+ * 2. Handle HTTP-specific concerns (headers, cookies, SSE setup)
220
+ * 3. Process middleware that may set up authentication/session state
221
+ *
222
+ * Other wirings (RPC/MCP/Queue/Scheduler) simply pass middleware/permissions/auth
223
+ * directly to runPikkuFunc without processing them.
224
+ *
225
+ * This function performs these steps:
219
226
  * 1. Sets URL parameters on the request.
220
227
  * 2. Validates the user session if required.
221
228
  * 3. Creates session-specific services.
@@ -346,6 +353,7 @@ const executeRouteWithMiddleware = async (
346
353
  data,
347
354
  permissions: route.permissions,
348
355
  coerceDataFromSchema: options.coerceDataFromSchema,
356
+ tags: route.tags,
349
357
  })
350
358
 
351
359
  // Respond with either a binary or JSON response based on configuration
@@ -364,7 +372,7 @@ const executeRouteWithMiddleware = async (
364
372
  await runMiddleware(
365
373
  { ...singletonServices, userSession },
366
374
  { http },
367
- middleware,
375
+ addMiddlewareForTags(middleware, route.tags),
368
376
  runMain
369
377
  )
370
378
 
@@ -72,6 +72,7 @@ export type CoreHTTPFunction = {
72
72
  eventChannel?: false
73
73
  returnsJSON?: false
74
74
  timeout?: number
75
+ tags?: string[]
75
76
  docs?: Partial<{
76
77
  description: string
77
78
  response: string