Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 6 additions & 1 deletion .github/workflows/BunClient.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
RAVENDB_TEST_SERVER_PATH: ./RavenDB/Server/Raven.Server
RAVENDB_TEST_SERVER_CERTIFICATE_PATH: ./certs/server.pfx
RAVENDB_TEST_CLIENT_CERT_PASSPHRASE: client11
RAVENDB_TEST_CLIENT_CERT_PATH: ./certs/nodejs.pem
RAVENDB_TEST_CLIENT_CERT_PATH: ${{ matrix.client-cert == 'pem' && './certs/nodejs.pem' || '' }}
RAVENDB_TEST_CA_PATH: /usr/local/share/ca-certificates/ca.crt
RAVENDB_TEST_HTTPS_SERVER_URL: https://localhost:8989
RAVENDB_BUILD_TYPE: nightly
Expand All @@ -36,6 +36,11 @@ jobs:
matrix:
bun-version: [latest]
serverVersion: ["6.2", "7.1", "7.2"]
client-cert: [pem]
include:
- bun-version: latest
serverVersion: "7.2"
client-cert: pfx
fail-fast: false

steps:
Expand Down
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2729,6 +2729,49 @@ Deno-specific notes:

An end-to-end smoke test lives in [`test/deno`](./test/deno).

## Bun

