@pikku/core 0.8.0 → 0.8.2

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 (39) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/events/channel/channel-handler.js +2 -0
  3. package/dist/events/mcp/index.d.ts +1 -0
  4. package/dist/events/mcp/index.js +1 -0
  5. package/dist/events/mcp/mcp-endpoint-registry.d.ts +47 -0
  6. package/dist/events/mcp/mcp-endpoint-registry.js +112 -0
  7. package/dist/events/queue/index.d.ts +1 -1
  8. package/dist/events/queue/index.js +1 -1
  9. package/dist/events/queue/register-queue-helper.d.ts +1 -1
  10. package/dist/events/queue/register-queue-helper.js +1 -1
  11. package/dist/events/rpc/rpc-runner.d.ts +1 -4
  12. package/dist/function/function-runner.d.ts +3 -2
  13. package/dist/function/function-runner.js +10 -42
  14. package/dist/index.d.ts +5 -1
  15. package/dist/index.js +5 -1
  16. package/dist/services/index.d.ts +2 -2
  17. package/dist/services/index.js +2 -2
  18. package/dist/services/jwt-service.d.ts +1 -1
  19. package/dist/types/core.types.d.ts +1 -10
  20. package/dist/types/core.types.js +1 -1
  21. package/package.json +2 -1
  22. package/src/events/channel/channel-handler.ts +4 -0
  23. package/src/events/http/http-runner.test.ts +2 -3
  24. package/src/events/mcp/index.ts +1 -0
  25. package/src/events/mcp/mcp-endpoint-registry.test.d.ts +1 -0
  26. package/src/events/mcp/mcp-endpoint-registry.test.ts +389 -0
  27. package/src/events/mcp/mcp-endpoint-registry.ts +155 -0
  28. package/src/events/queue/index.ts +1 -1
  29. package/src/events/queue/register-queue-helper.ts +1 -1
  30. package/src/function/function-runner.ts +21 -66
  31. package/src/index.ts +5 -1
  32. package/src/middleware/auth-apikey.ts +1 -1
  33. package/src/middleware/auth-bearer.ts +1 -1
  34. package/src/middleware/auth-cookie.ts +1 -1
  35. package/src/services/index.ts +2 -2
  36. package/src/services/jwt-service.ts +1 -1
  37. package/src/types/core.types.ts +1 -10
  38. package/tsconfig.tsbuildinfo +1 -1
  39. package/lcov.info +0 -4895
