Skip to content

feat(hot): a secret on the endpoint, a file per transport, and publish - #2451

Merged
alexander-akait merged 6 commits into
mainfrom
feat/hot-token
Oct 2, 2026
Merged

alexander-akait merged 6 commits into
mainfrom
feat/hot-token

Conversation

@alexander-akait

@alexander-akait alexander-akait commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

#2452 and #2453 were merged into this branch, so this PR now carries all three pieces of work. They are still one commit each, in this order:

commit what
feat(hot): require a secret on the endpoint, with \hot.token`` the token
refactor(hot): load only the transport that was chosen EventSourceServer extracted, both transports lazily required
refactor(hot): put each piece where it belongs each transport owns its own CORS default, pathMatch to utils
feat(hot): add \publish`, and deprecate `hot.progress`` the publish interface
fix(hot): address the review on \hot.token`` the three findings below

hot.token

A secret on the hot endpoint that a client has to carry to connect. Without it anything that can reach the port can open the stream and read what a build reports — module paths, and the source frames webpack puts in a failed build's errors.

middleware(compiler, { hot: { token: true } });
// -> the injected client's entry query carries token=xJ3kP9mQ2vR8
// -> GET /__webpack_dev_middleware_hot without it answers 403

hot.cors already scoped which origins a page may be on. This scopes who may connect at all, which is the part an allowlist cannot cover: a request from outside a browser carries no Origin to check.

Off by default, on both transports

true in the next major release. The reason it cannot be the default today is worth stating, because an earlier revision of this PR had it on for the WebSocket transport and that was wrong:

A token only reaches the browser on the entry this middleware adds, and inject being on does not mean an entry was added. injectHotClient declines in three cases — every entry point already pulls the client in, hot.transport is a function, the target is not the web. Requiring a token by default turns each of those into a 403 on every client.

The first is wiring the README documents: the client listed in your own entry, with inject left alone. Measured on that setup before the default changed:

endpoint requires a token : "9hZ5X8PP69Bm"
client entries injected   : 0
=> every client refused

"The WebSocket transport is unreleased, so nothing is connecting to it that would not carry one" is true — published 8.3.0's schema has only path, heartbeat, progress and statsOptions under hot — but it answers the wrong question. The break is in supported wiring within the same version, not in clients already on the wire.

Asking for one where no client was injected warns rather than leaving an unexplained refusal.

What it does not protect

Stated in the README rather than left to be discovered: the token reaches the browser in the bundle, as a literal in the client's entry query. A page that can read your bundle cross-origin can read the token out of it — the technique in CVE-2026-6402, a <script> tag plus intercepting webpack's module registration. hot.cors is what answers that, and the two are complementary rather than alternatives.

So the token's job is narrower and worth naming precisely: it stops anything that can reach the port but cannot read a bundle — another process on the machine, something on the LAN, a request that is not a browser at all.

For a client you wired yourself

A hand-wired entry is built before the middleware exists, so it can never carry a per-run token. Two ways out, both documented:

// a fixed one you choose
middleware(compiler, { hot: { token: "a-secret-of-your-own" } });

// or read what was minted, and hand it over yourself
const instance = middleware(compiler, { hot: { token: true } });
instance.token; // "xJ3kP9mQ2vR8"

hot.inject: false turns the requirement off even when asked for: with nothing injected there is no way to hand a token over, and requiring one would refuse a client the developer wired correctly.


A file per transport

hot.js had one transport in a file of its own and the other inline, which made the structure read as if Server-Sent Events were the special case rather than the default. Both are files now, and hot.js is the lifecycle and the payloads — down from 231 lines of stream plumbing.

Each transport owns what is specific to it, next to the code that applies it:

// servers/EventSourceServer.js
const HOT_DEFAULT_CORS_SSE = true;

// servers/WebSocketServer.js
const HOT_DEFAULT_CORS_WS = CORS_LOCAL_ORIGINS;

What stays in hot.js is the mint that hands one token to whichever transport was built — genuinely shared. pathMatch moves to utils.js, since middleware.js was reaching through hot.js for it.

