@pikku/core 0.12.64 → 0.12.67

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 (74) hide show
  1. package/CHANGELOG.md +311 -0
  2. package/dist/index.d.ts +2 -2
  3. package/dist/index.js +1 -1
  4. package/dist/permissions.d.ts +12 -4
  5. package/dist/permissions.js +11 -32
  6. package/dist/scopes.d.ts +14 -0
  7. package/dist/scopes.js +39 -8
  8. package/dist/testing/service-tests.js +37 -0
  9. package/dist/types/core.types.d.ts +22 -0
  10. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +64 -0
  11. package/dist/wirings/ai-agent/ai-agent-prepare.js +103 -5
  12. package/dist/wirings/ai-agent/ai-agent-runner.js +5 -1
  13. package/dist/wirings/ai-agent/ai-agent-stream.js +28 -7
  14. package/dist/wirings/ai-agent/ai-agent.types.d.ts +29 -1
  15. package/dist/wirings/ai-agent/index.d.ts +1 -1
  16. package/dist/wirings/ai-agent/index.js +1 -1
  17. package/dist/wirings/ai-agent/voice-input.js +3 -3
  18. package/dist/wirings/cli/cli-runner.js +3 -0
  19. package/dist/wirings/cli/command-parser.d.ts +2 -0
  20. package/dist/wirings/cli/command-parser.js +59 -2
  21. package/dist/wirings/credential/credential.types.d.ts +14 -0
  22. package/dist/wirings/credential/validate-credential-definitions.js +1 -0
  23. package/dist/wirings/gateway/gateway-runner.js +100 -50
  24. package/dist/wirings/gateway/gateway.types.d.ts +8 -5
  25. package/dist/wirings/http/http.types.d.ts +3 -3
  26. package/dist/wirings/scope/validate-scope-definitions.d.ts +8 -0
  27. package/dist/wirings/scope/validate-scope-definitions.js +16 -1
  28. package/dist/wirings/secret/secret.types.d.ts +14 -0
  29. package/dist/wirings/secret/validate-secret-definitions.js +2 -0
  30. package/dist/wirings/variable/validate-variable-definitions.js +2 -0
  31. package/dist/wirings/variable/variable.types.d.ts +14 -0
  32. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +36 -6
  33. package/dist/wirings/workflow/pikku-workflow-service.d.ts +8 -0
  34. package/dist/wirings/workflow/pikku-workflow-service.js +16 -0
  35. package/dist/wirings/workflow/workflow.types.d.ts +0 -2
  36. package/package.json +2 -1
  37. package/src/index.ts +2 -1
  38. package/src/permissions.test.ts +14 -8
  39. package/src/permissions.ts +14 -36
  40. package/src/scopes.test.ts +37 -1
  41. package/src/scopes.ts +48 -9
  42. package/src/testing/service-tests.ts +49 -0
  43. package/src/types/core.types.ts +23 -0
  44. package/src/wirings/ai-agent/ai-agent-authorization.test.ts +204 -0
  45. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +175 -0
  46. package/src/wirings/ai-agent/ai-agent-prepare.ts +132 -5
  47. package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +207 -0
  48. package/src/wirings/ai-agent/ai-agent-runner.ts +7 -0
  49. package/src/wirings/ai-agent/ai-agent-stream.test.ts +103 -0
  50. package/src/wirings/ai-agent/ai-agent-stream.ts +38 -6
  51. package/src/wirings/ai-agent/ai-agent.types.ts +29 -0
  52. package/src/wirings/ai-agent/index.ts +4 -0
  53. package/src/wirings/ai-agent/voice-input.test.ts +90 -0
  54. package/src/wirings/ai-agent/voice-input.ts +8 -10
  55. package/src/wirings/cli/cli-runner.ts +4 -0
  56. package/src/wirings/cli/command-parser.test.ts +130 -0
  57. package/src/wirings/cli/command-parser.ts +80 -2
  58. package/src/wirings/credential/credential.types.ts +14 -0
  59. package/src/wirings/credential/validate-credential-definitions.ts +1 -0
  60. package/src/wirings/gateway/gateway-authorization.test.ts +444 -0
  61. package/src/wirings/gateway/gateway-runner.ts +114 -68
  62. package/src/wirings/gateway/gateway.types.ts +7 -9
  63. package/src/wirings/http/http.types.ts +6 -4
  64. package/src/wirings/scope/scope.test.ts +25 -0
  65. package/src/wirings/scope/validate-scope-definitions.ts +16 -1
  66. package/src/wirings/secret/secret.types.ts +14 -0
  67. package/src/wirings/secret/validate-secret-definitions.ts +2 -0
  68. package/src/wirings/variable/validate-variable-definitions.ts +2 -0
  69. package/src/wirings/variable/variable.types.ts +14 -0
  70. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +36 -6
  71. package/src/wirings/workflow/pikku-workflow-service.ts +36 -0
  72. package/src/wirings/workflow/workflow-on-error.test.ts +154 -0
  73. package/src/wirings/workflow/workflow.types.ts +0 -2
  74. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,314 @@
