@pikku/core 0.9.3 → 0.9.5

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 (52) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/function/function-runner.d.ts +2 -1
  3. package/dist/function/function-runner.js +22 -15
  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 +70 -0
  8. package/dist/middleware-runner.js +105 -2
  9. package/dist/permissions.d.ts +57 -1
  10. package/dist/permissions.js +122 -0
  11. package/dist/pikku-state.d.ts +3 -1
  12. package/dist/pikku-state.js +3 -1
  13. package/dist/types/core.types.d.ts +10 -5
  14. package/dist/wirings/channel/channel-handler.js +1 -0
  15. package/dist/wirings/channel/local/local-channel-runner.js +26 -15
  16. package/dist/wirings/channel/serverless/serverless-channel-runner.js +5 -2
  17. package/dist/wirings/http/http-runner.js +20 -3
  18. package/dist/wirings/http/http.types.d.ts +5 -4
  19. package/dist/wirings/mcp/mcp-runner.js +27 -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 +71 -16
  25. package/dist/wirings/queue/queue.types.d.ts +19 -2
  26. package/dist/wirings/rpc/rpc-runner.d.ts +4 -2
  27. package/dist/wirings/scheduler/scheduler-runner.js +54 -24
  28. package/dist/wirings/scheduler/scheduler.types.d.ts +17 -2
  29. package/package.json +1 -1
  30. package/src/function/function-runner.test.ts +450 -0
  31. package/src/function/function-runner.ts +25 -26
  32. package/src/function/functions.types.ts +3 -1
  33. package/src/index.ts +10 -1
  34. package/src/middleware-runner.test.ts +329 -0
  35. package/src/middleware-runner.ts +131 -2
  36. package/src/permissions.test.ts +403 -42
  37. package/src/permissions.ts +182 -1
  38. package/src/pikku-state.ts +10 -2
  39. package/src/types/core.types.ts +10 -5
  40. package/src/wirings/channel/channel-handler.ts +1 -0
  41. package/src/wirings/channel/local/local-channel-runner.ts +33 -16
  42. package/src/wirings/channel/serverless/serverless-channel-runner.ts +5 -2
  43. package/src/wirings/http/http-runner.ts +21 -3
  44. package/src/wirings/http/http.types.ts +5 -4
  45. package/src/wirings/mcp/mcp-runner.ts +37 -17
  46. package/src/wirings/mcp/mcp.types.ts +7 -0
  47. package/src/wirings/queue/index.ts +2 -0
  48. package/src/wirings/queue/queue-runner.ts +92 -19
  49. package/src/wirings/queue/queue.types.ts +20 -1
  50. package/src/wirings/scheduler/scheduler-runner.ts +80 -32
  51. package/src/wirings/scheduler/scheduler.types.ts +22 -1
  52. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,450 @@
