Skip to content

[DEV-1642] Add first-class bearer token credential support#493

Merged
w1am merged 4 commits intomasterfrom
w1am/dev-1642-add-first-class-bearer-token-credential-support-to-node
May 6, 2026
Merged

[DEV-1642] Add first-class bearer token credential support#493
w1am merged 4 commits intomasterfrom
w1am/dev-1642-add-first-class-bearer-token-credential-support-to-node

Conversation

@w1am
Copy link
Copy Markdown
Collaborator

@w1am w1am commented Apr 30, 2026

Summary

Adds first-class bearer-token authentication to KurrentDB.

You can now pass a bearer token anywhere credentials are accepted:

await client.appendToStream("orders", events, {
  credentials: { bearerToken: "eyJ..." },
});

Or configure a refresh-aware token source that runs once per RPC:

client.setCredentialsProvider(async () => ({
  bearerToken: await fetchAccessToken(),
}));

The provider runs ahead of every gRPC, HTTP-fallback, and bridge-backed read (readAll, readStream), so cached-until-expiry strategies (Azure Entra, OIDC, custom OAuth) just work.

When credentialsProvider is set, appendToStream falls back from the cached batchAppend streaming RPC to unary append so the provider runs per call.

Resolution order

  1. Per-call credentials on the request
  2. credentialsProvider, if set
  3. Connection-string defaults

Connection strings remain basic-only. Bearer tokens stay programmatic.

Type changes

  • New BearerCredentials interface, plus a Credentials union of BasicCredentials | BearerCredentials.
  • The previous Credentials interface (basic-only) is renamed to BasicCredentials.

Code that imports Credentials and reads .username / .password directly will now need to narrow with isBasicCredentials(c) or switch its annotation to BasicCredentials.

Closes DEV-1642

@linear
Copy link
Copy Markdown

linear Bot commented Apr 30, 2026

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented Apr 30, 2026

Review Summary by Qodo

(Agentic_describe updated until commit a6daf49)

Add first-class bearer token credential support with refresh-aware provider

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add first-class bearer token credential support alongside basic auth
• Implement CredentialsProvider for refresh-aware token sources per RPC
• Refactor credential resolution with per-call override precedence
• Export type predicates isBasicCredentials and isBearerCredentials
• Update OpenTelemetry instrumentation to track auth kind
Diagram
flowchart LR
  A["Per-call credentials"] -->|highest priority| D["Authorization header"]
  B["CredentialsProvider"] -->|medium priority| D
  C["Default credentials"] -->|lowest priority| D
  D -->|gRPC metadata| E["gRPC requests"]
  D -->|HTTP header| F["HTTP fallback"]
  D -->|bridge| G["Bridge-backed reads"]
Loading

Grey Divider

File Changes

1. packages/db-client/src/types/index.ts ✨ Enhancement +30/-2

Split Credentials into BasicCredentials and BearerCredentials union

packages/db-client/src/types/index.ts


2. packages/db-client/src/utils/credentials.ts ✨ Enhancement +41/-0

Add credential type predicates and authorization header builder

packages/db-client/src/utils/credentials.ts


3. packages/db-client/src/Client/index.ts ✨ Enhancement +100/-25

Implement CredentialsProvider with resolution precedence logic

packages/db-client/src/Client/index.ts


View more (14)
4. packages/db-client/src/Client/http.ts ✨ Enhancement +21/-10

Refactor HTTP auth to use ResolveAuthorizationHeader callback

packages/db-client/src/Client/http.ts


5. packages/db-client/src/Client/parseConnectionString.ts ✨ Enhancement +2/-2

Update connection string parsing to use BasicCredentials type

packages/db-client/src/Client/parseConnectionString.ts


6. packages/db-client/src/streams/readAll.ts ✨ Enhancement +21/-20

Resolve bridge credentials via resolveBridgeCredentials method

packages/db-client/src/streams/readAll.ts


7. packages/db-client/src/streams/readStream.ts ✨ Enhancement +36/-45

Resolve bridge credentials via resolveBridgeCredentials method

packages/db-client/src/streams/readStream.ts


8. packages/db-client/src/index.ts ✨ Enhancement +1/-0

Export credential type predicates for public API

packages/db-client/src/index.ts


9. packages/opentelemetry/src/attributes.ts ✨ Enhancement +1/-0