1
+ ## 0.12.67
2
+
3
+ ### Patch Changes
4
+
5
+ - ae4f59a: Gate admin capabilities on scopes, and scaffold user management
6
+
7
+ Admin capabilities were gated on `user.role === 'admin'` — a single text column
8
+ meaning "can do everything". Impersonating a user, rebinding a shared
9
+ credential and reading the user directory are distinct capabilities that one
10
+ user can hold independently, so they are now scopes on an `admin` tree:
11
+
12
+ | Gate | Scope |
13
+ | -------------------------------------- | ------------------------ |
14
+ | impersonation | `admin:impersonate` |
15
+ | `credentialOAuth`'s `canLinkSingleton` | `admin:credentials:link` |
16
+ | reading the user directory | `admin:users:list` |
17
+ | creating a user out of band | `admin:users:create` |
18
+ | ban / unban | `admin:users:ban` |
19
+ | delete a user | `admin:users:remove` |
20
+ | revoke a user's sessions | `admin:users:sessions` |
21
+ | set a user's password | `admin:users:password` |
22
+
23
+ Holding the bare `admin` scope satisfies all of them via pikku's existing
24
+ parent-grant rule, so it is a one-for-one replacement for the old role.
25
+
26
+ better-auth's `admin()` plugin is still what implements ban, delete,
27
+ session-revocation and set-password, so it stays. Its `user.role` column is no
28
+ longer something pikku grants: it is _projected_ from the scope store when a
29
+ session is built, and only from the scopes whose capability better-auth's own
30
+ endpoints gate on the caller's role. Someone granted `admin:users:list` can read
31
+ the directory — which goes straight to the auth adapter — without gaining the
32
+ power to ban, and revoking a scope demotes the role on the next sign-in. Scopes
33
+ remain the single source of truth.
34
+
35
+ New `scaffold.userAdmin` in `pikku.config.json` generates the whole set —
36
+ `pikkuAdminListUsers`, `pikkuAdminCreateUser`, `pikkuAdminSetUserBanned`,
37
+ `pikkuAdminRemoveUser`, `pikkuAdminRevokeUserSessions` and
38
+ `pikkuAdminSetUserPassword` — into your project. Listing or banning a user is
39
+ ordinary application behaviour and must not require installing the console.
40
+ Codegen fails with an actionable error if better-auth is wired without
41
+ `admin()`. The console's Users page calls these same functions, showing each
42
+ action only where the caller holds its scope.
43
+
44
+ Every scaffold now emits a directory named for its domain — `scaffold/admin/`,
45
+ `scaffold/rpc/`, `scaffold/agent/`, `scaffold/auth/`, `scaffold/console/`,
46
+ `scaffold/graph/`, `scaffold/realtime/`, `scaffold/scenarios/`,
47
+ `scaffold/webhook/`, `scaffold/workflow/` — holding its wiring file beside a
48
+ `*.schemas.gen.ts` sibling, and every generated payload is a zod schema instead
49
+ of a TypeScript generic. The schemas have to stand alone: the inspector reads a
50
+ zod schema by importing the module that declares it, which it cannot do for a
51
+ wiring file whose relative pikku-types import per-unit deploy codegen rewrites.
52
+
53
+ Resolving a schema by reference rather than by name also fixes the agent HTTP
54
+ surface. `agentCaller` and `agentStreamCaller` take the same payload but had to
55
+ repeat the type literal verbatim in each generic position, because the extractor
56
+ synthesised the schema name from the _function_ name and so recorded an
57
+ `inputSchemaName` with no schema behind it whenever the two shared a named
58
+ alias — every agent call through that alias failed with `MissingSchemaError`.
59
+ One `AgentCall` schema now backs both.
60
+
61
+ Where a payload's shape belongs to `@pikku/core` (`WorkflowRunStatus`,
62
+ `FunctionCoverageReport`, `StubCall[]`) the generated function carries no
63
+ `output` schema and the inspector infers it from the handler's return type;
64
+ re-declaring a core type in zod would be a second definition free to drift.
65
+
66
+ Upgrading rewrites the layout in place: codegen prunes the pre-directory copy of
67
+ each scaffold file before it inspects the source tree, since the old flat file
68
+ still wires the same routes and leaving it behind would wire everything twice.
69
+
70
+ `@pikku/core` gains `hasScopes(required, held)`, the non-throwing counterpart to
71
+ `verifyScopes`, and declares `auth` on `CoreSingletonServices` — the auth
72
+ instance the generated `pikkuServices` wrapper already injected but never typed.
73
+ A scope root declared twice (an addon and its host both contributing the same
74
+ `admin` tree) now flattens to one entry per id instead of emitting it twice.
75
+
76
+ BREAKING: there is no role fallback for the scope-gated capabilities. An app
77
+ that relied on the old default must register a `ScopeService` and grant `admin`
78
+ (or a narrower `admin:*` scope). Every gate fails closed and warns when no
79
+ `ScopeService` is registered. `delegatedAuth`'s `defaultRole`/`mapRole` now
80
+ grant a pikku role through the `ScopeService` instead of writing better-auth's
81
+ `role` column, and the `credentialOAuth` platform user no longer sets `banned`.
82
+
83
+ BREAKING: the console reads its user directory over the scaffolded
84
+ `pikkuAdminListUsers` RPC (gated on `admin:users:list`, backed by better-auth's
85
+ `$context.adapter`) instead of `client.admin.listUsers`, and
86
+ `UsersTableUser`/`UsersTableLabels` no longer carry `role` — there is no role
87
+ column to render. `@pikku/addon-console` no longer ships a `console:listUsers`
88
+ function: user management is not the console's job, so a host that wants the
89
+ Users page must enable `scaffold.userAdmin`.
90
+
91
+ ## 0.12.66
92
+
93
+ ### Patch Changes
94
+
95
+ - 5f19016: Widen the generated agent HTTP surface, and guard attachment downloads against SSRF.
96
+
97
+ `agentCaller` and `agentStreamCaller` declared only `message`, `threadId` and
98
+ `resourceId` (plus `context` on the stream route), so `attachments`, `model`,
99
+ `temperature` — all accepted by `AIAgentInput` — were unreachable over the
100
+ shipped HTTP contract. No deployed app could send an attachment or a per-request
101
+ model override. Both callers now share an `AgentCallerInput` type covering every
102
+ optional field and forward each one to the RPC.
103
+
104
+ Both callers declare that shape **inline** in the generic position rather than
105
+ behind a shared named alias: the schema extractor only reads type literals there
106
+ and synthesises the schema name from the function name. Behind an alias it
107
+ records an `inputSchemaName` with no schema generated for it, and every agent
108
+ HTTP call then fails at runtime with `MissingSchemaError`.
109
+
110
+ Widening that surface makes caller-supplied attachment URLs reachable, which is
111
+ an SSRF vector: the AI SDK downloads attachment URLs **server-side** whenever the
112
+ model cannot consume them natively, using an unguarded `fetch`. A caller could
113
+ point an attachment at the cloud metadata endpoint or another internal host and
114
+ have the response relayed into the model's context. `VercelAIAgentRunner` now
115
+ passes an `experimental_download` implementation backed by `safeFetch` (which
116
+ refuses private/internal hosts and non-HTTP schemes, and re-validates every
117
+ redirect hop) to both `streamText` and `generateText`. URLs the model supports
118
+ natively are passed through untouched, so the provider still fetches those
119
+ itself.
120
+
121
+ The runner takes an optional `allowedAttachmentHosts` allowlist, carried across
122
+ `withApiKey`. `safeFetch` is now exported from `@pikku/core/safe-fetch`.
123
+
124
+ - 78e4778: Stop a failed message persist during an agent stream from killing the process.
125
+
126
+ 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.
127
+
128
+ - 4324652: Scope AI agent thread reads to the calling session.
129
+
130
+ The generated thread-management functions (`getAgentThreads`,
131
+ `getAgentThreadMessages`, `getAgentThreadRuns`, `deleteAgentThread`) keyed purely
132
+ off a caller-supplied `threadId` and treated `resourceId` as an optional filter,
133
+ so omitting it enumerated every tenant's threads.
134
+ - `listThreads` gains an `owners` **authorization constraint** (distinct from the
135
+ `resourceId` filter): an empty array matches nothing, and it is always derived
136
+ from the session, never from input. Implemented across the Kysely, Redis and
137
+ MongoDB agent run services, with LIKE/regex metacharacter escaping so an owner
138
+ id containing `_` or `%` cannot match a foreign owner.
139
+ - The three `threadId`-keyed functions are now guarded by an `isThreadOwner`
140
+ `pikkuPermission` rather than an in-body check. A thread that does not exist is
141
+ denied rather than 404'd, so it is indistinguishable from one owned by someone
142
+ else.
143
+ - New `@pikku/core/ai-agent` helpers: `canAccessThread`, `threadOwnerConstraint`,
144
+ `sessionPrincipals`, `isOwnedByPrincipal`.
145
+
146
+ Services destructured by a wired function are now non-optional inside it.
147
+
148
+ The inspector already aggregated the services used by every wired `func`,
149
+ `permissions` and `middleware` into `RequiredSingletonServices`, but the
150
+ generated function types defaulted their service parameter to the raw `Services`
151
+ — so a service declared `foo?: Foo` still arrived as possibly-undefined, forcing
152
+ `if (!foo) throw new MissingServiceError(...)` guards that could never fire.
153
+ Generated types now expose `WiredSingletonServices` / `WiredServices`
154
+ (`RequiredSingletonServices & Services`) and default the `RequiredServices`
155
+ generic of functions, permissions, middleware, auth and approval-description
156
+ helpers to them. Optionality now means only what it should: "this service may
157
+ not be created, because nothing uses it".
158
+
159
+ - de044f8: Fix the agent tool-list permission filter failing open.
160
+
161
+ `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).
162
+
163
+ `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.
164
+
165
+ - cd1a811: warn instead of silently ignoring unknown long CLI options
166
+
167
+ An unknown long option (`--sektion functions` or `--sektion=functions`) was parsed
168
+ into the options object and then silently dropped by the command's input schema —
169
+ the command ran with the real option at its default and produced plausible-but-wrong
170
+ output. Unknown long options are still accepted (forward compatibility is preserved),
171
+ but the parser now records a warning that the runner prints to stderr, e.g.
172
+ `Warning: Unknown option: --sektion (ignored) Did you mean --section?`.
173
+
174
+ - 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.
175
+ - b501612: Enforce authorization consistently across `pikku*` primitives.
176
+ - `pikkuAIAgent` now enforces `permissions` (previously accepted but never
177
+ checked) and gains `auth` and `scopes`. Scopes are checked before permissions.
178
+ `auth` defaults to `false`, matching `pikkuSessionlessFunc`, since agents are
179
+ typically invoked from an already-authenticated function or from sessionless
180
+ contexts such as crons and queue workers.
181
+ - `pikkuWorkflowFunc` / `pikkuWorkflowComplexFunc` schema config gains `auth`
182
+ and `scopes` alongside `permissions`.
183
+ - `pikkuScenario` no longer accepts `auth`, `scopes`, or `permissions` —
184
+ scenarios drive the app as actors and authorize per step.
185
+ - `wireGateway` no longer accepts `permissions`. A gateway proxies to an agent,
186
+ so access is governed by normal auth plus the target agent's own rules.
187
+ - Removed the dead `permissions` field from `CoreWorkflow`, which was never read.
188
+
189
+ Closed two paths that reached user code without authorization:
190
+ - Gateway handlers were invoked directly, so a handler's own `auth`, `scopes`
191
+ and `permissions` were never evaluated. Webhook, websocket and listener
192
+ gateways now invoke the handler through the function runner. Handlers are
193
+ sessionless by default (inbound gateway traffic is platform-authenticated by
194
+ the adapter, not session-bearing); declare `auth: true` to require a session.
195
+ A gateway's own `auth` field is now honoured too — it was previously ignored.
196
+ Gateway middleware runs before the gate, so `wire.setSession()` in gateway
197
+ middleware — the idiomatic way to map a verified platform sender to a user —
198
+ is visible to the handler's `auth` and `scopes`.
199
+ - Resuming a suspended agent run (`resumeAIAgentSync`, `resumeAIAgent`) checked
200
+ run ownership but never re-ran the agent's own gate, so a scope or permission
201
+ revoked while a run was suspended did not prevent the caller from resuming it
202
+ and approving its pending tool calls. Both now re-run `assertAgentAuthorized`
203
+ before any state is mutated.
204
+
205
+ - eb37b1e: Fix `voiceInput` middleware losing the runner receiver: it grabbed
206
+ `aiAgentRunner.transcribe` as a bare method reference, so calling it left `this`
207
+ undefined and threw `Cannot read properties of undefined (reading 'getModel')`
208
+ on the first audio attachment. It now calls `aiAgentRunner.transcribe(...)`
209
+ directly, preserving the receiver.
210
+
211
+ ## 0.12.65
212
+
213
+ ### Patch Changes
214
+
215
+ - 1a86d3f: Fix a fanout collapsing into a single step, and preserve graph node config.
216
+ - A fanout took its `stepName` from the first step of its body. Node ids _are_
217
+ step names, so the loop and that step got the same id and the step overwrote
218
+ the loop: `await Promise.all(users.map(...))` rendered as one plain call, and
219
+ everything after the loop became unreachable. A fanout is not itself a cached
220
+ step, so it no longer borrows a name.
221
+ - A `workflow.sleep` or `workflow.suspend` inside a fanout body was dropped at
222
+ extraction — `FanoutStepMeta.body` was typed RPC-only. It now admits sleep and
223
+ suspend, and the regenerated body emits them.
224
+ - Regenerating a `pikkuWorkflowGraph` dropped `onError`, `retries` and
225
+ `retryDelay` from every node, and graph-level `notes`. All four are honoured
226
+ at runtime, so the round trip silently changed behaviour.
227
+
228
+ - 1a86d3f: Support multi-step fanout bodies in DSL workflows.
229
+
230
+ A `Promise.all(array.map(...))` (or `for...of`) body containing more than one
231
+ `workflow.do` call previously extracted only a single step: `const`-captured
232
+ steps were skipped entirely by the parallel extractor, so a body like
233
+
234
+ ```ts
235
+ await Promise.all(
236
+ users.map(async (u) => {
237
+ const digestData = await workflow.do('Get pipeline', 'getDigestData', {
238
+ userId: u.id,
239
+ })
240
+ await workflow.do('Send digest', 'sendDigestEmail', { ...digestData })
241
+ })
242
+ )
243
+ ```
244
+
245
+ produced a graph with `getDigestData` missing and `sendDigestEmail` referencing
246
+ an unregistered variable. `FanoutStepMeta.child` is replaced by
247
+ `FanoutStepMeta.body: RpcStepMeta[]`, holding the per-iteration steps inline in
248
+ the same workflow — no sub-workflow boundary. Per-iteration `const` bindings are
249
+ now registered so later steps in the same iteration can reference them, and the
250
+ sequential path no longer hard-errors on bodies with more than one step.
251
+
252
+ - 1a86d3f: Add `onError` compensation to DSL workflows.
253
+
254
+ A DSL workflow had no way to express error handling at all — `try/catch` is not
255
+ an allowed statement, and step options carried only `retries`/`retryDelay`. A
256
+ step can now name a compensation RPC:
257
+
258
+ ```ts
259
+ await workflow.do(
260
+ 'Charge',
261
+ 'chargeCard',
262
+ { id },
263
+ {
264
+ retries: 3,
265
+ onError: 'refundOrder',
266
+ }
267
+ )
268
+ ```
269
+
270
+ Semantics mirror a graph node's `onError` exactly: once the step's retries are
271
+ exhausted the handler is invoked with `{ error: { message } }` and the original
272
+ error is still thrown. This is compensation, not recovery — the workflow fails
273
+ either way. The handler runs as its own durable step, so a replay cannot
274
+ compensate twice, and it does not inherit `onError` itself.
275
+
276
+ The handler is materialised as a real graph node, so it is wired like any other
277
+ RPC and the console draws a dashed red "on error" edge to it rather than the
278
+ route being invisible.
279
+
280
+ - 1a86d3f: Stop silently dropping switch cases and spread returns from workflow graphs.
281
+ - A fall-through case (`case 'a': case 'b': ...`) recorded only the last value.
282
+ A run entering on `'a'` therefore appeared to match no case at all. Empty
283
+ clauses now carry through to the entry they fall into — the next non-empty
284
+ case, otherwise `default`, otherwise the switch exit.
285
+ - `return { ...r, extra: 1 }` produced a return node listing only `extra`, so
286
+ the graph claimed an output shape the workflow does not have, with no
287
+ diagnostic. `return r` produced no return node at all. `ReturnStepMeta` now
288
+ records a `spread` list, and the regenerated code emits it.
289
+
290
+ - 1a86d3f: Stop corrupting values when regenerating a workflow from its graph.
291
+ - A numeric `workflow.sleep('Wait', 5000)` came back as `'5000'`, and a numeric
292
+ `retryDelay` likewise. Durations are `string | number`; only strings are
293
+ quoted now.
294
+ - An assignment to a context variable was stored as an opaque `value`, so
295
+ `count = count + 1` regenerated as `count = 'count + 1'` — an expression
296
+ turned into a string literal. `SetStepMeta` now carries a separate
297
+ `expression` field (mirroring `SwitchCaseMeta`), so a string literal and a
298
+ code expression are no longer indistinguishable in the meta.
299
+ - A `next` that was not a single node id was coerced with a string cast: an
300
+ array became the bogus id `'a,b'` and a branch-key record became
301
+ `'[object Object]'`, severing every downstream node. Arrays, key-based
302
+ routing tables and condition lists now each render in their own shape.
303
+ - A `filter`/`some`/`every` node with no `outputVar` emitted
304
+ `const undefined = ...`, which does not parse.
305
+
306
+ - 1a86d3f: Keep a `workflow.sleep` whose duration is only known at runtime (a loop
307
+ variable, a field off the input). The closure evaluates it, so it is legal DSL;
308
+ its source text is recorded as an `expression` and emitted raw when regenerating
309
+ code, as a set step already does.
310
+ - 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.
311
+
1
312
  ## 0.12.64