@@ -0,0 +1,389 @@
1
+ import { test, describe, beforeEach } from 'node:test'
2
+ import * as assert from 'assert'
3
+ import { MCPEndpointRegistry } from './mcp-endpoint-registry.js'
4
+
5
+ describe('MCPEndpointRegistry', () => {
6
+ let registry: MCPEndpointRegistry
7
+
8
+ beforeEach(() => {
9
+ registry = new MCPEndpointRegistry()
10
+ })
11
+
12
+ describe('constructor', () => {
13
+ test('should initialize with empty maps', () => {
14
+ assert.deepStrictEqual(registry.getTools(), [])
15
+ assert.deepStrictEqual(registry.getResources(), [])
16
+ assert.deepStrictEqual(registry.getPrompts(), [])
17
+ })
18
+ })
19
+
20
+ describe('loadFromMCPJson', () => {
21
+ test('should load tools from MCP JSON', async () => {
22
+ const mockData = {
23
+ tools: [
24
+ {
25
+ name: 'test-tool',
26
+ title: 'Test Tool',
27
+ description: 'A test tool',
28
+ parameters: { type: 'object' },
29
+ returns: { type: 'string' },
30
+ enabled: true,
31
+ },
32
+ ],
33
+ }
34
+
35
+ await registry.loadFromMCPJson(mockData)
36
+
37
+ const tools = registry.getTools()
38
+ assert.strictEqual(tools.length, 1)
39
+ assert.strictEqual(tools[0].name, 'test-tool')
40
+ assert.strictEqual(tools[0].title, 'Test Tool')
41
+ assert.strictEqual(tools[0].description, 'A test tool')
42
+ assert.deepStrictEqual(tools[0].inputSchema, { type: 'object' })
43
+ assert.deepStrictEqual(tools[0].outputSchema, { type: 'string' })
44
+ assert.strictEqual(tools[0].enabled, true)
45
+ })
46
+
47
+ test('should load resources from MCP JSON', async () => {
48
+ const mockData = {
49
+ resources: [
50
+ {
51
+ name: 'test-resource',
52
+ uri: 'file://test.txt',
53
+ description: 'A test resource',
54
+ parameters: { type: 'object' },
55
+ enabled: false,
56
+ },
57
+ ],
58
+ }
59
+
60
+ await registry.loadFromMCPJson(mockData)
61
+
62
+ const resources = registry.getResources()
63
+ assert.strictEqual(resources.length, 1)
64
+ assert.strictEqual(resources[0].title, 'test-resource')
65
+ assert.strictEqual(resources[0].uri, 'file://test.txt')
66
+ assert.strictEqual(resources[0].description, 'A test resource')
67
+ assert.deepStrictEqual(resources[0].inputSchema, { type: 'object' })
68
+ assert.strictEqual(resources[0].enabled, false)
69
+ })
70
+
71
+ test('should load prompts from MCP JSON', async () => {
72
+ const mockData = {
73
+ prompts: [
74
+ {
75
+ name: 'test-prompt',
76
+ description: 'A test prompt',
77
+ arguments: { type: 'object' },
78
+ },
79
+ ],
80
+ }
81
+
82
+ await registry.loadFromMCPJson(mockData)
83
+
84
+ const prompts = registry.getPrompts()
85
+ assert.strictEqual(prompts.length, 1)
86
+ assert.strictEqual(prompts[0].name, 'test-prompt')
87
+ assert.strictEqual(prompts[0].description, 'A test prompt')
88
+ assert.deepStrictEqual(prompts[0].inputSchema, { type: 'object' })
89
+ assert.strictEqual(prompts[0].enabled, true)
90
+ })
91
+
92
+ test('should handle tools with default enabled state', async () => {
93
+ const mockData = {
94
+ tools: [
95
+ {
96
+ name: 'default-tool',
97
+ title: 'Default Tool',
98
+ description: 'Tool with default enabled state',
99
+ },
100
+ ],
101
+ }
102
+
103
+ await registry.loadFromMCPJson(mockData)
104
+
105
+ const tools = registry.getTools()
106
+ assert.strictEqual(tools.length, 1)
107
+ assert.strictEqual(tools[0].enabled, true)
108
+ })
109
+
110
+ test('should handle empty arrays', async () => {
111
+ const mockData = {
112
+ tools: [],
113
+ resources: [],
114
+ prompts: [],
115
+ }
116
+
117
+ await registry.loadFromMCPJson(mockData)
118
+
119
+ assert.deepStrictEqual(registry.getTools(), [])
120
+ assert.deepStrictEqual(registry.getResources(), [])
121
+ assert.deepStrictEqual(registry.getPrompts(), [])
122
+ })
123
+
124
+ test('should handle missing arrays', async () => {
125
+ const mockData = {}
126
+
127
+ await registry.loadFromMCPJson(mockData)
128
+
129
+ assert.deepStrictEqual(registry.getTools(), [])
130
+ assert.deepStrictEqual(registry.getResources(), [])
131
+ assert.deepStrictEqual(registry.getPrompts(), [])
132
+ })
133
+
134
+ test('should handle empty object gracefully', async () => {
135
+ // Test with empty object
136
+ await registry.loadFromMCPJson({})
137
+ assert.deepStrictEqual(registry.getTools(), [])
138
+ assert.deepStrictEqual(registry.getResources(), [])
139
+ assert.deepStrictEqual(registry.getPrompts(), [])
140
+ })
141
+ })
142
+
143
+ describe('meta setters and getters', () => {
144
+ test('should set and check resource meta', () => {
145
+ const meta = {
146
+ 'test-resource': {
147
+ uri: 'file://test.txt',
148
+ title: 'Test Resource',
149
+ description: 'Test description',
150
+ pikkuFuncName: 'testResource',
151
+ inputSchema: null,
152
+ outputSchema: null,
153
+ mimeType: 'text/plain',
154
+ },
155
+ }
156
+ registry.setResourcesMeta(meta)
157
+ assert.strictEqual(registry.hasResource('test-resource'), true)
158
+ assert.strictEqual(registry.hasResource('nonexistent'), false)
159
+ })
160
+
161
+ test('should set and check tool meta', () => {
162
+ const meta = {
163
+ 'test-tool': {
164
+ name: 'test-tool',
165
+ title: 'Test Tool',
166
+ description: 'Test description',
167
+ pikkuFuncName: 'testTool',
168
+ inputSchema: null,
169
+ outputSchema: null,
170
+ },
171
+ }
172
+ registry.setToolsMeta(meta)
173
+ assert.strictEqual(registry.hasTool('test-tool'), true)
174
+ assert.strictEqual(registry.hasTool('nonexistent'), false)
175
+ })
176
+
177
+ test('should set and check prompt meta', () => {
178
+ const meta = {
179
+ 'test-prompt': {
180
+ name: 'test-prompt',
181
+ description: 'Test Prompt',
182
+ pikkuFuncName: 'testPrompt',
183
+ inputSchema: null,
184
+ outputSchema: null,
185
+ arguments: [],
186
+ },
187
+ }
188
+ registry.setPromptsMeta(meta)
189
+ assert.strictEqual(registry.hasPrompt('test-prompt'), true)
190
+ assert.strictEqual(registry.hasPrompt('nonexistent'), false)
191
+ })
192
+ })
193
+
194
+ describe('individual item getters', () => {
195
+ beforeEach(async () => {
196
+ const mockData = {
197
+ tools: [
198
+ {
199
+ name: 'tool1',
200
+ title: 'Tool 1',
201
+ description: 'First tool',
202
+ enabled: true,
203
+ },
204
+ ],
205
+ resources: [
206
+ {
207
+ name: 'resource1',
208
+ uri: 'file://resource1.txt',
209
+ description: 'First resource',
210
+ enabled: true,
211
+ },
212
+ ],
213
+ prompts: [
214
+ {
215
+ name: 'prompt1',
216
+ description: 'First prompt',
217
+ enabled: true,
218
+ },
219
+ ],
220
+ }
221
+
222
+ await registry.loadFromMCPJson(mockData)
223
+ })
224
+
225
+ test('should get tool by name', () => {
226
+ const tool = registry.getTool('tool1')
227
+ assert.strictEqual(tool?.name, 'tool1')
228
+ assert.strictEqual(tool?.title, 'Tool 1')
229
+ assert.strictEqual(registry.getTool('nonexistent'), undefined)
230
+ })
231
+
232
+ test('should get resource by name', () => {
233
+ const resource = registry.getResource('resource1')
234
+ assert.strictEqual(resource?.title, 'resource1')
235
+ assert.strictEqual(resource?.uri, 'file://resource1.txt')
236
+ assert.strictEqual(registry.getResource('nonexistent'), undefined)
237
+ })
238
+
239
+ test('should get prompt by name', () => {
240
+ const prompt = registry.getPrompt('prompt1')
241
+ assert.strictEqual(prompt?.name, 'prompt1')
242
+ assert.strictEqual(prompt?.description, 'First prompt')
243
+ assert.strictEqual(registry.getPrompt('nonexistent'), undefined)
244
+ })
245
+ })
246
+
247
+ describe('getTools filters enabled tools', () => {
248
+ test('should only return enabled tools', async () => {
249
+ const mockData = {
250
+ tools: [
251
+ {
252
+ name: 'enabled-tool',
253
+ title: 'Enabled Tool',
254
+ description: 'This tool is enabled',
255
+ enabled: true,
256
+ },
257
+ {
258
+ name: 'disabled-tool',
259
+ title: 'Disabled Tool',
260
+ description: 'This tool is disabled',
261
+ enabled: false,
262
+ },
263
+ ],
264
+ }
265
+
266
+ await registry.loadFromMCPJson(mockData)
267
+
268
+ const tools = registry.getTools()
269
+ assert.strictEqual(tools.length, 1)
270
+ assert.strictEqual(tools[0].name, 'enabled-tool')
271
+ })
272
+ })
273
+
274
+ describe('enable/disable methods', () => {
275
+ beforeEach(async () => {
276
+ const mockData = {
277
+ tools: [
278
+ {
279
+ name: 'tool1',
280
+ title: 'Tool 1',
281
+ description: 'First tool',
282
+ enabled: true,
283
+ },
284
+ {
285
+ name: 'tool2',
286
+ title: 'Tool 2',
287
+ description: 'Second tool',
288
+ enabled: false,
289
+ },
290
+ ],
291
+ resources: [
292
+ {
293
+ name: 'resource1',
294
+ uri: 'file://resource1.txt',
295
+ description: 'First resource',
296
+ enabled: true,
297
+ },
298
+ ],
299
+ prompts: [
300
+ {
301
+ name: 'prompt1',
302
+ description: 'First prompt',
303
+ enabled: true,
304
+ },
305
+ ],
306
+ }
307
+
308
+ await registry.loadFromMCPJson(mockData)
309
+ })
310
+
311
+ test('should enable/disable tools and return change status', () => {
312
+ let changed = registry.enableTools({ tool1: false, tool2: true })
313
+ assert.strictEqual(changed, true)
314
+
315
+ const tool1 = registry.getTool('tool1')
316
+ const tool2 = registry.getTool('tool2')
317
+ assert.strictEqual(tool1?.enabled, false)
318
+ assert.strictEqual(tool2?.enabled, true)
319
+
320
+ changed = registry.enableTools({ tool1: false, tool2: true })
321
+ assert.strictEqual(changed, false)
322
+ })
323
+
324
+ test('should handle nonexistent tools', () => {
325
+ const changed = registry.enableTools({ nonexistent: true })
326
+ assert.strictEqual(changed, false)
327
+ })
328
+
329
+ test('should enable/disable prompts and return change status', () => {
330
+ let changed = registry.enablePrompts({ prompt1: false })
331
+ assert.strictEqual(changed, true)
332
+
333
+ const prompt1 = registry.getPrompt('prompt1')
334
+ assert.strictEqual(prompt1?.enabled, false)
335
+
336
+ changed = registry.enablePrompts({ prompt1: false })
337
+ assert.strictEqual(changed, false)
338
+ })
339
+
340
+ test('should handle nonexistent prompts', () => {
341
+ const changed = registry.enablePrompts({ nonexistent: true })
342
+ assert.strictEqual(changed, false)
343
+ })
344
+
345
+ test('should enable/disable resources', () => {
346
+ const changed = registry.enableResources({ resource1: false })
347
+ assert.strictEqual(changed, true)
348
+
349
+ const resource1 = registry.getResource('resource1')
350
+ assert.strictEqual(resource1?.enabled, false)
351
+ })
352
+ })
353
+
354
+ describe('edge cases', () => {
355
+ test('should handle null/undefined values in MCP JSON', async () => {
356
+ const mockData = {
357
+ tools: [
358
+ {
359
+ name: 'incomplete-tool',
360
+ title: null,
361
+ description: undefined,
362
+ },
363
+ ],
364
+ }
365
+
366
+ await registry.loadFromMCPJson(mockData)
367
+
368
+ const tools = registry.getTools()
369
+ assert.strictEqual(tools.length, 1)
370
+ assert.strictEqual(tools[0].name, 'incomplete-tool')
371
+ assert.strictEqual(tools[0].title, null)
372
+ assert.strictEqual(tools[0].description, undefined)
373
+ })
374
+
375
+ test('should handle non-array values for tools/resources/prompts', async () => {
376
+ const mockData = {
377
+ tools: 'not an array',
378
+ resources: null,
379
+ prompts: undefined,
380
+ }
381
+
382
+ await registry.loadFromMCPJson(mockData)
383
+
384
+ assert.deepStrictEqual(registry.getTools(), [])
385
+ assert.deepStrictEqual(registry.getResources(), [])
386
+ assert.deepStrictEqual(registry.getPrompts(), [])
387
+ })
388
+ })
389
+ })
@@ -0,0 +1,155 @@
1
+ import { MCPResourceMeta, MCPToolMeta, MCPPromptMeta } from '@pikku/core'
2
+
3
+ export interface MCPToolEndpoint {
4
+ name: string
5
+ title: string
6
+ description: string
7
+ inputSchema?: any
8
+ outputSchema?: any
9
+ enabled: boolean
10
+ }
11
+
12
+ export interface MCPResourceEndpoint {
13
+ uri: string
14
+ title: string
15
+ description: string
16
+ inputSchema?: any
17
+ mimeType?: string
18
+ enabled: boolean
19
+ }
20
+
21
+ export interface MCPPromptEndpoint {
22
+ name: string
23
+ description: string
24
+ inputSchema?: any
25
+ enabled: boolean
26
+ }
27
+
28
+ export class MCPEndpointRegistry {
29
+ private tools: Map<string, MCPToolEndpoint> = new Map()
30
+ private resources: Map<string, MCPResourceEndpoint> = new Map()
31
+ private prompts: Map<string, MCPPromptEndpoint> = new Map()
32
+ private resourcesMeta: MCPResourceMeta = {}
33
+ private toolsMeta: MCPToolMeta = {}
34
+ private promptsMeta: MCPPromptMeta = {}
35
+
36
+ async loadFromMCPJson(mcpData: any): Promise<void> {
37
+ if (mcpData.tools && Array.isArray(mcpData.tools)) {
38
+ for (const tool of mcpData.tools) {
39
+ this.tools.set(tool.name, {
40
+ name: tool.name,
41
+ title: tool.title,
42
+ description: tool.description,
43
+ inputSchema: tool.parameters,
44
+ outputSchema: tool.returns,
45
+ enabled: tool.enabled !== undefined ? tool.enabled : true,
46
+ })
47
+ }
48
+ }
49
+ if (mcpData.resources && Array.isArray(mcpData.resources)) {
50
+ for (const resource of mcpData.resources) {
51
+ this.resources.set(resource.name, {
52
+ title: resource.name,
53
+ uri: resource.uri,
54
+ description: resource.description,
55
+ inputSchema: resource.parameters,
56
+ enabled: resource.enabled !== undefined ? resource.enabled : true,
57
+ })
58
+ }
59
+ }
60
+ if (mcpData.prompts && Array.isArray(mcpData.prompts)) {
61
+ for (const prompt of mcpData.prompts) {
62
+ this.prompts.set(prompt.name, {
63
+ name: prompt.name,
64
+ description: prompt.description,
65
+ inputSchema: prompt.arguments,
66
+ enabled: prompt.enabled !== undefined ? prompt.enabled : true,
67
+ })
68
+ }
69
+ }
70
+ }
71
+
72
+ setResourcesMeta(meta: MCPResourceMeta): void {
73
+ this.resourcesMeta = meta
74
+ }
75
+
76
+ setToolsMeta(meta: MCPToolMeta): void {
77
+ this.toolsMeta = meta
78
+ }
79
+
80
+ setPromptsMeta(meta: MCPPromptMeta): void {
81
+ this.promptsMeta = meta
82
+ }
83
+
84
+ getTools(): MCPToolEndpoint[] {
85
+ return Array.from(this.tools.values()).filter((tool) => tool.enabled)
86
+ }
87
+
88
+ getResources(): MCPResourceEndpoint[] {
89
+ return Array.from(this.resources.values())
90
+ }
91
+
92
+ getPrompts(): MCPPromptEndpoint[] {
93
+ return Array.from(this.prompts.values())
94
+ }
95
+
96
+ getTool(name: string): MCPToolEndpoint | undefined {
97
+ return this.tools.get(name)
98
+ }
99
+
100
+ getResource(name: string): MCPResourceEndpoint | undefined {
101
+ return this.resources.get(name)
102
+ }
103
+
104
+ getPrompt(name: string): MCPPromptEndpoint | undefined {
105
+ return this.prompts.get(name)
106
+ }
107
+
108
+ hasResource(name: string): boolean {
109
+ return name in this.resourcesMeta
110
+ }
111
+
112
+ hasTool(name: string): boolean {
113
+ return name in this.toolsMeta
114
+ }
115
+
116
+ hasPrompt(name: string): boolean {
117
+ return name in this.promptsMeta
118
+ }
119
+
120
+ enableTools(tools: Record<any, boolean>): boolean {
121
+ let changed = false
122
+ for (const [name, enabled] of Object.entries(tools)) {
123
+ const tool = this.tools.get(name)
124
+ if (tool && tool.enabled !== enabled) {
125
+ tool.enabled = enabled
126
+ changed = true
127
+ }
128
+ }
129
+ return changed
130
+ }
131
+
132
+ enablePrompts(prompts: Record<any, boolean>): boolean {
133
+ let changed = false
134
+ for (const [name, enabled] of Object.entries(prompts)) {
135
+ const prompt = this.prompts.get(name)
136
+ if (prompt && prompt.enabled !== enabled) {
137
+ prompt.enabled = enabled
138
+ changed = true
139
+ }
140
+ }
141
+ return changed
142
+ }
143
+
144
+ enableResources(resources: Record<any, boolean>): boolean {
145
+ let changed = false
146
+ for (const [name, enabled] of Object.entries(resources)) {
147
+ const resource = this.resources.get(name)
148
+ if (resource && resource.enabled !== enabled) {
149
+ resource.enabled = enabled
150
+ changed = true
151
+ }
152
+ }
153
+ return changed
154
+ }
155
+ }
@@ -14,5 +14,5 @@ export { validateWorkerConfig } from './validate-worker-config.js'
14
14
  export type { QueueConfigMapping } from './validate-worker-config.js'
