@pikku/core 0.9.11 → 0.10.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 (124) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/errors/errors.d.ts +2 -0
  3. package/dist/errors/errors.js +6 -0
  4. package/dist/function/function-runner.d.ts +15 -9
  5. package/dist/function/function-runner.js +55 -37
  6. package/dist/function/functions.types.d.ts +67 -1
  7. package/dist/function/functions.types.js +43 -1
  8. package/dist/index.d.ts +2 -6
  9. package/dist/index.js +2 -6
  10. package/dist/middleware/auth-apikey.d.ts +19 -11
  11. package/dist/middleware/auth-apikey.js +33 -33
  12. package/dist/middleware/auth-bearer.d.ts +34 -7
  13. package/dist/middleware/auth-bearer.js +53 -36
  14. package/dist/middleware/auth-cookie.d.ts +30 -12
  15. package/dist/middleware/auth-cookie.js +51 -43
  16. package/dist/middleware/timeout.d.ts +6 -2
  17. package/dist/middleware/timeout.js +7 -9
  18. package/dist/middleware-runner.d.ts +46 -43
  19. package/dist/middleware-runner.js +100 -69
  20. package/dist/permissions.d.ts +82 -20
  21. package/dist/permissions.js +176 -62
  22. package/dist/pikku-state.d.ts +46 -1
  23. package/dist/pikku-state.js +16 -1
  24. package/dist/services/content-service.d.ts +2 -3
  25. package/dist/services/index.d.ts +0 -1
  26. package/dist/services/index.js +0 -1
  27. package/dist/services/local-content.d.ts +2 -4
  28. package/dist/types/core.types.d.ts +110 -4
  29. package/dist/types/core.types.js +44 -1
  30. package/dist/utils.d.ts +1 -1
  31. package/dist/wirings/channel/channel-handler.d.ts +2 -2
  32. package/dist/wirings/channel/channel-handler.js +16 -12
  33. package/dist/wirings/channel/channel-runner.d.ts +2 -2
  34. package/dist/wirings/channel/channel-runner.js +6 -24
  35. package/dist/wirings/channel/channel.types.d.ts +15 -14
  36. package/dist/wirings/channel/local/local-channel-runner.js +12 -12
  37. package/dist/wirings/channel/serverless/serverless-channel-runner.js +7 -8
  38. package/dist/wirings/cli/channel/cli-channel-runner.d.ts +12 -0
  39. package/dist/wirings/cli/channel/cli-channel-runner.js +83 -0
  40. package/dist/wirings/cli/channel/index.d.ts +1 -0
  41. package/dist/wirings/cli/channel/index.js +1 -0
  42. package/dist/wirings/cli/cli-runner.d.ts +42 -0
  43. package/dist/wirings/cli/cli-runner.js +352 -0
  44. package/dist/wirings/cli/cli.types.d.ts +204 -0
  45. package/dist/wirings/cli/cli.types.js +1 -0
  46. package/dist/wirings/cli/command-parser.d.ts +19 -0
  47. package/dist/wirings/cli/command-parser.js +403 -0
  48. package/dist/wirings/cli/index.d.ts +5 -0
  49. package/dist/wirings/cli/index.js +5 -0
  50. package/dist/wirings/http/http-runner.d.ts +59 -10
  51. package/dist/wirings/http/http-runner.js +138 -131
  52. package/dist/wirings/http/http.types.d.ts +11 -10
  53. package/dist/wirings/http/index.d.ts +1 -1
  54. package/dist/wirings/http/index.js +1 -1
  55. package/dist/wirings/http/pikku-fetch-http-response.js +3 -1
  56. package/dist/wirings/http/routers/http-router.d.ts +0 -2
  57. package/dist/wirings/http/routers/path-to-regex.d.ts +1 -1
  58. package/dist/wirings/http/routers/path-to-regex.js +5 -34
  59. package/dist/wirings/mcp/mcp-endpoint-registry.d.ts +1 -1
  60. package/dist/wirings/mcp/mcp-runner.d.ts +4 -5
  61. package/dist/wirings/mcp/mcp-runner.js +32 -34
  62. package/dist/wirings/mcp/mcp.types.d.ts +38 -14
  63. package/dist/wirings/queue/queue-runner.d.ts +2 -2
  64. package/dist/wirings/queue/queue-runner.js +25 -27
  65. package/dist/wirings/queue/queue.types.d.ts +6 -5
  66. package/dist/wirings/rpc/index.d.ts +1 -1
  67. package/dist/wirings/rpc/index.js +1 -1
  68. package/dist/wirings/rpc/rpc-runner.d.ts +6 -18
  69. package/dist/wirings/rpc/rpc-runner.js +13 -8
  70. package/dist/wirings/scheduler/scheduler-runner.d.ts +2 -2
  71. package/dist/wirings/scheduler/scheduler-runner.js +38 -35
  72. package/dist/wirings/scheduler/scheduler.types.d.ts +5 -4
  73. package/package.json +9 -7
  74. package/src/errors/errors.ts +6 -0
  75. package/src/factory-functions.test.ts +60 -1
  76. package/src/function/function-runner.test.ts +81 -26
  77. package/src/function/function-runner.ts +89 -57
  78. package/src/function/functions.types.ts +85 -2
  79. package/src/index.ts +6 -19
  80. package/src/middleware/auth-apikey.ts +42 -57
  81. package/src/middleware/auth-bearer.ts +66 -49
  82. package/src/middleware/auth-cookie.ts +63 -82
  83. package/src/middleware/timeout.ts +10 -14
  84. package/src/middleware-runner.test.ts +24 -16
  85. package/src/middleware-runner.ts +115 -87
  86. package/src/permissions.test.ts +26 -27
  87. package/src/permissions.ts +216 -104
  88. package/src/pikku-state.ts +79 -2
  89. package/src/services/content-service.ts +5 -4
  90. package/src/services/index.ts +0 -1
  91. package/src/services/local-content.ts +9 -4
  92. package/src/types/core.types.ts +143 -5
  93. package/src/utils.ts +1 -1
  94. package/src/wirings/channel/channel-handler.ts +18 -21
  95. package/src/wirings/channel/channel-runner.ts +19 -32
  96. package/src/wirings/channel/channel.types.ts +29 -16
  97. package/src/wirings/channel/local/local-channel-runner.ts +25 -15
  98. package/src/wirings/channel/serverless/serverless-channel-runner.ts +15 -17
  99. package/src/wirings/cli/channel/cli-channel-runner.ts +123 -0
  100. package/src/wirings/cli/channel/index.ts +1 -0
  101. package/src/wirings/cli/cli-runner.test.ts +403 -0
  102. package/src/wirings/cli/cli-runner.ts +539 -0
  103. package/src/wirings/cli/cli.types.ts +350 -0
  104. package/src/wirings/cli/command-parser.test.ts +445 -0
  105. package/src/wirings/cli/command-parser.ts +504 -0
  106. package/src/wirings/cli/index.ts +13 -0
  107. package/src/wirings/http/http-runner.test.ts +8 -7
  108. package/src/wirings/http/http-runner.ts +165 -158
  109. package/src/wirings/http/http.types.ts +58 -11
  110. package/src/wirings/http/index.ts +7 -1
  111. package/src/wirings/http/pikku-fetch-http-response.ts +3 -1
  112. package/src/wirings/http/routers/http-router.ts +0 -2
  113. package/src/wirings/http/routers/path-to-regex.test.ts +4 -5
  114. package/src/wirings/http/routers/path-to-regex.ts +6 -43
  115. package/src/wirings/mcp/mcp-endpoint-registry.ts +1 -1
  116. package/src/wirings/mcp/mcp-runner.ts +58 -55
  117. package/src/wirings/mcp/mcp.types.ts +65 -14
  118. package/src/wirings/queue/queue-runner.ts +44 -47
  119. package/src/wirings/queue/queue.types.ts +10 -6
  120. package/src/wirings/rpc/index.ts +1 -1
  121. package/src/wirings/rpc/rpc-runner.ts +27 -9
  122. package/src/wirings/scheduler/scheduler-runner.ts +58 -56
  123. package/src/wirings/scheduler/scheduler.types.ts +10 -3
  124. package/tsconfig.tsbuildinfo +1 -1
