Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
21f3c3e
feat: Implement SecureReq class for enhanced HTTPS requests
piquark6046 Apr 3, 2026
d06599b
fix: standardize HTTP protocol casing to lowercase in documentation a…
piquark6046 Apr 3, 2026
e1ea56b
feat: add timeout and abort signal support to SecureReq requests; enh…
piquark6046 Apr 3, 2026
bebf916
fix: increase timeout values for tests and server responses to improv…
piquark6046 Apr 3, 2026
d476cb9
fix: correct path for type definitions in package.json
piquark6046 Apr 3, 2026
695f598
feat: Enhance HTTP/2 support and automatic probing
piquark6046 Apr 3, 2026
5e77f59
feat: Implement secure transport negotiation for HTTP/2 and enhance r…
piquark6046 Apr 3, 2026
0228e66
feat: Add CreateTestClient function and secure connection count track…
piquark6046 Apr 3, 2026
aa1d284
docs: clarify http/3 placeholder preference and reuse negotiated TLSS…
piquark6046 Apr 3, 2026
25ffe86
test: add comprehensive test suite for SecureReq, covering timeouts, …
piquark6046 Apr 3, 2026
812f80c
feat: add support for following redirects with configurable limits in…
piquark6046 Apr 3, 2026
88e72d4
docs: update README for clarity on ExpectedAs parameter behavior
piquark6046 Apr 3, 2026
ed93611
refactor: remove global SecureReq instance and proxy, simplify export…
piquark6046 Apr 3, 2026
d7c28ff
docs: add `SimpleSecureReq` usage examples and API reference to README
piquark6046 Apr 3, 2026
d3a78fa
feat: enhance error handling for TLSv1.2 curve mismatches and add rel…
piquark6046 Apr 3, 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
17 changes: 17 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,23 @@ on:
branches: [ "**" ]

jobs:
build:
name: Run build
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
steps:
- name: Set up NodeJS
uses: actions/setup-node@v6
with:
node-version: 'lts/*'
- name: Checkout repository
uses: actions/checkout@v6
- name: Install dependencies
run: npm i
- name: Run build
run: npm run build
eslint:
name: Run ESLint
runs-on: ubuntu-latest
Expand Down
129 changes: 104 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
# SecureReq 🔐

**SecureReq** is a lightweight TypeScript utility for making secure HTTPS requests with strict TLS defaults and typed response parsing.
**SecureReq** is a lightweight TypeScript utility for secure HTTP requests with strict TLS defaults, automatic http/1.1 to http/2 negotiation, streaming I/O, and typed response parsing.

---

## 🚀 Quick Summary

- **Small, dependency-light** wrapper around Node's `https` for typed responses and safer TLS defaults.
- **Class-first** API that probes each origin with `http/1.1` first, then upgrades future requests to `http/2` when appropriate.
- Also exposes `SimpleSecureReq`, a shared client instance for one-off requests without manual construction.
- Automatic HTTP/2 probing is conservative: only safe body-less auto requests are retried from negotiation failure to `http/1.1`.
- Supports **response compression** with `zstd`, `gzip`, and `deflate`.
- Supports optional **redirect following** with configurable redirect limits.
- Supports **streaming uploads and streaming downloads**.
- Defaults to **TLSv1.3**, Post Quantum Cryptography key exchange, a limited set of strongest ciphers, and a `User-Agent` header.
- Supports typed response parsing: `JSON`, `String`, or raw `ArrayBuffer`.

---

Expand All @@ -24,36 +28,89 @@ npm install @typescriptprime/securereq

## Usage Examples 🔧

Import and call the helper:
Create a client and reuse it per origin:

```ts
import { HTTPSRequest } from '@typescriptprime/securereq'
import { Readable } from 'node:stream'
import { SecureReq } from '@typescriptprime/securereq'

const client = new SecureReq()

// First request to an origin uses http/1.1 probing.
const first = await client.Request(new URL('https://api64.ipify.org?format=json'), {
ExpectedAs: 'JSON',
})

// Later safe requests to the same origin can probe and establish http/2 automatically.
const second = await client.Request(new URL('https://api64.ipify.org?format=json'), {
ExpectedAs: 'JSON',
})

console.log(first.Protocol) // 'http/1.1'
console.log(second.Protocol) // 'http/2' when available after the safe probe

// Follow redirects automatically
const redirected = await client.Request(new URL('https://example.com/old-path'), {
ExpectedAs: 'String',
FollowRedirects: true,
MaxRedirects: 5,
})

console.log(redirected.Body)

// Stream upload + stream download
const streamed = await client.Request(new URL('https://example.com/upload'), {
HttpMethod: 'POST',
Payload: Readable.from(['chunk-1', 'chunk-2']),
ExpectedAs: 'Stream',
})

for await (const chunk of streamed.Body) {
console.log(chunk)
}
```

For quick one-off requests, you can use the exported shared client:

// JSON (auto-detected by .json path) or explicit
const url = new URL('https://api64.ipify.org?format=json')
const res = await HTTPSRequest(url)
console.log(res.StatusCode) // number
console.log(res.Body) // ArrayBuffer or parsed JSON depending on `ExpectedAs` and URL
```ts
import { SimpleSecureReq } from '@typescriptprime/securereq'

// Force string
const html = await HTTPSRequest(new URL('https://www.example.com/'), { ExpectedAs: 'String' })
console.log(typeof html.Body) // 'string'
const response = await SimpleSecureReq.Request(new URL('https://api64.ipify.org?format=json'), {
ExpectedAs: 'JSON',
})

// Force ArrayBuffer
const raw = await HTTPSRequest(new URL('https://example.com/'), { ExpectedAs: 'ArrayBuffer' })
console.log(raw.Body instanceof ArrayBuffer)
console.log(response.Body)
```

---

## API Reference 📚

### HTTPSRequest(Url, Options?)
### `new SecureReq(Options?)`

- Recommended entry point.
- Keeps per-origin capability state:
- first request is sent with `http/1.1`
- `Accept-Encoding: zstd, gzip, deflate`
- later safe requests can probe `http/2`, and capability updates only reflect observed protocol/compression evidence
- `Close()` closes cached http/2 sessions.
- `OriginCapabilityCacheLimit` bounds remembered origin capability entries with LRU-style eviction.
- Invalid constructor options fail fast during initialization.

### `SimpleSecureReq`

- An exported shared `SecureReq` instance for simple or occasional requests.
- Useful when you do not need to manage your own client lifecycle manually.
- Supports the same `.Request()`, `.GetOriginCapabilities()`, and `.Close()` methods as a manually created `SecureReq` instance.

### `client.Request(Url, Options?)`

- `Url: URL` — Target URL (must be an instance of `URL`).
- `Options?: HTTPSRequestOptions` — Optional configuration object.

Returns: `Promise<HTTPSResponse<T>>` where `T` is determined by `ExpectedAs`.
Returns:
- If `ExpectedAs` is specified, `Promise<HTTPSResponse<T>>`
- If `ExpectedAs` is omitted, `Promise<HTTPSResponse<unknown>>`

Throws:
- `TypeError` when `Url` is not a `URL` instance.
Expand All @@ -62,33 +119,55 @@ Throws:
### HTTPSRequestOptions

Fields:
- `TLS?: { IsHTTPSEnforced?: boolean, MinTLSVersion?: 'TLSv1.2'|'TLSv1.3', MaxTLSVersion?: 'TLSv1.2'|'TLSv1.3', Ciphers?: string[], KeyExchanges?: string[] }`
- `TLS?: { IsHTTPSEnforced?: boolean, MinTLSVersion?: 'TLSv1.2'|'TLSv1.3', MaxTLSVersion?: 'TLSv1.2'|'TLSv1.3', Ciphers?: string[], KeyExchanges?: string[], RejectUnauthorized?: boolean }`
- Defaults: `IsHTTPSEnforced: true`, both Min and Max set to `TLSv1.3`, a small secure cipher list and key exchange choices.
- When `IsHTTPSEnforced` is `true`, a non-`https:` URL will throw.
- `KeyExchanges` is forwarded to Node.js as the TLS supported groups / curve list. For strict `TLSv1.2` + ECDSA servers, overly narrow values such as only `X25519` may fail; include a compatible certificate curve such as `P-256` when needed.
- `HttpHeaders?: Record<string,string>` — Custom headers. A `User-Agent` header is provided by default.
- `ExpectedAs?: 'JSON'|'String'|'ArrayBuffer'` — How to parse the response body.
- `HttpMethod?: 'GET'|'POST'|'PUT'|'DELETE'|'PATCH'|'HEAD'|'OPTIONS'`
- `Payload?: string | ArrayBuffer | Uint8Array | Readable | AsyncIterable`
- `ExpectedAs?: 'JSON'|'String'|'ArrayBuffer'|'Stream'` — How to parse the response body.
- Omitting `ExpectedAs` keeps the runtime extension heuristic (`.json`, `.txt`, fallback `ArrayBuffer`) but the body type is intentionally `unknown`.
- `PreferredProtocol?: 'auto'|'http/1.1'|'http/2'|'http/3'`
- `http/3` is currently a placeholder preference and uses the same TCP/TLS negotiation path as `http/2` until native HTTP/3 transport is added.
- `EnableCompression?: boolean` — Enables automatic `Accept-Encoding` negotiation and transparent response decompression.
- `FollowRedirects?: boolean` — Follows redirect responses with a `Location` header.
- `MaxRedirects?: number` — Maximum redirect hops when `FollowRedirects` is enabled. Default: `5`.
- `TimeoutMs?: number` — Aborts the request if headers or body transfer exceed the given number of milliseconds.
- `Signal?: AbortSignal` — Cancels the request using a standard abort signal.

### HTTPSResponse

- `{ StatusCode: number, Headers: Record<string,string|string[]|undefined>, Body: T }`
- `{ StatusCode: number, Headers: Record<string,string|string[]|undefined>, Body: T, Protocol: 'http/1.1'|'http/2', ContentEncoding: 'identity'|'zstd'|'gzip'|'deflate', DecodedBody: boolean }`

Notes:
- If `ExpectedAs` is omitted, a heuristic is used: `.json` → `JSON`, `.txt` → `String`, otherwise `ArrayBuffer`.
- If `ExpectedAs` is omitted, a heuristic is still used at runtime: `.json` → `JSON`, `.txt` → `String`, otherwise `ArrayBuffer`.
- Because omitted `ExpectedAs` may produce different runtime body shapes, the TypeScript return type is `unknown`. Prefer explicit `ExpectedAs` in application code.
- When `ExpectedAs` is `JSON`, the body is parsed and an error is thrown if parsing fails.
- When `ExpectedAs` is `Stream`, the body is returned as a Node.js readable stream.
- Redirects are returned as-is by default. Set `FollowRedirects: true` to follow them.
- `301`/`302` convert `POST` into `GET`, `303` converts non-`HEAD` methods into `GET`, and `307`/`308` preserve method and payload.
- Redirects that require replaying a streaming payload are rejected instead of silently re-sending the stream.

---

## Security & Behavior Notes 🔐