15
15
 
16
16
  // Queue registration helper
17
- export { registerqueueWorkers } from './register-queue-helper.js'
17
+ export { registerQueueWorkers } from './register-queue-helper.js'
18
18
  export type { QueueRegistrationCallback } from './register-queue-helper.js'
@@ -23,7 +23,7 @@ export type QueueRegistrationCallback<T = any> = (
23
23
  * @param logger - Optional logger for info/error messages
24
24
  * @returns Record of validation results by queue name
25
25
  */
26
- export async function registerqueueWorkers<T = any>(
26
+ export async function registerQueueWorkers<T = any>(
27
27
  configMappings: QueueConfigMapping,
28
28
  logger: Logger,
29
29
  registerCallback: QueueRegistrationCallback<T>
@@ -3,7 +3,6 @@ import { runMiddleware } from '../middleware-runner.js'
3
3
  import { verifyPermissions } from '../permissions.js'
4
4
  import { pikkuState } from '../pikku-state.js'
5
5
  import { coerceTopLevelDataFromSchema, validateSchema } from '../schema.js'
6
- import { Logger } from '../services/logger.js'
7
6
  import {
8
7
  CoreServices,
9
8
  CoreUserSession,
@@ -14,56 +13,6 @@ import {
14
13
  CorePikkuFunctionConfig,
15
14
  } from './functions.types.js'
16
15
 
17
- // Permission merging function
18
- function mergePermissions(
19
- logger: Logger,
20
- funcName: string,
21
- funcPermissions?: CorePermissionGroup,
22
- transportPermissions?: CorePermissionGroup
23
- ): CorePermissionGroup | undefined {
24
- if (!funcPermissions && !transportPermissions) {
25
- return undefined
26
- }
27
-
28
- if (!funcPermissions) {
29
- return transportPermissions
30
- }
31
-
32
- if (!transportPermissions) {
33
- return funcPermissions
34
- }
35
-
36
- // Start with a copy of function permissions
37
- const mergedPermissions = { ...funcPermissions }
38
-
39
- // Merge in transport permissions
40
- for (const [key, transportValue] of Object.entries(transportPermissions)) {
41
- if (key in mergedPermissions) {
42
- // For permission arrays, concatenate and deduplicate values
43
- if (
44
- Array.isArray(mergedPermissions[key]) &&
45
- Array.isArray(transportValue)
46
- ) {
47
- mergedPermissions[key] = [
48
- ...new Set([...mergedPermissions[key], ...transportValue]),
49
- ]
50
- }
51
- // For other types, warn about conflict and use the more restrictive (transport-level) value
52
- else {
53
- logger.warn(
54
- `Permission conflict on key "${key}" for function "${funcName}". Using transport-level permission.`
55
- )
56
- mergedPermissions[key] = transportValue
57
- }
58
- } else {
59
- // If key doesn't exist in function permissions, add it
60
- mergedPermissions[key] = transportValue
61
- }
62
- }
63
-
64
- return mergedPermissions
65
- }
66
-
67
16
  export const addFunction = (
68
17
  funcName: string,
69
18
  funcConfig: CorePikkuFunctionConfig<any, any>
@@ -91,12 +40,14 @@ export const runPikkuFunc = async <In = any, Out = any>(
91
40
  data,
92
41
  session,
93
42
  permissions: transportPermissions,
43
+ middleware: transportMiddleware,
94
44
  coerceDataFromSchema,
95
45
  }: {
96
46
  getAllServices: () => Promise<CoreServices> | CoreServices
97
47
  data: In
98
48
  session?: CoreUserSession
99
49
  permissions?: CorePermissionGroup
50
+ middleware?: PikkuFunctionMiddleware[]
100
51
  coerceDataFromSchema?: boolean
101
52
  }
102
53
  ): Promise<Out> => {
@@ -132,25 +83,29 @@ export const runPikkuFunc = async <In = any, Out = any>(
132
83
  }
133
84
  }
134
85
 
135
- // Execute permission checks
136
- const mergedPermissions = mergePermissions(
137
- allServices.logger,
138
- funcName,
139
- funcConfig.permissions,
140
- transportPermissions
141
- )
142
- const permissioned = await verifyPermissions(
143
- mergedPermissions,
144
- allServices,
145
- data,
146
- session
147
- )
86
+ let permissioned = true
87
+ if (funcConfig.permissions) {
88
+ permissioned = await verifyPermissions(
89
+ funcConfig.permissions,
90
+ allServices,
91
+ data,
92
+ session
93
+ )
94
+ }
95
+ if (!permissioned && transportPermissions) {
96
+ permissioned = await verifyPermissions(
97
+ transportPermissions,
98
+ allServices,
99
+ data,
100
+ session
101
+ )
102
+ }
148
103
 
149
104
  if (permissioned === false) {
150
105
  throw new ForbiddenError('Permission denied')
151
106
  }
152
107
 
153
- if (funcConfig.middleware) {
108
+ if (transportMiddleware || funcConfig.middleware) {
154
109
  return (await runMiddleware<PikkuFunctionMiddleware>(
155
110
  allServices,
156
111
  {
@@ -158,7 +113,7 @@ export const runPikkuFunc = async <In = any, Out = any>(
158
113
  mcp: allServices.mcp,
159
114
  rpc: allServices.rpc,
160
115
  },
161
- funcConfig.middleware,
116
+ [...(transportMiddleware || []), ...(funcConfig.middleware || [])],
162
117
  async () => await funcConfig.func(allServices, data, session!)
163
118
  )) as Out
164
119
  }
package/src/index.ts CHANGED
@@ -5,7 +5,11 @@ export * from './types/core.types.js'
5
5
  export * from './function/index.js'
6
6
  export * from './pikku-request.js'
7
7
  export * from './pikku-response.js'
8
- export * from './services/index.js'
8
+ export * from './services/logger.js'
9
+ export * from './services/schema-service.js'
10
+ export * from './services/jwt-service.js'
11
+ export * from './services/secret-service.js'
12
+ export * from './services/user-session-service.js'
9
13
  export * from './events/http/index.js'
10
14
  export * from './events/channel/index.js'
11
15
  export * from './events/scheduler/index.js'