@pikku/core 0.12.62 → 0.12.64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/dist/dev/hot-reload.js +13 -55
  3. package/dist/dev/module-runner.d.ts +14 -0
  4. package/dist/dev/module-runner.js +59 -0
  5. package/dist/dev/reload-meta.d.ts +13 -0
  6. package/dist/dev/reload-meta.js +22 -0
  7. package/dist/errors/errors.d.ts +12 -0
  8. package/dist/errors/errors.js +19 -0
  9. package/dist/function/function-runner.d.ts +5 -5
  10. package/dist/function/function-runner.js +39 -111
  11. package/dist/function/functions.types.d.ts +17 -7
  12. package/dist/index.d.ts +5 -1
  13. package/dist/index.js +3 -1
  14. package/dist/middleware/auth-apikey.d.ts +1 -0
  15. package/dist/middleware/auth-bearer.d.ts +1 -0
  16. package/dist/middleware/auth-cookie.d.ts +1 -0
  17. package/dist/middleware/cors.d.ts +1 -0
  18. package/dist/middleware/index.d.ts +1 -1
  19. package/dist/middleware/index.js +1 -1
  20. package/dist/middleware/remote-auth.d.ts +1 -0
  21. package/dist/middleware/telemetry.d.ts +2 -0
  22. package/dist/middleware/timeout.d.ts +1 -0
  23. package/dist/permissions.d.ts +32 -25
  24. package/dist/permissions.js +104 -185
  25. package/dist/pikku-state.js +0 -2
  26. package/dist/scopes.d.ts +17 -0
  27. package/dist/scopes.js +59 -0
  28. package/dist/services/ai-embedding-service.d.ts +31 -0
  29. package/dist/services/ai-embedding-service.js +1 -0
  30. package/dist/services/credential-wire-service.d.ts +3 -1
  31. package/dist/services/credential-wire-service.js +10 -4
  32. package/dist/services/index.d.ts +4 -0
  33. package/dist/services/index.js +2 -0
  34. package/dist/services/meta-service.d.ts +15 -2
  35. package/dist/services/meta-service.js +51 -1
  36. package/dist/services/queue-webhook-service.d.ts +55 -0
  37. package/dist/services/queue-webhook-service.js +136 -0
  38. package/dist/services/scope-service.d.ts +67 -0
  39. package/dist/services/scope-service.js +1 -0
  40. package/dist/services/typed-secret-service.d.ts +8 -0
  41. package/dist/services/typed-secret-service.js +39 -4
  42. package/dist/services/webhook-service.d.ts +140 -0
  43. package/dist/services/webhook-service.js +44 -0
  44. package/dist/services/workflow-service.d.ts +7 -0
  45. package/dist/types/core.types.d.ts +34 -6
  46. package/dist/types/state.types.d.ts +20 -2
  47. package/dist/utils/hmac.d.ts +16 -0
  48. package/dist/utils/hmac.js +26 -0
  49. package/dist/utils/safe-fetch.d.ts +51 -0
  50. package/dist/utils/safe-fetch.js +192 -0
  51. package/dist/wirings/ai-agent/ai-agent-agui.d.ts +84 -0
  52. package/dist/wirings/ai-agent/ai-agent-agui.js +305 -0
  53. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +41 -1
  54. package/dist/wirings/ai-agent/ai-agent-prepare.js +146 -6
  55. package/dist/wirings/ai-agent/ai-agent-runner.js +6 -1
  56. package/dist/wirings/ai-agent/ai-agent-stream.js +61 -10
  57. package/dist/wirings/ai-agent/ai-agent.types.d.ts +27 -1
  58. package/dist/wirings/ai-agent/index.d.ts +1 -0
  59. package/dist/wirings/ai-agent/index.js +1 -0
  60. package/dist/wirings/ai-agent/voice-input.d.ts +1 -0
  61. package/dist/wirings/ai-agent/voice-input.js +2 -38
  62. package/dist/wirings/ai-agent/voice-output.d.ts +1 -0
  63. package/dist/wirings/channel/channel-common.js +0 -1
  64. package/dist/wirings/channel/channel-handler.js +1 -4
  65. package/dist/wirings/channel/channel-middleware-runner.d.ts +12 -0
  66. package/dist/wirings/channel/channel-middleware-runner.js +34 -19
  67. package/dist/wirings/channel/channel.types.d.ts +2 -6
  68. package/dist/wirings/cli/cli-runner.js +0 -2
  69. package/dist/wirings/cli/cli.types.d.ts +1 -2
  70. package/dist/wirings/http/http-routes.js +0 -3
  71. package/dist/wirings/http/http-runner.d.ts +9 -29
  72. package/dist/wirings/http/http-runner.js +13 -35
  73. package/dist/wirings/http/http.types.d.ts +1 -9
  74. package/dist/wirings/mcp/mcp-runner.js +0 -2
  75. package/dist/wirings/mcp/mcp.types.d.ts +5 -11
  76. package/dist/wirings/oauth2/index.d.ts +0 -3
  77. package/dist/wirings/oauth2/index.js +1 -2
  78. package/dist/wirings/rpc/addon-runner.d.ts +28 -0
  79. package/dist/wirings/rpc/addon-runner.js +173 -0
  80. package/dist/wirings/rpc/index.d.ts +5 -1
  81. package/dist/wirings/rpc/index.js +3 -1
  82. package/dist/wirings/rpc/remote-addon-auth.d.ts +26 -0
  83. package/dist/wirings/rpc/remote-addon-auth.js +43 -0
  84. package/dist/wirings/rpc/rpc-runner.d.ts +19 -0
  85. package/dist/wirings/rpc/rpc-runner.js +109 -3
  86. package/dist/wirings/rpc/rpc-types.d.ts +15 -1
  87. package/dist/wirings/rpc/wire-addon.js +9 -0
  88. package/dist/wirings/rpc/wire-remote-addon.d.ts +47 -0
  89. package/dist/wirings/rpc/wire-remote-addon.js +19 -0
  90. package/dist/wirings/scope/index.d.ts +3 -0
  91. package/dist/wirings/scope/index.js +2 -0
  92. package/dist/wirings/scope/scope.types.d.ts +39 -0
  93. package/dist/wirings/scope/scope.types.js +1 -0
  94. package/dist/wirings/scope/validate-scope-definitions.d.ts +16 -0
  95. package/dist/wirings/scope/validate-scope-definitions.js +76 -0
  96. package/dist/wirings/scope/wire-scope.d.ts +33 -0
  97. package/dist/wirings/scope/wire-scope.js +32 -0
  98. package/dist/wirings/workflow/dsl/index.d.ts +1 -1
  99. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +53 -1
  100. package/dist/wirings/workflow/graph/graph-node.d.ts +2 -0
  101. package/dist/wirings/workflow/graph/graph-runner.js +3 -0
  102. package/dist/wirings/workflow/graph/wire-workflow-graph.d.ts +6 -0
  103. package/dist/wirings/workflow/graph/wire-workflow-graph.js +1 -0
  104. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +5 -0
  105. package/dist/wirings/workflow/index.d.ts +2 -2
  106. package/dist/wirings/workflow/index.js +1 -1
  107. package/dist/wirings/workflow/pikku-workflow-service.d.ts +51 -1
  108. package/dist/wirings/workflow/pikku-workflow-service.js +171 -0
  109. package/dist/wirings/workflow/workflow.types.d.ts +1 -1
  110. package/package.json +5 -1
  111. package/run-tests.sh +1 -0
  112. package/src/dev/hot-reload.ts +13 -68
  113. package/src/dev/module-runner.test.ts +169 -0
  114. package/src/dev/module-runner.ts +103 -0
  115. package/src/dev/reload-meta.test.ts +31 -2
  116. package/src/dev/reload-meta.ts +26 -0
  117. package/src/errors/errors.ts +24 -0
  118. package/src/function/function-runner.test.ts +276 -142
  119. package/src/function/function-runner.ts +52 -145
  120. package/src/function/functions.types.ts +28 -38
  121. package/src/index.ts +8 -6
  122. package/src/middleware/index.ts +1 -5
  123. package/src/permissions.test.ts +153 -372
  124. package/src/permissions.ts +139 -259
  125. package/src/pikku-state.ts +0 -2
  126. package/src/scopes.test.ts +167 -0
  127. package/src/scopes.ts +69 -0
  128. package/src/services/ai-embedding-service.ts +31 -0
  129. package/src/services/credential-wire-service.ts +10 -4
  130. package/src/services/index.ts +20 -0
  131. package/src/services/meta-service.ts +68 -3
  132. package/src/services/queue-webhook-service.test.ts +408 -0
  133. package/src/services/queue-webhook-service.ts +182 -0
  134. package/src/services/scope-service.ts +75 -0
  135. package/src/services/typed-secret-service.test.ts +35 -0
  136. package/src/services/typed-secret-service.ts +39 -4
  137. package/src/services/webhook-service.ts +180 -0
  138. package/src/services/workflow-service.ts +7 -0
  139. package/src/types/core.types.ts +42 -14
  140. package/src/types/state.types.ts +23 -3
  141. package/src/utils/hmac.ts +27 -0
  142. package/src/utils/safe-fetch.test.ts +373 -0
  143. package/src/utils/safe-fetch.ts +213 -0
  144. package/src/wirings/ai-agent/ai-agent-agui.test.ts +1127 -0
  145. package/src/wirings/ai-agent/ai-agent-agui.ts +386 -0
  146. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +308 -0
  147. package/src/wirings/ai-agent/ai-agent-prepare.ts +192 -8
  148. package/src/wirings/ai-agent/ai-agent-runner.test.ts +48 -1
  149. package/src/wirings/ai-agent/ai-agent-runner.ts +21 -0
  150. package/src/wirings/ai-agent/ai-agent-stream.test.ts +198 -10
  151. package/src/wirings/ai-agent/ai-agent-stream.ts +84 -10
  152. package/src/wirings/ai-agent/ai-agent.types.ts +28 -0
  153. package/src/wirings/ai-agent/index.ts +1 -0
  154. package/src/wirings/ai-agent/voice-input.ts +6 -35
  155. package/src/wirings/channel/channel-common.ts +0 -1
  156. package/src/wirings/channel/channel-handler.ts +1 -9
  157. package/src/wirings/channel/channel-middleware-runner.test.ts +28 -2
  158. package/src/wirings/channel/channel-middleware-runner.ts +39 -27
  159. package/src/wirings/channel/channel.types.ts +0 -6
  160. package/src/wirings/cli/cli-runner.ts +0 -2
  161. package/src/wirings/cli/cli.types.ts +0 -2
  162. package/src/wirings/http/http-routes.ts +0 -3
  163. package/src/wirings/http/http-runner.test.ts +1 -11
  164. package/src/wirings/http/http-runner.ts +19 -40
  165. package/src/wirings/http/http.types.ts +1 -10
  166. package/src/wirings/mcp/mcp-runner.ts +0 -2
  167. package/src/wirings/mcp/mcp.types.ts +3 -11
  168. package/src/wirings/oauth2/index.ts +0 -3
  169. package/src/wirings/rpc/addon-runner.ts +233 -0
  170. package/src/wirings/rpc/index.ts +17 -1
  171. package/src/wirings/rpc/remote-addon-auth.ts +69 -0
  172. package/src/wirings/rpc/rpc-runner.test.ts +319 -16
  173. package/src/wirings/rpc/rpc-runner.ts +155 -3
  174. package/src/wirings/rpc/rpc-types.ts +16 -5
  175. package/src/wirings/rpc/wire-addon.test.ts +6 -3
  176. package/src/wirings/rpc/wire-addon.ts +9 -0
  177. package/src/wirings/rpc/wire-remote-addon.ts +57 -0
  178. package/src/wirings/scope/index.ts +14 -0
  179. package/src/wirings/scope/scope.test.ts +135 -0
  180. package/src/wirings/scope/scope.types.ts +44 -0
  181. package/src/wirings/scope/validate-scope-definitions.ts +110 -0
  182. package/src/wirings/scope/wire-scope.ts +34 -0
  183. package/src/wirings/workflow/dsl/index.ts +4 -0
  184. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +61 -0
  185. package/src/wirings/workflow/graph/graph-node.ts +2 -0
  186. package/src/wirings/workflow/graph/graph-runner.test.ts +58 -0
  187. package/src/wirings/workflow/graph/graph-runner.ts +3 -0
  188. package/src/wirings/workflow/graph/wire-workflow-graph.ts +7 -0
  189. package/src/wirings/workflow/graph/workflow-graph.types.ts +5 -0
  190. package/src/wirings/workflow/index.ts +5 -0
  191. package/src/wirings/workflow/pikku-workflow-service.test.ts +341 -0
  192. package/src/wirings/workflow/pikku-workflow-service.ts +236 -0
  193. package/src/wirings/workflow/workflow-step-session.test.ts +21 -5
  194. package/src/wirings/workflow/workflow.types.ts +4 -0
  195. package/tsconfig.tsbuildinfo +1 -1
  196. package/src/wirings/oauth2/oauth2-client.test.ts +0 -930
  197. package/src/wirings/oauth2/oauth2-client.ts +0 -331
  198. package/src/wirings/oauth2/oauth2-routes.ts +0 -234
