@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.
- package/CHANGELOG.md +368 -0
- package/dist/dev/hot-reload.js +13 -55
- package/dist/dev/module-runner.d.ts +14 -0
- package/dist/dev/module-runner.js +59 -0
- package/dist/dev/reload-meta.d.ts +13 -0
- package/dist/dev/reload-meta.js +22 -0
- package/dist/errors/errors.d.ts +12 -0
- package/dist/errors/errors.js +19 -0
- package/dist/function/function-runner.d.ts +5 -5
- package/dist/function/function-runner.js +39 -111
- package/dist/function/functions.types.d.ts +17 -7
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/middleware/auth-apikey.d.ts +1 -0
- package/dist/middleware/auth-bearer.d.ts +1 -0
- package/dist/middleware/auth-cookie.d.ts +1 -0
- package/dist/middleware/cors.d.ts +1 -0
- package/dist/middleware/index.d.ts +1 -1
- package/dist/middleware/index.js +1 -1
- package/dist/middleware/remote-auth.d.ts +1 -0
- package/dist/middleware/telemetry.d.ts +2 -0
- package/dist/middleware/timeout.d.ts +1 -0
- package/dist/permissions.d.ts +42 -27
- package/dist/permissions.js +101 -203
- package/dist/pikku-state.js +0 -2
- package/dist/scopes.d.ts +17 -0
- package/dist/scopes.js +59 -0
- package/dist/services/ai-embedding-service.d.ts +31 -0
- package/dist/services/ai-embedding-service.js +1 -0
- package/dist/services/credential-wire-service.d.ts +3 -1
- package/dist/services/credential-wire-service.js +10 -4
- package/dist/services/index.d.ts +4 -0
- package/dist/services/index.js +2 -0
- package/dist/services/meta-service.d.ts +15 -2
- package/dist/services/meta-service.js +51 -1
- package/dist/services/queue-webhook-service.d.ts +55 -0
- package/dist/services/queue-webhook-service.js +136 -0
- package/dist/services/scope-service.d.ts +67 -0
- package/dist/services/scope-service.js +1 -0
- package/dist/services/typed-secret-service.d.ts +8 -0
- package/dist/services/typed-secret-service.js +39 -4
- package/dist/services/webhook-service.d.ts +140 -0
- package/dist/services/webhook-service.js +44 -0
- package/dist/services/workflow-service.d.ts +7 -0
- package/dist/testing/service-tests.js +37 -0
- package/dist/types/core.types.d.ts +34 -6
- package/dist/types/state.types.d.ts +20 -2
- package/dist/utils/hmac.d.ts +16 -0
- package/dist/utils/hmac.js +26 -0
- package/dist/utils/safe-fetch.d.ts +51 -0
- package/dist/utils/safe-fetch.js +192 -0
- package/dist/wirings/ai-agent/ai-agent-agui.d.ts +84 -0
- package/dist/wirings/ai-agent/ai-agent-agui.js +305 -0
- package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +105 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +249 -11
- package/dist/wirings/ai-agent/ai-agent-runner.js +10 -1
- package/dist/wirings/ai-agent/ai-agent-stream.js +88 -16
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +56 -2
- package/dist/wirings/ai-agent/index.d.ts +2 -1
- package/dist/wirings/ai-agent/index.js +2 -1
- package/dist/wirings/ai-agent/voice-input.d.ts +1 -0
- package/dist/wirings/ai-agent/voice-input.js +5 -41
- package/dist/wirings/ai-agent/voice-output.d.ts +1 -0
- package/dist/wirings/channel/channel-common.js +0 -1
- package/dist/wirings/channel/channel-handler.js +1 -4
- package/dist/wirings/channel/channel-middleware-runner.d.ts +12 -0
- package/dist/wirings/channel/channel-middleware-runner.js +34 -19
- package/dist/wirings/channel/channel.types.d.ts +2 -6
- package/dist/wirings/cli/cli-runner.js +3 -2
- package/dist/wirings/cli/cli.types.d.ts +1 -2
- package/dist/wirings/cli/command-parser.d.ts +2 -0
- package/dist/wirings/cli/command-parser.js +59 -2
- package/dist/wirings/credential/credential.types.d.ts +14 -0
- package/dist/wirings/credential/validate-credential-definitions.js +1 -0
- package/dist/wirings/gateway/gateway-runner.js +100 -50
- package/dist/wirings/gateway/gateway.types.d.ts +8 -5
- package/dist/wirings/http/http-routes.js +0 -3
- package/dist/wirings/http/http-runner.d.ts +9 -29
- package/dist/wirings/http/http-runner.js +9 -34
- package/dist/wirings/http/http.types.d.ts +4 -12
- package/dist/wirings/mcp/mcp-runner.js +0 -2
- package/dist/wirings/mcp/mcp.types.d.ts +5 -11
- package/dist/wirings/oauth2/index.d.ts +0 -3
- package/dist/wirings/oauth2/index.js +1 -2
- package/dist/wirings/rpc/addon-runner.d.ts +28 -0
- package/dist/wirings/rpc/addon-runner.js +173 -0
- package/dist/wirings/rpc/index.d.ts +5 -1
- package/dist/wirings/rpc/index.js +3 -1
- package/dist/wirings/rpc/remote-addon-auth.d.ts +26 -0
- package/dist/wirings/rpc/remote-addon-auth.js +43 -0
- package/dist/wirings/rpc/rpc-runner.d.ts +19 -0
- package/dist/wirings/rpc/rpc-runner.js +109 -3
- package/dist/wirings/rpc/rpc-types.d.ts +15 -1
- package/dist/wirings/rpc/wire-addon.js +9 -0
- package/dist/wirings/rpc/wire-remote-addon.d.ts +47 -0
- package/dist/wirings/rpc/wire-remote-addon.js +19 -0
- package/dist/wirings/scope/index.d.ts +3 -0
- package/dist/wirings/scope/index.js +2 -0
- package/dist/wirings/scope/scope.types.d.ts +39 -0
- package/dist/wirings/scope/scope.types.js +1 -0
- package/dist/wirings/scope/validate-scope-definitions.d.ts +16 -0
- package/dist/wirings/scope/validate-scope-definitions.js +76 -0
- package/dist/wirings/scope/wire-scope.d.ts +33 -0
- package/dist/wirings/scope/wire-scope.js +32 -0
- package/dist/wirings/secret/secret.types.d.ts +14 -0
- package/dist/wirings/secret/validate-secret-definitions.js +2 -0
- package/dist/wirings/variable/validate-variable-definitions.js +2 -0
- package/dist/wirings/variable/variable.types.d.ts +14 -0
- package/dist/wirings/workflow/dsl/index.d.ts +1 -1
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +89 -7
- package/dist/wirings/workflow/graph/graph-node.d.ts +2 -0
- package/dist/wirings/workflow/graph/graph-runner.js +3 -0
- package/dist/wirings/workflow/graph/wire-workflow-graph.d.ts +6 -0
- package/dist/wirings/workflow/graph/wire-workflow-graph.js +1 -0
- package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +5 -0
- package/dist/wirings/workflow/index.d.ts +2 -2
- package/dist/wirings/workflow/index.js +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +59 -1
- package/dist/wirings/workflow/pikku-workflow-service.js +187 -0
- package/dist/wirings/workflow/workflow.types.d.ts +1 -3
- package/package.json +6 -1
- package/run-tests.sh +1 -0
- package/src/dev/hot-reload.ts +13 -68
- package/src/dev/module-runner.test.ts +169 -0
- package/src/dev/module-runner.ts +103 -0
- package/src/dev/reload-meta.test.ts +31 -2
- package/src/dev/reload-meta.ts +26 -0
- package/src/errors/errors.ts +24 -0
- package/src/function/function-runner.test.ts +276 -142
- package/src/function/function-runner.ts +52 -145
- package/src/function/functions.types.ts +28 -38
- package/src/index.ts +8 -6
- package/src/middleware/index.ts +1 -5
- package/src/permissions.test.ts +160 -373
- package/src/permissions.ts +137 -279
- package/src/pikku-state.ts +0 -2
- package/src/scopes.test.ts +167 -0
- package/src/scopes.ts +69 -0
- package/src/services/ai-embedding-service.ts +31 -0
- package/src/services/credential-wire-service.ts +10 -4
- package/src/services/index.ts +20 -0
- package/src/services/meta-service.ts +68 -3
- package/src/services/queue-webhook-service.test.ts +408 -0
- package/src/services/queue-webhook-service.ts +182 -0
- package/src/services/scope-service.ts +75 -0
- package/src/services/typed-secret-service.test.ts +35 -0
- package/src/services/typed-secret-service.ts +39 -4
- package/src/services/webhook-service.ts +180 -0
- package/src/services/workflow-service.ts +7 -0
- package/src/testing/service-tests.ts +49 -0
- package/src/types/core.types.ts +42 -14
- package/src/types/state.types.ts +23 -3
- package/src/utils/hmac.ts +27 -0
- package/src/utils/safe-fetch.test.ts +373 -0
- package/src/utils/safe-fetch.ts +213 -0
- package/src/wirings/ai-agent/ai-agent-agui.test.ts +1127 -0
- package/src/wirings/ai-agent/ai-agent-agui.ts +386 -0
- package/src/wirings/ai-agent/ai-agent-authorization.test.ts +204 -0
- package/src/wirings/ai-agent/ai-agent-prepare.test.ts +483 -0
- package/src/wirings/ai-agent/ai-agent-prepare.ts +324 -13
- package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +207 -0
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +48 -1
- package/src/wirings/ai-agent/ai-agent-runner.ts +28 -0
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +301 -10
- package/src/wirings/ai-agent/ai-agent-stream.ts +122 -16
- package/src/wirings/ai-agent/ai-agent.types.ts +57 -0
- package/src/wirings/ai-agent/index.ts +5 -0
- package/src/wirings/ai-agent/voice-input.test.ts +90 -0
- package/src/wirings/ai-agent/voice-input.ts +9 -40
- package/src/wirings/channel/channel-common.ts +0 -1
- package/src/wirings/channel/channel-handler.ts +1 -9
- package/src/wirings/channel/channel-middleware-runner.test.ts +28 -2
- package/src/wirings/channel/channel-middleware-runner.ts +39 -27
- package/src/wirings/channel/channel.types.ts +0 -6
- package/src/wirings/cli/cli-runner.ts +4 -2
- package/src/wirings/cli/cli.types.ts +0 -2
- package/src/wirings/cli/command-parser.test.ts +130 -0
- package/src/wirings/cli/command-parser.ts +80 -2
- package/src/wirings/credential/credential.types.ts +14 -0
- package/src/wirings/credential/validate-credential-definitions.ts +1 -0
- package/src/wirings/gateway/gateway-authorization.test.ts +444 -0
- package/src/wirings/gateway/gateway-runner.ts +114 -68
- package/src/wirings/gateway/gateway.types.ts +7 -9
- package/src/wirings/http/http-routes.ts +0 -3
- package/src/wirings/http/http-runner.test.ts +1 -11
- package/src/wirings/http/http-runner.ts +16 -39
- package/src/wirings/http/http.types.ts +7 -14
- package/src/wirings/mcp/mcp-runner.ts +0 -2
- package/src/wirings/mcp/mcp.types.ts +3 -11
- package/src/wirings/oauth2/index.ts +0 -3
- package/src/wirings/rpc/addon-runner.ts +233 -0
- package/src/wirings/rpc/index.ts +17 -1
- package/src/wirings/rpc/remote-addon-auth.ts +69 -0
- package/src/wirings/rpc/rpc-runner.test.ts +319 -16
- package/src/wirings/rpc/rpc-runner.ts +155 -3
- package/src/wirings/rpc/rpc-types.ts +16 -5
- package/src/wirings/rpc/wire-addon.test.ts +6 -3
- package/src/wirings/rpc/wire-addon.ts +9 -0
- package/src/wirings/rpc/wire-remote-addon.ts +57 -0
- package/src/wirings/scope/index.ts +14 -0
- package/src/wirings/scope/scope.test.ts +135 -0
- package/src/wirings/scope/scope.types.ts +44 -0
- package/src/wirings/scope/validate-scope-definitions.ts +110 -0
- package/src/wirings/scope/wire-scope.ts +34 -0
- package/src/wirings/secret/secret.types.ts +14 -0
- package/src/wirings/secret/validate-secret-definitions.ts +2 -0
- package/src/wirings/variable/validate-variable-definitions.ts +2 -0
- package/src/wirings/variable/variable.types.ts +14 -0
- package/src/wirings/workflow/dsl/index.ts +4 -0
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +97 -6
- package/src/wirings/workflow/graph/graph-node.ts +2 -0
- package/src/wirings/workflow/graph/graph-runner.test.ts +58 -0
- package/src/wirings/workflow/graph/graph-runner.ts +3 -0
- package/src/wirings/workflow/graph/wire-workflow-graph.ts +7 -0
- package/src/wirings/workflow/graph/workflow-graph.types.ts +5 -0
- package/src/wirings/workflow/index.ts +5 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +341 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +272 -0
- package/src/wirings/workflow/workflow-on-error.test.ts +154 -0
- package/src/wirings/workflow/workflow-step-session.test.ts +21 -5
- package/src/wirings/workflow/workflow.types.ts +4 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/src/wirings/oauth2/oauth2-client.test.ts +0 -930
- package/src/wirings/oauth2/oauth2-client.ts +0 -331
- 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
|
package/dist/dev/hot-reload.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { watch } from 'node:fs';
|
|
2
|
-
import { stat
|
|
3
|
-
import { basename,
|
|
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
|
-
//
|
|
37
|
-
// (
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
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
|
|
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;
|
package/dist/dev/reload-meta.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/errors/errors.d.ts
CHANGED
|
@@ -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
|
package/dist/errors/errors.js
CHANGED
|
@@ -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
|