Add KURRENT_DB_AUTH_KIND attribute for span telemetry

packages/opentelemetry/src/attributes.ts


10. packages/opentelemetry/src/instrumentation.ts ✨ Enhancement +35/-8

Track auth kind in span attributes using describeAuth utility

packages/opentelemetry/src/instrumentation.ts


11. packages/opentelemetry/src/utils.ts ✨ Enhancement +30/-1

Add describeAuth function to derive auth context from credentials

packages/opentelemetry/src/utils.ts


12. packages/test/src/connection/defaultCredentials.test.ts 🧪 Tests +88/-4

Add bearer token and credentialsProvider test coverage

packages/test/src/connection/defaultCredentials.test.ts


13. packages/test/src/connection/reconnect/no-reconnection.test.ts Formatting +4/-1

Format credentials object for consistency

packages/test/src/connection/reconnect/no-reconnection.test.ts


14. packages/test/src/opentelemetry/instrumentation.test.ts 🧪 Tests +8/-3

Update instrumentation tests for auth kind attribute

packages/test/src/opentelemetry/instrumentation.test.ts


15. packages/test/src/utils/index.ts ✨ Enhancement +2/-2

Update test utilities to use BasicCredentials type

packages/test/src/utils/index.ts


16. packages/db-client/package.json Dependencies +1/-1

Bump @kurrent/bridge dependency to 0.2.0

packages/db-client/package.json


17. packages/benchmark/package.json Dependencies +1/-1

Bump @kurrent/bridge dependency to 0.2.0

packages/benchmark/package.json


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented Apr 30, 2026

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Bearer tokens logged in debug 🐞 Bug ⛨ Security
Description
HTTP fallback logging prints the full request headers object, and with bearer-token support this can
include the full Authorization: Bearer <token> value. This leaks access tokens into logs whenever
debug logging is enabled.
Code

packages/db-client/src/Client/http.ts[R77-89]

