@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
@@ -1,8 +1,6 @@
1
1
  import type {
2
2
  CoreServices,
3
3
  CoreUserSession,
4
- PikkuWiringTypes,
5
- PermissionMetadata,
6
4
  PikkuWire,
7
5
  } from './types/core.types.js'
8
6
  import type {
@@ -11,16 +9,25 @@ import type {
11
9
  } from './function/functions.types.js'
12
10
  import { pikkuState } from './pikku-state.js'
13
11
  import { ForbiddenError } from './errors/errors.js'
14
- import { freezeDedupe, getTagGroups } from './utils.js'
15
12
 
16
13
  /**
17
- * 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.
14
+ * Evaluates a single permission group. A group ORs its branches together and
15
+ * ANDs the entries within a branch: the group passes if at least one branch
16
+ * (an array of permission functions, all of which must return true — or a lone
17
+ * function) is satisfied.
18
+ *
19
+ * This is the only place OR lives in authorization. Everything cross-cutting —
20
+ * global permissions, scopes, and auth — ANDs together; a function's own
21
+ * `permissions` group is the sole place where "owner OR admin"-style
22
+ * alternatives are expressed.
23
+ *
24
+ * @param permissions - The permission group to evaluate.
18
25
  * @param services - The core services required for permission validation.
19
- * @param data - The data to be used in the permission validation functions.
26
+ * @param data - The request data passed to each permission function.
20
27
  * @param wire - The wire object containing request/response context and session.
21
- * @returns A promise that resolves to void.
28
+ * @returns A promise resolving to true if the group is satisfied.
22
29
  */
23
- const verifyPermissions = async <Out = any>(
30
+ const verifyPermissions = async (
24
31
  permissions: CorePermissionGroup,
25
32
  services: CoreServices,
26
33
  data: any,
@@ -30,7 +37,6 @@ const verifyPermissions = async <Out = any>(
30
37
  return true
31
38
  }
32
39
 
33
- let valid = false
34
40
  const permissionGroups = Object.values(permissions)
35
41
  if (permissionGroups.length === 0) {
36
42
  return true
@@ -42,69 +48,39 @@ const verifyPermissions = async <Out = any>(
42
48
  funcs.map((func) => func(services, data, wire))
43
49
  )
44
50
  if (permissioned.every((result) => result)) {
45
- valid = true
51
+ return true
46
52
  }
47
53
  } else {
48
- valid = await funcs(services, data, wire as any)
49
- }
50
- if (valid) {
51
- return true
54
+ if (await funcs(services, data, wire as any)) {
55
+ return true
56
+ }
52
57
  }
53
58
  }
54
59
  return false
55
60
  }
56
61
 
57
- /**
58
- * Retrieves a registered permission function by its name.
59
- *
60
- * This function looks up permissions that was registered with registerPermission.
61
- * It's used internally by the framework to resolve permission references in metadata.
62
- *
63
- * @param {string} name - The unique name (pikkuFuncId) of the permission function.
64
- * @returns {CorePikkuPermission | undefined} The permission function, or undefined if not found.
65
- *
66
- * @internal
67
- */
68
- const getPermissionByName = (name: string): CorePikkuPermission | undefined => {
69
- const permissionStore = pikkuState(null, 'misc', 'permissions')
70
- const permission = permissionStore[name]
71
- if (Array.isArray(permission) && permission.length === 1) {
72
- return permission[0]
73
- }
74
- return undefined
75
- }
76
62
 
77
- /**
78
- * Registers tag-scoped permissions. Applies to any wiring that carries the
79
- * matching tag.
80
- *
81
- * For tree-shaking, wrap in a factory:
82
- * `export const x = () => addTagPermission('tag', [...])`
83
- *
84
- * @example
85
- * export const adminPermissions = () => addTagPermission('admin', [adminPermission])
86
- */
87
- export const addTagPermission = (
88
- tag: string,
89
- permissions: CorePermissionGroup | CorePikkuPermission[],
90
- packageName: string | null = null
91
- ): CorePermissionGroup | CorePikkuPermission[] => {
92
- const tagGroups = pikkuState(packageName, 'permissions', 'tagGroup')
93
- if (tagGroups[tag]) {
94
- throw new Error(
95
- `Permissions for tag '${tag}' already exist. Use a different tag or remove the existing permissions first.`
96
- )
63
+ const globalPermissionsCache: Record<
64
+ string,
65
+ readonly (CorePermissionGroup | CorePikkuPermission)[]
66
+ > = {}
67
+
68
+ export const clearPermissionsCache = () => {
69
+ for (const key of Object.keys(globalPermissionsCache)) {
70
+ delete globalPermissionsCache[key]
97
71
  }
98
- tagGroups[tag] = permissions
99
- return permissions
100
72
  }
101
73
 
102
74
  /**
103
- * Registers wire-agnostic global permissions. Runs at the top of every
104
- * wiring's permission resolution before wire-, tag-, and function-level
105
- * entries across all wire types.
75
+ * Registers wire-agnostic global permissions. Global permissions are an AND
76
+ * gate: every registered requirement must pass on every function invocation,
77
+ * regardless of transport. A requirement may be a single permission function
78
+ * or a permission group (in which case that group ORs internally, but the
79
+ * group as a whole must still pass).
106
80
  *
107
- * Resolution order: global wire tag function.
81
+ * Global permissions never satisfy a function's own `permissions` — the two
82
+ * are independent gates that both have to pass, so a broad global can only
83
+ * ever narrow access.
108
84
  *
109
85
  * @example
110
86
  * addGlobalPermission([signedInUser])
@@ -122,257 +98,129 @@ export const addGlobalPermission = (
122
98
  } else {
123
99
  state.push(permissions)
124
100
  }
101
+ clearPermissionsCache()
125
102
  return permissions
126
103
  }
127
104
 
128
- const combinedPermissionsCache: Record<
129
- PikkuWiringTypes,
130
- Record<string, readonly (CorePermissionGroup | CorePikkuPermission)[]>
131
- > = {
132
- http: {},
133
- rpc: {},
134
- channel: {},
135
- queue: {},
136
- scheduler: {},
137
- trigger: {},
138
- mcp: {},
139
- agent: {},
140
- cli: {},
141
- workflow: {},
142
- gateway: {},
143
- }
144
-
145
- export const clearPermissionsCache = () => {
146
- for (const key of Object.keys(
147
- combinedPermissionsCache
148
- ) as PikkuWiringTypes[]) {
149
- combinedPermissionsCache[key] = {}
150
- }
105
+ /**
106
+ * @deprecated Tag-level permissions were removed in #972 — permissions are now
107
+ * function-scoped only (declare them on the function via `pikkuFunc({ permissions })`).
108
+ * This throwing stub exists solely so the pinned bootstrap CLI (which still
109
+ * generates an `addTagPermission` wrapper) can resolve the import at build time;
110
+ * it is never called. Delete once `PIKKU_CLI_VERSION` in the CLI build is bumped
111
+ * past the release that removes tag permissions.
112
+ */
113
+ export const addTagPermission = (
114
+ _tag: string,
115
+ _permissions: CorePermissionGroup | CorePikkuPermission[],
116
+ _packageName: string | null = null
117
+ ): never => {
118
+ throw new Error(
119
+ 'addTagPermission was removed in #972 — tag-level permissions no longer exist. Declare permissions on the function definition instead: pikkuFunc({ permissions }).'
120
+ )
151
121
  }
152
122
 
153
123
  /**
154
- * Combines wiring-specific permissions with function-level permissions.
155
- *
156
- * This function resolves permission metadata into actual permission functions and combines them.
157
- * It filters out wire permissions without tags from inheritedPermissions to avoid duplication
158
- * (those are passed separately as wirePermissions).
159
- *
160
- * @param {object} options - Configuration object for combining permissions.
161
- * @param {PermissionMetadata[] | undefined} options.wireInheritedPermissions - Metadata from wiring (HTTP + tags + wire with tags).
162
- * @param {CorePermissionGroup | CorePikkuPermission[] | undefined} options.wirePermissions - Inline wire permissions.
163
- * @param {PermissionMetadata[] | undefined} options.funcInheritedPermissions - Function permissions metadata (only tags).
164
- * @param {CorePermissionGroup | CorePikkuPermission[] | undefined} options.funcPermissions - Inline function permissions.
165
- * @returns {(CorePermissionGroup | CorePikkuPermission)[]} Combined array of resolved permissions.
166
- *
167
- * @example
168
- * ```typescript
169
- * const combined = combinePermissions(wireType, wireId, {
170
- * wireInheritedPermissions: meta.permissions,
171
- * wirePermissions: inlinePermissions,
172
- * funcInheritedPermissions: funcMeta.permissions,
173
- * funcPermissions: funcConfig.permissions
174
- * })
175
- * ```
124
+ * Resolves the registered global permission requirements for a package,
125
+ * caching the resolved array per package. The cache is keyed by package name
126
+ * (an empty string for the root package) so packages never collide, and it is
127
+ * cleared whenever globals are registered or on hot reload.
176
128
  */
177
- const combinePermissions = (
178
- wireType: PikkuWiringTypes,
179
- uid: string,
180
- {
181
- wireInheritedPermissions,
182
- wirePermissions,
183
- funcInheritedPermissions,
184
- funcPermissions,
185
- packageName = null,
186
- }: {
187
- wireInheritedPermissions?: PermissionMetadata[]
188
- wirePermissions?: CorePermissionGroup | CorePikkuPermission[]
189
- funcInheritedPermissions?: PermissionMetadata[]
190
- funcPermissions?: CorePermissionGroup | CorePikkuPermission[]
191
- packageName?: string | null
192
- } = {}
129
+ const resolveGlobalPermissions = (
130
+ packageName: string | null
193
131
  ): readonly (CorePermissionGroup | CorePikkuPermission)[] => {
194
- if (combinedPermissionsCache[wireType][uid]) {
195
- return combinedPermissionsCache[wireType][uid]
132
+ const key = packageName ?? ''
133
+ const cached = globalPermissionsCache[key]
134
+ if (cached) {
135
+ return cached
196
136
  }
197
-
198
- const resolved: (CorePermissionGroup | CorePikkuPermission)[] = []
199
-
200
137
  const globals = pikkuState(
201
138
  packageName,
202
139
  'permissions',
203
140
  'global'
204
141
  ) as unknown as (CorePermissionGroup | CorePikkuPermission)[]
205
- if (globals && globals.length > 0) {
206
- resolved.push(...globals)
207
- }
208
-
209
- if (wireInheritedPermissions) {
210
- for (const meta of wireInheritedPermissions) {
211
- if (meta.type === 'http') {
212
- // Look up HTTP permission group from pikkuState
213
- const group = pikkuState(packageName, 'permissions', 'httpGroup')[
214
- meta.route
215
- ]
216
- if (group) {
217
- if (Array.isArray(group)) {
218
- resolved.push(...group)
219
- } else {
220
- resolved.push(group)
221
- }
222
- }
223
- } else if (meta.type === 'tag') {
224
- const groups = getTagGroups(
225
- pikkuState(packageName, 'permissions', 'tagGroup'),
226
- meta.tag
227
- )
228
- for (const group of groups) {
229
- if (Array.isArray(group)) {
230
- resolved.push(...group)
231
- } else {
232
- resolved.push(group)
233
- }
234
- }
235
- } else if (meta.type === 'wire') {
236
- // Individual wire permission (exported, not inline)
237
- const permission = getPermissionByName(meta.name)
238
- if (permission) {
239
- resolved.push(permission)
240
- }
241
- }
242
- }
243
- }
244
-
245
- // 2. Add inline wire permissions
246
- if (wirePermissions) {
247
- if (Array.isArray(wirePermissions)) {
248
- resolved.push(...wirePermissions)
249
- } else {
250
- resolved.push(wirePermissions)
251
- }
252
- }
253
-
254
- // 3. Resolve function inherited permissions (only tags, wire permissions already handled)
255
- if (funcInheritedPermissions) {
256
- for (const meta of funcInheritedPermissions) {
257
- if (meta.type === 'tag') {
258
- const groups = getTagGroups(
259
- pikkuState(packageName, 'permissions', 'tagGroup'),
260
- meta.tag
261
- )
262
- for (const group of groups) {
263
- if (Array.isArray(group)) {
264
- resolved.push(...group)
265
- } else {
266
- resolved.push(group)
267
- }
268
- }
269
- }
270
- // Note: wire permissions are already handled in wireInheritedPermissions
271
- }
272
- }
273
-
274
- // 4. Add inline function permissions
275
- if (funcPermissions) {
276
- if (Array.isArray(funcPermissions)) {
277
- resolved.push(...funcPermissions)
278
- } else {
279
- resolved.push(funcPermissions)
280
- }
281
- }
282
-
283
- // Deduplicate and freeze
284
- combinedPermissionsCache[wireType][uid] = freezeDedupe(resolved) as readonly (
285
- | CorePermissionGroup
286
- | CorePikkuPermission
287
- )[]
288
-
289
- return combinedPermissionsCache[wireType][uid]
142
+ const resolved = globals && globals.length > 0 ? [...globals] : []
143
+ globalPermissionsCache[key] = resolved
144
+ return resolved
290
145
  }
291
146
 
147
+ const asGroup = (
148
+ entry: CorePermissionGroup | CorePikkuPermission
149
+ ): CorePermissionGroup =>
150
+ typeof entry === 'function' ? { permission: entry } : entry
151
+
292
152
  /**
293
- * Runs permission checks using combined permissions from all sources.
294
- * Combines permissions from wire and function levels, then validates them.
153
+ * Runs authorization for a function invocation. Two independent gates, both of
154
+ * which must pass:
155
+ *
156
+ * 1. Global permissions — AND. Every registered global requirement must pass.
157
+ * 2. Function permissions — OR. The function's own `permissions` group must be
158
+ * satisfied (see {@link verifyPermissions}).
159
+ *
160
+ * A passing global requirement never contributes to the function gate, so a
161
+ * broad global like `signedIn` can't satisfy an admin-only function.
295
162
  */
296
- export const runPermissions = async (
297
- wireType: PikkuWiringTypes,
298
- uid: string,
299
- {
300
- wireInheritedPermissions,
301
- wirePermissions,
302
- funcInheritedPermissions,
303
- funcPermissions,
304
- services,
305
- wire,
306
- data,
307
- packageName = null,
308
- }: {
309
- wireInheritedPermissions?: PermissionMetadata[]
310
- wirePermissions?: CorePermissionGroup | CorePikkuPermission[]
311
- funcInheritedPermissions?: PermissionMetadata[]
312
- funcPermissions?: CorePermissionGroup | CorePikkuPermission[]
313
- services: CoreServices
314
- wire: PikkuWire<any, never, any, CoreUserSession, never, never, never>
315
- data: any
316
- packageName?: string | null
163
+ export const runPermissions = async ({
164
+ funcPermissions,
165
+ services,
166
+ wire,
167
+ data,
168
+ packageName = null,
169
+ label = 'function',
170
+ }: {
171
+ funcPermissions?: CorePermissionGroup | CorePikkuPermission[]
172
+ services: CoreServices
173
+ wire: PikkuWire<any, never, any, CoreUserSession, never, never, never>
174
+ data: any
175
+ packageName?: string | null
176
+ /** What the non-global gate is called in debug logs, e.g. 'function', 'agent'. */
177
+ label?: string
178
+ }) => {
179
+ const globals = resolveGlobalPermissions(packageName)
180
+ for (const entry of globals) {
181
+ if (!(await verifyPermissions(asGroup(entry), services, data, wire))) {
182
+ services.logger.debug('Permission denied - global permission')
183
+ throw new ForbiddenError('Permission denied')
184
+ }
317
185
  }
318
- ) => {
319
- // Combine all permissions: wireInheritedPermissions → wirePermissions → funcInheritedPermissions → funcPermissions
320
- const allPermissions = combinePermissions(wireType, uid, {
321
- wireInheritedPermissions,
322
- wirePermissions,
323
- funcInheritedPermissions,
324
- funcPermissions,
325
- packageName,
326
- })
327
186
 
328
- // Check all combined permissions - at least one must pass if any exist
329
- if (allPermissions.length > 0) {
330
- for (const permission of allPermissions) {
331
- const result = await verifyPermissions(
332
- typeof permission === 'function' ? { permission } : permission,
333
- services,
334
- data,
335
- wire
336
- )
337
- if (result) {
338
- return
187
+ if (funcPermissions) {
188
+ const group = Array.isArray(funcPermissions)
189
+ ? { permissions: funcPermissions }
190
+ : funcPermissions
191
+ if (group && Object.keys(group).length > 0) {
192
+ if (!(await verifyPermissions(group, services, data, wire))) {
193
+ services.logger.debug(`Permission denied - ${label} permission`)
194
+ throw new ForbiddenError('Permission denied')
339
195
  }
340
196
  }
341
- services.logger.debug('Permission denied - combined permissions')
342
- throw new ForbiddenError('Permission denied')
343
197
  }
344
198
  }
345
199
 
346
200
  /**
347
201
  * Checks whether a session passes the auth checks (pikkuAuth only) for a
348
202
  * given function/agent. Skips pikkuPermission checks since those require
349
- * request data which isn't available at filter time.
203
+ * request data which isn't available at filter time. Global auth requirements
204
+ * are included so a filtered list honours app-wide auth.
350
205
  *
351
- * @param funcPermissions - The PermissionMetadata[] from function or agent metadata
206
+ * `funcPermissions` is the live {@link CorePermissionGroup} from the function or
207
+ * agent config, not the metadata form: the `pikkuAuth` brand only survives on
208
+ * the actual predicate objects, and the by-name registry those metadata entries
209
+ * would resolve against is never populated. Passing metadata here would silently
210
+ * collect nothing and let every gated tool through.
211
+ *
212
+ * @param funcPermissions - The live permission group from the func/agent config
352
213
  * @param session - The user's session
353
214
  * @param services - Singleton services
354
215
  * @param packageName - Optional package namespace
355
- * @returns true if the session passes all auth checks (or no auth checks exist)
216
+ * @returns true if the session passes the auth checks (or no auth checks exist)
356
217
  */
357
218
  export const checkAuthPermissions = async (
358
- funcPermissions: PermissionMetadata[] | undefined,
219
+ funcPermissions: CorePermissionGroup | undefined,
359
220
  session: CoreUserSession,
360
221
  services: CoreServices,
361
222
  packageName: string | null = null
362
223
  ): Promise<boolean> => {
363
- if (!funcPermissions?.length) return true
364
-
365
- const allPermissions = combinePermissions(
366
- 'agent',
367
- `authcheck:${Math.random()}`,
368
- {
369
- funcInheritedPermissions: funcPermissions,
370
- packageName,
371
- }
372
- )
373
-
374
- if (allPermissions.length === 0) return true
375
-
376
224
  const wire = { session } as unknown as PikkuWire<
377
225
  any,
378
226
  never,
@@ -382,15 +230,18 @@ export const checkAuthPermissions = async (
382
230
  never
383
231
  >
384
232
 
385
- // Extract only pikkuAuth permissions (marked with __pikkuAuth)
233
+ // Collect auth-only (pikkuAuth) predicates from globals and the function's
234
+ // referenced permissions. Data-dependent permissions are ignored — they
235
+ // can't be evaluated without request data at filter time.
386
236
  const authPerms: CorePikkuPermission<any, any, any>[] = []
387
- for (const permission of allPermissions) {
388
- if (typeof permission === 'function') {
389
- if ((permission as any).__pikkuAuth) {
390
- authPerms.push(permission)
237
+
238
+ const collect = (perm: CorePermissionGroup | CorePikkuPermission) => {
239
+ if (typeof perm === 'function') {
240
+ if ((perm as any).__pikkuAuth) {
241
+ authPerms.push(perm)
391
242
  }
392
- } else if (permission && typeof permission === 'object') {
393
- for (const funcs of Object.values(permission)) {
243
+ } else if (perm && typeof perm === 'object') {
244
+ for (const funcs of Object.values(perm)) {
394
245
  const arr = Array.isArray(funcs) ? funcs : [funcs]
395
246
  for (const fn of arr) {
396
247
  if (typeof fn === 'function' && (fn as any).__pikkuAuth) {
@@ -401,10 +252,17 @@ export const checkAuthPermissions = async (
401
252
  }
402
253
  }
403
254
 
255
+ for (const entry of resolveGlobalPermissions(packageName)) {
256
+ collect(entry)
257
+ }
258
+
259
+ if (funcPermissions) {
260
+ collect(funcPermissions)
261
+ }
262
+
404
263
  // No auth permissions = allowed (only data-dependent permissions exist)
405
264
  if (authPerms.length === 0) return true
406
265
 
407
- // All auth permissions must pass
408
266
  for (const perm of authPerms) {
409
267
  const result = await perm(services, null, wire)
410
268
  if (result) return true
@@ -141,8 +141,6 @@ const createEmptyPackageState = (): PikkuPackageState => ({
141
141
  tagGroup: {},
142
142
  },
143
143
  permissions: {
144
- tagGroup: {},
145
- httpGroup: {},
146
144
  global: [] as any,
147
145
  },
148
146
  misc: {