@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
package/CHANGELOG.md CHANGED
@@ -1,3 +1,371 @@
1
+ ## 0.12.66
2
+
3
+ ### Patch Changes
4
+
5
+ - 5f19016: Widen the generated agent HTTP surface, and guard attachment downloads against SSRF.
6
+
7
+ `agentCaller` and `agentStreamCaller` declared only `message`, `threadId` and
8
+ `resourceId` (plus `context` on the stream route), so `attachments`, `model`,
9
+ `temperature` — all accepted by `AIAgentInput` — were unreachable over the
10
+ shipped HTTP contract. No deployed app could send an attachment or a per-request
11
+ model override. Both callers now share an `AgentCallerInput` type covering every
12
+ optional field and forward each one to the RPC.
13
+
14
+ Both callers declare that shape **inline** in the generic position rather than
15
+ behind a shared named alias: the schema extractor only reads type literals there
16
+ and synthesises the schema name from the function name. Behind an alias it
17
+ records an `inputSchemaName` with no schema generated for it, and every agent
18
+ HTTP call then fails at runtime with `MissingSchemaError`.
19
+
20
+ Widening that surface makes caller-supplied attachment URLs reachable, which is
21
+ an SSRF vector: the AI SDK downloads attachment URLs **server-side** whenever the
22
+ model cannot consume them natively, using an unguarded `fetch`. A caller could
23
+ point an attachment at the cloud metadata endpoint or another internal host and
24
+ have the response relayed into the model's context. `VercelAIAgentRunner` now
25
+ passes an `experimental_download` implementation backed by `safeFetch` (which
26
+ refuses private/internal hosts and non-HTTP schemes, and re-validates every
27
+ redirect hop) to both `streamText` and `generateText`. URLs the model supports
28
+ natively are passed through untouched, so the provider still fetches those
29
+ itself.
30
+
31
+ The runner takes an optional `allowedAttachmentHosts` allowlist, carried across
32
+ `withApiKey`. `safeFetch` is now exported from `@pikku/core/safe-fetch`.
33
+
34
+ - 78e4778: Stop a failed message persist during an agent stream from killing the process.
35
+
36
+ The persisting channel flushes from inside `send`, which is synchronous and so cannot await the flush. Any rejection — a dropped storage connection, or a model reusing a `toolCallId`, which is a primary key in AI storage — escaped as an unhandled rejection and took the whole server down. Persistence from `send` is now best-effort and logged; the awaited `flush()` on the suspend paths still surfaces failures to its caller.
37
+
38
+ - 4324652: Scope AI agent thread reads to the calling session.
39
+
40
+ The generated thread-management functions (`getAgentThreads`,
41
+ `getAgentThreadMessages`, `getAgentThreadRuns`, `deleteAgentThread`) keyed purely
42
+ off a caller-supplied `threadId` and treated `resourceId` as an optional filter,
43
+ so omitting it enumerated every tenant's threads.
44
+ - `listThreads` gains an `owners` **authorization constraint** (distinct from the
45
+ `resourceId` filter): an empty array matches nothing, and it is always derived
46
+ from the session, never from input. Implemented across the Kysely, Redis and
47
+ MongoDB agent run services, with LIKE/regex metacharacter escaping so an owner
48
+ id containing `_` or `%` cannot match a foreign owner.
49
+ - The three `threadId`-keyed functions are now guarded by an `isThreadOwner`
50
+ `pikkuPermission` rather than an in-body check. A thread that does not exist is
51
+ denied rather than 404'd, so it is indistinguishable from one owned by someone
52
+ else.
53
+ - New `@pikku/core/ai-agent` helpers: `canAccessThread`, `threadOwnerConstraint`,
54
+ `sessionPrincipals`, `isOwnedByPrincipal`.
55
+
56
+ Services destructured by a wired function are now non-optional inside it.
57
+
58
+ The inspector already aggregated the services used by every wired `func`,
59
+ `permissions` and `middleware` into `RequiredSingletonServices`, but the
60
+ generated function types defaulted their service parameter to the raw `Services`
61
+ — so a service declared `foo?: Foo` still arrived as possibly-undefined, forcing
62
+ `if (!foo) throw new MissingServiceError(...)` guards that could never fire.
63
+ Generated types now expose `WiredSingletonServices` / `WiredServices`
64
+ (`RequiredSingletonServices & Services`) and default the `RequiredServices`
65
+ generic of functions, permissions, middleware, auth and approval-description
66
+ helpers to them. Optionality now means only what it should: "this service may
67
+ not be created, because nothing uses it".
68
+
69
+ - de044f8: Fix the agent tool-list permission filter failing open.
70
+
71
+ `buildToolDefs` filtered permission-gated tools by resolving `checkAuthPermissions` from a function's _metadata_ — a by-name lookup into the `misc/permissions` state that nothing ever populates. It therefore collected no predicate and returned `true`, so every auth-gated tool was offered to the model regardless of session (its input schema and description leaked, and the model could attempt calls that then failed at invocation).
72
+
73
+ `checkAuthPermissions` now takes the live `CorePermissionGroup` from the function/agent config, where the `pikkuAuth` brand actually survives — matching how the agent's own gate and the function runner already resolve permissions by reference. The dead by-name lookup (`getPermissionByName`) is removed. Enforcement on invocation was never affected; this closes the exposure gap in the offered tool list.
74
+
75
+ - cd1a811: warn instead of silently ignoring unknown long CLI options
76
+
77
+ An unknown long option (`--sektion functions` or `--sektion=functions`) was parsed
78
+ into the options object and then silently dropped by the command's input schema —
79
+ the command ran with the real option at its default and produced plausible-but-wrong
80
+ output. Unknown long options are still accepted (forward compatibility is preserved),
81
+ but the parser now records a warning that the runner prints to stderr, e.g.
82
+ `Warning: Unknown option: --sektion (ignored) Did you mean --section?`.
83
+
84
+ - 19fa6f0: Fix `HTTPRouteConfig` and `HTTPRoutesGroupConfig`'s default `PikkuPermission`/`PikkuMiddleware` type parameters under-specifying their own generic arguments (e.g. `CorePikkuPermission<any>` instead of `CorePikkuPermission<any, any, any>`). The missing arguments silently fell back to `CorePikkuPermission`'s own defaults (`CoreServices`, with `schema` optional) instead of `any`, so a project whose generated services type guarantees `schema` is always present (any project using `WiredServices`-style non-optional services) failed to type-check against `defineHTTPRoutes`/`wireHTTPRoutes` with a misleading `index signature` error.
85
+ - b501612: Enforce authorization consistently across `pikku*` primitives.
86
+ - `pikkuAIAgent` now enforces `permissions` (previously accepted but never
87
+ checked) and gains `auth` and `scopes`. Scopes are checked before permissions.
88
+ `auth` defaults to `false`, matching `pikkuSessionlessFunc`, since agents are
89
+ typically invoked from an already-authenticated function or from sessionless
90
+ contexts such as crons and queue workers.
91
+ - `pikkuWorkflowFunc` / `pikkuWorkflowComplexFunc` schema config gains `auth`
92
+ and `scopes` alongside `permissions`.
93
+ - `pikkuScenario` no longer accepts `auth`, `scopes`, or `permissions` —
94
+ scenarios drive the app as actors and authorize per step.
95
+ - `wireGateway` no longer accepts `permissions`. A gateway proxies to an agent,
96
+ so access is governed by normal auth plus the target agent's own rules.
97
+ - Removed the dead `permissions` field from `CoreWorkflow`, which was never read.
98
+
99
+ Closed two paths that reached user code without authorization:
100
+ - Gateway handlers were invoked directly, so a handler's own `auth`, `scopes`
101
+ and `permissions` were never evaluated. Webhook, websocket and listener
102
+ gateways now invoke the handler through the function runner. Handlers are
103
+ sessionless by default (inbound gateway traffic is platform-authenticated by
104
+ the adapter, not session-bearing); declare `auth: true` to require a session.
105
+ A gateway's own `auth` field is now honoured too — it was previously ignored.
106
+ Gateway middleware runs before the gate, so `wire.setSession()` in gateway
107
+ middleware — the idiomatic way to map a verified platform sender to a user —
108
+ is visible to the handler's `auth` and `scopes`.
109
+ - Resuming a suspended agent run (`resumeAIAgentSync`, `resumeAIAgent`) checked
110
+ run ownership but never re-ran the agent's own gate, so a scope or permission
111
+ revoked while a run was suspended did not prevent the caller from resuming it
112
+ and approving its pending tool calls. Both now re-run `assertAgentAuthorized`
113
+ before any state is mutated.
114
+
115
+ - eb37b1e: Fix `voiceInput` middleware losing the runner receiver: it grabbed
116
+ `aiAgentRunner.transcribe` as a bare method reference, so calling it left `this`
117
+ undefined and threw `Cannot read properties of undefined (reading 'getModel')`
118
+ on the first audio attachment. It now calls `aiAgentRunner.transcribe(...)`
119
+ directly, preserving the receiver.
120
+
121
+ ## 0.12.65
122
+
123
+ ### Patch Changes
124
+
125
+ - 1a86d3f: Fix a fanout collapsing into a single step, and preserve graph node config.
126
+ - A fanout took its `stepName` from the first step of its body. Node ids _are_
127
+ step names, so the loop and that step got the same id and the step overwrote
128
+ the loop: `await Promise.all(users.map(...))` rendered as one plain call, and
129
+ everything after the loop became unreachable. A fanout is not itself a cached
130
+ step, so it no longer borrows a name.
131
+ - A `workflow.sleep` or `workflow.suspend` inside a fanout body was dropped at
132
+ extraction — `FanoutStepMeta.body` was typed RPC-only. It now admits sleep and
133
+ suspend, and the regenerated body emits them.
134
+ - Regenerating a `pikkuWorkflowGraph` dropped `onError`, `retries` and
135
+ `retryDelay` from every node, and graph-level `notes`. All four are honoured
136
+ at runtime, so the round trip silently changed behaviour.
137
+
138
+ - 1a86d3f: Support multi-step fanout bodies in DSL workflows.
139
+
140
+ A `Promise.all(array.map(...))` (or `for...of`) body containing more than one
141
+ `workflow.do` call previously extracted only a single step: `const`-captured
142
+ steps were skipped entirely by the parallel extractor, so a body like
143
+
144
+ ```ts
145
+ await Promise.all(
146
+ users.map(async (u) => {
147
+ const digestData = await workflow.do('Get pipeline', 'getDigestData', {
148
+ userId: u.id,
149
+ })
150
+ await workflow.do('Send digest', 'sendDigestEmail', { ...digestData })
151
+ })
152
+ )
153
+ ```
154
+
155
+ produced a graph with `getDigestData` missing and `sendDigestEmail` referencing
156
+ an unregistered variable. `FanoutStepMeta.child` is replaced by
157
+ `FanoutStepMeta.body: RpcStepMeta[]`, holding the per-iteration steps inline in
158
+ the same workflow — no sub-workflow boundary. Per-iteration `const` bindings are
159
+ now registered so later steps in the same iteration can reference them, and the
160
+ sequential path no longer hard-errors on bodies with more than one step.
161
+
162
+ - 1a86d3f: Add `onError` compensation to DSL workflows.
163
+
164
+ A DSL workflow had no way to express error handling at all — `try/catch` is not
165
+ an allowed statement, and step options carried only `retries`/`retryDelay`. A
166
+ step can now name a compensation RPC:
167
+
168
+ ```ts
169
+ await workflow.do(
170
+ 'Charge',
171
+ 'chargeCard',
172
+ { id },
173
+ {
174
+ retries: 3,
175
+ onError: 'refundOrder',
176
+ }
177
+ )
178
+ ```
179
+
180
+ Semantics mirror a graph node's `onError` exactly: once the step's retries are
181
+ exhausted the handler is invoked with `{ error: { message } }` and the original
182
+ error is still thrown. This is compensation, not recovery — the workflow fails
183
+ either way. The handler runs as its own durable step, so a replay cannot
184
+ compensate twice, and it does not inherit `onError` itself.
185
+
186
+ The handler is materialised as a real graph node, so it is wired like any other
187
+ RPC and the console draws a dashed red "on error" edge to it rather than the
188
+ route being invisible.
189
+
190
+ - 1a86d3f: Stop silently dropping switch cases and spread returns from workflow graphs.
191
+ - A fall-through case (`case 'a': case 'b': ...`) recorded only the last value.
192
+ A run entering on `'a'` therefore appeared to match no case at all. Empty
193
+ clauses now carry through to the entry they fall into — the next non-empty
194
+ case, otherwise `default`, otherwise the switch exit.
195
+ - `return { ...r, extra: 1 }` produced a return node listing only `extra`, so
196
+ the graph claimed an output shape the workflow does not have, with no
197
+ diagnostic. `return r` produced no return node at all. `ReturnStepMeta` now
198
+ records a `spread` list, and the regenerated code emits it.
199
+
200
+ - 1a86d3f: Stop corrupting values when regenerating a workflow from its graph.
201
+ - A numeric `workflow.sleep('Wait', 5000)` came back as `'5000'`, and a numeric
202
+ `retryDelay` likewise. Durations are `string | number`; only strings are
203
+ quoted now.
204
+ - An assignment to a context variable was stored as an opaque `value`, so
205
+ `count = count + 1` regenerated as `count = 'count + 1'` — an expression
206
+ turned into a string literal. `SetStepMeta` now carries a separate
207
+ `expression` field (mirroring `SwitchCaseMeta`), so a string literal and a
208
+ code expression are no longer indistinguishable in the meta.
209
+ - A `next` that was not a single node id was coerced with a string cast: an
210
+ array became the bogus id `'a,b'` and a branch-key record became
211
+ `'[object Object]'`, severing every downstream node. Arrays, key-based
212
+ routing tables and condition lists now each render in their own shape.
213
+ - A `filter`/`some`/`every` node with no `outputVar` emitted
214
+ `const undefined = ...`, which does not parse.
215
+
216
+ - 1a86d3f: Keep a `workflow.sleep` whose duration is only known at runtime (a loop
217
+ variable, a field off the input). The closure evaluates it, so it is legal DSL;
218
+ its source text is recorded as an `expression` and emitted raw when regenerating
219
+ code, as a set step already does.
220
+ - 3d76f51: Add an optional `docsUrl` to `wireSecret`, `wireVariable`, and `wireCredential`, so a console or deploy UI reporting a missing value can link the user to where they obtain it instead of showing a bare identifier.
221
+
222
+ ## 0.12.64
223
+
224
+ ### Patch Changes
225
+
226
+ - 7ab5287: Security hardening (follow-up to the #966 C1/C3 fixes):
227
+ - SSRF (C1): `isPrivateHost` now also rejects alias/encoded forms that resolve to internal targets — trailing-dot FQDNs (`localhost.`), the reserved `*.localhost` name, IPv4-mapped IPv6 (`::ffff:127.0.0.1`), octal/decimal/hex-encoded IPv4 (`0177.0.0.1`, `2130706433`, `0x7f000001`), and the full `fe80::/10` link-local range. `safeFetch` also strips `Authorization` and `Cookie` headers whenever a redirect crosses origin so credentials cannot leak to a redirected host.
228
+ - Forgeable approval markers (C3): the sub-agent approval marker is now identified by a non-forgeable `Symbol` brand set only by framework code, instead of the plain `__approvalRequired` string key. A delegating tool's LLM-shaped `result.object` (plain JSON) can no longer conjure an approval/suspension even though the tool is allowed to forward approvals.
229
+
230
+ - e86bc17: Security + feature: bind AI agent thread/run ownership to the authenticated session and add `sessionScope: 'user' | 'org'` to agents. The owner key is now the trusted principal (`session.userId`, or `session.orgId` for org-scoped agents) composed with the client `resourceId` (`principal:resourceId`), so a client-supplied `resourceId` sub-partitions within the caller's own boundary but can never widen access to another user's or org's threads. Resolution is idempotent (safe for sub-agent recursion and resume); org scope with no session org is denied; sessionless `user` wirings fall back to the bare `resourceId`.
231
+ - a9b96a0: Security: only honor the `__approvalRequired` suspension marker from framework sub-agent tools (`forwardsApproval`), so an attacker-influenced ordinary tool result can no longer forge an approval/suspension.
232
+ - 3f7fc54: Security: SSRF-harden outgoing webhook delivery and voice-input audio fetch (validate scheme + block private/internal hosts, re-validate every redirect hop via a shared `safeFetch`); stop the channel stream-middleware cache from reusing an earlier run's per-invocation middleware closures across runs.
233
+ - c478794: Simplify authorization to be session + function based (#972). Permissions are now function-scoped only: global permissions AND together, a function's own permissions OR together, and the two are independent gates that both must pass — a broad global can no longer satisfy an admin-only function. Removed wire-, tag-, and HTTP-route-level permissions (`addTagPermission`, `addHTTPPermission`, wire-level `permissions` on HTTP/channel/MCP wirings). Tags are now organizational only. `auth` (session presence) and tag/HTTP middleware are unchanged.
234
+ - 3f04ae4: Fix `pikku dev` hot-reload memory leak (#975). Changed user files were re-imported under a fresh URL on every reload (a `data:` URL on Node, a uniquely-named temp sibling on Bun), which permanently leaked a record in the native ESM loader map — the dev server climbed to `JavaScript heap out of memory` during long editing sessions (worse on Bun, which the sandbox dev server runs on). Reloading now goes through an evictable module runner that transpiles the source and runs it via `vm.compileFunction`, holding exports under a stable path key so each edit overwrites one slot and the previous module is collected. Heap stays bounded on both runtimes.
235
+ - 90d9f04: Scope `console:getAddonInstalledPackage` to the addon's own `.pikku` metadata.
236
+
237
+ Previously every addon returned the _app's_ secrets/wirings (read from the app's
238
+ `.pikku` root), so the installed-package view couldn't show what a given addon
239
+ actually requires. `MetaService` gains optional `readPackageFile`/`readPackageDir`
240
+ helpers (implemented by `LocalMetaService`, which resolves the addon package's
241
+ root from node_modules), and `getAddonInstalledPackage` now reads secrets,
242
+ variables, wirings, schemas, README and package.json from the addon package
243
+ itself. It also reads and returns the addon's `credentials` meta (OAuth2 + wire
244
+ credentials), which was never surfaced before — entries with an `oauth2` field
245
+ are the OAuth integrations to connect.
246
+
247
+ - cb079cc: A workflow-graph node's `func` can now reference a registered AI agent by name, dispatched as an agent run like sub-workflows, with `ref()` resolving the agent's output keys.
248
+ - cb079cc: `pikkuAIAgent` gains a `workflows: []` capability: a referenced workflow is exposed to the LLM as a tool that runs inline and returns its output.
249
+ - 0a7db82: AI agent tool `execute()` failures are now logged via `logger.error` unconditionally (then rethrown), instead of only surfacing when a tool-call middleware hook is registered.
250
+ - 981c4db: Add a model-baked `AIEmbeddingService` interface and optional `aiEmbedding` slot on `CoreSingletonServices`, with separate `embedDocuments`/`embedQuery` methods for vector-store addons.
251
+ - 13474a6: Generate a `ScopeId` union from `wireScope` declarations.
252
+
253
+ `pikku all` now emits `.pikku/scopes/pikku-scopes.gen.ts` with a `ScopeId` union
254
+ of every declared scope, plus a wildcard form for each node that has
255
+ descendants. A project's generated `pikkuFunc` narrows `scopes` to that union,
256
+ so an undeclared scope is a compile error with editor autocomplete:
257
+
258
+ ```ts
259
+ wireScope({ admin: { scopes: { invoices: { scopes: { create: {} } } } } })
260
+
261
+ pikkuFunc({
262
+ scopes: ['admin:invoices:create'], // ✓ autocompleted
263
+ func: ...,
264
+ })
265
+
266
+ pikkuFunc({
267
+ scopes: ['admin:invoice:create'], // ✗ compile error (typo)
268
+ func: ...,
269
+ })
270
+ ```
271
+
272
+ The inspector independently rejects undeclared scopes, so a cast that defeats
273
+ the compiler is still caught at build time.
274
+
275
+ Also fixes `getArrayPropertyValue` dropping any array behind a cast — idiomatic
276
+ `tags: ['a'] as const` was previously invisible to the inspector and silently
277
+ omitted from meta.
278
+
279
+ - 5a2b0d5: Prune removed addons on `pikku dev` hot-reload. Deleting an addon wiring (`*.addon.ts`) regenerated `.pikku` on disk but left its `wireAddon` entry stranded in the live `pikkuState(null,'addons','packages')` map until a full restart (the reimport path is add-only), so `getInstalledAddons` kept reporting deleted addons. `reloadGeneratedMeta`'s sibling `reconcileAddonRegistry(declaredNamespaces)` now drops any addon namespace the fresh inspection no longer declares, and the dev watcher calls it with `inspectorState.rpc.wireAddonDeclarations`. Routes already reconcile (http meta is replaced wholesale + router reset); function-impl entries are intentionally left since the workflow service registers framework internals there that aren't in the generated set.
280
+ - 13474a6: feat(scopes): grant scopes directly to a user, not only through roles
281
+
282
+ A scope can now be granted to a user directly, outside of any role.
283
+ `resolveScopes` returns the union of a user's role-derived scopes and their
284
+ direct grants, so a one-off capability no longer requires inventing a role.
285
+ - `@pikku/core`: `ScopeService` gains `addScopeToUser` / `removeScopeFromUser` /
286
+ `listUserScopes`.
287
+ - `@pikku/kysely`: a new `pikku_user_scope` table (FK into `pikku_scopes`, so the
288
+ database still refuses an undeclared grant; `ON DELETE CASCADE` from `user`,
289
+ so deleting a user takes their direct grants with it). `resolveScopes` unions
290
+ it with the role join.
291
+ - `@pikku/addon-console`: `scopeAddScopeToUser` / `scopeRemoveScopeFromUser`
292
+ (gated by `pikku:scopes:manage`), and `scopeListUserRoles` now also returns
293
+ `directScopes`.
294
+ - `@pikku/console`: a **Direct scopes** section in the user roles drawer to grant
295
+ and revoke scopes directly, showing them distinctly from the resolved union.
296
+
297
+ Also: the Scopes page now distinguishes a permission error (a console admin
298
+ without `pikku:scopes:read`) from an actual scope-service outage, instead of
299
+ showing "the scope service may be unavailable" for both.
300
+
301
+ - ee040dc: fix(ai-agent): resolve addon-scoped services when generating a tool's approval description. The `approvalDescription` for an addon function ran against a cold per-package services cache and silently fell back to root services, so descriptions reading addon-only services (e.g. a todo store) threw and the approval `reason` never reached the client. It now builds the addon's singleton services the same way the tool's `execute` path does (#971).
302
+ - cb079cc: `pikkuWorkflowGraph` nodes accept an optional `notes?: string` and the graph an optional `notes?: string[]`; notes are documentation only and excluded from `graphHash`.
303
+ - 13474a6: feat: ScopeService.listScopes
304
+
305
+ Exposes the scope vocabulary held in the store — everything a role can be
306
+ composed from — flagging any scope that is still present but no longer declared
307
+ in code (inert, and awaiting `pikku scopes prune`).
308
+
309
+ - 9f0d0eb: Migrate the `--oauth` addon scaffold off `OAuth2Client`. A scaffolded OAuth2
310
+ addon service used to construct `new OAuth2Client(config, appCredentialSecretId,
311
+ secrets)` and do its own token exchange/refresh — the responsibility better-auth
312
+ now owns via the credential service. The `pikku new addon --oauth` scaffold (and
313
+ the OpenAPI `--openapi` generator) now emit a service that receives a ready
314
+ access token: `services.ts` uses `createWireServices` + `wire.getCredential<{
315
+ accessToken: string }>(name)` and the service does a plain `fetch` with
316
+ `Authorization: Bearer ${accessToken}`, matching the existing per-user
317
+ bearer/apikey credential scaffold. With no remaining consumers, `OAuth2Client`
318
+ (`@pikku/core/oauth2`) and its test are removed; the `./oauth2` export keeps the
319
+ `OAuth2AppCredential` / `OAuth2Token` types.
320
+ - 13474a6: Add scopes: declared, statically-checked authorization scopes on pikkuFunc.
321
+
322
+ A scope is a capability string the session must hold. Unlike `permissions` —
323
+ which OR together across global/wire/tag/function levels — scopes are an AND
324
+ gate that runs before them, so adding one can only ever narrow access.
325
+
326
+ ```ts
327
+ wireScope({
328
+ admin: {
329
+ scopes: { invoices: { scopes: { create: {} } } },
330
+ },
331
+ })
332
+
333
+ export const createInvoice = pikkuFunc({
334
+ scopes: ['admin:invoices:create'],
335
+ func: async (services, data) => { ... },
336
+ })
337
+ ```
338
+
339
+ The gate runs after the auth check and before the request body is evaluated,
340
+ since scopes depend only on the session. A session lacking a required scope
341
+ gets a `MissingScopeError` (403) naming it. Wildcards grant subtrees:
342
+ `admin:*` satisfies `admin` and `admin:invoices:create`.
343
+
344
+ `session.scopes` is populated by whoever builds the session — core reads it and
345
+ never fetches, keeping the runner free of I/O. The new `ScopeService` interface
346
+ resolves scopes at the session boundary.
347
+
348
+ - 70fa400: Add outgoing webhooks — `webhookService.send()` enqueues signed deliveries onto a retrying queue, `@pikku/kysely`'s `KyselyWebhookService` persists per-attempt delivery history, and `@pikku/console` gains a read-only `/webhooks` page; also caches resolved secrets in `TypedSecretService` and registers inline-`func` metadata for queue/scheduler/trigger/gateway wirings.
349
+ - 7b2ea23: `wireAddon` can install one addon package as multiple named instances, each with its own per-instance singleton services and `secretOverrides`/`variableOverrides`/`credentialOverrides` that alias logical names to real project secrets/variables/credentials.
350
+ - 1dc77d5: Remove the old, pre-better-auth OAuth2 credential runtime now that the
351
+ `credentialOAuth` plugin owns credential linking, storage and refresh.
352
+ - `@pikku/core`: drop the unused `createOAuth2Handler` HTTP-routes flow (and its
353
+ `CreateOAuth2HandlerOptions`) from the `./oauth2` entrypoint. The credential
354
+ schema types (`OAuth2AppCredential`, `OAuth2Token`) and the `OAuth2Client`
355
+ API helper remain exported.
356
+ - `@pikku/addon-console`: delete the six `oauth-*` console functions
357
+ (connect/disconnect/status/exchange-tokens/refresh-token/test-token) and the
358
+ `OAuthService` behind them — credential connections now flow through
359
+ better-auth's `/credential-oauth/link` + `/callback`.
360
+ - `@pikku/console`: the credential UI no longer calls the removed
361
+ `console:oauth*` RPCs. Per-user and singleton (platform) OAuth2 credentials
362
+ connect via the `/credential-oauth/link` full-page redirect and disconnect via
363
+ `console:credentialDelete`; the `/oauth/callback` popup page is removed.
364
+
365
+ - 416606c: Fix a TypeScript 6 `PikkuWire` constraint collapse that made `rpc` a required field: narrow `PikkuRPC` default type params from `any` to `Function` and replace bare `any` TypedRPC args with `PikkuRPC`.
366
+ - d2a6eea: Add `wireRemoteAddon` — consume a hosted addon's `remote: true` RPCs transparently over HTTP, with the addon installed as a devDependency (types only). `rpc('ns:fn', input)` dispatches to the host's `/remote/rpc/:rpcName`, authenticating as a client with a token bound from a local source (`{ credentialId }` per-user, `{ secretId }` platform, or a custom `resolve()`), or omitted for a public surface. This is any-machine → hosted-library client auth, distinct from the trusted mesh (`PIKKU_REMOTE_SECRET`). A new `.remote.gen.d.ts` RPC map exposes only the `remote: true` surface to consumers. `pikku` verify errors if a `wireRemoteAddon` package is a production dependency (or missing) instead of a devDependency, and if a bound `credentialId`/`secretId` isn't wired.
367
+ - 30e62ee: Add `workflow.approval(reason, { schema, expiry })` — a return-valued, expiring human-in-the-loop gate that stays closed until a decision is recorded (via `workflowService.approveStep` or `POST /workflow/:workflowName/approve/:runId`), unlike the one-shot `workflow.suspend()`.
368
+
1
369
  ## 0.12.63
2
370
 
3
371
  ### Patch Changes
@@ -1,13 +1,12 @@
1
1
  import { watch } from 'node:fs';
2
- import { stat, readFile, copyFile, rm } from 'node:fs/promises';
3
- import { basename, dirname, join, resolve, relative } from 'node:path';
4
- import { pathToFileURL } from 'node:url';
5
- import { register } from 'tsx/esm/api';
2
+ import { stat } from 'node:fs/promises';
3
+ import { basename, join, resolve, relative } from 'node:path';
6
4
  import { pikkuState } from '../pikku-state.js';
7
5
  import { clearMiddlewareCache } from '../middleware-runner.js';
8
6
  import { clearPermissionsCache } from '../permissions.js';
9
7
  import { clearChannelMiddlewareCache } from '../wirings/channel/channel-middleware-runner.js';
10
8
  import { httpRouter } from '../wirings/http/routers/http-router.js';
9
+ import { createModuleRunner } from './module-runner.js';
11
10
  export * from './reload-meta.js';
12
11
  const isFunctionConfig = (value) => {
13
12
  return (typeof value === 'object' &&
@@ -33,57 +32,18 @@ const findCompiledFile = async (tsFile, srcDir, pikkuDir) => {
33
32
  }
34
33
  return null;
35
34
  };
36
- // Use data: URLs to import modules. This bypasses TypeScript loaders
37
- // (e.g. tsx) that intercept file:// imports and break dynamic ESM loading.
38
- // Each import gets unique content so there's no module cache to worry about.
39
- let tsxRegistered = false;
40
- const ensureTsxRegistered = () => {
41
- if (tsxRegistered)
42
- return;
43
- register();
44
- tsxRegistered = true;
45
- };
46
- let tempCounter = 0;
47
- const reimportModule = async (filePath, useTsx = false) => {
48
- try {
49
- if (useTsx) {
50
- // Import a uniquely-named sibling copy: a `?t=` query does NOT bust
51
- // the cache on either runtime (Bun keys module identity on the bare
52
- // path; tsx's transform cache keys on the file path too), so a fresh
53
- // path is the only reliable re-import. Same directory → identical
54
- // resolution for relative and package-`imports` (#…) specifiers. The
55
- // dot-prefix keeps it out of the watcher.
56
- if (!process.versions.bun) {
57
- // Node needs tsx's loader to import raw .ts; Bun imports it natively.
58
- ensureTsxRegistered();
59
- }
60
- const abs = resolve(filePath);
61
- const tempPath = join(dirname(abs), `.pikku-hot-${++tempCounter}-${basename(abs)}`);
62
- await copyFile(abs, tempPath);
63
- try {
64
- return await import(pathToFileURL(tempPath).href);
65
- }
66
- finally {
67
- await rm(tempPath, { force: true }).catch(() => {
68
- // Best-effort temp cleanup; a leftover dotfile is watcher-ignored.
69
- });
70
- }
71
- }
72
- const content = await readFile(resolve(filePath), 'utf-8');
73
- const dataUrl = 'data:text/javascript;base64,' + Buffer.from(content).toString('base64');
74
- return await import(dataUrl);
75
- }
76
- catch {
77
- return null;
78
- }
79
- };
35
+ // Changed user files are re-run through an evictable module runner keyed by a
36
+ // stable path (createModuleRunner), NOT re-imported under a fresh URL. A
37
+ // fresh-URL import (a `data:` URL on Node, a uniquely-named temp sibling on
38
+ // Bun) permanently leaks a record in the native ESM loader map on every
39
+ // reload, OOMing long editing sessions; the runner overwrites a single slot so
40
+ // the previous module is collected. See ./module-runner.ts.
80
41
  const isWatchedTsFile = (filename) => {
81
42
  return (filename.endsWith('.ts') &&
82
43
  !filename.endsWith('.test.ts') &&
83
44
  !filename.endsWith('.d.ts') &&
84
45
  !filename.endsWith('.gen.ts') &&
85
- // Hidden files: editor/sed atomic-write temps and our own hot-reload
86
- // sibling copies must never trigger a reload of themselves.
46
+ // Hidden files: editor/sed atomic-write temps must never trigger a reload.
87
47
  !basename(filename).startsWith('.'));
88
48
  };
89
49
  export async function pikkuDevReloader(options) {
@@ -92,6 +52,7 @@ export async function pikkuDevReloader(options) {
92
52
  const absPikkuDir = resolve(pikkuDir);
93
53
  const watchers = [];
94
54
  const functionsMap = pikkuState(null, 'function', 'functions');
55
+ const moduleRunner = createModuleRunner();
95
56
  const handleFileChange = async (changedTsFile) => {
96
57
  const start = Date.now();
97
58
  const reloadedNames = [];
@@ -101,11 +62,7 @@ export async function pikkuDevReloader(options) {
101
62
  return;
102
63
  const compiledFile = await findCompiledFile(changedTsFile, srcDir, absPikkuDir);
103
64
  const importPath = compiledFile ?? changedTsFile;
104
- const usedTsxFallback = !compiledFile;
105
- if (usedTsxFallback) {
106
- logger.debug(`Hot-reload using tsx fallback for: ${relative(process.cwd(), changedTsFile)}`);
107
- }
108
- const mod = await reimportModule(importPath, usedTsxFallback);
65
+ const mod = await moduleRunner.run(importPath);
109
66
  if (!mod) {
110
67
  logger.error(`Failed to import: ${relative(process.cwd(), importPath)} (keeping old code)`);
111
68
  return;
@@ -197,6 +154,7 @@ export async function pikkuDevReloader(options) {
197
154
  for (const watcher of watchers) {
198
155
  watcher.close();
199
156
  }
157
+ moduleRunner.clear();
200
158
  },
201
159
  // Drain the queue of recently re-imported files and import them again.
202
160
  // A dev-server watcher calls this AFTER its codegen pass has refreshed
@@ -0,0 +1,14 @@
1
+ export interface PikkuModuleRunner {
2
+ /** Run a user module by absolute path, returning its exports. Repeated runs
3
+ * of the same path overwrite a single registry slot (the previous module is
4
+ * collected). Returns `null` if transpile/compile/evaluation fails, so the
5
+ * caller can keep the previously-loaded code. */
6
+ run: (absPath: string) => Promise<Record<string, unknown> | null>;
7
+ /** Drop a single module from the registry. */
8
+ evict: (absPath: string) => void;
9
+ /** Drop every module from the registry. */
10
+ clear: () => void;
11
+ /** Number of modules currently held. */
12
+ readonly size: number;
13
+ }
14
+ export declare const createModuleRunner: () => PikkuModuleRunner;
@@ -0,0 +1,59 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { compileFunction } from 'node:vm';
6
+ let transformSync;
7
+ const loadTransform = async () => {
8
+ if (transformSync)
9
+ return transformSync;
10
+ // esbuild is a dev-only dependency, hoisted from @pikku/cli at runtime — the
11
+ // same lazy pattern the reloader previously used for tsx. It is never loaded
12
+ // in production runtimes (the `./dev` export is dev-only).
13
+ const esbuild = await import('esbuild');
14
+ transformSync = esbuild.transformSync;
15
+ return transformSync;
16
+ };
17
+ export const createModuleRunner = () => {
18
+ const registry = new Map();
19
+ const run = async (filePath) => {
20
+ const absPath = resolve(filePath);
21
+ try {
22
+ const transform = await loadTransform();
23
+ const source = await readFile(absPath, 'utf-8');
24
+ // No sourcemap: an inline sourcemap embeds a base64 copy of the source
25
+ // that the engine retains per compile, reintroducing linear growth on
26
+ // Node — the exact leak this runner exists to remove. `filename` still
27
+ // anchors stack traces to the user file.
28
+ const { code } = transform(source, {
29
+ loader: absPath.endsWith('.ts') ? 'ts' : 'js',
30
+ format: 'cjs',
31
+ sourcefile: absPath,
32
+ });
33
+ const fn = compileFunction(code, ['require', 'exports', 'module', '__filename', '__dirname'], { filename: absPath });
34
+ const require = createRequire(pathToFileURL(absPath));
35
+ const moduleObj = { exports: {} };
36
+ fn(require, moduleObj.exports, moduleObj, absPath, dirname(absPath));
37
+ registry.set(absPath, moduleObj.exports);
38
+ return moduleObj.exports;
39
+ }
40
+ catch {
41
+ // Transpile/compile/evaluate failure (a bad edit, or the one known
42
+ // limitation — a file using top-level `await`, which can't be emitted in
43
+ // `cjs` form). Return null so the caller keeps the previously-loaded code.
44
+ return null;
45
+ }
46
+ };
47
+ return {
48
+ run,
49
+ evict: (filePath) => {
50
+ registry.delete(resolve(filePath));
51
+ },
52
+ clear: () => {
53
+ registry.clear();
54
+ },
55
+ get size() {
56
+ return registry.size;
57
+ },
58
+ };
59
+ };
@@ -22,3 +22,16 @@ export interface ReloadGeneratedMetaOptions {
22
22
  * never imported); those still need a restart.
23
23
  */
24
24
  export declare function reloadGeneratedMeta(options: ReloadGeneratedMetaOptions): Promise<void>;
25
+ /**
26
+ * Prunes the live addon registry down to what the current source still declares.
27
+ *
28
+ * Dev hot-reload re-imports only changed/added files, so a deleted `*.addon.ts`
29
+ * leaves its `wireAddon` entry stranded in `pikkuState(null,'addons','packages')`
30
+ * until a full restart — the reason `getInstalledAddons` reports removed addons.
31
+ * The Map holds definitions (namespace → package config), not runtime state, so
32
+ * dropping any namespace the fresh inspection no longer declares is safe.
33
+ *
34
+ * `declaredNamespaces` is the set of `wireAddon` names from the just-regenerated
35
+ * inspection (`inspectorState.rpc.wireAddonDeclarations.keys()`).
36
+ */
37
+ export declare function reconcileAddonRegistry(declaredNamespaces: Iterable<string>, logger?: Logger): void;
@@ -97,3 +97,25 @@ export async function reloadGeneratedMeta(options) {
97
97
  logger.error(`Hot-reload schema recompilation failed: ${err instanceof Error ? err.message : String(err)}`);
98
98
  }
99
99
  }
100
+ /**
101
+ * Prunes the live addon registry down to what the current source still declares.
102
+ *
103
+ * Dev hot-reload re-imports only changed/added files, so a deleted `*.addon.ts`
104
+ * leaves its `wireAddon` entry stranded in `pikkuState(null,'addons','packages')`
105
+ * until a full restart — the reason `getInstalledAddons` reports removed addons.
106
+ * The Map holds definitions (namespace → package config), not runtime state, so
107
+ * dropping any namespace the fresh inspection no longer declares is safe.
108
+ *
109
+ * `declaredNamespaces` is the set of `wireAddon` names from the just-regenerated
110
+ * inspection (`inspectorState.rpc.wireAddonDeclarations.keys()`).
111
+ */
112
+ export function reconcileAddonRegistry(declaredNamespaces, logger) {
113
+ const declared = new Set(declaredNamespaces);
114
+ const addons = pikkuState(null, 'addons', 'packages');
115
+ for (const namespace of [...addons.keys()]) {
116
+ if (!declared.has(namespace)) {
117
+ addons.delete(namespace);
118
+ logger?.info(`• Removed unwired addon "${namespace}"`);
119
+ }
120
+ }
121
+ }
@@ -60,6 +60,18 @@ export declare class MissingCredentialError extends PikkuError {
60
60
  };
61
61
  constructor(credentialName: string, credentialType: 'oauth2' | 'apikey', connectUrl?: string);
62
62
  }
63
+ /**
64
+ * The session does not hold a scope required by the function. The payload
65
+ * names the missing scope so the client can surface what to request.
66
+ * @group Error
67
+ */
68
+ export declare class MissingScopeError extends PikkuError {
69
+ payload: {
70
+ error: 'missing_scope';
71
+ scope: string;
72
+ };
73
+ constructor(scope: string);
74
+ }
63
75
  /**
64
76
  * The session is readonly and cannot access a non-readonly function.
65
77
  * @group Error
@@ -105,6 +105,25 @@ addError(MissingCredentialError, {
105
105
  status: 403,
106
106
  message: 'A required credential is not configured.',
107
107
  });
108
+ /**
109
+ * The session does not hold a scope required by the function. The payload
110
+ * names the missing scope so the client can surface what to request.
111
+ * @group Error
112
+ */
113
+ export class MissingScopeError extends PikkuError {
114
+ payload;
115
+ constructor(scope) {
116
+ super(`Missing required scope: ${scope}`);
117
+ this.payload = {
118
+ error: 'missing_scope',
119
+ scope,
120
+ };
121
+ }
122
+ }
123
+ addError(MissingScopeError, {
124
+ status: 403,
125
+ message: 'The session does not hold a scope required by this function.',
126
+ });
108
127
  /**
109
128
  * The session is readonly and cannot access a non-readonly function.
110
129
  * @group Error