@voltro/protocol 0.64.0 → 0.65.0
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 +67 -0
- package/dist/index.d.ts +75 -6
- package/dist/index.js +483 -476
- package/dist/rest.d.ts +24 -2
- package/dist/rest.js +1 -1
- package/dist/{scopes-D9t3tYc7.js → scopes-C86BDyNJ.js} +33 -19
- package/dist/session.js +6 -6
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,73 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.65.0] — 2026-09-04
|
|
43
|
+
|
|
44
|
+
### ⚠ BREAKING
|
|
45
|
+
|
|
46
|
+
- **@voltro/plugin-ai-flows** — `@voltro/plugin-ai-flows` declares `internal: true` on its `flow.run` workflow, so it no longer becomes a start rpc. Every start is server-side and always was: the plugin's own guarded procedures, the cadence tick, and `chainTo`'s `workflows.child`. There is no meaningful client start — the payload carries `runId`, the id of a row the caller must already have created.
|
|
47
|
+
|
|
48
|
+
Leaving the decision off was not a lax default. An app installs the plugin by re-exporting the descriptor from its own `flow.run.workflow.tsx`, which makes it one of the app's DISCOVERED workflows — exactly the set the boot gate judges — so under `security.defaultDeny` (on by default) the app's boot was REFUSED, naming a workflow the app had no way to decide about. A first-party descriptor must satisfy the framework's own gate, and a test now holds every workflow any first-party package ships to that.
|
|
49
|
+
|
|
50
|
+
Migration: start a flow through the plugin's guarded procedures. Server code is unaffected.
|
|
51
|
+
|
|
52
|
+
**`voltro update` carries you across this** — codemod `0.65.0/01_ai-flows-run-is-internal`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.65.0).
|
|
53
|
+
- **@voltro/runtime, @voltro/protocol, @voltro/cli, @voltro/voltro** — `EffectStore` gains `one`, `maybeOne` and `first` — the same single-row reads `ctx.store` offers, taking a builder or a descriptor.
|
|
54
|
+
|
|
55
|
+
Their absence made two `voltro doctor` rules mutually unsatisfiable: `row-not-found` prescribes `.maybeOne(builder)`, `store-effect-lift` prescribes `yield* EffectStore`, and the Effect surface had no `.maybeOne`. Following both produced `Property 'maybeOne' does not exist on type 'EffectStoreOps'`. Nothing was hard about it — the object the service closes over has always been the full fluent store, and every call site was already casting to say so; only the declared type was narrower. `transactional` inherits the wider surface.
|
|
56
|
+
|
|
57
|
+
`one` fails with `NoRowFound` on its own channel rather than collapsing into `StoreOperationFailed`, for the reason the catch boundary already gives about `TenantRowNotFound`: stringifying a deliberate, catchable refusal is how it stops being one. There is no `onMissing` on the Effect surface — `Effect.catchTag('NoRowFound', …)` says the same thing and keeps the channel exact.
|
|
58
|
+
|
|
59
|
+
`NoRowFound` moves to `@voltro/protocol` (re-exported from `@voltro/runtime`, so nothing that imported it moves). The agent guide already said it was declarable in a descriptor's `error:` union, and a descriptor is browser-loaded while `@voltro/runtime` is on the browser-safety guard's server-only list — so following that sentence produced a refused boot. The package it sat in was the only thing making the guidance false.
|
|
60
|
+
|
|
61
|
+
**`voltro update` carries you across this** — codemod `0.65.0/02_effect-store-and-guard-unions`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.65.0).
|
|
62
|
+
|
|
63
|
+
### Added
|
|
64
|
+
|
|
65
|
+
- **@voltro/protocol, @voltro/runtime, @voltro/voltro** — `guards: [{ authenticated: true }]` — the access decision that says "the caller must have an identity", and nothing narrower.
|
|
66
|
+
|
|
67
|
+
It was the third case the vocabulary could not express, and every substitute was wrong in a different way. `{ scope: … }` refuses every real user in an app that issues no raw scopes, which is what authority resolved per team in the executor looks like. A relationship guard has no tuple to match on the first call, when the resource does not exist yet. And `openAccess` is untrue wherever the auth fallback is a deliberate anonymous subject — the honest reason would read "whoever can open the socket may spend our model budget". Apps were writing a resource policy whose entire content was "you are logged in".
|
|
68
|
+
|
|
69
|
+
`checkGuards` already distinguished this internally: its return type has always been `ScopeError | Unauthenticated | null`. The guard makes the distinction sayable. It ENFORCES, so it satisfies `security.defaultDeny` — unlike `openAccess`, which declares that nothing is checked — and it composes: every entry in `guards:` must pass.
|
|
70
|
+
|
|
71
|
+
Both enforcement loops carry it, the synchronous one and the Effect one that runs whenever a policy or resource-scoped guard is present. The approval serialiser stores it too: a variant it dropped would be an identity requirement that quietly stopped applying the moment a decision went through an approval.
|
|
72
|
+
|
|
73
|
+
The golden churn here is one member added to `AnyGuardSpec` / `AnyCheckSpec`. Nothing already written changes meaning; only a switch that is EXHAUSTIVE over the union needs an arm, and that case is carried by the manual codemod `0.65.0/02_effect-store-and-guard-unions` alongside the store-layer change it ships with.
|
|
74
|
+
|
|
75
|
+
### Changed
|
|
76
|
+
|
|
77
|
+
- **@voltro/cli** — Four `voltro doctor` corrections, each where the rule's premise was narrower than the code it read:
|
|
78
|
+
|
|
79
|
+
- `n-plus-one` follows a dependent chain through ORDINARY bindings. It compared the next read's text against the names earlier reads were DIRECTLY assigned to, so `const row = rows[0]` between two reads made the second look independent — and a finite chain whose next key comes out of the previous row was reported as an N+1 you are writing, which `Effect.all` cannot fix. - `input/uncapped-array` reads the `input:` expression alone, plus the schemas it names. It scanned the whole file, so a descriptor whose input holds no array and whose output returns `items: Schema.Array(Item)` was reported — and a `maxItems` on a RESPONSE bounds nothing a request can ask for. - `mutation/target-op-mismatch` treats `target: [...]` as the set of allowed ops per table. Comparing each declared target ALONE asked "does the executor do anything but insert", answered yes because delete was also declared, and reported a complete declaration. - `bulk-write-in-loop` recommends only APIs that exist. It named `insertManyIgnore` and `upsertMany`, neither of which the store has; `insertIgnore` and `upsert` resolve a conflict per row and have no set-based form, so the advice is now to keep the loop inside one transaction and bound the input.
|
|
80
|
+
- **@voltro/cli** — `voltro update`'s codemod preview splits the count in the summary line: `6 in range for 0.63.0 → 0.64.0 — 6 print written steps for you to apply`. The per-entry markers were already there and the legend explained them, but both sit BELOW the count, so "6 codemods" formed the expectation of six automatic rewrites before the reader reached either. Six manual codemods is six pieces of hand work, and that is the number somebody schedules an afternoon around.
|
|
81
|
+
|
|
82
|
+
### Fixed
|
|
83
|
+
|
|
84
|
+
- **@voltro/cli** — `open-reason-unverified` had three defects that between them let it be silenced by rewording while missing the case it exists for:
|
|
85
|
+
|
|
86
|
+
- The reason literal was matched with one quote class that forbade every quote character INSIDE the string, so a reason naming its guard in backticks did not parse and the rule returned without opening the executor at all. - `can` is a framework guard name AND an ordinary English word, so any reason containing "anyone can call it" named a guard the executor did not have. A lowercase word now counts only when written as code — `can()` or in backticks. - The executor test was token PRESENCE, which the import line alone satisfies. It requires a CALL, the same shape the sibling rule already used.
|
|
87
|
+
|
|
88
|
+
`input/uncapped-array` covers `*.query.ts` as well. A read does not write, which is what the old wording was about, but that does not make it bounded: a hundred thousand ids in one `getByIds` is the caller choosing the size of the query, and the body limit bounds bytes rather than entries.
|
|
89
|
+
|
|
90
|
+
`store-effect-lift` names both preconditions its promise depends on — that a typed error only reaches the caller once the descriptor DECLARES it, and that the class comes from `@voltro/protocol`. Naming a benefit and neither precondition is how a rule earns a large diff with nothing observable at the end of it.
|
|
91
|
+
- **@voltro/runtime** — An `.encrypted()` column written through `upsert` is bound to the row that RECEIVES it. On the conflict path that row is the existing one, whose id is not the id the caller offered, so the ciphertext went to disk authenticated against an id that row does not have — and AES-GCM then refused it: `FieldDecryptionError … Unsupported state or unable to authenticate data`.
|
|
92
|
+
|
|
93
|
+
The damage was not the failed call. The row keeps the mis-bound value, so every later read of it throws too, and the write reports success on the way in. The conflict is now resolved before the encrypt (`upsertDestinationId`, shared by both codec wrappers), which costs one extra read and only for a call that actually writes a secret.
|
|
94
|
+
|
|
95
|
+
It bites wherever the row carries an id at encrypt time — always on the memory store, and on every SQL dialect when the id is the caller's rather than the database's, since `EXCLUDED.<col>` / `VALUES(<col>)` / `MERGE` all carry the offered bytes onto the existing row.
|
|
96
|
+
|
|
97
|
+
`updateMany` through the BOOT store now refuses an `.encrypted()` column instead of writing a binding nothing can authenticate: a set-based write has no row, and the request-scoped store is the one that expands it per row.
|
|
98
|
+
- **@voltro/cli** — `.framework/dist/server/` — the SSR bundle and the precompiled app config — is marked `{"type":"module"}` like the api and start bundles already were, so a production `voltro start` no longer opens with `MODULE_TYPELESS_PACKAGE_JSON` for an artefact the app did not write. All three directories go through one `writeEsmPackageMarker`, and a guard now asks every module that runs a bundler whether it marks what it wrote.
|
|
99
|
+
- **@voltro/cli** — `voltro new workflow` scaffolds a descriptor that declares `internal: true`, with the three options in a comment above it. The template emitted no access decision at all, so the file it wrote refused the app's own boot under `security.defaultDeny` — a scaffold whose first act is to break the build.
|
|
100
|
+
- **@voltro/database, @voltro/runtime, @voltro/protocol, @voltro/voltro** — `StoreOperationFailed.cause` no longer carries the driver's sentence or a stack trace. It was `String(error)` on a `FiberFailure`, which is the message AND the stack — so an absolute server path reached every caller that declared the error, and every subscriber on a stream carrying it.
|
|
101
|
+
|
|
102
|
+
The words were the worse half. `@voltro/database`'s own header already states why a driver message may not be forwarded: postgres attaches `Failing row contains (…)` — the entire row, `.sensitive()` columns included — to a not-null and a check violation, mysql echoes the duplicate value, mssql the truncated one. That rule was written for `ConstraintViolation` and the error beside it broke it.
|
|
103
|
+
|
|
104
|
+
`wireSafeCause` builds the field now: the constraint KIND and its constraint/column NAME, or the driver's code, and nothing that was ever a value. The full detail is logged server-side instead, so diagnosis is not traded away for the fix.
|
|
105
|
+
- **@voltro/cli** — The inspect action behind the dashboard's workflow "resume" refuses a run that is not suspended, instead of reporting a resume that did not happen. The engine resumes only a run carrying a terminal `Suspended` reply; the action called it, ignored the answer, flipped the row to `running` and recorded a `run-resumed` event regardless. For a run already stuck in `running` — the state somebody presses that button in — every visible sign said it had worked, including a fabricated entry in the timeline of the run being investigated.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
42
109
|
## [0.64.0] — 2026-09-04
|
|
43
110
|
|
|
44
111
|
### ⚠ BREAKING
|
package/dist/index.d.ts
CHANGED
|
@@ -89,15 +89,15 @@ export declare interface AnyApprovalPolicy {
|
|
|
89
89
|
readonly reason?: string;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
/** A guard entry is a scope check, a relationship check,
|
|
93
|
-
* no-check-needed decision. */
|
|
94
|
-
export declare type AnyCheckSpec = GuardCheckSpec | PolicyCheckSpec | OpenAccessSpec;
|
|
92
|
+
/** A guard entry is a scope check, a relationship check, an identity check, or
|
|
93
|
+
* a declared no-check-needed decision. */
|
|
94
|
+
export declare type AnyCheckSpec = GuardCheckSpec | PolicyCheckSpec | AuthenticatedCheckSpec | OpenAccessSpec;
|
|
95
95
|
|
|
96
96
|
/** Any declared event, with the generics erased — for registries and audits. */
|
|
97
97
|
export declare type AnyEventDescriptor = EventDescriptor<string, Schema.Schema.Any, Schema.Schema.Any>;
|
|
98
98
|
|
|
99
|
-
/** A guard is
|
|
100
|
-
export declare type AnyGuardSpec<Input = unknown> = GuardSpec<Input> | PolicyGuardSpec<Input
|
|
99
|
+
/** A guard is a scope check, a relationship check, or an identity check. */
|
|
100
|
+
export declare type AnyGuardSpec<Input = unknown> = GuardSpec<Input> | PolicyGuardSpec<Input> | AuthenticatedGuardSpec;
|
|
101
101
|
|
|
102
102
|
export declare const APIKEY_ISSUE_ORG_SCOPE = "apikeys:issue:org";
|
|
103
103
|
|
|
@@ -460,6 +460,36 @@ export declare interface AuthCallbackRouter {
|
|
|
460
460
|
readonly post: (path: string, handler: AuthRouteHandler) => void;
|
|
461
461
|
}
|
|
462
462
|
|
|
463
|
+
/**
|
|
464
|
+
* The runtime-erased form of `AuthenticatedGuardSpec` — "somebody must be
|
|
465
|
+
* there, and nothing narrower". See that type for why the vocabulary needed it.
|
|
466
|
+
*/
|
|
467
|
+
export declare interface AuthenticatedCheckSpec {
|
|
468
|
+
readonly authenticated: true;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* "The caller must have an identity" — and nothing narrower.
|
|
473
|
+
*
|
|
474
|
+
* The third case the vocabulary was missing, and the gap was not theoretical.
|
|
475
|
+
* An app whose authority lives per-team in its executors holds NO raw scopes on
|
|
476
|
+
* the identity, so `{ scope }` refuses every real user; a relationship guard
|
|
477
|
+
* cannot help when the resource does not exist yet (the first message of a new
|
|
478
|
+
* thread); and `openAccess` would be a lie whenever the auth fallback is a
|
|
479
|
+
* deliberate anonymous subject. The one thing left to decide — is anybody
|
|
480
|
+
* there — had no spelling, so apps wrote a resource policy whose entire content
|
|
481
|
+
* was "you are logged in".
|
|
482
|
+
*
|
|
483
|
+
* `checkGuards` has always distinguished this internally: its return type is
|
|
484
|
+
* `ScopeError | Unauthenticated | null`. This makes the distinction sayable.
|
|
485
|
+
*
|
|
486
|
+
* It is a real DECISION, so it satisfies `security.defaultDeny` — unlike
|
|
487
|
+
* `openAccess`, it also enforces something.
|
|
488
|
+
*/
|
|
489
|
+
export declare interface AuthenticatedGuardSpec {
|
|
490
|
+
readonly authenticated: true;
|
|
491
|
+
}
|
|
492
|
+
|
|
463
493
|
/**
|
|
464
494
|
* The framework's per-request auth middleware. Attach to an RpcGroup with
|
|
465
495
|
* `group.middleware(AuthMiddleware)` to require every call to flow through
|
|
@@ -2488,6 +2518,12 @@ export declare interface InsertTarget<Input = unknown, Row = unknown, Item = Rec
|
|
|
2488
2518
|
readonly shapeItem?: ((input: Input, optimisticId: string) => Item) | undefined;
|
|
2489
2519
|
}
|
|
2490
2520
|
|
|
2521
|
+
/** Runtime narrowing for the identity variant. */
|
|
2522
|
+
export declare const isAuthenticatedCheck: (g: AnyCheckSpec) => g is AuthenticatedCheckSpec;
|
|
2523
|
+
|
|
2524
|
+
/** Narrow a guard entry to the identity variant. */
|
|
2525
|
+
export declare const isAuthenticatedGuard: <I>(g: AnyGuardSpec<I>) => g is AuthenticatedGuardSpec;
|
|
2526
|
+
|
|
2491
2527
|
export declare const isEventDescriptor: (value: unknown) => value is AnyEventDescriptor;
|
|
2492
2528
|
|
|
2493
2529
|
/** True when every row in the set carries an `id`. The dispatcher uses
|
|
@@ -2683,6 +2719,34 @@ export declare const normalizeDescriptor: (descriptor: ProcedureDescriptor) => C
|
|
|
2683
2719
|
*/
|
|
2684
2720
|
export declare const normalizeSource: (source: ReactivitySource | ReadonlyArray<ReactivitySource> | undefined) => string | ReadonlyArray<string> | undefined;
|
|
2685
2721
|
|
|
2722
|
+
/**
|
|
2723
|
+
* `.one()` matched a number of rows other than exactly one.
|
|
2724
|
+
*
|
|
2725
|
+
* `found` distinguishes the two failure modes without a second error type:
|
|
2726
|
+
* `0` is "the row you required is missing", `2` is "your filter is not as
|
|
2727
|
+
* unique as you assumed" (the terminal probes with LIMIT 2, so `2` means
|
|
2728
|
+
* "at least two" — it never counts the whole set just to report a number).
|
|
2729
|
+
*
|
|
2730
|
+
* It lives HERE, not in `@voltro/runtime`, and the reason is a promise the
|
|
2731
|
+
* framework was already making. The agent guide says "`NoRowFound` is
|
|
2732
|
+
* declarable in the descriptor's `error:` union" — and a descriptor is loaded
|
|
2733
|
+
* by the BROWSER, while `@voltro/runtime` is on the browser-safety guard's
|
|
2734
|
+
* server-only list. So following that sentence produced a refused boot. The
|
|
2735
|
+
* class is browser-safe by construction (a `Schema.TaggedError` over two
|
|
2736
|
+
* scalars), so the package it sat in was the only thing making the guidance
|
|
2737
|
+
* false. `@voltro/runtime` re-exports it, so nothing that imported it moves.
|
|
2738
|
+
*/
|
|
2739
|
+
export declare class NoRowFound extends NoRowFound_base {
|
|
2740
|
+
get message(): string;
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
declare const NoRowFound_base: Schema.TaggedErrorClass<NoRowFound, "NoRowFound", {
|
|
2744
|
+
readonly _tag: Schema.tag<"NoRowFound">;
|
|
2745
|
+
} & {
|
|
2746
|
+
table: typeof Schema.String;
|
|
2747
|
+
found: typeof Schema.Number;
|
|
2748
|
+
}>;
|
|
2749
|
+
|
|
2686
2750
|
/**
|
|
2687
2751
|
* What a plugin contributes to the OTel layer via `contributeObservability`.
|
|
2688
2752
|
* The OTel types are intentionally `unknown` so this browser-safe protocol
|
|
@@ -4632,7 +4696,12 @@ declare const StoreOperationFailed_base: Schema.TaggedErrorClass<StoreOperationF
|
|
|
4632
4696
|
operation: typeof Schema.String;
|
|
4633
4697
|
/** The table the op targeted. */
|
|
4634
4698
|
table: typeof Schema.String;
|
|
4635
|
-
/**
|
|
4699
|
+
/**
|
|
4700
|
+
* A BOUNDED description: the constraint kind and its constraint/column
|
|
4701
|
+
* NAME, or the driver's code. Never the driver's sentence and never a
|
|
4702
|
+
* stack — `wireSafeCause` builds it, and the reasoning is there. The full
|
|
4703
|
+
* detail is logged server-side instead.
|
|
4704
|
+
*/
|
|
4636
4705
|
cause: typeof Schema.String;
|
|
4637
4706
|
}>;
|
|
4638
4707
|
|