- Strict TLS defaults lean on **TLSv1.3** and a reduced cipher list to encourage secure transport out of the box.
- TLS options are forwarded to Node's HTTPS layer (`minVersion`, `maxVersion`, `ciphers`, `ecdhCurve`).
- TLS options are forwarded to Node's HTTPS or http/2 TLS layer (`minVersion`, `maxVersion`, `ciphers`, `ecdhCurve`).
- When SecureReq performs an ALPN probe for HTTPS, the negotiated `TLSSocket` is reused for the actual `http/2` or `http/1.1` request instead of opening a second TLS connection.
- The library uses `zod` for runtime validation of options.
- Compression negotiation is origin-scoped. Subdomains are tracked independently.
- `GetOriginCapabilities().PreferredProtocol` is updated from actual observed transport, and automatic fallback only occurs for safe negotiation failures before request bytes are sent.
- `GetOriginCapabilities().SupportedCompressions` is only narrowed when the response provided actual compression evidence.
- `GetOriginCapabilities().PreferredProtocol` reflects the currently usable transport (`http/1.1` or `http/2`), while `HTTP3Advertised` records whether the origin advertised `h3`.
- http/3 advertisement points are recorded from response headers, but Node.js built-in http/3 transport is not yet used.

---

## Development & Testing 🧪

- Build: `npm run build` (uses `esbuild` + `tsc` for types)
- Build: `npm run build` (uses `tsc -p sources/tsconfig.json`)
- Test: `npm test` (uses `ava`)
- Lint: `npm run lint`

Expand All @@ -102,4 +181,4 @@ Contributions, bug reports and PRs are welcome — please follow the repository'

## License

This project is licensed under the **Apache-2.0** License. See the `LICENSE` file for details.
This project is licensed under the **Apache-2.0** License. See the `LICENSE` file for details.
4 changes: 2 additions & 2 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ const config = [
"quotes": ["error", "single"],
"@typescript-eslint/no-unused-vars": "warn",
'@typescript-eslint/naming-convention': ['error', {
selector: ['variableLike', 'parameterProperty', 'classProperty', 'typeProperty'],
selector: ['variableLike', 'parameterProperty', 'classProperty', 'typeProperty', 'classMethod'],
format: ['PascalCase']
}]
}
}
]

export default config
export default config
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@
"exports": {
".": {
"node": "./dist/index.js",
"types": "./dist/types/index.d.ts"
"types": "./dist/index.d.ts"
}
},
"types": "./dist/types/index.d.ts",
"types": "./dist/index.d.ts",
"files": [
"dist/**/*",
"LICENSE",
Expand Down
22 changes: 22 additions & 0 deletions sources/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as Process from 'node:process'
import * as TLS from 'node:tls'
import type { HTTPCompressionAlgorithm, HTTPMethod, HTTPSRequestOptions } from './type.js'

export const DefaultTLSOptions = {
IsHTTPSEnforced: true,
MinTLSVersion: 'TLSv1.3',
MaxTLSVersion: 'TLSv1.3',
Ciphers: ['TLS_AES_256_GCM_SHA384', 'TLS_CHACHA20_POLY1305_SHA256'],
KeyExchanges: ['X25519MLKEM768', 'X25519'],
RejectUnauthorized: true,
} as const satisfies NonNullable<HTTPSRequestOptions['TLS']>

export const DefaultHTTPHeaders = {
'user-agent': `node/${Process.version} ${Process.platform} ${Process.arch} workspace/false`,
} as const

export const DefaultSupportedCompressions: HTTPCompressionAlgorithm[] = ['zstd', 'gzip', 'deflate']
export const ConnectionSpecificHeaders = new Set(['connection', 'host', 'http2-settings', 'keep-alive', 'proxy-connection', 'te', 'transfer-encoding', 'upgrade'])
export const PayloadEnabledMethods = new Set<HTTPMethod>(['GET', 'POST', 'PUT', 'PATCH', 'OPTIONS'])
export const AutomaticHTTP2ProbeMethods = new Set<HTTPMethod>(['GET', 'HEAD'])
export const AvailableTLSCiphers = new Set(TLS.getCiphers().map(Cipher => Cipher.toLowerCase()))
Loading
Loading