# 2026-07-16 — v0.8.0: client/transport rework + handshake hardening, auth profiles, cert/MFA removal

The 0.7 and 0.8 work, read as one set of changes. 0.7.x was cut in a hurry to
unblock an external review and was never checked against the spec end to end —
treat those tags as pre-release. **0.8.0 is the first release to pass a full
conformance pass against `spec/protocol.md`**; the hardening landed across several
independent review rounds (auth-surface design review, cross-language port audit,
two neutral security passes), every finding re-verified in shipped code and pinned
by a regression test.

Do **not** assume wire compatibility with pre-0.8.0 tags — only 0.8.0 is
spec-conformant. Test suite grown to 298.

## Client & transport semantics (0.7)

Behavioural rework of the client and channel layer — separate from the 0.8
security pass below.

- **BREAKING — auto-retry removed.** The client no longer silently re-sends a
  call. A sent request that times out rejects with `RPCAbortedError` and leaves
  the outcome **UNKNOWN** — the caller decides whether replaying is safe. A plain
  local `RPCError` means the request provably never left.
- **BREAKING — deferred reset.** The session resets only on a *sent* call that
  gets `RPCAbortedError("TIMEOUT")`; guardrail rejections (`CHANNEL`, `ABORTED`,
  queued-frame failures) no longer tear down and re-handshake the session.
- **Replay window.** Server keeps a bounded seen-nonce set (configurable
  `replayWindow`, default 4096) so a captured frame can't be replayed within the
  window.
- **`abortPending`.** New client API to abort all in-flight calls at once (each
  rejects by its sent-status class), on top of per-call `AbortSignal`.
- **Make-before-break session continuity.** A new handshake is negotiated as a
  candidate while the live session keeps serving; the old session is only
  replaced once the candidate is confirmed, so a re-handshake doesn't drop
  in-flight work. Handshake states are resilient across transient channel gaps.
- **Reconnecting channel adapters.** WebSocket and socket adapters reconnect and
  the session survives transport loss — a call issued while the channel is down
  completes after recovery without a re-handshake.
- **BREAKING — default `sendTimeout` 10s → 3s** for faster failure detection of an
  unsent frame.
- **Port-complete spec + first KAT vectors.** `spec/protocol.md`,
  `spec/security.md` assessment, and the initial known-answer test vectors landed
  here — the groundwork the 0.8.0 conformance pass later verified end to end.

## Security

### Handshake

- **High — candidate phase now has its own absolute deadline.** A candidate
  session could be promoted after its confirmation window elapsed. Server stamps
  `candidateDeadline` on install and checks it before `promoteCandidate()`;
  companion fix drops a double-`onError` report on one failed attempt (`reported`
  flag).
- **Synchronous auth callbacks can no longer overrun `handshakeTimeout`.** Timer +
  boolean let a synchronous `verify`/`secret`/`sign` win the race (post-`await`
  continuation is a microtask, runs before the timer). Now an absolute `Date.now()`
  deadline is checked after every auth callback and before candidate install,
  session publish, and reply send — both peers.
- **Candidate promotion split from payload decoding.** An AEAD-authenticated frame
  with malformed inner msgpack used to fail the whole decrypt — candidate not
  promoted, timer not cleared, nonce not recorded — stalling a valid peer to
  timeout. AEAD opening (`createAeadOpener`) is now separate from
  `decodePlaintext`; candidate is promoted and nonce recorded the moment Poly1305
  verifies, regardless of inner payload shape.
- **`maxPendingHandshakes` caps concurrent in-flight handshakes** so a peer with
  never-settling auth callbacks can't accumulate unbounded pending work.
- **Failed handshake-reply send drops the candidate** immediately (when
  `candidateEpoch` still matches) instead of lingering to its timer; exactly one
  error reported.
- **Low — handshake epoch exhaustion is a terminal client error.** No ceiling
  guard on the client epoch counter; past `0xffffffff` every hello would be
  silently dropped server-side as `Invalid epoch`, surfacing only as opaque
  timeouts. `startHandshake` now rejects with `RPCError("CLIENT", "Handshake epoch
  exhausted; destroy and recreate client")`. Unreachable in practice
  (~4.3 × 10⁹ handshakes); no wire/reuse impact.

### Crypto / secrets

- **Medium-High — all-zero secret rejected at any length.** Guard only covered 32
  bytes. `isEmptySecret` now rejects any-length all-zero buffers;
  `deriveSessionSecret` throws `TypeError("secret must not be all-zero")`.

### Replay / framing