#2450 made the WebSocket server lazy; both are now, so a project on 'ws' no longer parses the SSE server either, and a custom transport parses neither.


publish(payload), and hot.progress deprecated

hot.progress applies webpack's ProgressPlugin to your compiler. A server that applies one itself — webpack-dev-server does — then has two of them on one compiler. Measuring a build is the server's call; carrying the result is the middleware's.

new webpack.ProgressPlugin((percent, message) => {
  instance.publish({
    action: "progress",
    percent: Math.round(percent * 100),
    message,
  });
}).apply(compiler);

The bundled client already renders { action: "progress" }, so a server with its own plugin had everywhere to put it and needed only this. Any action the clients understand goes through it; { action: "reload" } is the other useful one.

hot.progress warns and keeps working until the next major release. hot.client.progress is unaffected and stays — it is the browser's end of this, and defaults to true, so a server publishing its own payload gets it drawn with no further configuration.

Two things the option did for you become the caller's: rounding the percent, and dropping a tick that rounds to the same whole number as the last one.


The review findings

  • A path that already carried a token ended up with two, and the endpoint reads the first — so the token configured here was the one ignored. A fragment was mishandled the same way: path#frag became path#frag?token=…. Now in client-src/utils/with-token.js, taken apart on # then ? rather than parsed with URL — which needs a base and would turn a relative path into an absolute one. 9 tests cover absolute, protocol-relative, rooted, relative, existing query, existing token, fragment and escaping.
  • The "no client was injected" warning printed the token. Infrastructure warnings travel into CI output, and a minted token is different every run — so printing it also invited the wrong fix, pasting a value already stale. It names token=<the token> and points at the token property.
  • A README example read instance.token from a configuration that does not ask for one, so it served { token: false }.

Notes for review

  • token is a real client option, not something the middleware slips into path. It has to be: a hand-wired entry has no path parameter to slip it into, which 7 e2e failures established before the option existed.
  • Comparison is crypto.timingSafeEqual, length-checked first since it throws on a mismatch.
  • 9 random bytes rather than 8 or 16 — base64url encodes 9 with no padding, so the query carries 12 characters and no =.
  • Both transports refuse with 403 before the CORS grant and before the stream opens; the WebSocket side answers rather than dropping the socket, so a client learns why.
  • hot.client.token exists as well as hot.token, which a symmetry test insisted on — every option the query takes is settable on the node side under the same name.
  • The e2e fixtures ask for a token, so the browser runs cover an endpoint that requires one.
  • Two README links fixed in passing: the etag row and both references to the attach method pointed at headings that do not exist.

Verified

  • npm run lint — clean (eslint, prettier, cspell, tsc, client types, schema-check)
  • unit — 7096 passed, 20 suites
  • e2e — 167 passed, 14 suites, 20 snapshots

🤖 Generated with Claude Code

https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

Summary by CodeRabbit

  • New Features
    • Added optional token authentication for hot-reload connections, with generated or fixed tokens.
    • Added publish(payload) for sending custom events to connected hot-reload clients, including progress updates.
  • Documentation
    • Marked hot.progress as deprecated; it remains available until the next major release.
    • Corrected README links.

@changeset-bot

changeset-bot Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b937c07

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
webpack-dev-middleware Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The pull request adds token support for hot endpoints and client connections. It resolves tokens from options, passes them to WebSocket and SSE transports, and appends them to injected or manual client URLs. It also adds publish(payload) on middleware instances, deprecates hot.progress, and updates types, tests, and documentation for the new behavior.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to b937c

Clarify that users should disable hot.progress before registering their own ProgressPlugin to avoid duplicate progress reports. This is a limited migration issue, not a broad workflow blocker.

Security Architecture Review

Security architecture risk: 🔵 Low · up to b937c

The change adds optional connection protection without opening a new remote publishing interface. Remaining risk is concentrated in custom integrations and refreshing clients when credentials change.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The protected scope is one hot instance's connected-client population and the build or application payloads broadcast to it. Possession of its token permits admission subject to the transport's remaining controls; no finer-grained payload authorization is added.

