Skip to content

[absorbed into #630 Phase 6] simplify SQL Browser authentication integration around the package #636

Description

@BorisTyshkevich

Part of #630.

Depends on: #635

Goal

Replace SQL Browser's transport-specific authentication bridge with a narrow authenticated request layer that composes SQL Browser credential/epoch/lifecycle policy with @altinity/clickhouse-http's low-level request and response-consumer APIs.

This is the trust-boundary unit. Preserve every current authentication, epoch, refresh, lifecycle, cancellation, and HTTP-classification invariant. The package must remain completely unaware of those policies.

Architecture decision

Create a focused SQL Browser module under src/net/, preferably src/net/authenticated-clickhouse.ts, which owns authenticated request composition.

The core primitive is equivalent to:

export async function authenticatedRequest(
  ctx: ChCtx,
  request: Omit<ClickHouseHttpRequest, 'authorization'>,
): Promise<Response>;

It must call the package's low-level request() only after SQL Browser has resolved and fenced the current credential.

Also provide SQL Browser convenience composition over package response consumers, not package convenience methods that would bypass SQL Browser auth policy:

export function authenticatedQueryJson<T>(ctx, request): Promise<T>;
export function authenticatedQueryText(ctx, request): Promise<string>;
export function authenticatedQueryProgress(ctx, request, callbacks): Promise<Response>;

These must be implemented as:

authenticatedRequest -> native Response -> package response consumer

not as package query*() calls that require the caller to pre-resolve Authorization outside the epoch/refresh fence.

Authentication invariants to preserve exactly

Request-epoch capture

  • capture ctx.currentEpoch?.() synchronously at entry, before the first await;
  • a context without epoch support remains backward-compatible: every request is current.

Request-input snapshot

Snapshot caller settings and params synchronously before the first credential await so later caller mutation cannot alter the committed request or its refresh retry.

Preserve the current early request-preparation failure timing: malformed settings/params that make URL serialization throw must fail before token lookup and must not be reported as an offline transport failure. Use the package URL serializer as the single implementation; a discarded preflight serialization is acceptable to preserve the timing contract.

Credential resolution

  • call ctx.getToken();
  • after every credential-related await, re-check epoch;
  • a stale request aborts with the existing AbortError-shaped superseded-request result before any replacement-session credential can be sent;
  • no token means invoke ctx.onSignedOut(undefined, requestEpoch) under current-epoch authority and throw the existing not-signed-in result.

Authorization

  • ctx.authHeader remains SQL Browser policy;
  • default remains Bearer when absent;
  • compute the complete Authorization string per attempt;
  • perform the final epoch fence immediately before the package low-level request;
  • pass the complete string to the package unchanged;
  • never cache Authorization in the package/client across attempts.

One refresh retry

Preserve the current bounded retry loop:

  • initial attempt;
  • at most one ctx.refresh() retry when auth is classified as expired/invalid and the session has not already been confirmed;
  • successful refresh obtains a fresh token and rechecks epoch after every await;
  • no generic network retry here.

401/403 / auth-expiry classification

Preserve current behavior:

  • HTTP 401/403 are candidates for auth expiry before the session is confirmed;
  • non-OK bodies may be cloned/read to detect token_verification_exception / token-expired markers;
  • the original native Response remains unconsumed for the eventual caller because auth inspection uses response.clone();
  • after every async cloned-body read, re-check epoch before side effects;
  • once ctx.authConfirmed is true, later 401/403 remain query-level ClickHouse outcomes and must not sign the user out;
  • first-contact unrecoverable auth denial parses ClickHouse's reason and calls onSignedOut(detail, requestEpoch) exactly under current authority.

Connection lifecycle

  • only a current successful HTTP 2xx settlement sets ctx.authConfirmed = true and calls onTransportConnected?.();
  • rejected non-aborted Fetch/network I/O calls onTransportOffline?.(error) only while current;
  • HTTP query failures are responses, not offline state;
  • caller cancellation is invisible to offline state;
  • stale requests never mutate replacement lifecycle state.

Cancellation

ChCtx ownership

Keep ChCtx as a SQL Browser type. It may move from ch-client.ts into the new authenticated module or a focused .types.ts file if that removes a dependency cycle, but it must remain outside the package because it contains SQL Browser token/refresh/lifecycle hooks.

AuthenticatedCancellationLease also remains SQL Browser-owned.

ch-client.ts integration

After this unit:

  • authedFetch is removed or becomes a compatibility export that delegates entirely to authenticatedRequest; no independent auth loop remains;
  • queryJson delegates to authenticatedQueryJson or authenticatedRequest + consumeJsonResponse;
  • current runQuery remains temporarily as SQL Browser result-mode policy, but its HTTP/stream mechanics delegate to the new authenticated layer + package consumers;
  • current exportQuery remains temporarily as export policy compatibility, but uses authenticatedRequest and package error parsing;
  • normal killQuery uses the authenticated layer plus package stateless killQuery/response primitives without moving cancellation ownership;
  • killQueryWithLease bypasses token/refresh/lifecycle exactly as today and builds a one-shot package client from the frozen lease's origin, fetch, and complete Authorization.

Do not move schema/catalog/lineage/documentation product SQL out of ch-client.ts in this unit.

Tests

Treat this unit as high-risk auth/lifecycle/cancellation work. Preserve and extend existing ch-client tests around:

Epoch races

  • epoch changes while getToken() awaits: no request sent;
  • epoch changes after refresh starts: replacement token not sent/mutated;
  • epoch changes while cloned error body is read: no refresh/sign-out/lifecycle write;
  • stale successful response returned to its caller but cannot report connected;
  • final epoch fence occurs before package request() side effect.

Refresh/auth classification

  • one refresh retry only;
  • no refresh after confirmed auth on query-level 401/403;
  • token-verification body marker follows auth-expiry path;
  • first-contact denial surfaces parsed server reason;
  • refresh failure signs out under current epoch only.

Lifecycle

  • current 2xx => connected;
  • current non-abort network rejection => offline;
  • HTTP 4xx/5xx => no offline;
  • AbortError/caller-aborted signal => no offline;
  • stale request => no lifecycle mutation.

Request fidelity

  • settings/params snapshot before first await;
  • malformed URL input fails before token read and is not classified offline;
  • exact Authorization per retry;
  • exact SQL and package request fields preserved.

Cancellation

Re-run the real-browser post-header cancellation and concurrent-isolation scenarios through the authenticated SQL Browser path, not only the package low-level client. This is the sabotage proof that the auth wrapper did not reintroduce the derived-signal defect that rejected #585.

Run the full repository gate plus the targeted browser suite:

npm run check:types
npm run check:arch
npm run check:schemas
npm run check:examples
npm test
npm run build
npm run test:client-spike:browser

Use the current renamed equivalent if earlier units changed the targeted script; Chromium and WebKit coverage is required.

Acceptance criteria

  • SQL Browser has one authenticated request implementation composed over package request().
  • The package still has no token, refresh, epoch, auth-mode, lifecycle, or sign-out concepts.
  • Settings/params snapshot and pre-await URL-validation timing remain compatible.
  • Every credential await and async auth-body classification is epoch-fenced.
  • Exactly one auth refresh retry remains.
  • Post-confirmation 401/403 remains a query outcome, not sign-out.
  • Only current 2xx reports connected and only current non-abort network rejection reports offline.
  • The original caller signal reaches the actual Fetch request with no derived-controller/listener bridge.
  • Real-browser tests prove post-header body cancellation through the authenticated path.
  • killQueryWithLease remains frozen-lease-only with no token read/refresh/lifecycle callback.
  • ch-client.ts no longer contains an independent authedFetch implementation.
  • Product-specific ClickHouse operations remain SQL Browser-owned.
  • Full gate and required browser tests pass.

Non-goals

Agent execution notes

Before planning, read #630#635, src/net/ch-client.ts in full, ConnectionSession/authenticated execution scope ownership in docs/ARCHITECTURE.md, all ChCtx/auth tests, and the real-browser cancellation harness. Build an invariant map for authority, epoch ordering, refresh count, lifecycle side effects, native Response ownership, and cancellation; this unit must receive high-risk /ship review treatment.

Metadata

Metadata

Assignees

No one assigned

    Labels

    refactorRestructuring without user-facing behavior changetech-debt

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions