@pikku/redis 0.12.12 → 0.12.14

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 CHANGED
@@ -1,3 +1,409 @@
1
+ ## 0.12.14
2
+
3
+ ### Patch Changes
4
+
5
+ - 6a307f0: Fix four latent correctness bugs in the function, RPC and error runtimes, and
6
+ remove dead code from the workflow service surface.
7
+
8
+ `WorkflowService.getNodesWithoutSteps` is gone. It was declared on the abstract
9
+ service and implemented by all five storage backends, and nothing ever called
10
+ it — hence the non-core packages in this changeset, which only lose that method.
11
+
12
+ **An RPC could execute twice.** `RPCService.invoke`, its addon path, and
13
+ `rpcWithWire` each wrapped the _execution_ of a resolved function in a `try`
14
+ whose `catch` treated `RPCNotFoundError` as "not found locally" and re-dispatched
15
+ the call through `deploymentService`. A nested `rpc.invoke` to a missing name,
16
+ raised from inside an already-running function, therefore re-ran that function on
17
+ a remote instance after its local side effects had already committed. Resolution
18
+ is now separated from execution on all three paths, so only a genuinely
19
+ unresolvable name reaches the fallback. What escapes to callers is unchanged.
20
+
21
+ **`addonNamespace` leaked between sibling calls.** The function runner's
22
+ middleware path restored `rpc`, `functionId`, `audit` and `addonNamespace` after
23
+ an invocation; the non-middleware path restored the first three but not the
24
+ fourth. A call into an addon function with no middleware left the addon's
25
+ namespace on the wire, so subsequent sibling calls resolved the wrong
26
+ per-instance singletons and `credentialOverrides`.
27
+
28
+ **Errors registered on a subclass never resolved.** `misc.errors` was typed
29
+ `Map<PikkuError, ErrorDetails>` — instances — while `addError` stores
30
+ constructors, a mismatch hidden by its `error: any` parameter. The instance
31
+ lookup in `getErrorResponse` was consequently dead, and lookup fell through to a
32
+ scan comparing `constructor.name`, so a subclass of a registered error got no
33
+ status mapping at all. The map is now typed `Map<PikkuErrorConstructor, …>` and
34
+ lookup walks the prototype chain first. Name matching is retained, deliberately,
35
+ as the fallback that keeps error mapping working when two copies of
36
+ `@pikku/core` are installed.
37
+
38
+ **`createWeakUID` collided across instances.** The prefix was
39
+ `Date.now().toString(36)` evaluated at module load, so any two instances loading
40
+ the module in the same millisecond emitted identical `channelId` and `requestId`
41
+ sequences — reproducibly, not just in principle. It is now seeded lazily from
42
+ `crypto.randomUUID()`.
43
+
44
+ Also: `pikkuState` keys its global map with `Symbol.for` rather than `Symbol`, so
45
+ two copies of the package share registrations instead of silently getting
46
+ disjoint state; and the local channel upgrade path no longer keys its middleware
47
+ cache on the raw request path, which grew the cache without bound while the
48
+ cached value never varied by path.
49
+
50
+ - afef587: Close eleven security weaknesses found in a review of `@pikku/core`. Most are
51
+ breaking, and two invalidate data or credentials already in the wild — read the
52
+ migration notes before upgrading.
53
+
54
+ **Breaking: AI agent thread ownership now fails closed.** Reading, listing,
55
+ resuming or approving an existing thread or run requires a resolved session
56
+ principal (`userId`, or `orgId` for `sessionScope: 'org'`), regardless of the
57
+ agent's `auth` setting. Previously a request without a session had no ownership
58
+ model at all: the caller-supplied `resourceId` was accepted as the ownership
59
+ key, so any caller could read or resume another party's thread by naming its
60
+ `resourceId`. Worse, `threadOwnerConstraint` returned `undefined` for a
61
+ sessionless caller, and `undefined` means _no filter_ rather than _no rows_ —
62
+ so `getAgentThreads` returned every thread in the deployment. It now returns
63
+ `string[]`, empty for a sessionless caller, which every storage backend already
64
+ treats as matching nothing. Sessionless agents still run one-shot conversations,
65
+ each with a fresh unguessable owner; what they lose is cross-request continuity.
66
+ Wire a session to restore it.
67
+
68
+ **Breaking: stored secrets and credentials must be re-entered.** `deriveKey` ran
69
+ a single unsalted round of SHA-256 over the passphrase and used the digest
70
+ directly as the AES-GCM key — roughly one hash per brute-force guess, with one
71
+ rainbow table working against every deployment. It is now PBKDF2-HMAC-SHA256 at
72
+ 600,000 iterations over a random salt. There is no compatibility path: every
73
+ value held by the kysely, mongodb and redis secret services and the kysely
74
+ credential service becomes undecryptable. They fail loud, naming the key and
75
+ `key_version`, so the app hard-fails on first secret read until each is re-set.
76
+
77
+ The KEK salt is scoped to the key version and stored alongside it, rather than
78
+ per secret, so a bulk read costs one derivation instead of N — `getSecrets` over
79
+ 50 secrets went from ~2.3s to ~48ms, and rotation from ~4.6s to ~94ms. This adds
80
+ a salt table (kysely), hash field (redis) or collection (mongodb), created
81
+ automatically on first use.
82
+
83
+ **Breaking: `PIKKU_REMOTE_SECRET` must be at least 32 characters.** The
84
+ remote-RPC session envelope moved from PBKDF2 to HKDF, which expands
85
+ high-entropy key material rather than stretching a low-entropy passphrase. That
86
+ took a remote hop from ~269ms to ~0.4ms — PBKDF2 was running twice per request —
87
+ but HKDF supplies no brute-force resistance, so the secret must carry the
88
+ entropy itself. A shorter secret now throws `WeakKeyMaterialError` at both ends.
89
+ Generate one with `openssl rand -base64 32` and roll it out to every service in
90
+ the mesh together: existing bearer tokens are format-incompatible, so a partial
91
+ rollout produces 401s until every instance is updated. The Cloudflare, Lambda
92
+ and Azure deployment services each hand-rolled a copy of `buildRemoteHeaders`
93
+ and now call the shared one, which is what keeps the two sides in step.
94
+
95
+ **Breaking: previously signed content URLs stop verifying.** `LocalContent`
96
+ signed only `{signedAt, expiresAt, notBefore}`, so a signature proved when a URL
97
+ was issued but never what it was issued for — any valid token was a skeleton
98
+ key, and swapping the pathname from a public thumbnail to a private document
99
+ still verified. The signature now binds the request path. Separately, the
100
+ verifier returned "valid" when no JWT service was wired, which is how
101
+ `pikku serve` ran: a forged `?signedAt=0&expiresAt=99999999999999` was accepted.
102
+ It now rejects with 403, `LocalContent` requires a `JWTService`, and
103
+ `pikku serve`/`pikku dev` mint an ephemeral per-process signing key so local
104
+ development works without shipping a fail-open path. In-flight signed URLs must
105
+ be re-issued.
106
+
107
+ **Request body size limits now apply to every adapter.** The `maxBodySize` cap
108
+ existed only in `PikkuFetchHTTPRequest`. The real hole was uWebSockets, which
109
+ drove `res.onData` itself and concatenated every chunk with no bound and nothing
110
+ downstream able to intervene; it now drops chunks past the limit and replies 413
111
+ before routing. Fastify delegates to its native `bodyLimit` (set only when
112
+ `maxBodySize` is configured, so fastify's stricter 1 MB default is never
113
+ loosened), and `PikkuExpressServer` feeds the limit into its body parsers. Two
114
+ paths can only reject rather than prevent, and are documented as such:
115
+ `express-middleware` mounted on your own app receives an already-parsed body, so
116
+ that deployment must bound its own parser; Next server actions bottom out at
117
+ `experimental.serverActions.bodySizeLimit`.
118
+
119
+ **Breaking: the console addon's privileged functions are gated by default.**
120
+ `wireAddon` gains a `scopes` option that applies to every function in the
121
+ addon's namespace, and the console scaffold now generates
122
+ `wireAddon({ name: 'console', package: '@pikku/addon-console', scopes: ['admin'] })`.
123
+ Previously the console's entire privileged surface — around 54 functions
124
+ including `credentialGet`, which returns a resolved OAuth token for an arbitrary
125
+ `userId`, `updateFunctionBody`, and `installAddon`, which shells out to a
126
+ package install — was protected only by an optional host-registered
127
+ `addGlobalPermission`. `resolveGlobalPermissions` returns `[]` when none is
128
+ registered and permission checking then no-ops, so an app that never registered
129
+ one served those functions to anyone, and with the template's default
130
+ `scaffold.rpc: "no-auth"` that meant unauthenticated. All of them now return 403
131
+ `MissingScopeError` without an `admin`, `admin:*` or `*` scope. **Regenerating
132
+ is required** — an app holding an old `console.gen.ts` stays open.
133
+ `installAddon` and `installOpenapiAddon` additionally declare their own
134
+ `auth: true, scopes: ['admin']`, and `getAgentThreads` now scopes its listing to
135
+ the session's own threads unless the caller holds admin.
136
+
137
+ Addon scopes are enforced in `runPikkuFunc` rather than at the RPC boundary,
138
+ because a wiring can reference an addon function directly — the inspector
139
+ records the addon's `packageName` on HTTP, channel, schedule, queue, CLI,
140
+ trigger, gateway and MCP wirings — and those paths never call `resolveNamespace`.
141
+ Enforcing at the RPC seam would have covered only the `namespace:function` form
142
+ while reading as complete.
143
+
144
+ **Breaking: `wireAddon`'s `auth` and `tags` now apply on direct wirings too.**
145
+ Both were read only by `resolveAddonFunction`, so they had exactly the hole
146
+ scopes had: `wireAddon({ name: 'console', package: '@pikku/addon-console', auth:
147
+ true, tags: ['admin'] })` gated `rpc('console:credentialGet')` and gated nothing
148
+ at all on an HTTP route wired straight to `credentialGet`. A consumer who
149
+ reached for the documented way to lock an addon down got a control that was
150
+ silently inert on every wiring except one. Both now resolve in `runPikkuFunc`.
151
+ `auth` merges as an OR — `auth: false` from an addon is ignored on a direct
152
+ wiring, because an addon may require a session the wiring did not but must never
153
+ waive one it did. Addon tags resolve to concrete middleware against the **root**
154
+ tag groups before the call rather than being folded into the function's
155
+ inherited middleware: `addTagMiddleware('admin', …)` is written by the consuming
156
+ app and registers under the root package, whereas `combineMiddleware` would look
157
+ the tag up under the addon's own `packageName`, where it does not exist.
158
+
159
+ One consequence worth naming: an addon that wires `auth: true` and also runs its
160
+ own sessionless internal work — a scheduled task or queue worker inside the
161
+ addon calling a sibling function — is now gated where it previously was not,
162
+ because a bare `rpc('fn')` from inside the addon reaches `runPikkuFunc` with the
163
+ addon's `packageName` like any other call. This reverses a decision that
164
+ deliberately scoped the gate to the namespaced boundary; that reasoning held
165
+ only while the boundary was real, and a direct wiring can enter an addon without
166
+ crossing it, so "already inside" is not something the runtime can infer. Such an
167
+ addon should carry authorization on the function via
168
+ `pikkuFunc({ permissions })`, which has always been enforced on every path. A
169
+ follow-up will add execution provenance so an intra-addon call can be
170
+ distinguished from an external one and skip the addon-level check; that needs a
171
+ marker no caller outside the process can set, which is its own design problem
172
+ and does not belong in a security fix.
173
+
174
+ **Codegen now warns when an exposed function has no gate (PKU574).** The
175
+ generated `POST /rpc/:rpcName` dispatcher forwards to `rpc.exposed`, which
176
+ refuses anything without `expose: true` — but nothing checked whether the
177
+ target was gated, because a dispatcher cannot know what it dispatches to. The
178
+ console shipped ~54 privileged functions through that gap and the toolchain was
179
+ silent. The inspector now reports every function that is exposed, sessionless,
180
+ and carries no `auth`, `scopes` or `permissions` of its own and none from a
181
+ governing `wireAddon`. It is a `warn`, not a critical: `expose: true` on an
182
+ ungated sessionless function is correct for a genuinely public endpoint, so it
183
+ blocks a build only under `--fail-on-warn`.
184
+
185
+ Two pieces of metadata were missing for this to be answerable statically, and
186
+ both are now recorded. A `pikkuSessionlessFunc`'s own `auth: true` was read at
187
+ runtime but never written to function meta, so a self-gated function was
188
+ indistinguishable from an ungated one — `sessionless` carries the baseline, and
189
+ `auth` now carries the tightening. And `wireAddon`'s `scopes`, `auth` and `tags`
190
+ were not parsed at all: the inspector recorded the addon's `rpcEndpoint` and its
191
+ secret, variable and credential overrides, and dropped every one of its gates.
192
+ An addon whose gates are not statically knowable is treated as gated, because a
193
+ false positive on a correctly-secured addon costs more than the one case it
194
+ would catch.
195
+
196
+ **Breaking: an application's global permissions now apply inside addons.**
197
+ `resolveGlobalPermissions` read only the bucket matching the function's own
198
+ package, but the generated `addGlobalPermission` wrapper takes no package
199
+ argument and always registers under the root. An app-wide rule like "every
200
+ request needs a signed-in user" therefore stopped at the addon boundary, and the
201
+ bucket an addon's functions did read was one no host could write to — which is
202
+ why the console addon's recommended `addGlobalPermission([isAdmin],
203
+ '@pikku/addon-console')` was never a gate anybody could actually install. A
204
+ function now resolves the root bucket and its own package's, root first.
205
+ Unioning is safe in a way nothing else here would be: globals AND, so adding the
206
+ root ones can only tighten. Package buckets stay one-way — a package's globals
207
+ never reach root functions, or an installed addon could gate the whole
208
+ application. Apps with both a root global and addon-provided functions will see
209
+ those functions gated where they previously were not.
210
+
211
+ **Codegen now records whether each HTTP route requires a session, and warns
212
+ about inert addon tags (PKU575).** Four separate things can demand a session —
213
+ the function's `sessionless`, its own `auth`, the route's (or its group's)
214
+ `auth`, and the addon it belongs to — and answering "which routes are open?"
215
+ meant joining all four by hand and knowing which wins. Each route's meta now
216
+ carries the resolved `requiresSession` alongside the route's own `auth`. Scopes
217
+ count as requiring one, since they are matched against the session's and fail
218
+ closed. Anything not statically knowable resolves to `true`, matching PKU574:
219
+ a route that looks stricter than it is costs less than one that looks open and
220
+ isn't. Separately, `wireAddon({ tags: ['admin'] })` reads like a gate and is
221
+ applied like one right up until no `addTagMiddleware('admin', …)` exists, at
222
+ which point it resolves to an empty list and gates nothing; that now warns.
223
+ Only addon tags are reported — a tag on a function is as likely to be
224
+ organizational, and warning about those would bury the case that matters.
225
+
226
+ **Object-shorthand permissions were missing from meta.** The inspector visited
227
+ `ts.PropertyAssignment` but not `ts.ShorthandPropertyAssignment`, so
228
+ `permissions: { canAdminOrg }` — enforced identically to the longhand form at
229
+ runtime, since `verifyPermissions` has a non-array branch — was recorded as _no
230
+ permissions at all_. That is the most dangerous direction for meta to be wrong
231
+ in: an audit reading it sees an open door where one is shut. It cost this review
232
+ a false IDOR report across ~35 billing and org functions before the source
233
+ contradicted the metadata.
234
+
235
+ **Functions that authorize in their own body can say so.** A webhook receiver
236
+ verifying a signature, or a handler redeeming a signed invite, is genuinely
237
+ closed while carrying no session, scope or permission — indistinguishable in
238
+ meta from one nobody remembered to gate, and so warned about forever by PKU574.
239
+ `selfAuthenticated: true` on the function config records the claim and silences
240
+ the warning for that function. It is declarative only: nothing at runtime reads
241
+ it and it grants nothing. Detection was rejected deliberately — inferring it
242
+ from the body means a function that _looks_ like it checks something silences
243
+ the warning while checking nothing, and a warning that is usually wrong stops
244
+ being read.
245
+
246
+ **Breaking: `scaffold.<feature>` is now `boolean | { auth?, path? }`, and `true`
247
+ means authenticated.** The old `'auth' | 'no-auth' | false` read like a
248
+ starter-file preference while being a live authorization decision, set three
249
+ directories from the functions it governed — the shape the console incident
250
+ took. A surface now becomes public only by writing `{ auth: false }`, so
251
+ omitting a field can never open anything: the failure mode of a forgotten flag
252
+ is a locked door. `{ path }` additionally overrides where the file is
253
+ generated, which previously could only be set for all features at once via
254
+ `pikkuDir`.
255
+
256
+ The legacy strings are **refused, not coerced**. `resolveScaffoldFeature` throws
257
+ naming the key and its replacement (`"rpc": "no-auth"` → `"rpc": { "auth":
258
+ false }`), and it does so at config load, not downstream. An earlier design used
259
+ a bare `string` for the output path, under which `"no-auth"` would have parsed
260
+ as a file named `no-auth` and every unmigrated config would have silently
261
+ produced nonsense; the object form makes any string invalid, so the failure is
262
+ loud.
263
+
264
+ The collapse is deliberately not uniform in effect. For `rpc` the flag was a
265
+ blanket "no anonymous RPC in this app" set on a dispatcher that cannot know what
266
+ it dispatches to; for `userAdmin` it was redundant, since the generated
267
+ functions are already `pikkuFunc` with `scopes: ['admin:users:list']`. But for
268
+ `agent`, `workflow`, `events` and `scenarios` it is the only gate — those
269
+ generate real endpoints the app never authors — so `true` keeps them
270
+ authenticated rather than opening them as a side effect of a config cleanup.
271
+ `webhook` and `remoteRpc` have no auth dimension at all (`serialize-remote-rpc.ts`
272
+ hardcodes `auth: false`), so an `{ auth }` on them is ignored. The three
273
+ configs in this repo are migrated preserving their current behaviour exactly.
274
+
275
+ **Queue job identities are signed.** A job carried the producer's `pikkuUserId`
276
+ as a plain string and the worker resolved a session from it with no
277
+ verification, so write access to the queue backend was act-as-any-user. The
278
+ identity is now `pq1.<claim>.<hmac>`, HMAC-SHA256 over the claim and the
279
+ canonicalized job payload, keyed by HKDF expansion of a new
280
+ `PIKKU_QUEUE_IDENTITY_SECRET`. Producers opt in by wrapping their queue service
281
+ with `SignedQueueService`. This fails safe rather than closed: with no secret
282
+ configured the identity is dropped and jobs still process, warning once per
283
+ process, so no existing deployment breaks on upgrade — it simply loses queue
284
+ identity until the secret is set. The payload rather than the job id is bound
285
+ because SQS, Cloudflare Queues, Azure and the in-memory service all mint ids
286
+ after `add` returns.
287
+
288
+ **Workflow inline state is read from the run record.** `isInline` consulted a
289
+ process-local `Map`, while `WorkflowRun.inline` is durable. Any instance that
290
+ did not start a run disagreed with the record, so one instance could dispatch a
291
+ queued job for a workflow another was already executing in-process. It is now
292
+ async and resolves through the durable identity, cached only when a context
293
+ already exists so a passive reader allocates nothing. The same `Map` also leaked:
294
+ `nextStepKey` fabricated replay state on every step, and `releaseContext`
295
+ refused to free anything carrying it, so runs whose steps executed outside a
296
+ `beginReplay` bracket — the step-worker queue path — stranded their context and
297
+ step state for the process lifetime. Contexts are now released by an explicit
298
+ execution counter. Step ordinals reset per execution rather than accumulating
299
+ across step-worker invocations in one process, which makes step naming
300
+ independent of how work was distributed.
301
+
302
+ **Secret reads fail loud in every store.** `MongoDBSecretService.getSecrets`
303
+ skipped rows that failed to decrypt, and the redis equivalent dropped every
304
+ rejection via `Promise.allSettled`, including the "No KEK available for
305
+ key_version N" configuration error. Both now throw, naming the key and its key
306
+ version, matching the kysely behaviour. This matters most alongside the KEK
307
+ change above: without it, an upgrade surfaces as a partial secrets map and an
308
+ opaque downstream failure instead of an error naming the secret to re-enter.
309
+
310
+ **A second middleware registration for a pattern no longer erases the first.**
311
+ `addHTTPMiddleware`, `addTagMiddleware` and `addChannelMiddleware` groups are
312
+ keyed by pattern or tag and held one source file each, so a second file's call
313
+ overwrote the first's. Codegen emits its imports from what is stored, so the
314
+ losing file was never imported and its middleware never registered — the
315
+ runtime composes repeated registrations for a pattern happily, and only codegen
316
+ dropped one. Adding an unrelated `addHTTPMiddleware('*', …)` to an app was
317
+ enough to silently unregister the generated better-auth session bridge, which
318
+ fails open and gives no sign until a request arrives without a session. A group
319
+ now carries every registration made for it, and all of them are imported.
320
+
321
+ - Updated dependencies [6a307f0]
322
+ - Updated dependencies [afef587]
323
+ - Updated dependencies [8075f6a]
324
+ - @pikku/core@0.12.74
325
+
326
+ ## 0.12.13
327
+
328
+ ### Patch Changes
329
+
330
+ - 8a2c993: Make the workflow service cheaper to run, and fix two ways it lost state.
331
+
332
+ The SQL workflow tables had no indexes at all, so every step read, every
333
+ history walk and every orchestrator tick was a sequential scan; five indexes
334
+ now cover the columns the engine actually queries by. A replay used to ask for
335
+ each step's row individually — O(N) reads per replay, O(N^2) over a run — and
336
+ now takes one read of the run's steps and serves the walk from it. A step
337
+ transition wrote the step row and its history row as two separate statements,
338
+ so a crash between them left a step saying `succeeded` whose history still said
339
+ `running`; both halves are now one transaction, and the history row is found by
340
+ attempt number rather than by sorting on `created_at`, which two attempts can
341
+ share. Resolving a dynamic workflow read and parsed every AI-generated workflow
342
+ in the deployment to `.find()` one by name; it is a point lookup now.
343
+
344
+ Waiting on a run no longer polls at a fixed interval. `pollIntervalMs` became a
345
+ ceiling rather than a cadence: polling starts at 10ms and widens towards it, so
346
+ a workflow that finishes in milliseconds is no longer held for a full second,
347
+ and a long-running one is not read at full rate for its whole life.
348
+
349
+ Two backend-specific defects: Redis kept a run's state as one JSON blob and
350
+ read-modified-wrote it, so parallel branches setting different variables
351
+ overwrote each other — state is a field per variable now, with the old blob
352
+ still read underneath so runs in flight keep what they had. Mongo's
353
+ `setStepScheduled` never wrote history, leaving a queued step reading as never
354
+ dispatched.
355
+
356
+ Also: dispatch no longer JSON round-trips every step payload before handing it
357
+ to a queue that serialises it anyway — the in-process dev queue, which is the
358
+ only one that was relying on it, does it itself now.
359
+
360
+ Two more defects. A transition whose step had no live attempt wrote its status
361
+ to the step row and silently nothing to history — the exact divergence the
362
+ transaction exists to prevent — and now repairs the step and writes the
363
+ missing row. And resolving a dynamic workflow was non-deterministic on all
364
+ three backends: a name can hold several active versions, and none of them
365
+ ordered the candidates, so which one ran could change between two calls
366
+ reading identical data. The newest version wins, with the graph hash breaking
367
+ a tie.
368
+
369
+ The two attempt columns and the five indexes are declared in the workflow
370
+ schema, so a fresh database gets them at boot. An existing one gets them from
371
+ a migration — `pikku db generate` writes the declaration down — rather than
372
+ from DDL issued at boot.
373
+
374
+ - a261006: **Breaking:** removed dynamic workflows — runtime-defined workflow graphs stored in the database and resolved by name instead of by codegen.
375
+
376
+ The feature was already half-gone. Its authoring surface (`createAgentWorkflow`, `saveAgentWorkflow`, `listAgentWorkflows`, `executeAgentWorkflow`, and the AI-agent instruction builder) was deleted in April 2026 along with its entire e2e suite, and nothing has written a dynamic workflow since. What remained could not execute one either: `executeAgentWorkflow` gated on `pikkuState('workflows', 'meta')`, which only codegen ever populates, so a graph that existed solely in the database was never findable. The two backend families had also drifted onto different `source` sentinels (`'ai-agent'` vs `'dynamic-workflow'`), and the two Redis implementations disagreed on key escaping — so at least one of them matched nothing. Rather than keep shipping plumbing for a path no caller could complete, it is removed until it can be reintroduced deliberately.
377
+
378
+ Removed:
379
+ - `getAIGeneratedWorkflows` from `WorkflowService` and `WorkflowRunService`, and from every backend (in-memory, Redis, MongoDB, Kysely, and the Cloudflare Durable Object service and client — the last two were already a `return []` stub and a rejection).
380
+ - The database-lookup fallbacks in `startWorkflow` and `runWorkflowJob` that resolved a workflow name against stored graphs when static meta had no match.
381
+ - `'dynamic-workflow'` from the `WorkflowRuntimeMeta['source']` union.
382
+ - `validateWorkflowWiring` and `computeEntryNodeIds` from `@pikku/core/workflow`. These validated AI-authored graphs and had no callers in core; the inspector keeps its own private entry-node computation for static graph wiring, which is unaffected.
383
+ - The `workflow-created` AI stream event and its AG-UI `pikku:workflow-created` custom event. Its only emitter went with the April deletion, so it could never fire.
384
+ - The console's `console:getAIWorkflows` RPC, the `useAIWorkflows` hook, the "Dynamic" workflow filter and badge, and the trigger-schema scraper that derived an input form from a stored graph's `$ref` bindings.
385
+
386
+ Kept, because static graph workflows depend on them and this is not a change to versioning:
387
+ - `upsertWorkflowVersion`, `getWorkflowVersion`, `updateWorkflowVersionStatus`, and the `workflowVersions` storage in every backend. These back version-mismatch replay: when a deployed graph's hash changes, in-flight runs continue against the exact graph they started on. No schema migration is needed — the table, its columns, and its `(workflowName, graphHash)` upsert key are unchanged.
388
+ - `generateMermaidDiagram`, which renders any workflow graph and is not specific to dynamic ones.
389
+
390
+ Static `pikkuWorkflowGraph` and DSL workflows are entirely unaffected: they resolve from codegen'd meta, which was always the only path that worked.
391
+
392
+ To revive this post-MVP, the deleted authoring code is recoverable in full — its prompt engineering (a compact tool table upfront, full schemas with flattened dotted output paths returned only after a validation failure) is worth reading before rewriting:
393
+
394
+ ```
395
+ git show f52f3308b^:packages/core/src/wirings/ai-agent/agent-dynamic-workflow.ts
396
+ git show f52f3308b^:packages/core/src/wirings/workflow/graph/graph-validation.ts
397
+ git show f52f3308b --stat # the April removal, incl. the three e2e feature files
398
+ ```
399
+
400
+ Note that reviving it needs more than restoring those files: the queued-step path (`executeWorkflowStep`), `onError` compensation, and sub-workflow resolution all read static meta only and would need a fallback for a graph that exists solely in the database.
401
+
402
+ - Updated dependencies [8a2c993]
403
+ - Updated dependencies [a261006]
404
+ - Updated dependencies [09973b9]
405
+ - @pikku/core@0.12.71
406
+
1
407
  ## 0.12.12
2
408
 
3
409
  ### Patch Changes
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 - present Yasser Fadl and Pikku contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,201 +1,40 @@
1
- # @pikku-workflows/redis
1
+ # @pikku/redis
2
2
 
3
- Redis-based implementation of WorkflowStateService for Pikku Workflows.
3
+ Redis-backed implementations of the Pikku service interfaces — workflow state,
4
+ agent runs, secrets, sessions, and the channel and event-hub stores.
4
5
 
5
- ## Features
6
+ Uses `ioredis`. Distributed run locking is handled with `SET NX` plus a TTL, so
7
+ concurrent workers can safely share a run.
6
8
 
7
- - **RedisWorkflowService**: Redis implementation of WorkflowStateService for persistent workflow execution state
8
- - **Fast in-memory storage**: Leverage Redis for high-performance workflow state management
9
- - **Configurable key prefix**: Use custom key prefixes for namespace isolation (default: 'workflows')
10
- - **Distributed locking**: Uses Redis SET NX with TTL for concurrent run safety
11
- - **Type-safe**: Full TypeScript support
12
-
13
- ## Installation
9
+ ## Install
14
10
 
15
11
  ```bash