2
313
 
3
314
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @module @pikku/core
3
3
  */
4
- export type { CommonWireMeta, CoreConfig, CorePikkuMiddleware, CorePikkuMiddlewareConfig, CorePikkuMiddlewareFactory, CorePikkuMiddlewareGroup, CoreServices, CoreSingletonServices, CoreUserSession, CreateConfig, ServerLifecycle, FunctionMeta, FunctionRuntimeMeta, FunctionServicesMeta, FunctionWiresMeta, FunctionsMeta, FunctionsRuntimeMeta, JSONPrimitive, JSONValue, MakeRequired, MiddlewareMetadata, MiddlewarePriority, PermissionMetadata, PickOptional, PickRequired, PikkuAIMiddlewareHooks, PikkuWire, PikkuRawWire, PikkuWiringTypes, PostgresConfig, RequireAtLeastOne, SecurityAuditIssue, SecurityAuditReport, SecurityAuditSummary, SecurityAuditUpdate, SecuritySeverity, SecurityUpdateLevel, SerializedError, WireServices, } from './types/core.types.js';
4
+ export type { AuthInstance, CommonWireMeta, CoreConfig, CorePikkuMiddleware, CorePikkuMiddlewareConfig, CorePikkuMiddlewareFactory, CorePikkuMiddlewareGroup, CoreServices, CoreSingletonServices, CoreUserSession, CreateConfig, ServerLifecycle, FunctionMeta, FunctionRuntimeMeta, FunctionServicesMeta, FunctionWiresMeta, FunctionsMeta, FunctionsRuntimeMeta, JSONPrimitive, JSONValue, MakeRequired, MiddlewareMetadata, MiddlewarePriority, PermissionMetadata, PickOptional, PickRequired, PikkuAIMiddlewareHooks, PikkuWire, PikkuRawWire, PikkuWiringTypes, PostgresConfig, RequireAtLeastOne, SecurityAuditIssue, SecurityAuditReport, SecurityAuditSummary, SecurityAuditUpdate, SecuritySeverity, SecurityUpdateLevel, SerializedError, WireServices, } from './types/core.types.js';
5
5
  export { pikkuAIMiddleware, pikkuChannelMiddleware, pikkuChannelMiddlewareFactory, pikkuMiddleware, pikkuMiddlewareFactory, } from './types/core.types.js';
6
6
  export type { CorePikkuAuth, CorePikkuAuthConfig, CorePikkuFunction, CorePikkuFunctionConfig, CorePikkuPermission, CorePikkuPermissionConfig, CorePikkuPermissionFactory, CorePikkuApprovalDescription, CorePermissionGroup, ZodLike, } from './function/functions.types.js';
7
7
  export { pikkuAuth, pikkuPermission, pikkuPermissionFactory, pikkuApprovalDescription, } from './function/functions.types.js';
@@ -50,7 +50,7 @@ export type { WireRemoteAddonConfig, RemoteAddonAuth, } from './wirings/rpc/wire
50
50
  export type { PikkuPackageState } from './types/state.types.js';
51
51
  export { runMiddleware, addTagMiddleware, addTagMiddleware as addMiddleware, addGlobalMiddleware, } from './middleware-runner.js';
52
52
  export { addGlobalPermission, checkAuthPermissions } from './permissions.js';
53
- export { verifyScopes } from './scopes.js';
53
+ export { hasScopes, verifyScopes } from './scopes.js';
54
54
  export { isSerializable, stopSingletonServices, pikkuServerLifecycle, } from './utils.js';
55
55
  export { getSingletonServices, getCreateWireServices, setSingletonServices, } from './pikku-state.js';
