Skip to content

Commit b6fd238

Browse files
authored
Merge ebf6131 into 5adb706
2 parents 5adb706 + ebf6131 commit b6fd238

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)