Skip to content

Commit 4ab4f9a

Browse files
davidsuDavid Susskind
andauthored
feat(app): add base44.app.getPublicSettings() (#268)
The generated app templates read the app's access policy by hand-rolling an axios client from a deep, non-public path: import { createAxiosClient } from '@base44/sdk/dist/utils/axios-client'; That path is not formal API — it resolves today only because the package ships no exports map, and it breaks silently the moment one is added. It also forces the template to hold the access token itself and to know the API route. base44.app.getPublicSettings() moves both concerns into the SDK: the request goes out on the client's own axios instance, so it carries the client's token and the caller handles neither. Gating still surfaces as a Base44Error with status 403 and data.extra_data.reason, which is what the templates branch on. The public_settings union moves out of AppLike so the response type and the app record share one definition. Co-authored-by: David Susskind <dev@example.com>
1 parent 5adb706 commit 4ab4f9a

7 files changed

Lines changed: 173 additions & 6 deletions

File tree

‎scripts/mintlify-post-processing/types-to-expose.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
"AiGatewayModule",
77
"AnalyticsModule",
88
"AppLogsModule",
9+
"AppModule",
10+
"AppPublicSettingsResponse",
911
"AuthModule",
1012
"ConnectorApiRequest",
1113
"ConnectorApiResponse",

‎src/client.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { createFunctionsModule } from "./modules/functions.js";
1212
import { createAgentsModule } from "./modules/agents.js";
1313
import { createAiGatewayModule } from "./modules/ai-gateway.js";
1414
import { createAppLogsModule } from "./modules/app-logs.js";
15+
import { createAppModule } from "./modules/app.js";
1516
import { createUsersModule } from "./modules/users.js";
1617
import { RoomsSocket, RoomsSocketConfig } from "./utils/socket-utils.js";
1718
import type {
@@ -240,6 +241,7 @@ export function createClient(config: CreateClientConfig): Base44Client {
240241
}),
241242
aiGateway: createAiGatewayModule({ serverUrl, token, appId }),
242243
appLogs: createAppLogsModule(axiosClient, appId),
244+
app: createAppModule(axiosClient, appId),
243245
users: createUsersModule(axiosClient, appId),
244246
analytics: createAnalyticsModule({
245247
axiosClient,

‎src/client.types.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { FunctionsModule } from "./modules/functions.types.js";
1010
import type { AgentsModule } from "./modules/agents.types.js";
1111
import type { AiGatewayModule } from "./modules/ai-gateway.types.js";
1212
import type { AppLogsModule } from "./modules/app-logs.types.js";
13+
import type { AppModule } from "./modules/app.types.js";
1314
import type { AnalyticsModule } from "./modules/analytics.types.js";
1415
import type { ActorsModule } from "./modules/actors.types.js";
1516

@@ -107,6 +108,8 @@ export interface Base44Client {
107108
analytics: AnalyticsModule;
108109
/** {@link AppLogsModule | App logs module} for tracking app usage. */
109110
appLogs: AppLogsModule;
111+
/** {@link AppModule | App module} for reading the app's own public configuration. */
112+
app: AppModule;
110113
/** {@link ActorsModule | Actors module} for subscribing to and sending messages via Cloudflare Durable Object-backed Actors. */
111114
actors: ActorsModule;
112115
/** {@link AuthModule | Auth module} for user authentication and management. */

‎src/index.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@ export type {
106106
} from "./modules/ai-gateway.types.js";
107107

108108
export type { AppLogsModule } from "./modules/app-logs.types.js";
109+
export type {
110+
AppModule,
111+
AppPublicSettings,
112+
AppPublicSettingsResponse,
113+
} from "./modules/app.types.js";
109114

110115
export type {
111116
ActorsModule,

‎src/modules/app.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { AxiosInstance } from "axios";
2+
import { AppModule } from "./app.types";
3+
4+
/**
5+
* Creates the app module for the Base44 SDK.
6+
*
7+
* @param axios - Axios instance
8+
* @param appId - Application ID
9+
* @returns App module for reading the app's own configuration
10+
* @internal
11+
*/
12+
export function createAppModule(
13+
axios: AxiosInstance,
14+
appId: string
15+
): AppModule {
16+
return {
17+
async getPublicSettings() {
18+
return axios.get(`/apps/public/prod/public-settings/by-id/${appId}`);
19+
},
20+
};
21+
}

‎src/modules/app.types.ts‎

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,63 @@
1+
/**
2+
* The app's access policy: whether the app is reachable without an account, and
3+
* who may sign in.
4+
*/
5+
export type AppPublicSettings =
6+
| "private_with_login"
7+
| "public_with_login"
8+
| "public_without_login"
9+
| "workspace_with_login"
10+
| string;
11+
12+
/**
13+
* The app's public configuration, as returned by {@link AppModule.getPublicSettings}.
14+
*/
15+
export interface AppPublicSettingsResponse {
16+
/** The app's ID. */
17+
id: string;
18+
/** The app's access policy. */
19+
public_settings: AppPublicSettings;
20+
}
21+
22+
/**
23+
* App module for reading the app's own public configuration.
24+
*
25+
* Use it to discover how the app is gated before rendering it, so a private app
26+
* can send the visitor to login instead of rendering an empty shell.
27+
*
28+
* ## Authentication Modes
29+
*
30+
* This module is available to use with a client in all authentication modes. The
31+
* client's token, when it has one, is sent with the request — a signed-in visitor
32+
* who has no access to the app is reported differently from an anonymous one.
33+
*/
34+
export interface AppModule {
35+
/**
36+
* Get the app's public configuration.
37+
*
38+
* Rejects with a {@linkcode Base44Error} when the visitor may not open the app:
39+
* `status` is `403` and `data.extra_data.reason` says why — `"auth_required"`
40+
* when the visitor must sign in, `"user_not_registered"` when the signed-in
41+
* visitor has no access to this app.
42+
*
43+
* @returns Promise resolving to the app's ID and access policy.
44+
*
45+
* @example
46+
* ```typescript
47+
* // Decide what to render before the app boots
48+
* try {
49+
* const { public_settings } = await base44.app.getPublicSettings();
50+
* console.log('App access policy:', public_settings);
51+
* } catch (error) {
52+
* if (error.status === 403) {
53+
* console.log('Blocked because:', error.data?.extra_data?.reason);
54+
* }
55+
* }
56+
* ```
57+
*/
58+
getPublicSettings(): Promise<AppPublicSettingsResponse>;
59+
}
60+
161
/**
262
* @internal
363
*/
@@ -59,12 +119,7 @@ export interface AppLike {
59119
agents?: Record<string, any>;
60120
logo_url?: string;
61121
slug?: string;
62-
public_settings?:
63-
| "private_with_login"
64-
| "public_with_login"
65-
| "public_without_login"
66-
| "workspace_with_login"
67-
| string;
122+
public_settings?: AppPublicSettings;
68123
is_blocked?: boolean;
69124
github_repo_url?: string;
70125
main_page?: string;

‎tests/unit/app.test.ts‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
2+
import nock from "nock";
3+
import { Base44Error, createClient } from "../../src/index.ts";
4+
5+
describe("App module", () => {
6+
const appId = "test-app-id";
7+
const serverUrl = "https://base44.app";
8+
const token = "user-token-456";
9+
const publicSettingsPath = `/api/apps/public/prod/public-settings/by-id/${appId}`;
10+
let base44: ReturnType<typeof createClient>;
11+
let scope: nock.Scope;
12+
13+
beforeEach(() => {
14+
base44 = createClient({ serverUrl, appId, token });
15+
scope = nock(serverUrl);
16+
});
17+
18+
afterEach(() => {
19+
nock.cleanAll();
20+
});
21+
22+
test("getPublicSettings returns the app id and its access policy", async () => {
23+
scope
24+
.get(publicSettingsPath)
25+
.reply(200, { id: appId, public_settings: "public_without_login" });
26+
27+
const settings = await base44.app.getPublicSettings();
28+
29+
expect(settings).toEqual({
30+
id: appId,
31+
public_settings: "public_without_login",
32+
});
33+
expect(scope.isDone()).toBe(true);
34+
});
35+
36+
test("getPublicSettings authenticates with the client's token, so callers never handle it", async () => {
37+
scope
38+
.get(publicSettingsPath)
39+
.matchHeader("Authorization", `Bearer ${token}`)
40+
.reply(200, { id: appId, public_settings: "private_with_login" });
41+
42+
await base44.app.getPublicSettings();
43+
44+
expect(scope.isDone()).toBe(true);
45+
});
46+
47+
test("getPublicSettings sends no Authorization header for an anonymous client", async () => {
48+
const anonymous = createClient({ serverUrl, appId });
49+
50+
scope
51+
.get(publicSettingsPath)
52+
.matchHeader("Authorization", (value) => value === undefined)
53+
.reply(200, { id: appId, public_settings: "public_without_login" });
54+
55+
await anonymous.app.getPublicSettings();
56+
57+
expect(scope.isDone()).toBe(true);
58+
});
59+
60+
test.each([
61+
["auth_required", "the visitor must sign in"],
62+
["user_not_registered", "the visitor has no access to this app"],
63+
])(
64+
"getPublicSettings surfaces a 403 %s as a Base44Error carrying the reason",
65+
async (reason) => {
66+
scope
67+
.get(publicSettingsPath)
68+
.reply(403, { extra_data: { app_id: appId, reason } });
69+
70+
const error = await base44.app
71+
.getPublicSettings()
72+
.catch((rejection) => rejection);
73+
74+
expect(error).toBeInstanceOf(Base44Error);
75+
expect(error.status).toBe(403);
76+
expect(error.data.extra_data.reason).toBe(reason);
77+
}
78+
);
79+
});

0 commit comments

Comments
 (0)