package/CHANGELOG.md CHANGED
@@ -1,3 +1,156 @@
1
+ ## 0.12.64
2
+
3
+ ### Patch Changes
4
+
5
+ - 7ab5287: Security hardening (follow-up to the #966 C1/C3 fixes):
6
+ - 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.
7
+ - 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.
8
+
9
+ - 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`.
10
+ - 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.
11
+ - 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.
12
+ - 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.
13
+ - 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.
14
+ - 90d9f04: Scope `console:getAddonInstalledPackage` to the addon's own `.pikku` metadata.
15
+
16
+ Previously every addon returned the _app's_ secrets/wirings (read from the app's
17
+ `.pikku` root), so the installed-package view couldn't show what a given addon
18
+ actually requires. `MetaService` gains optional `readPackageFile`/`readPackageDir`
19
+ helpers (implemented by `LocalMetaService`, which resolves the addon package's
20
+ root from node_modules), and `getAddonInstalledPackage` now reads secrets,
21
+ variables, wirings, schemas, README and package.json from the addon package
22
+ itself. It also reads and returns the addon's `credentials` meta (OAuth2 + wire
23
+ credentials), which was never surfaced before — entries with an `oauth2` field
24
+ are the OAuth integrations to connect.
25
+
26
+ - 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.
27
+ - cb079cc: `pikkuAIAgent` gains a `workflows: []` capability: a referenced workflow is exposed to the LLM as a tool that runs inline and returns its output.
28
+ - 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.
29
+ - 981c4db: Add a model-baked `AIEmbeddingService` interface and optional `aiEmbedding` slot on `CoreSingletonServices`, with separate `embedDocuments`/`embedQuery` methods for vector-store addons.
30
+ - 13474a6: Generate a `ScopeId` union from `wireScope` declarations.
31
+
32
+ `pikku all` now emits `.pikku/scopes/pikku-scopes.gen.ts` with a `ScopeId` union
33
+ of every declared scope, plus a wildcard form for each node that has
34
+ descendants. A project's generated `pikkuFunc` narrows `scopes` to that union,
35
+ so an undeclared scope is a compile error with editor autocomplete:
36
+
37
+ ```ts
38
+ wireScope({ admin: { scopes: { invoices: { scopes: { create: {} } } } } })
39
+
40
+ pikkuFunc({
41
+ scopes: ['admin:invoices:create'], // ✓ autocompleted
42
+ func: ...,
43
+ })
44
+
45
+ pikkuFunc({
46
+ scopes: ['admin:invoice:create'], // ✗ compile error (typo)
47
+ func: ...,
48
+ })
49
+ ```
50
+
51
+ The inspector independently rejects undeclared scopes, so a cast that defeats
52
+ the compiler is still caught at build time.
53
+
54
+ Also fixes `getArrayPropertyValue` dropping any array behind a cast — idiomatic
55
+ `tags: ['a'] as const` was previously invisible to the inspector and silently
56
+ omitted from meta.
57
+
58
+ - 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.
59
+ - 13474a6: feat(scopes): grant scopes directly to a user, not only through roles
60
+
61
+ A scope can now be granted to a user directly, outside of any role.
62
+ `resolveScopes` returns the union of a user's role-derived scopes and their
63
+ direct grants, so a one-off capability no longer requires inventing a role.
64
+ - `@pikku/core`: `ScopeService` gains `addScopeToUser` / `removeScopeFromUser` /
65
+ `listUserScopes`.
66
+ - `@pikku/kysely`: a new `pikku_user_scope` table (FK into `pikku_scopes`, so the
67
+ database still refuses an undeclared grant; `ON DELETE CASCADE` from `user`,
68
+ so deleting a user takes their direct grants with it). `resolveScopes` unions
69
+ it with the role join.
70
+ - `@pikku/addon-console`: `scopeAddScopeToUser` / `scopeRemoveScopeFromUser`
71
+ (gated by `pikku:scopes:manage`), and `scopeListUserRoles` now also returns
72
+ `directScopes`.
73
+ - `@pikku/console`: a **Direct scopes** section in the user roles drawer to grant
74
+ and revoke scopes directly, showing them distinctly from the resolved union.
75
+
76
+ Also: the Scopes page now distinguishes a permission error (a console admin
77
+ without `pikku:scopes:read`) from an actual scope-service outage, instead of
78
+ showing "the scope service may be unavailable" for both.
79
+
80
+ - 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).
81
+ - cb079cc: `pikkuWorkflowGraph` nodes accept an optional `notes?: string` and the graph an optional `notes?: string[]`; notes are documentation only and excluded from `graphHash`.
82
+ - 13474a6: feat: ScopeService.listScopes
83
+
84
+ Exposes the scope vocabulary held in the store — everything a role can be
85
+ composed from — flagging any scope that is still present but no longer declared
86
+ in code (inert, and awaiting `pikku scopes prune`).
87
+
88
+ - 9f0d0eb: Migrate the `--oauth` addon scaffold off `OAuth2Client`. A scaffolded OAuth2
89
+ addon service used to construct `new OAuth2Client(config, appCredentialSecretId,
90
+ secrets)` and do its own token exchange/refresh — the responsibility better-auth
91
+ now owns via the credential service. The `pikku new addon --oauth` scaffold (and
92
+ the OpenAPI `--openapi` generator) now emit a service that receives a ready
93
+ access token: `services.ts` uses `createWireServices` + `wire.getCredential<{
94
+ accessToken: string }>(name)` and the service does a plain `fetch` with
95
+ `Authorization: Bearer ${accessToken}`, matching the existing per-user
96
+ bearer/apikey credential scaffold. With no remaining consumers, `OAuth2Client`
97
+ (`@pikku/core/oauth2`) and its test are removed; the `./oauth2` export keeps the
98
+ `OAuth2AppCredential` / `OAuth2Token` types.
99
+ - 13474a6: Add scopes: declared, statically-checked authorization scopes on pikkuFunc.
100
+
101
+ A scope is a capability string the session must hold. Unlike `permissions` —
102
+ which OR together across global/wire/tag/function levels — scopes are an AND
103
+ gate that runs before them, so adding one can only ever narrow access.
104
+
105
+ ```ts
106
+ wireScope({
107
+ admin: {
108
+ scopes: { invoices: { scopes: { create: {} } } },
109
+ },
110
+ })
111
+
112
+ export const createInvoice = pikkuFunc({
113
+ scopes: ['admin:invoices:create'],
114
+ func: async (services, data) => { ... },
115
+ })
116
+ ```
117
+
118
+ The gate runs after the auth check and before the request body is evaluated,
119
+ since scopes depend only on the session. A session lacking a required scope
120
+ gets a `MissingScopeError` (403) naming it. Wildcards grant subtrees:
121
+ `admin:*` satisfies `admin` and `admin:invoices:create`.
122
+
123
+ `session.scopes` is populated by whoever builds the session — core reads it and
124
+ never fetches, keeping the runner free of I/O. The new `ScopeService` interface
125
+ resolves scopes at the session boundary.
126
+
127
+ - 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.
128
+ - 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.
129
+ - 1dc77d5: Remove the old, pre-better-auth OAuth2 credential runtime now that the
130
+ `credentialOAuth` plugin owns credential linking, storage and refresh.
131
+ - `@pikku/core`: drop the unused `createOAuth2Handler` HTTP-routes flow (and its
132
+ `CreateOAuth2HandlerOptions`) from the `./oauth2` entrypoint. The credential
133
+ schema types (`OAuth2AppCredential`, `OAuth2Token`) and the `OAuth2Client`
134
+ API helper remain exported.
135
+ - `@pikku/addon-console`: delete the six `oauth-*` console functions
136
+ (connect/disconnect/status/exchange-tokens/refresh-token/test-token) and the
137
+ `OAuthService` behind them — credential connections now flow through
138
+ better-auth's `/credential-oauth/link` + `/callback`.
139
+ - `@pikku/console`: the credential UI no longer calls the removed
140
+ `console:oauth*` RPCs. Per-user and singleton (platform) OAuth2 credentials
141
+ connect via the `/credential-oauth/link` full-page redirect and disconnect via
142
+ `console:credentialDelete`; the `/oauth/callback` popup page is removed.
143
+
144
+ - 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`.
145
+ - 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.
146
+ - 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()`.
147
+
148
+ ## 0.12.63
149
+
150
+ ### Patch Changes
151
+
152
+ - ae65588: `fetchData` now defaults `exposeErrors` to `!isProduction()`, so a non-production HTTP server returns the error `message` and `stack` on unexpected 500s instead of a bare `{ errorId }`. A dev/sandbox RPC that 500s is now debuggable from the response alone; production (NODE_ENV=production) still returns only the errorId.
153
+
1
154
  ## 0.12.62
2
155
 
3
156
  ### 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
@@ -1,12 +1,13 @@
1
- import type { CoreUserSession, CorePikkuMiddleware, PikkuWiringTypes, PikkuRawWire, MiddlewareMetadata, PermissionMetadata, CoreSingletonServices, CreateWireServices } from '../types/core.types.js';
1
+ import type { CoreUserSession, CorePikkuMiddleware, PikkuWiringTypes, PikkuRawWire, MiddlewareMetadata, CoreSingletonServices, CreateWireServices } from '../types/core.types.js';
2
2
  import type { CorePikkuChannelMiddleware } from '../wirings/channel/channel.types.js';
3
- import type { CorePermissionGroup, CorePikkuFunctionConfig, CorePikkuPermission } from './functions.types.js';
3
+ import type { CorePikkuFunctionConfig } from './functions.types.js';
4
4
  import type { SessionService } from '../services/user-session-service.js';
5
5
  import { PikkuCredentialWireService } from '../services/credential-wire-service.js';
6
+ import type { AddonInstance } from '../wirings/rpc/addon-runner.js';
6
7
  export declare const addFunction: (funcName: string, funcConfig: CorePikkuFunctionConfig<any, any>, packageName?: string | null) => void;
7
8
  export declare const getFunctionNames: (packageName?: string | null) => string[];
8
9
  export declare const getAllFunctionNames: () => string[];
9
- export declare const runPikkuFunc: <In = any, Out = any>(wireType: PikkuWiringTypes, wireId: string, funcName: string, { singletonServices, createWireServices, data, auth: wiringAuth, inheritedMiddleware, wireMiddleware, inheritedChannelMiddleware, wireChannelMiddleware, inheritedPermissions, wirePermissions, coerceDataFromSchema, tags, wire, sessionService, credentialWireService, packageName, }: {
10
+ export declare const runPikkuFunc: <In = any, Out = any>(wireType: PikkuWiringTypes, wireId: string, funcName: string, { singletonServices, createWireServices, data, auth: wiringAuth, inheritedMiddleware, wireMiddleware, inheritedChannelMiddleware, wireChannelMiddleware, coerceDataFromSchema, wire, sessionService, credentialWireService, packageName, addonInstance, }: {
10
11
  singletonServices: CoreSingletonServices;
11
12
  createWireServices?: CreateWireServices;
12
13
  data: () => Promise<In> | In;
@@ -15,12 +16,11 @@ export declare const runPikkuFunc: <In = any, Out = any>(wireType: PikkuWiringTy
15
16
  wireMiddleware?: CorePikkuMiddleware[];
16
17
  inheritedChannelMiddleware?: MiddlewareMetadata[];
17
18
  wireChannelMiddleware?: CorePikkuChannelMiddleware[];
18
- inheritedPermissions?: PermissionMetadata[];
19
- wirePermissions?: CorePermissionGroup | CorePikkuPermission[];
20
19
  coerceDataFromSchema?: boolean;
21
20
  tags?: string[];
22
21
  wire: PikkuRawWire;
23
22
  sessionService?: SessionService<CoreUserSession>;
24
23
  credentialWireService?: PikkuCredentialWireService;
25
24
  packageName?: string | null;
25
+ addonInstance?: AddonInstance;
26
26
  }) => Promise<Out>;