From 398e42785b97d5aaea97d1dafdc1925b7addff79 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 07:49:15 +0000 Subject: [PATCH] feat(auth): make auth.hasToken() public hasToken() already existed on the internal auth surface for the analytics module. Apps need the same check to skip auth.me() for anonymous visitors (a request that can only 401) without reading localStorage themselves, so move it onto the public AuthModule and drop InternalAuthModule. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017E6ba4b3bzDDW4VXPzWFbw --- packages/sdk/src/modules/analytics.ts | 6 ++--- packages/sdk/src/modules/auth.ts | 4 +-- packages/sdk/src/modules/auth.types.ts | 33 +++++++++++------------ packages/sdk/tests/unit/analytics.test.ts | 6 ++--- 4 files changed, 22 insertions(+), 27 deletions(-) diff --git a/packages/sdk/src/modules/analytics.ts b/packages/sdk/src/modules/analytics.ts index 62fcb27c..5a91b17d 100644 --- a/packages/sdk/src/modules/analytics.ts +++ b/packages/sdk/src/modules/analytics.ts @@ -9,7 +9,7 @@ import { SessionContext, } from "./analytics.types"; import { getSharedInstance } from "../utils/sharedInstance.js"; -import type { InternalAuthModule } from "./auth.types"; +import type { AuthModule } from "./auth.types"; import { generateUuid, isReactNative } from "../utils/common.js"; export const USER_HEARTBEAT_EVENT_NAME = "__user_heartbeat_event__"; @@ -61,7 +61,7 @@ export interface AnalyticsModuleArgs { axiosClient: AxiosInstance; serverUrl: string; appId: string; - userAuthModule: InternalAuthModule; + userAuthModule: AuthModule; enabled: boolean; } @@ -360,7 +360,7 @@ export function resetAnalyticsSessionContext() { } async function getSessionContext( - userAuthModule: InternalAuthModule + userAuthModule: AuthModule ): Promise { if (!analyticsSharedState.sessionContext) { // With no token there is no identity to resolve: `me()` can only answer 401, diff --git a/packages/sdk/src/modules/auth.ts b/packages/sdk/src/modules/auth.ts index b9e23747..111b886d 100644 --- a/packages/sdk/src/modules/auth.ts +++ b/packages/sdk/src/modules/auth.ts @@ -1,7 +1,7 @@ import { AxiosInstance } from "axios"; import { AuthModuleOptions, - InternalAuthModule, + AuthModule, User, VerifyOtpParams, ChangePasswordParams, @@ -92,7 +92,7 @@ export function createAuthModule( functionsAxiosClient: AxiosInstance, appId: string, options: AuthModuleOptions -): InternalAuthModule { +): AuthModule { // In-flight `me()` request, shared by concurrent callers. The analytics // module resolves its session context through `me()` at client construction, // at the same moment most apps issue their own `me()`. Browsers serialize the diff --git a/packages/sdk/src/modules/auth.types.ts b/packages/sdk/src/modules/auth.types.ts index 7c080efe..c99ba43c 100644 --- a/packages/sdk/src/modules/auth.types.ts +++ b/packages/sdk/src/modules/auth.types.ts @@ -344,6 +344,21 @@ export interface AuthModule { */ isAuthenticated(): Promise; + /** + * Checks whether an access token is currently set on the client. + * + * Reports only that a token is present, not that it's valid: an expired or revoked token still returns `true`. Use it to skip requests that can't succeed without a session, such as {@linkcode me | me()} for an anonymous visitor, and use {@linkcode isAuthenticated | isAuthenticated()} to confirm a session with the server. The token is the one passed to {@linkcode createClient | createClient()} or set with {@linkcode setToken | setToken()}, and {@linkcode logout | logout()} clears it. A session held only in an HTTP-only cookie isn't visible to the client and returns `false`. + * + * @returns `true` if the client has an access token, `false` otherwise. + * + * @example + * ```typescript + * // Skip the user lookup for anonymous visitors + * const user = base44.auth.hasToken() ? await base44.auth.me() : null; + * ``` + */ + hasToken(): boolean; + /** * Invites a user to the app. * @@ -552,21 +567,3 @@ export interface AuthModule { */ changePassword(params: ChangePasswordParams): Promise; } - -/** - * The auth module as constructed internally, before it is narrowed to - * {@link AuthModule} on the public client. Not exported from the package - * index — SDK consumers see only {@link AuthModule}. - * - * @internal - */ -export interface InternalAuthModule extends AuthModule { - /** - * Whether an access token is currently set on the client. - * - * Reports only the presence of a token, never its validity — an expired or - * revoked token still reads as `true`. Callers use this to skip requests that - * could not succeed without a session, not to decide that one is valid. - */ - hasToken(): boolean; -} diff --git a/packages/sdk/tests/unit/analytics.test.ts b/packages/sdk/tests/unit/analytics.test.ts index d851e99c..bad1da6e 100644 --- a/packages/sdk/tests/unit/analytics.test.ts +++ b/packages/sdk/tests/unit/analytics.test.ts @@ -7,7 +7,7 @@ import { } from "../../src/index.ts"; import { getSharedInstance } from "../../src/utils/sharedInstance.ts"; import { resetAnalyticsSessionContext } from "../../src/modules/analytics.ts"; -import { InternalAuthModule, User } from "../../src/modules/auth.types.ts"; +import { User } from "../../src/modules/auth.types.ts"; import { AxiosInstance } from "axios"; describe("Analytics Module", () => { @@ -246,9 +246,7 @@ describe("Analytics Module", () => { test("should report token presence across identity changes", () => { const client = createClient({ serverUrl, appId }); - // `hasToken` lives on the internal auth surface only; the public client - // narrows to AuthModule, so reach past the narrowing deliberately here. - const auth = client.auth as InternalAuthModule; + const auth = client.auth; expect(auth.hasToken()).toBe(false);