chore: release server 0.8.57, cli 0.10.49, core 0.7.44#1107
Conversation
Two user-facing server fixes have been sitting unreleased. The Bun listener ignored X-Forwarded-Proto/Host, so behind a TLS terminating proxy every absolute URL an app derived came out http://. This one shipped: webjs.dev runs on Bun behind Cloudflare and served an http:// og:image, and the blast radius is OAuth callbacks, canonical tags and sitemap entries too. And a root middleware.ts was never loaded, because the lookup was the single literal middleware.js, with no error to notice. The cli carries the other half of the middleware fix (dev only watched two of the four extensions the server loads, so a root middleware.mts loaded but never triggered a dev restart) plus the scaffold's docs URLs. Core is one line, the docs URL in the reactive-property error. Normally that would ride the next functional bump, but a URL baked into a thrown error is the one thing a published version can never take back, so it goes out with these. Changelogs are hand-written rather than generated: the docs-move squash subject is feat(website), which the backfill would have attributed to the server and cli packages as a website feature instead of naming the middleware fix each package actually got.
These become the public release notes, so they have to be true. The cli entry claimed 'webjs help' printed the old docs host. It did not: that string is in the unknown---template rejection message. The same sentence named only the generated footer, while the commit also repointed the generated root-layout header nav and the tips printed after webjs create. The server notes omitted the two error-message URL changes the release actually ships (the server-only-module stub throw and the TypeScript strip failure). Core got its own release for exactly that class of change in this PR, so leaving it out of server under-reported what publishes. The forwarded-proto entry linked only its commit; its PR is #1091. The squash subject lost the suffix, which is why it was missed, but these notes are hand-written precisely so that does not matter.
vivek7405
left a comment
There was a problem hiding this comment.
Reviewed this the way a release deserves, since it cannot be taken back once it publishes.
The bump set and levels check out: ui, mcp and intellisense have no unreleased commits touching their published trees, all three bumps are patch-correct against what is live on npm, every in-repo caret range still satisfies, the lockfile changes exactly the three workspace version fields, and the wrapper versions are correctly left to the release workflow.
Three things were wrong in the notes themselves, all now fixed in c081a4a9. The cli entry claimed webjs help printed the old docs host; it did not, that string is in the unknown---template rejection message, and the same sentence named only the generated footer while the commit also repointed the generated header nav and the post-create tips. The server notes omitted the two error-message URL changes this release actually ships, which is inconsistent given core got its own release for exactly that class of change in this same PR. And the forwarded-proto entry linked only its commit when its PR is #1091; the squash subject lost the suffix, which is why it was missed, but these notes are hand-written precisely so that does not matter.
One thing I got wrong OUTSIDE this PR, worth stating plainly: my release-debt audit missed the editor plugins. The VS Code extension has an unreleased change from the docs move (its webjs.docs command still opens the old host in the published 0.2.2) and webjs.nvim carries an unreleased vendored-intellisense change. Neither is bumped here, and neither publishes through release.yml, so they are a separate decision rather than something to fold in silently.
The forwarded entry was titled 'on the Bun listener' and asserted the node shell was always correct. It was not. On node, 0.8.56 built the origin by resolving the request target against the proxy headers, and three client-controlled inputs could poison it: a //-prefixed request target became the authority AND silently rewrote the path, so a different route matched; X-Forwarded-Proto was honored for any scheme, so 'javascript' produced an origin of the literal string null; and a malformed forwarded host threw Invalid URL as a 500. Node is the default runtime. A node user reading the old note would conclude nothing in this release affected them, and once published that cannot be corrected. The entry now leads with the fact that both runtimes are affected and names each input. The cli note claimed a root middleware.mts 'loaded but never restarted' and that middleware.ts was 'loaded by neither'. Neither state ever shipped: before this release the server resolved only middleware.js while the supervisor already watched .ts, so .ts was watched and never loaded, and .mts was neither. Rephrased as what it is, the CLI half of a paired fix. The same defect is in the source comment the wording came from, left alone here so this PR stays free of source changes. The cli surface list also omitted the agent skill the scaffold ships, which is the largest agent-facing surface in a generated app.
vivek7405
left a comment
There was a problem hiding this comment.
Second pass, and it caught the most important thing in this release.
The forwarded entry was titled "on the Bun listener" and stated outright that the node shell was always correct. That is false, and node is the default runtime. On 0.8.56 the node path built the origin by resolving the request target against the proxy headers, and three client-controlled inputs poisoned it. I verified each rather than taking the report on faith:
GET //evil.com/xresolved to originhttp://evil.comwith the path silently rewritten to/x, so a different route matched than the one requested. Confirmed in a REPL against the exact 0.8.56 expression.X-Forwarded-Proto: javascriptproduced an origin of the literal stringnull, so every absolute URL the app derived becamenull/....- A malformed forwarded host threw
Invalid URL, surfacing as a 500 on the fetch path.
The repo's own added test is even named "a //-prefixed path cannot become the authority (both entry points)", so the code always knew this was not Bun-only; only my notes said otherwise. A node user reading the old text would have concluded nothing here affected them, and a published release cannot take that back. Rewritten in 26486c1e to lead with both runtimes being affected and to name each input, plus the Bun absolute-form request-line hole that was also unmentioned.
Two smaller corrections in the same commit. The cli note claimed a root middleware.mts "loaded but never restarted" and that middleware.ts was "loaded by neither"; neither state ever shipped, since before this release the server resolved only middleware.js while the supervisor already watched .ts. And the cli surface list omitted the agent skill the scaffold ships, which is the largest agent-facing surface in a generated app.
Two things deliberately NOT changed here:
The same faulty wording exists in the source comment in dev-supervisor.js that my changelog inherited it from. Fixing it would put source into a release PR whose entire safety argument is that it contains none, so it goes separately.
The website renders these entries as 23 sibling bullets for 3 entries, because a blank line inside a list item closes the item without leaving the list. That is a pre-existing renderer bug that hand-written multi-paragraph notes make glaring, it affects only /changelog (GitHub Releases renders the same markdown correctly), and the website reads changelogs from the repo so it stays fixable after publish. Filed as #1108 rather than bent around by writing shorter notes; a security fix needs the space to be explained.
Merge with squash, not rebase. release.yml triggers on --diff-filter=A over changelog/**.md against HEAD~1. This branch adds the files in one commit and modifies them in later ones, so a rebase merge puts a modify-only commit at HEAD, matches nothing, and silently skips every publish step with no failure signal.
Two corrections, both in the direction of not under-reporting to node users, which is the failure this file has now had twice. An absolute-form request line was listed as a Bun-only exposure. It is not: node's HTTP server hands back req.url verbatim for 'GET http://evil.com/x', so 0.8.56 resolved it to that origin with the path rewritten, exactly like the //-prefixed case. Verified against a raw socket rather than reasoned about. It is now the fourth input in the shared list, with the two runtime-specific ones marked as such. The parity sentence claimed the shells cannot drift on 'any of the guards above'. #1091 has a commit titled 'stop claiming the two listener shells cannot drift', and the shipped JSDoc scopes the guarantee to the header and origin decisions while noting an absolute-form request line still routes differently on each. Reasserting what the source PR deliberately walked back is worse than saying nothing, so the note now matches the code's own wording. Also adds the upgrade consequence to the middleware entry. A dormant root middleware starting to execute is the point of the fix, but it is a behaviour change on a patch bump: every --template api app ships a root middleware.ts applying cors() with placeholder origins, which goes live. And the cli skill note now covers both content updates it publishes, not just the middleware line.
b765787 to
e3d80e8
Compare
vivek7405
left a comment
There was a problem hiding this comment.
Third pass. Two more real problems, and both are the same failure mode this file has now had three times: under-reporting to node users.
A fourth origin input, wrongly filed under Bun. I listed three client-controlled inputs and attributed the absolute-form request line to Bun alone. Node does it too. I checked with a raw socket rather than reasoning about it: node's HTTP server hands back req.url === "http://evil.com/x" for GET http://evil.com/x HTTP/1.1, and 0.8.56's expression resolved that to origin http://evil.com with the path rewritten to /x, bit for bit the same as the //-prefixed case. It is now the fourth entry in the shared list, with the two genuinely runtime-specific items marked as such.
A parity claim the source PR explicitly retracted. I wrote that the two shells "cannot drift on the trust switch, the comma-chain rule, or any of the guards above". #1091 contains a commit titled "docs: stop claiming the two listener shells cannot drift", and the shipped JSDoc scopes the guarantee to the header and origin decisions, adding that an absolute-form request line still routes differently between them. Reasserting in release notes what the source PR deliberately walked back is worse than saying nothing at all. The wording now matches the code's own.
Also added: the upgrade consequence of the middleware fix. A dormant root middleware starting to execute IS the fix, but it is a behaviour change arriving on a patch bump, and every --template api app ships a root middleware.ts applying cors() with a placeholder allow-list that goes live on upgrade. Someone reading release notes to decide whether to take a patch deserves to know that. And the cli's skill note now covers both content updates it publishes rather than one.
All fixed in e3d80e8d. The mechanical side stayed clean through three rounds of edits: bumps are still patch-correct against npm, ranges satisfied, lockfile exactly three version lines, frontmatter and commit_count unchanged and consistent.
The lead-in said the first two inputs applied to every runtime and the fourth to Bun, which was wrong for three of the four and contradicted the fourth bullet's own text two lines below it. Probed Bun directly rather than reasoning: for 'GET //evil.com/x' it returns http://real.example//evil.com/x, so the origin stays real and the path is preserved. Inputs 1 to 3 were node-only, 1 because Bun's parser does not treat // as an authority and 2 and 3 because the pre-fix Bun handler never read a forwarded header at all. Only the absolute-form request line hit both shells. Same under-report-to-node direction as the last two rounds, this time introduced by the sentence written to fix it.
vivek7405
left a comment
There was a problem hiding this comment.
Fourth pass. One finding, and it is one I introduced in round 3 while fixing the round-2 problem.
The lead-in claimed the first two inputs applied to every runtime and the fourth to Bun. Three of the four attributions were wrong, and it contradicted the fourth bullet's own text two lines below it, which correctly says both shells.
I probed Bun rather than reasoning about it: for GET //evil.com/x it returns http://real.example//evil.com/x, so the origin stays real and the path is preserved. Inputs 1 through 3 were node-only, the first because Bun's parser does not treat // as an authority, the second and third because the pre-fix Bun handler never read a forwarded header at all. Only the absolute-form request line hit both.
Fixed in 7f568eff.
Worth naming the pattern, since it is now four rounds running in the same direction: every error in this file has under-reported exposure to node users, and this last one was introduced by the sentence written to correct the previous one. The lesson for a security-relevant release note is that a summary line that partitions the detail is itself a claim needing verification, not connective tissue. The bullets were right each time; the sentence summarizing them was not.
Mechanics re-verified for the fourth time and unchanged: bumps patch-correct against npm, ranges satisfied, lockfile exactly three version lines, frontmatter and commit_count consistent, release.yml tie-break puts cli last so the wrapper lockstep fires.
The note called it 'the class-field error thrown when a reactive property is shadowed'. No such runtime error exists in core: class-field shadowing is caught statically by the no-class-field webjs check rule in the server package. The one docs URL in the whole core package sits in the error thrown when a component declares its own static properties block, which is what e07ea3a changed. Both halves of the sentence named something that does not exist, and it was the entire body of the core note. The bullet title said 'reactive-property error' too, so it is retitled to match.
vivek7405
left a comment
There was a problem hiding this comment.
Fifth pass. One finding, in the file I had scrutinized least.
The core note described "the class-field error thrown when a reactive property is shadowed". No such runtime error exists in core. Class-field shadowing is caught statically by the no-class-field check rule, which lives in the server package. The single docs.webjs.dev URL in the whole core package sits in the error thrown when a component declares its own static properties block, and that is what the commit changed. Both halves of the sentence named something that does not exist, and it was the entire body of the note. Retitled and rewritten in 9ee71687.
That the core file was where this survived five rounds is the tell: attention went to the server note because it carries the security content, and the one-line package got a sentence written from memory rather than from the source. On an irreversible publish, the small note deserves the same grep as the large one.
Mechanics re-verified: npm has 0.8.56 / 0.10.48 / 0.7.43, so these are the correct next patches with no gaps; every dependent range admits them unwidened; lockfile is exactly the three version lines; frontmatter parses under publish-npm.js; commit_count 2/1/1 matches; both PR links resolve to their merge commits; CI green.
The cli note said a generated app should carry the current URL 'rather than one that costs every reader a redirect'. Both cost one hop: the bare /docs 308s to /docs/getting-started, just as docs.webjs.dev 301s to webjs.dev. Verified against production rather than assumed. The real gain is consolidation onto the domain the docs live on, which is what the note now says. The core and server notes are unaffected: their URLs are deep links that return 200 directly. Pointing the scaffold links at /docs/getting-started would make the claim true, but that is a source change and this PR stays source-free.
vivek7405
left a comment
There was a problem hiding this comment.
Sixth pass. One finding, and a good one.
The cli note justified the URL swap as sparing readers a redirect. Both links cost exactly one hop: the bare /docs 308s to /docs/getting-started, just as docs.webjs.dev 301s to webjs.dev. I checked production rather than assuming, and the 308 is deliberate, I built it in #1101 so the docs would not have a second landing page competing with the introduction.
So the stated benefit was false. The real one is consolidation onto the domain the documentation actually lives on, which is what the note now says. Fixed in 397f9b92. The core and server notes are unaffected: their URLs are deep links returning 200 directly, which I verified rather than inferring from the cli case.
Pointing the scaffold links at /docs/getting-started would make the original claim true and is arguably the better link anyway, but that is a source change and this PR stays source-free by design.
That is six rounds, and every single finding has been a claim in the prose rather than anything mechanical. The versions, ranges, lockfile, and workflow compatibility have been correct since the first commit and have now been re-verified six times. What kept being wrong was the writing about the code, which is worth remembering for the next release: the diff is the easy part to get right.
The last commit removed a claim that was true. The scaffold links are the bare form, and the old one chains two redirects (docs.webjs.dev to webjs.dev/docs to /docs/getting-started) while the new one costs one. I had checked each hop in isolation and concluded both cost one, which is what a single-request probe shows and what a curl -L redirect count does not. The lesson is narrow and worth keeping: a claim about a redirect chain has to be measured as a chain.
vivek7405
left a comment
There was a problem hiding this comment.
Seventh pass, and it caught me over-correcting.
Round 6 removed a claim that was true. The scaffold links are the bare form, and the old one chains two redirects (docs.webjs.dev to webjs.dev/docs to /docs/getting-started) where the new one costs one. Measured with a redirect count along the whole chain:
https://docs.webjs.dev hops=2 final=https://webjs.dev/docs/getting-started
https://webjs.dev/docs hops=1 final=https://webjs.dev/docs/getting-started
My round-6 probe checked each hop in isolation and concluded both cost one, which is exactly what a single-request probe shows and what a chain measurement does not. Restored in cc8dda64, now stated as the chain rather than as an unqualified "a redirect".
Worth stating plainly since it is the seventh round: six of these findings were me over-claiming, and this one was me over-correcting a claim that had been right. Both come from writing a sentence about behaviour I had not measured in the form the sentence asserts. A single-hop probe does not answer a question about a chain.
Releases
@webjsdev/server0.8.57,@webjsdev/cli0.10.49, and@webjsdev/core0.7.44.Why now
Two user-facing server fixes have been sitting unreleased since 0.8.56.
The Bun listener ignored
X-Forwarded-Proto/X-Forwarded-Host. Behind a TLS-terminating proxy, every absolute URL an app derived came outhttp://. This one actually shipped: webjs.dev runs on Bun behind Cloudflare and served<meta property="og:image" content="http://webjs.dev/public/og.png">. The blast radius is every absolute URL an app derives, so OAuth callbacks, canonical tags, and sitemap entries were affected too, not just OG tags. Anyone running WebJs on Bun behind a proxy has this today.A root
middleware.tswas never loaded. The lookup was the single literalmiddleware.js, so an app whose root middleware was TypeScript ran with no global middleware at all: no error, no warning, indistinguishable from an app that has none.examples/blogships a rootmiddleware.tsthat had never run.The cli carries the other half of that fix (dev watched two of the four extensions the server resolves, so a root
middleware.mtsloaded but never triggered a dev restart) plus the scaffold's docs URLs, so a newly generated app is born pointing atwebjs.dev/docsinstead of the redirecting old host.Core is a single line: the docs URL inside the reactive-property error. By convention a docs-only change rides the next functional bump, and it is folded in here deliberately instead. A URL baked into a thrown error is the one thing a published version can never take back, so every release it sits out is more installs carrying a stale link.
Version levels
All three are patches. Every dependent pins a caret range (
^0.8.0,^0.10.0,^0.7.0/^0.7.1), so a patch stays inside them and no dependent range needed widening.package-lock.jsonis regenerated (three version lines) sonpm cistays in sync.The
create-webjsandwebjsdevwrapper versions are deliberately untouched: the release workflow owns them in lockstep with the cli.Changelogs are hand-written
scripts/backfill-changelog.jsextracts entries from squash subjects, and the docs-move commit is titledfeat(website): serve the documentation at webjs.dev/docs. It touched the server and cli trees, so the generator would have attributed a website feature to both packages rather than naming the middleware fix each one actually received. Same over-attribution shape as the earlier@webjsdev/uichangelog problem. Each changelog is written to the surface its own package actually changed.Test plan
npm teston this branch: 3324 pass, 0 fail.npm ci, covered by the Unit + Build jobs) and changelog format (covered by Conventions).node_modules/@webjsdev/*points at the worktree's own packages. The default whole-directory symlink resolves them to the primary checkout, so the test file and the app under test load two different copies of the server in one process. That is the known fresh-worktree trap, not a defect in this branch; with resolution corrected all suites pass.After merge
Adding
changelog/**.mdtomaintriggersrelease.ymltonpm publishand cut GitHub Releases (idempotent). Thennpm update -g webjsdevandbun add -g webjsdevto refresh the global CLI once the versions are live.