@pikku/core 0.12.62 → 0.12.64

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 (198) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/dist/dev/hot-reload.js +13 -55
  3. package/dist/dev/module-runner.d.ts +14 -0
  4. package/dist/dev/module-runner.js +59 -0
  5. package/dist/dev/reload-meta.d.ts +13 -0
  6. package/dist/dev/reload-meta.js +22 -0
  7. package/dist/errors/errors.d.ts +12 -0
  8. package/dist/errors/errors.js +19 -0
  9. package/dist/function/function-runner.d.ts +5 -5
  10. package/dist/function/function-runner.js +39 -111
  11. package/dist/function/functions.types.d.ts +17 -7
  12. package/dist/index.d.ts +5 -1
  13. package/dist/index.js +3 -1
  14. package/dist/middleware/auth-apikey.d.ts +1 -0
  15. package/dist/middleware/auth-bearer.d.ts +1 -0
  16. package/dist/middleware/auth-cookie.d.ts +1 -0
  17. package/dist/middleware/cors.d.ts +1 -0
  18. package/dist/middleware/index.d.ts +1 -1
  19. package/dist/middleware/index.js +1 -1
  20. package/dist/middleware/remote-auth.d.ts +1 -0
  21. package/dist/middleware/telemetry.d.ts +2 -0
  22. package/dist/middleware/timeout.d.ts +1 -0
  23. package/dist/permissions.d.ts +32 -25
  24. package/dist/permissions.js +104 -185
  25. package/dist/pikku-state.js +0 -2
  26. package/dist/scopes.d.ts +17 -0
  27. package/dist/scopes.js +59 -0
  28. package/dist/services/ai-embedding-service.d.ts +31 -0
  29. package/dist/services/ai-embedding-service.js +1 -0
  30. package/dist/services/credential-wire-service.d.ts +3 -1
  31. package/dist/services/credential-wire-service.js +10 -4
  32. package/dist/services/index.d.ts +4 -0
  33. package/dist/services/index.js +2 -0
  34. package/dist/services/meta-service.d.ts +15 -2
  35. package/dist/services/meta-service.js +51 -1
  36. package/dist/services/queue-webhook-service.d.ts +55 -0
  37. package/dist/services/queue-webhook-service.js +136 -0
  38. package/dist/services/scope-service.d.ts +67 -0
  39. package/dist/services/scope-service.js +1 -0
  40. package/dist/services/typed-secret-service.d.ts +8 -0
  41. package/dist/services/typed-secret-service.js +39 -4
  42. package/dist/services/webhook-service.d.ts +140 -0
  43. package/dist/services/webhook-service.js +44 -0
  44. package/dist/services/workflow-service.d.ts +7 -0
  45. package/dist/types/core.types.d.ts +34 -6
  46. package/dist/types/state.types.d.ts +20 -2
  47. package/dist/utils/hmac.d.ts +16 -0
  48. package/dist/utils/hmac.js +26 -0
  49. package/dist/utils/safe-fetch.d.ts +51 -0
  50. package/dist/utils/safe-fetch.js +192 -0
  51. package/dist/wirings/ai-agent/ai-agent-agui.d.ts +84 -0
  52. package/dist/wirings/ai-agent/ai-agent-agui.js +305 -0
  53. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +41 -1
  54. package/dist/wirings/ai-agent/ai-agent-prepare.js +146 -6
  55. package/dist/wirings/ai-agent/ai-agent-runner.js +6 -1
  56. package/dist/wirings/ai-agent/ai-agent-stream.js +61 -10
  57. package/dist/wirings/ai-agent/ai-agent.types.d.ts +27 -1
  58. package/dist/wirings/ai-agent/index.d.ts +1 -0
  59. package/dist/wirings/ai-agent/index.js +1 -0
  60. package/dist/wirings/ai-agent/voice-input.d.ts +1 -0
  61. package/dist/wirings/ai-agent/voice-input.js +2 -38
  62. package/dist/wirings/ai-agent/voice-output.d.ts +1 -0
  63. package/dist/wirings/channel/channel-common.js +0 -1
  64. package/dist/wirings/channel/channel-handler.js +1 -4
  65. package/dist/wirings/channel/channel-middleware-runner.d.ts +12 -0
  66. package/dist/wirings/channel/channel-middleware-runner.js +34 -19
  67. package/dist/wirings/channel/channel.types.d.ts +2 -6
  68. package/dist/wirings/cli/cli-runner.js +0 -2
  69. package/dist/wirings/cli/cli.types.d.ts +1 -2
  70. package/dist/wirings/http/http-routes.js +0 -3
  71. package/dist/wirings/http/http-runner.d.ts +9 -29
  72. package/dist/wirings/http/http-runner.js +13 -35
  73. package/dist/wirings/http/http.types.d.ts +1 -9
  74. package/dist/wirings/mcp/mcp-runner.js +0 -2
  75. package/dist/wirings/mcp/mcp.types.d.ts +5 -11
  76. package/dist/wirings/oauth2/index.d.ts +0 -3
  77. package/dist/wirings/oauth2/index.js +1 -2
  78. package/dist/wirings/rpc/addon-runner.d.ts +28 -0
  79. package/dist/wirings/rpc/addon-runner.js +173 -0
  80. package/dist/wirings/rpc/index.d.ts +5 -1
  81. package/dist/wirings/rpc/index.js +3 -1
  82. package/dist/wirings/rpc/remote-addon-auth.d.ts +26 -0
  83. package/dist/wirings/rpc/remote-addon-auth.js +43 -0
  84. package/dist/wirings/rpc/rpc-runner.d.ts +19 -0
  85. package/dist/wirings/rpc/rpc-runner.js +109 -3
  86. package/dist/wirings/rpc/rpc-types.d.ts +15 -1
  87. package/dist/wirings/rpc/wire-addon.js +9 -0
  88. package/dist/wirings/rpc/wire-remote-addon.d.ts +47 -0
  89. package/dist/wirings/rpc/wire-remote-addon.js +19 -0
  90. package/dist/wirings/scope/index.d.ts +3 -0
  91. package/dist/wirings/scope/index.js +2 -0
  92. package/dist/wirings/scope/scope.types.d.ts +39 -0
  93. package/dist/wirings/scope/scope.types.js +1 -0
  94. package/dist/wirings/scope/validate-scope-definitions.d.ts +16 -0
  95. package/dist/wirings/scope/validate-scope-definitions.js +76 -0
  96. package/dist/wirings/scope/wire-scope.d.ts +33 -0
  97. package/dist/wirings/scope/wire-scope.js +32 -0
  98. package/dist/wirings/workflow/dsl/index.d.ts +1 -1
  99. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +53 -1
  100. package/dist/wirings/workflow/graph/graph-node.d.ts +2 -0
  101. package/dist/wirings/workflow/graph/graph-runner.js +3 -0
  102. package/dist/wirings/workflow/graph/wire-workflow-graph.d.ts +6 -0
  103. package/dist/wirings/workflow/graph/wire-workflow-graph.js +1 -0
  104. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +5 -0
  105. package/dist/wirings/workflow/index.d.ts +2 -2
  106. package/dist/wirings/workflow/index.js +1 -1
  107. package/dist/wirings/workflow/pikku-workflow-service.d.ts +51 -1
  108. package/dist/wirings/workflow/pikku-workflow-service.js +171 -0
  109. package/dist/wirings/workflow/workflow.types.d.ts +1 -1
  110. package/package.json +5 -1
  111. package/run-tests.sh +1 -0
  112. package/src/dev/hot-reload.ts +13 -68
  113. package/src/dev/module-runner.test.ts +169 -0
  114. package/src/dev/module-runner.ts +103 -0
  115. package/src/dev/reload-meta.test.ts +31 -2
  116. package/src/dev/reload-meta.ts +26 -0
  117. package/src/errors/errors.ts +24 -0
  118. package/src/function/function-runner.test.ts +276 -142
  119. package/src/function/function-runner.ts +52 -145
  120. package/src/function/functions.types.ts +28 -38
  121. package/src/index.ts +8 -6
  122. package/src/middleware/index.ts +1 -5
  123. package/src/permissions.test.ts +153 -372
  124. package/src/permissions.ts +139 -259
  125. package/src/pikku-state.ts +0 -2
  126. package/src/scopes.test.ts +167 -0
  127. package/src/scopes.ts +69 -0
  128. package/src/services/ai-embedding-service.ts +31 -0
  129. package/src/services/credential-wire-service.ts +10 -4
  130. package/src/services/index.ts +20 -0
  131. package/src/services/meta-service.ts +68 -3
  132. package/src/services/queue-webhook-service.test.ts +408 -0
  133. package/src/services/queue-webhook-service.ts +182 -0
  134. package/src/services/scope-service.ts +75 -0
  135. package/src/services/typed-secret-service.test.ts +35 -0
  136. package/src/services/typed-secret-service.ts +39 -4
  137. package/src/services/webhook-service.ts +180 -0
  138. package/src/services/workflow-service.ts +7 -0
  139. package/src/types/core.types.ts +42 -14
  140. package/src/types/state.types.ts +23 -3
  141. package/src/utils/hmac.ts +27 -0
  142. package/src/utils/safe-fetch.test.ts +373 -0
  143. package/src/utils/safe-fetch.ts +213 -0
  144. package/src/wirings/ai-agent/ai-agent-agui.test.ts +1127 -0
  145. package/src/wirings/ai-agent/ai-agent-agui.ts +386 -0
  146. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +308 -0
  147. package/src/wirings/ai-agent/ai-agent-prepare.ts +192 -8
  148. package/src/wirings/ai-agent/ai-agent-runner.test.ts +48 -1
  149. package/src/wirings/ai-agent/ai-agent-runner.ts +21 -0
  150. package/src/wirings/ai-agent/ai-agent-stream.test.ts +198 -10
  151. package/src/wirings/ai-agent/ai-agent-stream.ts +84 -10
  152. package/src/wirings/ai-agent/ai-agent.types.ts +28 -0
  153. package/src/wirings/ai-agent/index.ts +1 -0
  154. package/src/wirings/ai-agent/voice-input.ts +6 -35
  155. package/src/wirings/channel/channel-common.ts +0 -1
  156. package/src/wirings/channel/channel-handler.ts +1 -9
  157. package/src/wirings/channel/channel-middleware-runner.test.ts +28 -2
  158. package/src/wirings/channel/channel-middleware-runner.ts +39 -27
  159. package/src/wirings/channel/channel.types.ts +0 -6
  160. package/src/wirings/cli/cli-runner.ts +0 -2
  161. package/src/wirings/cli/cli.types.ts +0 -2
  162. package/src/wirings/http/http-routes.ts +0 -3
  163. package/src/wirings/http/http-runner.test.ts +1 -11
  164. package/src/wirings/http/http-runner.ts +19 -40
  165. package/src/wirings/http/http.types.ts +1 -10
  166. package/src/wirings/mcp/mcp-runner.ts +0 -2
  167. package/src/wirings/mcp/mcp.types.ts +3 -11
  168. package/src/wirings/oauth2/index.ts +0 -3
  169. package/src/wirings/rpc/addon-runner.ts +233 -0
  170. package/src/wirings/rpc/index.ts +17 -1
  171. package/src/wirings/rpc/remote-addon-auth.ts +69 -0
  172. package/src/wirings/rpc/rpc-runner.test.ts +319 -16
  173. package/src/wirings/rpc/rpc-runner.ts +155 -3
  174. package/src/wirings/rpc/rpc-types.ts +16 -5
  175. package/src/wirings/rpc/wire-addon.test.ts +6 -3
  176. package/src/wirings/rpc/wire-addon.ts +9 -0
  177. package/src/wirings/rpc/wire-remote-addon.ts +57 -0
  178. package/src/wirings/scope/index.ts +14 -0
  179. package/src/wirings/scope/scope.test.ts +135 -0
  180. package/src/wirings/scope/scope.types.ts +44 -0
  181. package/src/wirings/scope/validate-scope-definitions.ts +110 -0
  182. package/src/wirings/scope/wire-scope.ts +34 -0
  183. package/src/wirings/workflow/dsl/index.ts +4 -0
  184. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +61 -0
  185. package/src/wirings/workflow/graph/graph-node.ts +2 -0
  186. package/src/wirings/workflow/graph/graph-runner.test.ts +58 -0
  187. package/src/wirings/workflow/graph/graph-runner.ts +3 -0
  188. package/src/wirings/workflow/graph/wire-workflow-graph.ts +7 -0
  189. package/src/wirings/workflow/graph/workflow-graph.types.ts +5 -0
  190. package/src/wirings/workflow/index.ts +5 -0
  191. package/src/wirings/workflow/pikku-workflow-service.test.ts +341 -0
  192. package/src/wirings/workflow/pikku-workflow-service.ts +236 -0
  193. package/src/wirings/workflow/workflow-step-session.test.ts +21 -5
  194. package/src/wirings/workflow/workflow.types.ts +4 -0
  195. package/tsconfig.tsbuildinfo +1 -1
  196. package/src/wirings/oauth2/oauth2-client.test.ts +0 -930
  197. package/src/wirings/oauth2/oauth2-client.ts +0 -331
  198. package/src/wirings/oauth2/oauth2-routes.ts +0 -234
