@pikku/core 0.9.3 → 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 (49) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/function/function-runner.d.ts +2 -1
  3. package/dist/function/function-runner.js +13 -3
  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 +2 -0
  12. package/dist/pikku-state.js +2 -0
  13. package/dist/types/core.types.d.ts +11 -5
  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 +13 -3
  18. package/dist/wirings/http/http.types.d.ts +1 -0
  19. package/dist/wirings/mcp/mcp-runner.js +22 -14
  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 +64 -16
  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/scheduler/scheduler-runner.js +49 -24
  28. package/dist/wirings/scheduler/scheduler.types.d.ts +17 -2
  29. package/package.json +1 -1
  30. package/src/function/function-runner.ts +21 -2
  31. package/src/function/functions.types.ts +3 -1
  32. package/src/index.ts +6 -1
  33. package/src/middleware-runner.ts +110 -2
  34. package/src/permissions.ts +71 -0
  35. package/src/pikku-state.ts +4 -0
  36. package/src/types/core.types.ts +11 -5
  37. package/src/wirings/channel/channel-handler.ts +1 -0
  38. package/src/wirings/channel/local/local-channel-runner.ts +33 -16
  39. package/src/wirings/channel/serverless/serverless-channel-runner.ts +5 -2
  40. package/src/wirings/http/http-runner.ts +13 -3
  41. package/src/wirings/http/http.types.ts +1 -0
  42. package/src/wirings/mcp/mcp-runner.ts +32 -17
  43. package/src/wirings/mcp/mcp.types.ts +7 -0
  44. package/src/wirings/queue/index.ts +2 -0
  45. package/src/wirings/queue/queue-runner.ts +82 -19
  46. package/src/wirings/queue/queue.types.ts +20 -1
  47. package/src/wirings/scheduler/scheduler-runner.ts +73 -32
  48. package/src/wirings/scheduler/scheduler.types.ts +22 -1
  49. package/tsconfig.tsbuildinfo +1 -1
@@ -1,4 +1,5 @@
1
1
  import { CorePikkuFunctionSessionless } from '../../function/functions.types.js';