- **Reflected `t: 2` frames no longer consume replay slots.** A direction guard
  runs before the nonce is recorded, closing a replay-window-narrowing reflection.
- Response-framing size limits tightened in the same pass.

### Input validation

- **Bigints beyond 64 bits rejected** (`INVALID_DATA`) instead of round-tripping a
  corrupted value; `BIGINT_MIN` / `BIGINT_MAX` exported.
- **Cyclic outbound graphs rejected** (`WeakSet` detection) instead of recursing to
  a stack overflow; non-cyclic repeated references still rebuilt independently.
- **`NaN` / `Infinity` no longer disable limits.** `maxPending`,
  `maxMessageBytes`, JWT `maxAge` validated as finite integers at construction.
  `maxMessageBytes: NaN` used to make `len > max` always false — trivially reached
  via `Number(process.env.X)`.

### Middleware

- **Middleware returning without calling `next()` fails closed** (`RPCError
  ("MIDDLEWARE", ...)`) — previously skipped the handler but returned success.
  Completion guarded on synchronous throw and bare return.
- **Fire-and-forget `next()` is no longer a supported pattern** — a middleware
  that completes while its `next()` promise is still pending fails closed with
  `RPCError("MIDDLEWARE", ...)` instead of replying with the middleware's own
  value while the handler outcome is silently dropped (inspired by tRPC's
  return-next guard — "did you forget to `return next()`?" — which uses a
  runtime envelope/marker rather than settlement tracking). A synchronous
  downstream throw is normalized into a rejected `next()` promise so a
  catch-fallback middleware is not misclassified. The detached downstream
  promise is still observed internally, so its rejection can never surface as
  an `unhandledRejection` and terminate the process.

### Auth payloads

- Auth payloads pass the full `sanitize()` gate and are version-checked; malformed
  or unversioned payloads rejected as `UNAUTHORIZED` before reaching app `verify`.

## Added

- **Normative auth profiles: JWT, Ed25519, ECDSA (P-256).** Each has a versioned
  wire schema (`v` field, unknown version rejected server-side) and published KAT
  vectors in `spec/protocol.md` / `test/unit/vectors.test.ts` — cross-language
  ports interoperate byte-for-byte.
- **`maxPendingHandshakes`** server option.
- **`BIGINT_MIN` / `BIGINT_MAX`** exports.
- **Port-complete spec + KAT vectors** — 44-item implementation checklist and full
  vectors (keys, proof, both transcripts, three auth-profile payloads) cross-checked
  spec ↔ tests ↔ implementation.

## Changed

- **BREAKING — Node floor `>=20.19.0`.** `package.json` declared a range the build
  and `@noble/hashes` 2.x never supported; declaration now matches reality. Node
  < 20.19.0 unsupported, not CI-tested.
- Spec/docs realigned with code: JWT credential visibility in the opening frame
  called out, asymmetric examples authenticate both directions, `protocol.md`
  context-error contract matches `server.ts`, malformed-envelope /
  stale-and-unknown-response-id handling pinned, sent-boundary for async adapters
  clarified.

## Removed

- **BREAKING — certificate + multifactor auth helpers deleted**
  (`createCertificateServerAuth`, `createMultifactorServerAuth`) with tests. The
  certificate helper was ECDSA verification under a name promising
  chain/expiry/revocation it never did (all in the app callback). The multifactor
  helper couldn't enforce same-principal binding — default `{ ...primary,
  ...secondary }` merge passed two different principals as MFA, and spread order
  could let an issuer-signed JWT claim overwrite a cryptographically verified
  field. Use JWT/Ed25519/ECDSA profiles or a custom `verify`; `spec/security.md`
  documents certificate and two-factor-with-binding recipes.

## Fixed

- Nested `undefined` in outbound values no longer diverges from the declared type
  contract.
- Reserved route/middleware names can no longer be registered.
- Malformed RPC responses (bad `ok`, absent/garbage `c`/`m`) dropped or coerced
  strictly per spec.

## Dependencies

- **`puppeteer` moved to `devDependencies`** — was in `optionalDependencies`
  (installs by default for consumers), dragging `ws` (high/moderate) and `js-yaml`
  (moderate) into production `npm audit`. Production audit now clean.
- `ws` bumped to `^8.21.0`.

## Verification

- Full gate green: 39 files, 298 tests; lint, typecheck, ESM+CJS build clean
  (Node 22.22.1).
- Every finding re-verified in shipped code (not on commit-message trust) and
  mapped to a regression test; KAT vectors byte-identical across spec/tests/impl.
- Full audit trail: `review/0.8/review-release-verdict-0.8.0.md`.