@@ -1,39 +1,21 @@
1
1
  import { describe, test, beforeEach } from 'node:test'
2
2
  import * as assert from 'node:assert'
3
- import { addTagPermission, runPermissions } from './permissions.js'
3
+ import {
4
+ addGlobalPermission,
5
+ runPermissions,
6
+ clearPermissionsCache,
7
+ checkAuthPermissions,
8
+ } from './permissions.js'
4
9
  import { pikkuState, resetPikkuState } from './pikku-state.js'
10
+ import { pikkuAuth } from './function/functions.types.js'
5
11
  import type { CoreServices, CoreUserSession } from './types/core.types.js'
6
- import type {
7
- CorePermissionGroup,
8
- CorePikkuPermission,
9
- } from './function/functions.types.js'
12
+ import type { CorePermissionGroup } from './function/functions.types.js'
10
13
 
11
14
  beforeEach(() => {
12
15
  resetPikkuState()
16
+ clearPermissionsCache()
13
17
  })
14
18
 
15
- // Test helper to get permissions for tags (not exported from core to keep it lean)
16
- const getPermissionsForTags = (
17
- tags?: string[]
18
- ): (CorePermissionGroup | CorePikkuPermission)[] => {
19
- if (!tags || tags.length === 0) {
20
- return []
21
- }
22
- const permissionsStore = pikkuState(null, 'permissions', 'tagGroup')
23
- const result: (CorePermissionGroup | CorePikkuPermission)[] = []
24
- for (const tag of new Set(tags)) {
25
- const tagPermissions = permissionsStore[tag]
26
- if (tagPermissions) {
27
- if (Array.isArray(tagPermissions)) {
28
- result.push(...tagPermissions)
29
- } else {
30
- result.push(tagPermissions)
31
- }
32
- }
33
- }
34
- return result
35
- }
36
-
37
19
  const mockServices: CoreServices = {
38
20
  logger: {
39
21
  info: () => {},
@@ -43,395 +25,194 @@ const mockServices: CoreServices = {
43
25
  },
44
26
  } as any
45
27
 
46
- const mockSession: CoreUserSession = {
47
- userId: 'test-user',
48
- } as any
49
-
50
- describe('addTagPermission', () => {
51
- test('should add single permission for a tag', () => {
52
- const mockPermission = async () => true
53
-
54
- addTagPermission('testTag', [mockPermission])
55
-
56
- const permissions = getPermissionsForTags(['testTag'])
57
- assert.equal(permissions.length, 1)
58
- assert.equal(permissions[0], mockPermission)
59
- })
60
-
61
- test('should add multiple permissions for a tag', () => {
62
- const mockPermission1 = async () => true
63
- const mockPermission2 = async () => false
64
-
65
- addTagPermission('multiTestTag', [mockPermission1, mockPermission2])
28
+ const mockSession: CoreUserSession = { userId: 'test-user' } as any
66
29
 
67
- const permissions = getPermissionsForTags(['multiTestTag'])
68
- assert.equal(permissions.length, 2)
69
- assert.equal(permissions[0], mockPermission1)
70
- assert.equal(permissions[1], mockPermission2)
30
+ const run = (opts: {
31
+ funcPermissions?: CorePermissionGroup | any[]
32
+ data?: any
33
+ wire?: any
34
+ packageName?: string | null
35
+ }) =>
36
+ runPermissions({
37
+ funcPermissions: opts.funcPermissions,
38
+ services: mockServices,
39
+ wire: (opts.wire ?? { session: mockSession }) as any,
40
+ data: opts.data ?? {},
41
+ packageName: opts.packageName ?? null,
71
42
  })
72
43
 
73
- test('should add permission group object for a tag', () => {
74
- const mockPermissionGroup: CorePermissionGroup = {
75
- permissions: [async () => true, async () => false],
76
- }
77
-
78
- addTagPermission('groupTestTag', mockPermissionGroup)
79
-
80
- const permissions = getPermissionsForTags(['groupTestTag'])
81
- assert.equal(permissions.length, 1)
82
- assert.equal(permissions[0], mockPermissionGroup)
44
+ describe('runPermissions function permissions (OR gate)', () => {
45
+ test('passes when no permissions are defined', async () => {
46
+ await run({})
83
47
  })
84
48
 
85
- test('should apply parent tag permission to namespaced tag via runPermissions', async () => {
86
- const billingPermission = async () => true
87
-
88
- addTagPermission('billing', [billingPermission])
89
-
90
- await runPermissions('rpc', Math.random().toString(), {
91
- wireInheritedPermissions: [{ type: 'tag', tag: 'billing:write' }],
92
- services: mockServices,
93
- wire: {},
94
- data: {},
95
- })
96
- })
97
-
98
- test('should apply both parent and exact tag permissions', async () => {
99
- const executionOrder: string[] = []
100
- const billingPermission = async () => {
101
- executionOrder.push('billing')
102
- return true
103
- }
104
- const billingReadPermission = async () => {
105
- executionOrder.push('billing:read')
106
- return true
107
- }
108
-
109
- addTagPermission('billing', [billingPermission])
110
- addTagPermission('billing:read', [billingReadPermission])
111
-
112
- await runPermissions('rpc', Math.random().toString(), {
113
- wireInheritedPermissions: [{ type: 'tag', tag: 'billing:read' }],
114
- services: mockServices,
115
- wire: {},
116
- data: {},
117
- })
118
-
119
- assert.deepEqual(executionOrder, ['billing:read'])
120
- })
121
-
122
- test('should resolve multi-level namespace permissions', async () => {
123
- const executionOrder: string[] = []
124
- const billingPermission = async () => {
125
- executionOrder.push('billing')
126
- return false
127
- }
128
- const billingReadPermission = async () => {
129
- executionOrder.push('billing:read')
130
- return false
131
- }
132
- const billingReadAdminPermission = async () => {
133
- executionOrder.push('billing:read:admin')
134
- return true
135
- }
136
-
137
- addTagPermission('billing', [billingPermission])
138
- addTagPermission('billing:read', [billingReadPermission])
139
- addTagPermission('billing:read:admin', [billingReadAdminPermission])
140
-
141
- await runPermissions('rpc', Math.random().toString(), {
142
- wireInheritedPermissions: [{ type: 'tag', tag: 'billing:read:admin' }],
143
- services: mockServices,
144
- wire: {},
145
- data: {},
49
+ test('passes when at least one group is satisfied (OR)', async () => {
50
+ await run({
51
+ funcPermissions: {
52
+ owner: [async () => false],
53
+ admin: [async () => true],
54
+ },
146
55
  })
147
-
148
- assert.deepEqual(executionOrder, ['billing:read:admin'])
149
56
  })
150
57
 
151
- test('should apply parent tag permission via funcInheritedPermissions', async () => {
152
- const billingPermission = async () => true
153
-
154
- addTagPermission('billing', [billingPermission])
155
-
156
- await runPermissions('rpc', Math.random().toString(), {
157
- funcInheritedPermissions: [{ type: 'tag', tag: 'billing:delete' }],
158
- services: mockServices,
159
- wire: {},
160
- data: {},
161
- })
58
+ test('throws when every group fails', async () => {
59
+ await assert.rejects(
60
+ run({
61
+ funcPermissions: {
62
+ owner: [async () => false],
63
+ admin: [async () => false],
64
+ },
65
+ }),
66
+ { message: 'Permission denied' }
67
+ )
162
68
  })
163
69
 
164
- test('should throw error when tag already exists', () => {
165
- const mockPermission = async () => true
166
-
167
- addTagPermission('duplicateTag', [mockPermission])
168
-
169
- assert.throws(
170
- () => {
171
- addTagPermission('duplicateTag', [mockPermission])
172
- },
173
- {
174
- message:
175
- "Permissions for tag 'duplicateTag' already exist. Use a different tag or remove the existing permissions first.",
176
- }
70
+ test('ANDs the entries within a single branch array', async () => {
71
+ await assert.rejects(
72
+ run({ funcPermissions: { grp: [async () => true, async () => false] } }),
73
+ { message: 'Permission denied' }
177
74
  )
75
+ await run({ funcPermissions: { grp: [async () => true, async () => true] } })
178
76
  })
179
- })
180
77
 
181
- describe('runPermissions', () => {
182
- test('should pass when no permissions are defined', async () => {
183
- // Should not throw
184
- await runPermissions('rpc', Math.random().toString(), {
185
- services: mockServices,
186
- wire: {},
187
- data: {},
78
+ test('accepts a bare permission array as a single AND branch', async () => {
79
+ await run({ funcPermissions: [async () => true] })
80
+ await assert.rejects(run({ funcPermissions: [async () => false] }), {
81
+ message: 'Permission denied',
188
82
  })
189
83
  })
190
84
 
191
- test('should execute wiring tag permissions first', async () => {
192
- const executionOrder: string[] = []
193
- const wiringTagPermission = async () => {
194
- executionOrder.push('wiringTag')
195
- return true
196
- }
197
-
198
- addTagPermission('wiringTag', [wiringTagPermission])
199
-
200
- await runPermissions('rpc', Math.random().toString(), {
201
- wireInheritedPermissions: [{ type: 'tag', tag: 'wiringTag' }],
202
- services: mockServices,
203
- wire: {},
204
- data: {},
85
+ test('passes request data and wire through to permission functions', async () => {
86
+ let received: any
87
+ const wire = { session: mockSession }
88
+ const data = { hello: 'world' }
89
+ await run({
90
+ funcPermissions: {
91
+ grp: [
92
+ async (services: any, d: any, w: any) => {
93
+ received = { services, d, w }
94
+ return true
95
+ },
96
+ ],
97
+ },
98
+ data,
99
+ wire,
205
100
  })
206
-
207
- assert.deepEqual(executionOrder, ['wiringTag'])
101
+ assert.equal(received.services, mockServices)
102
+ assert.equal(received.d, data)
103
+ assert.equal(received.w, wire)
208
104
  })
105
+ })
209
106
 
210
- test('should execute all permission levels in correct order', async () => {
211
- const executionOrder: string[] = []
212
-
213
- const wiringTagPermission = async () => {
214
- executionOrder.push('wiringTag')
215
- return true
216
- }
217
- const funcTagPermission = async () => {
218
- executionOrder.push('funcTag')
219
- return true
220
- }
221
-
222
- addTagPermission('wiringTag', [wiringTagPermission])
223
- addTagPermission('funcTag', [funcTagPermission])
224
-
225
- const wirePermissions: CorePermissionGroup = {
226
- permissions: [
227
- async () => {
228
- executionOrder.push('wiringPermission')
229
- return true
230
- },
231
- ],
232
- }
233
-
234
- const funcPermissions: CorePermissionGroup = {
235
- permissions: [
236
- async () => {
237
- executionOrder.push('funcPermission')
238
- return true
239
- },
240
- ],
241
- }
242
-
243
- await runPermissions('rpc', Math.random().toString(), {
244
- wireInheritedPermissions: [{ type: 'tag', tag: 'wiringTag' }],
245
- wirePermissions,
246
- funcInheritedPermissions: [{ type: 'tag', tag: 'funcTag' }],
247
- funcPermissions,
248
- services: mockServices,
249
- wire: {},
250
- data: {},
251
- })
107
+ describe('runPermissions global permissions (AND gate)', () => {
108
+ test('requires every registered global to pass', async () => {
109
+ addGlobalPermission([async () => true])
110
+ await run({})
252
111
 
253
- // Order: wireInheritedPermissions (wiringTag) only (short-circuit on first passing group)
254
- assert.deepEqual(executionOrder, ['wiringTag'])
112
+ resetPikkuState()
113
+ clearPermissionsCache()
114
+ addGlobalPermission([async () => false])
115
+ await assert.rejects(run({}), { message: 'Permission denied' })
255
116
  })
256
117
 
257
- test('should implement "at least one must pass" logic for tag permissions', async () => {
258
- const failingPermission = async () => false
259
- const passingPermission = async () => true
260
-
261
- addTagPermission('atLeastOneTestTag', [
262
- failingPermission,
263
- passingPermission,
264
- ])
265
-
266
- // Should not throw because at least one permission passes
267
- await runPermissions('rpc', Math.random().toString(), {
268
- wireInheritedPermissions: [{ type: 'tag', tag: 'atLeastOneTestTag' }],
269
- services: mockServices,
270
- wire: {},
271
- data: {},
272
- })
118
+ test('ANDs multiple registered globals', async () => {
119
+ addGlobalPermission([async () => true])
120
+ addGlobalPermission([async () => false])
121
+ await assert.rejects(run({}), { message: 'Permission denied' })
273
122
  })
274
123
 
275
- test('should throw error when all wiring tag permissions fail', async () => {
276
- const failingPermission1 = async () => false
277
- const failingPermission2 = async () => false
278
-
279
- addTagPermission('allFailTestTag', [failingPermission1, failingPermission2])
280
-
281
- await assert.rejects(
282
- runPermissions('rpc', Math.random().toString(), {
283
- wireInheritedPermissions: [{ type: 'tag', tag: 'allFailTestTag' }],
284
- services: mockServices,
285
- wire: {},
286
- data: {},
287
- }),
288
- {
289
- message: 'Permission denied',
290
- }
291
- )
124
+ test('a global group entry ORs internally but must still pass', async () => {
125
+ addGlobalPermission({
126
+ a: [async () => false],
127
+ b: [async () => true],
128
+ } as CorePermissionGroup)
129
+ await run({})
292
130
  })
131
+ })
293
132
 
294
- test('should throw error when wiring permissions fail', async () => {
295
- const wirePermissions: CorePermissionGroup = {
296
- permissions: [async () => false],
297
- }
298
-
133
+ describe('runPermissions gates are independent (no escalation)', () => {
134
+ test('a passing global does NOT satisfy a failing function permission', async () => {
135
+ // The classic escalation bug: a broad global must not grant an admin-only fn.
136
+ addGlobalPermission([async () => true])
299
137
  await assert.rejects(
300
- runPermissions('rpc', Math.random().toString(), {
301
- wirePermissions,
302
- services: mockServices,
303
- wire: {},
304
- data: {},
305
- }),
306
- {
307
- message: 'Permission denied',
308
- }
138
+ run({ funcPermissions: { admin: [async () => false] } }),
139
+ { message: 'Permission denied' }
309
140
  )
310
141
  })
311
142
 
312
- test('should throw error when all function tag permissions fail', async () => {
313
- const failingPermission = async () => false
314
-
315
- addTagPermission('funcTag', [failingPermission])
316
-
143
+ test('a passing function permission does NOT bypass a failing global', async () => {
144
+ addGlobalPermission([async () => false])
317
145
  await assert.rejects(
318
- runPermissions('rpc', Math.random().toString(), {
319
- funcInheritedPermissions: [{ type: 'tag', tag: 'funcTag' }],
320
- services: mockServices,
321
- wire: {},
322
- data: {},
323
- }),
324
- {
325
- message: 'Permission denied',
326
- }
146
+ run({ funcPermissions: { anyone: [async () => true] } }),
147
+ { message: 'Permission denied' }
327
148
  )
328
149
  })
329
150
 
330
- test('should throw error when function permissions fail', async () => {
331
- const funcPermissions: CorePermissionGroup = {
332
- permissions: [async () => false],
333
- }
334
-
335
- await assert.rejects(
336
- runPermissions('rpc', Math.random().toString(), {
337
- funcPermissions,
338
- services: mockServices,
339
- wire: {},
340
- data: {},
341
- }),
342
- {
343
- message: 'Permission denied',
344
- }
345
- )
151
+ test('passes only when both gates pass', async () => {
152
+ addGlobalPermission([async () => true])
153
+ await run({ funcPermissions: { admin: [async () => true] } })
346
154
  })
155
+ })
347
156
 
348
- test('should evaluate all permissions and pass if at least one succeeds', async () => {
349
- const executionOrder: string[] = []
350
-
351
- const failingWiringTagPermission = async () => {
352
- executionOrder.push('wiringTag')
353
- return false
354
- }
355
-
356
- addTagPermission('failingWiringTag', [failingWiringTagPermission])
357
-
358
- const wirePermissions: CorePermissionGroup = {
359
- permissions: [
360
- async () => {
361
- executionOrder.push('wiringPermission')
362
- return true
363
- },
364
- ],
365
- }
366
-
367
- // Should NOT throw because at least one permission (wirePermissions) passes
368
- await runPermissions('rpc', Math.random().toString(), {
369
- wireInheritedPermissions: [{ type: 'tag', tag: 'failingWiringTag' }],
370
- wirePermissions,
371
- services: mockServices,
372
- wire: {},
373
- data: {},
157
+ describe('runPermissions package isolation', () => {
158
+ test('globals are scoped per package', async () => {
159
+ addGlobalPermission([async () => false], 'pkg-a')
160
+ // A different package sees no globals, so it passes.
161
+ await run({ packageName: 'pkg-b' })
162
+ // The registering package is gated.
163
+ await assert.rejects(run({ packageName: 'pkg-a' }), {
164
+ message: 'Permission denied',
374
165
  })
375
-
376
- // Both permissions should be evaluated
377
- assert.deepEqual(executionOrder, ['wiringTag', 'wiringPermission'])
378
166
  })
167
+ })
379
168
 
380
- test('should handle array permissions in tag-based permissions', async () => {
381
- const arrayPermission = [async () => true, async () => false]
382
-
383
- addTagPermission('arrayTestTag', arrayPermission)
384
-
385
- // Should not throw because verifyPermissions handles array properly
386
- await runPermissions('rpc', Math.random().toString(), {
387
- wireInheritedPermissions: [{ type: 'tag', tag: 'arrayTestTag' }],
388
- services: mockServices,
389
- wire: {},
390
- data: {},
391
- })
169
+ describe('checkAuthPermissions', () => {
170
+ test('returns true when there are no auth predicates', async () => {
171
+ assert.equal(
172
+ await checkAuthPermissions(undefined, mockSession, mockServices),
173
+ true
174
+ )
392
175
  })
393
176
 
394
- test('should handle permission group objects in tag-based permissions', async () => {
395
- const permissionGroup: CorePermissionGroup = {
396
- permissions: [async () => true],
397
- }
398
-
399
- addTagPermission('objectTestTag', permissionGroup)
177
+ test('honours a global pikkuAuth predicate at filter time', async () => {
178
+ addGlobalPermission([pikkuAuth(async () => false)])
179
+ assert.equal(
180
+ await checkAuthPermissions(undefined, mockSession, mockServices),
181
+ false
182
+ )
400
183
 
401
- // Should not throw because verifyPermissions handles objects properly
402
- await runPermissions('rpc', Math.random().toString(), {
403
- wireInheritedPermissions: [{ type: 'tag', tag: 'objectTestTag' }],
404
- services: mockServices,
405
- wire: {},
406
- data: {},
407
- })
184
+ resetPikkuState()
185
+ clearPermissionsCache()
186
+ addGlobalPermission([pikkuAuth(async () => true)])
187
+ assert.equal(
188
+ await checkAuthPermissions(undefined, mockSession, mockServices),
189
+ true
190
+ )
408
191
  })
409
192
 
410
- test('should pass correct parameters to permission functions', async () => {
411
- let receivedServices: any
412
- let receivedWire: any
413
- let receivedData: any
414
-
415
- const testPermission = async (services: any, data: any, wire: any) => {
416
- receivedServices = services
417
- receivedData = data
418
- receivedWire = wire
419
- return true
420
- }
421
-
422
- const testData = { test: 'data' }
423
-
424
- addTagPermission('paramTestTag', [testPermission])
425
-
426
- await runPermissions('rpc', Math.random().toString(), {
427
- wireInheritedPermissions: [{ type: 'tag', tag: 'paramTestTag' }],
428
- services: mockServices,
429
- wire: {},
430
- data: testData,
431
- })
193
+ test('resolves a function-referenced (wire) auth predicate by name', async () => {
194
+ const store = pikkuState(null, 'misc', 'permissions')
195
+ store['isAdmin'] = [pikkuAuth(async () => false)] as any
196
+ assert.equal(
197
+ await checkAuthPermissions(
198
+ [{ type: 'wire', name: 'isAdmin' }],
199
+ mockSession,
200
+ mockServices
201
+ ),
202
+ false
203
+ )
204
+ })
432
205
 
433
- assert.equal(receivedServices, mockServices)
434
- assert.equal(receivedData, testData)
435
- assert.deepEqual(receivedWire, {})
206
+ test('ignores data-dependent permissions (no auth marker)', async () => {
207
+ const store = pikkuState(null, 'misc', 'permissions')
208
+ store['ownsRow'] = [async () => false] as any
209
+ assert.equal(
210
+ await checkAuthPermissions(
211
+ [{ type: 'wire', name: 'ownsRow' }],
212
+ mockSession,
213
+ mockServices
214
+ ),
215
+ true
216
+ )
436
217
  })
437
218
  })