The client runs on [Bun](https://bun.com), including X.509 client-certificate
authentication with either a **PEM** bundle or a **PKCS#12 (PFX)** archive - configure
`authOptions` exactly like on Node.

```javascript
import { DocumentStore } from "ravendb";
import { readFileSync } from "node:fs";

const authOptions = {
type: "pfx",
certificate: readFileSync("client.pfx"), // a Buffer, as on Node
password: "secret", // optional
ca: readFileSync("ca.pem") // optional, for a private CA
};

const store = new DocumentStore("https://a.free.example.ravendb.cloud", "MyDatabase", authOptions);
store.initialize();
```

Bun-specific notes:

- **A PEM certificate is presented through Bun's `tls` fetch option**, a PKCS#12 archive
through a `node:https` transport the client installs for it. Bun's `fetch` silently
ignores `tls.pfx` ([oven-sh/bun#41958](https://github.com/oven-sh/bun/issues/41958),
[oven-sh/bun#17543](https://github.com/oven-sh/bun/issues/17543)), which would send the
request uncertified; its `node:https` implementation does honour `pfx`
([oven-sh/bun#14417](https://github.com/oven-sh/bun/issues/14417)).
- **A PKCS#12 archive needs Bun 1.4.0 or newer.** Earlier versions present no client
certificate on either transport, so the client throws an actionable error at
`initialize()` instead of sending uncertified requests. Convert the archive with
`openssl pkcs12 -in cert.pfx -out cert.pem -nodes` and use `type: "pem"` to stay on an
older Bun.
- **HTTP decompression is off by default on Bun** (`conventions.useHttpDecompression`),
unchanged by the above - the `node:https` transport asks for and decodes `gzip`, `deflate`, `br`
(and `zstd` where the runtime's `zlib` has it)
responses when you turn it on.
- **The Changes API does not work over TLS on Bun.** Bun replaces the `ws` package with
its own WebSocket, which ignores the Node TLS options the client passes (certificate,
`ca`, even `rejectUnauthorized`), so the connection fails the TLS handshake against a
secured server. Run Changes API consumers on Node.

## Custom fetch: bring your own transport

On runtimes where the client cannot provide authentication itself,
Expand Down
5 changes: 0 additions & 5 deletions src/Auth/Certificate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,11 +272,6 @@ export class PfxCertificate extends Certificate {
public toBunTlsOptions(): BunTlsOptions {
const options = super.toBunTlsOptions();

console.warn(
"WARNING: PFX certificates are not currently supported in Bun runtime. " +
"The connection may fail. Please use PEM certificates instead. "
);

return {
...options,
pfx: this._certificate as Buffer,
Expand Down
6 changes: 3 additions & 3 deletions src/Http/RavenCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export abstract class RavenCommand<TResult> {

log.info(`Send command ${this.constructor.name} to ${uri}${body ? " with body " + body : ""}.`);

const bodyToUse = fetcher ? RavenCommand.maybeWrapBody(body) : body;
const bodyToUse = fetcher ? RavenCommand.maybeWrapBody(body, fetcher) : body;

let optionsToUse: RequestInit | BunFetchRequestInit;

Expand Down Expand Up @@ -244,8 +244,8 @@ export abstract class RavenCommand<TResult> {
return err;
}

private static maybeWrapBody(body: any) {
if (body instanceof Readable) {
private static maybeWrapBody(body: any, fetcher: any) {
if (body instanceof Readable && !fetcher?.acceptsNodeStreamBody) {
throw new Error("Requests using stream.Readable as payload are not yet supported!");
}

Expand Down
40 changes: 37 additions & 3 deletions src/Http/RequestExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ import { EOL } from "../Utility/OsUtil.js";
import { importFix } from "../Utility/ImportUtil.js";
import { RuntimeUtil } from "../Utility/RuntimeUtil.js";
import { createDenoHttpClient, DenoHttpClient, validateDenoCertificateSupport } from "../Utility/DenoHttpUtil.js";
import {
BunHttpTransport,
createNodeHttpsTransport,
requiresNodeHttpsTransport,
validateBunCertificateSupport
} from "../Utility/BunHttpUtil.js";

const DEFAULT_REQUEST_OPTIONS = {};

Expand Down Expand Up @@ -247,6 +253,12 @@ export class RequestExecutor implements IDisposable {
/** Deno only: the Deno.HttpClient presenting the client certificate, built lazily in _getDenoHttpClient(). */
private _denoHttpClient: DenoHttpClient = null;

/** Bun only: the node:https transport presenting a PKCS#12 certificate, built lazily in _getBunHttpTransport(). */
private _bunHttpTransport: BunHttpTransport = null;

/** Bun only: the certificate is a PKCS#12 archive, which only _getBunHttpTransport() can present. */
private _presentsCertificateViaNodeHttps = false;

public static requestPostProcessor: (req: HttpRequestParameters) => void = null;

public get customHttpRequestOptions(): HttpRequestParametersWithoutUri {
Expand Down Expand Up @@ -363,8 +375,9 @@ export class RequestExecutor implements IDisposable {
// Runtimes whose fetch has no undici dispatcher (Bun, Cloudflare Workers, Deno)
// can't use an http agent -- there is nothing to build. A configured certificate
// is presented another way there: Bun via the `tls` request option (set in
// _setDefaultRequestOptions), Deno via a Deno.HttpClient `client` request option
// (set in _createRequest).
// _setDefaultRequestOptions) or, for a PKCS#12 archive Bun's fetch cannot present,
// a node:https `fetcher` (set in _createRequest); Deno via a Deno.HttpClient
// `client` request option (also set in _createRequest).
if (!RuntimeUtil.supportsUndiciDispatcher()) {
// On workerd a client certificate can never be presented from user-space
// unless mTLS is wired through conventions.customFetch (checked above).
Expand Down Expand Up @@ -425,6 +438,10 @@ export class RequestExecutor implements IDisposable {
if (RuntimeUtil.isDeno()) {
validateDenoCertificateSupport(Certificate.createFromOptions(authOptions));
}

if (RuntimeUtil.isBun()) {
validateBunCertificateSupport(Certificate.createFromOptions(authOptions));
}
}

private compressionHeaders(params: HttpRequestParameters) {
Expand Down Expand Up @@ -500,6 +517,7 @@ export class RequestExecutor implements IDisposable {
this._conventions = conventions.clone();
this._authOptions = authOptions;
this._certificate = Certificate.createFromOptions(this._authOptions);
this._presentsCertificateViaNodeHttps = RuntimeUtil.isBun() && requiresNodeHttpsTransport(this._certificate);
this._setDefaultRequestOptions();

this._defaultTimeout = conventions.requestTimeout;
Expand Down Expand Up @@ -1508,6 +1526,11 @@ export class RequestExecutor implements IDisposable {
req.client = this._getDenoHttpClient();
}

// Bun's fetch ignores `tls.pfx`, so a PKCS#12 archive is presented through a node:https fetcher.
if (this._presentsCertificateViaNodeHttps && !this.conventions.customFetch) {
req.fetcher = this._getBunHttpTransport().fetch;
}

urlRef(req.uri);
req.headers = req.headers || {};

Expand Down Expand Up @@ -2033,7 +2056,7 @@ export class RequestExecutor implements IDisposable {
DEFAULT_REQUEST_OPTIONS,
this._customHttpRequestOptions);

if (RuntimeUtil.isBun() && this._certificate) {
if (RuntimeUtil.isBun() && this._certificate && !this._presentsCertificateViaNodeHttps) {
this._defaultRequestOptions.tls = this._certificate.toBunTlsOptions();
}
}
Expand All @@ -2046,6 +2069,14 @@ export class RequestExecutor implements IDisposable {
return this._denoHttpClient;
}

private _getBunHttpTransport(): BunHttpTransport {
if (!this._bunHttpTransport) {
this._bunHttpTransport = createNodeHttpsTransport(this._certificate);
}

return this._bunHttpTransport;
}

public dispose(): void {
this._log.info("Dispose.");

Expand All @@ -2071,6 +2102,9 @@ export class RequestExecutor implements IDisposable {
// Deno only: release the connection pool behind the client certificate.
this._denoHttpClient?.close();
this._denoHttpClient = null;

this._bunHttpTransport?.close();
this._bunHttpTransport = null;
}

public async getRequestedNode(nodeTag: string, throwIfContainsFailures = false): Promise<CurrentIndexAndNode> {
Expand Down
4 changes: 2 additions & 2 deletions src/Types/BunTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ export interface BunTlsOptions {
/**
* PFX or PKCS12 encoded certificate and private key
*
* @warning This property is included for Node.js compatibility, but PFX certificates
* are NOT currently supported in Bun runtime. Use PEM certificates (cert/key/ca) instead.
* @warning Bun's fetch ignores this option - only PEM material (cert/key/ca) is honoured
* there (oven-sh/bun#41958, oven-sh/bun#17543).
*/
pfx?: Buffer;
}
Expand Down
Loading
Loading