+    const authorization = await this.#resolveAuthorizationHeader(
+      options.credentials
+    );
+
+    return new Promise<T>((resolve, reject) => {
      const headers: Record<string, string> = {
        "content-type": "application/json",
        ...(options.headers ?? {}),
      };
-      const credentials = options.credentials ?? this.#defaultUserCredentials;

-      if (!this.#insecure && credentials) {
-        headers["Authorization"] = `Basic ${Buffer.from(
-          `${credentials.username}:${credentials.password}`
-        ).toString("base64")}`;
+      if (authorization) {
+        headers["Authorization"] = authorization;
      }
Evidence
The HTTP fallback path now sets the verbatim Authorization header based on resolved credentials
(which can be a Bearer token). The debug log prints the headers map, so the bearer token value can
be emitted to logs.

packages/db-client/src/Client/http.ts[71-89]
packages/db-client/src/Client/http.ts[129-134]
packages/db-client/src/utils/credentials.ts[32-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The HTTP fallback debug log prints request headers, which now can contain `Authorization: Bearer ...` tokens. This is sensitive and should not be logged verbatim.

### Issue Context
Bearer tokens are typically high-value credentials; leaking them into logs can lead to account/API compromise.

### Fix Focus Areas
- Redact or omit the `Authorization` header from the logged object (e.g., replace with `"[REDACTED]"`), while keeping other headers for diagnostics.

- packages/db-client/src/Client/http.ts[81-89]
- packages/db-client/src/Client/http.ts[129-134]

### Acceptance criteria
- Debug logging never outputs raw Authorization header values (Basic or Bearer).
- Debug output remains useful (e.g., logs method, URL, and non-sensitive headers).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Provider skipped on cached append ✓ Resolved 🐞 Bug ≡ Correctness
Description
The new credentialsProvider is documented/used as a per-request credential source, but
appendToStream can still take the cached batchAppend streaming path, meaning subsequent
appendToStream calls may reuse a stream created under earlier credentials without re-invoking the
provider. This can break token-rotation semantics (stale token) and contradicts the added
tests/contract expecting the provider to run per append RPC.
Code

packages/db-client/src/Client/index.ts[R718-736]

+    // Per-call credentials take precedence. Otherwise prefer a refresh-aware
+    // provider over the static default so token refresh actually runs per RPC.
+    let resolveCredentials:
+      | (() => Credentials | Promise<Credentials>)
+      | undefined;
    if (credentials) {
+      resolveCredentials = () => credentials;
+    } else if (this.#credentialsProvider) {
+      resolveCredentials = this.#credentialsProvider;
+    } else if (this.#defaultCredentials) {
+      const defaults = this.#defaultCredentials;
+      resolveCredentials = () => defaults;
+    }
+
+    if (resolveCredentials) {
      options.credentials = grpcCallCredentials.createFromMetadataGenerator(
-        this.createCredentialsMetadataGenerator(credentials)
+        this.createMetadataGenerator(resolveCredentials)
      );
    }
Evidence
Client.callArguments prefers the credentialsProvider to ensure refresh runs per RPC, but batchAppend
uses a cached gRPC streaming call (via GRPCStreamCreator + streamCache). Because appendToStream
chooses batchAppend whenever no per-call credentials are provided, a configured provider may not be
consulted again on later appendToStream invocations if the cached stream is reused; the new test
suite explicitly expects provider to be called once per appendToStream call.

packages/db-client/src/Client/index.ts[405-413]
packages/db-client/src/Client/index.ts[718-736]
packages/db-client/src/Client/index.ts[433-448]
packages/db-client/src/streams/appendToStream/index.ts[69-92]
packages/db-client/src/streams/appendToStream/batchAppend.ts[33-36]
packages/db-client/src/streams/appendToStream/batchAppend.ts[55-65]
packages/db-client/src/streams/appendToStream/batchAppend.ts[124-125]
packages/test/src/connection/defaultCredentials.test.ts[85-108]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`appendToStream` may use the cached `batchAppend` streaming RPC even when `credentialsProvider` is configured. Because the stream is cached/reused, later `appendToStream` calls can reuse credentials established when the stream was created and not re-run the provider, which defeats per-call token refresh/rotation.

### Issue Context
- `Client.callArguments()` is designed to invoke the provider per outbound request.
- `batchAppend` reuses a cached streaming call, which is effectively one long-lived RPC shared across multiple logical `appendToStream` calls.

### Fix Focus Areas
- Update the routing logic so `appendToStream` does **not** use the cached `batchAppend` path when `this.credentialsProvider` is set (or otherwise ensure the cached stream is not reused under changing credentials).

- packages/db-client/src/streams/appendToStream/index.ts[83-92]
- packages/db-client/src/streams/appendToStream/batchAppend.ts[33-36]
- packages/db-client/src/streams/appendToStream/batchAppend.ts[55-65]
- packages/db-client/src/Client/index.ts[433-448]

### Acceptance criteria
- With `client.setCredentialsProvider(...)` configured, multiple `appendToStream` calls invoke the provider for each call (as the test expects).
- No cached streaming append call is reused across logical appends when credentials are refresh/rotation-driven.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Bearer creds ignored in reads ✓ Resolved 🐞 Bug ⛨ Security
Description
readStream/readAll explicitly drop bearer-token credentials (and do not use
authorizationHeaderProvider), so callers can pass { bearerToken } and the operation will proceed
without applying it. This silently changes the effective auth used for these reads and can lead to
requests running under unintended credentials.
Code

packages/db-client/src/streams/readStream.ts[R74-79]

+    // Bearer-token credentials and authorizationHeaderProvider are not yet
+    // plumbed through the Rust bridge — see DEV-1644/DEV-1643.
+    credentials:
+      baseOptions.credentials && "username" in baseOptions.credentials
+        ? baseOptions.credentials
+        : undefined,
Evidence
The Rust-bridge read paths only forward username/password credentials and map any bearerToken
credentials to undefined, despite Credentials now supporting bearer tokens; this makes bearer tokens
a no-op for these APIs.

packages/db-client/src/streams/readStream.ts[68-80]
packages/db-client/src/streams/readAll.ts[67-80]
packages/db-client/src/types/index.ts[331-341]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`readStream`/`readAll` currently accept `BaseOptions.credentials: Credentials`, but they silently ignore `{ bearerToken }` by dropping it before calling the Rust bridge. This means callers can think they’re authenticating with a bearer token while the call proceeds without that credential.

## Issue Context
Bearer tokens and `authorizationHeaderProvider` are intentionally not plumbed through the Rust bridge yet, but silently ignoring user-supplied auth is unsafe and surprising.

## Fix Focus Areas
- packages/db-client/src/streams/readStream.ts[68-80]
- packages/db-client/src/streams/readAll.ts[67-80]

## Suggested fix
- If `baseOptions.credentials` is present and is a bearer token (`"bearerToken" in baseOptions.credentials`), throw an explicit error (e.g. `UnsupportedError` or `InvalidArgumentError`) stating bearer tokens aren’t supported for `readStream`/`readAll` yet.
- Consider also failing (or warning) when `this.authorizationHeaderProvider` is set for these operations (since it also won’t be used), to avoid silently using connection-string/default credentials.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. OTel peer version too broad 🐞 Bug ☼ Reliability
Description
@kurrent/opentelemetry now imports isBasicCredentials from @kurrent/kurrentdb-client, but its
peerDependency still allows any ^1 and the instrumentation patches 1.*. Using this
instrumentation with an older 1.x client that doesn’t export isBasicCredentials can break at
module load/runtime.
Code

packages/opentelemetry/src/utils.ts[R3-8]

+import {
+  isBasicCredentials,
+  type Credentials,
+  type EventData,
+  type JSONEventData,
+} from "@kurrent/kurrentdb-client";
Evidence
The OpenTelemetry package now has a hard import of a newly-exported symbol from the client, while
still declaring compatibility with any 1.x client version. That creates a version-skew failure mode
for consumers that update @kurrent/opentelemetry without updating @kurrent/kurrentdb-client to
the release that introduced that export.

packages/opentelemetry/src/utils.ts[3-8]
packages/opentelemetry/package.json[40-42]
packages/opentelemetry/src/instrumentation.ts[64-68]
packages/db-client/src/index.ts[5-15]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`@kurrent/opentelemetry` now imports `isBasicCredentials` from `@kurrent/kurrentdb-client` but still declares a broad peer dependency (`^1`). This can fail if a consumer uses an older 1.x client that doesn't export `isBasicCredentials`.

### Issue Context
This is a cross-package compatibility problem within the monorepo:
- The instrumentation claims support for all 1.* clients.
- The new import effectively requires a minimum client version.

### Fix Focus Areas
Choose one:
1) Remove the dependency by inlining a small local type guard in the OTel package (`credentials && typeof credentials === 'object' && 'username' in credentials`) instead of importing `isBasicCredentials`.
2) Or, tighten compatibility: bump `peerDependencies['@kurrent/kurrentdb-client']` to the minimum version that contains `isBasicCredentials` (and consider narrowing the patched module version range similarly).

- packages/opentelemetry/src/utils.ts[3-8]
- packages/opentelemetry/package.json[40-42]
- packages/opentelemetry/src/instrumentation.ts[64-68]

### Acceptance criteria
- `@kurrent/opentelemetry` works (loads and runs) with any client version allowed by its peer dependency range.
- No hard dependency on newly-added exports unless peer dependency ranges enforce it.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Provider reruns on redirects 🐞 Bug ≡ Correctness
Description
HTTP.makeRequest resolves the Authorization header before the request, but on a 307 redirect it
recursively calls makeRequest again, which re-resolves authorization and can invoke
authorizationHeaderProvider multiple times for a single logical operation. This can cause
inconsistent Authorization values across redirects for providers whose return value changes per
call.
Code

packages/db-client/src/Client/http.ts[R105-120]

+  ): Promise<T> => {
+    const authorization = await this.resolveAuthorizationHeader(
+      options.credentials
+    );
+
+    return new Promise<T>((resolve, reject) => {
      const headers: Record<string, string> = {
        "content-type": "application/json",
        ...(options.headers ?? {}),
      };
-      const credentials = options.credentials ?? this.#defaultUserCredentials;

-      if (!this.#insecure && credentials) {
-        headers["Authorization"] = `Basic ${Buffer.from(
-          `${credentials.username}:${credentials.password}`
-        ).toString("base64")}`;
+      if (authorization) {
+        headers["Authorization"] = authorization;
      }

      const ca = this.#channelCredentials.rootCertificate
Evidence
The HTTP path computes authorization at the top of makeRequest, but redirect handling re-enters
makeRequest rather than reusing the already-resolved header. The test suite demonstrates providers
are expected to be callable and may return different values per invocation, so repeated invocations
during redirects can change behavior mid-operation.

packages/db-client/src/Client/http.ts[100-156]
packages/test/src/connection/defaultCredentials.test.ts[76-112]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`HTTP.makeRequest` resolves the Authorization header once, but on HTTP 307 it calls `makeRequest(...)` again, which re-runs `resolveAuthorizationHeader()` and may call `authorizationHeaderProvider()` multiple times.

## Issue Context
`authorizationHeaderProvider` is explicitly designed to be invoked per call and may return different values per invocation (the tests alternate values). If an HTTP fallback request is redirected, multiple invocations within one logical operation can produce inconsistent auth headers.

## Fix Focus Areas
- packages/db-client/src/Client/http.ts[100-156]

## Suggested fix
- Resolve `authorization` once per logical operation and reuse it for redirect follow-ups.
- One approach: add an internal optional parameter like `authorizationOverride?: string` to `makeRequest`, and in the 307 handler call `makeRequest(..., options, body, authorization)`.
- Alternatively, when `authorization` is resolved, inject it into a cloned `options.headers` for subsequent recursive calls so redirects reuse the same header.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Provider AbortSignal never used ✓ Resolved 🐞 Bug ☼ Reliability
Description
AuthorizationHeaderProvider is defined with an optional AbortSignal parameter, but the
implementation always calls it with zero arguments, so the signal is never observable by providers.
This makes the public API contract misleading and prevents cancellation-aware token providers from
using the signal even when available.
Code

packages/db-client/src/Client/index.ts[R677-684]

+      Promise.resolve()
+        .then(() => provider())
+        .then((header) => {
+          const metadata = new Metadata();
+          metadata.add("authorization", header);
+          cb(null, metadata);
+        })
+        .catch((err) => cb(err as Error));
Evidence
The type explicitly allows a signal parameter, but both the gRPC metadata generator and HTTP
fallback invoke the provider as provider() / authorizationHeaderProvider() with no arguments, so
the signal will always be undefined.

packages/db-client/src/types/index.ts[342-352]
packages/db-client/src/Client/index.ts[665-685]
packages/db-client/src/Client/http.ts[77-97]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`AuthorizationHeaderProvider` is typed as `(signal?: AbortSignal) => string | Promise<string>`, but the code never passes an argument when invoking it. This makes the `signal` parameter effectively dead and misleading.

## Issue Context
Even if HTTP fallback doesn’t currently have an abort signal, the gRPC path may have per-call cancellation context that could be propagated.

## Fix Focus Areas
- packages/db-client/src/types/index.ts[342-352]
- packages/db-client/src/Client/index.ts[665-685]
- packages/db-client/src/Client/http.ts[77-97]

## Suggested fix
Choose one:
1) **Plumb a signal where available**: update the gRPC path to pass a signal value into the provider (e.g., capture whatever cancellation signal is available per call and call `provider(signal)`), and leave HTTP passing `undefined`.
2) **Remove the signal from the public type**: if there is no plan to support it, change `AuthorizationHeaderProvider` to `() => string | Promise<string>` and update docs accordingly.

Also update the JSDoc to match the chosen behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread packages/db-client/src/streams/readStream.ts Outdated
@w1am w1am marked this pull request as draft April 30, 2026 22:46
@w1am w1am changed the title feat: add first-class bearer token credential support (gRPC path) feat: add first-class bearer token credential support May 6, 2026
@w1am w1am force-pushed the w1am/dev-1642-add-first-class-bearer-token-credential-support-to-node branch from 510fd17 to a6daf49 Compare May 6, 2026 07:17
@w1am w1am changed the title feat: add first-class bearer token credential support [DEV-1642] Add first-class bearer token credential support May 6, 2026
@w1am w1am marked this pull request as ready for review May 6, 2026 07:21
@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 6, 2026

Persistent review updated to latest commit a6daf49

Comment thread packages/db-client/src/Client/index.ts
Comment thread packages/db-client/src/Client/http.ts
@w1am w1am self-assigned this May 6, 2026
@w1am w1am merged commit 4360274 into master May 6, 2026
30 checks passed
@w1am w1am deleted the w1am/dev-1642-add-first-class-bearer-token-credential-support-to-node branch May 6, 2026 07:53
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@w1am 👉 Created pull request targeting release/v1.2: #503

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant