Skip to content

feat: support strict notification subprotocol authentication - #266

Merged
argilzar merged 3 commits into
mainfrom
feat/notification-strict-subprotocol
Nov 5, 2026
Merged

argilzar merged 3 commits into
mainfrom
feat/notification-strict-subprotocol

Conversation

@argilzar

@argilzar argilzar commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Add opt-in NotificationClientOptions.allowQueryFallback: false for consumers that must never retry API keys or bearer tokens in request URLs. Existing clients retain the default compatibility fallback.

Strict mode requires negotiation of the offered authentication scheme before sending a subscription. Initial handshake errors, closes before opening, and missing/wrong scheme negotiation report a fixed observer error without starting query fallback. The SDK option must subsequently be forwarded by wrappers such as data-pump; this PR alone does not enable it in clustered push.

Review also reproduced two existing lifecycle defects now corrected here: reconnect scheduling previously kept the connect guard set, and disconnect did not invalidate asynchronous setup. Reconnect now releases the guard; generation checks prevent canceled setup and stale socket callbacks from opening or subscribing, and disconnect clears reconnect timers. In-flight HTTP/token requests are not aborted. Added regressions cover accepted/rejected replacement handshakes and cancellation during metadata fetch, socket negotiation, and reconnect backoff. The expanded fixture fails against the previous PR head and passes with these fixes.

Final quality review also found error→abnormal-close could reconnect after observer termination. The corrected terminal-error path invalidates the socket generation, clears pending retry state and reports the error once; subsequent close cannot resubscribe. Regression covers strict and compatibility modes, RED at the preceding head and GREEN at the current head. Close-only abnormal disconnects still reconnect.

Validation: the isolated transport regression fails against unchanged SDK source and passes with this change. It covers API-key and bearer success/failure paths and default fallback compatibility. Full suite: 249 pass; typecheck, lint, ESM/CJS/declaration builds and diff check pass on Bun 1.4.2. Network boundaries use synthetic fixtures; no live credentials or subscriptions.

Limits: no general logging-safety claim, no sanitization of caller-supplied endpoint URLs, no deployment or consumer activation. Draft pending independent review. Usable discovery tools were unavailable; scope follows the previously read native HTTP programme and verified SDK source.

@argilzar
argilzar marked this pull request as ready for review November 2, 2026 16:34
@ramattraomnicable

Copy link
Copy Markdown

Ramattra review

Summary: No blocking issues found. The strict-mode credential handling and the lifecycle fixes are correct, and the regression coverage is strong.

What I verified

  • [Security] Strict mode (allowQueryFallback: false) genuinely keeps credentials out of the URL: shouldFallBackToQueryTransport() short-circuits on allowQueryFallback !== false, so authTransport can never flip to "query", and buildCredentialHandshake() returns an empty query for the subprotocol transport. This holds across reconnects too, since authTransport is never mutated. The new URL(socket.url).search === "" assertions pin this down.
  • [Correctness] The generation counter is applied consistently: bumped in connect(), disconnect(), rejectStrictHandshake(), and the terminal handleWebSocketError() path; checked at every post-await point in connect() and at the top of every socket callback (onopen/onmessage/onclose/onerror). I walked the disconnect-during-metadata / -socket-setup / -backoff cases and the terminal-error-then-close cascade — in each, the stale callback is correctly ignored, so no socket opens or resubscribes after teardown, and observer.error fires exactly once.
  • [Correctness] Not resetting _isConnecting on the early generation !== this.generation returns in connect() is sound: the only thing that can advance the generation mid-connect() (before the socket exists) is disconnect(), which already set the flag to false and now owns it.
  • [Tests] The fixture drives real handshake-accepted/rejected, reconnect-revalidation, cancellation, and terminal-error scenarios against the actual client, and the matrix assertions (socket count, sent count, failure message, isConnecting/isOpen) line up with the code paths. The .test.ts wrapper asserts a clean child exit — a green-but-empty run isn't possible since it also requires the "strict notification transport passed" marker on stdout.

Notes (non-blocking)

  • fallBackToQueryTransport() calls void this.connect() without the .catch(...) that the reconnect-timer path now has. Since connect()'s try/catch rethrows on a current generation, a metadata/token fetch that rejects during the legacy fallback reconnect would surface as an unhandled promise rejection rather than an observer.error. Edge case (needs the fetch to fail precisely on the fallback attempt), but worth mirroring the reconnect path's .catch for consistency.
  • The documented limitations (in-flight HTTP/token requests not aborted; no endpoint-URL sanitization; wrappers like data-pump must forward the option) read as deliberate scope boundaries, not gaps — just flagging that clustered-push activation is explicitly out of this PR.

Automated review by Ramattra — comments only, a human decides on merge.

@argilzar
argilzar merged commit b25287a into main Nov 5, 2026
1 check passed
@argilzar
argilzar deleted the feat/notification-strict-subprotocol branch November 5, 2026 08:32
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.

3 participants