@base44-preview/sdk 0.8.46-pr.278.d5b4fe4 → 0.8.47-pr.279.adde4da

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -5,6 +5,7 @@ import { createAuthModule } from "./modules/auth.js";
5
5
  import { createSsoModule } from "./modules/sso.js";
6
6
  import { createConnectorsModule, createUserConnectorsModule, } from "./modules/connectors.js";
7
7
  import { getAccessToken } from "./utils/auth-utils.js";
8
+ import { createFetchWithAuth } from "./utils/fetch-with-auth.js";
8
9
  import { createFunctionsModule } from "./modules/functions.js";
9
10
  import { createAgentsModule } from "./modules/agents.js";
10
11
  import { createAiGatewayModule } from "./modules/ai-gateway.js";
@@ -260,6 +261,15 @@ export function createClient(config) {
260
261
  // Assemble and return the client
261
262
  const client = {
262
263
  ...userModules,
264
+ /** See {@link Base44Client.fetchWithAuth}. */
265
+ fetchWithAuth: createFetchWithAuth({
266
+ axios: axiosClient,
267
+ serviceRoleAxios: serviceRoleAxiosClient,
268
+ appId: String(appId),
269
+ serverUrl,
270
+ functionsVersion,
271
+ platformHeaders: optionalHeaders,
272
+ }),
263
273
  /**
264
274
  * Sets a new authentication token for all subsequent requests.
265
275
  *
@@ -10,6 +10,7 @@ import type { AppLogsModule } from "./modules/app-logs.types.js";
10
10
  import type { AppModule } from "./modules/app.types.js";
11
11
  import type { AnalyticsModule } from "./modules/analytics.types.js";
12
12
  import type { ActorsModule } from "./modules/actors.types.js";
13
+ import type { FetchWithAuthInit } from "./utils/fetch-with-auth.js";
13
14
  /**
14
15
  * Options for creating a Base44 client.
15
16
  */
@@ -139,6 +140,60 @@ export interface Base44Client {
139
140
  integrations: IntegrationsModule;
140
141
  /** Cleanup function to disconnect WebSocket connections. Call when you're done with the client. */
141
142
  cleanup: () => void;
143
+ /**
144
+ * Calls one of your app's own server routes with this client's credentials attached.
145
+ *
146
+ * Base44 keeps a user's access token in the browser's local storage, and the platform puts its own headers on a server request — so a plain `fetch()` to your app's routes carries neither, and the route sees an anonymous caller with no way to build a client. `fetchWithAuth()` is the same `fetch()` with whatever this client holds added, which is what lets the route act on behalf of the caller.
147
+ *
148
+ * What that means depends on where the client came from, because a client can only send what it has:
149
+ *
150
+ * - In a browser, from {@linkcode createClient | createClient()}: the signed-in user's `Authorization: Bearer <token>`. When nobody is signed in the request goes without it, so routes open to anonymous callers keep working.
151
+ * - In one of your server routes, from {@linkcode createClientFromRequest | createClientFromRequest()}: everything that function reads back — the caller's token, `Base44-App-Id`, `Base44-Api-Url`, `Base44-Functions-Version`, the signed `Base44-State`, `X-Data-Env`, and the app's per-request service credential. The callee's own `createClientFromRequest()` then rebuilds the client you are holding, service role included.
152
+ *
153
+ * That second case is why route-to-route calls need this. A sub-request carries nothing from the request that triggered it — your framework builds it from your arguments alone — so a route reached by a plain `fetch()` sees no headers at all and its `createClientFromRequest()` throws on the missing `Base44-App-Id`.
154
+ *
155
+ * Requests are restricted to your app's own origin, which is what keeps these credentials inside your app: pass a relative path beginning with a single `/`, such as `/api/orders`. An absolute URL, a protocol-relative `//host`, or anything else a URL parser would read as another origin throws. To call a Base44 backend function, use {@linkcode FunctionsModule.fetch | functions.fetch()}; for another origin, use plain `fetch()`.
156
+ *
157
+ * Two routes that need the same logic should call a shared function rather than each other — cheaper than an HTTP round trip, and it needs no headers at all. Hop when the hop is the point: rendering a page server-side, or going through a route for its own caching and route rules.
158
+ *
159
+ * @param path - A relative path on your app's own origin, such as `/api/orders`.
160
+ * @param init - Optional [`RequestInit`](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit) options such as `method`, `headers`, `body`, and `signal`, plus `fetch`: the transport that resolves a root-relative path against your app's routes. It defaults to the global `fetch`, which does that in a browser but not on a server — in Nitro pass its own (`import { fetch } from "nitro"`), which dispatches in-process with no network hop. Any header you set yourself is kept, so you can deliberately hand the callee a different identity.
161
+ * @returns Promise resolving to a native [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response).
162
+ * @throws {Error} When `path` is not a relative path on your app's own origin.
163
+ *
164
+ * @example
165
+ * ```typescript
166
+ * // Browser: call your app's own server route as the signed-in user
167
+ * const response = await base44.fetchWithAuth('/api/orders');
168
+ * const orders = await response.json();
169
+ * ```
170
+ *
171
+ * @example
172
+ * ```typescript
173
+ * // POST with a JSON body
174
+ * const response = await base44.fetchWithAuth('/api/orders', {
175
+ * method: 'POST',
176
+ * headers: { 'Content-Type': 'application/json' },
177
+ * body: JSON.stringify({ productId: 'abc', quantity: 2 }),
178
+ * });
179
+ *
180
+ * if (!response.ok) {
181
+ * throw new Error(`Request failed: ${response.status}`);
182
+ * }
183
+ * ```
184
+ *
185
+ * @example
186
+ * ```typescript
187
+ * // Server-side render: reach the app's own route as this request
188
+ * import { fetch } from 'nitro';
189
+ * import { createClientFromRequest } from '@base44/sdk';
190
+ *
191
+ * const base44 = createClientFromRequest(event.req);
192
+ * const response = await base44.fetchWithAuth('/api/items', { fetch });
193
+ * const items = await response.json();
194
+ * ```
195
+ */
196
+ fetchWithAuth(path: string, init?: FetchWithAuthInit): Promise<Response>;
142
197
  /**
143
198
  * Sets a new authentication token for all subsequent requests.
144
199
  *
@@ -0,0 +1,46 @@
1
+ import type { AxiosInstance } from "axios";
2
+ /** `fetch`-compatible transport that can resolve a root-relative path. */
3
+ type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
4
+ /** Options for {@link Base44Client.fetchWithAuth}. */
5
+ export interface FetchWithAuthInit extends RequestInit {
6
+ /**
7
+ * The `fetch` that resolves a root-relative path against your app's own
8
+ * routes. Defaults to the global `fetch`, which does that in a browser but
9
+ * not on a server, where a path with no origin has nothing to resolve
10
+ * against. In Nitro pass its own, which routes a leading-slash path
11
+ * in-process: `import { fetch } from "nitro"`.
12
+ */
13
+ fetch?: FetchLike;
14
+ }
15
+ /**
16
+ * Builds the client's `fetchWithAuth`: a `fetch` to the app's own origin that
17
+ * carries whatever credentials this client holds.
18
+ *
19
+ * That is the user's access token in a browser, and from
20
+ * `createClientFromRequest()` the full set that function reads back — so a
21
+ * route reached this way rebuilds the caller's client, service role included.
22
+ * The service credential is minted per request for the app as a whole, not for
23
+ * one route, so a handler in the same worker already runs with that authority;
24
+ * reaching it through a route hop is the privilege it would have had by
25
+ * importing a shared function. What must never happen is a credential leaving
26
+ * the app, and the relative-path rule, not a shorter header list, is what
27
+ * prevents that.
28
+ *
29
+ * @param axios - The user-scoped instance. Its `Authorization` default is the
30
+ * live token: it follows `setToken()` and is deleted on `logout()`, so a
31
+ * request never carries a token the user no longer has. From
32
+ * `createClientFromRequest()` it holds the caller's own token.
33
+ * @param serviceRoleAxios - The service-role instance, holding the app's
34
+ * per-request credential as its own `Authorization` default. A browser client
35
+ * has none, so nothing is sent.
36
+ * @internal
37
+ */
38
+ export declare function createFetchWithAuth({ axios, serviceRoleAxios, appId, serverUrl, functionsVersion, platformHeaders, }: {
39
+ axios: AxiosInstance;
40
+ serviceRoleAxios: AxiosInstance;
41
+ appId: string;
42
+ serverUrl: string;
43
+ functionsVersion?: string;
44
+ platformHeaders?: Record<string, string>;
45
+ }): (path: string, init?: FetchWithAuthInit) => Promise<Response>;
46
+ export {};
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Builds the client's `fetchWithAuth`: a `fetch` to the app's own origin that
3
+ * carries whatever credentials this client holds.
4
+ *
5
+ * That is the user's access token in a browser, and from
6
+ * `createClientFromRequest()` the full set that function reads back — so a
7
+ * route reached this way rebuilds the caller's client, service role included.
8
+ * The service credential is minted per request for the app as a whole, not for
9
+ * one route, so a handler in the same worker already runs with that authority;
10
+ * reaching it through a route hop is the privilege it would have had by
11
+ * importing a shared function. What must never happen is a credential leaving
12
+ * the app, and the relative-path rule, not a shorter header list, is what
13
+ * prevents that.
14
+ *
15
+ * @param axios - The user-scoped instance. Its `Authorization` default is the
16
+ * live token: it follows `setToken()` and is deleted on `logout()`, so a
17
+ * request never carries a token the user no longer has. From
18
+ * `createClientFromRequest()` it holds the caller's own token.
19
+ * @param serviceRoleAxios - The service-role instance, holding the app's
20
+ * per-request credential as its own `Authorization` default. A browser client
21
+ * has none, so nothing is sent.
22
+ * @internal
23
+ */
24
+ export function createFetchWithAuth({ axios, serviceRoleAxios, appId, serverUrl, functionsVersion, platformHeaders, }) {
25
+ const inherited = new Headers(platformHeaders);
26
+ const bearer = (client) => {
27
+ const header = client.defaults.headers.common["Authorization"];
28
+ return typeof header === "string" && header.startsWith("Bearer ")
29
+ ? header
30
+ : null;
31
+ };
32
+ return async function fetchWithAuth(path, init = {}) {
33
+ assertOwnOriginPath(path);
34
+ const { fetch: transport = fetch, ...requestInit } = init;
35
+ const headers = new Headers(init.headers);
36
+ // A caller-supplied value always wins, so a route can hand the callee a
37
+ // different identity on purpose (say, dropping Authorization to render a
38
+ // page as anonymous).
39
+ const inherit = (name, value) => {
40
+ if (value && !headers.has(name))
41
+ headers.set(name, value);
42
+ };
43
+ // Exactly what createClientFromRequest reads, so the callee can rebuild
44
+ // this client. Keep the two in step.
45
+ inherit("Authorization", bearer(axios));
46
+ inherit("Base44-Service-Authorization", bearer(serviceRoleAxios));
47
+ inherit("Base44-App-Id", appId);
48
+ inherit("Base44-Api-Url", serverUrl);
49
+ inherit("Base44-Functions-Version", functionsVersion);
50
+ inherit("Base44-State", inherited.get("Base44-State"));
51
+ inherit("X-Data-Env", inherited.get("X-Data-Env"));
52
+ // The path is passed through untouched: resolving it here would need a
53
+ // document, and a root-relative path is already what a runtime that
54
+ // dispatches in-process (Nitro's `fetch`) expects. `host` is deliberately
55
+ // never sent — such a runtime synthesizes the sub-request's origin from it,
56
+ // so forwarding the inbound one would point the hop at another host.
57
+ return transport(path, { ...requestInit, headers });
58
+ };
59
+ }
60
+ function assertOwnOriginPath(path) {
61
+ if (typeof path !== "string" || path === "") {
62
+ throw new Error("fetchWithAuth() requires a path, such as '/api/orders'.");
63
+ }
64
+ // Check what a URL parser would see, not the raw string: it drops every ASCII
65
+ // tab/newline anywhere in the input and trims leading C0/space, so
66
+ // "/<tab>/evil.example" would pass a naive prefix check and then resolve to
67
+ // another host.
68
+ const asParsed = path.replace(/[\t\n\r]/g, "").replace(/^[\x00-\x20]+/, "");
69
+ // One leading slash is the whole rule: it cannot carry a scheme, and it rules
70
+ // out the two forms that reach another origin — "//host" and, since URL
71
+ // parsing treats a backslash as a slash, "/\host".
72
+ if (!asParsed.startsWith("/") ||
73
+ asParsed.startsWith("//") ||
74
+ asParsed.startsWith("/\\")) {
75
+ throw new Error(`fetchWithAuth() only sends requests to your app's own origin, so your app's credentials never reach a third party. "${path}" is not a path on it — pass a relative path such as '/api/orders'. Use base44.functions.fetch() to call a Base44 backend function, or plain fetch() for another origin.`);
76
+ }
77
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base44-preview/sdk",
3
- "version": "0.8.46-pr.278.d5b4fe4",
3
+ "version": "0.8.47-pr.279.adde4da",
4
4
  "description": "JavaScript SDK for Base44 API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",