@browsercore/tls
v0.3.0
Published
TLS 1.3 (and 1.2 fallback) client implemented entirely in TypeScript. Depends on @browsercore/transport and @browsercore/crypto — never node:crypto directly.
Downloads
919
Readme
@browsercore/tls
A TLS 1.3 client implemented entirely in TypeScript. TLS 1.2 fallback is intentionally not implemented — requesting a TLS 1.2-only handshake is rejected up front with a typed error rather than failing silently mid-flight.
Responsibility
Owns the full TLS handshake, record layer, key schedule, and X.509 certificate
validation. Provides an encrypted byte stream over an existing
@browsercore/transport connection so higher layers never touch plaintext on the wire.
What it does NOT know about
- HTTP (any version)
- Browser fingerprints
- Cookies
It knows about byte streams (@browsercore/transport) and cryptographic primitives
(@browsercore/crypto). It never imports node:crypto directly — that boundary
is @browsercore/crypto's job, which keeps the crypto backend replaceable.
Public API
import { connect } from "@browsercore/transport";
import { connectTls, resolveProfile, TlsHandshakeError } from "@browsercore/tls";
const transport = await connect({ host: "example.com", port: 443 });
const tls = await connectTls({
transport,
serverName: "example.com",
profile: resolveProfile("modern-tls13", "example.com"),
alpnProtocols: ["h2", "http/1.1"],
handshakeTimeoutMs: 10_000,
});
const response = await tls.read();
await tls.write(new TextEncoder().encode("GET / HTTP/1.1\r\n"));
await tls.close();Types
| Export | Kind | Purpose |
| --- | --- | --- |
| TlsConnection | interface | Public contract higher layers depend on |
| connectTls() | function | Perform the TLS handshake over a transport |
| TlsConnectionImpl | class | Concrete connection (thin coordinator over ./connection/ modules) |
| TlsState | discriminated union | connecting \| handshaking \| open \| closed |
| CloseReason | discriminated union | Why a TLS connection closed |
| ProtocolVersion | discriminated union | TLS 1.2 / TLS 1.3 with wire codes |
| CipherSuite | string-literal union | Negotiated AEAD + hash |
| ClientHelloConfig | interface | ClientHello configuration (placeholder for @browsercore/profiles) |
| TlsProfile | interface | Named, reusable ClientHello config |
| resolveProfile() | function | Look up a profile by name and fill in serverName |
| TlsError | class | Base typed error |
| TlsHandshakeError | class | Handshake failure at a specific phase |
| TlsDecryptError | class | Record decryption / auth failure |
| TlsAlertError | class | TLS alert with level + description |
Dependency graph
@browsercore/tls
├─ @browsercore/transport
└─ @browsercore/cryptoNo other @browsercore/* packages are imported. Shared build, lint,
and test config comes from @browsercore/dev (see Development).
Architecture
The package is a thin coordinator + focused pure-function modules. The
connection class (TlsConnectionImpl) owns mutable state (read buffer,
transcript, traffic secrets, sequence counters) and the public surface
(handshake / read / write / close / on). Every byte-level computation
lives in a module under src/connection/ and is written as functions over
explicit inputs, so the protocol logic is unit-testable without a live
connection.
| Module | Responsibility |
| --- | --- |
| src/record/record.ts | Record header parse/serialize, AEAD encrypt/decrypt (delegates to @browsercore/crypto) |
| src/handshake/client-hello.ts | ClientHello builder (SNI, supported_versions, key_share, signature_algorithms, ALPN) |
| src/handshake/server-hello.ts | ServerHello parser — validates cipher suite + version |
| src/handshake/state-machine.ts | Handshake phase state machine with phase-tagged errors |
| src/crypto/keySchedule.ts | TLS 1.3 key schedule (RFC 8446 §7.1): HKDF-Expand-Label, traffic secrets |
| src/certificates/ | X.509 DER parse, hostname validation (RFC 6125), chain verification |
| src/extensions/ | Extension types, parsers, and wire encoders |
| src/connection/handshake-driver.ts | Handshake choreography (what to send, what to read, when to derive) |
| src/connection/record-layer.ts | TLS 1.3 inner-content-type wrapping, nonce XOR, record framing |
| src/connection/key-exchange.ts | (EC)DHE shared secret, transcript hash, server Finished verification |
| src/connection/handshake-messages.ts | EncryptedExtensions/Certificate/Finished parsing, client Finished builder |
| src/connection/lifecycle.ts | Timeouts, alerts, state transitions, post-handshake record dispatch |
Errors all carry a kind discriminator so callers can narrow and inspect
without leaking backend specifics (TlsHandshakeError(phase),
TlsDecryptError, TlsAlertError(level, description)).
Not implemented
- TLS 1.2 fallback — the client speaks TLS 1.3 only
- Post-handshake messages (NewSessionTicket, KeyUpdate)
- Session resumption / PSK / 0-RTT
- Mutual TLS (client certificate)
- Certificate compression
- HelloRetryRequest
- Additional key-share groups beyond X25519
Development
This repo shares tooling with the @browsercore/* family via
@browsercore/dev. That package
is the single source of truth for:
tsconfigstrict flags —tsconfig.jsonextends@browsercore/dev/tsconfig.base.jsonvitestconfig —vitest.config.tsimportsdefinePackageConfigfrom@browsercore/dev/vitestoxlintconfig —oxlint.config.tsimports the base ruleset from@browsercore/dev/oxlintcoverage-mdbin — shipped via@browsercore/dev'sbin/, used asnpx coverage-md
@browsercore/dev is declared in devDependencies as "file:../dev" for local
development (the monorepo layout). When installing from npm, consumers resolve the
published version.
Scripts
npm run typecheck # tsc --noEmit (strict mode — noUncheckedIndexedAccess, exactOptionalPropertyTypes)
npm run lint # oxlint --type-aware src/ (tests excluded)
npm test # vitest run (in-process fixture server, no real network)
npm run build # tsc -p tsconfig.build.json → dist/
npx coverage-md # write COVERAGE.md + coverage/badge.json from coverage-summary.jsonRun a single test file:
npx vitest run tests/handshake.test.tsRun tests by name pattern:
npx vitest run -t "rejects a non-browser User-Agent"Lint config
Linting is type-aware oxlint via oxlint.config.ts. The file extends the shared
@browsercore/dev/oxlint base (which enables the typescript, unicorn, import,
promise, and node plugins with correctness/suspicious/pedantic as errors).
The old .oxlintrc.json has been removed.
Private fields use the native TypeScript private keyword — the legacy
_-prefixed naming convention has been fully migrated away, so no
no-underscore-dangle allowlist is required.
Requires Node >= 26. ESM only ("type": "module").