56
56
  export { clearPikkuRuntimeState } from './test-utils.js';
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ export { wireAddon } from './wirings/rpc/wire-addon.js';
18
18
  export { wireRemoteAddon } from './wirings/rpc/wire-remote-addon.js';
19
19
  export { runMiddleware, addTagMiddleware, addTagMiddleware as addMiddleware, addGlobalMiddleware, } from './middleware-runner.js';
20
20
  export { addGlobalPermission, checkAuthPermissions } from './permissions.js';
21
- export { verifyScopes } from './scopes.js';
21
+ export { hasScopes, verifyScopes } from './scopes.js';
22
22
  export { isSerializable, stopSingletonServices, pikkuServerLifecycle, } from './utils.js';
23
23
  export { getSingletonServices, getCreateWireServices, setSingletonServices, } from './pikku-state.js';
24
24
  export { clearPikkuRuntimeState } from './test-utils.js';
@@ -1,4 +1,4 @@
1
- import type { CoreServices, CoreUserSession, PermissionMetadata, PikkuWire } from './types/core.types.js';
1
+ import type { CoreServices, CoreUserSession, PikkuWire } from './types/core.types.js';
2
2
  import type { CorePermissionGroup, CorePikkuPermission } from './function/functions.types.js';
3
3
  export declare const clearPermissionsCache: () => void;
