@pikku/core 0.9.4 → 0.9.6

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