@pikku/core 0.9.10 → 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 (100) hide show
  1. package/CHANGELOG.md +12 -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/services/local-content.js +2 -2
  16. package/dist/types/core.types.d.ts +51 -2
  17. package/dist/types/core.types.js +21 -0
  18. package/dist/wirings/channel/channel-handler.d.ts +2 -2
  19. package/dist/wirings/channel/channel-handler.js +12 -9
  20. package/dist/wirings/channel/channel-runner.d.ts +0 -2
  21. package/dist/wirings/channel/channel-runner.js +2 -3
  22. package/dist/wirings/channel/channel.types.d.ts +5 -3
  23. package/dist/wirings/channel/local/local-channel-runner.js +12 -12
  24. package/dist/wirings/channel/serverless/serverless-channel-runner.js +7 -8
  25. package/dist/wirings/cli/channel/cli-channel-runner.d.ts +12 -0
  26. package/dist/wirings/cli/channel/cli-channel-runner.js +80 -0
  27. package/dist/wirings/cli/channel/index.d.ts +1 -0
  28. package/dist/wirings/cli/channel/index.js +1 -0
  29. package/dist/wirings/cli/cli-runner.d.ts +32 -0
  30. package/dist/wirings/cli/cli-runner.js +328 -0
  31. package/dist/wirings/cli/cli.types.d.ts +177 -0
  32. package/dist/wirings/cli/cli.types.js +1 -0
  33. package/dist/wirings/cli/command-parser.d.ts +19 -0
  34. package/dist/wirings/cli/command-parser.js +373 -0
  35. package/dist/wirings/cli/index.d.ts +5 -0
  36. package/dist/wirings/cli/index.js +5 -0
  37. package/dist/wirings/http/http-runner.d.ts +29 -10
  38. package/dist/wirings/http/http-runner.js +103 -131
  39. package/dist/wirings/http/http.types.d.ts +10 -10
  40. package/dist/wirings/http/index.d.ts +1 -1
  41. package/dist/wirings/http/index.js +1 -1
  42. package/dist/wirings/http/pikku-fetch-http-response.js +3 -1
  43. package/dist/wirings/http/routers/http-router.d.ts +0 -2
  44. package/dist/wirings/http/routers/path-to-regex.d.ts +1 -1
  45. package/dist/wirings/http/routers/path-to-regex.js +5 -34
  46. package/dist/wirings/mcp/mcp-runner.d.ts +4 -5
  47. package/dist/wirings/mcp/mcp-runner.js +30 -34
  48. package/dist/wirings/mcp/mcp.types.d.ts +11 -8
  49. package/dist/wirings/queue/queue-runner.d.ts +2 -2
  50. package/dist/wirings/queue/queue-runner.js +25 -27
  51. package/dist/wirings/queue/queue.types.d.ts +6 -5
  52. package/dist/wirings/rpc/index.d.ts +1 -1
  53. package/dist/wirings/rpc/index.js +1 -1
  54. package/dist/wirings/rpc/rpc-runner.d.ts +6 -18
  55. package/dist/wirings/rpc/rpc-runner.js +13 -8
  56. package/dist/wirings/scheduler/scheduler-runner.d.ts +2 -2
  57. package/dist/wirings/scheduler/scheduler-runner.js +37 -35
  58. package/dist/wirings/scheduler/scheduler.types.d.ts +4 -3
  59. package/package.json +4 -3
  60. package/src/function/function-runner.test.ts +73 -23
  61. package/src/function/function-runner.ts +74 -55
  62. package/src/index.ts +8 -1
  63. package/src/middleware-runner.test.ts +24 -16
  64. package/src/middleware-runner.ts +136 -83
  65. package/src/permissions.ts +1 -0
  66. package/src/pikku-state.ts +47 -2
  67. package/src/services/content-service.ts +5 -4
  68. package/src/services/index.ts +0 -1
  69. package/src/services/local-content.ts +11 -6
  70. package/src/types/core.types.ts +74 -3
  71. package/src/wirings/channel/channel-handler.ts +9 -13
  72. package/src/wirings/channel/channel-runner.ts +2 -6
  73. package/src/wirings/channel/channel.types.ts +5 -1
  74. package/src/wirings/channel/local/local-channel-runner.ts +25 -15
  75. package/src/wirings/channel/serverless/serverless-channel-runner.ts +15 -17
  76. package/src/wirings/cli/channel/cli-channel-runner.ts +118 -0
  77. package/src/wirings/cli/channel/index.ts +1 -0
  78. package/src/wirings/cli/cli-runner.test.ts +382 -0
  79. package/src/wirings/cli/cli-runner.ts +503 -0
  80. package/src/wirings/cli/cli.types.ts +320 -0
  81. package/src/wirings/cli/command-parser.test.ts +440 -0
  82. package/src/wirings/cli/command-parser.ts +470 -0
  83. package/src/wirings/cli/index.ts +12 -0
  84. package/src/wirings/http/http-runner.test.ts +8 -7
  85. package/src/wirings/http/http-runner.ts +126 -159
  86. package/src/wirings/http/http.types.ts +56 -11
  87. package/src/wirings/http/index.ts +1 -1
  88. package/src/wirings/http/pikku-fetch-http-response.ts +3 -1
  89. package/src/wirings/http/routers/http-router.ts +0 -2
  90. package/src/wirings/http/routers/path-to-regex.test.ts +4 -5
  91. package/src/wirings/http/routers/path-to-regex.ts +6 -43
  92. package/src/wirings/mcp/mcp-runner.ts +56 -55
  93. package/src/wirings/mcp/mcp.types.ts +23 -8
  94. package/src/wirings/queue/queue-runner.ts +44 -47
  95. package/src/wirings/queue/queue.types.ts +10 -6
  96. package/src/wirings/rpc/index.ts +1 -1
  97. package/src/wirings/rpc/rpc-runner.ts +27 -9
  98. package/src/wirings/scheduler/scheduler-runner.ts +57 -56
  99. package/src/wirings/scheduler/scheduler.types.ts +9 -2
  100. package/tsconfig.tsbuildinfo +1 -1
@@ -1,8 +1,7 @@
1
1
  import { PikkuWiringTypes, } from '../../types/core.types.js';
2
- import { MissingSessionError, NotFoundError } from '../../errors/errors.js';
2
+ import { NotFoundError } from '../../errors/errors.js';
3
3
  import { closeSessionServices, createWeakUID, isSerializable, } from '../../utils.js';
4
- import { PikkuUserSessionService, } from '../../services/user-session-service.js';
5
- import { combineMiddleware, runMiddleware } from '../../middleware-runner.js';
4
+ import { PikkuUserSessionService } from '../../services/user-session-service.js';
6
5
  import { handleHTTPError } from '../../handle-error.js';
7
6
  import { pikkuState } from '../../pikku-state.js';
8
7
  import { PikkuFetchHTTPResponse } from './pikku-fetch-http-response.js';
@@ -11,33 +10,38 @@ import { addFunction, runPikkuFunc } from '../../function/function-runner.js';
11
10
  import { rpcService } from '../rpc/rpc-runner.js';
12
11
  import { httpRouter } from './routers/http-router.js';
13
12
  /**
14
- * Registers middleware either globally or for a specific route.
13
+ * Registers HTTP middleware for a specific route pattern.
15
14
  *
16
- * When a string route pattern is provided along with middleware, the middleware
17
- * is applied only to that route. Otherwise, if an array is provided, it is treated
18
- * as global middleware (applied to all routes).
15
+ * This function registers middleware at runtime that will be applied to
16
+ * HTTP routes matching the specified pattern.
17
+ *
18
+ * For tree-shaking benefits, wrap in a factory function:
19
+ * `export const x = () => addHTTPMiddleware('pattern', [...])`
19
20
  *
20
21
  * @template PikkuMiddleware The middleware type.
21
- * @param {PikkuMiddleware[] | string} routeOrMiddleware - Either a global middleware array or a route pattern string.
22
- * @param {PikkuMiddleware[]} [middleware] - The middleware array to apply when a route pattern is specified.
22
+ * @param {string} pattern - Route pattern (e.g., '*' for all routes, '/api/*' for specific routes).
23
+ * @param {CorePikkuMiddlewareGroup} middleware - Array of middleware for this route pattern.
24
+ *
25
+ * @returns {CorePikkuMiddlewareGroup} The middleware array (for chaining/wrapping).
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * // Recommended: tree-shakeable
30
+ * export const httpGlobal = () => addHTTPMiddleware('*', [
31
+ * corsMiddleware,
32
+ * loggingMiddleware
33
+ * ])
34
+ *
35
+ * // Also works: no tree-shaking
36
+ * export const apiMiddleware = addHTTPMiddleware('/api/*', [
37
+ * authMiddleware
38
+ * ])
39
+ * ```
23
40
  */
