Skip to content

feat(sql) Add HTTP Network Transport and Header Resolution - #10523

Open
stephenarosaj wants to merge 2 commits into
tree/mainfrom
tree/birch-transport
Open

stephenarosaj wants to merge 2 commits into
tree/mainfrom
tree/birch-transport

Conversation

@stephenarosaj

@stephenarosaj stephenarosaj commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Add HTTP Network Transport and Header Resolution to Firebase SQL

Overview

✨ Implements FirebaseSqlTransport and wire-level header resolution for @firebase/sql, handling Firebase Auth and App Check token attachment, telemetry headers, Cloud Workstation emulator credentials, and single-shot HTTP 401 token force-refresh retries.

Highlights

✨ Key changes:

  • Added FirebaseSqlTransport to execute requests with concurrent Auth and App Check token resolution.
  • Added single-shot HTTP 401 retry logic that force-refreshes the Firebase Auth token (getToken({ forceRefresh: true })), cancels the unconsumed 401 response body stream, and retries only when a new access token is obtained.
  • Extracted header validation, normalization, HeadersInit merging, and wire-level header resolution (x-firebase-auth-token, x-firebase-appcheck, x-goog-api-client, x-firebase-gmpid) into headers.ts, populating DISALLOWED_HEADER_KEYS with protected Firebase and telemetry headers.
  • Wired FirebaseSqlTransport instantiation into FirebaseSql and added unit test suites for header resolution and transport execution.
Changelog

Implementation Changes

  • transport.ts
    • Added FirebaseSqlTransportInterface, FirebaseSqlTransportOptions, and FirebaseSqlTransport
    • Implemented, onAuthTokenChanged, Cloud Workstation credentials: 'include' forwarding, and single-shot HTTP 401 force-refresh retry
  • headers.ts
    • Added FirebaseHeaders, DISALLOWED_HEADER_KEYS, getGoogApiClientValue, mergeHeadersInit, and resolveRequestHeaders
    • Moved validateHeaders and normalizeHeaders from settings.ts
  • settings.ts
    • Imported validateHeaders from headers.ts and removed placeholder header helpers
  • FirebaseSql.ts
    • Initialized _transport (FirebaseSqlTransport) with service settings, appId, and token providers
    • Added debug logging for cached vs. newly initialized FirebaseSql instances
  • fetch.ts
    • Exported Fetch type alias (typeof fetch)
  • index.ts
    • Re-exported transport.ts from the network barrel

Test Changes

  • transport.test.ts
    • Added unit tests covering onAuthTokenChanged, AbortSignal and network error propagation, Cloud Workstation credentials, HeadersInit format parity, Auth/App Check/telemetry headers, and deduplicated HTTP 401 force-refresh retries
  • headers.test.ts
    • Added unit tests for resolveRequestHeaders covering custom headers, token attachment, token overwrites, and telemetry header precedence
  • mocks.ts
    • Added MockAuthProvider and MockAppCheckProvider test helpers
  • FirebaseSql.test.ts
    • Minor formatting cleanup
  • AbstractTokenProvider.test.ts
    • Minor formatting cleanup

@changeset-bot

changeset-bot Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: a915960

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces the HTTP transport layer (FirebaseSqlTransport) for Firebase SQL, which handles token resolution, telemetry headers, Cloud Workstation cookie forwarding, and automatic single-shot token refresh/retry on 401 responses. It also refactors header validation and normalization into a dedicated network/headers.ts module. Feedback on the changes suggests several improvements: optimizing validateHeaders by precomputing the lowercased disallowed keys set and using Object.keys to avoid unnecessary allocations; providing an explicit compare function when sorting entries in normalizeHeaders to prevent unexpected array-to-string conversion behavior; passing this.onAuthTokenChanged directly to avoid an unnecessary closure; and defensively checking if response.body.cancel is a function to prevent runtime errors in hybrid environments.

Comment thread packages/sql/src/network/headers.ts Outdated
Comment thread packages/sql/src/network/headers.ts Outdated
Comment thread packages/sql/src/network/headers.ts Outdated
Comment thread packages/sql/src/network/transport.ts Outdated
Comment thread packages/sql/src/network/transport.ts Outdated
@stephenarosaj stephenarosaj changed the title squash merge tree/birch-transport to public feat(sql) Add HTTP Network Transport and Header Resolution Oct 26, 2026
@stephenarosaj
stephenarosaj marked this pull request as ready for review October 26, 2026 17:34
@stephenarosaj
stephenarosaj requested a review from a team as a code owner October 26, 2026 17:34
Comment thread packages/sql/src/network/headers.ts Outdated
Comment thread packages/sql/src/network/transport.ts

@aashishpatil-g aashishpatil-g left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Left some questions.

Comment thread packages/sql/src/network/transport.ts

@dconeybe dconeybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I've left comments written by AI, but all personally vetted and edited by me. I'll come back and do a human review once these comments are addressed or refuted.

Comment on lines +99 to +108
const [authToken, appCheckToken] = await Promise.all([
this.authProvider
? this.authProvider.getToken({ forceRefresh: false }).catch(() => null)
: Promise.resolve(null),
this.appCheckProvider
? this.appCheckProvider
.getToken({ forceRefresh: false })
.catch(() => null)
: Promise.resolve(null)
]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[HIGH] Auth getToken() failures are swallowed, silently sending a signed-in user's request unauthenticated. (AI code review)

FirebaseAuthProvider.fetchToken already maps the benign auth/token-not-initialized to null, so this .catch(() => null) only swallows real failures (e.g. network error during refresh, revoked/disabled user). The request then omits x-firebase-auth-token, and a query backed by row-level security runs as anonymous and can succeed with different results (empty or public-only rows), with no signal to the caller. The 401 retry doesn't help, since it requires a non-null authToken. Data Connect's getWithAuth lets these errors through for this reason.

Auth failures should reject (or at minimum be logged); App Check can reasonably stay fail-open. The test "omits token headers and still executes the request when getToken() rejects" would need updating accordingly.

* Callback invoked when the Firebase Auth token is refreshed or changed.
*/
onAuthTokenChanged = (token: string | null): void => {
logDebug(`Auth Token Changed: ${token}`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[HIGH] The raw Firebase Auth ID token is written to the debug log. (AI code review)

At debug log level this prints a valid bearer JWT (~1h lifetime). Debug logs are routinely pasted into GitHub issues, support tickets and CI output, leaking a credential that can be replayed as that user. Data Connect JS has the same pattern, which is worth fixing too (out of scope for this PR). App Check tokens are bearer credentials too, so any App Check token logging should be redacted the same way.

Log a partially redacted token instead, as the Data Connect Android SDK does in toScrubbedAccessToken(). Keeping a short prefix/suffix lets logs be correlated across token refreshes without exposing the token. It also logs the App Check "placeholder" token (sent when App Check fails to get a real token) unredacted and labeled, which is very helpful when debugging App Check issues. A TypeScript equivalent:

/**
 * The App Check "placeholder" token, i.e. base64 of `{"error":"UNKNOWN_ERROR"}`,
 * sent when App Check fails to obtain a real token.
 * See `formatDummyToken(defaultTokenErrorData)` in `@firebase/app-check`.
 */
const PLACEHOLDER_APP_CHECK_TOKEN = 'eyJlcnJvciI6IlVOS05PV05fRVJST1IifQ==';

/**
 * Returns a partially redacted access token, keeping only a short prefix and
 * suffix so that tokens can be correlated in logs without exposing them.
 */
export function redactToken(token: string | null): string {
  if (token === null) {
    return 'null';
  }
  if (token === PLACEHOLDER_APP_CHECK_TOKEN) {
    return `${token} (the "placeholder" App Check token)`;
  }
  if (token.length < 30) {
    return '<redacted>';
  }
  return `${token.slice(0, 6)}<redacted>${token.slice(-6)}`;
}

Usage: logDebug(`Auth Token Changed: ${redactToken(token)}`);

export class FirebaseSqlTransport implements FirebaseSqlTransportInterface {
readonly settings?: SqlServiceSettings;
protected _appId?: string;
protected _authToken: string | null = null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[LOW] _authToken is written by a listener but never read. (AI code review)

Every request gets its token from authProvider.getToken(), so this field, the listener registered in the constructor (L79), and the mutating onAuthTokenChanged on the public FirebaseSqlTransportInterface appear to be unused, and they add an Auth listener per FirebaseSql instance. Consider removing them until something reads them, or adding a comment noting which upcoming change will use them.

): Record<string, string> {
const headers = new Headers();

mergeHeadersInit(headers, init?.headers);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[LOW] Reserved headers are only rejected in settings.headers; per-request init.headers can still set them. (AI code review)

validateHeaders only runs on settings.headers, while init.headers is merged as-is here. As a result:

  • x-firebase-auth-token / x-firebase-appcheck are only overwritten when the SDK has a token; otherwise a caller-supplied value goes out as-is (the transport test using 'custom-user-jwt' relies on this).
  • X-Firebase-SqlConnect-Affinity, X-Goog-Request-Params, X-Firebase-Client, X-Firebase-Client-Log-Type and x-client-version are never rejected.

go/firebasesql:sdk:headers specifies that setting a "reserved" header MUST result in an exception. Since init.headers will come from postgrest-js, where users can set headers (e.g. .setHeader()), consider applying the same check to init.headers and throwing FirebaseSqlError (Code.INVALID_ARGUMENT), as validateHeaders does for settings.headers. The 'custom-user-jwt' test would then need to expect a rejection.

export const FirebaseHeaders = Object.freeze({
GOOG_API_CLIENT: 'x-goog-api-client' as const satisfies FirebaseHeaders,
CLIENT_VERSION: 'x-client-version' as const satisfies FirebaseHeaders,
FIREBASE_GMPDID: 'x-firebase-gmpid' as const satisfies FirebaseHeaders,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[MEDIUM] Typo in constant name: FIREBASE_GMPDID → FIREBASE_GMPID. (AI code review)

Comment on lines +53 to +65
/** Lowerecased HTTP Header keys forbidden for FSQL SDK */
const FORBIDDEN_HEADERS = new Set<string>([
'X-Firebase-SqlConnect-Affinity',
'X-Goog-Request-Params',
'X-Firebase-Client',
'X-Firebase-Client-Log-Type'
]);

/** @internal */
export const LOWERCASE_DISALLOWED_HEADER_KEYS = new Set<string>([
...Object.values(FirebaseHeaders),
...Array.from(FORBIDDEN_HEADERS).map(key => key.toLowerCase())
]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[LOW] FORBIDDEN_HEADERS is documented as "Lowerecased" but holds mixed-case values, and LOWERCASE_DISALLOWED_HEADER_KEYS is an exported, mutable Set. (AI code review)

Consider writing the FORBIDDEN_HEADERS entries in lowercase (dropping the .map(key => key.toLowerCase())) so the comment holds and fixing the "Lowerecased" typo. Also consider typing the export as ReadonlySet<string> so importers can't .add() / .delete() and change the protected-header policy.

Also, please sync this list with the authoritative "reserved" headers list in go/firebasesql:sdk:headers and leave a comment here indicating such.

Comment on lines +139 to +140
appCheckToken &&
headers.set(FirebaseHeaders.FIREBASE_APPCHECK, appCheckToken.token);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[LOW] Auth and App Check token checks are inconsistent: an empty App Check token string is still sent as a header. (AI code review)

Auth is only attached when authToken.accessToken is truthy, but App Check only checks the result object, so { token: '' } would send an empty x-firebase-appcheck header. Consider checking appCheckToken?.token for consistency. Note that the test "omits token headers when providers return null or empty tokens" doesn't cover this, since MockAppCheckProvider maps '' to null before it reaches resolveRequestHeaders.

expect(result[FirebaseHeaders.FIREBASE_APPCHECK]).toBeUndefined();
});

it('overwrites caller-supplied auth and App Check tokens over SDK tokens', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[LOW] Test name reads as the opposite of what it asserts. (AI code review)

"overwrites caller-supplied auth and App Check tokens over SDK tokens" suggests the caller's tokens win, but the assertions verify the SDK tokens win. Consider renaming to match the equivalent test in transport.test.ts ("...with SDK tokens").

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants