@pikku/core 0.12.0 → 0.12.1

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 (262) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/errors/errors.d.ts +6 -0
  3. package/dist/errors/errors.js +10 -0
  4. package/dist/errors/index.d.ts +1 -0
  5. package/dist/errors/index.js +1 -0
  6. package/dist/function/function-runner.d.ts +3 -3
  7. package/dist/function/function-runner.js +66 -44
  8. package/dist/function/functions.types.d.ts +1 -0
  9. package/dist/handle-error.d.ts +2 -2
  10. package/dist/handle-error.js +2 -2
  11. package/dist/index.d.ts +5 -3
  12. package/dist/index.js +2 -1
  13. package/dist/internal.d.ts +3 -0
  14. package/dist/internal.js +2 -0
  15. package/dist/middleware/auth-bearer.d.ts +1 -1
  16. package/dist/middleware/auth-bearer.js +1 -1
  17. package/dist/middleware/auth-cookie.d.ts +2 -2
  18. package/dist/middleware/auth-cookie.js +1 -1
  19. package/dist/middleware/cors.d.ts +2 -2
  20. package/dist/middleware/cors.js +45 -37
  21. package/dist/middleware-runner.d.ts +1 -1
  22. package/dist/permissions.d.ts +2 -2
  23. package/dist/pikku-state.d.ts +9 -8
  24. package/dist/pikku-state.js +30 -17
  25. package/dist/schema.d.ts +4 -4
  26. package/dist/schema.js +9 -5
  27. package/dist/services/ai-agent-runner-service.d.ts +22 -3
  28. package/dist/services/ai-run-state-service.d.ts +1 -1
  29. package/dist/services/ai-storage-service.d.ts +1 -1
  30. package/dist/services/content-service.d.ts +6 -0
  31. package/dist/services/gopass-secrets.d.ts +1 -1
  32. package/dist/services/gopass-secrets.js +3 -0
  33. package/dist/services/in-memory-ai-run-state-service.d.ts +15 -0
  34. package/dist/services/in-memory-ai-run-state-service.js +44 -0
  35. package/dist/services/in-memory-trigger-service.d.ts +0 -1
  36. package/dist/services/in-memory-trigger-service.js +6 -8
  37. package/dist/services/index.d.ts +3 -2
  38. package/dist/services/index.js +1 -0
  39. package/dist/services/local-content.d.ts +2 -1
  40. package/dist/services/local-content.js +6 -1
  41. package/dist/services/local-secrets.d.ts +2 -2
  42. package/dist/services/local-variables.d.ts +1 -1
  43. package/dist/services/logger-console.d.ts +2 -1
  44. package/dist/services/scheduler-service.d.ts +0 -12
  45. package/dist/services/scheduler-service.js +0 -6
  46. package/dist/services/scoped-secret-service.d.ts +1 -1
  47. package/dist/services/trigger-service.d.ts +0 -7
  48. package/dist/services/user-session-service.d.ts +3 -3
  49. package/dist/services/workflow-service.d.ts +2 -3
  50. package/dist/types/core.types.d.ts +24 -18
  51. package/dist/types/state.types.d.ts +17 -15
  52. package/dist/utils.d.ts +5 -5
  53. package/dist/utils.js +12 -9
  54. package/dist/wirings/ai-agent/ai-agent-assistant-ui.d.ts +5 -0
  55. package/dist/wirings/ai-agent/ai-agent-assistant-ui.js +146 -0
  56. package/dist/wirings/ai-agent/ai-agent-helpers.d.ts +28 -0
  57. package/dist/wirings/ai-agent/ai-agent-helpers.js +40 -0
  58. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +4 -5
  59. package/dist/wirings/ai-agent/ai-agent-prepare.js +66 -30
  60. package/dist/wirings/ai-agent/ai-agent-registry.d.ts +1 -1
  61. package/dist/wirings/ai-agent/ai-agent-runner.js +107 -8
  62. package/dist/wirings/ai-agent/ai-agent-stream.d.ts +2 -1
  63. package/dist/wirings/ai-agent/ai-agent-stream.js +246 -87
  64. package/dist/wirings/ai-agent/ai-agent.types.d.ts +61 -2
  65. package/dist/wirings/ai-agent/index.d.ts +3 -1
  66. package/dist/wirings/ai-agent/index.js +2 -0
  67. package/dist/wirings/channel/channel-common.d.ts +2 -2
  68. package/dist/wirings/channel/channel-handler.d.ts +3 -3
  69. package/dist/wirings/channel/channel-handler.js +4 -4
  70. package/dist/wirings/channel/channel-runner.d.ts +5 -5
  71. package/dist/wirings/channel/channel-runner.js +3 -2
  72. package/dist/wirings/channel/channel-store.d.ts +1 -1
  73. package/dist/wirings/channel/channel.types.d.ts +4 -6
  74. package/dist/wirings/channel/local/local-channel-runner.d.ts +7 -2
  75. package/dist/wirings/channel/local/local-channel-runner.js +4 -3
  76. package/dist/wirings/channel/local/local-eventhub-service.d.ts +2 -2
  77. package/dist/wirings/channel/log-channels.d.ts +1 -1
  78. package/dist/wirings/channel/pikku-abstract-channel-handler.d.ts +1 -1
  79. package/dist/wirings/channel/serverless/serverless-channel-runner.d.ts +4 -4
  80. package/dist/wirings/channel/serverless/serverless-channel-runner.js +25 -19
  81. package/dist/wirings/cli/channel/cli-channel-runner.d.ts +1 -1
  82. package/dist/wirings/cli/cli-runner.d.ts +2 -2
  83. package/dist/wirings/cli/cli.types.d.ts +3 -3
  84. package/dist/wirings/cli/command-parser.d.ts +1 -1
  85. package/dist/wirings/http/http-runner.d.ts +9 -9
  86. package/dist/wirings/http/http-runner.js +33 -14
  87. package/dist/wirings/http/http.types.d.ts +2 -6
  88. package/dist/wirings/http/log-http-routes.d.ts +1 -1
  89. package/dist/wirings/http/pikku-fetch-http-request.d.ts +1 -1
  90. package/dist/wirings/http/pikku-fetch-http-response.d.ts +2 -2
  91. package/dist/wirings/http/pikku-fetch-http-response.js +6 -1
  92. package/dist/wirings/http/routers/http-router.d.ts +1 -1
  93. package/dist/wirings/http/routers/path-to-regex.d.ts +2 -2
  94. package/dist/wirings/mcp/mcp-endpoint-registry.d.ts +1 -1
  95. package/dist/wirings/mcp/mcp-runner.d.ts +0 -3
  96. package/dist/wirings/mcp/mcp-runner.js +4 -2
  97. package/dist/wirings/mcp/mcp.types.d.ts +2 -2
  98. package/dist/wirings/oauth2/oauth2-client.d.ts +3 -3
  99. package/dist/wirings/oauth2/wire-oauth2-credential.d.ts +1 -1
  100. package/dist/wirings/queue/index.d.ts +1 -1
  101. package/dist/wirings/queue/index.js +1 -1
  102. package/dist/wirings/queue/queue-runner.d.ts +1 -12
  103. package/dist/wirings/queue/queue-runner.js +4 -12
  104. package/dist/wirings/queue/queue.types.d.ts +3 -9
  105. package/dist/wirings/queue/register-queue-helper.d.ts +2 -2
  106. package/dist/wirings/queue/register-queue-helper.js +1 -1
  107. package/dist/wirings/queue/validate-worker-config.d.ts +1 -1
  108. package/dist/wirings/rpc/index.d.ts +2 -0
  109. package/dist/wirings/rpc/index.js +1 -0
  110. package/dist/wirings/rpc/rpc-runner.d.ts +36 -14
  111. package/dist/wirings/rpc/rpc-runner.js +56 -28
  112. package/dist/wirings/rpc/rpc-types.d.ts +14 -2
  113. package/dist/wirings/rpc/wire-addon.d.ts +10 -0
  114. package/dist/wirings/rpc/wire-addon.js +9 -0
  115. package/dist/wirings/scheduler/index.d.ts +1 -1
  116. package/dist/wirings/scheduler/index.js +1 -1
  117. package/dist/wirings/scheduler/log-schedulers.d.ts +1 -1
  118. package/dist/wirings/scheduler/scheduler-runner.d.ts +3 -10
  119. package/dist/wirings/scheduler/scheduler-runner.js +4 -25
  120. package/dist/wirings/scheduler/scheduler.types.d.ts +2 -2
  121. package/dist/wirings/trigger/pikku-trigger-service.d.ts +2 -6
  122. package/dist/wirings/trigger/pikku-trigger-service.js +11 -16
  123. package/dist/wirings/trigger/trigger-runner.d.ts +1 -5
  124. package/dist/wirings/trigger/trigger-runner.js +4 -3
  125. package/dist/wirings/workflow/pikku-workflow-service.d.ts +2 -6
  126. package/dist/wirings/workflow/pikku-workflow-service.js +20 -29
  127. package/dist/wirings/workflow/workflow.types.d.ts +2 -2
  128. package/package.json +2 -1
  129. package/src/crypto-utils.test.ts +116 -0
  130. package/src/errors/error.test.ts +143 -2
  131. package/src/errors/errors.ts +10 -0
  132. package/src/errors/index.ts +1 -0
  133. package/src/function/function-runner.test.ts +2 -2
  134. package/src/function/function-runner.ts +72 -49
  135. package/src/function/functions.types.ts +1 -0
  136. package/src/handle-error.test.ts +361 -0
  137. package/src/handle-error.ts +4 -4
  138. package/src/index.ts +3 -10
  139. package/src/internal.ts +6 -0
  140. package/src/middleware/auth-apikey.test.ts +1 -1
  141. package/src/middleware/auth-bearer.test.ts +1 -1
  142. package/src/middleware/auth-bearer.ts +2 -5
  143. package/src/middleware/auth-cookie.test.ts +1 -1
  144. package/src/middleware/auth-cookie.ts +3 -5
  145. package/src/middleware/cors.test.ts +424 -0
  146. package/src/middleware/cors.ts +14 -6
  147. package/src/middleware/remote-auth.test.ts +488 -0
  148. package/src/middleware-runner.test.ts +1 -1
  149. package/src/middleware-runner.ts +1 -1
  150. package/src/permissions.test.ts +2 -2
  151. package/src/permissions.ts +2 -2
  152. package/src/pikku-state.test.ts +224 -0
  153. package/src/pikku-state.ts +40 -28
  154. package/src/schema.test.ts +198 -6
  155. package/src/schema.ts +11 -7
  156. package/src/services/ai-agent-runner-service.ts +15 -3
  157. package/src/services/ai-run-state-service.ts +1 -1
  158. package/src/services/ai-storage-service.ts +1 -1
  159. package/src/services/content-service.ts +7 -0
  160. package/src/services/gopass-secrets.ts +4 -1
  161. package/src/services/in-memory-ai-run-state-service.ts +73 -0
  162. package/src/services/in-memory-trigger-service.ts +6 -10
  163. package/src/services/in-memory-workflow-service.test.ts +351 -0
  164. package/src/services/index.ts +2 -1
  165. package/src/services/local-content.ts +9 -3
  166. package/src/services/local-secrets.test.ts +80 -0
  167. package/src/services/local-secrets.ts +2 -2
  168. package/src/services/local-variables.test.ts +61 -0
  169. package/src/services/local-variables.ts +1 -1
  170. package/src/services/logger-console.test.ts +118 -0
  171. package/src/services/logger-console.ts +2 -1
  172. package/src/services/scheduler-service.ts +0 -18
  173. package/src/services/scoped-secret-service.test.ts +74 -0
  174. package/src/services/scoped-secret-service.ts +1 -1
  175. package/src/services/trigger-service.ts +0 -21
  176. package/src/services/typed-secret-service.test.ts +93 -0
  177. package/src/services/typed-variables-service.test.ts +73 -0
  178. package/src/services/user-session-service.test.ts +113 -0
  179. package/src/services/user-session-service.ts +3 -3
  180. package/src/services/workflow-service.ts +2 -12
  181. package/src/time-utils.test.ts +1 -1
  182. package/src/types/core.types.ts +25 -19
  183. package/src/types/state.types.ts +24 -15
  184. package/src/utils.test.ts +350 -0
  185. package/src/utils.ts +14 -13
  186. package/src/wirings/ai-agent/ai-agent-assistant-ui.test.ts +363 -0
  187. package/src/wirings/ai-agent/ai-agent-assistant-ui.ts +214 -0
  188. package/src/wirings/ai-agent/ai-agent-helpers.test.ts +152 -0
  189. package/src/wirings/ai-agent/ai-agent-helpers.ts +76 -0
  190. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +115 -0
  191. package/src/wirings/ai-agent/ai-agent-prepare.ts +79 -45
  192. package/src/wirings/ai-agent/ai-agent-registry.ts +1 -1
  193. package/src/wirings/ai-agent/ai-agent-runner.test.ts +245 -3
  194. package/src/wirings/ai-agent/ai-agent-runner.ts +121 -7
  195. package/src/wirings/ai-agent/ai-agent-stream.test.ts +430 -24
  196. package/src/wirings/ai-agent/ai-agent-stream.ts +377 -102
  197. package/src/wirings/ai-agent/ai-agent.types.ts +57 -2
  198. package/src/wirings/ai-agent/index.ts +11 -0
  199. package/src/wirings/channel/channel-common.ts +2 -2
  200. package/src/wirings/channel/channel-handler.ts +13 -9
  201. package/src/wirings/channel/channel-runner.ts +6 -6
  202. package/src/wirings/channel/channel-store.ts +1 -1
  203. package/src/wirings/channel/channel.types.ts +4 -8
  204. package/src/wirings/channel/local/local-channel-runner.test.ts +19 -12
  205. package/src/wirings/channel/local/local-channel-runner.ts +18 -13
  206. package/src/wirings/channel/local/local-eventhub-service.test.ts +2 -2
  207. package/src/wirings/channel/local/local-eventhub-service.ts +2 -2
  208. package/src/wirings/channel/log-channels.ts +1 -1
  209. package/src/wirings/channel/pikku-abstract-channel-handler.ts +1 -1
  210. package/src/wirings/channel/serverless/serverless-channel-runner.ts +37 -32
  211. package/src/wirings/cli/channel/cli-channel-runner.ts +1 -1
  212. package/src/wirings/cli/cli-runner.test.ts +1 -1
  213. package/src/wirings/cli/cli-runner.ts +11 -4
  214. package/src/wirings/cli/cli.types.ts +3 -3
  215. package/src/wirings/cli/command-parser.test.ts +1 -1
  216. package/src/wirings/cli/command-parser.ts +1 -1
  217. package/src/wirings/http/http-routes.test.ts +1 -1
  218. package/src/wirings/http/http-runner.test.ts +9 -17
  219. package/src/wirings/http/http-runner.ts +43 -27
  220. package/src/wirings/http/http.types.ts +1 -14
  221. package/src/wirings/http/log-http-routes.ts +1 -1
  222. package/src/wirings/http/pikku-fetch-http-request.ts +1 -1
  223. package/src/wirings/http/pikku-fetch-http-response.ts +12 -5
  224. package/src/wirings/http/routers/http-router.ts +1 -1
  225. package/src/wirings/http/routers/path-to-regex.test.ts +1 -1
  226. package/src/wirings/http/routers/path-to-regex.ts +4 -3
  227. package/src/wirings/mcp/mcp-endpoint-registry.ts +5 -1
  228. package/src/wirings/mcp/mcp-runner.ts +9 -15
  229. package/src/wirings/mcp/mcp.types.ts +2 -2
  230. package/src/wirings/oauth2/oauth2-client.ts +3 -3
  231. package/src/wirings/oauth2/wire-oauth2-credential.ts +1 -1
  232. package/src/wirings/queue/index.ts +0 -1
  233. package/src/wirings/queue/queue-runner.test.ts +52 -25
  234. package/src/wirings/queue/queue-runner.ts +8 -30
  235. package/src/wirings/queue/queue.types.ts +3 -13
  236. package/src/wirings/queue/register-queue-helper.ts +3 -5
  237. package/src/wirings/queue/validate-worker-config.test.ts +108 -0
  238. package/src/wirings/queue/validate-worker-config.ts +4 -1
  239. package/src/wirings/rpc/index.ts +2 -0
  240. package/src/wirings/rpc/rpc-runner.ts +92 -49
  241. package/src/wirings/rpc/rpc-types.ts +17 -2
  242. package/src/wirings/rpc/wire-addon.ts +20 -0
  243. package/src/wirings/scheduler/index.ts +0 -1
  244. package/src/wirings/scheduler/log-schedulers.ts +1 -1
  245. package/src/wirings/scheduler/scheduler-runner.test.ts +49 -60
  246. package/src/wirings/scheduler/scheduler-runner.ts +9 -56
  247. package/src/wirings/scheduler/scheduler.types.ts +2 -2
  248. package/src/wirings/secret/validate-secret-definitions.test.ts +140 -0
  249. package/src/wirings/trigger/pikku-trigger-service.ts +17 -39
  250. package/src/wirings/trigger/trigger-runner.test.ts +79 -0
  251. package/src/wirings/trigger/trigger-runner.ts +8 -11
  252. package/src/wirings/variable/validate-variable-definitions.test.ts +91 -0
  253. package/src/wirings/workflow/graph/graph-runner.test.ts +14 -22
  254. package/src/wirings/workflow/graph/template.test.ts +49 -0
  255. package/src/wirings/workflow/pikku-workflow-service.test.ts +15 -27
  256. package/src/wirings/workflow/pikku-workflow-service.ts +29 -45
  257. package/src/wirings/workflow/workflow-helpers.test.ts +129 -0
  258. package/src/wirings/workflow/workflow.types.ts +2 -2
  259. package/tsconfig.tsbuildinfo +1 -1
  260. package/dist/wirings/workflow/workflow-utils.d.ts +0 -1
  261. package/dist/wirings/workflow/workflow-utils.js +0 -1
  262. package/src/wirings/workflow/workflow-utils.ts +0 -0