1
+ import { describe, test, beforeEach } from 'node:test'
2
+ import * as assert from 'node:assert'
3
+ import { addFunction, runPikkuFunc } from './function-runner.js'
4
+ import { addMiddleware, addPermission } from '../index.js'
5
+ import { resetPikkuState, pikkuState } from '../pikku-state.js'
6
+ import { CoreServices, CorePikkuMiddleware } from '../types/core.types.js'
7
+ import { CorePermissionGroup } from './functions.types.js'
8
+
9
+ beforeEach(() => {
10
+ resetPikkuState()
11
+ })
12
+
13
+ // Helper function to add function with metadata for tests
14
+ const addTestFunction = (funcName: string, funcConfig: any) => {
15
+ addFunction(funcName, funcConfig)
16
+ pikkuState('function', 'meta')[funcName] = {
17
+ pikkuFuncName: funcName,
18
+ inputSchemaName: null,
19
+ outputSchemaName: null,
20
+ }
21
+ }
22
+
23
+ const mockServices: CoreServices = {
24
+ logger: {
25
+ info: () => {},
26
+ warn: () => {},
27
+ error: () => {},
28
+ debug: () => {},
29
+ },
30
+ } as any
31
+
32
+ describe('runPikkuFunc - Integration Tests', () => {
33
+ test('should execute middleware in correct order: wiringTags → wiringMiddleware → funcMiddleware → funcTags', async () => {
34
+ const executionOrder: string[] = []
35
+ const createMiddleware = (name: string): CorePikkuMiddleware => {
36
+ return async (services, interaction, next) => {
37
+ executionOrder.push(name)
38
+ await next()
39
+ }
40
+ }
41
+
42
+ addMiddleware('wiringTag', [createMiddleware('wiringTag')])
43
+ addMiddleware('funcTag', [createMiddleware('funcTag')])
44
+
45
+ // Register function with middleware and tags
46
+ addTestFunction('testFunc', {
47
+ func: async () => {
48
+ executionOrder.push('main')
49
+ return 'success'
50
+ },
51
+ middleware: [createMiddleware('funcMiddleware')],
52
+ tags: ['funcTag'],
53
+ })
54
+
55
+ const result = await runPikkuFunc('testFunc', {
56
+ getAllServices: () => mockServices,
57
+ data: {},
58
+ middleware: [createMiddleware('wiringMiddleware')],
59
+ tags: ['wiringTag'],
60
+ })
61
+
62
+ assert.equal(result, 'success')
63
+ // Order: wiringTags, wiringMiddleware, funcMiddleware, funcTags
64
+ assert.deepEqual(executionOrder, [
65
+ 'wiringTag',
66
+ 'wiringMiddleware',
67
+ 'funcMiddleware',
68
+ 'funcTag',
69
+ 'main',
70
+ ])
71
+ })
72
+
73
+ test('should execute permissions in correct order: wiringTags → wiringPermissions → funcTags → funcPermissions', async () => {
74
+ const executionOrder: string[] = []
75
+
76
+ // Setup tagged permissions
77
+ const wiringTagPermission = async () => {
78
+ executionOrder.push('wiringTag')
79
+ return true
80
+ }
81
+ const funcTagPermission = async () => {
82
+ executionOrder.push('funcTag')
83
+ return true
84
+ }
85
+
86
+ addPermission('wiringTag', [wiringTagPermission])
87
+ addPermission('funcTag', [funcTagPermission])
88
+
89
+ // Setup direct permissions
90
+ const wiringPermissions: CorePermissionGroup = {
91
+ permissions: [
92
+ async () => {
93
+ executionOrder.push('wiringPermission')
94
+ return true
95
+ },
96
+ ],
97
+ }
98
+ const funcPermissions: CorePermissionGroup = {
99
+ permissions: [
100
+ async () => {
101
+ executionOrder.push('funcPermission')
102
+ return true
103
+ },
104
+ ],
105
+ }
106
+
107
+ // Register function with permissions and tags
108
+ addTestFunction('testFunc', {
109
+ func: async () => {
110
+ executionOrder.push('main')
111
+ return 'success'
112
+ },
113
+ permissions: funcPermissions,
114
+ tags: ['funcTag'],
115
+ })
116
+
117
+ const result = await runPikkuFunc('testFunc', {
118
+ getAllServices: () => mockServices,
119
+ data: {},
120
+ permissions: wiringPermissions,
121
+ tags: ['wiringTag'],
122
+ })
123
+
124
+ assert.equal(result, 'success')
125
+ // Order: wiringTags → wiringPermissions → funcTags → funcPermissions
126
+ assert.deepEqual(executionOrder, [
127
+ 'wiringTag',
128
+ 'wiringPermission',
129
+ 'funcTag',
130
+ 'funcPermission',
131
+ 'main',
132
+ ])
133
+ })
134
+
135
+ test('should throw specific error for wiring tag permission failures', async () => {
136
+ const failingWiringTagPermission = async () => false
137
+
138
+ addPermission('wiringTag', [failingWiringTagPermission])
139
+
140
+ addTestFunction('testFunc', {
141
+ func: async () => 'success',
142
+ })
143
+
144
+ await assert.rejects(
145
+ runPikkuFunc('testFunc', {
146
+ getAllServices: () => mockServices,
147
+ data: {},
148
+ tags: ['wiringTag'],
149
+ }),
150
+ {
151
+ message: 'Permission denied - wiring tag permissions',
152
+ }
153
+ )
154
+ })
155
+
156
+ test('should throw specific error for wiring permission failures', async () => {
157
+ const wiringPermissions: CorePermissionGroup = {
158
+ permissions: [async () => false],
159
+ }
160
+
161
+ addTestFunction('testFunc', {
162
+ func: async () => 'success',
163
+ })
164
+
165
+ await assert.rejects(
166
+ runPikkuFunc('testFunc', {
167
+ getAllServices: () => mockServices,
168
+ data: {},
169
+ permissions: wiringPermissions,
170
+ }),
171
+ {
172
+ message: 'Permission denied - wiring permissions',
173
+ }
174
+ )
175
+ })
176
+
177
+ test('should throw specific error for function tag permission failures', async () => {
178
+ const failingFuncTagPermission = async () => false
179
+
180
+ addPermission('funcTag', [failingFuncTagPermission])
181
+
182
+ addTestFunction('testFunc', {
183
+ func: async () => 'success',
184
+ tags: ['funcTag'],
185
+ })
186
+
187
+ await assert.rejects(
188
+ runPikkuFunc('testFunc', {
189
+ getAllServices: () => mockServices,
190
+ data: {},
191
+ }),
192
+ {
193
+ message: 'Permission denied - function tag permissions',
194
+ }
195
+ )
196
+ })
197
+
198
+ test('should throw specific error for function permission failures', async () => {
199
+ const funcPermissions: CorePermissionGroup = {
200
+ permissions: [async () => false],
201
+ }
202
+
203
+ addTestFunction('testFunc', {
204
+ func: async () => 'success',
205
+ permissions: funcPermissions,
206
+ })
207
+
208
+ await assert.rejects(
209
+ runPikkuFunc('testFunc', {
210
+ getAllServices: () => mockServices,
211
+ data: {},
212
+ }),
213
+ {
214
+ message: 'Permission denied - function permissions',
215
+ }
216
+ )
217
+ })
218
+
219
+ test('should deduplicate middleware when same middleware appears in tags and direct', async () => {
220
+ let executionCount = 0
221
+
222
+ const duplicatedMiddleware: CorePikkuMiddleware = async (
223
+ services,
224
+ interaction,
225
+ next
226
+ ) => {
227
+ executionCount++
228
+ await next()
229
+ }
230
+
231
+ // Add same middleware to tag and directly
232
+ addMiddleware('testTag', [duplicatedMiddleware])
233
+
234
+ addTestFunction('testFunc', {
235
+ func: async () => 'success',
236
+ middleware: [duplicatedMiddleware], // Same middleware directly
237
+ tags: ['testTag'], // Same middleware via tag
238
+ })
239
+
240
+ await runPikkuFunc('testFunc', {
241
+ getAllServices: () => mockServices,
242
+ data: {},
243
+ })
244
+
245
+ // Should only execute once due to deduplication
246
+ assert.equal(executionCount, 1)
247
+ })
248
+
249
+ test('should handle mixed permission types correctly', async () => {
250
+ const executionOrder: string[] = []
251
+
252
+ // Mix of array and object permissions
253
+ const arrayPermission = [
254
+ async () => {
255
+ executionOrder.push('arrayPermission1')
256
+ return false
257
+ },
258
+ async () => {
259
+ executionOrder.push('arrayPermission2')
260
+ return true
261
+ },
262
+ ]
263
+
264
+ const objectPermission: CorePermissionGroup = {
265
+ permissions: [
266
+ async () => {
267
+ executionOrder.push('objectPermission')
268
+ return true
269
+ },
270
+ ],
271
+ }
272
+
273
+ addPermission('mixedTag', arrayPermission)
274
+
275
+ addTestFunction('testFunc', {
276
+ func: async () => {
277
+ executionOrder.push('main')
278
+ return 'success'
279
+ },
280
+ permissions: objectPermission,
281
+ tags: ['mixedTag'],
282
+ })
283
+
284
+ const result = await runPikkuFunc('testFunc', {
285
+ getAllServices: () => mockServices,
286
+ data: {},
287
+ })
288
+
289
+ assert.equal(result, 'success')
290
+ // Should execute tag permissions first, then function permissions
291
+ assert.deepEqual(executionOrder, [
292
+ 'arrayPermission1',
293
+ 'arrayPermission2',
294
+ 'objectPermission',
295
+ 'main',
296
+ ])
297
+ })
298
+
299
+ test('should work without any middleware or permissions', async () => {
300
+ addTestFunction('simpleFunc', {
301
+ func: async () => 'simple success',
302
+ })
303
+
304
+ const result = await runPikkuFunc('simpleFunc', {
305
+ getAllServices: () => mockServices,
306
+ data: {},
307
+ })
308
+
309
+ assert.equal(result, 'simple success')
310
+ })
311
+
312
+ test('should work with only wiring-level middleware and permissions', async () => {
313
+ const executionOrder: string[] = []
314
+
315
+ const wiringMiddleware: CorePikkuMiddleware = async (
316
+ services,
317
+ interaction,
318
+ next
319
+ ) => {
320
+ executionOrder.push('wiringMiddleware')
321
+ await next()
322
+ }
323
+
324
+ const wiringPermissions: CorePermissionGroup = {
325
+ permissions: [
326
+ async () => {
327
+ executionOrder.push('wiringPermission')
328
+ return true
329
+ },
330
+ ],
331
+ }
332
+
333
+ addTestFunction('testFunc', {
334
+ func: async () => {
335
+ executionOrder.push('main')
336
+ return 'success'
337
+ },
338
+ })
339
+
340
+ const result = await runPikkuFunc('testFunc', {
341
+ getAllServices: () => mockServices,
342
+ data: {},
343
+ middleware: [wiringMiddleware],
344
+ permissions: wiringPermissions,
345
+ })
346
+
347
+ assert.equal(result, 'success')
348
+ assert.deepEqual(executionOrder, [
349
+ 'wiringPermission',
350
+ 'wiringMiddleware',
351
+ 'main',
352
+ ])
353
+ })
354
+
355
+ test('should work with only function-level middleware and permissions', async () => {
356
+ const executionOrder: string[] = []
357
+
358
+ const funcMiddleware: CorePikkuMiddleware = async (
359
+ services,
360
+ interaction,
361
+ next
362
+ ) => {
363
+ executionOrder.push('funcMiddleware')
364
+ await next()
365
+ }
366
+
367
+ const funcPermissions: CorePermissionGroup = {
368
+ permissions: [
369
+ async () => {
370
+ executionOrder.push('funcPermission')
371
+ return true
372
+ },
373
+ ],
374
+ }
375
+
376
+ addTestFunction('testFunc', {
377
+ func: async () => {
378
+ executionOrder.push('main')
379
+ return 'success'
380
+ },
381
+ middleware: [funcMiddleware],
382
+ permissions: funcPermissions,
383
+ })
384
+
385
+ const result = await runPikkuFunc('testFunc', {
386
+ getAllServices: () => mockServices,
387
+ data: {},
388
+ })
389
+
390
+ assert.equal(result, 'success')
391
+ assert.deepEqual(executionOrder, [
392
+ 'funcPermission',
393
+ 'funcMiddleware',
394
+ 'main',
395
+ ])
396
+ })
397
+
398
+ test('should pass correct parameters to function', async () => {
399
+ let receivedServices: any
400
+ let receivedData: any
401
+ let receivedSession: any
402
+
403
+ const testData = { test: 'data' }
404
+ const testSession = { userId: 'test-user' }
405
+
406
+ addTestFunction('testFunc', {
407
+ func: async (services, data, session) => {
408
+ receivedServices = services
409
+ receivedData = data
410
+ receivedSession = session
411
+ return 'success'
412
+ },
413
+ })
414
+
415
+ await runPikkuFunc('testFunc', {
416
+ getAllServices: () => mockServices,
417
+ data: testData,
418
+ session: testSession,
419
+ })
420
+
421
+ assert.equal(receivedServices, mockServices)
422
+ assert.equal(receivedData, testData)
423
+ assert.equal(receivedSession, testSession)
424
+ })
425
+
426
+ test('should handle async getAllServices function', async () => {
427
+ let servicesProvided: any
428
+
429
+ addTestFunction('testFunc', {
430
+ func: async (services) => {
431
+ servicesProvided = services
432
+ return 'success'
433
+ },
434
+ })
435
+
436
+ const asyncGetServices = async () => {
437
+ // Simulate async service creation
438
+ await new Promise((resolve) => setTimeout(resolve, 1))
439
+ return mockServices
440
+ }
441
+
442
+ const result = await runPikkuFunc('testFunc', {
443
+ getAllServices: asyncGetServices,
444
+ data: {},
445
+ })
446
+
447
+ assert.equal(result, 'success')
448
+ assert.equal(servicesProvided, mockServices)
449
+ })
450
+ })
@@ -1,6 +1,6 @@
1
1
  import { ForbiddenError } from '../errors/errors.js'
