@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 +10 -0
- package/dist/client.types.d.ts +55 -0
- package/dist/utils/fetch-with-auth.d.ts +46 -0
- package/dist/utils/fetch-with-auth.js +77 -0
- package/package.json +1 -1
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
|
*
|
package/dist/client.types.d.ts
CHANGED
|
@@ -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
|
+
}
|