@bunbase-ae/js 1.0.1-next.7.bc3dec2 → 1.2.0
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/README.md +104 -0
- package/package.json +22 -3
- package/src/admin.ts +9 -5
- package/src/auth.ts +116 -5
- package/src/client.ts +4 -0
- package/src/http.ts +40 -7
- package/src/index.ts +3 -8
- package/src/storage.ts +2 -29
- package/src/types.ts +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# @bunbase-ae/js
|
|
2
|
+
|
|
3
|
+
TypeScript/JavaScript SDK for [BunBase](https://docs-bunbase.palmcode.ae).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
bun add @bunbase-ae/js
|
|
9
|
+
# or
|
|
10
|
+
npm install @bunbase-ae/js
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { BunBaseClient } from "@bunbase-ae/js";
|
|
17
|
+
|
|
18
|
+
const client = new BunBaseClient({ url: "https://your-bunbase-instance.example.com" });
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## API
|
|
22
|
+
|
|
23
|
+
### `BunBaseClient`
|
|
24
|
+
|
|
25
|
+
The root client. All sub-clients are accessed from here.
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
const client = new BunBaseClient({ url: "https://..." });
|
|
29
|
+
|
|
30
|
+
client.collection("posts") // CollectionClient
|
|
31
|
+
client.auth // AuthClient
|
|
32
|
+
client.realtime // RealtimeClient
|
|
33
|
+
client.storage // StorageClient
|
|
34
|
+
client.admin // AdminClient
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Collections
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
const posts = client.collection("posts");
|
|
41
|
+
|
|
42
|
+
// List records
|
|
43
|
+
const { items, total } = await posts.list({ sort: "-_created_at", limit: 20 });
|
|
44
|
+
|
|
45
|
+
// Get one record
|
|
46
|
+
const post = await posts.get("record-id");
|
|
47
|
+
|
|
48
|
+
// Create
|
|
49
|
+
const created = await posts.create({ title: "Hello", body: "..." });
|
|
50
|
+
|
|
51
|
+
// Update
|
|
52
|
+
await posts.update("record-id", { title: "Updated" });
|
|
53
|
+
|
|
54
|
+
// Delete
|
|
55
|
+
await posts.delete("record-id");
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Auth
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// Register
|
|
62
|
+
await client.auth.register({ email: "user@example.com", password: "secret" });
|
|
63
|
+
|
|
64
|
+
// Login
|
|
65
|
+
await client.auth.login({ email: "user@example.com", password: "secret" });
|
|
66
|
+
|
|
67
|
+
// Logout
|
|
68
|
+
await client.auth.logout();
|
|
69
|
+
|
|
70
|
+
// Subscribe to token changes (login, refresh, logout)
|
|
71
|
+
// session is { accessToken, refreshToken } when authenticated, or null when logged out
|
|
72
|
+
client.auth.onAuthChange((session) => {
|
|
73
|
+
if (session) {
|
|
74
|
+
console.log(session.accessToken, session.refreshToken);
|
|
75
|
+
} else {
|
|
76
|
+
console.log("logged out");
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Realtime
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const unsub = client.realtime.subscribe("posts", (event) => {
|
|
85
|
+
console.log(event.type, event.record);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Later:
|
|
89
|
+
unsub();
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Storage
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
// Upload a file
|
|
96
|
+
const result = await client.storage.upload("avatars", file);
|
|
97
|
+
|
|
98
|
+
// Get a public URL
|
|
99
|
+
const url = client.storage.url("avatars", "filename.png");
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Documentation
|
|
103
|
+
|
|
104
|
+
Full API reference and guides: [https://docs-bunbase.palmcode.ae](https://docs-bunbase.palmcode.ae)
|
package/package.json
CHANGED
|
@@ -1,11 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bunbase-ae/js",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "TypeScript/JavaScript SDK for BunBase",
|
|
6
|
-
"
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"homepage": "https://docs-bunbase.palmcode.ae",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/palmcode-ae/bunbase.git",
|
|
11
|
+
"directory": "sdk/js"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/palmcode-ae/bunbase/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"bunbase",
|
|
18
|
+
"sdk",
|
|
19
|
+
"typescript",
|
|
20
|
+
"database",
|
|
21
|
+
"realtime",
|
|
22
|
+
"auth",
|
|
23
|
+
"storage"
|
|
24
|
+
],
|
|
7
25
|
"files": [
|
|
8
|
-
"src"
|
|
26
|
+
"src",
|
|
27
|
+
"README.md"
|
|
9
28
|
],
|
|
10
29
|
"main": "./src/index.ts",
|
|
11
30
|
"exports": {
|
package/src/admin.ts
CHANGED
|
@@ -374,6 +374,14 @@ class AdminUsersClient {
|
|
|
374
374
|
return res.items;
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
+
async createApiKeyFor(userId: string, name: string): Promise<AdminApiKey & { key: string }> {
|
|
378
|
+
return this.http.request<AdminApiKey & { key: string }>(
|
|
379
|
+
"POST",
|
|
380
|
+
`/api/v1/admin/users/${userId}/api-keys`,
|
|
381
|
+
{ body: { name } },
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
377
385
|
async revokeApiKey(userId: string, keyId: string): Promise<void> {
|
|
378
386
|
await this.http.request<{ ok: boolean }>(
|
|
379
387
|
"DELETE",
|
|
@@ -520,11 +528,7 @@ class AdminCollectionsClient {
|
|
|
520
528
|
if (opts.sort) params.sort = opts.sort;
|
|
521
529
|
if (opts.includeDeleted) params.include_deleted = "true";
|
|
522
530
|
if (opts.search) params.search = opts.search;
|
|
523
|
-
if (opts.filter)
|
|
524
|
-
for (const [field, value] of Object.entries(opts.filter)) {
|
|
525
|
-
params[`filter[${field}]`] = value;
|
|
526
|
-
}
|
|
527
|
-
}
|
|
531
|
+
if (opts.filter) Object.assign(params, opts.filter);
|
|
528
532
|
return this.http.request<AdminListResult<AdminRecord>>(
|
|
529
533
|
"GET",
|
|
530
534
|
`/api/v1/admin/collections/${collection}/records`,
|
package/src/auth.ts
CHANGED
|
@@ -4,7 +4,15 @@
|
|
|
4
4
|
// handles the API calls and wires the results back into HttpClient.
|
|
5
5
|
|
|
6
6
|
import type { HttpClient } from "./http";
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
ApiKey,
|
|
9
|
+
AuthResult,
|
|
10
|
+
AuthUser,
|
|
11
|
+
LoginResult,
|
|
12
|
+
PersistSessionOptions,
|
|
13
|
+
TotpChallenge,
|
|
14
|
+
TotpSetup,
|
|
15
|
+
} from "./types";
|
|
8
16
|
|
|
9
17
|
// Reactive snapshot of auth state. Consumed by useAuth() via useSyncExternalStore.
|
|
10
18
|
export interface AuthSnapshot {
|
|
@@ -13,6 +21,10 @@ export interface AuthSnapshot {
|
|
|
13
21
|
error: Error | null;
|
|
14
22
|
}
|
|
15
23
|
|
|
24
|
+
// Represents the active session passed to onAuthChange listeners.
|
|
25
|
+
// Token-based sessions carry access/refresh tokens; API-key sessions carry the key.
|
|
26
|
+
export type AuthSession = { accessToken: string; refreshToken: string } | { apiKey: string };
|
|
27
|
+
|
|
16
28
|
export class AuthClient {
|
|
17
29
|
// Cached user — set on every successful login/register/me/verifyTotp call.
|
|
18
30
|
// Cleared on logout. Lets useAuth() initialize synchronously without a network round-trip.
|
|
@@ -150,6 +162,7 @@ export class AuthClient {
|
|
|
150
162
|
this.cachedUser = null;
|
|
151
163
|
this.patchSnapshot({ user: null });
|
|
152
164
|
this.http.clearTokens();
|
|
165
|
+
this.http.setApiKey(null);
|
|
153
166
|
}
|
|
154
167
|
}
|
|
155
168
|
|
|
@@ -162,6 +175,7 @@ export class AuthClient {
|
|
|
162
175
|
this.cachedUser = null;
|
|
163
176
|
this.patchSnapshot({ user: null });
|
|
164
177
|
this.http.clearTokens();
|
|
178
|
+
this.http.setApiKey(null);
|
|
165
179
|
return { sessions_revoked: result.sessions_revoked };
|
|
166
180
|
}
|
|
167
181
|
|
|
@@ -225,9 +239,28 @@ export class AuthClient {
|
|
|
225
239
|
await this.http.request<{ ok: boolean }>("POST", "/api/v1/auth/resend-verification");
|
|
226
240
|
}
|
|
227
241
|
|
|
228
|
-
// Returns true if an access token is currently held (does not validate expiry).
|
|
242
|
+
// Returns true if an access token or API key is currently held (does not validate expiry).
|
|
229
243
|
isAuthenticated(): boolean {
|
|
230
|
-
return this.http.getAccessToken() !== null;
|
|
244
|
+
return this.http.getAccessToken() !== null || this.http.getApiKey() !== null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Log in using a long-lived API key (e.g. for RFID / hardware-token login flows).
|
|
248
|
+
//
|
|
249
|
+
// Sets the API key on the HTTP layer, fetches /auth/me with it to verify
|
|
250
|
+
// the key is valid and retrieve the user, then updates the auth snapshot so
|
|
251
|
+
// that useAuth() and getCachedUser() reflect the authenticated state.
|
|
252
|
+
//
|
|
253
|
+
// On error (invalid or revoked key) the API key is cleared and the error is
|
|
254
|
+
// re-thrown. Call auth.logout() to sign out — it clears the API key too.
|
|
255
|
+
async loginWithApiKey(apiKey: string): Promise<AuthUser> {
|
|
256
|
+
this.http.setApiKey(apiKey);
|
|
257
|
+
try {
|
|
258
|
+
const user = await this.me(); // me() already updates cachedUser and snapshot
|
|
259
|
+
return user;
|
|
260
|
+
} catch (err) {
|
|
261
|
+
this.http.setApiKey(null);
|
|
262
|
+
throw err;
|
|
263
|
+
}
|
|
231
264
|
}
|
|
232
265
|
|
|
233
266
|
// Restore a previously persisted session (e.g. from localStorage or Keychain).
|
|
@@ -292,9 +325,12 @@ export class AuthClient {
|
|
|
292
325
|
await this.http.request<{ ok: boolean }>("DELETE", `/api/v1/auth/api-keys/${id}`);
|
|
293
326
|
}
|
|
294
327
|
|
|
295
|
-
// ─── Auth change
|
|
328
|
+
// ─── Auth change listeners ────────────────────────────────────────────────
|
|
296
329
|
|
|
297
|
-
// Register a listener
|
|
330
|
+
// Register a listener for token-based auth transitions: fires with
|
|
331
|
+
// { accessToken, refreshToken } on login/refresh, and null on logout.
|
|
332
|
+
// API-key sessions do not trigger this listener — use onApiKeyChange for those.
|
|
333
|
+
//
|
|
298
334
|
// Returns an unsubscribe function.
|
|
299
335
|
onAuthChange(
|
|
300
336
|
listener: (session: { accessToken: string; refreshToken: string } | null) => void,
|
|
@@ -308,6 +344,81 @@ export class AuthClient {
|
|
|
308
344
|
});
|
|
309
345
|
}
|
|
310
346
|
|
|
347
|
+
// Register a listener for API-key auth transitions: fires with the key string
|
|
348
|
+
// on loginWithApiKey, and null when the key is cleared (e.g. after logout).
|
|
349
|
+
// Token-based sessions do not trigger this listener — use onAuthChange for those.
|
|
350
|
+
//
|
|
351
|
+
// Returns an unsubscribe function.
|
|
352
|
+
onApiKeyChange(listener: (apiKey: string | null) => void): () => void {
|
|
353
|
+
return this.http.onApiKeyChange(listener);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Wire automatic session persistence against a StorageAdapter.
|
|
357
|
+
//
|
|
358
|
+
// Called by BunBaseClient when `persistSession` is set. Sets loading=true
|
|
359
|
+
// on the snapshot while the async read is in flight, then restores the
|
|
360
|
+
// session (if tokens are found) and sets loading=false. Also registers an
|
|
361
|
+
// onAuthChange listener that writes tokens to storage on login/refresh and
|
|
362
|
+
// clears them on logout.
|
|
363
|
+
//
|
|
364
|
+
// The method is intentionally synchronous — the async work is fire-and-forget
|
|
365
|
+
// so the constructor can call it without awaiting.
|
|
366
|
+
initPersistence(opts: PersistSessionOptions): void {
|
|
367
|
+
const { storage, prefix } = opts;
|
|
368
|
+
const atKey = `${prefix}_bb_at`;
|
|
369
|
+
const rtKey = `${prefix}_bb_rt`;
|
|
370
|
+
const userKey = `${prefix}_bb_user`;
|
|
371
|
+
|
|
372
|
+
this.patchSnapshot({ loading: true });
|
|
373
|
+
|
|
374
|
+
void (async () => {
|
|
375
|
+
try {
|
|
376
|
+
const [at, rt, userStr] = await Promise.all([
|
|
377
|
+
storage.getItem(atKey),
|
|
378
|
+
storage.getItem(rtKey),
|
|
379
|
+
storage.getItem(userKey),
|
|
380
|
+
]);
|
|
381
|
+
if (at && rt) {
|
|
382
|
+
let user: AuthUser | undefined;
|
|
383
|
+
if (userStr) {
|
|
384
|
+
try {
|
|
385
|
+
user = JSON.parse(userStr) as AuthUser;
|
|
386
|
+
} catch {
|
|
387
|
+
// Corrupt cached user — ignore and proceed without it.
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
this.restoreSession(at, rt, user);
|
|
391
|
+
}
|
|
392
|
+
} finally {
|
|
393
|
+
this.patchSnapshot({ loading: false });
|
|
394
|
+
}
|
|
395
|
+
})();
|
|
396
|
+
|
|
397
|
+
// Wrap every storage call so that both synchronous throws (e.g. localStorage
|
|
398
|
+
// quota / security errors) and async rejections are absorbed silently and
|
|
399
|
+
// never propagate as unhandled rejections into the login/logout/refresh flow.
|
|
400
|
+
const safeWrite = (fn: () => unknown): void => {
|
|
401
|
+
try {
|
|
402
|
+
Promise.resolve(fn()).catch(() => {});
|
|
403
|
+
} catch {
|
|
404
|
+
// absorb synchronous throws
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
this.onAuthChange((session) => {
|
|
409
|
+
if (session) {
|
|
410
|
+
safeWrite(() => storage.setItem(atKey, session.accessToken));
|
|
411
|
+
safeWrite(() => storage.setItem(rtKey, session.refreshToken));
|
|
412
|
+
const user = this.getCachedUser();
|
|
413
|
+
if (user) safeWrite(() => storage.setItem(userKey, JSON.stringify(user)));
|
|
414
|
+
} else {
|
|
415
|
+
safeWrite(() => storage.removeItem(atKey));
|
|
416
|
+
safeWrite(() => storage.removeItem(rtKey));
|
|
417
|
+
safeWrite(() => storage.removeItem(userKey));
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
311
422
|
// Periodically validate the active session so admin revocations are detected.
|
|
312
423
|
//
|
|
313
424
|
// FALLBACK: If the app uses RealtimeClient, auth events (session_revoked,
|
package/src/client.ts
CHANGED
|
@@ -34,6 +34,10 @@ export class BunBaseClient {
|
|
|
34
34
|
this.storage = new StorageClient(this.http);
|
|
35
35
|
this.realtime = new RealtimeClient(wsUrl, this.http);
|
|
36
36
|
this.admin = new AdminClient(this.http);
|
|
37
|
+
|
|
38
|
+
if (options.persistSession) {
|
|
39
|
+
this.auth.initPersistence(options.persistSession);
|
|
40
|
+
}
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
get baseUrl(): string {
|
package/src/http.ts
CHANGED
|
@@ -15,22 +15,41 @@ interface TokenStore {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export type TokenChangeListener = (tokens: TokenStore) => void;
|
|
18
|
+
export type ApiKeyChangeListener = (apiKey: string | null) => void;
|
|
18
19
|
|
|
19
20
|
export class HttpClient {
|
|
20
21
|
private tokens: TokenStore = { accessToken: null, refreshToken: null };
|
|
21
22
|
private refreshPromise: Promise<void> | null = null;
|
|
22
23
|
private tokenListeners: TokenChangeListener[] = [];
|
|
24
|
+
private apiKeyListeners: ApiKeyChangeListener[] = [];
|
|
23
25
|
|
|
24
26
|
baseUrl: string;
|
|
25
|
-
private
|
|
27
|
+
private _apiKey: string | null;
|
|
26
28
|
private adminSecret: string | null;
|
|
27
29
|
|
|
28
30
|
constructor(baseUrl: string, apiKey?: string, adminSecret?: string) {
|
|
29
31
|
this.baseUrl = baseUrl;
|
|
30
|
-
this.
|
|
32
|
+
this._apiKey = apiKey ?? null;
|
|
31
33
|
this.adminSecret = adminSecret ?? null;
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
setApiKey(key: string | null): void {
|
|
37
|
+
if (this._apiKey === key) return;
|
|
38
|
+
this._apiKey = key;
|
|
39
|
+
this.notifyApiKeyListeners();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getApiKey(): string | null {
|
|
43
|
+
return this._apiKey;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
onApiKeyChange(listener: ApiKeyChangeListener): () => void {
|
|
47
|
+
this.apiKeyListeners.push(listener);
|
|
48
|
+
return () => {
|
|
49
|
+
this.apiKeyListeners = this.apiKeyListeners.filter((l) => l !== listener);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
34
53
|
setAdminSecret(secret: string | null): void {
|
|
35
54
|
this.adminSecret = secret;
|
|
36
55
|
}
|
|
@@ -66,11 +85,15 @@ export class HttpClient {
|
|
|
66
85
|
for (const l of this.tokenListeners) l(this.tokens);
|
|
67
86
|
}
|
|
68
87
|
|
|
88
|
+
private notifyApiKeyListeners(): void {
|
|
89
|
+
for (const l of this.apiKeyListeners) l(this._apiKey);
|
|
90
|
+
}
|
|
91
|
+
|
|
69
92
|
// Returns the auth headers this client would attach to a request.
|
|
70
93
|
// Used by StorageClient for XHR-based uploads with progress tracking.
|
|
71
94
|
getAuthHeaders(): Record<string, string> {
|
|
72
95
|
if (this.adminSecret) return { Authorization: `Bearer ${this.adminSecret}` };
|
|
73
|
-
if (this.
|
|
96
|
+
if (this._apiKey) return { "X-Api-Key": this._apiKey };
|
|
74
97
|
if (this.tokens.accessToken) return { Authorization: `Bearer ${this.tokens.accessToken}` };
|
|
75
98
|
return {};
|
|
76
99
|
}
|
|
@@ -85,6 +108,8 @@ export class HttpClient {
|
|
|
85
108
|
formData?: FormData;
|
|
86
109
|
query?: Record<string, string>;
|
|
87
110
|
skipAuth?: boolean;
|
|
111
|
+
signal?: AbortSignal;
|
|
112
|
+
keepalive?: boolean;
|
|
88
113
|
} = {},
|
|
89
114
|
): Promise<T> {
|
|
90
115
|
const res = await this.send(method, path, options);
|
|
@@ -94,7 +119,7 @@ export class HttpClient {
|
|
|
94
119
|
res.status === 401 &&
|
|
95
120
|
!options.skipAuth &&
|
|
96
121
|
!this.adminSecret &&
|
|
97
|
-
!this.
|
|
122
|
+
!this._apiKey &&
|
|
98
123
|
this.tokens.refreshToken
|
|
99
124
|
) {
|
|
100
125
|
try {
|
|
@@ -136,6 +161,8 @@ export class HttpClient {
|
|
|
136
161
|
formData?: FormData;
|
|
137
162
|
query?: Record<string, string>;
|
|
138
163
|
skipAuth?: boolean;
|
|
164
|
+
signal?: AbortSignal;
|
|
165
|
+
keepalive?: boolean;
|
|
139
166
|
},
|
|
140
167
|
): Promise<Response> {
|
|
141
168
|
let url = `${this.baseUrl}${path}`;
|
|
@@ -148,8 +175,8 @@ export class HttpClient {
|
|
|
148
175
|
if (!options.skipAuth) {
|
|
149
176
|
if (this.adminSecret) {
|
|
150
177
|
headers.Authorization = `Bearer ${this.adminSecret}`;
|
|
151
|
-
} else if (this.
|
|
152
|
-
headers["X-Api-Key"] = this.
|
|
178
|
+
} else if (this._apiKey) {
|
|
179
|
+
headers["X-Api-Key"] = this._apiKey;
|
|
153
180
|
} else if (this.tokens.accessToken) {
|
|
154
181
|
headers.Authorization = `Bearer ${this.tokens.accessToken}`;
|
|
155
182
|
}
|
|
@@ -164,7 +191,13 @@ export class HttpClient {
|
|
|
164
191
|
body = JSON.stringify(options.body);
|
|
165
192
|
}
|
|
166
193
|
|
|
167
|
-
return fetch(url, {
|
|
194
|
+
return fetch(url, {
|
|
195
|
+
method,
|
|
196
|
+
headers,
|
|
197
|
+
body,
|
|
198
|
+
signal: options.signal,
|
|
199
|
+
keepalive: options.keepalive,
|
|
200
|
+
});
|
|
168
201
|
}
|
|
169
202
|
|
|
170
203
|
private async parse<T>(res: Response): Promise<T> {
|
package/src/index.ts
CHANGED
|
@@ -38,14 +38,7 @@ export { AuthClient, type AuthSnapshot } from "./auth";
|
|
|
38
38
|
export { BunBaseClient } from "./client";
|
|
39
39
|
export { CollectionClient } from "./collection";
|
|
40
40
|
export { RealtimeClient, type SubscribeOptions } from "./realtime";
|
|
41
|
-
export {
|
|
42
|
-
type ImageTransformFit,
|
|
43
|
-
type ImageTransformFormat,
|
|
44
|
-
type ImageTransformOptions,
|
|
45
|
-
type SignedUploadResult,
|
|
46
|
-
StorageClient,
|
|
47
|
-
type UploadOptions,
|
|
48
|
-
} from "./storage";
|
|
41
|
+
export { type SignedUploadResult, StorageClient, type UploadOptions } from "./storage";
|
|
49
42
|
export {
|
|
50
43
|
type AggregateFunction,
|
|
51
44
|
type AggregateResult,
|
|
@@ -73,9 +66,11 @@ export {
|
|
|
73
66
|
type ListQuery,
|
|
74
67
|
type ListResult,
|
|
75
68
|
type LoginResult,
|
|
69
|
+
type PersistSessionOptions,
|
|
76
70
|
type RealtimeCallback,
|
|
77
71
|
type RealtimeEvent,
|
|
78
72
|
type RealtimeEventType,
|
|
73
|
+
type StorageAdapter,
|
|
79
74
|
type TotpChallenge,
|
|
80
75
|
type TotpSetup,
|
|
81
76
|
type UnsubscribeFn,
|
package/src/storage.ts
CHANGED
|
@@ -4,22 +4,6 @@ import type { HttpClient } from "./http";
|
|
|
4
4
|
import type { FileRecord } from "./types";
|
|
5
5
|
import { BunBaseError } from "./types";
|
|
6
6
|
|
|
7
|
-
export type ImageTransformFit = "cover" | "contain" | "inside";
|
|
8
|
-
export type ImageTransformFormat = "webp" | "jpeg" | "png";
|
|
9
|
-
|
|
10
|
-
export interface ImageTransformOptions {
|
|
11
|
-
/** Output width in pixels (1–4096). */
|
|
12
|
-
width?: number;
|
|
13
|
-
/** Output height in pixels (1–4096). */
|
|
14
|
-
height?: number;
|
|
15
|
-
/** How to fit the image into the given dimensions. Default: "inside". */
|
|
16
|
-
fit?: ImageTransformFit;
|
|
17
|
-
/** Convert to this output format. Omit to keep the original format. */
|
|
18
|
-
format?: ImageTransformFormat;
|
|
19
|
-
/** Quality for JPEG/WebP, 1–100. Default: 85. */
|
|
20
|
-
quality?: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
7
|
export interface UploadOptions {
|
|
24
8
|
// Target storage bucket. Defaults to "default" if omitted.
|
|
25
9
|
bucket?: string;
|
|
@@ -206,21 +190,10 @@ export class StorageClient {
|
|
|
206
190
|
// Do NOT use this for private files — the browser cannot send the
|
|
207
191
|
// Authorization header via <img src>. For private files, call signedUrl()
|
|
208
192
|
// to get a time-limited signed URL instead.
|
|
209
|
-
downloadUrl(id: string, filename?: string | null
|
|
193
|
+
downloadUrl(id: string, filename?: string | null): string {
|
|
210
194
|
if (id.startsWith("http")) return id;
|
|
211
195
|
const base = `${this.http.baseUrl}/api/v1/storage/${encodeURIComponent(id)}`;
|
|
212
|
-
|
|
213
|
-
if (transform) {
|
|
214
|
-
const params = new URLSearchParams();
|
|
215
|
-
if (transform.width != null) params.set("w", String(transform.width));
|
|
216
|
-
if (transform.height != null) params.set("h", String(transform.height));
|
|
217
|
-
if (transform.fit) params.set("fit", transform.fit);
|
|
218
|
-
if (transform.format) params.set("format", transform.format);
|
|
219
|
-
if (transform.quality != null) params.set("q", String(transform.quality));
|
|
220
|
-
const qs = params.toString();
|
|
221
|
-
if (qs) url = `${url}?${qs}`;
|
|
222
|
-
}
|
|
223
|
-
return url;
|
|
196
|
+
return filename ? `${base}/${encodeURIComponent(filename)}` : base;
|
|
224
197
|
}
|
|
225
198
|
|
|
226
199
|
async list(): Promise<FileRecord[]> {
|
package/src/types.ts
CHANGED
|
@@ -237,6 +237,25 @@ export interface ApiKey {
|
|
|
237
237
|
created_at: number;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
// Minimal storage interface — deliberately async-compatible so the same API
|
|
241
|
+
// works with synchronous Web Storage (localStorage, sessionStorage) and
|
|
242
|
+
// Promise-based stores such as React Native's AsyncStorage or any custom
|
|
243
|
+
// adapter (SecureStore, IndexedDB wrappers, etc.).
|
|
244
|
+
export interface StorageAdapter {
|
|
245
|
+
getItem(key: string): string | null | Promise<string | null>;
|
|
246
|
+
setItem(key: string, value: string): void | Promise<void>;
|
|
247
|
+
removeItem(key: string): void | Promise<void>;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export interface PersistSessionOptions {
|
|
251
|
+
// Storage backend. Pass `localStorage`, `sessionStorage`, AsyncStorage, or
|
|
252
|
+
// any object that satisfies the StorageAdapter interface.
|
|
253
|
+
storage: StorageAdapter;
|
|
254
|
+
// Key prefix — isolates tokens per app so concurrent tabs don't clobber
|
|
255
|
+
// each other. Keys written: `{prefix}_bb_at`, `{prefix}_bb_rt`, `{prefix}_bb_user`.
|
|
256
|
+
prefix: string;
|
|
257
|
+
}
|
|
258
|
+
|
|
240
259
|
export interface BunBaseClientOptions {
|
|
241
260
|
// Base URL of the BunBase server — no trailing slash.
|
|
242
261
|
url: string;
|
|
@@ -248,6 +267,10 @@ export interface BunBaseClientOptions {
|
|
|
248
267
|
// the token refresh flow. Use for server-to-server admin calls or Studio.
|
|
249
268
|
// Alternatively, authenticate as a user with the "admin" role via login().
|
|
250
269
|
adminSecret?: string;
|
|
270
|
+
// When set, the SDK automatically restores the session on construction and
|
|
271
|
+
// persists it on every auth change (login, refresh, logout). The loading
|
|
272
|
+
// field on AuthSnapshot is true while the async restore is in flight.
|
|
273
|
+
persistSession?: PersistSessionOptions;
|
|
251
274
|
}
|
|
252
275
|
|
|
253
276
|
export class BunBaseError extends Error {
|