@@ -1,6 +1,11 @@
1
1
  import { test } from 'node:test'
2
2
  import assert from 'node:assert'
3
- import { pikkuPermission, pikkuMiddleware } from './index.js'
3
+ import {
4
+ pikkuPermission,
5
+ pikkuMiddleware,
6
+ pikkuPermissionFactory,
7
+ pikkuMiddlewareFactory,
8
+ } from './index.js'
4
9
 
5
10
  test('pikkuPermission factory function', async () => {
6
11
  const permission = pikkuPermission(async ({ logger }, data, session) => {
@@ -35,3 +40,57 @@ test('pikkuMiddleware returns the same function', async () => {
35
40
 
36
41
  assert.strictEqual(wrappedFn, originalFn)
37
42
  })
43
+
44
+ test('pikkuPermissionFactory creates a factory function', async () => {
45
+ const factory = pikkuPermissionFactory<{ role: string }>(({ role }) => {
46
+ return pikkuPermission(async ({ logger }, data, session) => {
47
+ return session?.role === role
48
+ })
49
+ })
50
+
51
+ assert.strictEqual(typeof factory, 'function')
52
+ assert.strictEqual(factory.length, 1) // input parameter
53
+
54
+ // Call the factory to get a permission
55
+ const permission = factory({ role: 'admin' })
56
+ assert.strictEqual(typeof permission, 'function')
57
+ assert.strictEqual(permission.length, 3) // services, data, session
58
+ })
59
+
60
+ test('pikkuMiddlewareFactory creates a factory function', async () => {
61
+ const factory = pikkuMiddlewareFactory<{ message: string }>(({ message }) => {
62
+ return pikkuMiddleware(async ({ logger }, interactions, next) => {
63
+ logger.info(message)
64
+ await next()
65
+ })
66
+ })
67
+
68
+ assert.strictEqual(typeof factory, 'function')
69
+ assert.strictEqual(factory.length, 1) // input parameter
70
+
71
+ // Call the factory to get middleware
72
+ const middleware = factory({ message: 'test' })
73
+ assert.strictEqual(typeof middleware, 'function')
74
+ assert.strictEqual(middleware.length, 3) // services, interactions, next
75
+ })
76
+
77
+ test('pikkuPermissionFactory returns the same factory', async () => {
78
+ const originalFactory = ({ role }) =>
79
+ pikkuPermission(async ({ logger }, data, session) => {
80
+ return session?.role === role
81
+ })
82
+ const wrappedFactory = pikkuPermissionFactory(originalFactory)
83
+
84
+ assert.strictEqual(wrappedFactory, originalFactory)
85
+ })
86
+
87
+ test('pikkuMiddlewareFactory returns the same factory', async () => {
88
+ const originalFactory = ({ message }) =>
89
+ pikkuMiddleware(async ({ logger }, interactions, next) => {
90
+ logger.info(message)
91
+ await next()
92
+ })
93
+ const wrappedFactory = pikkuMiddlewareFactory(originalFactory)
94
+
95
+ assert.strictEqual(wrappedFactory, originalFactory)
96
+ })
@@ -17,14 +17,24 @@ beforeEach(() => {
17
17
  // Helper function to add function with metadata for tests
18
18
  const addTestFunction = (funcName: string, funcConfig: any) => {
19
19
  addFunction(funcName, funcConfig)
20
+ // Convert tags to middleware metadata
21
+ const middleware = funcConfig.tags
22
+ ? funcConfig.tags.map((tag: string) => ({ type: 'tag' as const, tag }))
23
+ : undefined
24
+ // Convert tags to permissions metadata
25
+ const permissions = funcConfig.tags
26
+ ? funcConfig.tags.map((tag: string) => ({ type: 'tag' as const, tag }))
27
+ : undefined
20
28
  pikkuState('function', 'meta')[funcName] = {
21
29
  pikkuFuncName: funcName,
22
30
  inputSchemaName: null,
23
31
  outputSchemaName: null,
32
+ middleware,
33
+ permissions,
24
34
  }
25
35
  }
26
36
 
27
- const mockServices: CoreServices = {
37
+ const mockSingletonServices = {
28
38
  logger: {
29
39
  info: () => {},
30
40
  warn: () => {},
@@ -33,8 +43,12 @@ const mockServices: CoreServices = {
33
43
  },
34
44
  } as any
35
45
 
46
+ const mockServices: CoreServices = {
47
+ ...mockSingletonServices,
48
+ } as any
49
+
36
50
  describe('runPikkuFunc - Integration Tests', () => {
37
- test('should execute middleware in correct order: wiringTags → wiringMiddleware → funcMiddlewarefuncTags', async () => {
51
+ test('should execute middleware in correct order: wiringTags → wiringMiddleware → funcTagsfuncMiddleware', async () => {
38
52
  const executionOrder: string[] = []
39
53
  const createMiddleware = (name: string): CorePikkuMiddleware => {
40
54
  return async (services, interaction, next) => {
@@ -61,20 +75,23 @@ describe('runPikkuFunc - Integration Tests', () => {
61
75
  Math.random().toString(),
62
76
  'testFunc',
63
77
  {
78
+ singletonServices: mockSingletonServices,
64
79
  getAllServices: () => mockServices,
65
- data: {},
66
- middleware: [createMiddleware('wiringMiddleware')],
67
- tags: ['wiringTag'],
80
+ data: () => ({}),
81
+ wireMiddleware: [createMiddleware('wiringMiddleware')],
82
+ inheritedMiddleware: [{ type: 'tag', tag: 'wiringTag' }],
83
+ auth: false,
84
+ interaction: {},
68
85
  }
69
86
  )
70
87
 
71
88
  assert.equal(result, 'success')
72
- // Order: wiringTags, wiringMiddleware, funcMiddleware, funcTags
89
+ // Order: wireInheritedMiddleware (tags), wireMiddleware, funcInheritedMiddleware (tags), funcMiddleware
73
90
  assert.deepEqual(executionOrder, [
74
91
  'wiringTag',
75
92
  'wiringMiddleware',
76
- 'funcMiddleware',
77
93
  'funcTag',
94
+ 'funcMiddleware',
78
95
  'main',
79
96
  ])
80
97
  })
@@ -128,10 +145,13 @@ describe('runPikkuFunc - Integration Tests', () => {
128
145
  Math.random().toString(),
129
146
  'testFunc',
130
147
  {
148
+ singletonServices: mockSingletonServices,
131
149
  getAllServices: () => mockServices,
132
- data: {},
133
- permissions: wiringPermissions,
150
+ data: () => ({}),
151
+ wirePermissions: wiringPermissions,
134
152
  tags: ['wiringTag'],
153
+ auth: false,
154
+ interaction: {},
135
155
  }
136
156
  )
137
157
 
@@ -157,9 +177,12 @@ describe('runPikkuFunc - Integration Tests', () => {
157
177
 
158
178
  await assert.rejects(
159
179
  runPikkuFunc(PikkuWiringTypes.rpc, Math.random().toString(), 'testFunc', {
180
+ singletonServices: mockSingletonServices,
160
181
  getAllServices: () => mockServices,
161
- data: {},
182
+ data: () => ({}),
162
183
  tags: ['wiringTag'],
184
+ auth: false,
185
+ interaction: {},
163
186
  }),
164
187
  {
165
188
  message: 'Permission denied',
@@ -178,9 +201,12 @@ describe('runPikkuFunc - Integration Tests', () => {
178
201
 
179
202
  await assert.rejects(
180
203
  runPikkuFunc(PikkuWiringTypes.rpc, Math.random().toString(), 'testFunc', {
204
+ singletonServices: mockSingletonServices,
181
205
  getAllServices: () => mockServices,
182
- data: {},
183
- permissions: wiringPermissions,
206
+ data: () => ({}),
207
+ wirePermissions: wiringPermissions,
208
+ auth: false,
209
+ interaction: {},
184
210
  }),
185
211
  {
186
212
  message: 'Permission denied',
@@ -200,8 +226,11 @@ describe('runPikkuFunc - Integration Tests', () => {
200
226
 
201
227
  await assert.rejects(
202
228
  runPikkuFunc(PikkuWiringTypes.rpc, Math.random().toString(), 'testFunc', {
229
+ singletonServices: mockSingletonServices,
203
230
  getAllServices: () => mockServices,
204
- data: {},
231
+ data: () => ({}),
232
+ auth: false,
233
+ interaction: {},
205
234
  }),
206
235
  {
207
236
  message: 'Permission denied',
@@ -221,8 +250,11 @@ describe('runPikkuFunc - Integration Tests', () => {
221
250
 
222
251
  await assert.rejects(
223
252
  runPikkuFunc(PikkuWiringTypes.rpc, Math.random().toString(), 'testFunc', {
253
+ singletonServices: mockSingletonServices,
224
254
  getAllServices: () => mockServices,
225
- data: {},
255
+ data: () => ({}),
256
+ auth: false,
257
+ interaction: {},
226
258
  }),
227
259
  {
228
260
  message: 'Permission denied',
@@ -256,8 +288,11 @@ describe('runPikkuFunc - Integration Tests', () => {
256
288
  Math.random().toString(),
257
289
  'testFunc',
258
290
  {
291
+ singletonServices: mockSingletonServices,
259
292
  getAllServices: () => mockServices,
260
- data: {},
293
+ data: () => ({}),
294
+ auth: false,
295
+ interaction: {},
261
296
  }
262
297
  )
263
298
 
@@ -305,8 +340,11 @@ describe('runPikkuFunc - Integration Tests', () => {
305
340
  Math.random().toString(),
306
341
  'testFunc',
307
342
  {
343
+ singletonServices: mockSingletonServices,
308
344
  getAllServices: () => mockServices,
309
- data: {},
345
+ data: () => ({}),
346
+ auth: false,
347
+ interaction: {},
310
348
  }
311
349
  )
312
350
 
@@ -330,8 +368,11 @@ describe('runPikkuFunc - Integration Tests', () => {
330
368
  Math.random().toString(),
331
369
  'simpleFunc',
332
370
  {
371
+ singletonServices: mockSingletonServices,
333
372
  getAllServices: () => mockServices,
334
- data: {},
373
+ data: () => ({}),
374
+ auth: false,
375
+ interaction: {},
335
376
  }
336
377
  )
337
378
 
@@ -371,17 +412,21 @@ describe('runPikkuFunc - Integration Tests', () => {
371
412
  Math.random().toString(),
372
413
  'testFunc',
373
414
  {
415
+ singletonServices: mockSingletonServices,
374
416
  getAllServices: () => mockServices,
375
- data: {},
376
- middleware: [wiringMiddleware],
377
- permissions: wiringPermissions,
417
+ data: () => ({}),
418
+ wireMiddleware: [wiringMiddleware],
419
+ wirePermissions: wiringPermissions,
420
+ auth: false,
421
+ interaction: {},
378
422
  }
379
423
  )
380
424
 
381
425
  assert.equal(result, 'success')
426
+ // Permissions run after middleware (middleware can set/modify session)
382
427
  assert.deepEqual(executionOrder, [
383
- 'wiringPermission',
384
428
  'wiringMiddleware',
429
+ 'wiringPermission',
385
430
  'main',
386
431
  ])
387
432
  })
@@ -421,15 +466,19 @@ describe('runPikkuFunc - Integration Tests', () => {
421
466
  Math.random().toString(),
422
467
  'testFunc',
423
468
  {
469
+ singletonServices: mockSingletonServices,
424
470
  getAllServices: () => mockServices,
425
- data: {},
471
+ data: () => ({}),
472
+ auth: false,
473
+ interaction: {},
426
474
  }
427
475
  )
428
476
 
429
477
  assert.equal(result, 'success')
478
+ // Permissions run after middleware (middleware can set/modify session)
430
479
  assert.deepEqual(executionOrder, [
431
- 'funcPermission',
432
480
  'funcMiddleware',
481
+ 'funcPermission',
433
482
  'main',
434
483
  ])
435
484
  })
@@ -456,9 +505,12 @@ describe('runPikkuFunc - Integration Tests', () => {
456
505
  Math.random().toString(),
457
506
  'testFunc',
458
507
  {
508
+ singletonServices: mockSingletonServices,
459
509
  getAllServices: () => mockServices,
460
- data: testData,
461
- session: testSession,
510
+ data: () => testData,
511
+ userSession: { get: () => testSession, set: () => {} } as any,
512
+ auth: false,
513
+ interaction: {},
462
514
  }
463
515
  )
464
516
 
@@ -488,8 +540,11 @@ describe('runPikkuFunc - Integration Tests', () => {
488
540
  Math.random().toString(),
489
541
  'testFunc',
490
542
  {
543
+ singletonServices: mockSingletonServices,
491
544
  getAllServices: asyncGetServices,
492
- data: {},
545
+ data: () => ({}),
546
+ auth: false,
547
+ interaction: {},
493
548
  }
494
549
  )
495
550
 
@@ -1,4 +1,3 @@
1
- import { ForbiddenError } from '../errors/errors.js'
2
1
  import { runMiddleware, combineMiddleware } from '../middleware-runner.js'
3
2
  import { runPermissions } from '../permissions.js'
4
3
  import { pikkuState } from '../pikku-state.js'
@@ -8,24 +7,23 @@ import {
8
7
  CoreUserSession,
9
8
  CorePikkuMiddleware,
10
9
  PikkuWiringTypes,
10
+ CoreSingletonServices,
11
+ PikkuInteraction,
12
+ MiddlewareMetadata,
13
+ PermissionMetadata,
11
14
  } from '../types/core.types.js'
12
15
  import {
13
16
  CorePermissionGroup,
14
- CorePikkuFunction,
15
17
  CorePikkuFunctionConfig,
16
- CorePikkuFunctionSessionless,
18
+ CorePikkuPermission,
17
19
  } from './functions.types.js'
20
+ import { UserSessionService } from '../services/user-session-service.js'
21
+ import { ForbiddenError } from '../errors/errors.js'
18
22
 
19
23
  export const addFunction = (
20
24
  funcName: string,
21
- funcConfig:
22
- | CorePikkuFunctionConfig<any, any>
23
- | CorePikkuFunctionSessionless<any, any>
24
- | CorePikkuFunction<any, any>
25
+ funcConfig: CorePikkuFunctionConfig<any, any>
25
26
  ) => {
26
- if (funcConfig instanceof Function) {
27
- funcConfig = { func: funcConfig }
28
- }
29
27
  pikkuState('function', 'functions').set(funcName, funcConfig)
30
28
  }
31
29
 
@@ -48,20 +46,32 @@ export const runPikkuFunc = async <In = any, Out = any>(
48
46
  funcName: string,
49
47
  {
50
48
  getAllServices,
49
+ singletonServices,
51
50
  data,
52
- session,
53
- permissions: wiringPermissions,
54
- middleware: wiringMiddleware,
51
+ userSession,
52
+ auth: wiringAuth,
53
+ inheritedMiddleware,
54
+ wireMiddleware,
55
+ inheritedPermissions,
56
+ wirePermissions,
55
57
  coerceDataFromSchema,
56
58
  tags = [],
59
+ interaction,
57
60
  }: {
58
- getAllServices: () => Promise<CoreServices> | CoreServices
59
- data: In
60
- session?: CoreUserSession
61
- permissions?: CorePermissionGroup
62
- middleware?: CorePikkuMiddleware[]
61
+ singletonServices: CoreSingletonServices
62
+ getAllServices: (
63
+ session?: CoreUserSession
64
+ ) => Promise<CoreServices> | CoreServices
65
+ userSession?: UserSessionService<CoreUserSession>
66
+ data: () => Promise<In> | In
67
+ auth?: boolean
68
+ inheritedMiddleware?: MiddlewareMetadata[]
69
+ wireMiddleware?: CorePikkuMiddleware[]
70
+ inheritedPermissions?: PermissionMetadata[]
71
+ wirePermissions?: CorePermissionGroup | CorePikkuPermission[]
63
72
  coerceDataFromSchema?: boolean
64
73
  tags?: string[]
74
+ interaction: PikkuInteraction
65
75
  }
66
76
  ): Promise<Out> => {
67
77
  const funcConfig = pikkuState('function', 'functions').get(funcName)
@@ -73,60 +83,82 @@ export const runPikkuFunc = async <In = any, Out = any>(
73
83
  throw new Error(`Function meta not found: ${funcName}`)
74
84
  }
75
85
 
76
- if (funcConfig.auth && !session) {
77
- throw new ForbiddenError(
78
- `Function ${funcName} requires authentication even though transport does not`
79
- )
80
- }
86
+ // Convert tags to PermissionMetadata and merge with inheritedPermissions
87
+ const mergedInheritedPermissions: PermissionMetadata[] = [
88
+ ...(inheritedPermissions || []),
89
+ ...(tags?.map((tag) => ({ type: 'tag' as const, tag })) || []),
90
+ ]
81
91
 
82
- const allServices = await getAllServices()
92
+ // Helper function to run permissions and execute the function
93
+ const executeFunction = async () => {
94
+ const session = userSession?.get()
95
+ if (wiringAuth === true || funcConfig.auth === true) {
96
+ // This means it was explicitly enabled in either wiring or function and has to be respected
97
+ if (!session) {
98
+ throw new ForbiddenError('Authentication required')
99
+ }
100
+ }
101
+ if (wiringAuth === undefined && funcConfig.auth === undefined) {
102
+ // We always default to requiring auth unless explicitly disabled
103
+ if (!session) {
104
+ throw new ForbiddenError('Authentication required')
105
+ }
106
+ }
83
107
 
84
- const inputSchemaName = funcMeta.inputSchemaName
85
- if (inputSchemaName) {
86
- // Validate request data against the defined schema, if any
87
- await validateSchema(
88
- allServices.logger,
89
- allServices.schema,
90
- inputSchemaName,
91
- data
92
- )
93
- // Coerce (top level) query string parameters or date objects if specified by the schema
94
- if (coerceDataFromSchema) {
95
- coerceTopLevelDataFromSchema(inputSchemaName, data)
108
+ // Evaluate the data from the lazy function
109
+ const actualData = await data()
110
+
111
+ // Validate and coerce data if schema is defined
112
+ const inputSchemaName = funcMeta.inputSchemaName
113
+ if (inputSchemaName) {
114
+ // Validate request data against the defined schema, if any
115
+ await validateSchema(
116
+ singletonServices.logger,
117
+ singletonServices.schema,
118
+ inputSchemaName,
119
+ actualData
120
+ )
121
+ // Coerce (top level) query string parameters or date objects if specified by the schema
122
+ if (coerceDataFromSchema) {
123
+ coerceTopLevelDataFromSchema(inputSchemaName, actualData)
124
+ }
96
125
  }
97
- }
98
126
 
99
- // Run permission checks in the specified order
100
- await runPermissions(wireType, wireId, {
101
- wiringTags: tags,
102
- wiringPermissions,
103
- funcTags: funcConfig.tags,
104
- funcPermissions: funcConfig.permissions,
105
- allServices,
106
- data,
107
- session,
108
- })
127
+ const allServices = await getAllServices(session)
128
+
129
+ // Run permissions check with combined permissions: inheritedPermissions (including tags) → wirePermissions → funcPermissions
130
+ await runPermissions(wireType, wireId, {
131
+ wireInheritedPermissions: mergedInheritedPermissions,
132
+ wirePermissions: wirePermissions,
133
+ funcInheritedPermissions: funcMeta.permissions,
134
+ funcPermissions: funcConfig.permissions,
135
+ allServices,
136
+ data: actualData,
137
+ session,
138
+ })
109
139
 
110
- // Combine all middleware: wiring tags → wiring middleware → func middleware → func tags
140
+ return await funcConfig.func(allServices, actualData, session!)
141
+ }
142
+
143
+ // Combine all middleware: inheritedMiddleware → wireMiddleware → funcMiddleware
111
144
  const allMiddleware = combineMiddleware(wireType, wireId, {
112
- wiringTags: tags,
113
- wiringMiddleware,
145
+ wireInheritedMiddleware: inheritedMiddleware,
146
+ wireMiddleware,
147
+ funcInheritedMiddleware: funcMeta.middleware,
114
148
  funcMiddleware: funcConfig.middleware,
115
- funcTags: funcConfig.tags,
116
149
  })
117
150
 
118
151
  if (allMiddleware.length > 0) {
119
152
  return (await runMiddleware<CorePikkuMiddleware>(
120
- allServices,
121
153
  {
122
- http: allServices.http,
123
- mcp: allServices.mcp,
124
- rpc: allServices.rpc,
154
+ ...singletonServices,
155
+ userSession,
125
156
  },
157
+ interaction,
126
158
  allMiddleware,
127
- async () => await funcConfig.func(allServices, data, session!)
159
+ executeFunction
128
160
  )) as Out
129
161
  }
130
162
 
131
- return (await funcConfig.func(allServices, data, session!)) as Out
163
+ return (await executeFunction()) as Out
132
164
  }
@@ -12,6 +12,7 @@ import type {
12
12
  *
13
13
  * @template In - The input type.
14
14
  * @template Out - The output type.
15
+ * @template ChannelData - The channel data type.
15
16
  * @template Services - The services type, defaults to `CoreServices`.
16
17
  * @template Session - The session type, defaults to `CoreUserSession`.
17
18
  */
@@ -39,6 +40,7 @@ export type CorePikkuFunction<
39
40
  *
40
41
  * @template In - The input type.
41
42
  * @template Out - The output type.
43
+ * @template ChannelData - The channel data type.
42
44
  * @template Services - The services type, defaults to `CoreServices`.
43
45
  * @template Session - The session type, defaults to `CoreUserSession`.
44
46
  */
@@ -74,17 +76,98 @@ export type CorePikkuPermission<
74
76
  Session extends CoreUserSession = CoreUserSession,
75
77
  > = (services: Services, data: In, session?: Session) => Promise<boolean>
76
78
 
79
+ /**
80
+ * Configuration object for creating a permission with metadata
81
+ *
82
+ * @template In - The input type.
83
+ * @template Services - The services type, defaults to `CoreServices`.
84
+ * @template Session - The session type, defaults to `CoreUserSession`.
85
+ */
86
+ export type CorePikkuPermissionConfig<
87
+ In = any,
88
+ Services extends CoreSingletonServices = CoreServices,
89
+ Session extends CoreUserSession = CoreUserSession,
90
+ > = {
91
+ /** The permission function */
92
+ func: CorePikkuPermission<In, Services, Session>
93
+ /** Optional human-readable name for the permission */
94
+ name?: string
95
+ /** Optional description of what the permission checks */
96
+ description?: string
97
+ }
98
+
77
99
  /**
78
100
  * Factory function for creating permissions with tree-shaking support
101
+ * Supports both direct function and configuration object syntax
102
+ *
103
+ * @example
104
+ * ```typescript
105
+ * // Direct function syntax
106
+ * export const adminPermission = pikkuPermission(
107
+ * async ({ logger }, data, session) => {
108
+ * return session?.role === 'admin'
109
+ * }
110
+ * )
111
+ *
112
+ * // Configuration object syntax with metadata
113
+ * export const adminPermission = pikkuPermission({
114
+ * name: 'Admin Permission',
115
+ * description: 'Checks if user has admin role',
116
+ * func: async ({ logger }, data, session) => {
117
+ * return session?.role === 'admin'
118
+ * }
119
+ * })
120
+ * ```
79
121
  */
80
122
  export const pikkuPermission = <
81
123
  In = any,
82
124
  Services extends CoreSingletonServices = CoreServices,
83
125
  Session extends CoreUserSession = CoreUserSession,
84
126
  >(
85
- permission: CorePikkuPermission<In, Services, Session>
127
+ permission:
128
+ | CorePikkuPermission<In, Services, Session>
129
+ | CorePikkuPermissionConfig<In, Services, Session>
86
130
  ): CorePikkuPermission<In, Services, Session> => {
87
- return permission
131
+ return typeof permission === 'function' ? permission : permission.func
132
+ }
133
+
134
+ /**
135
+ * A factory function that takes input and returns a permission
136
+ * Used when permissions need configuration/input parameters
137
+ *
138
+ * @template In - The input type for the factory.
139
+ * @template Services - The services type, defaults to `CoreServices`.
140
+ * @template Session - The session type, defaults to `CoreUserSession`.
141
+ */
142
+ export type CorePikkuPermissionFactory<
143
+ In = any,
144
+ Services extends CoreSingletonServices = CoreServices,
145
+ Session extends CoreUserSession = CoreUserSession,
146
+ > = (input: In) => CorePikkuPermission<any, Services, Session>
147
+
148
+ /**
149
+ * Factory function for creating permission factories
150
+ * Use this when your permission needs configuration/input parameters
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * export const requireRole = pikkuPermissionFactory<{ role: string }>(({
155
+ * role
156
+ * }) => {
157
+ * return pikkuPermission(async ({ logger }, data, session) => {
158
+ * if (!session || session.role !== role) {
159
+ * logger.warn(`Permission denied: required role '${role}'`)
160
+ * return false
161
+ * }
162
+ * return true
163
+ * })
164
+ * })
165
+ * ```
166
+ */
167
+ export const pikkuPermissionFactory = <In = any>(
168
+ factory: CorePikkuPermissionFactory<In>
169
+ ): CorePikkuPermissionFactory<In> => {
170
+ return factory
88
171
  }
89
172
 
90
173
  export type CorePermissionGroup<PikkuPermission = CorePikkuPermission<any>> =
package/src/index.ts CHANGED
@@ -16,28 +16,15 @@ export * from './wirings/scheduler/index.js'
16
16
  export * from './wirings/rpc/index.js'
17
17
  export * from './wirings/queue/index.js'
18
18
  export * from './wirings/mcp/index.js'
19
+ export * from './wirings/cli/index.js'
19
20
  export * from './errors/index.js'
20
21
  export * from './middleware/index.js'
21
22
  export * from './utils.js'
22
23
  export * from './time-utils.js'
23
24
  export { pikkuState } from './pikku-state.js'
24
- export { addMiddleware, runMiddleware } from './middleware-runner.js'
25
- export { addPermission } from './permissions.js'
26
- export { wireHTTP, addHTTPMiddleware } from './wirings/http/http-runner.js'
27
- export { wireChannel } from './wirings/channel/channel-runner.js'
28
- export { wireScheduler } from './wirings/scheduler/scheduler-runner.js'
29
- export { wireQueueWorker } from './wirings/queue/queue-runner.js'
30
25
  export {
31
- wireMCPResource,
32
- wireMCPTool,
33
- wireMCPPrompt,
34
- runMCPResource,
35
- runMCPTool,
36
- runMCPPrompt,
37
- getMCPTools,
38
- getMCPResources,
39
- getMCPPrompts,
40
- getMCPResourcesMeta,
41
- getMCPToolsMeta,
42
- getMCPPromptsMeta,
43
- } from './wirings/mcp/mcp-runner.js'
26
+ runMiddleware,
27
+ addMiddleware,
28
+ getMiddlewareByName,
29
+ } from './middleware-runner.js'
30
+ export { addPermission } from './permissions.js'