2
- import { runMiddleware } from '../middleware-runner.js'
3
- import { verifyPermissions } from '../permissions.js'
2
+ import { runMiddleware, combineMiddleware } from '../middleware-runner.js'
3
+ import { runPermissions } from '../permissions.js'
4
4
  import { pikkuState } from '../pikku-state.js'
5
5
  import { coerceTopLevelDataFromSchema, validateSchema } from '../schema.js'
6
6
  import {
@@ -47,9 +47,10 @@ export const runPikkuFunc = async <In = any, Out = any>(
47
47
  getAllServices,
48
48
  data,
49
49
  session,
50
- permissions: transportPermissions,
51
- middleware: transportMiddleware,
50
+ permissions: wiringPermissions,
51
+ middleware: wiringMiddleware,
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)
@@ -91,29 +93,26 @@ export const runPikkuFunc = async <In = any, Out = any>(
91
93
  }
92
94
  }
93
95
 
94
- let permissioned = true
95
- if (funcConfig.permissions) {
96
- permissioned = await verifyPermissions(
97
- funcConfig.permissions,
98
- allServices,
99
- data,
100
- session
101
- )
102
- }
103
- if (!permissioned && transportPermissions) {
104
- permissioned = await verifyPermissions(
105
- transportPermissions,
106
- allServices,
107
- data,
108
- session
109
- )
110
- }
96
+ // Run permission checks in the specified order
97
+ await runPermissions({
98
+ wiringTags: tags,
99
+ wiringPermissions,
100
+ funcTags: funcConfig.tags,
101
+ funcPermissions: funcConfig.permissions,
102
+ allServices,
103
+ data,
104
+ session,
105
+ })
111
106
 