4
4
  /**
@@ -36,12 +36,14 @@ export declare const addTagPermission: (_tag: string, _permissions: CorePermissi
36
36
  * A passing global requirement never contributes to the function gate, so a
37
37
  * broad global like `signedIn` can't satisfy an admin-only function.
38
38
  */
39
- export declare const runPermissions: ({ funcPermissions, services, wire, data, packageName, }: {
39
+ export declare const runPermissions: ({ funcPermissions, services, wire, data, packageName, label, }: {
40
40
  funcPermissions?: CorePermissionGroup | CorePikkuPermission[];
41
41
  services: CoreServices;
42
42
  wire: PikkuWire<any, never, any, CoreUserSession, never, never, never>;
43
43
  data: any;
44
44
  packageName?: string | null;
45
+ /** What the non-global gate is called in debug logs, e.g. 'function', 'agent'. */
46
+ label?: string;
45
47
  }) => Promise<void>;
46
48
  /**
47
49
  * Checks whether a session passes the auth checks (pikkuAuth only) for a
@@ -49,10 +51,16 @@ export declare const runPermissions: ({ funcPermissions, services, wire, data, p
49
51
  * request data which isn't available at filter time. Global auth requirements
50
52
  * are included so a filtered list honours app-wide auth.
51
53
  *
52
- * @param funcPermissions - The PermissionMetadata[] from function or agent metadata
54
+ * `funcPermissions` is the live {@link CorePermissionGroup} from the function or
55
+ * agent config, not the metadata form: the `pikkuAuth` brand only survives on
56
+ * the actual predicate objects, and the by-name registry those metadata entries
57
+ * would resolve against is never populated. Passing metadata here would silently
58
+ * collect nothing and let every gated tool through.
59
+ *
60
+ * @param funcPermissions - The live permission group from the func/agent config
53
61
  * @param session - The user's session
54
62
  * @param services - Singleton services
55
63
  * @param packageName - Optional package namespace
56
64
  * @returns true if the session passes the auth checks (or no auth checks exist)
57
65
  */
58
- export declare const checkAuthPermissions: (funcPermissions: PermissionMetadata[] | undefined, session: CoreUserSession, services: CoreServices, packageName?: string | null) => Promise<boolean>;
66
+ export declare const checkAuthPermissions: (funcPermissions: CorePermissionGroup | undefined, session: CoreUserSession, services: CoreServices, packageName?: string | null) => Promise<boolean>;
@@ -40,26 +40,6 @@ const verifyPermissions = async (permissions, services, data, wire) => {
40
40
  }
41
41
  return false;
42
42
  };
43
- /**
44
- * Retrieves a registered permission function by its name.
45
- *
46
- * This function looks up permissions that was registered with registerPermission.
47
- * It's used internally by the framework to resolve permission references in metadata.
48
- *
49
- * @param {string} name - The unique name (pikkuFuncId) of the permission function.
50
- * @param {string | null} packageName - Optional package namespace.
51
- * @returns {CorePikkuPermission | undefined} The permission function, or undefined if not found.
52
- *
53
- * @internal
54
- */
55
- const getPermissionByName = (name, packageName = null) => {
56
- const permissionStore = pikkuState(packageName, 'misc', 'permissions');
57
- const permission = permissionStore[name];
58
- if (Array.isArray(permission) && permission.length === 1) {
59
- return permission[0];
60
- }
61
- return undefined;
62
- };
63
43
  const globalPermissionsCache = {};
64
44
  export const clearPermissionsCache = () => {
65
45
  for (const key of Object.keys(globalPermissionsCache)) {
@@ -131,7 +111,7 @@ const asGroup = (entry) => typeof entry === 'function' ? { permission: entry } :
131
111
  * A passing global requirement never contributes to the function gate, so a
132
112
  * broad global like `signedIn` can't satisfy an admin-only function.
133
113
  */
134
- export const runPermissions = async ({ funcPermissions, services, wire, data, packageName = null, }) => {
114
+ export const runPermissions = async ({ funcPermissions, services, wire, data, packageName = null, label = 'function', }) => {
135
115
  const globals = resolveGlobalPermissions(packageName);
136
116
  for (const entry of globals) {
137
117
  if (!(await verifyPermissions(asGroup(entry), services, data, wire))) {
@@ -145,7 +125,7 @@ export const runPermissions = async ({ funcPermissions, services, wire, data, pa
145
125
  : funcPermissions;
146
126
  if (group && Object.keys(group).length > 0) {
147
127
  if (!(await verifyPermissions(group, services, data, wire))) {
148
- services.logger.debug('Permission denied - function permission');
128
+ services.logger.debug(`Permission denied - ${label} permission`);
149
129
  throw new ForbiddenError('Permission denied');
150
130
  }
151
131
  }
@@ -157,7 +137,13 @@ export const runPermissions = async ({ funcPermissions, services, wire, data, pa
157
137
  * request data which isn't available at filter time. Global auth requirements
158
138
  * are included so a filtered list honours app-wide auth.
159
139
  *
160
- * @param funcPermissions - The PermissionMetadata[] from function or agent metadata
140
+ * `funcPermissions` is the live {@link CorePermissionGroup} from the function or
141
+ * agent config, not the metadata form: the `pikkuAuth` brand only survives on
142
+ * the actual predicate objects, and the by-name registry those metadata entries
143
+ * would resolve against is never populated. Passing metadata here would silently
144
+ * collect nothing and let every gated tool through.
145
+ *
146
+ * @param funcPermissions - The live permission group from the func/agent config
161
147
  * @param session - The user's session
162
148
  * @param services - Singleton services
163
149
  * @param packageName - Optional package namespace
@@ -189,15 +175,8 @@ export const checkAuthPermissions = async (funcPermissions, session, services, p
189
175
  for (const entry of resolveGlobalPermissions(packageName)) {
190
176
  collect(entry);
191
177
  }
192
- if (funcPermissions?.length) {
193
- for (const meta of funcPermissions) {
194
- if (meta.type === 'wire') {
195
- const permission = getPermissionByName(meta.name, packageName);
196
- if (permission) {
197
- collect(permission);
198
- }
199
- }
200
- }
178
+ if (funcPermissions) {
179
+ collect(funcPermissions);
201
180
  }
202
181
  // No auth permissions = allowed (only data-dependent permissions exist)
203
182
  if (authPerms.length === 0)
package/dist/scopes.d.ts CHANGED
@@ -1,4 +1,18 @@
1
1
  import type { CoreUserSession } from './types/core.types.js';
2
+ /**
3
+ * Whether a set of held grants satisfies every required scope.
4
+ *
5
+ * The non-throwing counterpart to {@link verifyScopes}, for deciding rather
6
+ * than enforcing — an authorization gate that falls back to another check when
7
+ * it is not satisfied, rather than rejecting the request outright.
8
+ *
9
+ * Fails closed: an absent or empty `held` satisfies nothing. An empty
10
+ * `required` is satisfied by anything.
11
+ *
12
+ * @param required - Scopes to check for. Empty means no gate.
13
+ * @param held - The grants held, e.g. `session.scopes`. May be undefined.
14
+ */
15
+ export declare const hasScopes: (required: readonly string[] | undefined, held: Iterable<string> | undefined) => boolean;
2
16
  /**
3
17
  * Verifies that a session holds every required scope, throwing on the first
4
18
  * one it does not.
package/dist/scopes.js CHANGED
@@ -31,6 +31,42 @@ const satisfyingGrants = (scope) => {
31
31
  * `admin`.
32
32
  */
33
33
  const holds = (held, scope) => satisfyingGrants(scope).some((grant) => held.has(grant));
34
+ /**
35
+ * The first required scope a set of held grants does not satisfy, or `null`
36
+ * when every one is satisfied.
37
+ *
38
+ * Scopes are an AND gate: every entry in `required` must be satisfied. This is
39
+ * deliberately distinct from `permissions`, which OR together — a scope can
40
+ * only ever narrow access, so adding one to a function can never widen it.
41
+ *
42
+ * Fails closed: an absent `held` satisfies nothing.
43
+ */
44
+ const firstUnsatisfied = (required, held) => {
45
+ if (!required || required.length === 0) {
46
+ return null;
47
+ }
48
+ const grants = new Set(held ?? []);
49
+ for (const scope of required) {
50
+ if (!holds(grants, scope)) {
51
+ return scope;
52
+ }
53
+ }
54
+ return null;
55
+ };
56
+ /**
57
+ * Whether a set of held grants satisfies every required scope.
58
+ *
59
+ * The non-throwing counterpart to {@link verifyScopes}, for deciding rather
60
+ * than enforcing — an authorization gate that falls back to another check when
61
+ * it is not satisfied, rather than rejecting the request outright.
62
+ *
63
+ * Fails closed: an absent or empty `held` satisfies nothing. An empty
64
+ * `required` is satisfied by anything.
65
+ *
66
+ * @param required - Scopes to check for. Empty means no gate.
67
+ * @param held - The grants held, e.g. `session.scopes`. May be undefined.
68
+ */
69
+ export const hasScopes = (required, held) => firstUnsatisfied(required, held) === null;
34
70
  /**
35
71
  * Verifies that a session holds every required scope, throwing on the first
36
72
  * one it does not.
@@ -47,13 +83,8 @@ const holds = (held, scope) => satisfyingGrants(scope).some((grant) => held.has(
47
83
  * @throws {MissingScopeError} Naming the first unsatisfied scope.
48
84
  */
49
85
  export const verifyScopes = (required, session) => {
50
- if (!required || required.length === 0) {
51
- return;
52
- }
53
- const held = new Set(session?.scopes ?? []);
54
- for (const scope of required) {
55
- if (!holds(held, scope)) {
56
- throw new MissingScopeError(scope);
57
- }
86
+ const missing = firstUnsatisfied(required, session?.scopes);
87
+ if (missing !== null) {
88
+ throw new MissingScopeError(missing);
58
89
  }
59
90
  };
@@ -742,6 +742,43 @@ export function defineServiceTests(config) {
742
742
  const threads = await agentService.listThreads();
743
743
  assert.ok(Array.isArray(threads));
744
744
  });
745
+ if (services.aiStorageService) {
746
+ const storageFactory = services.aiStorageService;
747
+ // The `owners` constraint is what keeps the generated thread-management
748
+ // functions from leaking across tenants: a caller may only list threads
749
+ // owned by one of their session principals, matching the
750
+ // `principal:sub-partition` composition resolveOwnerResourceId writes.
751
+ test('listThreads scopes to the given owners, including sub-partitions', async () => {
752
+ const storage = await storageFactory();
753
+ await storage.createThread('owner-alice');
754
+ await storage.createThread('owner-alice:project-1');
755
+ await storage.createThread('owner-bob:secret');
756
+ const threads = await agentService.listThreads({
757
+ owners: ['owner-alice'],
758
+ });
759
+ const ids = threads.map((t) => t.resourceId);
760
+ assert.ok(ids.includes('owner-alice'));
761
+ assert.ok(ids.includes('owner-alice:project-1'));
762
+ assert.ok(!ids.some((id) => id.startsWith('owner-bob')), "another owner's threads must not be listed");
763
+ });
764
+ test('listThreads with an owner does not match a lookalike prefix', async () => {
765
+ const storage = await storageFactory();
766
+ await storage.createThread('owner-al');
767
+ await storage.createThread('owner-alice-evil:p');
768
+ const threads = await agentService.listThreads({
769
+ owners: ['owner-al'],
770
+ });
771
+ const ids = threads.map((t) => t.resourceId);
772
+ assert.ok(ids.includes('owner-al'));
773
+ assert.ok(!ids.includes('owner-alice-evil:p'));
774
+ });
775
+ test('listThreads with an empty owners list returns nothing', async () => {
776
+ const storage = await storageFactory();
777
+ await storage.createThread('owner-empty-check');
778
+ const threads = await agentService.listThreads({ owners: [] });
779
+ assert.deepEqual(threads, []);
780
+ });
781
+ }
745
782
  test('getThread returns null for missing', async () => {
746
783
  const thread = await agentService.getThread('missing-thread');
747
784
  assert.equal(thread, null);
@@ -209,6 +209,21 @@ export interface CoreUserSession {
209
209
  */
210
210
  scopes?: string[];
211
211
  }
212
+ /**
213
+ * The shape pikku needs from whatever auth library a project wires: something
214
+ * that can answer an HTTP request and expose its own endpoints as callable
215
+ * methods. Kept structural so core stays independent of any one auth package —
216
+ * `@pikku/better-auth`'s `BetterAuthInstance` is this type.
217
+ */
218
+ export interface AuthInstance {
219
+ handler: (request: Request) => Promise<Response>;
220
+ api: Record<string, any>;
221
+ /**
222
+ * The auth library's resolved context. Optional because a hand-built instance
223
+ * may omit it.
224
+ */
225
+ $context?: Promise<any>;
226
+ }
212
227
  /**
213
228
  * Interface for core singleton services provided by Pikku.
214
229
  */
@@ -280,6 +295,13 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
280
295
  * better-auth's `mapSession`), never by the function runner.
281
296
  */
282
297
  scopeService?: ScopeService;
298
+ /**
299
+ * The project's resolved auth instance, built once by the factory an auth
300
+ * package registers (e.g. `pikkuBetterAuth`) and injected by the generated
301
+ * `pikkuServices` wrapper — which is why service factories are forbidden from
302
+ * returning it themselves. Absent when the project wires no auth.
303
+ */
304
+ auth?: () => Promise<AuthInstance>;
283
305
  }
284
306
  /**
285
307
  * Represents different forms of wire within Pikku and the outside world.