16
- npm install @pikku-workflows/redis ioredis
17
- # or
18
- yarn add @pikku-workflows/redis ioredis
12
+ npm install @pikku/redis ioredis
19
13
  ```
20
14
 
21
15
  ## Usage
22
16
 
23
- ### Basic Setup (Remote Mode)
24
-
25
17
  ```typescript
26
18
  import Redis from 'ioredis'
27
- import { RedisWorkflowService } from '@pikku-workflows/redis'
28
- import { BullQueueService } from '@pikku/queue-bullmq'
29
-
30
- // Create Redis connection
31
- const redis = new Redis({
32
- host: 'localhost',
33
- port: 6379,
34
- })
35
-
36
- // Create queue service for remote mode
37
- const queueService = new BullQueueService('redis://localhost:6379')
19
+ import { RedisWorkflowService } from '@pikku/redis'
38
20
 
39
- // Create workflow state service
40
21
  const workflowService = new RedisWorkflowService(
41
- redis,
22
+ new Redis('redis://localhost:6379'),
42
23
  queueService,
43
24
  'workflows'
44
25
  )
45
26
 
46
- // Initialize (verifies connection)
47
- await workflowService.init()
48
- ```
49
-
50
- ### Inline Mode Setup (Testing)
51
-
52
- For testing, pass `undefined` as the queue service to enable inline mode:
53
-
54
- ```typescript
55
- // Create workflow state service without queue = inline mode
56
- const workflowService = new RedisWorkflowService(
57
- redis,
58
- undefined, // No queue service = inline mode
59
- 'workflows'
60
- )
61
-
62
- await workflowService.init()
63
- ```
64
-
65
- ### Custom Key Prefix
66
-
67
- ```typescript
68
- // Use a custom key prefix
69
- const workflowService = new RedisWorkflowService(
70
- redis,
71
- queueService,
72
- 'myapp_workflows'
73
- )
74
- await workflowService.init()
75
- ```
76
-
77
- ### With Connection String
78
-
79
- ```typescript
80
- // Create service with Redis connection string
81
- const workflowService = new RedisWorkflowService(
82
- 'redis://localhost:6379',
83
- queueService
84
- )
85
27
  await workflowService.init()
