Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ee2f1de
refactor: remove certificate-based and multifactor authentication hel…
Cyace84 Jul 14, 2026
73f9d3a
refactor: remove certificate and multifactor authentication tests
Cyace84 Jul 14, 2026
a8027d3
feat: add auth profile payloads for jwt, ed25519, and ecdsa with tests
Cyace84 Jul 14, 2026
b13d697
fix: clarify sent boundary handling for async adapters and update doc…
Cyace84 Jul 14, 2026
10cbf74
Refactor protocol specifications and enhance error handling
Cyace84 Jul 15, 2026
c07d36d
refactor: improve formatting and clarity in protocol documentation
Cyace84 Jul 15, 2026
059320f
fix(protocol): align validation and middleware behavior with spec
Cyace84 Jul 15, 2026
15e83d5
fix(sanitize): reject cyclic outbound graphs; close Batch 4 spec ambi…
Cyace84 Jul 15, 2026
6e45c1d
fix(protocol): clarify handshake failure conditions and enhance direc…
Cyace84 Jul 15, 2026
36ebeaa
fix(server): guard middleware completion on sync throw and return
Cyace84 Jul 15, 2026
48ae07e
fix: harden handshake deadline, sanitize, replay, and secret guards
Cyace84 Jul 15, 2026
726e928
Merge branch 'main' of github.com:dotexorg/saferpc into 0.8.0-fixes
Cyace84 Jul 16, 2026
9428901
fix: enhance security checks for replay protection and response frami…
Cyace84 Jul 16, 2026
d0edc02
fix: implement maxPendingHandshakes to limit concurrent handshake att…
Cyace84 Jul 16, 2026
0afc5b9
fix: handle handshake epoch exhaustion to prevent silent failures
Cyace84 Jul 16, 2026
f367dca
fix: update changelog for v0.8.0 release, detailing client/transport …
Cyace84 Jul 16, 2026
4ae81cf
feat: Add review documents and tests for 0.8.0 release
Cyace84 Jul 16, 2026
7cec0e6
fix: enforce middleware contract by rejecting fire-and-forget next() …
Cyace84 Jul 16, 2026
1673991
fix: enhance middleware error handling for synchronous throws and upd…
Cyace84 Jul 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,3 @@ node_modules/
/*.js.map
/build
*.tgz
review-*
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ npm install @dotex/saferpc
- **Typed procedures** with Zod input/output validation
- **End-to-end encryption.** X25519 ECDH, XSalsa20-Poly1305 AEAD, HKDF-SHA-256, with forward secrecy by design
- **Lazy handshake** on the first call. Lazy re-handshake on the next call when the session drops — failures surface with a typed code, never silently resent
- **Three auth modes:** pre-shared secret, asymmetric (Ed25519 / ECDSA / JWT / cert / multifactor), or both for defense-in-depth
- **Three auth modes:** pre-shared secret, asymmetric (Ed25519 / ECDSA / JWT), or both for defense-in-depth
- **Synchronous** `client()` and `server()`. Runs in Node.js, browsers, Service Workers, React Native, Vercel Edge, Cloudflare Workers, Deno Deploy
- **Tiny surface.** `@noble/*` crypto, `@msgpack/msgpack`, `zod`, and nothing else
- **Pure ESM + CJS dual build**, side-effect-free, tree-shakeable
Expand Down Expand Up @@ -102,7 +102,7 @@ auth: {
}
```

Built-in helpers cover Ed25519, ECDSA P-256, JWT, certificate-based, and multifactor auth. All bind their proof to the handshake transcript, so a captured payload cannot be replayed into a new session. The full threat model lives in [spec/security.md](./spec/security.md).
Built-in helpers cover Ed25519, ECDSA P-256, and JWT auth. All bind their proof to the handshake transcript, so a captured payload cannot be replayed into a new session. Certificate-based and multi-factor schemes are composed from `sign`/`verify` directly — recipes and the full threat model live in [spec/security.md](./spec/security.md).

## Errors

Expand Down Expand Up @@ -137,7 +137,7 @@ src/
auth/
index.ts : combined re-exports (deriveSessionSecret + client + server)
client.ts : Ed25519, ECDSA, JWT client helpers
server.ts : Ed25519, ECDSA, JWT, certificate, multifactor server helpers
server.ts : Ed25519, ECDSA, JWT server helpers
index.ts : public entry point
```

Expand All @@ -158,21 +158,22 @@ import { createEd25519ServerAuth } from "@dotex/saferpc/auth/server";

## Compatibility

Node.js 18+, modern browsers, Service / Web / Shared Workers, React Native, Vercel Edge, Cloudflare Workers, Deno Deploy. WebCrypto is required only for the ECDSA and certificate helpers.
Node.js 20.19+ (the `@noble/*` dependencies require `>=20.19.0`; older 18.x / 20.18 fail to load the ESM-only crypto packages under CommonJS), modern browsers, Service / Web / Shared Workers, React Native, Vercel Edge, Cloudflare Workers, Deno Deploy. WebCrypto is required only for the ECDSA helpers.

## Project status

`0.x` with a stable wire protocol (`saferpc-v1` HKDF info, `saferpc-hs-{hello,reply}-v1` transcript prefixes). Test coverage for handshake attacks, replay, tampering, type confusion, prototype pollution, middleware misuse, and DoS limits lives in `test/security/`. An internal line-by-line security review — including the honest list of residual risks and open issues — is published in [spec/assessment.md](./spec/assessment.md). A 1.0 release will lock the public API surface.

## Releasing

One command bumps the version, publishes to npm, and pushes the tag:
Two commands: bump the version, then publish (which also pushes the tag):

```bash
npm version patch # or: minor / major / 1.2.3-beta.0
npm publish
```

`prepublishOnly` runs lint, tests, and build before publishing. The `postversion` hook then runs `npm publish && git push --follow-tags`. The pushed `vX.Y.Z` tag triggers `.github/workflows/release.yml`, which verifies the version is live on npm and creates a GitHub Release with auto-generated changelog notes since the previous tag.
The `version` hook syncs and stages `jsr.json` into the version commit. `prepublishOnly` runs lint, tests, and build before publishing; `postpublish` runs `git push --follow-tags`. The pushed `vX.Y.Z` tag triggers `.github/workflows/release.yml`, which verifies the version is live on npm and creates a GitHub Release with auto-generated changelog notes since the previous tag.

If `npm publish` fails, the tag exists locally but is not pushed. Fix the issue and re-run `npm publish && git push --follow-tags`. To abort, run `git tag -d vX.Y.Z && git reset --hard HEAD~1`.

Expand Down
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ One file per sync round. Newest first. Each entry is dated and named by the main

| Date | What landed |
|------|-------------|
| 2026-07-16 | v0.8.0 (0.7 + 0.8 work): no-auto-retry + deferred-reset + replay-window + abortPending, make-before-break + reconnecting ws/socket channels, sendTimeout 10s→3s, port-complete spec+KAT vectors; handshake absolute deadlines + maxPendingHandshakes + epoch-exhaustion guard, normative JWT/Ed25519/ECDSA auth profiles, cert+MFA helpers removed, bigint/cyclic/NaN input validation, all-zero-secret + replay-slot + directionality fixes, middleware fire-and-forget next() fails closed + sync-throw normalized, Node ≥20.19 |

---

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# 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`.
Loading
Loading