2
+ import { CorePikkuMiddleware } from '../../types/core.types.js';
2
3
  export type PikkuMCP<Tools extends string = any> = {
3
4
  uri?: string;
4
5
  sendResourceUpdated: (uri: string) => void;
@@ -38,7 +39,7 @@ export type MCPPromptMeta = Record<string, Omit<CoreMCPPrompt, 'func'> & {
38
39
  /**
39
40
  * Represents an MCP resource with specific properties.
40
41
  */
41
- export type CoreMCPResource<PikkuFunction = CorePikkuFunctionSessionless<any, any>> = {
42
+ export type CoreMCPResource<PikkuFunction = CorePikkuFunctionSessionless<any, any>, PikkuMiddleware = CorePikkuMiddleware<any>> = {
42
43
  uri: string;
43
44
  title: string;
44
45
  description?: string;
@@ -47,11 +48,12 @@ export type CoreMCPResource<PikkuFunction = CorePikkuFunctionSessionless<any, an
47
48
  streaming?: boolean;
48
49
  func: PikkuFunction;
49
50
  tags?: string[];
51
+ middleware?: PikkuMiddleware[];
50
52
  };
51
53
  /**
52
54
  * Represents an MCP tool with specific properties.
53
55
  */
54
- export type CoreMCPTool<PikkuFunction = CorePikkuFunctionSessionless<any, any>> = {
56
+ export type CoreMCPTool<PikkuFunction = CorePikkuFunctionSessionless<any, any>, PikkuMiddleware = CorePikkuMiddleware<any>> = {
55
57
  name: string;
56
58
  title?: string;
57
59
  description: string;
@@ -59,15 +61,17 @@ export type CoreMCPTool<PikkuFunction = CorePikkuFunctionSessionless<any, any>>
59
61
  func: PikkuFunction;
60
62
  tags?: string[];
61
63
  streaming?: boolean;
64
+ middleware?: PikkuMiddleware[];
62
65
  };
63
66
  /**
64
67
  * Represents an MCP prompt with specific properties.
65
68
  */
66
- export type CoreMCPPrompt<PikkuFunction = CorePikkuFunctionSessionless<any, MCPPromptResponse>> = {
69
+ export type CoreMCPPrompt<PikkuFunction = CorePikkuFunctionSessionless<any, MCPPromptResponse>, PikkuMiddleware = CorePikkuMiddleware<any>> = {
67
70
  name: string;
68
71
  description?: string;
69
72
  func: PikkuFunction;
70
73
  tags?: string[];
74
+ middleware?: PikkuMiddleware[];
71
75
  };
72
76
  export type JsonRpcRequest = {
73
77
  jsonrpc: string;
@@ -1,5 +1,5 @@
1
1
  export type * from './queue.types.js';
2
- export { wireQueueWorker, runQueueJob, getQueueWorkers, removeQueueWorker, } from './queue-runner.js';
2
+ export { wireQueueWorker, runQueueJob, getQueueWorkers, removeQueueWorker, QueueJobDiscardedError, QueueJobFailedError, } from './queue-runner.js';
3
3
  export { validateWorkerConfig } from './validate-worker-config.js';
4
4
  export type { QueueConfigMapping } from './validate-worker-config.js';
5
5
  export { registerQueueWorkers } from './register-queue-helper.js';
@@ -1,5 +1,5 @@
1
1
  // Queue processor management
2
- export { wireQueueWorker, runQueueJob, getQueueWorkers, removeQueueWorker, } from './queue-runner.js';
2
+ export { wireQueueWorker, runQueueJob, getQueueWorkers, removeQueueWorker, QueueJobDiscardedError, QueueJobFailedError, } from './queue-runner.js';
3
3
  // Configuration validation
4
4
  export { validateWorkerConfig } from './validate-worker-config.js';
5
5
  // Queue registration helper
@@ -1,7 +1,20 @@
1
1
  import type { CoreServices } from '../../types/core.types.js';
2
2
  import type { CoreQueueWorker, QueueJob } from './queue.types.js';
3
3
  import type { CorePikkuFunctionSessionless } from '../../function/functions.types.js';
4
+ import { PikkuError } from '../../errors/error-handler.js';
4
5
  import { CreateSessionServices } from '../../types/core.types.js';
6
+ /**
7
+ * Error class for when a queue job is explicitly failed
8
+ */
9
+ export declare class QueueJobFailedError extends PikkuError {
10
+ constructor(jobId: string, reason?: string);
11
+ }
12
+ /**
13
+ * Error class for when a queue job is explicitly discarded
14
+ */
15
+ export declare class QueueJobDiscardedError extends PikkuError {
16
+ constructor(jobId: string, reason?: string);
17
+ }
5
18
  /**
6
19
  * Add a queue processor to the system
7
20
  */
@@ -17,8 +30,9 @@ export declare function removeQueueWorker(name: string): Promise<void>;
17
30
  /**
18
31
  * Process a single queue job - this function is called by queue consumers
19
32
  */
20
- export declare function runQueueJob({ singletonServices, createSessionServices, job, }: {
33
+ export declare function runQueueJob({ singletonServices, createSessionServices, job, updateProgress, }: {
21
34
  singletonServices: CoreServices;
22
35
  createSessionServices?: CreateSessionServices;
23
36
  job: QueueJob;
37
+ updateProgress?: (progress: number | string | object) => Promise<void>;
24
38
  }): Promise<void>;
@@ -1,14 +1,33 @@
1
- import { getErrorResponse } from '../../errors/error-handler.js';
1
+ import { getErrorResponse, PikkuError } from '../../errors/error-handler.js';
2
2
  import { pikkuState } from '../../pikku-state.js';
3
3
  import { addFunction, runPikkuFunc } from '../../function/function-runner.js';
4
+ import { addMiddlewareForTags, runMiddleware } from '../../middleware-runner.js';
4
5
  /**
5
6
  * Error class for queue processor not found
6
7
  */
7
- class QueueWorkerNotFoundError extends Error {
8
+ class QueueWorkerNotFoundError extends PikkuError {
8
9
  constructor(name) {
9
10
  super(`Queue processor not found: ${name}`);
10
11
  }
11
12
  }
13
+ /**
14
+ * Error class for when a queue job is explicitly failed
15
+ */
16
+ export class QueueJobFailedError extends PikkuError {
17
+ constructor(jobId, reason) {
18
+ super(`Queue job ${jobId} failed${reason ? `: ${reason}` : ''}`);
19
+ this.name = 'QueueJobFailedError';
20
+ }
21
+ }
22
+ /**
23
+ * Error class for when a queue job is explicitly discarded
24
+ */
25
+ export class QueueJobDiscardedError extends PikkuError {
26
+ constructor(jobId, reason) {
27
+ super(`Queue job ${jobId} discarded${reason ? `: ${reason}` : ''}`);
28
+ this.name = 'QueueJobDiscardedError';
29
+ }
30
+ }
12
31
  /**
13
32
  * Add a queue processor to the system
14
33
  */
@@ -45,28 +64,57 @@ export async function removeQueueWorker(name) {
45
64
  /**
46
65
  * Process a single queue job - this function is called by queue consumers
47
66
  */
48
- export async function runQueueJob({ singletonServices, createSessionServices, job, }) {
67
+ export async function runQueueJob({ singletonServices, createSessionServices, job, updateProgress, }) {
49
68
  const logger = singletonServices.logger;
50
69
  const meta = pikkuState('queue', 'meta');
51
70
  const processorMeta = meta[job.queueName];
52
71
  if (!processorMeta) {
53
72
  throw new Error(`Processor metadata not found for: ${job.queueName}`);
54
73
  }
74
+ // Get the queue worker registration to access middleware
75
+ const registrations = pikkuState('queue', 'registrations');
76
+ const queueWorker = registrations.get(job.queueName);
77
+ if (!queueWorker) {
78
+ throw new Error(`Queue worker registration not found for: ${job.queueName}`);
79
+ }
80
+ // Create the queue interaction object
81
+ const queue = {
82
+ queueName: job.queueName,
83
+ jobId: job.id,
84
+ updateProgress: updateProgress ||
85
+ (async (progress) => {
86
+ logger.info(`Job ${job.id} progress: ${progress}`);
87
+ // Default implementation - just log the progress
88
+ }),
89
+ fail: async (reason) => {
90
+ throw new QueueJobFailedError(job.id, reason);
91
+ },
92
+ discard: async (reason) => {
93
+ throw new QueueJobDiscardedError(job.id, reason);
94
+ },
95
+ };
55
96
  try {
56
97
  logger.info(`Processing job ${job.id} in queue ${job.queueName}`);
57
- // Use provided singleton services
58
- const getAllServices = () => ({
59
- ...singletonServices,
60
- ...(createSessionServices
61
- ? createSessionServices(singletonServices, {}, undefined)
62
- : {}),
63
- });
64
- // Execute the pikku function with the job data
65
- const result = await runPikkuFunc(processorMeta.pikkuFuncName, {
66
- getAllServices,
67
- data: job.data,
68
- });
69
- logger.debug(`Successfully processed job ${job.id} in queue ${job.queueName}`);
98
+ let result;
99
+ // Main job execution logic wrapped for middleware handling
100
+ const runMain = async () => {
101
+ // Use provided singleton services
102
+ const getAllServices = () => ({
103
+ ...singletonServices,
104
+ ...(createSessionServices
105
+ ? createSessionServices(singletonServices, { queue }, undefined)
106
+ : {}),
107
+ });
108
+ // Execute the pikku function with the job data
109
+ result = await runPikkuFunc(processorMeta.pikkuFuncName, {
110
+ getAllServices,
111
+ data: job.data,
112
+ tags: queueWorker.tags,
113
+ });
114
+ logger.debug(`Successfully processed job ${job.id} in queue ${job.queueName}`);
115
+ };
116
+ // Get middleware for tags and combine with queue-specific middleware
117
+ await runMiddleware(singletonServices, { queue }, addMiddlewareForTags(queueWorker.middleware, queueWorker.tags), runMain);
70
118
  return result;
71
119
  }
72
120
  catch (error) {
@@ -1,4 +1,4 @@
1
- import { PikkuDocs } from '../../types/core.types.js';
1
+ import { PikkuDocs, CorePikkuMiddleware } from '../../types/core.types.js';
2
2
  import { CorePikkuFunctionSessionless } from '../../function/functions.types.js';
3
3
  import { QueueConfigMapping } from './validate-worker-config.js';
4
4
  /**
@@ -144,11 +144,28 @@ export type queueWorkersMeta = Record<string, {
144
144
  /**
145
145
  * Core queue processor definition
146
146
  */
147
- export type CoreQueueWorker<PikkuFunction = CorePikkuFunctionSessionless<any, any>> = {
147
+ export type CoreQueueWorker<PikkuFunction = CorePikkuFunctionSessionless<any, any>, PikkuMiddleware = CorePikkuMiddleware<any>> = {
148
148
  queueName: string;
149
149
  func: PikkuFunction;
150
150
  config?: PikkuWorkerConfig;
151
151
  docs?: PikkuDocs;
152
152
  session?: undefined;
153
153
  tags?: string[];
154
+ middleware?: PikkuMiddleware[];
154
155
  };
156
+ /**
157
+ * Represents a queue interaction object for middleware
158
+ * Provides information and actions for the current queue job execution
159
+ */
160
+ export interface PikkuQueue {
161
+ /** The name of the queue being processed */
162
+ queueName: string;
163
+ /** The current job ID */
164
+ jobId: string;
165
+ /** Update job progress (0-100 or custom value) */
166
+ updateProgress: (progress: number | string | object) => Promise<void>;
167
+ /** Fail the current job with optional reason */
168
+ fail: (reason?: string) => Promise<void>;
169
+ /** Discard/delete the job without retrying */
170
+ discard: (reason?: string) => Promise<void>;
171
+ }
@@ -13,11 +13,14 @@ export declare class PikkuRPCService {
13
13
  };
14
14
  logger: import("../../index.js").Logger;
15
15
  variables: import("../../services/variables-service.js").VariablesService;
16
- http?: import("../http/http.types.js").PikkuHTTP;
16
+ http?: import("../http/http.types.js").PikkuHTTP | undefined;
17
17
  mcp?: import("../mcp/mcp.types.js").PikkuMCP;
18
18
  rpc?: import("./rpc-types.js").PikkuRPC;
19
- userSession?: import("../../index.js").UserSessionService<import("../../types/core.types.js").CoreUserSession> | undefined;
20
19
  channel?: import("../channel/channel.types.js").PikkuChannel<unknown, unknown>;
20
+ scheduledTask?: import("../scheduler/scheduler.types.js").PikkuScheduledTask | undefined;
21
+ queue?: import("../queue/queue.types.js").PikkuQueue | undefined;
22
+ s?: any;
23
+ userSession?: import("../../index.js").UserSessionService<import("../../types/core.types.js").CoreUserSession> | undefined;
21
24
  };
22
25
  }
23
26
  export declare const rpcService: PikkuRPCService;
@@ -1,8 +1,9 @@
1
- import { getErrorResponse } from '../../errors/error-handler.js';
1
+ import { getErrorResponse, PikkuError } from '../../errors/error-handler.js';
2
2
  import { closeSessionServices } from '../../utils.js';
3
3
  import { pikkuState } from '../../pikku-state.js';
4
4
  import { addFunction, runPikkuFunc } from '../../function/function-runner.js';
5
5
  import { rpcService } from '../rpc/rpc-runner.js';
6
+ import { addMiddlewareForTags, runMiddleware } from '../../middleware-runner.js';
6
7
  export const wireScheduler = (scheduledTask) => {
7
8
  const meta = pikkuState('scheduler', 'meta');
8
9
  const taskMeta = meta[scheduledTask.name];
@@ -16,38 +17,62 @@ export const wireScheduler = (scheduledTask) => {
16
17
  }
17
18
  tasks.set(scheduledTask.name, scheduledTask);
18
19
  };
19
- class ScheduledTaskNotFoundError extends Error {
20
+ class ScheduledTaskNotFoundError extends PikkuError {
20
21
  constructor(title) {
21
22
  super(`Scheduled task not found: ${title}`);
22
23
  }
23
24
  }
25
+ class ScheduledTaskSkippedError extends PikkuError {
26
+ constructor(taskName, reason) {
27
+ super(`Scheduled task '${taskName}' was skipped${reason ? `: ${reason}` : ''}`);
28
+ this.name = 'ScheduledTaskSkippedError';
29
+ }
30
+ }
24
31
  export async function runScheduledTask({ name, session, singletonServices, createSessionServices, }) {
25
32
  let sessionServices;
33
+ const task = pikkuState('scheduler', 'tasks').get(name);
34
+ const meta = pikkuState('scheduler', 'meta')[name];
35
+ if (!task) {
36
+ throw new ScheduledTaskNotFoundError(`Scheduled task not found: ${name}`);
37
+ }
38
+ if (!meta) {
39
+ throw new ScheduledTaskNotFoundError(`Scheduled task meta not found: ${name}`);
40
+ }
41
+ // Create the scheduled task interaction object
42
+ const scheduledTask = {
43
+ name,
44
+ schedule: task.schedule,
45
+ executionTime: new Date(),
46
+ skip: (reason) => {
47
+ throw new ScheduledTaskSkippedError(name, reason);
48
+ },
49
+ };
26
50
  try {
27
- const task = pikkuState('scheduler', 'tasks').get(name);
28
- const meta = pikkuState('scheduler', 'meta')[name];
29
- if (!task) {
30
- throw new ScheduledTaskNotFoundError(`Scheduled task not found: ${name}`);
31
- }
32
- if (!meta) {
33
- throw new ScheduledTaskNotFoundError(`Scheduled task meta not found: ${name}`);
34
- }
35
51
  singletonServices.logger.info(`Running schedule task: ${name} | schedule: ${task.schedule}`);
36
- const getAllServices = async () => {
37
- if (createSessionServices) {
38
- const sessionServices = await createSessionServices(singletonServices, {}, session);
39
- return rpcService.injectRPCService({
40
- ...singletonServices,
41
- ...sessionServices,
42
- });
43
- }
44
- return singletonServices;
52
+ let result;
53
+ // Main scheduled task execution logic wrapped for middleware handling
54
+ const runMain = async () => {
55
+ const getAllServices = async () => {
56
+ if (createSessionServices) {
57
+ const services = await createSessionServices(singletonServices, { scheduledTask }, session);
58
+ sessionServices = services;
59
+ return rpcService.injectRPCService({
60
+ ...singletonServices,
61
+ ...services,
62
+ });
63
+ }
64
+ return singletonServices;
65
+ };
66
+ result = await runPikkuFunc(meta.pikkuFuncName, {
67
+ getAllServices,
68
+ session,
69
+ data: undefined,
70
+ tags: task.tags,
71
+ });
45
72
  };
46
- return await runPikkuFunc(meta.pikkuFuncName, {
47
- getAllServices,
48
- session,
49
- data: undefined,
50
- });
73
+ // Get middleware for tags and run middleware
74
+ await runMiddleware(singletonServices, { scheduledTask }, addMiddlewareForTags(task.middleware, task.tags), runMain);
75
+ return result;
51
76
  }
52
77
  catch (e) {
53
78
  const errorResponse = getErrorResponse(e);
@@ -1,4 +1,4 @@
1
- import { PikkuDocs, CoreUserSession } from '../../types/core.types.js';
1
+ import { PikkuDocs, CoreUserSession, CorePikkuMiddleware } from '../../types/core.types.js';
2
2
  import { CorePikkuFunctionSessionless } from '../../function/functions.types.js';
3
3
  /**
4
4
  * Represents metadata for scheduled tasks, including title, schedule, and documentation.
@@ -14,10 +14,25 @@ export type ScheduledTasksMeta<UserSession extends CoreUserSession = any> = Reco
14
14
  /**
15
15
  * Represents a core scheduled task.
16
16
  */
17
- export type CoreScheduledTask<PikkuFunction = CorePikkuFunctionSessionless<void, void>> = {
17
+ export type CoreScheduledTask<PikkuFunction = CorePikkuFunctionSessionless<void, void>, PikkuMiddleware = CorePikkuMiddleware<any>> = {
18
18
  name: string;
19
19
  schedule: string;
20
20
  func: PikkuFunction;
21
21
  docs?: PikkuDocs;
22
22
  tags?: string[];
23
+ middleware?: PikkuMiddleware[];
23
24
  };
25
+ /**
26
+ * Represents a scheduled task interaction object for middleware
27
+ * Provides information about the current scheduled task execution
28
+ */
29
+ export interface PikkuScheduledTask {
30
+ /** The name of the scheduled task being executed */
31
+ name: string;
32
+ /** The cron schedule expression */
33
+ schedule: string;
34
+ /** Current execution timestamp */
35
+ executionTime: Date;
36
+ /** Skip the current task execution */
37
+ skip: (reason?: string) => void;
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/core",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "module": "dist/index.js",
@@ -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 {
@@ -50,6 +50,7 @@ export const runPikkuFunc = async <In = any, Out = any>(
50
50
  permissions: transportPermissions,
51
51
  middleware: transportMiddleware,
52
52
  coerceDataFromSchema,
53
+ tags = [],
53
54
  }: {
54
55
  getAllServices: () => Promise<CoreServices> | CoreServices
55
56
  data: In
@@ -57,6 +58,7 @@ export const runPikkuFunc = async <In = any, Out = any>(
57
58
  permissions?: CorePermissionGroup
58
59
  middleware?: CorePikkuMiddleware[]
59
60
  coerceDataFromSchema?: boolean
61
+ tags?: string[]
60
62
  }
61
63
  ): Promise<Out> => {
62
64
  const funcConfig = pikkuState('function', 'functions').get(funcName)
@@ -92,7 +94,23 @@ export const runPikkuFunc = async <In = any, Out = any>(
92
94
  }
93
95
 
94
96
  let permissioned = true
95
- 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) {
96
114
  permissioned = await verifyPermissions(
97
115
  funcConfig.permissions,
98
116
  allServices,
@@ -100,6 +118,7 @@ export const runPikkuFunc = async <In = any, Out = any>(
100
118
  session
101
119
  )
102
120
  }
121
+
103
122
  if (!permissioned && transportPermissions) {
104
123
  permissioned = await verifyPermissions(
105
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
+ }