@pikku/core 0.9.4 → 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 (38) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/function/function-runner.d.ts +1 -1
  3. package/dist/function/function-runner.js +22 -25
  4. package/dist/index.d.ts +2 -2
  5. package/dist/index.js +2 -2
  6. package/dist/middleware-runner.d.ts +19 -11
  7. package/dist/middleware-runner.js +27 -13
  8. package/dist/permissions.d.ts +19 -3
  9. package/dist/permissions.js +61 -1
  10. package/dist/pikku-state.d.ts +2 -2
  11. package/dist/pikku-state.js +3 -3
  12. package/dist/types/core.types.d.ts +3 -4
  13. package/dist/wirings/channel/local/local-channel-runner.js +5 -2
  14. package/dist/wirings/channel/serverless/serverless-channel-runner.js +5 -2
  15. package/dist/wirings/http/http-runner.js +9 -2
  16. package/dist/wirings/http/http.types.d.ts +4 -4
  17. package/dist/wirings/mcp/mcp-runner.js +8 -3
  18. package/dist/wirings/queue/queue-runner.js +9 -2
  19. package/dist/wirings/rpc/rpc-runner.d.ts +1 -2
  20. package/dist/wirings/scheduler/scheduler-runner.js +8 -3
  21. package/package.json +1 -1
  22. package/src/function/function-runner.test.ts +450 -0
  23. package/src/function/function-runner.ts +23 -43
  24. package/src/index.ts +6 -2
  25. package/src/middleware-runner.test.ts +329 -0
  26. package/src/middleware-runner.ts +38 -17
  27. package/src/permissions.test.ts +403 -42
  28. package/src/permissions.ts +115 -5
  29. package/src/pikku-state.ts +9 -5
  30. package/src/types/core.types.ts +3 -4
  31. package/src/wirings/channel/local/local-channel-runner.ts +5 -5
  32. package/src/wirings/channel/serverless/serverless-channel-runner.ts +5 -5
  33. package/src/wirings/http/http-runner.ts +10 -2
  34. package/src/wirings/http/http.types.ts +4 -4
  35. package/src/wirings/mcp/mcp-runner.ts +8 -3
  36. package/src/wirings/queue/queue-runner.ts +12 -2
  37. package/src/wirings/scheduler/scheduler-runner.ts +10 -3
  38. 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, getPermissionsForTags } 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,8 +47,8 @@ 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
53
  tags = [],
54
54
  }: {
@@ -93,46 +93,26 @@ export const runPikkuFunc = async <In = any, Out = any>(
93
93
  }
94
94
  }
95
95
 
96
- let permissioned = true
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) {
114
- permissioned = await verifyPermissions(
115
- funcConfig.permissions,
116
- allServices,
117
- data,
118
- session
119
- )
120
- }
121
-
122
- if (!permissioned && transportPermissions) {
123
- permissioned = await verifyPermissions(
124
- transportPermissions,
125
- allServices,
126
- data,
127
- session
128
- )
129
- }
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
+ })
130
106
 
131
- if (permissioned === false) {
132
- throw new ForbiddenError('Permission denied')
133
- }
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
+ })
134
114
 
135
- if (transportMiddleware || funcConfig.middleware) {
115
+ if (allMiddleware.length > 0) {
136
116
  return (await runMiddleware<CorePikkuMiddleware>(
137
117
  allServices,
138
118
  {
@@ -140,7 +120,7 @@ export const runPikkuFunc = async <In = any, Out = any>(
140
120
  mcp: allServices.mcp,
141
121
  rpc: allServices.rpc,
142
122
  },
143
- [...(transportMiddleware || []), ...(funcConfig.middleware || [])],
123
+ allMiddleware,
144
124
  async () => await funcConfig.func(allServices, data, session!)
145
125
  )) as Out
146
126
  }
package/src/index.ts CHANGED
@@ -24,9 +24,13 @@ export { pikkuState } from './pikku-state.js'
24
24
  export {
25
25
  runMiddleware,
26
26
  addMiddleware,
27
- addMiddlewareForTags,
27
+ combineMiddleware,
28
28
  } from './middleware-runner.js'
29
- export { addPermission, getPermissionsForTags } from './permissions.js'
29
+ export {
30
+ addPermission,
31
+ getPermissionsForTags,
32
+ runPermissions,
33
+ } from './permissions.js'
30
34
  export { wireHTTP, addHTTPMiddleware } from './wirings/http/http-runner.js'
31
35
  export { wireChannel } from './wirings/channel/channel-runner.js'
32
36
  export { wireScheduler } from './wirings/scheduler/scheduler-runner.js'