@@ -3,8 +3,8 @@ import * as assert from 'node:assert'
3
3
  import { addFunction, runPikkuFunc } from './function-runner.js'
4
4
  import { addMiddleware, addPermission } from '../index.js'
5
5
  import { resetPikkuState, pikkuState } from '../pikku-state.js'
6
- import { CoreServices, CorePikkuMiddleware } from '../types/core.types.js'
7
- import { CorePermissionGroup } from './functions.types.js'
6
+ import type { CoreServices, CorePikkuMiddleware } from '../types/core.types.js'
7
+ import type { CorePermissionGroup } from './functions.types.js'
8
8
 
9
9
  beforeEach(() => {
10
10
  resetPikkuState()
@@ -6,7 +6,7 @@ import {
6
6
  import { runPermissions } from '../permissions.js'
7
7
  import { pikkuState } from '../pikku-state.js'
8
8
  import { coerceTopLevelDataFromSchema, validateSchema } from '../schema.js'
9
- import {
9
+ import type {
10
10
  CoreServices,
11
11
  CoreUserSession,
12
12
  CorePikkuMiddleware,
@@ -19,25 +19,23 @@ import {
19
19
  CoreConfig,
20
20
  } from '../types/core.types.js'
21
21
  import type { CorePikkuChannelMiddleware } from '../wirings/channel/channel.types.js'
22
- import {
22
+ import type {
23
23
  CorePermissionGroup,
24
24
  CorePikkuFunctionConfig,
25
25
  CorePikkuPermission,
26
26
  } from './functions.types.js'
27
27
  import { parseVersionedId } from '../version.js'
28
- import {
29
- SessionService,
30
- createFunctionSessionWireProps,
31
- } from '../services/user-session-service.js'
32
- import { ForbiddenError } from '../errors/errors.js'
28
+ import type { SessionService } from '../services/user-session-service.js'
29
+ import { createFunctionSessionWireProps } from '../services/user-session-service.js'
30
+ import { ForbiddenError, ReadonlySessionError } from '../errors/errors.js'
33
31
  import { rpcService } from '../wirings/rpc/rpc-runner.js'
34
32
  import { closeWireServices } from '../utils.js'
35
33
 
36
34
  /**
37
- * Get or create singleton services for an external package.
35
+ * Get or create singleton services for an addon package.
38
36
  * Services are cached in pikkuState to avoid recreation on each call.
39
37
  *
40
- * @param packageName - The external package name
38
+ * @param packageName - The addon package name
41
39
  * @param parentServices - The parent/caller's singleton services (used as base)
42
40
  * @returns The package's singleton services
43
41
  */
@@ -97,8 +95,8 @@ export const getAllFunctionNames = (): string[] => {
97
95
  const mainFunctionsMeta = pikkuState(null, 'function', 'meta')
98
96
  functions.push(...Object.keys(mainFunctionsMeta))
99
97
 
100
- const externalPackages = pikkuState(null, 'rpc', 'externalPackages')
101
- for (const [namespace, config] of externalPackages) {
98
+ const addons = pikkuState(null, 'rpc', 'addons')
99
+ for (const [namespace, config] of addons) {
102
100
  const packageFunctionsMeta = pikkuState(config.package, 'function', 'meta')
103
101
  for (const funcName of Object.keys(packageFunctionsMeta)) {
104
102
  functions.push(`${namespace}:${funcName}`)
@@ -167,11 +165,8 @@ export const runPikkuFunc = async <In = any, Out = any>(
167
165
  packageName?: string | null
168
166
  }
169
167
  ): Promise<Out> => {
170
- wire = {
171
- ...wire,
172
- wireType: wire.wireType ?? wireType,
173
- wireId: wire.wireId ?? wireId,
174
- }
168
+ wire.wireType ??= wireType
169
+ wire.wireId ??= wireId
175
170
 
176
171
  const funcMap = pikkuState(packageName, 'function', 'functions')
177
172
  let funcConfig = funcMap.get(funcName)
@@ -198,7 +193,7 @@ export const runPikkuFunc = async <In = any, Out = any>(
198
193
  throw new Error(`Function meta not found: ${funcName}`)
199
194
  }
200
195
 
201
- // For external packages, get or create their singleton services
196
+ // For addon packages, get or create their singleton services
202
197
  const resolvedSingletonServices = packageName
203
198
  ? await getOrCreatePackageSingletonServices(packageName, singletonServices)
204
199
  : singletonServices
@@ -228,23 +223,27 @@ export const runPikkuFunc = async <In = any, Out = any>(
228
223
  : wire
229
224
 
230
225
  // Convert tags to PermissionMetadata and merge with inheritedPermissions
231
- const mergedInheritedPermissions: PermissionMetadata[] = [
232
- ...(inheritedPermissions || []),
233
- ...(tags?.map((tag) => ({ type: 'tag' as const, tag })) || []),
234
- ]
226
+ let mergedInheritedPermissions: PermissionMetadata[]
227
+ if (tags && tags.length > 0) {
228
+ mergedInheritedPermissions = [
229
+ ...(inheritedPermissions || []),
230
+ ...tags.map((tag) => ({ type: 'tag' as const, tag })),
231
+ ]
232
+ } else {
233
+ mergedInheritedPermissions = inheritedPermissions || []
234
+ }
235
235
 
236
236
  // Helper function to run permissions and execute the function
237
237
  const executeFunction = async () => {
238
- const functionWireProps = sessionService
239
- ? createFunctionSessionWireProps(sessionService)
240
- : undefined
241
-
242
- const wireWithSession: PikkuWire = {
243
- ...resolvedWire,
244
- ...functionWireProps,
238
+ if (sessionService) {
239
+ resolvedWire.session = sessionService.freezeInitial()
240
+ resolvedWire.setSession = (s: any) => sessionService.set(s)
241
+ resolvedWire.clearSession = () => sessionService.clear()
242
+ resolvedWire.getSession = () => sessionService.get()
243
+ resolvedWire.hasSessionChanged = () => sessionService.sessionChanged
245
244
  }
246
245
 
247
- const session = wireWithSession.session
246
+ const session = resolvedWire.session
248
247
 
249
248
  if (funcMeta.sessionless) {
250
249
  if (wiringAuth === true || funcConfig.auth === true) {
@@ -257,7 +256,8 @@ export const runPikkuFunc = async <In = any, Out = any>(
257
256
  resolvedSingletonServices.logger.warn(
258
257
  `Function '${funcName}' requires a session but auth was explicitly disabled — use pikkuSessionlessFunc instead.`
259
258
  )
260
- } else if (!session) {
259
+ }
260
+ if (!session) {
261
261
  throw new ForbiddenError('Authentication required')
262
262
  }
263
263
  } else {
@@ -270,6 +270,10 @@ export const runPikkuFunc = async <In = any, Out = any>(
270
270
  }
271
271
  }
272
272
 
273
+ if ((session as any)?.readonly && !funcMeta.readonly) {
274
+ throw new ReadonlySessionError()
275
+ }
276
+
273
277
  // Evaluate the data from the lazy function
274
278
  const actualData = await data()
275
279
 
@@ -290,32 +294,51 @@ export const runPikkuFunc = async <In = any, Out = any>(
290
294
  }
291
295
  }
292
296
 
293
- await runPermissions(wireType, wireId, {
294
- wireInheritedPermissions: mergedInheritedPermissions,
295
- wirePermissions: wirePermissions,
296
- funcInheritedPermissions: funcMeta.permissions,
297
- funcPermissions: funcConfig.permissions,
298
- services: resolvedSingletonServices,
299
- wire: { ...wireWithSession, rpc: undefined } as any,
300
- data: actualData,
301
- packageName,
302
- })
297
+ if (
298
+ mergedInheritedPermissions.length > 0 ||
299
+ wirePermissions ||
300
+ funcMeta.permissions ||
301
+ funcConfig.permissions
302
+ ) {
303
+ await runPermissions(wireType, wireId, {
304
+ wireInheritedPermissions: mergedInheritedPermissions,
305
+ wirePermissions: wirePermissions,
306
+ funcInheritedPermissions: funcMeta.permissions,
307
+ funcPermissions: funcConfig.permissions,
308
+ services: resolvedSingletonServices,
309
+ wire: resolvedWire as any,
310
+ data: actualData,
311
+ packageName,
312
+ })
313
+ }
303
314
 
304
315
  const wireServices = await resolvedCreateWireServices?.(
305
316
  resolvedSingletonServices,
306
- wireWithSession
317
+ resolvedWire
307
318
  )
308
319
  try {
309
- const services = { ...resolvedSingletonServices, ...wireServices }
310
- const rpc = rpcService.getContextRPCService(services, wireWithSession, {
311
- sessionService,
312
- })
313
- return await funcConfig.func(services, actualData, {
314
- ...wireWithSession,
315
- rpc,
320
+ const services =
321
+ wireServices && Object.keys(wireServices).length > 0
322
+ ? { ...resolvedSingletonServices, ...wireServices }
323
+ : resolvedSingletonServices
324
+ Object.defineProperty(resolvedWire, 'rpc', {
325
+ get() {
326
+ const rpc = rpcService.getContextRPCService(services, resolvedWire, {
327
+ sessionService,
328
+ })
329
+ Object.defineProperty(resolvedWire, 'rpc', {
330
+ value: rpc,
331
+ writable: true,
332
+ configurable: true,
333
+ })
334
+ return rpc
335
+ },
336
+ configurable: true,
337
+ enumerable: true,
316
338
  })
339
+ return await funcConfig.func(services, actualData, resolvedWire)
317
340
  } finally {
318
- if (wireServices) {
341
+ if (wireServices && Object.keys(wireServices).length > 0) {
319
342
  await closeWireServices(resolvedSingletonServices.logger, wireServices)
320
343
  }
321
344
  }
@@ -255,6 +255,7 @@ export type CorePikkuFunctionConfig<
255
255
  expose?: boolean
256
256
  remote?: boolean
257
257
  mcp?: boolean
258
+ readonly?: boolean
258
259
  requiresApproval?: boolean
259
260
  func: PikkuFunction
260
261
  auth?: boolean
@@ -0,0 +1,361 @@
1
+ import { describe, test, beforeEach } from 'node:test'
2
+ import assert from 'node:assert'
3
+ import { handleHTTPError } from './handle-error.js'
4
+ import { addError } from './errors/error-handler.js'
5
+ import {
6
+ NotFoundError,
7
+ BadRequestError,
8
+ ForbiddenError,
9
+ } from './errors/errors.js'
10
+ import { resetPikkuState } from './pikku-state.js'
11
+
12
+ const createMockLogger = () => {
13
+ const logs: { level: string; args: any[] }[] = []
14
+ return {
15
+ info: (...args: any[]) => logs.push({ level: 'info', args }),
16
+ warn: (...args: any[]) => logs.push({ level: 'warn', args }),
17
+ error: (...args: any[]) => logs.push({ level: 'error', args }),
18
+ debug: (...args: any[]) => logs.push({ level: 'debug', args }),
19
+ _logs: logs,
20
+ }
21
+ }
22
+
23
+ const createMockHTTP = () => {
24
+ const state = {
25
+ statusCode: undefined as number | undefined,
26
+ jsonBody: undefined as any,
27
+ closed: false,
28
+ }
29
+ return {
30
+ request: {},
31
+ response: {
32
+ status: (code: number) => {
33
+ state.statusCode = code
34
+ return {
35
+ json: (body: any) => {
36
+ state.jsonBody = body
37
+ },
38
+ }
39
+ },
40
+ json: (body: any) => {
41
+ state.jsonBody = body
42
+ },
43
+ close: () => {
44
+ state.closed = true
45
+ },
46
+ },
47
+ _state: state,
48
+ }
49
+ }
50
+
51
+ beforeEach(() => {
52
+ resetPikkuState()
53
+ })
54
+
55
+ describe('handleHTTPError', () => {
56
+ test('should skip NotFoundError when respondWith404 is false', () => {
57
+ const logger = createMockLogger()
58
+ const http = createMockHTTP()
59
+ const error = new NotFoundError()
60
+
61
+ handleHTTPError(
62
+ error,
63
+ http as any,
64
+ 'tracker-1',
65
+ logger as any,
66
+ [],
67
+ false,
68
+ false
69
+ )
70
+
71
+ assert.strictEqual(http._state.statusCode, undefined)
72
+ assert.strictEqual(http._state.jsonBody, undefined)
73
+ })
74
+
75
+ test('should handle NotFoundError when respondWith404 is true', () => {
76
+ const logger = createMockLogger()
77
+ const http = createMockHTTP()
78
+ const error = new NotFoundError()
79
+
80
+ handleHTTPError(
81
+ error,
82
+ http as any,
83
+ 'tracker-1',
84
+ logger as any,
85
+ [],
86
+ true,
87
+ false
88
+ )
89
+
90
+ assert.strictEqual(http._state.statusCode, 404)
91
+ assert.deepStrictEqual(http._state.jsonBody, {
92
+ message: 'The server cannot find the requested resource.',
93
+ payload: undefined,
94
+ traceId: 'tracker-1',
95
+ })
96
+ })
97
+
98
+ test('should set correct status and message for known errors', () => {
99
+ const logger = createMockLogger()
100
+ const http = createMockHTTP()
101
+ const error = new BadRequestError('Invalid input')
102
+
103
+ handleHTTPError(
104
+ error,
105
+ http as any,
106
+ 'tracker-2',
107
+ logger as any,
108
+ [],
109
+ true,
110
+ false
111
+ )
112
+
113
+ assert.strictEqual(http._state.statusCode, 400)
114
+ assert.ok(http._state.jsonBody.message.includes('client error'))
115
+ assert.strictEqual(http._state.jsonBody.traceId, 'tracker-2')
116
+ })
117
+
118
+ test('should set status 403 for ForbiddenError', () => {
119
+ const logger = createMockLogger()
120
+ const http = createMockHTTP()
121
+ const error = new ForbiddenError()
122
+
123
+ handleHTTPError(
124
+ error,
125
+ http as any,
126
+ 'tracker-3',
127
+ logger as any,
128
+ [],
129
+ true,
130
+ false
131
+ )
132
+
133
+ assert.strictEqual(http._state.statusCode, 403)
134
+ })
135
+
136
+ test('should log warning when status code is in logWarningsForStatusCodes', () => {
137
+ const logger = createMockLogger()
138
+ const http = createMockHTTP()
139
+ const error = new BadRequestError('bad')
140
+
141
+ handleHTTPError(
142
+ error,
143
+ http as any,
144
+ 'tracker-4',
145
+ logger as any,
146
+ [400],
147
+ true,
148
+ false
149
+ )
150
+
151
+ const warns = logger._logs.filter((l) => l.level === 'warn')
152
+ assert.ok(warns.length >= 1)
153
+ assert.ok(warns.some((w) => w.args[0].includes('tracker-4')))
154
+ })
155
+
156
+ test('should not log warning when status code is not in logWarningsForStatusCodes', () => {
157
+ const logger = createMockLogger()
158
+ const http = createMockHTTP()
159
+ const error = new BadRequestError('bad')
160
+
161
+ handleHTTPError(
162
+ error,
163
+ http as any,
164
+ 'tracker-5',
165
+ logger as any,
166
+ [500],
167
+ true,
168
+ false
169
+ )
170
+
171
+ const warns = logger._logs.filter((l) => l.level === 'warn')
172
+ assert.strictEqual(warns.length, 0)
173
+ })
174
+
175
+ test('should handle unknown errors with 500 status', () => {
176
+ const logger = createMockLogger()
177
+ const http = createMockHTTP()
178
+ const error = new Error('something unexpected')
179
+
180
+ handleHTTPError(
181
+ error,
182
+ http as any,
183
+ 'tracker-6',
184
+ logger as any,
185
+ [],
186
+ true,
187
+ false
188
+ )
189
+
190
+ assert.strictEqual(http._state.statusCode, 500)
191
+ assert.deepStrictEqual(http._state.jsonBody, { errorId: 'tracker-6' })
192
+ })
193
+
194
+ test('should log unknown errors with error level', () => {
195
+ const logger = createMockLogger()
196
+ const http = createMockHTTP()
197
+ const error = new Error('unexpected')
198
+
199
+ handleHTTPError(
200
+ error,
201
+ http as any,
202
+ undefined,
203
+ logger as any,
204
+ [],
205
+ true,
206
+ false
207
+ )
208
+
209
+ const errors = logger._logs.filter((l) => l.level === 'error')
210
+ assert.ok(errors.length >= 1)
211
+ assert.ok(errors.some((e) => e.args[0] === 'unexpected'))
212
+ })
213
+
214
+ test('should not include errorId in response when trackerId is undefined for unknown errors', () => {
215
+ const logger = createMockLogger()
216
+ const http = createMockHTTP()
217
+ const error = new Error('unexpected')
218
+
219
+ handleHTTPError(
220
+ error,
221
+ http as any,
222
+ undefined,
223
+ logger as any,
224
+ [],
225
+ true,
226
+ false
227
+ )
228
+
229
+ assert.strictEqual(http._state.statusCode, 500)
230
+ assert.strictEqual(http._state.jsonBody, undefined)
231
+ })
232
+
233
+ test('should bubble error when bubbleError is true', () => {
234
+ const logger = createMockLogger()
235
+ const http = createMockHTTP()
236
+ const error = new BadRequestError('bad')
237
+
238
+ assert.throws(
239
+ () =>
240
+ handleHTTPError(
241
+ error,
242
+ http as any,
243
+ 'tracker',
244
+ logger as any,
245
+ [],
246
+ true,
247
+ true
248
+ ),
249
+ (e: any) => e === error
250
+ )
251
+ })
252
+
253
+ test('should not bubble error when bubbleError is false', () => {
254
+ const logger = createMockLogger()
255
+ const http = createMockHTTP()
256
+ const error = new BadRequestError('bad')
257
+
258
+ handleHTTPError(
259
+ error,
260
+ http as any,
261
+ 'tracker',
262
+ logger as any,
263
+ [],
264
+ true,
265
+ false
266
+ )
267
+ // Should not throw
268
+ })
269
+
270
+ test('should call response.close when available', () => {
271
+ const logger = createMockLogger()
272
+ const http = createMockHTTP()
273
+ const error = new BadRequestError('bad')
274
+
275
+ handleHTTPError(
276
+ error,
277
+ http as any,
278
+ 'tracker',
279
+ logger as any,
280
+ [],
281
+ true,
282
+ false
283
+ )
284
+
285
+ assert.strictEqual(http._state.closed, true)
286
+ })
287
+
288
+ test('should handle undefined http gracefully', () => {
289
+ const logger = createMockLogger()
290
+ const error = new BadRequestError('bad')
291
+
292
+ handleHTTPError(error, undefined, 'tracker', logger as any, [], true, false)
293
+ // Should not throw
294
+ })
295
+
296
+ test('should include payload from error in known error response', () => {
297
+ const logger = createMockLogger()
298
+ const http = createMockHTTP()
299
+ const error = new BadRequestError('bad') as any
300
+ error.payload = { field: 'email', reason: 'invalid' }
301
+
302
+ handleHTTPError(
303
+ error,
304
+ http as any,
305
+ 'tracker',
306
+ logger as any,
307
+ [],
308
+ true,
309
+ false
310
+ )
311
+
312
+ assert.deepStrictEqual(http._state.jsonBody.payload, {
313
+ field: 'email',
314
+ reason: 'invalid',
315
+ })
316
+ })
317
+
318
+ test('should handle non-Error objects as errors', () => {
319
+ const logger = createMockLogger()
320
+ const http = createMockHTTP()
321
+ const error = 'just a string error'
322
+
323
+ handleHTTPError(
324
+ error,
325
+ http as any,
326
+ 'tracker',
327
+ logger as any,
328
+ [],
329
+ true,
330
+ false
331
+ )
332
+
333
+ assert.strictEqual(http._state.statusCode, 500)
334
+ const errors = logger._logs.filter((l) => l.level === 'error')
335
+ assert.ok(errors.some((e) => e.args[0] === 'just a string error'))
336
+ })
337
+
338
+ test('should handle warning log without trackerId', () => {
339
+ const logger = createMockLogger()
340
+ const http = createMockHTTP()
341
+ const error = new BadRequestError('bad')
342
+
343
+ handleHTTPError(
344
+ error,
345
+ http as any,
346
+ undefined,
347
+ logger as any,
348
+ [400],
349
+ true,
350
+ false
351
+ )
352
+
353
+ const warns = logger._logs.filter((l) => l.level === 'warn')
354
+ assert.ok(warns.length >= 1)
355
+ assert.ok(
356
+ !warns.some(
357
+ (w) => typeof w.args[0] === 'string' && w.args[0].includes('Warning id')
358
+ )
359
+ )
360
+ })
361
+ })
@@ -1,7 +1,7 @@
1
1
  import { getErrorResponse } from './errors/error-handler.js'
2
2
  import { NotFoundError } from './errors/errors.js'
3
- import { Logger } from './services/logger.js'
4
- import { PikkuHTTP } from './wirings/http/http.types.js'
3
+ import type { Logger } from './services/logger.js'
4
+ import type { PikkuHTTP } from './wirings/http/http.types.js'
5
5
 
6
6
  /**
7
7
  * Handle errors that occur during route processing
@@ -44,11 +44,11 @@ export const handleHTTPError = (
44
44
  if (trackerId) {
45
45
  logger.warn(`Warning id: ${trackerId}`)
46
46
  }
47
- logger.warn(e)
47
+ logger.warn(e instanceof Error ? e.message : e)
48
48
  }
49
49
  } else {
50
50
  // Handle unexpected errors
51
- logger.error(e)
51
+ logger.error(e instanceof Error ? e.message : e)
52
52
  http?.response?.status(500)
53
53
 
54
54
  if (trackerId) {
package/src/index.ts CHANGED
@@ -12,8 +12,6 @@ export type {
12
12
  CoreSingletonServices,
13
13
  CoreUserSession,
14
14
  CreateConfig,
15
- CreateSingletonServices,
16
- CreateWireServices,
17
15
  FunctionMeta,
18
16
  FunctionRuntimeMeta,
19
17
  FunctionServicesMeta,
@@ -69,13 +67,6 @@ export {
69
67
  isVersionedId,
70
68
  parseVersionedId,
71
69
  } from './version.js'
72
- export {
73
- pikkuState,
74
- initializePikkuState,
75
- resetPikkuState,
76
- addPackageServiceFactories,
77
- getPikkuMetaDir,
78
- } from './pikku-state.js'
79
70
  export { runPikkuFunc } from './function/function-runner.js'
80
71
  export { runCLICommand, pikkuCLIRender } from './wirings/cli/cli-runner.js'
81
72
  export { fetch } from './wirings/http/http-runner.js'
@@ -99,13 +90,15 @@ export type { HTTPMethod } from './wirings/http/http.types.js'
99
90
  export type { GraphNodeConfig } from './wirings/workflow/graph/workflow-graph.types.js'
100
91
  export { createGraph } from './wirings/workflow/graph/graph-node.js'
101
92
  export { workflow as wireWorkflow } from './wirings/workflow/workflow-helpers.js'
93
+ export { wireAddon } from './wirings/rpc/wire-addon.js'
94
+ export type { WireAddonConfig } from './wirings/rpc/wire-addon.js'
102
95
  export type { PikkuPackageState } from './types/state.types.js'
103
96
  export { runMiddleware, addMiddleware } from './middleware-runner.js'
104
97
  export { addPermission } from './permissions.js'
105
98
  export { isSerializable, stopSingletonServices } from './utils.js'
99
+ export { getSingletonServices, getCreateWireServices } from './pikku-state.js'
106
100
  export {
107
101
  type ScheduledTaskInfo,
108
102
  type ScheduledTaskSummary,
109
- type SchedulerRuntimeHandlers,
110
103
  } from './services/scheduler-service.js'
111
104
  export { SchedulerService } from './services/scheduler-service.js'
@@ -0,0 +1,6 @@
1
+ export { pikkuState, resetPikkuState } from './pikku-state.js'
2
+ export { httpRouter } from './wirings/http/routers/http-router.js'
3
+ export type {
4
+ CreateSingletonServices,
5
+ CreateWireServices,
6
+ } from './types/core.types.js'
@@ -2,7 +2,7 @@ import { describe, test, beforeEach } from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
3
  import { authAPIKey } from './auth-apikey.js'
4
4
  import { resetPikkuState } from '../pikku-state.js'
5
- import { CoreUserSession } from '../types/core.types.js'
5
+ import type { CoreUserSession } from '../types/core.types.js'
6
6
  import {
7
7
  PikkuSessionService,
8
8
  createMiddlewareSessionWireProps,
@@ -2,7 +2,7 @@ import { describe, test, beforeEach } from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
3
  import { authBearer } from './auth-bearer.js'
4
4
  import { resetPikkuState } from '../pikku-state.js'
5
- import { CoreUserSession } from '../types/core.types.js'
5
+ import type { CoreUserSession } from '../types/core.types.js'
6
6
  import {
7
7
  PikkuSessionService,
8
8
  createMiddlewareSessionWireProps,
@@ -1,9 +1,6 @@
1
1
  import { InvalidSessionError } from '../errors/errors.js'
2
- import {
3
- CoreUserSession,
4
- pikkuMiddleware,
5
- pikkuMiddlewareFactory,
6
- } from '../types/core.types.js'
2
+ import type { CoreUserSession } from '../types/core.types.js'
3
+ import { pikkuMiddleware, pikkuMiddlewareFactory } from '../types/core.types.js'
7
4
 
8
5
  /**
9
6
  * Bearer token middleware that extracts and validates tokens from the Authorization header.