86
28
  ```
87
29
 
88
- ### With Existing Connection
89
-
90
- ```typescript
91
- // Share connection with other services
92
- const redis = new Redis('redis://localhost:6379')
93
- const workflowService = new RedisWorkflowService(redis, queueService)
94
- // Connection is shared, won't be closed by workflowService.close()
95
- ```
96
-
97
- ### With Config (Owned Connection)
98
-
99
- ```typescript
100
- // Let service create its own connection
101
- const workflowService = new RedisWorkflowService(
102
- { host, port, password },
103
- queueService
104
- )
105
- await workflowService.init()
106
- // Later...
107
- await workflowService.close() // Closes the connection
108
- ```
109
-
110
- ## Redis Data Structure
111
-
112
- The service uses the following Redis data structures:
113
-
114
- ### Workflow Runs
115
-
116
- - **Hash**: `{keyPrefix}:run:{runId}` - Stores workflow run data
117
- - `id`: Run ID
118
- - `workflow`: Workflow name
119
- - `status`: Current status ('running', 'completed', 'failed', 'cancelled')
120
- - `input`: JSON string of input data
121
- - `output`: JSON string of output data (if completed)
122
- - `error`: JSON string of error (if failed)
123
- - `createdAt`: Timestamp
124
- - `updatedAt`: Timestamp
125
-
126
- ### Workflow Steps
127
-
128
- - **Hash**: `{keyPrefix}:step:{runId}:{stepName}` - Stores step execution data
129
- - `stepId`: Unique step ID
130
- - `status`: Step status ('pending', 'scheduled', 'succeeded', 'failed')
131
- - `rpcName`: RPC function name (if RPC step)
132
- - `data`: JSON string of step input data
133
- - `result`: JSON string of result (if succeeded)
134
- - `error`: JSON string of error (if failed)
135
- - `retries`: Number of retry attempts allowed
136
- - `retryDelay`: Delay between retries
137
- - `attemptCount`: Current attempt number
138
- - `createdAt`: Timestamp
139
- - `updatedAt`: Timestamp
140
-
141
- ### Workflow Step History
142
-
143
- - **List**: `{keyPrefix}:history:{runId}` - Stores all step attempts in chronological order
144
- - Each entry contains complete step state for that attempt
145
-
146
- ### Locking
147
-
148
- - **String**: `{keyPrefix}:lock:{runId}` - Distributed lock with TTL (30 seconds)
149
-
150
- ## API
151
-
152
- ### RedisWorkflowService
153
-
154
- Extends `WorkflowStateService` from `@pikku/core/workflow`.
155
-
156
- #### Constructor
157
-
158
- ```typescript
159
- new RedisWorkflowService(
160
- connectionOrConfig: Redis | RedisOptions | string,
161
- queue?: any,
162
- keyPrefix?: string
163
- )
164
- ```
165
-
166
- - `connectionOrConfig`: ioredis Redis instance, RedisOptions config, or connection string
167
- - `queue`: Optional queue service for remote workflow execution
168
- - `keyPrefix`: Redis key prefix (default: 'workflows')
169
-
170
- #### Methods
171
-
172
- - `init()`: Initialize the service (verifies Redis connection)
173
- - `createRun(workflowName, input)`: Create a new workflow run
174
- - `getRun(id)`: Get workflow run by ID
175
- - `getRunHistory(runId)`: Get all step attempts in chronological order
176
- - `updateRunStatus(id, status, output?, error?)`: Update run status
177
- - `insertStepState(runId, stepName, rpcName, data, stepOptions?)`: Insert initial step state
178
- - `getStepState(runId, stepName)`: Get step state with attempt count
179
- - `setStepScheduled(stepId)`: Mark step as scheduled
180
- - `setStepRunning(stepId)`: Mark step as running
181
- - `setStepResult(stepId, result)`: Store step result and mark as succeeded
182
- - `setStepError(stepId, error)`: Store step error and mark as failed
183
- - `createRetryAttempt(failedStepId)`: Create a new retry attempt for a failed step
184
- - `withRunLock(id, fn)`: Execute function with distributed lock
185
- - `close()`: Close Redis connection (if owned)
186
-
187
- ## Documentation
188
-
189
- For complete workflow documentation, see [pikku.dev/docs/workflows](https://pikku.dev/docs/workflows)
190
-
191
- ## Locking Behavior
30
+ The first argument also accepts a connection string or an `ioredis` options
31
+ object, in which case the service owns the connection and `close()` will end
32
+ it. Omit `queueService` to run workflows inline, which is useful in tests. The
33
+ third argument is the key prefix, defaulting to `workflows`.
192
34
 
193
- The `withRunLock` method uses Redis SET NX with a 30-second TTL for distributed locking:
35
+ ## Docs
194
36
 
195
- - Retries up to 10 times with 100ms delay between attempts
196
- - Automatically releases lock after function execution
197
- - Uses Lua script to ensure only the lock owner can release it
198
- - Lock automatically expires after 30 seconds if not released
37
+ https://pikku.dev/docs
199
38
 
200
39
  ## License
201
40
 
@@ -13,8 +13,12 @@ export declare class RedisSecretService implements SecretService {
13
13
  private keyVersion;
14
14
  private previousKey?;
15
15
  private keyPrefix;
16
+ private kekSalts;
17
+ private keks;
16
18
  constructor(connectionOrConfig: Redis | RedisOptions | string, config: RedisSecretServiceConfig);
17
19
  private secretKey;
20
+ private saltKey;
21
+ private getKEKSalt;
18
22
  private getKEK;
19
23
  getSecret<T = string>(key: string): Promise<T>;
20
24
  hasSecret(key: string): Promise<boolean>;