@voltro/plugin-atlassian 0.51.0 → 0.52.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 +127 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,133 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.52.0] — 2026-08-25
|
|
43
|
+
|
|
44
|
+
### ⚠ BREAKING
|
|
45
|
+
|
|
46
|
+
- **@voltro/protocol, @voltro/runtime, @voltro/cli, @voltro/web, @voltro/plugin-storage, @voltro/plugin-ratelimit** — The HTTP surface is complete — seven gaps closed, each proven against the real listener.
|
|
47
|
+
|
|
48
|
+
**Binary byte streams.** REST handlers return `bytes(stream, { contentType, contentLength?, contentDisposition? })` (lazy thunk form defers opening the source); plugin routes return `byteStream`. Piped, never buffered (a 256 MiB export streams with bounded heap), never compressed. Idempotency × stream is DECIDED: `streaming: true` on an idempotent method with an idempotency binding is a mount error (a stream cannot cache a replayable body — retries would 409 until the TTL); an undeclared stream releases its claim at return. Storage's full-object download rides the generic path now.
|
|
49
|
+
|
|
50
|
+
**Negotiated compression + conditional GET.** brotli/gzip negotiated with a compressible-type allowlist, 1 KiB threshold, `Vary: Accept-Encoding` always on compressible types — on the api's buffered responses AND `voltro start`'s HTML (`http.compression.{enabled,minBytes}`). The BREACH position is structural: `POST /rpc` responses are NEVER compressed. ISR keeps ONE uncompressed entry and compresses per hit. `voltro start` answers `If-None-Match` with 304 (weak md5 tags over the uncompressed body); REST routes can declare `etag: true` (GET, weak SHA-1 content tag).
|
|
51
|
+
|
|
52
|
+
**Raw WebSocket gateways.** `defineWebSocket({ path, auth, onConnection })` in a `*.ws.ts` file — for FOREIGN protocols (a Yjs provider, a device fleet); app realtime stays the subscription protocol. `auth` is required with no default: `'subject'` runs the same chain as rpc/SSR BEFORE the upgrade (401 while it is still http) and binds the connection to the credential's expiry (close code 4001); `'public'` is a written-down decision. Every gateway path joins the upgrade origin guard (cross-site WebSocket hijacking → 403). Teardown at construction; plain GET → 426; duplicate paths refuse the boot.
|
|
53
|
+
|
|
54
|
+
**Body caps everywhere.** The 8 MiB cap used to guard only `/rpc`; plugin routes read uncapped and webhooks read uncapped AND UTF-8-round-tripped (corrupting binary bodies — fixed, proven byte-for-byte). `http.maxBodyBytes` (env `VOLTRO_MAX_BODY_BYTES`), per-route overrides on `defineRestRoute` and webhook handlers; a shared path takes its group's widest override. 413 for both request shapes.
|
|
55
|
+
|
|
56
|
+
**Full method unions.** PATCH/HEAD/OPTIONS are first-class on plugin and REST routes; HEAD is admitted wherever GET is (RFC 9110) with the transport dropping the body.
|
|
57
|
+
|
|
58
|
+
**BREAKING — the interceptor chain is fail-closed.** A throwing `onHttpRequest` interceptor is a 500 + a log line now; it used to be swallowed, which let a crashed security gate silently stop guarding. The manual codemod tells interceptor authors where the decision lives: propagate (a gate) or catch-and-degrade-loudly (protection with a dependency) — plugin-ratelimit's httpShield now does the latter, so a Redis outage cannot become a self-inflicted API outage. No app-authored call sites change shape, hence `apiSurface: compatible` — the break is the error POLARITY of one plugin-author hook, carried by the manual note.
|
|
59
|
+
|
|
60
|
+
**Middleware response headers + CSP nonce.** `middleware.ts` can return `responseHeaders` (applied on every render response shape, both boot paths; prerendered static files are the documented proxy-side limit) and `cspNonce` — the framework stamps every script tag of that render (React's own included) while the policy header stays the middleware's. `isr` + `cspNonce` is refused loudly: a cached nonce is a lie the browser enforces.
|
|
61
|
+
|
|
62
|
+
Deliberate limits: no multipart parser on REST/webhook routes (the storage upload routes are the sanctioned file path); static-file response headers belong at the proxy.
|
|
63
|
+
- **@voltro/plugin-row-history, @voltro/cli** — `@voltro/plugin-versioning` is renamed to `@voltro/plugin-row-history` — the name now says what it does.
|
|
64
|
+
|
|
65
|
+
The old name collided head-on with API versioning (versioned REST routes, `/v1` → `/v2`, sunset flow), which `defineRestRoute` now supports as a first-class `version:` field. What this plugin does is row history + time travel (`rowHistory` / `rowAsOf` / `restoreAsOf` / `diffVersions` over `_voltro_row_history`); every comparison table that filed it under API versioning was reading the name, not the feature.
|
|
66
|
+
|
|
67
|
+
Renamed with it: the factory (`versioningPlugin` → `rowHistoryPlugin`), the options type (`VersioningPluginOptions` → `RowHistoryPluginOptions`), and the default instance alias (`versioning` → `row-history` — the inspect endpoint path and boot-log lines; an explicit `alias:` you passed is untouched). The table name (`_voltro_row_history`) and `VOLTRO_ROW_HISTORY_TTL_HOURS` already carried the new name: no data movement, no env change, no migration.
|
|
68
|
+
|
|
69
|
+
The codemod rewrites imports, the factory call sites and the options-type references, and prints the one step it cannot do — swapping the dependency in package.json.
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
|
|
73
|
+
- **@voltro/cli** — `voltro doctor` now reports every declared `@voltro/*` dependency with no import site — the residue a migration off a framework package leaves in `package.json`, where it keeps getting installed, walked by `voltro update`, and read as evidence the package is in use, its breaking-change notes included.
|
|
74
|
+
|
|
75
|
+
Scoped to `@voltro/*` deliberately (third-party packages have too many legitimate no-import shapes), and three states are distinguished and printed: exempt by name with a reason (`@voltro/cli` is the binary, `@voltro/devtools` is mounted by `voltro dev`, `@voltro/sql-*` drivers are loaded from config); not-measurable-yet for `@voltro/client`/`@voltro/web` on a tree where codegen has never run (a missing measurement, not a dead dependency); and unimported — advisory, never fatal. A mention in a comment or an error string does not count as an import, and a commented-out import counts least of all: it is the artefact the rule exists to see past. Also in `voltro doctor --json` under `unimportedDeps`, `null` when no `package.json` could be read.
|
|
76
|
+
- **@voltro/web, @voltro/cli** — Islands now save real bytes — every `interactive: 'islands'` page gets its OWN browser entry.
|
|
77
|
+
|
|
78
|
+
Measured on the reference fixture (pinned in `bundle-budget.json`, 2026-08-25): an islands page's first load is **59.6 KB gz** against **181.9 KB gz** for a fully hydrated page — react + the island runtime + that page's islands, no router, no Effect runtime, no subscription cache. The bundle-budget gate pins a hard <70 KB bound AND the ratio (<50 % of a full page), so a regression that re-couples the entries fails loudly.
|
|
79
|
+
|
|
80
|
+
- **`@voltro/web/islands`** is the new react-only subpath — `island()` and the hydration runtime import only react + react-dom/client. Importing the `@voltro/web` BARREL (or `@voltro/i18n`) anywhere in an island's import graph is now a BUILD error naming file and specifier: an island hydrates provider-less, router hooks and `useT()` throw there anyway, and the barrel would pull the Effect runtime into the slim entry. - **The build finds each page's islands** through its relative import graph (transitively, through components in between) and emits one shell + entry per islands page. `interactive` must be a source LITERAL to select the slim entry — a computed value ships the full entry as before, and the build says so. - **All three paths**: `voltro build` (SSG renders into the per-page shell, main-shell stylesheets folded in), `voltro start` (ssr/isr islands routes serve their shell), `voltro dev` (same mechanism on demand — violations fire in dev, not first in CI). Fixed on the way: a STATIC islands page fell through dev's render gate to the SPA fallback and loaded the full app entry — dev now server-renders it like production. - **Framework islands**: an island importing `@voltro/client` (`useSubscription`, …) is detected — that page's entry boots the rpc client and wraps each island root in `VoltroRuntimeProvider`, so the island receives live data. Presentational pages never pay for the client core. - **`hydrate: 'only'`** (Astro's `client:only`): the server renders an empty placeholder — a browser-only lib touching `window` in render no longer crashes the SSR pass — and the client mounts fresh with `createRoot`. - **Island props are declared lossy where they are**: props cross an HTML attribute as JSON, so a `Date` arrives as an ISO string and `Map`/`Set`/ functions do not arrive at all — dev warns naming the island and prop. - The per-island `manualChunks` rule is gone: with per-page entries as additional rollup inputs it MERGED the shared react modules into the island group (a second-React-instance shape); per-island chunks were also redundant — the entry already scopes to the page's islands, and they were statically preloaded anyway.
|
|
81
|
+
|
|
82
|
+
Limits: an islands page reached via SPA navigation from a full page runs in the already-loaded app bundle (the saving applies to hard loads of the islands page); a page's islands share one entry (hydrate strategies control WHEN each hydrates, not when it downloads).
|
|
83
|
+
|
|
84
|
+
Why the golden churn is compatible: `hydrate: 'only'` widens a union, `hydrateIslandsOnPage` gains an optional options argument, and the subpath is a new export.
|
|
85
|
+
- **@voltro/ui, @voltro/client, @voltro/web, @voltro/cli** — Forms without JavaScript — `<AutoForm>` on a server-rendered page now works with JS disabled, end to end.
|
|
86
|
+
|
|
87
|
+
The form always renders `action="/form/<mutationTag>"` + `method="post"`; with JS alive, `onSubmit` intercepts exactly as before (optimistic rpc path unchanged). The `/form/*` endpoint is mounted by the WEB listener on BOTH boot paths (`voltro dev` and `voltro start`, one shared builder):
|
|
88
|
+
|
|
89
|
+
- **Origin-checked at the door** with the same `classifyRequestOrigin` the api's rpc listener runs — a cross-origin form POST is a 403 before a byte of the body is parsed. (The server-side forward reaches the api as a no-browser-origin request, so the web listener's check is the one that guards this surface.) - **One validation path.** The urlencoded body maps through the schema-driven `formDataToInput` (checkbox present/absent → true/false, `''` on number/date omits the field — never a silent 0 —, repeated keys → arrays, non-numeric strings pass through RAW so the decode fails honestly instead of minting NaN, unknown keys dropped) and validates with the SAME `validateFields` the client-side submit runs — byte-identical field errors. - **PRG on success**: 303 back to the submitting page, or to `<AutoForm redirectTo>` (same-origin relative paths only — anything else is refused, a hidden field must not become an open redirect). Reloading the redirected-to page cannot resubmit. - **422 re-render on validation failure**: the referring page renders in the same response with field errors + submitted values in the SAME error UI (`role="alert"`, aria unchanged), `cache-control: no-store`, bypassing the ISR cache in both directions. An rpc refusal after valid input (guard, server error) renders as a form-level error. The flash also embeds as a JSON script, so a page whose bundle arrives late hydrates to the identical state. - **Valid submits forward server-side over `POST /rpc`** with the request's cookie — auth middleware, guards and the rpc interceptor chain run identically to every other mutation.
|
|
90
|
+
|
|
91
|
+
New `<AutoForm>` props: `formKey` (several forms per page — the 422 re-render re-fills only the submitted one), `redirectTo`, and `action={false}` for purely static deploys where `/form/*` does not exist. Headless: `useFormBinding` gains `flash` + `formError`, `@voltro/web` gains `useFormFlash(formKey)`.
|
|
92
|
+
|
|
93
|
+
Also fixed on the way: a required `Schema.Boolean` field with no default used to block the JS submit as "missing" while its checkbox rendered visibly unchecked — boolean fields now seed `false`, agreeing with what the user sees (and with the no-JS mapping).
|
|
94
|
+
|
|
95
|
+
Deliberate limits: the no-JS error re-render needs an `ssr`/`isr` page (a static page cannot be re-rendered with request state; a minimal error page is the fallback); a purely static deploy has no `/form/*` endpoint (use `action={false}`); file uploads stay JS-only (multipart → 415). On an ssr page pass `schema` explicitly — descriptor resolution is a client-runtime feature and the SSR render would otherwise show no fields.
|
|
96
|
+
|
|
97
|
+
Bundle note: the `serverContext` chunk group is renamed `serverRequest` by the context's move to @voltro/client — identical 190 B gz, re-pinned in `bundle-budget.json` (fresh full measurement 2026-08-25: firstLoad 185,494 B gz, was 185,309 — +185 B from the form-flash read in the request context).
|
|
98
|
+
|
|
99
|
+
Why the golden churn is compatible: every addition is a new export or an optional prop; `FormBinding.formError` is a new member of the hook's RETURN type (nothing in the public API accepts a caller-built `FormBinding`), and `ServerRequestContextValue.formFlash` is optional.
|
|
100
|
+
- **@voltro/runtime, @voltro/cli, @voltro/cms, @voltro/plugin-broadcast** — On-demand ISR revalidation — the third invalidation axis next to time (`revalidate`) and CDC (`cacheInvalidatesOn`). Server code in the api process calls `revalidatePath('/blog/[slug]')` / `revalidateTable('posts')` / `revalidateTag('pricing')` (exported from `@voltro/runtime`; callable from mutations, actions, webhook receivers and REST routes) and the matching ISR cache entries fall on EVERY `voltro start` replica — including on dialects with no CDC at all, which is the case this exists for.
|
|
101
|
+
|
|
102
|
+
Transport is dialect-shaped, either or both: on postgres a `pg_notify` rides the SAME LISTEN connection the CDC invalidator already holds (no broker needed); everywhere else the broadcast broker carries it (`BROADCAST_URL` on both deployments; channel namespaced by `VOLTRO_BROADCAST_NAMESPACE` — deliberately env-derived, because this channel pairs an api with its WEB app and no shared name is derivable). A web process with isr routes and neither transport warns loudly at boot; under `voltro dev` the calls are documented debug-logged no-ops. Tags share ONE mechanism with tables — `cacheInvalidatesOn` accepts free strings, so `revalidateTag` is the same sink under another name.
|
|
103
|
+
|
|
104
|
+
Correctness edges built in: `revalidatePath` against a `static` route is a NAMED error on the web process (never a silent no-op); a purge landing while an SWR refresh or miss fill renders is guarded by a per-key generation counter on BOTH cache backends — the pre-purge page cannot be written back with a full TTL, and a refused write also suppresses the postgres backend's fire-and-forget upsert so no replica resurrects a deleted row. A content type declares `revalidate: { paths, tags }` and `publish()`/`unpublish()` fire them after commit.
|
|
105
|
+
|
|
106
|
+
Proven end-to-end (`scripts/revalidate-e2e.mjs`): 1 api + 2 `voltro start` replicas behind Redis (warm → purge → both fresh, with a negative control), the sqlite dialect leg, and a broker-less postgres leg where the NOTIFY line alone carries the purge to a LISTEN-only replica.
|
|
107
|
+
|
|
108
|
+
`apiSurface: compatible` — additive only: the new `@voltro/runtime` revalidation exports, an optional `revalidate` on `ContentTypeSpec`, the optional `onRevalidate`/`revalidateChannel` on the CDC invalidator options, a widened `BroadcastChannelKind`, and `IsrCache.set`'s new optional generation guard (plus `generation()`).
|
|
109
|
+
- **@voltro/protocol, @voltro/plugin-openapi** — `defineRestRoute` takes an opt-in `version:` — versioned REST APIs with a sunset flow.
|
|
110
|
+
|
|
111
|
+
`version: 'v2'` + `path: '/customers'` mounts the route at `/v2/customers`, the same `/vN/` convention the `publicApi:` projection and the built-in `/v1/api-keys` surface already use. The path is normalised ONCE at definition time, so every consumer — the mount, path-param matching, the idempotency scope, tracing spans, the OpenAPI generator — sees the mounted path and none can disagree.
|
|
112
|
+
|
|
113
|
+
Deliberate edges:
|
|
114
|
+
|
|
115
|
+
- **Opt-in, no auto-prefix.** A route without `version:` keeps its literal path untouched — an automatic prefix would silently move every deployed route. A path that already starts with `/vN/` AND declares `version:` is refused loudly at definition (both spellings at once is never what the author meant). - **Two versions are two descriptors.** The old version is ordinary code — visible, testable, deletable — carrying `deprecated:` (the replacement pointer) and `sunset:` (the date it starts answering `410 Gone`; the 410 body now also names which `version` died). - **One OpenAPI document for every version.** The `/vN/` prefix already separates the paths; each versioned operation is grouped under a version tag and carries `x-voltro-api-version` for tooling. No `?version=` filtered spec — a second document shape for information the paths already state. - The rpc socket stays outside URL versioning on purpose: the generated client is versioned with the server it was generated from. A stale browser tab runs the previous client until reload — that skew window exists and is documented, not solved by URLs.
|
|
116
|
+
- **@voltro/web, @voltro/cli** — Opt-in View Transitions for SPA navigations. `router.viewTransitions: true` in a web `app.config.ts` runs every route swap — `<Link>` clicks, `navigate(...)`, back/forward — through `document.startViewTransition`; individual navigations override the default in either direction with `navigate(to, { transition })` / `<Link transition>`.
|
|
117
|
+
|
|
118
|
+
The visual swap is the router's deferred-navigation commit, flushed synchronously inside the transition callback — by that point the target's lazy chunk and loaders have settled, so the flushed tree renders with data in hand. Three deliberate behaviors: `defer()` fields (and an explicit `Pending` skeleton's settled content) resolve AFTER the transition as ordinary updates, never a second animation; navigating while a transition is animating skips the running one (last navigation wins, nothing queues); overlay/dialog state changes never trigger one — a root snapshot would cross-fade the whole viewport for a one-layer change.
|
|
119
|
+
|
|
120
|
+
Fallback is exact: a browser without the API, and any user with `prefers-reduced-motion: reduce`, gets today's untransitioned swap — same timing, nothing to feature-detect. Styling is plain `::view-transition-*` CSS (no animation DSL); cross-document transitions for static/MPA pages are a one-line `@view-transition` CSS opt-in with no framework involvement.
|
|
121
|
+
|
|
122
|
+
Proven in a real chromium (`scripts/browser-view-transitions.mjs`): called on navigation, silent under reduced-motion, harmless with the API deleted, one transition across a `defer()` commit, rapid double-navigation lands on the last target — plus the jsdom wiring suite and the generated-entry flag check shared by all three web boot paths.
|
|
123
|
+
|
|
124
|
+
`apiSurface: compatible` — additive only: a new optional `viewTransitions` on `RouterProps`, optional `transition` on `NavigateOptions`/`LinkProps`, and the optional `router` block on the web app config.
|
|
125
|
+
- **@voltro/web, @voltro/cli** — Schema-typed search params — the query-string half of the URL is now part of the type graph.
|
|
126
|
+
|
|
127
|
+
A page declares its contract once:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
export const searchParams = Schema.Struct({
|
|
131
|
+
q: Schema.optionalWith(Schema.String, { default: () => '' }),
|
|
132
|
+
page: Schema.optionalWith(Schema.NumberFromString, { default: () => 1 }),
|
|
133
|
+
})
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
and gets, end to end:
|
|
137
|
+
|
|
138
|
+
- **Typed reads** — `useSearchParams(searchParams)` returns the decoded, defaulted shape, SSR-aware; the zero-arg call keeps returning the raw `URLSearchParams`. Decoding is TOTAL: an invalid query falls back to the schema's defaults instead of crashing a render; only a schema that cannot even decode `{}` (a required field with no default) throws, naming the fix. - **Typed links** — the generated `routes` builder brands the route's URL with the schema's shape through a TYPE-ONLY page import (zero value edges: code-splitting is untouched, pinned by test), and `withQuery` type-checks against it — a misspelt key or wrong value type is a compile error. The link-side encode is canonical and schema-free (strings/numbers/booleans, arrays as repeated keys); a roundtrip test pins that it produces exactly what the schema's decode accepts. Deliberately ONE generic signature rather than overloads: with overloads, a wrong key would silently fall through to the permissive untyped form and the compile error would never fire. - **Typed writes** — `useSetSearchParams(searchParams)` returns the typed setter: its object form replaces the query (same semantics as the untyped form), and its updater form receives the CURRENT decoded params, so keeping `?filter` across a page flip is one explicit spread — `setParams((p) => ({ ...p, page: p.page + 1 }))` — instead of a hand-rolled merge. - **A fail-closed isr gate** — `renderMode: 'isr'` plus a `searchParams` export is refused at boot: the isr cache is keyed by path (+tenant+locale), not query, so the first variant would be cached for every query — and the gate catches the re-exported spelling (`export { searchParams } from …`, the mirror-route pattern) too, not only the local declaration. - **A doctor rule** — a page that exports the schema but keeps reading the query with zero-arg `useSearchParams()` is flagged with the typed spelling.
|
|
139
|
+
|
|
140
|
+
Deliberate limits: array fields decode a single occurrence as a one-element array (link shape stays stable); `siblingApps` routes stay untyped (their schemas live in a foreign compile graph); `static` pages see the defaults at build time and decode live on the client.
|
|
141
|
+
|
|
142
|
+
Why the golden churn is compatible: `VoltroRouteUrl` gains a type parameter with a DEFAULT (`<TSearch = unknown>`), so every existing bare `VoltroRouteUrl` spelling still compiles, and the new brand member is an OPTIONAL phantom property of type `unknown` — assignability in both directions is unchanged. `withQuery`'s parameter for the untyped case is strictly WIDER than before (adds `boolean` and array values); no call that compiled stops compiling.
|
|
143
|
+
|
|
144
|
+
### Fixed
|
|
145
|
+
|
|
146
|
+
- **@voltro/cli** — `voltro agents-md`'s copied `agent-docs/` mirror no longer keeps orphaned modules across a re-seed.
|
|
147
|
+
|
|
148
|
+
The copy fallback (projects where `@voltro/cli` isn't resolvable) merged into an existing `agent-docs/` directory: when a module was renamed upstream (`plugins/versioning.md` → `plugins/row-history.md`), a `--force` re-seed brought the new file and left the old one sitting beside it — a stale generated doc teaching a package name that no longer exists, which is exactly the claim≠code drift the guide exists to prevent. The mirror is wholly framework-owned, so a re-seed now replaces it (the generator prunes its own output dir the same way).
|
|
149
|
+
- **@voltro/cli** — `voltro check`'s observed-diff footer now accounts for every declared procedure, and no result line states a verdict without the count it is a verdict about.
|
|
150
|
+
|
|
151
|
+
Two defects, both found by readers doing arithmetic on the output:
|
|
152
|
+
|
|
153
|
+
- The diff computes TWO kinds of blindness — a procedure that never ran, and one that ran with no table access recorded — and the printer named only the first. The printed counts came out short of the total, with no way to tell an unshown category from a defect in `check` itself. The buckets are now derived from the result type, so a third one cannot be added without the label map failing to compile, and a partition that does not close prints as such instead of quietly under-counting. - `no declared/observed mismatches` was a bare verdict sitting under its denominator, and was quotable — and quoted — without it, as a clean bill of health for a surface where almost nothing had been exercised. Every result line now carries its scope (`no declared/observed mismatch among the 12 that ran`), and at zero coverage the section reports the ABSENCE of a comparison (`nothing was compared — a declaration is only checked against a procedure that RAN`) rather than the absence of findings. The two are different facts and only one of them is evidence.
|
|
154
|
+
- **@voltro/cli** — An `isr` render no longer sees the requesting visitor's credentials — in `voltro dev` and `voltro start` alike.
|
|
155
|
+
|
|
156
|
+
An isr page's HTML is cached under tenant+locale and served to every visitor inside the revalidate window, but the render itself ran with the FULL request: loaders received the session cookie, and `ctx.query` was bound to it. A loader that read subject-scoped data on an isr page therefore cached the first visitor's data and served it to everyone — cache poisoning by construction.
|
|
157
|
+
|
|
158
|
+
The render boundary is fail-closed now: before an isr render runs, the cookie jar (except `voltro:locale`), the `authorization` header and every `x-voltro-*` header are stripped, for the loaders, `ctx.query` AND the `useServerRequest()` snapshot. What survives is exactly what the cache key and locale resolution read: `x-tenant`, `accept-language`, and the locale cookie — so a `de` visitor's fill still lands under the `de` key. One shared helper (`isrCredentialStrip.ts`), called by both boot paths, so dev renders isr anonymously exactly as production does — a page can no longer look personalised in dev and silently serve shared HTML in production.
|
|
159
|
+
|
|
160
|
+
Behavioural consequence, on purpose: a subject-reading loader on an isr page now gets the anonymous answer. A page whose loader needs the signed-in subject belongs on `renderMode: 'ssr'`.
|
|
161
|
+
- **@voltro/cli, @voltro/runtime** — **`voltro serve` registers plugin rpc routes again — every plugin-contributed procedure answered `Unknown request tag` in production while `voltro dev` registered all of them.** The serve path mirrored dev's plugin-route block by hand and mirrored exactly half of it: the collision check ran (so nothing warned) and the returned routes were discarded as a bare expression statement, never reaching the buckets the rpc registry is built from. Every plugin route was indistinguishable from a tag that never existed — `useUpload`'s storage tags, presence, every inspect-less plugin rpc — while the app's own procedures answered normally, so the registry looked alive.
|
|
162
|
+
|
|
163
|
+
Three changes, each aimed at the way this stayed invisible:
|
|
164
|
+
|
|
165
|
+
- Both boot paths now call ONE shared builder (`mergePluginRoutesInto`) whose buckets are required parameters — a returned list can be discarded by a statement that typechecks; a function you cannot call without handing it the sinks cannot have its effect dropped. A reachability test drives a real socket with a plugin tag and an invented-tag control, and a source pin keeps the helper pair from being reassembled by hand in either path. - The boot line `plugin routes registered` prints WHENEVER plugins are installed, count included — zero is a finding, and silence is how this shipped. `voltro check` against a running server now also diffs source-declared tags against the live registry (`declared vs live:`), and its offline manifest includes plugin routes, which it previously did not. - Every `Defect` frame the server sends is now also a server log line (`rpc defect sent to client`, ws and http rpc). The defect string used to exist only inside the WebSocket frame — visible in whoever's browser console, invisible to the operator whose server produced it.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
42
169
|
## [0.51.0] — 2026-08-24
|
|
43
170
|
|
|
44
171
|
### ⚠ BREAKING
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-atlassian",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.0",
|
|
4
4
|
"description": "Jira + Confluence plugin — JiraService + ConfluenceService over the Atlassian REST/Greenhopper/Agile APIs, with a pluggable per-subject credentials resolver (PAT), transient retry + Retry-After, timeouts, an SSRF-guarded PAT-free avatar proxy, and optional response caching via @voltro/cache.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"node": ">=24.0.0"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@voltro/integration-http": "0.
|
|
47
|
-
"@voltro/protocol": "0.
|
|
46
|
+
"@voltro/integration-http": "0.52.0",
|
|
47
|
+
"@voltro/protocol": "0.52.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"effect": "^3.22.0"
|