Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 85 additions & 20 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import {
createUserConnectorsModule,
} from "./modules/connectors.js";
import { getAccessToken } from "./utils/auth-utils.js";
import {
exchangeEmbedToken,
takeEmbedTokenFromUrl,
} from "./utils/embed-session.js";
import { createFetchWithAuth } from "./utils/fetch-with-auth.js";
import { createFunctionsModule } from "./modules/functions.js";
import { createAgentsModule } from "./modules/agents.js";
Expand Down Expand Up @@ -79,7 +83,6 @@ export function createClient(config: CreateClientConfig): Base44Client {
serverUrl = "https://base44.app",
appId,
analytics,
token,
serviceToken,
requiresAuth = false,
appBaseUrl,
Expand All @@ -91,12 +94,19 @@ export function createClient(config: CreateClientConfig): Base44Client {
// Normalize appBaseUrl to always be a string (empty if not provided or invalid)
const normalizedAppBaseUrl = typeof appBaseUrl === "string" ? appBaseUrl : "";

const embedOtt = takeEmbedTokenFromUrl();

// A declaration, not a const: this block sits above the auth module.
function getToken(): string | null {
return userAuthModule.getToken() ?? (embedOtt ? null : getAccessToken());
}

const socketConfig: RoomsSocketConfig = {
serverUrl,
mountPath: "/ws-user-apps/socket.io/",
transports: ["websocket"],
appId,
token,
getToken,
};

let socket: ReturnType<typeof RoomsSocket> | null = null;
Expand All @@ -110,6 +120,10 @@ export function createClient(config: CreateClientConfig): Base44Client {
return socket;
};

// Apps pass getAccessToken() in as `token`, which in a frame is the OTT —
// what the exchange trades for a session, never a bearer itself.
const token = embedOtt ? undefined : config.token;

const headers = {
...optionalHeaders,
"X-App-Id": String(appId),
Expand Down Expand Up @@ -174,20 +188,65 @@ export function createClient(config: CreateClientConfig): Base44Client {
appBaseUrl: normalizedAppBaseUrl,
serverUrl,
token,
embedded: Boolean(embedOtt),
// The socket carries its token on the handshake, so it can only pick a
// new one up by redialling — or, on logout, by dropping what it has.
onSessionChange: (hasSession) =>
hasSession ? socket?.reconnect() : socket?.disconnect(),
}
);

// Apply the access token before any module that may issue authenticated
// requests during construction (notably analytics, which fires an init
// event whose flush calls auth.me()). Without this, the first User/me
// request is built before setToken runs and goes out unauthenticated.
if (typeof window !== "undefined") {
// Not in a frame: a stored token there belongs to an earlier visitor.
if (typeof window !== "undefined" && !embedOtt) {
const accessToken = token || getAccessToken();
if (accessToken) {
userAuthModule.setToken(accessToken);
}
}

const session = embedOtt
? exchangeEmbedToken({ serverUrl, appId, ott: embedOtt })
: null;

// Never rejects: every request waits on it, so one failure here must not
// become a rejection on each of them.
const authReady: Promise<void> = session
? session
.then((sessionToken) => {
if (sessionToken) {
userAuthModule.setToken(sessionToken, false);
return;
}
const error = new Error(
"Base44: the embed token was refused, so this app is not signed in.",
);
console.error(error.message);
options?.onError?.(error);
})
.catch((e) => {
console.error("Base44: applying the embedded session failed:", e);
})
: Promise.resolve();

if (session) {
// Registered after createAxiosClient's so it runs first (axios unshifts),
// letting the anonymous-visitor header see the Authorization we just set.
for (const client of [axiosClient, functionsAxiosClient]) {
client.interceptors.request.use(async (requestConfig) => {
await authReady;
const sessionToken = getToken();
if (sessionToken && !requestConfig.headers.get("Authorization")) {
requestConfig.headers.set("Authorization", `Bearer ${sessionToken}`);
}
return requestConfig;
});
}
}

const actorsModule = createActorsModule({
appId,
// serverUrl is often relative/empty (same-origin app); the proxy-fallback
Expand All @@ -197,9 +256,13 @@ export function createClient(config: CreateClientConfig): Base44Client {
typeof window !== "undefined" ? window.location?.origin : undefined,
),
functionsVersion,
getAuthToken: () => token || getAccessToken(),
getAuthToken: async () => {
await authReady;
return getToken();
},
mintConnectionToken: async (actorName, room, connectionId) => {
const authToken = token || getAccessToken();
await authReady;
const authToken = getToken();
return await actorsAxiosClient.post<unknown, ActorConnectionCredentials>(
`/apps/${appId}/actors/${encodeURIComponent(actorName)}/connection-token`,
{ room, connection_id: connectionId },
Expand Down Expand Up @@ -229,12 +292,12 @@ export function createClient(config: CreateClientConfig): Base44Client {
connectors: createUserConnectorsModule(axiosClient, appId),
auth: userAuthModule,
functions: createFunctionsModule(functionsAxiosClient, appId, {
waitForAuth: () => authReady,
getAuthHeaders: () => {
const headers: Record<string, string> = {};
// Get current token from storage or initial config
const currentToken = token || getAccessToken();
if (currentToken) {
headers["Authorization"] = `Bearer ${currentToken}`;
const sessionToken = getToken();
if (sessionToken) {
headers["Authorization"] = `Bearer ${sessionToken}`;
}
return headers;
},
Expand All @@ -245,9 +308,10 @@ export function createClient(config: CreateClientConfig): Base44Client {
getSocket,
appId,
serverUrl,
token,
// Sync, unlike everything else: these return a URL, not a promise.
getToken,
}),
aiGateway: createAiGatewayModule({ serverUrl, token, appId }),
aiGateway: createAiGatewayModule({ serverUrl, getToken, appId }),
appLogs: createAppLogsModule(axiosClient, appId),
app: createAppModule(axiosClient, appId),
users: createUsersModule(axiosClient, appId),
Expand Down Expand Up @@ -293,9 +357,15 @@ export function createClient(config: CreateClientConfig): Base44Client {
getSocket,
appId,
serverUrl,
token,
// The user's token, deliberately: this is read only for the `?token=` on
// a channel URL handed to that user. Never the service credential.
getToken,
}),
aiGateway: createAiGatewayModule({
serverUrl,
getToken: () => serviceToken,
appId,
}),
aiGateway: createAiGatewayModule({ serverUrl, token: serviceToken, appId }),
appLogs: createAppLogsModule(serviceRoleAxiosClient, appId),
cleanup: () => {
if (socket) {
Expand Down Expand Up @@ -332,6 +402,7 @@ export function createClient(config: CreateClientConfig): Base44Client {
serverUrl,
functionsVersion,
platformHeaders: optionalHeaders,
waitForAuth: () => authReady,
}),

/**
Expand All @@ -350,13 +421,7 @@ export function createClient(config: CreateClientConfig): Base44Client {
* ```
*/
setToken(newToken: string) {
userModules.auth.setToken(newToken);
if (socket) {
socket.updateConfig({
token: newToken,
});
}
socketConfig.token = newToken;
userAuthModule.setToken(newToken, true);
},

/**
Expand Down
7 changes: 4 additions & 3 deletions src/modules/actors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ interface ActorsConfig {
appId: string;
/** Current user access token, if authenticated. Rides the WS query on the
* proxy-fallback path so the platform proxy can authenticate the connection;
* anonymous connects omit it. */
getAuthToken(): string | null | undefined;
* anonymous connects omit it. Awaited per dial, so a session still being
* exchanged is in hand before the URL is built. */
getAuthToken(): Promise<string | null | undefined>;
/** Same semantics as function calls: editors with a non-prod version get the
* draft actor script; everyone else gets the published one. */
functionsVersion?: string;
Expand Down Expand Up @@ -150,7 +151,7 @@ class Connection {
instanceId,
this.id,
config.appId,
config.getAuthToken(),
await config.getAuthToken(),
config.functionsVersion,
);
};
Expand Down
7 changes: 3 additions & 4 deletions src/modules/agents.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { getAccessToken } from "../utils/auth-utils.js";
import { ModelFilterParams } from "../types.js";
import {
AgentConversation,
Expand All @@ -13,7 +12,7 @@ export function createAgentsModule({
getSocket,
appId,
serverUrl,
token,
getToken,
}: AgentsModuleConfig): AgentsModule {
const baseURL = `/apps/${appId}/agents`;

Expand Down Expand Up @@ -102,7 +101,7 @@ export function createAgentsModule({
const baseUrl = `${serverUrl}/api/apps/${appId}/agents/${encodeURIComponent(
agentName
)}/whatsapp`;
const accessToken = token ?? getAccessToken();
const accessToken = getToken();

if (accessToken) {
return `${baseUrl}?token=${accessToken}`;
Expand All @@ -116,7 +115,7 @@ export function createAgentsModule({
const baseUrl = `${serverUrl}/api/apps/${appId}/agents/${encodeURIComponent(
agentName
)}/telegram`;
const accessToken = token ?? getAccessToken();
const accessToken = getToken();

if (accessToken) {
return `${baseUrl}?token=${accessToken}`;
Expand Down
12 changes: 8 additions & 4 deletions src/modules/agents.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,8 @@ export interface AgentsModuleConfig {
appId: string;
/** Server URL */
serverUrl?: string;
/** Authentication token */
token?: string;
/** Returns the current authentication token, if any */
getToken: () => string | null;
}

/**
Expand Down Expand Up @@ -391,7 +391,9 @@ export interface AgentsModule {
* Gets WhatsApp connection URL for an agent.
*
* Generates a URL that users can use to connect with the agent through WhatsApp.
* The URL includes authentication if a token is available.
* The URL includes authentication if a token is available. In an app a
* platform has embedded, that is only once the session has been exchanged —
* await a call such as `base44.auth.me()` before building the URL.
*
* @param agentName - The name of the agent.
* @returns WhatsApp connection URL.
Expand All @@ -410,7 +412,9 @@ export interface AgentsModule {
* Gets Telegram connection URL for an agent.
*
* Generates a URL that users can use to connect with the agent through Telegram.
* The URL includes authentication if a token is available. When the user opens
* The URL includes authentication if a token is available. In an app a
* platform has embedded, that is only once the session has been exchanged —
* await a call such as `base44.auth.me()` before building the URL. When the user opens
* this URL, they are redirected to the agent's Telegram bot with an activation
* code that securely links their account.
*
Expand Down
5 changes: 2 additions & 3 deletions src/modules/ai-gateway.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { getAccessToken } from "../utils/auth-utils.js";
import {
AiGatewayModule,
AiGatewayModuleConfig,
Expand All @@ -7,12 +6,12 @@ import {

export function createAiGatewayModule({
serverUrl,
token,
getToken,
appId,
}: AiGatewayModuleConfig): AiGatewayModule {
const connection = (): AiGatewayConnection => ({
baseURL: `${serverUrl}/api/apps/${appId}/ai/openai/v1`,
token: token ?? getAccessToken() ?? "",
token: getToken() ?? "",
});

return {
Expand Down
4 changes: 2 additions & 2 deletions src/modules/ai-gateway.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ export interface AiGatewayConnection {
export interface AiGatewayModuleConfig {
/** Server URL */
serverUrl?: string;
/** Authentication token */
token?: string;
/** Returns the current authentication token, if any */
getToken: () => string | null | undefined;
/** Application ID */
appId: string;
}
Expand Down
Loading
Loading