@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
@@ -1,58 +1,419 @@
1
- import { test, describe } from 'node:test'
2
- import assert from 'assert'
3
- import { getErrorResponse, addError } from './errors/error-handler.js'
1
+ import { describe, test, beforeEach } from 'node:test'
2
+ import * as assert from 'node:assert'
4
3
  import {
5
- ForbiddenError,
6
- BadRequestError,
7
- NotFoundError,
8
- } from './errors/errors.js'
9
-
10
- describe('getErrorResponse', () => {
11
- test('should return the correct error response for BadRequestError', () => {
12
- const error = new BadRequestError()
13
- const response = getErrorResponse(error)
14
- assert.deepStrictEqual(response, {
15
- status: 400,
16
- message:
17
- 'The server cannot or will not process the request due to client error (e.g., malformed request syntax).',
4
+ addPermission,
5
+ getPermissionsForTags,
6
+ runPermissions,
7
+ } from './permissions.js'
8
+ import { resetPikkuState } from './pikku-state.js'
9
+ import { CoreServices, CoreUserSession } from './types/core.types.js'
10
+ import { CorePermissionGroup } from './function/functions.types.js'
11
+
12
+ beforeEach(() => {
13
+ resetPikkuState()
14
+ })
15
+
16
+ const mockServices: CoreServices = {
17
+ logger: {
18
+ info: () => {},
19
+ warn: () => {},
20
+ error: () => {},
21
+ debug: () => {},
22
+ },
23
+ } as any
24
+
25
+ const mockSession: CoreUserSession = {
26
+ userId: 'test-user',
27
+ } as any
28
+
29
+ describe('addPermission', () => {
30
+ test('should add single permission for a tag', () => {
31
+ const mockPermission = async () => true
32
+
33
+ addPermission('testTag', [mockPermission])
34
+
35
+ const permissions = getPermissionsForTags(['testTag'])
36
+ assert.equal(permissions.length, 1)
37
+ assert.equal(permissions[0], mockPermission)
38
+ })
39
+
40
+ test('should add multiple permissions for a tag', () => {
41
+ const mockPermission1 = async () => true
42
+ const mockPermission2 = async () => false
43
+
44
+ addPermission('multiTestTag', [mockPermission1, mockPermission2])
45
+
46
+ const permissions = getPermissionsForTags(['multiTestTag'])
47
+ assert.equal(permissions.length, 2)
48
+ assert.equal(permissions[0], mockPermission1)
49
+ assert.equal(permissions[1], mockPermission2)
50
+ })
51
+
52
+ test('should add permission group object for a tag', () => {
53
+ const mockPermissionGroup: CorePermissionGroup = {
54
+ permissions: [async () => true, async () => false],
55
+ }
56
+
57
+ addPermission('groupTestTag', mockPermissionGroup)
58
+
59
+ const permissions = getPermissionsForTags(['groupTestTag'])
60
+ assert.equal(permissions.length, 1)
61
+ assert.equal(permissions[0], mockPermissionGroup)
62
+ })
63
+
64
+ test('should throw error when tag already exists', () => {
65
+ const mockPermission = async () => true
66
+
67
+ addPermission('duplicateTag', [mockPermission])
68
+
69
+ assert.throws(
70
+ () => {
71
+ addPermission('duplicateTag', [mockPermission])
72
+ },
73
+ {
74
+ message:
75
+ "Permissions for tag 'duplicateTag' already exist. Use a different tag or remove the existing permissions first.",
76
+ }
77
+ )
78
+ })
79
+ })
80
+
81
+ describe('getPermissionsForTags', () => {
82
+ test('should return empty array when no tags provided', () => {
83
+ const permissions = getPermissionsForTags([])
84
+ assert.deepEqual(permissions, [])
85
+ })
86
+
87
+ test('should return empty array when tags is undefined', () => {
88
+ const permissions = getPermissionsForTags(undefined)
89
+ assert.deepEqual(permissions, [])
90
+ })
91
+
92
+ test('should return permissions for single tag', () => {
93
+ const mockPermission = async () => true
94
+ addPermission('singleTestTag', [mockPermission])
95
+
96
+ const permissions = getPermissionsForTags(['singleTestTag'])
97
+ assert.equal(permissions.length, 1)
98
+ assert.equal(permissions[0], mockPermission)
99
+ })
100
+
101
+ test('should return permissions for multiple tags', () => {
102
+ const mockPermission1 = async () => true
103
+ const mockPermission2 = async () => false
104
+
105
+ addPermission('tag1', [mockPermission1])
106
+ addPermission('tag2', [mockPermission2])
107
+
108
+ const permissions = getPermissionsForTags(['tag1', 'tag2'])
109
+ assert.equal(permissions.length, 2)
110
+ assert.equal(permissions[0], mockPermission1)
111
+ assert.equal(permissions[1], mockPermission2)
112
+ })
113
+
114
+ test('should handle array permissions correctly', () => {
115
+ const mockPermission1 = async () => true
116
+ const mockPermission2 = async () => false
117
+
118
+ addPermission('arrayHandleTestTag', [mockPermission1, mockPermission2])
119
+
120
+ const permissions = getPermissionsForTags(['arrayHandleTestTag'])
121
+ assert.equal(permissions.length, 2)
122
+ assert.equal(permissions[0], mockPermission1)
123
+ assert.equal(permissions[1], mockPermission2)
124
+ })
125
+
126
+ test('should handle non-array permissions correctly', () => {
127
+ const mockPermissionGroup: CorePermissionGroup = {
128
+ permissions: [async () => true],
129
+ }
130
+
131
+ addPermission('objectHandleTestTag', mockPermissionGroup)
132
+
133
+ const permissions = getPermissionsForTags(['objectHandleTestTag'])
134
+ assert.equal(permissions.length, 1)
135
+ assert.equal(permissions[0], mockPermissionGroup)
136
+ })
137
+
138
+ test('should deduplicate tags', () => {
139
+ const mockPermission = async () => true
140
+ addPermission('dedupeTestTag', [mockPermission])
141
+
142
+ const permissions = getPermissionsForTags([
143
+ 'dedupeTestTag',
144
+ 'dedupeTestTag',
145
+ ])
146
+ assert.equal(permissions.length, 1)
147
+ assert.equal(permissions[0], mockPermission)
148
+ })
149
+
150
+ test('should ignore non-existent tags', () => {
151
+ const mockPermission = async () => true
152
+ addPermission('existingTag', [mockPermission])
153
+
154
+ const permissions = getPermissionsForTags(['existingTag', 'nonExistentTag'])
155
+ assert.equal(permissions.length, 1)
156
+ assert.equal(permissions[0], mockPermission)
157
+ })
158
+ })
159
+
160
+ describe('runPermissions', () => {
161
+ test('should pass when no permissions are defined', async () => {
162
+ // Should not throw
163
+ await runPermissions({
164
+ allServices: mockServices,
165
+ data: {},
166
+ session: mockSession,
18
167
  })
19
168
  })
20
169
 
21
- test('should return the correct error response for NotFoundError', () => {
22
- const error = new NotFoundError()
23
- const response = getErrorResponse(error)
24
- assert.deepStrictEqual(response, {
25
- status: 404,
26
- message: 'The server cannot find the requested resource.',
170
+ test('should execute wiring tag permissions first', async () => {
171
+ const executionOrder: string[] = []
172
+ const wiringTagPermission = async () => {
173
+ executionOrder.push('wiringTag')
174
+ return true
175
+ }
176
+
177
+ addPermission('wiringTag', [wiringTagPermission])
178
+
179
+ await runPermissions({
180
+ wiringTags: ['wiringTag'],
181
+ allServices: mockServices,
182
+ data: {},
183
+ session: mockSession,
184
+ })
185
+
186
+ assert.deepEqual(executionOrder, ['wiringTag'])
187
+ })
188
+
189
+ test('should execute all permission levels in correct order', async () => {
190
+ const executionOrder: string[] = []
191
+
192
+ const wiringTagPermission = async () => {
193
+ executionOrder.push('wiringTag')
194
+ return true
195
+ }
196
+ const funcTagPermission = async () => {
197
+ executionOrder.push('funcTag')
198
+ return true
199
+ }
200
+
201
+ addPermission('wiringTag', [wiringTagPermission])
202
+ addPermission('funcTag', [funcTagPermission])
203
+
204
+ const wiringPermissions: CorePermissionGroup = {
205
+ permissions: [
206
+ async () => {
207
+ executionOrder.push('wiringPermission')
208
+ return true
209
+ },
210
+ ],
211
+ }
212
+
213
+ const funcPermissions: CorePermissionGroup = {
214
+ permissions: [
215
+ async () => {
216
+ executionOrder.push('funcPermission')
217
+ return true
218
+ },
219
+ ],
220
+ }
221
+
222
+ await runPermissions({
223
+ wiringTags: ['wiringTag'],
224
+ wiringPermissions,
225
+ funcTags: ['funcTag'],
226
+ funcPermissions,
227
+ allServices: mockServices,
228
+ data: {},
229
+ session: mockSession,
27
230
  })
231
+
232
+ // Order: wiringTags → wiringPermissions → funcTags → funcPermissions
233
+ assert.deepEqual(executionOrder, [
234
+ 'wiringTag',
235
+ 'wiringPermission',
236
+ 'funcTag',
237
+ 'funcPermission',
238
+ ])
28
239
  })
29
240
 
30
- test('should return the correct error response for ForbiddenError', () => {
31
- const error = new ForbiddenError()
32
- const response = getErrorResponse(error)
33
- assert.deepStrictEqual(response, {
34
- status: 403,
35
- message:
36
- 'The client does not have permission to access the requested resource.',
241
+ test('should implement "at least one must pass" logic for tag permissions', async () => {
242
+ const failingPermission = async () => false
243
+ const passingPermission = async () => true
244
+
245
+ addPermission('atLeastOneTestTag', [failingPermission, passingPermission])
246
+
247
+ // Should not throw because at least one permission passes
248
+ await runPermissions({
249
+ wiringTags: ['atLeastOneTestTag'],
250
+ allServices: mockServices,
251
+ data: {},
252
+ session: mockSession,
37
253
  })
38
254
  })
39
255
 
40
- test('should return undefined for an unknown error', () => {
41
- class UnknownError extends Error {}
42
- const error = new UnknownError()
43
- const response = getErrorResponse(error)
44
- assert.strictEqual(response, undefined)
256
+ test('should throw error when all wiring tag permissions fail', async () => {
257
+ const failingPermission1 = async () => false
258
+ const failingPermission2 = async () => false
259
+
260
+ addPermission('allFailTestTag', [failingPermission1, failingPermission2])
261
+
262
+ await assert.rejects(
263
+ runPermissions({
264
+ wiringTags: ['allFailTestTag'],
265
+ allServices: mockServices,
266
+ data: {},
267
+ session: mockSession,
268
+ }),
269
+ {
270
+ message: 'Permission denied - wiring tag permissions',
271
+ }
272
+ )
273
+ })
274
+
275
+ test('should throw error when wiring permissions fail', async () => {
276
+ const wiringPermissions: CorePermissionGroup = {
277
+ permissions: [async () => false],
278
+ }
279
+
280
+ await assert.rejects(
281
+ runPermissions({
282
+ wiringPermissions,
283
+ allServices: mockServices,
284
+ data: {},
285
+ session: mockSession,
286
+ }),
287
+ {
288
+ message: 'Permission denied - wiring permissions',
289
+ }
290
+ )
291
+ })
292
+
293
+ test('should throw error when all function tag permissions fail', async () => {
294
+ const failingPermission = async () => false
295
+
296
+ addPermission('funcTag', [failingPermission])
297
+
298
+ await assert.rejects(
299
+ runPermissions({
300
+ funcTags: ['funcTag'],
301
+ allServices: mockServices,
302
+ data: {},
303
+ session: mockSession,
304
+ }),
305
+ {
306
+ message: 'Permission denied - function tag permissions',
307
+ }
308
+ )
45
309
  })
46
310
 
47
- test('should return the correct error response for a custom error added to apiErrors', () => {
48
- class CustomError extends Error {}
49
- const customError = new CustomError()
50
- const customErrorDetails = { status: 400, message: 'Custom Error' }
311
+ test('should throw error when function permissions fail', async () => {
312
+ const funcPermissions: CorePermissionGroup = {
313
+ permissions: [async () => false],
314
+ }
51
315
 
52
- // Add the custom error to the apiErrors map
53
- addError(CustomError, customErrorDetails)
316
+ await assert.rejects(
317
+ runPermissions({
318
+ funcPermissions,
319
+ allServices: mockServices,
320
+ data: {},
321
+ session: mockSession,
322
+ }),
323
+ {
324
+ message: 'Permission denied - function permissions',
325
+ }
326
+ )
327
+ })
328
+
329
+ test('should stop execution at first failing level', async () => {
330
+ const executionOrder: string[] = []
331
+
332
+ const failingWiringTagPermission = async () => {
333
+ executionOrder.push('wiringTag')
334
+ return false
335
+ }
336
+
337
+ addPermission('failingWiringTag', [failingWiringTagPermission])
338
+
339
+ const wiringPermissions: CorePermissionGroup = {
340
+ permissions: [
341
+ async () => {
342
+ executionOrder.push('wiringPermission')
343
+ return true
344
+ },
345
+ ],
346
+ }
347
+
348
+ await assert.rejects(
349
+ runPermissions({
350
+ wiringTags: ['failingWiringTag'],
351
+ wiringPermissions,
352
+ allServices: mockServices,
353
+ data: {},
354
+ session: mockSession,
355
+ })
356
+ )
357
+
358
+ // Should only execute wiring tag permissions, not wiring permissions
359
+ assert.deepEqual(executionOrder, ['wiringTag'])
360
+ })
361
+
362
+ test('should handle array permissions in tag-based permissions', async () => {
363
+ const arrayPermission = [async () => true, async () => false]
364
+
365
+ addPermission('arrayTestTag', arrayPermission)
366
+
367
+ // Should not throw because verifyPermissions handles array properly
368
+ await runPermissions({
369
+ wiringTags: ['arrayTestTag'],
370
+ allServices: mockServices,
371
+ data: {},
372
+ session: mockSession,
373
+ })
374
+ })
375
+
376
+ test('should handle permission group objects in tag-based permissions', async () => {
377
+ const permissionGroup: CorePermissionGroup = {
378
+ permissions: [async () => true],
379
+ }
380
+
381
+ addPermission('objectTestTag', permissionGroup)
382
+
383
+ // Should not throw because verifyPermissions handles objects properly
384
+ await runPermissions({
385
+ wiringTags: ['objectTestTag'],
386
+ allServices: mockServices,
387
+ data: {},
388
+ session: mockSession,
389
+ })
390
+ })
391
+
392
+ test('should pass correct parameters to permission functions', async () => {
393
+ let receivedServices: any
394
+ let receivedData: any
395
+ let receivedSession: any
396
+
397
+ const testPermission = async (services: any, data: any, session: any) => {
398
+ receivedServices = services
399
+ receivedData = data
400
+ receivedSession = session
401
+ return true
402
+ }
403
+
404
+ const testData = { test: 'data' }
405
+
406
+ addPermission('paramTestTag', [testPermission])
407
+
408
+ await runPermissions({
409
+ wiringTags: ['paramTestTag'],
410
+ allServices: mockServices,
411
+ data: testData,
412
+ session: mockSession,
413
+ })
54
414
 
55
- const response = getErrorResponse(customError)
56
- assert.deepStrictEqual(response, customErrorDetails)
415
+ assert.equal(receivedServices, mockServices)
416
+ assert.equal(receivedData, testData)
417
+ assert.equal(receivedSession, mockSession)
57
418
  })
58
419
  })
@@ -1,5 +1,10 @@
1
1
  import { CoreServices, CoreUserSession } from './types/core.types.js'
2
- import { CorePermissionGroup } from './function/functions.types.js'
2
+ import {
3
+ CorePermissionGroup,
4
+ CorePikkuPermission,
5
+ } from './function/functions.types.js'
6
+ import { pikkuState } from './pikku-state.js'
7
+ import { ForbiddenError } from './errors/errors.js'
3
8
 
4
9
  /**
5
10
  * This function validates permissions by iterating over permission groups and executing the corresponding permission functions. If all functions in at least one group return true, the permission is considered valid.
@@ -41,3 +46,179 @@ export const verifyPermissions = async (
41
46
  }
42
47
  return false
43
48
  }
49
+
50
+ /**
51
+ * Adds global permissions for a specific tag.
52
+ *
53
+ * This function allows you to register permissions that will be applied to
54
+ * any wiring (HTTP, Channel, Queue, Scheduler, MCP) that includes the matching tag.
55
+ *
56
+ * @param {string} tag - The tag that the permissions should apply to.
57
+ * @param {any[]} permissions - The permissions array to apply for the specified tag.
58
+ *
59
+ * @throws {Error} If permissions for the tag already exist.
60
+ *
61
+ * @example
62
+ * ```typescript
63
+ * // Add admin permissions for admin endpoints
64
+ * addPermission('admin', [adminPermission])
65
+ *
66
+ * // Add authentication permissions for auth endpoints
67
+ * addPermission('auth', [authPermission])
68
+ *
69
+ * // Add read permissions for all API endpoints
70
+ * addPermission('api', [readPermission])
71
+ * ```
72
+ */
73
+ export const addPermission = (
74
+ tag: string,
75
+ permissions: CorePermissionGroup | CorePikkuPermission[]
76
+ ) => {
77
+ const permissionsStore = pikkuState('misc', 'permissions')
78
+
79
+ // Check if tag already exists
80
+ if (permissionsStore[tag]) {
81
+ throw new Error(
82
+ `Permissions for tag '${tag}' already exist. Use a different tag or remove the existing permissions first.`
83
+ )
84
+ }
85
+
86
+ permissionsStore[tag] = permissions
87
+ }
88
+
89
+ /**
90
+ * Retrieves permissions for a given set of tags.
91
+ *
92
+ * This function looks up all permissions registered for any of the provided tags
93
+ * and returns them as a flattened array.
94
+ *
95
+ * @param {string[]} tags - Array of tags to look up permissions for.
96
+ * @returns {any[]} Array of permission functions that apply to the given tags.
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * // Get all permissions for tags 'api' and 'auth'
101
+ * const permissions = getPermissionsForTags(['api', 'auth'])
102
+ * ```
103
+ */
104
+ export const getPermissionsForTags = (
105
+ tags?: string[]
106
+ ): Array<CorePermissionGroup | CorePikkuPermission> => {
107
+ if (!tags || tags.length === 0) {
108
+ return []
109
+ }
110
+
111
+ const permissionsStore = pikkuState('misc', 'permissions')
112
+ const applicablePermissions: Array<
113
+ CorePermissionGroup | CorePikkuPermission
114
+ > = []
115
+
116
+ // Collect permissions for all matching tags
117
+ for (const tag of new Set(tags)) {
118
+ const tagPermissions = permissionsStore[tag]
119
+ if (tagPermissions) {
120
+ if (Array.isArray(tagPermissions)) {
121
+ applicablePermissions.push(...tagPermissions)
122
+ } else {
123
+ applicablePermissions.push(tagPermissions)
124
+ }
125
+ }
126
+ }
127
+
128
+ return applicablePermissions
129
+ }
130
+
131
+ /**
132
+ * Runs permission checks in the specified order:
133
+ * 1) wiring tag permissions - at least one must pass if any exist
134
+ * 2) wiring permissions - must pass if defined
135
+ * 3) function tag permissions - at least one must pass if any exist
136
+ * 4) function permissions - must pass if defined
137
+ */
138
+ export const runPermissions = async ({
139
+ wiringTags,
140
+ wiringPermissions,
141
+ funcTags,
142
+ funcPermissions,
143
+ allServices,
144
+ data,
145
+ session,
146
+ }: {
147
+ wiringTags?: string[]
148
+ wiringPermissions?: CorePermissionGroup
149
+ funcTags?: string[]
150
+ funcPermissions?: CorePermissionGroup
151
+ allServices: CoreServices
152
+ data: any
153
+ session?: CoreUserSession
154
+ }) => {
155
+ let permissioned = true
156
+
157
+ // 1. Wiring tag permissions - at least one must pass if any exist
158
+ const wiringTaggedPermissions = getPermissionsForTags(wiringTags || [])
159
+ if (wiringTaggedPermissions.length > 0) {
160
+ permissioned = false // Start false, need at least one to pass
161
+ for (const permissions of wiringTaggedPermissions) {
162
+ const result = await verifyPermissions(
163
+ typeof permissions === 'function' ? { permissions } : permissions,
164
+ allServices,
165
+ data,
166
+ session
167
+ )
168
+ if (result) {
169
+ permissioned = true
170
+ break // At least one passed, we're good at this level
171
+ }
172
+ }
173
+ if (!permissioned) {
174
+ throw new ForbiddenError('Permission denied - wiring tag permissions')
175
+ }
176
+ }
177
+
178
+ // 2. Wiring permissions - must pass if defined
179
+ if (wiringPermissions) {
180
+ permissioned = await verifyPermissions(
181
+ wiringPermissions,
182
+ allServices,
183
+ data,
184
+ session
185
+ )
186
+ if (!permissioned) {
187
+ throw new ForbiddenError('Permission denied - wiring permissions')
188
+ }
189
+ }
190
+
191
+ // 3. Function tag permissions - at least one must pass if any exist
192
+ const funcTaggedPermissions = getPermissionsForTags(funcTags || [])
193
+ if (funcTaggedPermissions.length > 0) {
194
+ permissioned = false // Start false, need at least one to pass
195
+ for (const permissions of funcTaggedPermissions) {
196
+ const result = await verifyPermissions(
197
+ typeof permissions === 'function' ? { permissions } : permissions,
198
+ allServices,
199
+ data,
200
+ session
201
+ )
202
+ if (result) {
203
+ permissioned = true
204
+ break // At least one passed, we're good at this level
205
+ }
206
+ }
207
+ if (!permissioned) {
208
+ throw new ForbiddenError('Permission denied - function tag permissions')
209
+ }
210
+ }
211
+
212
+ // 4. Function permissions - must pass if defined
213
+ if (funcPermissions) {
214
+ permissioned = await verifyPermissions(
215
+ funcPermissions,
216
+ allServices,
217
+ data,
218
+ session
219
+ )
220
+ if (!permissioned) {
221
+ throw new ForbiddenError('Permission denied - function permissions')
222
+ }
223
+ }
224
+ }
@@ -10,7 +10,11 @@ import {
10
10
  ScheduledTasksMeta,
11
11
  } from './wirings/scheduler/scheduler.types.js'
12
12
  import { ErrorDetails, PikkuError } from './errors/error-handler.js'
13
- import { CorePikkuFunctionConfig } from './function/functions.types.js'
13
+ import {
14
+ CorePermissionGroup,
15
+ CorePikkuFunctionConfig,
16
+ CorePikkuPermission,
17
+ } from './function/functions.types.js'
14
18
  import {
15
19
  queueWorkersMeta,
16
20
  CoreQueueWorker,
@@ -67,6 +71,8 @@ interface PikkuState {
67
71
  misc: {
68
72
  errors: Map<PikkuError, ErrorDetails>
69
73
  schemas: Map<string, any>
74
+ middleware: Record<string, CorePikkuMiddleware[]>
75
+ permissions: Record<string, CorePermissionGroup | CorePikkuPermission[]>
70
76
  }
71
77
  }
72
78
 
@@ -107,7 +113,9 @@ export const resetPikkuState = () => {
107
113
  },
108
114
  misc: {
109
115
  errors: globalThis.pikkuState?.misc?.errors || new Map(),
110
- schemas: globalThis.pikkuState?.misc?.schema || new Map(),
116
+ schemas: new Map(),
117
+ middleware: {},
118
+ permissions: {},
111
119
  },
112
120
  } as PikkuState
113
121
  }