112
- if (permissioned === false) {
113
- throw new ForbiddenError('Permission denied')
114
- }
107
+ // Combine all middleware: wiring tags → wiring middleware → func middleware → func tags
108
+ const allMiddleware = combineMiddleware({
109
+ wiringTags: tags,
110
+ wiringMiddleware,
111
+ funcMiddleware: funcConfig.middleware,
112
+ funcTags: funcConfig.tags,
113
+ })
115
114
 
116
- if (transportMiddleware || funcConfig.middleware) {
115
+ if (allMiddleware.length > 0) {
117
116
  return (await runMiddleware<CorePikkuMiddleware>(
118
117
  allServices,
119
118
  {
@@ -121,7 +120,7 @@ export const runPikkuFunc = async <In = any, Out = any>(
121
120
  mcp: allServices.mcp,
122
121
  rpc: allServices.rpc,
123
122
  },
124
- [...(transportMiddleware || []), ...(funcConfig.middleware || [])],
123
+ allMiddleware,
125
124
  async () => await funcConfig.func(allServices, data, session!)
126
125
  )) as Out
127
126
  }
@@ -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,16 @@ 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
+ combineMiddleware,
28
+ } from './middleware-runner.js'
29
+ export {
30
+ addPermission,
31
+ getPermissionsForTags,
32
+ runPermissions,
33
+ } from './permissions.js'
25
34
  export { wireHTTP, addHTTPMiddleware } from './wirings/http/http-runner.js'
26
35
  export { wireChannel } from './wirings/channel/channel-runner.js'
27
36
  export { wireScheduler } from './wirings/scheduler/scheduler-runner.js'