24
- export const addHTTPMiddleware = (routeOrMiddleware, middleware) => {
25
- const middlewareStore = pikkuState('http', 'middleware');
26
- let route = '*';
27
- if (typeof routeOrMiddleware === 'string') {
28
- route = routeOrMiddleware;
29
- middleware = middleware;
30
- }
31
- else {
32
- middleware = routeOrMiddleware;
33
- }
34
- const currentMiddleware = middlewareStore.get(route);
35
- if (currentMiddleware) {
36
- middlewareStore.set(route, [...currentMiddleware, ...middleware]);
37
- }
38
- else {
39
- middlewareStore.set(route, middleware);
40
- }
41
+ export const addHTTPMiddleware = (pattern, middleware) => {
42
+ const httpGroups = pikkuState('middleware', 'httpGroup');
43
+ httpGroups[pattern] = middleware;
44
+ return middleware;
41
45
  };
42
46
  /**
43
47
  * Adds a new route to the global HTTP route registry.
@@ -92,8 +96,6 @@ const getMatchingRoute = (requestType, requestPath) => {
92
96
  params: matchedPath.params,
93
97
  route,
94
98
  permissions: route.permissions,
95
- httpMiddleware: matchedPath.middleware,
96
- middleware: route.middleware,
97
99
  meta: meta,
98
100
  };
99
101
  }
@@ -122,16 +124,7 @@ export const createHTTPInteraction = (request, response) => {
122
124
  return http;
123
125
  };
124
126
  /**
125
- * Validates the input data and executes the route handler with associated middleware.
126
- *
127
- * NOTE: HTTP wiring handles middleware differently from other wirings (RPC, MCP, Queue, etc.)
128
- * because HTTP needs to:
129
- * 1. Check session early for performance (before expensive body parsing)
130
- * 2. Handle HTTP-specific concerns (headers, cookies, SSE setup)
131
- * 3. Process middleware that may set up authentication/session state
132
- *
133
- * Other wirings (RPC/MCP/Queue/Scheduler) simply pass middleware/permissions/auth
134
- * directly to runPikkuFunc without processing them.
127
+ * Validates the input data and executes the route handler
135
128
  *
136
129
  * This function performs these steps:
137
130
  * 1. Sets URL parameters on the request.
@@ -144,103 +137,90 @@ export const createHTTPInteraction = (request, response) => {
144
137
  * 8. Sends the appropriate response.
145
138
  *
146
139
  * @param {Object} services - A collection of shared services and utilities.
147
- * @param {Object} matchedRoute - Contains route details, URL parameters, middleware, and optional schema.
140
+ * @param {Object} matchedRoute - Contains route details, URL parameters, and optional schema.
148
141
  * @param {PikkuHTTP | undefined} http - The HTTP interaction object.
149
142
  * @param {Object} options - Options for route execution (e.g., whether to coerce query strings to arrays).
150
143
  * @returns {Promise<any>} An object containing the route handler result and session services (if any).
151
144
  * @throws Throws errors like MissingSessionError or ForbiddenError on validation failures.
152
145
  */
153
- const executeRouteWithMiddleware = async (services, matchedRoute, http, options) => {
154
- const { matchedPath, params, route, httpMiddleware, middleware, meta } = matchedRoute;
155
- const { singletonServices, userSession, createSessionServices, skipUserSession, requestId, } = services;
146
+ const executeRoute = async (services, matchedRoute, http, options) => {
147
+ const userSession = new PikkuUserSessionService();
148
+ const { params, route, meta } = matchedRoute;
149
+ const { singletonServices, createSessionServices, skipUserSession, requestId, } = services;
156
150
  const requiresSession = route.auth !== false;
157
151
  let sessionServices;
158
152
  let result;
159
153
  // Attach URL parameters to the request object
160
154
  http?.request?.setParams(params);
161
155
  singletonServices.logger.info(`Matched route: ${route.route} | method: ${route.method.toUpperCase()} | auth: ${requiresSession.toString()}`);
162
- // Main route execution logic wrapped for middleware handling
163
- const runMain = async () => {
164
- const session = userSession.get();
165
- // Ensure session is available when required
166
- if (skipUserSession && requiresSession) {
167
- throw new Error("Can't skip trying to get user session if auth is required");
156
+ // Ensure session is available when required
157
+ if (skipUserSession && requiresSession) {
158
+ throw new Error("Can't skip trying to get user session if auth is required");
159
+ }
160
+ const data = () => http.request.data();
161
+ let channel;
162
+ if (matchedRoute.route.sse) {
163
+ const response = http?.response;
164
+ if (!response) {
165
+ throw new Error('SSE requires a valid HTTP response object');
168
166
  }
169
- if (requiresSession && !session) {
170
- singletonServices.logger.info({
171
- action: 'Rejecting route (invalid session)',
172
- path: matchedPath,
173
- });
174
- throw new MissingSessionError();
167
+ if (!response.setMode) {
168
+ throw new Error('Response object does not support SSE mode');
175
169
  }
176
- const data = await http.request.data();
177
- const getAllServices = async () => {
178
- let channel;
179
- if (matchedRoute.route.sse) {
180
- const response = http?.response;
181
- if (!response) {
182
- throw new Error('SSE requires a valid HTTP response object');
183
- }
184
- if (!response.setMode) {
185
- throw new Error('Response object does not support SSE mode');
186
- }
187
- response.setMode('stream');
188
- response.header('Content-Type', 'text/event-stream');
189
- response.header('Cache-Control', 'no-cache');
190
- response.header('Connection', 'keep-alive');
191
- response.header('Transfer-Encoding', 'chunked');
192
- channel = {
193
- channelId: requestId,
194
- openingData: data,
195
- send: (data) => {
196
- response.arrayBuffer(isSerializable(data) ? JSON.stringify(data) : data);
197
- },
198
- close: () => {
199
- channel.state = 'closed';
200
- response.close?.();
201
- },
202
- state: 'open',
203
- };
204
- }
205
- // Create session-specific services for handling the request
206
- sessionServices = await createSessionServices({ ...singletonServices, userSession, channel }, { http }, session);
207
- return rpcService.injectRPCService({
208
- ...singletonServices,
209
- ...sessionServices,
210
- http,
211
- userSession,
212
- channel,
213
- });
170
+ response.setMode('stream');
171
+ response.header('Content-Type', 'text/event-stream');
172
+ response.header('Cache-Control', 'no-cache');
173
+ response.header('Connection', 'keep-alive');
174
+ response.header('Transfer-Encoding', 'chunked');
175
+ channel = {
176
+ channelId: requestId,
177
+ openingData: await data(),
178
+ send: (data) => {
179
+ response.arrayBuffer(isSerializable(data) ? JSON.stringify(data) : data);
180
+ },
181
+ close: () => {
182
+ channel.state = 'closed';
183
+ response.close?.();
184
+ },
185
+ state: 'open',
214
186
  };
215
- result = await runPikkuFunc(PikkuWiringTypes.http, `${meta.method}:${meta.route}`, meta.pikkuFuncName, {
216
- getAllServices,
217
- session,
218
- data,
219
- permissions: route.permissions,
220
- coerceDataFromSchema: options.coerceDataFromSchema,
221
- tags: route.tags,
222
- });
223
- // Respond with either a binary or JSON response based on configuration
224
- if (route.returnsJSON === false) {
225
- http?.response?.arrayBuffer(result);
226
- }
227
- else {
228
- http?.response?.json(result);
229
- }
230
- http?.response?.status(200);
231
- // TODO: Evaluate if the response stream should be explicitly ended.
232
- // http?.response?.end()
187
+ }
188
+ const interaction = { http, channel };
189
+ const getAllServices = async (session) => {
190
+ let channel;
191
+ // Create session-specific services for handling the request
192
+ sessionServices = await createSessionServices({ ...singletonServices, userSession, channel }, { http }, session);
193
+ return rpcService.injectRPCService({
194
+ ...singletonServices,
195
+ ...sessionServices,
196
+ http,
197
+ userSession,
198
+ channel,
199
+ }, interaction, route.auth);
233
200
  };
234
- // Get function config for middleware and tags
235
- const funcConfig = pikkuState('function', 'functions').get(meta.pikkuFuncName);
236
- // Execute middleware, then run the main logic
237
- await runMiddleware({ ...singletonServices, userSession }, { http }, combineMiddleware(PikkuWiringTypes.http, `${meta.method}:${meta.route}`, {
238
- httpMiddleware,
239
- wiringMiddleware: middleware,
240
- wiringTags: route.tags,
241
- funcMiddleware: funcConfig?.middleware,
242
- funcTags: funcConfig?.tags,
243
- }), runMain);
201
+ result = await runPikkuFunc(PikkuWiringTypes.http, `${meta.method}:${meta.route}`, meta.pikkuFuncName, {
202
+ singletonServices,
203
+ getAllServices,
204
+ auth: route.auth !== false,
205
+ userSession,
206
+ data,
207
+ permissions: route.permissions,
208
+ inheritedMiddleware: meta.middleware,
209
+ wireMiddleware: route.middleware,
210
+ coerceDataFromSchema: options.coerceDataFromSchema,
211
+ tags: route.tags,
212
+ interaction,
213
+ });
214
+ // Respond with either a binary or JSON response based on configuration
215
+ if (route.returnsJSON === false) {
216
+ http?.response?.arrayBuffer(result);
217
+ }
218
+ else {
219
+ http?.response?.json(result);
220
+ }
221
+ http?.response?.status(200);
222
+ // TODO: Evaluate if the response stream should be explicitly ended.
223
+ // http?.response?.end()
244
224
  return sessionServices ? { result, sessionServices } : { result };
245
225
  };
246
226
  /**
@@ -298,11 +278,10 @@ export const pikkuFetch = async (request, params) => {
298
278
  * @param {RunHTTPWiringOptions & RunHTTPWiringParams} options - Options such as singleton services, session handling, and error configuration.
299
279
  * @returns {Promise<Out | void>} The output from the route handler or void if an error occurred.
300
280
  */
301
- export const fetchData = async (request, response, { singletonServices, createSessionServices, skipUserSession = false, respondWith404 = true, logWarningsForStatusCodes = [], coerceDataFromSchema = true, bubbleErrors = false, generateRequestId, ignoreMiddleware = false, }) => {
281
+ export const fetchData = async (request, response, { singletonServices, createSessionServices, skipUserSession = false, respondWith404 = true, logWarningsForStatusCodes = [], coerceDataFromSchema = true, bubbleErrors = false, generateRequestId, }) => {
302
282
  const requestId = request.getHeader?.('x-request-id') ||
303
283
  generateRequestId?.() ||
304
284
  createWeakUID();
305
- const userSession = new PikkuUserSessionService();
306
285
  let sessionServices;
307
286
  let result;
308
287
  // Combine the request and response into one interaction object
@@ -323,19 +302,12 @@ export const fetchData = async (request, response, { singletonServices, createSe
323
302
  }
324
303
  // Execute the matched route along with its middleware and session management
325
304
  ;
326
- ({ result, sessionServices } = await executeRouteWithMiddleware({
305
+ ({ result, sessionServices } = await executeRoute({
327
306
  singletonServices,
328
- userSession,
329
307
  createSessionServices,
330
308
  skipUserSession,
331
309
  requestId,
332
- }, ignoreMiddleware
333
- ? {
334
- ...matchedRoute,
335
- middleware: undefined,
336
- httpMiddleware: undefined,
337
- }
338
- : matchedRoute, http, { coerceDataFromSchema }));
310
+ }, matchedRoute, http, { coerceDataFromSchema }));
339
311
  return result;
340
312
  }
341
313
  catch (e) {
@@ -1,7 +1,7 @@
1
1
  import type { SerializeOptions } from 'cookie';
2
2
  import type { PikkuError } from '../../errors/error-handler.js';
3
- import type { PikkuDocs, CoreServices, CoreSingletonServices, CoreUserSession, CreateSessionServices, CorePikkuMiddleware } from '../../types/core.types.js';
4
- import type { CorePikkuFunction, CorePikkuFunctionSessionless, CorePikkuPermission, CorePermissionGroup } from '../../function/functions.types.js';
3
+ import type { PikkuDocs, CoreServices, CoreSingletonServices, CoreUserSession, CreateSessionServices, CorePikkuMiddleware, MiddlewareMetadata } from '../../types/core.types.js';
4
+ import type { CorePikkuFunction, CorePikkuFunctionSessionless, CorePikkuPermission, CorePermissionGroup, CorePikkuFunctionConfig } from '../../function/functions.types.js';
5
5
  type ExtractHTTPWiringParams<S extends string> = S extends `${string}:${infer Param}/${infer Rest}` ? Param | ExtractHTTPWiringParams<`/${Rest}`> : S extends `${string}:${infer Param}` ? Param : never;
6
6
  export type AssertHTTPWiringParams<In, HTTPWiring extends string> = ExtractHTTPWiringParams<HTTPWiring> extends keyof In ? unknown : [
7
7
  'Error: HTTPWiring parameters',
@@ -16,7 +16,6 @@ export type RunHTTPWiringOptions = Partial<{
16
16
  coerceDataFromSchema: boolean;
17
17
  bubbleErrors: boolean;
18
18
  generateRequestId: () => string;
19
- ignoreMiddleware: boolean;
20
19
  }>;
21
20
  export type RunHTTPWiringParams = {
22
21
  singletonServices: CoreSingletonServices;
@@ -68,10 +67,10 @@ export type PikkuQuery<T = Record<string, string | undefined>> = Record<string,
68
67
  * @template PikkuFunctionSessionless - The sessionless API function type, defaults to `CorePikkuFunctionSessionless`.
69
68
  * @template PikkuPermission - The permission function type, defaults to `CorePikkuPermission`.
70
69
  */
71
- export type CoreHTTPFunctionWiring<In, Out, R extends string, PikkuFunction = CorePikkuFunction<In, Out>, PikkuFunctionSessionless = CorePikkuFunctionSessionless<In, Out>, PikkuPermission = CorePikkuPermission<In>, PikkuMiddleware = CorePikkuMiddleware> = (CoreHTTPFunction & {
70
+ export type CoreHTTPFunctionWiring<In, Out, R extends string, PikkuFunction extends CorePikkuFunction<In, Out, any, any, any> = CorePikkuFunction<In, Out>, PikkuFunctionSessionless extends CorePikkuFunctionSessionless<In, Out, any, any, any> = CorePikkuFunctionSessionless<In, Out>, PikkuPermission extends CorePikkuPermission<In, any, any> = CorePikkuPermission<In, any, any>, PikkuMiddleware extends CorePikkuMiddleware<any, any> = CorePikkuMiddleware<any>> = (CoreHTTPFunction & {
72
71
  route: R;
73
72
  method: HTTPMethod;
74
- func: PikkuFunction;
73
+ func: CorePikkuFunctionConfig<PikkuFunction, PikkuPermission, PikkuMiddleware>;
75
74
  permissions?: CorePermissionGroup<PikkuPermission>;
76
75
  auth?: true;
77
76
  tags?: string[];
@@ -80,7 +79,7 @@ export type CoreHTTPFunctionWiring<In, Out, R extends string, PikkuFunction = Co
80
79
  }) | (CoreHTTPFunction & {
81
80
  route: R;
82
81
  method: HTTPMethod;
83
- func: PikkuFunctionSessionless;
82
+ func: CorePikkuFunctionConfig<PikkuFunctionSessionless, PikkuPermission, PikkuMiddleware>;
84
83
  permissions?: undefined;
85
84
  auth?: false;
86
85
  tags?: string[];
@@ -89,7 +88,7 @@ export type CoreHTTPFunctionWiring<In, Out, R extends string, PikkuFunction = Co
89
88
  }) | (CoreHTTPFunction & {
90
89
  route: R;
91
90
  method: 'get';
92
- func: PikkuFunction;
91
+ func: CorePikkuFunctionConfig<PikkuFunction, PikkuPermission, PikkuMiddleware>;
93
92
  permissions?: CorePermissionGroup<PikkuPermission>;
94
93
  auth?: true;
95
94
  sse?: boolean;
@@ -98,7 +97,7 @@ export type CoreHTTPFunctionWiring<In, Out, R extends string, PikkuFunction = Co
98
97
  }) | (CoreHTTPFunction & {
99
98
  route: R;
100
99
  method: 'get';
101
- func: PikkuFunctionSessionless;
100
+ func: CorePikkuFunctionConfig<PikkuFunctionSessionless, PikkuPermission, PikkuMiddleware>;
102
101
  permissions?: undefined;
103
102
  auth?: false;
104
103
  sse?: boolean;
@@ -107,7 +106,7 @@ export type CoreHTTPFunctionWiring<In, Out, R extends string, PikkuFunction = Co
107
106
  }) | (CoreHTTPFunction & {
108
107
  route: R;
109
108
  method: 'post';
110
- func: PikkuFunction;
109
+ func: CorePikkuFunctionConfig<PikkuFunction, PikkuPermission, PikkuMiddleware>;
111
110
  permissions?: CorePermissionGroup<PikkuPermission>;
112
111
  auth?: true;
113
112
  query?: Array<keyof In>;
@@ -117,7 +116,7 @@ export type CoreHTTPFunctionWiring<In, Out, R extends string, PikkuFunction = Co
117
116
  }) | (CoreHTTPFunction & {
118
117
  route: R;
119
118
  method: 'post';
120
- func: PikkuFunctionSessionless;
119
+ func: CorePikkuFunctionConfig<PikkuFunctionSessionless, PikkuPermission, PikkuMiddleware>;
121
120
  permissions?: undefined;
122
121
  auth?: false;
123
122
  query?: Array<keyof In>;
@@ -146,6 +145,7 @@ export type HTTPWiringMeta = {
146
145
  docs?: PikkuDocs;
147
146
  tags?: string[];
148
147
  sse?: true;
148
+ middleware?: MiddlewareMetadata[];
149
149
  };
150
150
  export type HTTPWiringsMeta = Record<HTTPMethod, Record<string, HTTPWiringMeta>>;
151
151
  export type HTTPFunctionsMeta = Array<{
@@ -1,5 +1,5 @@
1
1
  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
- export { fetch, fetchData, wireHTTP } from './http-runner.js';
4
+ export { fetch, fetchData, wireHTTP, addHTTPMiddleware } from './http-runner.js';
5
5
  export type * from './http.types.js';
@@ -1,4 +1,4 @@
1
1
  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
- export { fetch, fetchData, wireHTTP } from './http-runner.js';
4
+ export { fetch, fetchData, wireHTTP, addHTTPMiddleware } from './http-runner.js';
@@ -88,7 +88,9 @@ export class PikkuFetchHTTPResponse {
88
88
  }
89
89
  toResponse(args) {
90
90
  const cookieHeader = Array.from(this.#cookies.entries()).map(([name, { value, flags }]) => serializeCookie(name, value, flags));
91
- this.#headers.set('Set-Cookie', cookieHeader.join(', '));
91
+ // Multiple Set-Cookie headers must be appended separately, not joined with commas
92
+ // per HTTP specification (RFC 6265)
93
+ cookieHeader.forEach((cookie) => this.#headers.append('Set-Cookie', cookie));
92
94
  return new Response(this.#body, {
93
95
  ...args,
94
96
  status: this.#statusCode,
@@ -1,10 +1,8 @@
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
  export type MatchResult = {
5
4
  route: string;
6
5
  params: Record<string, any>;
7
- middleware?: CorePikkuMiddleware[];
8
6
  } | null;
9
7
  export interface Router {
10
8
  match(method: HTTPMethod, path: string): MatchResult;
@@ -3,8 +3,8 @@ import { HTTPMethod } from '../http.types.js';
3
3
  export declare class PathToRegexRouter implements Router {
4
4
  private compiledRoutes;
5
5
  private staticRoutes;
6
- private precompiledMiddleware;
7
6
  private isInitialized;
7
+ reset(): void;
8
8
  initialize(): void;
9
9
  match(method: HTTPMethod, path: string): MatchResult;
10
10
  }
@@ -3,16 +3,15 @@ import { pikkuState } from '../../../pikku-state.js';
3
3
  export class PathToRegexRouter {
4
4
  compiledRoutes = new Map();
5
5
  staticRoutes = new Map();
6
- precompiledMiddleware = new Map();
7
6
  isInitialized = false;
7
+ reset() {
8
+ this.compiledRoutes = new Map();
9
+ this.staticRoutes = new Map();
10
+ this.isInitialized = false;
11
+ }
8
12
  initialize() {
9
13
  const routes = pikkuState('http', 'routes');
10
14
  const channelRoutes = pikkuState('channel', 'channels');
11
- const middlewareMap = pikkuState('http', 'middleware');
12
- // Precompile middleware lookups
13
- for (const [middlewareRoute, middlewareArray] of middlewareMap.entries()) {
14
- this.precompiledMiddleware.set(middlewareRoute, middlewareArray);
15
- }
16
15
  // Helper function to compile routes for a given method
17
16
  const compileRoutesForMethod = (method, routeEntries) => {
18
17
  const methodCompiledRoutes = this.compiledRoutes.get(method) || new Map();
@@ -24,35 +23,10 @@ export class PathToRegexRouter {
24
23
  : `/${routePath}`;
25
24
  // Check if route is static (no parameters or wildcards)
26
25
  const isStaticRoute = !/\*|:/.test(normalizedRoutePath);
27
- // Precompute middleware for this route
28
- const routeMiddleware = [];
29
- // Add global middleware (*)
30
- const globalMiddleware = this.precompiledMiddleware.get('*');
31
- if (globalMiddleware) {
32
- routeMiddleware.push(...globalMiddleware);
33
- }
34
- // Add route-specific middleware
35
- for (const [middlewareRoute, middlewareArray,] of this.precompiledMiddleware.entries()) {
36
- if (middlewareRoute !== '*') {
37
- // Use regex test for pattern matching
38
- try {
39
- if (new RegExp(middlewareRoute).test(normalizedRoutePath)) {
40
- routeMiddleware.push(...middlewareArray);
41
- }
42
- }
43
- catch {
44
- // If regex is invalid, do exact match
45
- if (middlewareRoute === normalizedRoutePath) {
46
- routeMiddleware.push(...middlewareArray);
47
- }
48
- }
49
- }
50
- }
51
26
  if (isStaticRoute) {
52
27
  // Store static routes for O(1) lookup
53
28
  methodStaticRoutes.set(normalizedRoutePath, {
54
29
  route: routePath, // Keep the original route path for lookup in pikkuState
55
- middleware: routeMiddleware,
56
30
  });
57
31
  }
58
32
  else {
@@ -63,7 +37,6 @@ export class PathToRegexRouter {
63
37
  methodCompiledRoutes.set(normalizedRoutePath, {
64
38
  matcher,
65
39
  route: routePath, // Keep the original route path for lookup in pikkuState
66
- middleware: routeMiddleware,
67
40
  });
68
41
  }
69
42
  }
@@ -93,7 +66,6 @@ export class PathToRegexRouter {
93
66
  return {
94
67
  route: staticRoute.route,
95
68
  params: {},
96
- middleware: staticRoute.middleware,
97
69
  };
98
70
  }
99
71
  }
@@ -109,7 +81,6 @@ export class PathToRegexRouter {
109
81
  return {
110
82
  route: compiledRoute.route,
111
83
  params: result.params,
112
- middleware: compiledRoute.middleware,
113
84
  };
114
85
  }
115
86
  }
@@ -1,12 +1,11 @@
1
1
  import { type CoreServices, type CoreSingletonServices, type CoreUserSession, type CreateSessionServices } from '../../types/core.types.js';
2
2
  import type { CoreMCPResource, CoreMCPTool, CoreMCPPrompt, JsonRpcRequest, JsonRpcResponse, JsonRpcErrorResponse, PikkuMCP } from './mcp.types.js';
3
- import type { CorePikkuFunctionSessionless } from '../../function/functions.types.js';
3
+ import type { CorePikkuFunctionConfig, CorePikkuFunctionSessionless } from '../../function/functions.types.js';
4
4
  export declare class MCPError extends Error {
5
5
  readonly error: JsonRpcErrorResponse;
6
6
  constructor(error: JsonRpcErrorResponse);
7
7
  }
8
8
  export type RunMCPEndpointParams<Tools extends string = any> = {
9
- session?: CoreUserSession;
10
9
  singletonServices: CoreSingletonServices;
11
10
  mcp?: PikkuMCP<Tools>;
12
11
  createSessionServices?: CreateSessionServices<CoreSingletonServices, CoreServices<CoreSingletonServices>, CoreUserSession>;
@@ -16,9 +15,9 @@ export type JsonRpcError = {
16
15
  message: string;
17
16
  data?: any;
18
17
  };
19
- export declare const wireMCPResource: <PikkuFunction extends CorePikkuFunctionSessionless<any, any>>(mcpResource: CoreMCPResource<PikkuFunction>) => void;
20
- export declare const wireMCPTool: <PikkuFunction extends CorePikkuFunctionSessionless<any, any>>(mcpTool: CoreMCPTool<PikkuFunction>) => void;
21
- export declare const wireMCPPrompt: <PikkuFunction extends CorePikkuFunctionSessionless<any, any>>(mcpPrompt: CoreMCPPrompt<PikkuFunction>) => void;
18
+ export declare const wireMCPResource: <PikkuFunctionConfig extends CorePikkuFunctionConfig<CorePikkuFunctionSessionless<any, any>> = CorePikkuFunctionConfig<CorePikkuFunctionSessionless<any, any>>>(mcpResource: CoreMCPResource<PikkuFunctionConfig>) => void;
19
+ export declare const wireMCPTool: <PikkuFunctionConfig extends CorePikkuFunctionConfig<CorePikkuFunctionSessionless<any, any>> = CorePikkuFunctionConfig<CorePikkuFunctionSessionless<any, any>>>(mcpTool: CoreMCPTool<PikkuFunctionConfig>) => void;
20
+ export declare const wireMCPPrompt: <PikkuFunctionConfig extends CorePikkuFunctionConfig<CorePikkuFunctionSessionless<any, any>> = CorePikkuFunctionConfig<CorePikkuFunctionSessionless<any, any>>>(mcpPrompt: CoreMCPPrompt<PikkuFunctionConfig>) => void;
22
21
  export declare function runMCPResource(request: JsonRpcRequest, params: RunMCPEndpointParams, uri: string): Promise<JsonRpcResponse>;
23
22
  export declare function runMCPTool(request: JsonRpcRequest, params: RunMCPEndpointParams, name: string): Promise<JsonRpcResponse>;
24
23
  export declare function runMCPPrompt(request: JsonRpcRequest, params: RunMCPEndpointParams, name: string): Promise<JsonRpcResponse>;
@@ -5,7 +5,6 @@ import { pikkuState } from '../../pikku-state.js';
5
5
  import { addFunction, runPikkuFunc } from '../../function/function-runner.js';
6
6
  import { rpcService } from '../rpc/rpc-runner.js';
7
7
  import { BadRequestError, NotFoundError } from '../../errors/errors.js';
8
- import { combineMiddleware, runMiddleware } from '../../middleware-runner.js';
9
8
  export class MCPError extends Error {
10
9
  error;
11
10
  constructor(error) {
@@ -21,7 +20,7 @@ export const wireMCPResource = (mcpResource) => {
21
20
  if (!mcpResourceMeta) {
22
21
  throw new Error(`MCP resource metadata not found for '${mcpResource.uri}'`);
23
22
  }
24
- addFunction(mcpResourceMeta.pikkuFuncName, mcpResource.func);
23
+ addFunction(mcpResourceMeta.pikkuFuncName, mcpResource);
25
24
  const resources = pikkuState('mcp', 'resources');
26
25
  if (resources.has(mcpResource.uri)) {
27
26
  throw new Error(`MCP resource already exists: ${mcpResource.uri}`);
@@ -101,7 +100,7 @@ export async function runMCPPrompt(request, params, name) {
101
100
  /**
102
101
  * JSON-RPC 2.0 compatible MCP endpoint runner
103
102
  */
104
- async function runMCPPikkuFunc(request, type, name, mcp, pikkuFuncName, { session, singletonServices, createSessionServices, mcp: interaction, }) {
103
+ async function runMCPPikkuFunc(request, type, name, mcp, pikkuFuncName, { singletonServices, createSessionServices, mcp: mcpInteraction, }) {
105
104
  let sessionServices;
106
105
  try {
107
106
  // Validate JSON-RPC request structure
@@ -115,38 +114,35 @@ async function runMCPPikkuFunc(request, type, name, mcp, pikkuFuncName, { sessio
115
114
  throw new NotFoundError(`MCP '${type}' PikkuFunction Mapping not found for '${name}'`);
116
115
  }
117
116
  singletonServices.logger.debug(`Running MCP ${type}: ${name}`);
118
- let result;
119
- // Main MCP execution logic wrapped for middleware handling
120
- const runMain = async () => {
121
- const getAllServices = async () => {
122
- if (createSessionServices) {
123
- const services = await createSessionServices(singletonServices, { mcp: interaction }, session);
124
- sessionServices = services;
125
- return rpcService.injectRPCService({
126
- ...singletonServices,
127
- ...services,
128
- mcp: interaction,
129
- });
130
- }
131
- return rpcService.injectRPCService({
132
- ...singletonServices,
133
- mcp: interaction,
134
- });
135
- };
136
- result = await runPikkuFunc(PikkuWiringTypes.mcp, `${type}:${name}`, pikkuFuncName, {
137
- getAllServices,
138
- session,
139
- data: request.params,
140
- tags: mcp.tags,
141
- });
117
+ const interaction = { mcp: mcpInteraction };
118
+ const getAllServices = async () => {
119
+ sessionServices = await createSessionServices?.(singletonServices, interaction, undefined);
120
+ return rpcService.injectRPCService({
121
+ ...singletonServices,
122
+ ...sessionServices,
123
+ }, interaction);
142
124
  };
143
- const funcConfig = pikkuState('function', 'functions').get(pikkuFuncName);
144
- await runMiddleware(singletonServices, { mcp: interaction }, combineMiddleware(PikkuWiringTypes.mcp, `${type}:${name}`, {
145
- wiringMiddleware: mcp.middleware,
146
- wiringTags: mcp.tags,
147
- funcMiddleware: funcConfig?.middleware,
148
- funcTags: funcConfig?.tags,
149
- }), runMain);
125
+ // Get metadata for the MCP endpoint to access pre-resolved middleware
126
+ let meta;
127
+ if (type === 'resource') {
128
+ meta = pikkuState('mcp', 'resourcesMeta')[name];
129
+ }
130
+ else if (type === 'tool') {
131
+ meta = pikkuState('mcp', 'toolsMeta')[name];
132
+ }
133
+ else if (type === 'prompt') {
134
+ meta = pikkuState('mcp', 'promptsMeta')[name];
135
+ }
136
+ const result = await runPikkuFunc(PikkuWiringTypes.mcp, `${type}:${name}`, pikkuFuncName, {
137
+ singletonServices,
138
+ getAllServices,
139
+ userSession: undefined, // TODO
140
+ data: () => request.params,
141
+ inheritedMiddleware: meta?.middleware,
142
+ wireMiddleware: mcp.middleware,
143
+ tags: mcp.tags,
144
+ interaction,
145
+ });
150
146
  return {
151
147
  id: request.id,
152
148
  result,