Security Findings and Attack Paths

  • inferred — An attacker who can read the injected bundle can obtain the bearer token and attempt a hot connection. This is an explicitly documented limitation of the added control, not demonstrated increased exposure over the base's token-free admission. Whether a deployed server prevents such bundle reads remains outside the available evidence.

Trust Boundaries and Controls

  • observed — Enabled token validation rejects missing, malformed, and mismatched values. It uses equal-length timing-safe comparison. SSE rejects before stream headers and registration; WebSocket rejects before upgrade and retains its subsequent origin check.
  • observed — Custom transport factories receive the token they should require, but runtime validation checks method shape rather than authorization behavior. Their handlers and upgrades remain transport-owned. No concrete downstream implementation was available to establish either compliance or a bypass.

Resilience and Maintainability Implications

  • inferred — The existing client cache remains keyed only by path within self, and same-path reconfiguration does not replace the captured token. This can complicate credential changes or multiple client configurations, but the inspected sharing occurs within one JavaScript global and does not establish a new cross-origin or tenant-isolation bypass.

Hardening Proposals

  • proposed — Document token enforcement explicitly in the custom-transport example and provide a conformance example that rejects unauthorized requests before registration, onConnect, or catch-up delivery. This would make delegated authorization less prone to integration drift.
  • proposed — Define the client refresh procedure for generated-token rotation and consider connection identity that includes the token and transport, with explicit teardown on identity changes. Recovery should obtain a fresh credential without weakening endpoint admission.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: hot-endpoint token protection, transport separation, and the new publish API. It is specific and concise.
