@pikku/core 0.12.63 → 0.12.66

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 (225) hide show
  1. package/CHANGELOG.md +368 -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 +42 -27
  24. package/dist/permissions.js +101 -203
  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/testing/service-tests.js +37 -0
  46. package/dist/types/core.types.d.ts +34 -6
  47. package/dist/types/state.types.d.ts +20 -2
  48. package/dist/utils/hmac.d.ts +16 -0
  49. package/dist/utils/hmac.js +26 -0
  50. package/dist/utils/safe-fetch.d.ts +51 -0
  51. package/dist/utils/safe-fetch.js +192 -0
  52. package/dist/wirings/ai-agent/ai-agent-agui.d.ts +84 -0
  53. package/dist/wirings/ai-agent/ai-agent-agui.js +305 -0
  54. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +105 -1
  55. package/dist/wirings/ai-agent/ai-agent-prepare.js +249 -11
  56. package/dist/wirings/ai-agent/ai-agent-runner.js +10 -1
  57. package/dist/wirings/ai-agent/ai-agent-stream.js +88 -16
  58. package/dist/wirings/ai-agent/ai-agent.types.d.ts +56 -2
  59. package/dist/wirings/ai-agent/index.d.ts +2 -1
  60. package/dist/wirings/ai-agent/index.js +2 -1
  61. package/dist/wirings/ai-agent/voice-input.d.ts +1 -0
  62. package/dist/wirings/ai-agent/voice-input.js +5 -41
  63. package/dist/wirings/ai-agent/voice-output.d.ts +1 -0
  64. package/dist/wirings/channel/channel-common.js +0 -1
  65. package/dist/wirings/channel/channel-handler.js +1 -4
  66. package/dist/wirings/channel/channel-middleware-runner.d.ts +12 -0
  67. package/dist/wirings/channel/channel-middleware-runner.js +34 -19
  68. package/dist/wirings/channel/channel.types.d.ts +2 -6
  69. package/dist/wirings/cli/cli-runner.js +3 -2
  70. package/dist/wirings/cli/cli.types.d.ts +1 -2
  71. package/dist/wirings/cli/command-parser.d.ts +2 -0
  72. package/dist/wirings/cli/command-parser.js +59 -2
  73. package/dist/wirings/credential/credential.types.d.ts +14 -0
  74. package/dist/wirings/credential/validate-credential-definitions.js +1 -0
  75. package/dist/wirings/gateway/gateway-runner.js +100 -50
  76. package/dist/wirings/gateway/gateway.types.d.ts +8 -5
  77. package/dist/wirings/http/http-routes.js +0 -3
  78. package/dist/wirings/http/http-runner.d.ts +9 -29
  79. package/dist/wirings/http/http-runner.js +9 -34
  80. package/dist/wirings/http/http.types.d.ts +4 -12
  81. package/dist/wirings/mcp/mcp-runner.js +0 -2
  82. package/dist/wirings/mcp/mcp.types.d.ts +5 -11
  83. package/dist/wirings/oauth2/index.d.ts +0 -3
  84. package/dist/wirings/oauth2/index.js +1 -2
  85. package/dist/wirings/rpc/addon-runner.d.ts +28 -0
  86. package/dist/wirings/rpc/addon-runner.js +173 -0
  87. package/dist/wirings/rpc/index.d.ts +5 -1
  88. package/dist/wirings/rpc/index.js +3 -1
  89. package/dist/wirings/rpc/remote-addon-auth.d.ts +26 -0
  90. package/dist/wirings/rpc/remote-addon-auth.js +43 -0
  91. package/dist/wirings/rpc/rpc-runner.d.ts +19 -0
  92. package/dist/wirings/rpc/rpc-runner.js +109 -3
  93. package/dist/wirings/rpc/rpc-types.d.ts +15 -1
  94. package/dist/wirings/rpc/wire-addon.js +9 -0
  95. package/dist/wirings/rpc/wire-remote-addon.d.ts +47 -0
  96. package/dist/wirings/rpc/wire-remote-addon.js +19 -0
  97. package/dist/wirings/scope/index.d.ts +3 -0
  98. package/dist/wirings/scope/index.js +2 -0
  99. package/dist/wirings/scope/scope.types.d.ts +39 -0
  100. package/dist/wirings/scope/scope.types.js +1 -0
  101. package/dist/wirings/scope/validate-scope-definitions.d.ts +16 -0
  102. package/dist/wirings/scope/validate-scope-definitions.js +76 -0
  103. package/dist/wirings/scope/wire-scope.d.ts +33 -0
  104. package/dist/wirings/scope/wire-scope.js +32 -0
  105. package/dist/wirings/secret/secret.types.d.ts +14 -0
  106. package/dist/wirings/secret/validate-secret-definitions.js +2 -0
  107. package/dist/wirings/variable/validate-variable-definitions.js +2 -0
  108. package/dist/wirings/variable/variable.types.d.ts +14 -0
  109. package/dist/wirings/workflow/dsl/index.d.ts +1 -1
  110. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +89 -7
  111. package/dist/wirings/workflow/graph/graph-node.d.ts +2 -0
  112. package/dist/wirings/workflow/graph/graph-runner.js +3 -0
  113. package/dist/wirings/workflow/graph/wire-workflow-graph.d.ts +6 -0
  114. package/dist/wirings/workflow/graph/wire-workflow-graph.js +1 -0
  115. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +5 -0
  116. package/dist/wirings/workflow/index.d.ts +2 -2
  117. package/dist/wirings/workflow/index.js +1 -1
  118. package/dist/wirings/workflow/pikku-workflow-service.d.ts +59 -1
  119. package/dist/wirings/workflow/pikku-workflow-service.js +187 -0
  120. package/dist/wirings/workflow/workflow.types.d.ts +1 -3
  121. package/package.json +6 -1
  122. package/run-tests.sh +1 -0
  123. package/src/dev/hot-reload.ts +13 -68
  124. package/src/dev/module-runner.test.ts +169 -0
  125. package/src/dev/module-runner.ts +103 -0
  126. package/src/dev/reload-meta.test.ts +31 -2
  127. package/src/dev/reload-meta.ts +26 -0
  128. package/src/errors/errors.ts +24 -0
  129. package/src/function/function-runner.test.ts +276 -142
  130. package/src/function/function-runner.ts +52 -145
  131. package/src/function/functions.types.ts +28 -38
  132. package/src/index.ts +8 -6
  133. package/src/middleware/index.ts +1 -5
  134. package/src/permissions.test.ts +160 -373
  135. package/src/permissions.ts +137 -279
  136. package/src/pikku-state.ts +0 -2
  137. package/src/scopes.test.ts +167 -0
  138. package/src/scopes.ts +69 -0
  139. package/src/services/ai-embedding-service.ts +31 -0
  140. package/src/services/credential-wire-service.ts +10 -4
  141. package/src/services/index.ts +20 -0
  142. package/src/services/meta-service.ts +68 -3
  143. package/src/services/queue-webhook-service.test.ts +408 -0
  144. package/src/services/queue-webhook-service.ts +182 -0
  145. package/src/services/scope-service.ts +75 -0
  146. package/src/services/typed-secret-service.test.ts +35 -0
  147. package/src/services/typed-secret-service.ts +39 -4
  148. package/src/services/webhook-service.ts +180 -0
  149. package/src/services/workflow-service.ts +7 -0
  150. package/src/testing/service-tests.ts +49 -0
  151. package/src/types/core.types.ts +42 -14
  152. package/src/types/state.types.ts +23 -3
  153. package/src/utils/hmac.ts +27 -0
  154. package/src/utils/safe-fetch.test.ts +373 -0
  155. package/src/utils/safe-fetch.ts +213 -0
  156. package/src/wirings/ai-agent/ai-agent-agui.test.ts +1127 -0
  157. package/src/wirings/ai-agent/ai-agent-agui.ts +386 -0
  158. package/src/wirings/ai-agent/ai-agent-authorization.test.ts +204 -0
  159. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +483 -0
  160. package/src/wirings/ai-agent/ai-agent-prepare.ts +324 -13
  161. package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +207 -0
  162. package/src/wirings/ai-agent/ai-agent-runner.test.ts +48 -1
  163. package/src/wirings/ai-agent/ai-agent-runner.ts +28 -0
  164. package/src/wirings/ai-agent/ai-agent-stream.test.ts +301 -10
  165. package/src/wirings/ai-agent/ai-agent-stream.ts +122 -16
  166. package/src/wirings/ai-agent/ai-agent.types.ts +57 -0
  167. package/src/wirings/ai-agent/index.ts +5 -0
  168. package/src/wirings/ai-agent/voice-input.test.ts +90 -0
  169. package/src/wirings/ai-agent/voice-input.ts +9 -40
  170. package/src/wirings/channel/channel-common.ts +0 -1
  171. package/src/wirings/channel/channel-handler.ts +1 -9
  172. package/src/wirings/channel/channel-middleware-runner.test.ts +28 -2
  173. package/src/wirings/channel/channel-middleware-runner.ts +39 -27
  174. package/src/wirings/channel/channel.types.ts +0 -6
  175. package/src/wirings/cli/cli-runner.ts +4 -2
  176. package/src/wirings/cli/cli.types.ts +0 -2
  177. package/src/wirings/cli/command-parser.test.ts +130 -0
  178. package/src/wirings/cli/command-parser.ts +80 -2
  179. package/src/wirings/credential/credential.types.ts +14 -0
  180. package/src/wirings/credential/validate-credential-definitions.ts +1 -0
  181. package/src/wirings/gateway/gateway-authorization.test.ts +444 -0
  182. package/src/wirings/gateway/gateway-runner.ts +114 -68
  183. package/src/wirings/gateway/gateway.types.ts +7 -9
  184. package/src/wirings/http/http-routes.ts +0 -3
  185. package/src/wirings/http/http-runner.test.ts +1 -11
  186. package/src/wirings/http/http-runner.ts +16 -39
  187. package/src/wirings/http/http.types.ts +7 -14
  188. package/src/wirings/mcp/mcp-runner.ts +0 -2
  189. package/src/wirings/mcp/mcp.types.ts +3 -11
  190. package/src/wirings/oauth2/index.ts +0 -3
  191. package/src/wirings/rpc/addon-runner.ts +233 -0
  192. package/src/wirings/rpc/index.ts +17 -1
  193. package/src/wirings/rpc/remote-addon-auth.ts +69 -0
  194. package/src/wirings/rpc/rpc-runner.test.ts +319 -16
  195. package/src/wirings/rpc/rpc-runner.ts +155 -3
  196. package/src/wirings/rpc/rpc-types.ts +16 -5
  197. package/src/wirings/rpc/wire-addon.test.ts +6 -3
  198. package/src/wirings/rpc/wire-addon.ts +9 -0
  199. package/src/wirings/rpc/wire-remote-addon.ts +57 -0
  200. package/src/wirings/scope/index.ts +14 -0
  201. package/src/wirings/scope/scope.test.ts +135 -0
  202. package/src/wirings/scope/scope.types.ts +44 -0
  203. package/src/wirings/scope/validate-scope-definitions.ts +110 -0
  204. package/src/wirings/scope/wire-scope.ts +34 -0
  205. package/src/wirings/secret/secret.types.ts +14 -0
  206. package/src/wirings/secret/validate-secret-definitions.ts +2 -0
  207. package/src/wirings/variable/validate-variable-definitions.ts +2 -0
  208. package/src/wirings/variable/variable.types.ts +14 -0
  209. package/src/wirings/workflow/dsl/index.ts +4 -0
  210. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +97 -6
  211. package/src/wirings/workflow/graph/graph-node.ts +2 -0
  212. package/src/wirings/workflow/graph/graph-runner.test.ts +58 -0
  213. package/src/wirings/workflow/graph/graph-runner.ts +3 -0
  214. package/src/wirings/workflow/graph/wire-workflow-graph.ts +7 -0
  215. package/src/wirings/workflow/graph/workflow-graph.types.ts +5 -0
  216. package/src/wirings/workflow/index.ts +5 -0
  217. package/src/wirings/workflow/pikku-workflow-service.test.ts +341 -0
  218. package/src/wirings/workflow/pikku-workflow-service.ts +272 -0
  219. package/src/wirings/workflow/workflow-on-error.test.ts +154 -0
  220. package/src/wirings/workflow/workflow-step-session.test.ts +21 -5
  221. package/src/wirings/workflow/workflow.types.ts +4 -2
  222. package/tsconfig.tsbuildinfo +1 -1
  223. package/src/wirings/oauth2/oauth2-client.test.ts +0 -930
  224. package/src/wirings/oauth2/oauth2-client.ts +0 -331
  225. package/src/wirings/oauth2/oauth2-routes.ts +0 -234
@@ -6,12 +6,12 @@ import {
6
6
  getFunctionNames,
7
7
  runPikkuFunc,
8
8
  } from './function-runner.js'
9
- import { addTagMiddleware, addTagPermission } from '../index.js'
9
+ import { addGlobalPermission, addTagMiddleware } from '../index.js'
10
10
  import { resetPikkuState, pikkuState } from '../pikku-state.js'
11
11
  import type { CoreServices, CorePikkuMiddleware } from '../types/core.types.js'
12
12
  import type { CorePermissionGroup } from './functions.types.js'
13
13
  import { PikkuSessionService } from '../services/user-session-service.js'
14
- import { ReadonlySessionError } from '../errors/errors.js'
14
+ import { MissingScopeError, ReadonlySessionError } from '../errors/errors.js'
15
15
  import { createInvocationAudit } from '../services/audit-service.js'
16
16
 
17
17
  beforeEach(() => {
@@ -21,21 +21,17 @@ beforeEach(() => {
21
21
  // Helper function to add function with metadata for tests
22
22
  const addTestFunction = (funcName: string, funcConfig: any) => {
23
23
  addFunction(funcName, funcConfig)
24
- // Convert tags to middleware metadata
24
+ // Convert tags to middleware metadata (tags are organizational only; they no
25
+ // longer carry permissions in the session+function authorization model).
25
26
  const middleware = funcConfig.tags
26
27
  ? funcConfig.tags.map((tag: string) => ({ type: 'tag' as const, tag }))
27
28
  : undefined
28
- // Convert tags to permissions metadata
29
- const permissions = funcConfig.tags
30
- ? funcConfig.tags.map((tag: string) => ({ type: 'tag' as const, tag }))
31
- : undefined
32
29
  pikkuState(null, 'function', 'meta')[funcName] = {
33
30
  pikkuFuncId: funcName,
34
31
  inputSchemaName: null,
35
32
  outputSchemaName: null,
36
33
  sessionless: true,
37
34
  middleware,
38
- permissions,
39
35
  }
40
36
  }
41
37
 
@@ -101,31 +97,16 @@ describe('runPikkuFunc - Integration Tests', () => {
101
97
  ])
102
98
  })
103
99
 
104
- test('should execute permissions in correct order: wiringTags wiringPermissions → funcTags → funcPermissions', async () => {
100
+ test('runs global (AND) then function (OR) permissions before the handler', async () => {
105
101
  const executionOrder: string[] = []
106
102
 
107
- // Setup tagged permissions
108
- const wiringTagPermission = async () => {
109
- executionOrder.push('wiringTag')
110
- return true
111
- }
112
- const funcTagPermission = async () => {
113
- executionOrder.push('funcTag')
114
- return true
115
- }
116
-
117
- addTagPermission('wiringTag', [wiringTagPermission])
118
- addTagPermission('funcTag', [funcTagPermission])
103
+ addGlobalPermission([
104
+ async () => {
105
+ executionOrder.push('globalPermission')
106
+ return true
107
+ },
108
+ ])
119
109
 
120
- // Setup direct permissions
121
- const wiringPermissions: CorePermissionGroup = {
122
- permissions: [
123
- async () => {
124
- executionOrder.push('wiringPermission')
125
- return true
126
- },
127
- ],
128
- }
129
110
  const funcPermissions: CorePermissionGroup = {
130
111
  permissions: [
131
112
  async () => {
@@ -135,14 +116,12 @@ describe('runPikkuFunc - Integration Tests', () => {
135
116
  ],
136
117
  }
137
118
 
138
- // Register function with permissions and tags
139
119
  addTestFunction('testFunc', {
140
120
  func: async () => {
141
121
  executionOrder.push('main')
142
122
  return 'success'
143
123
  },
144
124
  permissions: funcPermissions,
145
- tags: ['funcTag'],
146
125
  })
147
126
 
148
127
  const result = await runPikkuFunc(
@@ -153,49 +132,33 @@ describe('runPikkuFunc - Integration Tests', () => {
153
132
  singletonServices: mockSingletonServices,
154
133
  getAllServices: () => mockServices,
155
134
  data: () => ({}),
156
- wirePermissions: wiringPermissions,
157
- tags: ['wiringTag'],
158
135
  auth: false,
159
136
  wire: {},
160
137
  }
161
138
  )
162
139
 
163
140
  assert.equal(result, 'success')
164
- // Order: wiringTags → main (short-circuit on first passing group)
165
- assert.deepEqual(executionOrder, ['wiringTag', 'main'])
166
- })
167
-
168
- test('should throw specific error for wiring tag permission failures', async () => {
169
- const failingWiringTagPermission = async () => false
170
-
171
- addTagPermission('wiringTag', [failingWiringTagPermission])
172
-
173
- addTestFunction('testFunc', {
174
- func: async () => 'success',
175
- })
176
-
177
- await assert.rejects(
178
- runPikkuFunc('rpc', Math.random().toString(), 'testFunc', {
179
- singletonServices: mockSingletonServices,
180
- getAllServices: () => mockServices,
181
- data: () => ({}),
182
- tags: ['wiringTag'],
183
- auth: false,
184
- wire: {},
185
- }),
186
- {
187
- message: 'Permission denied',
188
- }
189
- )
141
+ assert.deepEqual(executionOrder, [
142
+ 'globalPermission',
143
+ 'funcPermission',
144
+ 'main',
145
+ ])
190
146
  })
191
147
 
192
- test('should throw specific error for wiring permission failures', async () => {
193
- const wiringPermissions: CorePermissionGroup = {
194
- permissions: [async () => false],
195
- }
148
+ test('a failing global permission denies before the function permission runs', async () => {
149
+ let funcPermissionRan = false
150
+ addGlobalPermission([async () => false])
196
151
 
197
152
  addTestFunction('testFunc', {
198
153
  func: async () => 'success',
154
+ permissions: {
155
+ allow: [
156
+ async () => {
157
+ funcPermissionRan = true
158
+ return true
159
+ },
160
+ ],
161
+ },
199
162
  })
200
163
 
201
164
  await assert.rejects(
@@ -203,7 +166,6 @@ describe('runPikkuFunc - Integration Tests', () => {
203
166
  singletonServices: mockSingletonServices,
204
167
  getAllServices: () => mockServices,
205
168
  data: () => ({}),
206
- wirePermissions: wiringPermissions,
207
169
  auth: false,
208
170
  wire: {},
209
171
  }),
@@ -211,16 +173,15 @@ describe('runPikkuFunc - Integration Tests', () => {
211
173
  message: 'Permission denied',
212
174
  }
213
175
  )
176
+ assert.equal(funcPermissionRan, false)
214
177
  })
215
178
 
216
- test('should throw specific error for function tag permission failures', async () => {
217
- const failingFuncTagPermission = async () => false
218
-
219
- addTagPermission('funcTag', [failingFuncTagPermission])
179
+ test('a passing global permission does not satisfy a failing function permission', async () => {
180
+ addGlobalPermission([async () => true])
220
181
 
221
182
  addTestFunction('testFunc', {
222
183
  func: async () => 'success',
223
- tags: ['funcTag'],
184
+ permissions: { admin: [async () => false] },
224
185
  })
225
186
 
226
187
  await assert.rejects(
@@ -294,39 +255,31 @@ describe('runPikkuFunc - Integration Tests', () => {
294
255
  assert.equal(executionCount, 1)
295
256
  })
296
257
 
297
- test('should handle mixed permission types correctly', async () => {
258
+ test('should OR function permission groups and short-circuit on the first pass', async () => {
298
259
  const executionOrder: string[] = []
299
260
 
300
- // Mix of array and object permissions
301
- const arrayPermission = [
302
- async () => {
303
- executionOrder.push('arrayPermission1')
304
- return false
305
- },
306
- async () => {
307
- executionOrder.push('arrayPermission2')
308
- return true
309
- },
310
- ]
311
-
312
- const objectPermission: CorePermissionGroup = {
313
- permissions: [
261
+ // Two groups ORed together: the first (owner) fails, the second (admin) passes.
262
+ const permissions: CorePermissionGroup = {
263
+ owner: [
264
+ async () => {
265
+ executionOrder.push('owner')
266
+ return false
267
+ },
268
+ ],
269
+ admin: [
314
270
  async () => {
315
- executionOrder.push('objectPermission')
271
+ executionOrder.push('admin')
316
272
  return true
317
273
  },
318
274
  ],
319
275
  }
320
276
 
321
- addTagPermission('mixedTag', arrayPermission)
322
-
323
277
  addTestFunction('testFunc', {
324
278
  func: async () => {
325
279
  executionOrder.push('main')
326
280
  return 'success'
327
281
  },
328
- permissions: objectPermission,
329
- tags: ['mixedTag'],
282
+ permissions,
330
283
  })
331
284
 
332
285
  const result = await runPikkuFunc(
@@ -343,12 +296,7 @@ describe('runPikkuFunc - Integration Tests', () => {
343
296
  )
344
297
 
345
298
  assert.equal(result, 'success')
346
- // Should execute tag permissions and short-circuit before function permissions
347
- assert.deepEqual(executionOrder, [
348
- 'arrayPermission1',
349
- 'arrayPermission2',
350
- 'main',
351
- ])
299
+ assert.deepEqual(executionOrder, ['owner', 'admin', 'main'])
352
300
  })
353
301
 
354
302
  test('should work without any middleware or permissions', async () => {
@@ -372,7 +320,7 @@ describe('runPikkuFunc - Integration Tests', () => {
372
320
  assert.equal(result, 'simple success')
373
321
  })
374
322
 
375
- test('should work with only wiring-level middleware and permissions', async () => {
323
+ test('should work with only wiring-level middleware', async () => {
376
324
  const executionOrder: string[] = []
377
325
 
378
326
  const wiringMiddleware: CorePikkuMiddleware = async (
@@ -384,15 +332,6 @@ describe('runPikkuFunc - Integration Tests', () => {
384
332
  await next()
385
333
  }
386
334
 
387
- const wiringPermissions: CorePermissionGroup = {
388
- permissions: [
389
- async () => {
390
- executionOrder.push('wiringPermission')
391
- return true
392
- },
393
- ],
394
- }
395
-
396
335
  addTestFunction('testFunc', {
397
336
  func: async () => {
398
337
  executionOrder.push('main')
@@ -409,19 +348,13 @@ describe('runPikkuFunc - Integration Tests', () => {
409
348
  getAllServices: () => mockServices,
410
349
  data: () => ({}),
411
350
  wireMiddleware: [wiringMiddleware],
412
- wirePermissions: wiringPermissions,
413
351
  auth: false,
414
352
  wire: {},
415
353
  }
416
354
  )
417
355
 
418
356
  assert.equal(result, 'success')
419
- // Permissions run after middleware (middleware can set/modify session)
420
- assert.deepEqual(executionOrder, [
421
- 'wiringMiddleware',
422
- 'wiringPermission',
423
- 'main',
424
- ])
357
+ assert.deepEqual(executionOrder, ['wiringMiddleware', 'main'])
425
358
  })
426
359
 
427
360
  test('should work with only function-level middleware and permissions', async () => {
@@ -1212,7 +1145,10 @@ describe('runPikkuFunc - Integration Tests', () => {
1212
1145
  addTestFunction('auditedInner', {
1213
1146
  audit: true,
1214
1147
  func: async (services: any) => {
1215
- await services.auditLog.write({ type: 'booking.update', source: 'explicit' })
1148
+ await services.auditLog.write({
1149
+ type: 'booking.update',
1150
+ source: 'explicit',
1151
+ })
1216
1152
  return 'inner-ok'
1217
1153
  },
1218
1154
  })
@@ -1223,15 +1159,20 @@ describe('runPikkuFunc - Integration Tests', () => {
1223
1159
  },
1224
1160
  })
1225
1161
 
1226
- const result = await runPikkuFunc('http', 'exposed-rpc-route', 'rpcCallerLike', {
1227
- singletonServices,
1228
- data: () => ({}),
1229
- auth: false,
1230
- wire: {},
1231
- createWireServices: async (services: any, wire: any) => ({
1232
- auditLog: createInvocationAudit(services.audit, wire),
1233
- }),
1234
- })
1162
+ const result = await runPikkuFunc(
1163
+ 'http',
1164
+ 'exposed-rpc-route',
1165
+ 'rpcCallerLike',
1166
+ {
1167
+ singletonServices,
1168
+ data: () => ({}),
1169
+ auth: false,
1170
+ wire: {},
1171
+ createWireServices: async (services: any, wire: any) => ({
1172
+ auditLog: createInvocationAudit(services.audit, wire),
1173
+ }),
1174
+ }
1175
+ )
1235
1176
 
1236
1177
  assert.equal(result, 'inner-ok')
1237
1178
  assert.equal(events.length, 1)
@@ -1249,7 +1190,10 @@ describe('runPikkuFunc - Integration Tests', () => {
1249
1190
  addTestFunction('auditedChildWriter', {
1250
1191
  audit: true,
1251
1192
  func: async (services: any) => {
1252
- await services.auditLog.write({ type: 'child.event', source: 'explicit' })
1193
+ await services.auditLog.write({
1194
+ type: 'child.event',
1195
+ source: 'explicit',
1196
+ })
1253
1197
  return 'child-ok'
1254
1198
  },
1255
1199
  })
@@ -1257,17 +1201,25 @@ describe('runPikkuFunc - Integration Tests', () => {
1257
1201
  addTestFunction('auditedParentWriter', {
1258
1202
  audit: true,
1259
1203
  func: async (services: any, _data: any, wire: any) => {
1260
- await services.auditLog.write({ type: 'parent.event', source: 'explicit' })
1204
+ await services.auditLog.write({
1205
+ type: 'parent.event',
1206
+ source: 'explicit',
1207
+ })
1261
1208
  return wire.rpc.invoke('auditedChildWriter', {})
1262
1209
  },
1263
1210
  })
1264
1211
 
1265
- const result = await runPikkuFunc('rpc', 'audited-parent-child', 'auditedParentWriter', {
1266
- singletonServices,
1267
- data: () => ({}),
1268
- auth: false,
1269
- wire: {},
1270
- })
1212
+ const result = await runPikkuFunc(
1213
+ 'rpc',
1214
+ 'audited-parent-child',
1215
+ 'auditedParentWriter',
1216
+ {
1217
+ singletonServices,
1218
+ data: () => ({}),
1219
+ auth: false,
1220
+ wire: {},
1221
+ }
1222
+ )
1271
1223
 
1272
1224
  assert.equal(result, 'child-ok')
1273
1225
  const byType = Object.fromEntries(events.map((e) => [e.type, e.functionId]))
@@ -1279,26 +1231,41 @@ describe('runPikkuFunc - Integration Tests', () => {
1279
1231
  const warnings: any[] = []
1280
1232
  const singletonServices = {
1281
1233
  ...mockSingletonServices,
1282
- logger: { ...mockSingletonServices.logger, warn: (msg: any) => warnings.push(msg) },
1234
+ logger: {
1235
+ ...mockSingletonServices.logger,
1236
+ warn: (msg: any) => warnings.push(msg),
1237
+ },
1283
1238
  audit: { audit: async () => {} },
1284
1239
  }
1285
1240
 
1286
1241
  addTestFunction('unauditedWriter', {
1287
1242
  func: async (services: any) => {
1288
- await services.auditLog.write({ type: 'dropped.event', source: 'explicit' })
1243
+ await services.auditLog.write({
1244
+ type: 'dropped.event',
1245
+ source: 'explicit',
1246
+ })
1289
1247
  return 'ok'
1290
1248
  },
1291
1249
  })
1292
1250
 
1293
- const result = await runPikkuFunc('rpc', 'unaudited-writer', 'unauditedWriter', {
1294
- singletonServices,
1295
- data: () => ({}),
1296
- auth: false,
1297
- wire: {},
1298
- createWireServices: async (services: any, wire: any) => ({
1299
- auditLog: createInvocationAudit(services.audit, wire, services.logger),
1300
- }),
1301
- })
1251
+ const result = await runPikkuFunc(
1252
+ 'rpc',
1253
+ 'unaudited-writer',
1254
+ 'unauditedWriter',
1255
+ {
1256
+ singletonServices,
1257
+ data: () => ({}),
1258
+ auth: false,
1259
+ wire: {},
1260
+ createWireServices: async (services: any, wire: any) => ({
1261
+ auditLog: createInvocationAudit(
1262
+ services.audit,
1263
+ wire,
1264
+ services.logger
1265
+ ),
1266
+ }),
1267
+ }
1268
+ )
1302
1269
 
1303
1270
  assert.equal(result, 'ok')
1304
1271
  assert.equal(warnings.length, 1)
@@ -1361,3 +1328,170 @@ describe('function-runner helpers', () => {
1361
1328
  ])
1362
1329
  })
1363
1330
  })
1331
+
1332
+ describe('runPikkuFunc - scopes', () => {
1333
+ const runWithScopes = (
1334
+ funcName: string,
1335
+ {
1336
+ scopes,
1337
+ sessionScopes,
1338
+ data = () => ({}),
1339
+ }: {
1340
+ scopes?: string[]
1341
+ sessionScopes?: string[]
1342
+ data?: () => any
1343
+ }
1344
+ ) =>
1345
+ runPikkuFunc('rpc', Math.random().toString(), funcName, {
1346
+ singletonServices: mockSingletonServices,
1347
+ data,
1348
+ auth: false,
1349
+ wire: { session: { userId: 'u1', scopes: sessionScopes } as any },
1350
+ })
1351
+
1352
+ test('runs the function when the session holds the required scope', async () => {
1353
+ addTestFunction('scoped', {
1354
+ func: async () => 'ok',
1355
+ scopes: ['invoices:create'],
1356
+ })
1357
+
1358
+ const result = await runWithScopes('scoped', {
1359
+ sessionScopes: ['invoices:create'],
1360
+ })
1361
+
1362
+ assert.equal(result, 'ok')
1363
+ })
1364
+
1365
+ test('throws MissingScopeError when the session lacks the scope', async () => {
1366
+ addTestFunction('scoped', {
1367
+ func: async () => 'ok',
1368
+ scopes: ['invoices:create'],
1369
+ })
1370
+
1371
+ await assert.rejects(
1372
+ () => runWithScopes('scoped', { sessionScopes: ['billing:read'] }),
1373
+ MissingScopeError
1374
+ )
1375
+ })
1376
+
1377
+ test('honours wildcards held by the session', async () => {
1378
+ addTestFunction('scoped', {
1379
+ func: async () => 'ok',
1380
+ scopes: ['admin:invoices:create'],
1381
+ })
1382
+
1383
+ const result = await runWithScopes('scoped', { sessionScopes: ['admin:*'] })
1384
+
1385
+ assert.equal(result, 'ok')
1386
+ })
1387
+
1388
+ test('does not gate a function that declares no scopes', async () => {
1389
+ addTestFunction('unscoped', { func: async () => 'ok' })
1390
+
1391
+ const result = await runWithScopes('unscoped', { sessionScopes: [] })
1392
+
1393
+ assert.equal(result, 'ok')
1394
+ })
1395
+
1396
+ test('reads scopes from function meta when the config omits them', async () => {
1397
+ addTestFunction('metaScoped', { func: async () => 'ok' })
1398
+ pikkuState(null, 'function', 'meta').metaScoped.scopes = ['invoices:create']
1399
+
1400
+ await assert.rejects(
1401
+ () => runWithScopes('metaScoped', { sessionScopes: [] }),
1402
+ MissingScopeError
1403
+ )
1404
+ })
1405
+
1406
+ // The load-bearing test. Scopes are an AND gate separate from permissions, so
1407
+ // a passing global (or function) permission must never satisfy a scope.
1408
+ test('a passing global permission does not satisfy a scope', async () => {
1409
+ addGlobalPermission([async () => true])
1410
+ addTestFunction('scoped', { func: async () => 'ok', scopes: ['admin'] })
1411
+
1412
+ await assert.rejects(
1413
+ () => runWithScopes('scoped', { sessionScopes: [] }),
1414
+ MissingScopeError
1415
+ )
1416
+ })
1417
+
1418
+ test('a passing function permission does not satisfy a scope', async () => {
1419
+ addTestFunction('scoped', {
1420
+ func: async () => 'ok',
1421
+ scopes: ['admin'],
1422
+ permissions: { allow: [async () => true] },
1423
+ })
1424
+
1425
+ await assert.rejects(
1426
+ () => runWithScopes('scoped', { sessionScopes: [] }),
1427
+ MissingScopeError
1428
+ )
1429
+ })
1430
+
1431
+ test('a held scope does not bypass a failing permission', async () => {
1432
+ addTestFunction('scoped', {
1433
+ func: async () => 'ok',
1434
+ scopes: ['admin'],
1435
+ permissions: { deny: [async () => false] },
1436
+ })
1437
+
1438
+ await assert.rejects(
1439
+ () => runWithScopes('scoped', { sessionScopes: ['admin'] }),
1440
+ { message: 'Permission denied' }
1441
+ )
1442
+ })
1443
+
1444
+ test('a failing scope short-circuits before permissions run', async () => {
1445
+ let permissionRan = false
1446
+ addTestFunction('scoped', {
1447
+ func: async () => 'ok',
1448
+ scopes: ['admin'],
1449
+ permissions: {
1450
+ allow: [
1451
+ async () => {
1452
+ permissionRan = true
1453
+ return true
1454
+ },
1455
+ ],
1456
+ },
1457
+ })
1458
+
1459
+ await assert.rejects(
1460
+ () => runWithScopes('scoped', { sessionScopes: [] }),
1461
+ MissingScopeError
1462
+ )
1463
+ assert.equal(permissionRan, false)
1464
+ })
1465
+
1466
+ // Scopes depend only on the session, so a denied request must never pay to
1467
+ // parse or validate its body.
1468
+ test('a failing scope short-circuits before the data is evaluated', async () => {
1469
+ addTestFunction('scoped', { func: async () => 'ok', scopes: ['admin'] })
1470
+
1471
+ await assert.rejects(
1472
+ () =>
1473
+ runWithScopes('scoped', {
1474
+ sessionScopes: [],
1475
+ data: () => {
1476
+ throw new Error('data should not have been evaluated')
1477
+ },
1478
+ }),
1479
+ MissingScopeError
1480
+ )
1481
+ })
1482
+
1483
+ test('fails closed when there is no session at all', async () => {
1484
+ addTestFunction('scoped', { func: async () => 'ok', scopes: ['admin'] })
1485
+
1486
+ await assert.rejects(
1487
+ () =>
1488
+ runPikkuFunc('rpc', Math.random().toString(), 'scoped', {
1489
+ singletonServices: mockSingletonServices,
1490
+ data: () => ({}),
1491
+ auth: false,
1492
+ wire: {},
1493
+ }),
1494
+ MissingScopeError
1495
+ )
1496
+ })
1497
+ })