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
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.
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/, preferablysrc/net/authenticated-clickhouse.ts, which owns authenticated request composition.The core primitive is equivalent to:
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:
These must be implemented as:
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
ctx.currentEpoch?.()synchronously at entry, before the first await;Request-input snapshot
Snapshot caller
settingsandparamssynchronously 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
ctx.getToken();AbortError-shaped superseded-request result before any replacement-session credential can be sent;ctx.onSignedOut(undefined, requestEpoch)under current-epoch authority and throw the existing not-signed-in result.Authorization
ctx.authHeaderremains SQL Browser policy;One refresh retry
Preserve the current bounded retry loop:
ctx.refresh()retry when auth is classified as expired/invalid and the session has not already been confirmed;401/403 / auth-expiry classification
Preserve current behavior:
token_verification_exception/ token-expired markers;response.clone();ctx.authConfirmedis true, later 401/403 remain query-level ClickHouse outcomes and must not sign the user out;onSignedOut(detail, requestEpoch)exactly under current authority.Connection lifecycle
ctx.authConfirmed = trueand callsonTransportConnected?.();onTransportOffline?.(error)only while current;Cancellation
AbortSignalthrough the package low-level request;ChCtxownershipKeep
ChCtxas a SQL Browser type. It may move fromch-client.tsinto the new authenticated module or a focused.types.tsfile if that removes a dependency cycle, but it must remain outside the package because it contains SQL Browser token/refresh/lifecycle hooks.AuthenticatedCancellationLeasealso remains SQL Browser-owned.ch-client.tsintegrationAfter this unit:
authedFetchis removed or becomes a compatibility export that delegates entirely toauthenticatedRequest; no independent auth loop remains;queryJsondelegates toauthenticatedQueryJsonorauthenticatedRequest + consumeJsonResponse;runQueryremains temporarily as SQL Browser result-mode policy, but its HTTP/stream mechanics delegate to the new authenticated layer + package consumers;exportQueryremains temporarily as export policy compatibility, but usesauthenticatedRequestand package error parsing;killQueryuses the authenticated layer plus package statelesskillQuery/response primitives without moving cancellation ownership;killQueryWithLeasebypasses token/refresh/lifecycle exactly as today and builds a one-shot package client from the frozen lease'sorigin,fetch, and complete Authorization.Do not move schema/catalog/lineage/documentation product SQL out of
ch-client.tsin this unit.Tests
Treat this unit as high-risk auth/lifecycle/cancellation work. Preserve and extend existing
ch-clienttests around:Epoch races
getToken()awaits: no request sent;request()side effect.Refresh/auth classification
Lifecycle
Request fidelity
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:browserUse the current renamed equivalent if earlier units changed the targeted script; Chromium and WebKit coverage is required.
Acceptance criteria
request().killQueryWithLeaseremains frozen-lease-only with no token read/refresh/lifecycle callback.ch-client.tsno longer contains an independentauthedFetchimplementation.Non-goals
ConnectionSessionpublic behavior.Agent execution notes
Before planning, read #630–#635,
src/net/ch-client.tsin full,ConnectionSession/authenticated execution scope ownership indocs/ARCHITECTURE.md, allChCtx/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/shipreview treatment.