Docstring Coverage ✅ Passed Docstring coverage is 94.74% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 19 functions across 24 files. (2 skipped: 2…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 Minor · Add token to HotClientOptions. · hot.d.ts:208

types/hot.d.ts:208
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add token to HotClientOptions.

src/options.json accepts hot.client.token, and src/utils.js serializes that override. However, HotClientOptions does not declare it. TypeScript therefore rejects an inline configuration such as { hot: { client: { token: "remote-secret" } } } as an excess property. (typescriptlang.org)

Add token?: string | undefined to HotClientOptions and update the source typedef if these declarations are generated.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 26e5489a-137a-45dd-8aac-e7ef24c566c4

📥 Commits

Reviewing files that changed from the base of the PR and between 88626e9 and eab2f01.

📒 Files selected for processing (18)
  • .changeset/hot-token.md
  • .cspell.json
  • README.md
  • client-src/index.js
  • src/hot.js
  • src/index.js
  • src/options.check.js
  • src/options.json
  • src/servers/WebSocketServer.js
  • src/utils.js
  • test/__snapshots__/validation-options.test.js.snap.webpack5
  • test/helpers/hot-app.js
  • test/hot.test.js
  • types/client/index.d.ts
  • types/hot.d.ts
  • types/index.d.ts
  • types/servers/WebSocketServer.d.ts
  • types/utils.d.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

Comment thread client-src/index.js Outdated
alexander-akait and others added 3 commits October 2, 2026 00:39
`hot.cors` is answered by `Origin`, and that is its weakness: a browser omits
`Origin` and the whole `Sec-Fetch-*` family when the destination is not
potentially trustworthy — plain `http` to anything but `localhost`, which
`host: "0.0.0.0"` gives you. webpack-dev-server shipped two fixes built on
those headers and both were bypassed exactly that way, CVE-2026-6402 and then
CVE-2026-14620. A token asks the browser to volunteer nothing.

`createHot` mints one per run, `injectHotClient` hands it to the client as
another entry-query option, and both wires check it with `timingSafeEqual`
before anything else — so a caller without one is told nothing about which
origins the endpoint would have allowed. A transport of your own is given it
too. Driven end to end:

  SSE, token: true       no token 403   wrong 403   right 200
  WS, default            no token 403   wrong 403   right CONNECTED
  WS, token: false       no token CONNECTED

The two transports default as they did for `cors`: `true` for the WebSocket,
which is unreleased so nothing is connecting to it that would not be handed
one, and `false` for Server-Sent Events, where requiring one would refuse every
client already connecting.

Two things the implementation had to account for, both found by the browser
tests rather than by reasoning:

`inject: false` turns the requirement off. The token reaches the browser
through the entry this middleware adds, so with nothing injected there is no
way to hand one over and requiring it would refuse a correctly wired client.

The client needed the option after all. The first attempt folded the token into
the `path` query on the assumption that the client uses that verbatim — true
for an injected client, useless for a hand-wired entry, which has no `path`
parameter and would not know what a bare `token=` meant. It is a client option
like the others now, and `hot.client.token` accepts it in node for a client
pointed at another endpoint, which keeps the two name sets identical — a test
asserts that and caught the asymmetry.

What it does not protect, said in the README rather than left implied: the
client reads the token from its entry query, so it is a string in the bundle.
Anything that can already read the bundle cross-origin reads the token with it,
and over plain `http` to a non-localhost address nothing stops that unless the
server sends `Cross-Origin-Resource-Policy`. This hardens every case where the
bundle is not readable, and is defence in depth where it is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
`createEventStream` moves out of `hot.js` into `src/servers/EventSourceServer.js`,
beside the WebSocket one, and both are required where the transport is picked
rather than at the top of the module. Neither is reached until a `createHot`
call asks for it:

  transport           EventSourceServer   WebSocketServer   ws
  "sse" (default)     loaded              —                 —
  "ws"                —                   loaded            loaded
  your own            —                   —                 —

Previously the event stream was parsed by every consumer, including one on a
WebSocket or on a transport of its own, because it lived in `hot.js`.

`src/servers/` now holds both, which is the shape webpack-dev-server's
`lib/servers/` has, and the CORS and token rules sit with the transport that
enforces them — `hot.js` keeps only the default each one starts from and the
mint that hands a token to both.

`createEventStream` is still exported from `hot.js`, as a wrapper that loads
the module on the first call, so an importer sees no change.

`requireServer` spells each path out rather than building one from its
argument: a bundler has to be able to see both statically.

Also fixes two fixtures that hand-wire a client and so have to carry a token of
their own, which is what a developer wiring their own entry has to do: the
worker app, and the cross-origin test that builds its own WebSocket url — that
one reads `instance.token` rather than hardcoding one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
Follow-up to extracting the event stream: the module boundaries were right but
three things were still on the wrong side of them.

Each transport's CORS default moves to the transport that applies it.
`HOT_DEFAULT_CORS_SSE` and `HOT_DEFAULT_CORS_WS` lived in `utils.js`, which
`hot.js` then imported the SSE one from purely to re-export it — it never used
it. `resolveCors` is called inside each server, so the default each one starts
from belongs there too, with the comment explaining why they differ.

`pathMatch` moves to `utils.js`. `middleware.js` was reaching through `hot.js`
for a url helper, which is the wrong direction: the middleware does not
otherwise depend on the hot module, and every other request helper it uses is
already in `utils.js`. `hot.js` did not use `pathMatch` itself either — that
import was another re-export.

And a comment that the lazy-loading change had stranded: "what a transport has
to do for itself" describes `CLIENT_STREAM_METHODS`, and `requireServer` had
been inserted between the two.

What is left in `hot.js` is one thing: the hot lifecycle and the payloads it
publishes — its own defaults, the contract a custom transport has to meet, and
`createHot`. Transport mechanics are in `./servers`, request helpers in
`./utils`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 Minor · Add token to HotClientOptions. · hot.d.ts:208

types/hot.d.ts:208
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add token to HotClientOptions.

hot.client.token is defined by the schema and serialized into the browser query. The source typedef and generated declaration omit it. An inline TypeScript configuration therefore fails excess-property checking.

Add the property to src/hot.js, then regenerate types/hot.d.ts.

Suggested fix
 // src/hot.js
  * @property {string=} name limit the runtime to one compilation's builds, the compilation's own name by default
+ * @property {string=} token the secret the runtime puts on its connection url
 // types/hot.d.ts
   name?: string | undefined;
+  token?: string | undefined;

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 3e2ad919-b9e0-419b-b0dd-028b3ae09662

📥 Commits

Reviewing files that changed from the base of the PR and between eab2f01 and 53cfd98.

📒 Files selected for processing (11)
  • .changeset/hot-token.md
  • README.md
  • src/hot.js
  • src/options.check.js
  • src/options.json
  • src/utils.js
  • test/helpers/hot-app.js
  • test/hot.test.js
  • test/inject-client.test.js
  • types/hot.d.ts
  • types/utils.d.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.

Comment thread README.md Outdated
Comment thread src/utils.js Outdated
@codecov

codecov Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.37681% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.25%. Comparing base (88626e9) to head (b937c07).

Files with missing lines Patch % Lines
src/servers/EventSourceServer.js 94.87% 4 Missing ⚠️
src/utils.js 96.29% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2451      +/-   ##
==========================================
- Coverage   96.34%   96.25%   -0.09%     
==========================================
  Files          20       22       +2     
  Lines        2378     2430      +52     
==========================================
+ Hits         2291     2339      +48     
- Misses         87       91       +4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Measuring a build is the server's call rather than the middleware's, and
`hot.progress` made it the middleware's: it applied `ProgressPlugin` to
your compiler, so a server that applies one itself — webpack-dev-server
does — ends up with two of them on one compiler.

What only the middleware can do is carry the result, and the bundled
client already renders `{ action: "progress" }`. So the option becomes one
public method: `instance.publish(payload)` puts a payload of your own on
the stream, and a server hands over what its own plugin reports. It is a
no-op when `hot` is off, so a caller does not have to ask first, and
nothing is sent when no client is connected — a `ProgressPlugin` tick
fires far more often than anyone is listening.

`hot.progress` keeps working until the next major release. Its browser
end, `hot.client.progress`, is unaffected and stays: a server publishing
its own progress payload gets it drawn with no further configuration.

Two things the option did for you become the caller's: rounding the
percent, and dropping a tick that rounds to the same whole number as the
last one.

Also fixes two README links that pointed at headings which do not exist,
for `etag` and for the `attach` method.


Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 486ce750-fde2-4501-94dd-259fe64a675b

📥 Commits

Reviewing files that changed from the base of the PR and between 53cfd98 and 621e013.

📒 Files selected for processing (19)
  • .changeset/deprecate-hot-progress.md
  • .changeset/instance-publish.md
  • .changeset/readme-anchors.md
  • README.md
  • src/hot.js
  • src/index.js
  • src/middleware.js
  • src/servers/EventSourceServer.js
  • src/servers/WebSocketServer.js
  • src/utils.js
  • test/e2e/cors.test.js
  • test/e2e/live-reload.test.js
  • test/e2e/worker.test.js
  • test/hot.test.js
  • test/instance-hot-api.test.js
  • types/hot.d.ts
  • types/index.d.ts
  • types/servers/EventSourceServer.d.ts
  • types/utils.d.ts
💤 Files with no reviewable changes (1)
  • types/hot.d.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

Comment thread README.md
Three things, all consequences of the token work rather than of the
refactors on top of it.

**A `path` that already carried a `token` ended up with two.** The client
appended `&token=`, and the endpoint reads the first one — so the token
configured here was the one ignored. A fragment was mishandled the same
way: `path#frag` became `path#frag?token=…`, putting the query inside the
fragment.

Now in `client-src/utils/with-token.js`, where it can be tested: the url
is taken apart on `#` and then `?` rather than parsed with `URL`, which
would need a base and would turn a relative path into an absolute one.
Every shape it can arrive in survives — absolute url, protocol-relative,
rooted path, relative path, with or without a query or fragment.

**The "no client was injected" warning printed the token.** Infrastructure
warnings travel into CI output, and a minted token is different every run,
so printing it also invited the wrong fix — pasting a value that is
already stale. It names `token=<the token>` and points at the `token`
property instead.

**A README example read `instance.token` from a configuration that does
not ask for one**, so it served `{ token: false }`. It asks for one now.
The sentence beside it still described the per-transport defaults this
option no longer has.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
@alexander-akait alexander-akait changed the title feat(hot): require a secret on the endpoint, with hot.token feat(hot): a secret on the endpoint, a file per transport, and publish Oct 2, 2026
`hot.client.token` was in the schema and serialized into the browser
query, but missing from the `HotClientOptions` typedef, so TypeScript
rejected it as an excess property:

    error TS2353: Object literal may only specify known properties, and
    'token' does not exist in type 'HotClientOptions'.

The test that keeps the schema and the client source to one set of names
now covers the typedef too. That was the third place holding this list and
the easiest to forget, which is how the omission shipped; removing the
line again fails the test by name.

The `publish` example published on every `ProgressPlugin` tick. The prose
beside it said the caller now owns both rounding the percent and dropping
a tick that repeats one, and then the example only rounded — so anyone
copying it sent a message per callback, most of them repeating what the
last one said. The example, the deprecation warning and the changeset all
show both now.

Also stopped overselling the no-clients short-circuit: it spares a caller
from asking whether anyone is listening, but with a page open every call
is still a message, so keeping a chatty source down to what changed stays
the caller's job.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

Copy link
Copy Markdown
Member Author

On the outside-diff finding — token missing from HotClientOptions

This one was raised in two review bodies rather than as an inline thread, so it has nowhere to be replied to. Answering here, because it was the only genuine bug of the set and it was flagged twice before I acted on it.

It is real. src/options.json accepts hot.client.token and src/utils.js serializes it into the browser query, but the typedef omitted it, so an inline configuration failed excess-property checking. Measured rather than assumed:

probe.ts(7,41): error TS2353: Object literal may only specify known properties,
and 'token' does not exist in type 'HotClientOptions'.

A control line using name — a sibling option that is declared — type-checked in the same file, which rules out the probe being wrong about the import. Both lines pass now.

What is worth more than the fix. This repo already has a test holding hot.client's names to one set, reading the schema and the client's setOverrides from source so it cannot drift:

A name the client acts on that the schema refuses is an option with no node spelling; one the schema takes that the client ignores silently does nothing; two names for one setting is an alias. Both sides are read from their own source, or this would just be a third place to forget.

The typedef was that third place, and it is where the published declarations are generated from — so the one list nobody checked is the one TypeScript users see. It is covered now, from source like the other two. Removing the line again fails the test naming - "token", so it is not vacuous.

Verified

  • npm run lint — clean (eslint, prettier, cspell, tsc, client types, schema-check)
  • unit — 7097 passed, 20 suites
  • e2e — 167 passed, 14 suites, 20 snapshots

Generated by Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: be684230-882e-4a31-ac20-e4b7b174faf6

📥 Commits

Reviewing files that changed from the base of the PR and between 95294b9 and b937c07.

📒 Files selected for processing (5)
  • .changeset/deprecate-hot-progress.md
  • README.md
  • src/hot.js
  • test/inject-client.test.js
  • types/hot.d.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • .changeset/deprecate-hot-progress.md
  • README.md

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.

Comment thread src/hot.js
// TODO in the next major release remove `progress` and this warning
if (options.progress) {
logger.warn(
"The 'hot.progress' option is deprecated and will be removed in the next major release. Measuring a build is the server's call, not the middleware's: a server that applies 'ProgressPlugin' itself — webpack-dev-server does — ends up with two of them on one compiler. Apply it yourself and hand what it reports to the middleware's 'publish' method, rounding the percent and dropping a tick that repeats one as this option did for you — the example is at https://github.com/webpack/webpack-dev-middleware#publishpayload. Until then this keeps working.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Tell users to remove hot.progress before adding a replacement plugin.

If hot.progress remains enabled, Line 565 also installs a ProgressPlugin. A user who follows this instruction and adds their own plugin can register two plugins and publish duplicate progress events. State that users must remove hot.progress before adding their own plugin.

@alexander-akait
alexander-akait merged commit 50441b4 into main Oct 2, 2026
24 checks passed
@alexander-akait
alexander-akait deleted the feat/hot-token branch October 2, 2026 09:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant