@koolbase/js 10.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,50 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@koolbase/js` are documented in this file. The format
4
+ is based on [Keep a Changelog][kac], and this project adheres to
5
+ [Semantic Versioning][semver].
6
+
7
+ [kac]: https://keepachangelog.com/en/1.1.0/
8
+ [semver]: https://semver.org/
9
+
10
+ ## 10.0.0
11
+
12
+ The first release. Numbered to match `@koolbase/react-native` and
13
+ `@koolbase/core`, which share one version and one core: this package is that
14
+ core composed for a browser, with sixty behavioural tests run against the
15
+ browser adapter before it shipped.
16
+
17
+ ### What is here
18
+
19
+ - **Auth** — email and password, phone + OTP, session persistence across
20
+ reloads, `restoreSession()`, `onAuthStateChange`, password reset, email
21
+ verification. Google and Apple sign-in through their web OAuth flows,
22
+ passing the ID token to the same `signInWithGoogle` / `signInWithApple`.
23
+ - **Database** — insert, query, update, delete, upsert, bulk delete, atomic
24
+ batches, populate, and semantic / lexical / hybrid search.
25
+ - **Offline** — cached reads, a durable write queue with baselines,
26
+ `pendingWrites()`, and conflicts you resolve four ways. Same semantics as
27
+ React Native, same tests.
28
+ - **Storage** — presigned upload and download, safe-by-default paths, bucket
29
+ limits as typed errors, public CDN URLs with image transforms, versioning.
30
+ - **Realtime** — one shared WebSocket, backoff on reconnect, events filtered
31
+ by the collection's read rule.
32
+ - **Functions**, **feature flags**, **remote config**, **version
33
+ enforcement**, **analytics**.
34
+
35
+ ### What is not, by design
36
+
37
+ - **Code push** — a native-bundle concept; the web ships on deploy.
38
+ - **Push messaging** — FCM tokens come from a native module. Use Web Push
39
+ through a service worker and your backend.
40
+ - **Native Google / Apple sign-in** — use the web OAuth flows.
41
+
42
+ ### Known limitations in this release
43
+
44
+ - **Single tab.** Two tabs share one IndexedDB and one write queue. Inserts
45
+ are idempotent so the damage is bounded, but a conflict resolved in one tab
46
+ can be re-resolved in another. Multi-tab coordination is next.
47
+ - **Session storage is not a keychain.** IndexedDB is readable by any script
48
+ on the page. Inject your own `KoolbaseAuthStorage` if your threat model
49
+ needs an httpOnly cookie or similar; the README says more.
50
+
package/README.md ADDED
@@ -0,0 +1,287 @@
1
+ # @koolbase/js
2
+
3
+ [![npm](https://img.shields.io/npm/v/@koolbase/js.svg)](https://www.npmjs.com/package/@koolbase/js)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ **From idea to app. And everything after.** Design your app, power it with a
7
+ complete backend, and keep shipping after release.
8
+
9
+ Koolbase for the browser: auth, database, storage, realtime, functions,
10
+ feature flags, remote config, and an offline write queue with conflict
11
+ resolution — one package, one `initialize()` call, TypeScript throughout.
12
+
13
+ Same core as [`@koolbase/react-native`](https://www.npmjs.com/package/@koolbase/react-native).
14
+ Same behaviour, proven by the same test suite on both hosts.
15
+
16
+ ---
17
+
18
+ ## Get started
19
+
20
+ 1. Create a free account at [app.koolbase.com](https://app.koolbase.com)
21
+ 2. Create a project and copy your public key from Environments
22
+ 3. Install:
23
+
24
+ ```bash
25
+ npm install @koolbase/js
26
+ ```
27
+
28
+ 4. Initialize once at startup:
29
+
30
+ ```typescript
31
+ import { Koolbase } from '@koolbase/js';
32
+
33
+ await Koolbase.initialize({
34
+ publicKey: 'pk_live_xxxx',
35
+ baseUrl: 'https://api.koolbase.com',
36
+ });
37
+ ```
38
+
39
+ No native modules, no build step, nothing to configure. Works in any modern
40
+ browser and in frameworks that render in one — React, Vue, Svelte, Next.js on
41
+ the client side.
42
+
43
+ > **Auth is automatic.** Database, storage and function calls authenticate as
44
+ > the signed-in user — nothing to pass. Sign in (or restore a session) and every
45
+ > request carries that identity.
46
+
47
+ ---
48
+
49
+ ## Authentication
50
+
51
+ ```typescript
52
+ await Koolbase.auth.register({ email: 'user@example.com', password: 'password' });
53
+
54
+ const session = await Koolbase.auth.login({
55
+ email: 'user@example.com',
56
+ password: 'password',
57
+ });
58
+
59
+ const me = Koolbase.auth.currentUser;
60
+
61
+ await Koolbase.auth.logout();
62
+
63
+ await Koolbase.auth.forgotPassword('user@example.com');
64
+
65
+ const unsubscribe = Koolbase.auth.onAuthStateChange((user) => {
66
+ console.log(user ? 'signed in' : 'signed out');
67
+ });
68
+ ```
69
+
70
+ ### Sessions across page loads
71
+
72
+ Sessions persist in IndexedDB by default, so a refresh does not sign the user
73
+ out. Restore before rendering:
74
+
75
+ ```typescript
76
+ import { RestoreResult } from '@koolbase/js';
77
+
78
+ const result = await Koolbase.auth.restoreSession();
79
+
80
+ switch (result) {
81
+ case RestoreResult.Restored: showApp(); break;
82
+ case RestoreResult.Offline: showApp(); break; // optimistic, no network yet
83
+ case RestoreResult.Expired: showLogin(); break;
84
+ case RestoreResult.NoSession: showLogin(); break;
85
+ }
86
+ ```
87
+
88
+ ### What browser storage does and does not protect
89
+
90
+ There is no keychain in a browser. IndexedDB, localStorage and every other
91
+ store a page can read are readable by any script running on that page — so an
92
+ XSS vulnerability in your app exposes the session. This is true of every
93
+ browser SDK from every vendor. The defences are a content-security policy and
94
+ short session lifetimes, not a storage backend.
95
+
96
+ If your app needs a different trade-off — an httpOnly cookie set by your own
97
+ backend, say — implement `KoolbaseAuthStorage` and pass it as
98
+ `config.authStorage`. The SDK will use yours instead of its own.
99
+
100
+ ### Phone + OTP
101
+
102
+ ```typescript
103
+ await Koolbase.auth.sendOtp({ phoneNumber: '+233200000000' });
104
+ await Koolbase.auth.verifyOtp({ phoneNumber: '+233200000000', code: '123456' });
105
+ ```
106
+
107
+ Configure your SMS provider (Twilio, Africa's Talking, or Hubtel) in the
108
+ dashboard under Phone Auth.
109
+
110
+ ### Google and Apple sign-in
111
+
112
+ The native sign-in flows are React Native features. In the browser, run the
113
+ provider's web OAuth flow yourself and pass the resulting ID token to
114
+ `Koolbase.auth.signInWithGoogle({ idToken })` or
115
+ `signInWithApple({ identityToken })` — the server-side verification is the
116
+ same.
117
+
118
+ ---
119
+
120
+ ## Database
121
+
122
+ ```typescript
123
+ await Koolbase.db.insert('posts', { title: 'Hello', published: true });
124
+
125
+ const { records, total } = await Koolbase.db.query('posts', {
126
+ filters: { published: true },
127
+ limit: 10,
128
+ orderBy: 'created_at',
129
+ orderDesc: true,
130
+ });
131
+
132
+ const post = records[0];
133
+ console.log(post.data.title); // your fields live under .data
134
+ console.log(post.id, post.collection); // metadata
135
+
136
+ await Koolbase.db.update('record-id', { title: 'Updated' });
137
+ await Koolbase.db.delete('record-id');
138
+ ```
139
+
140
+ `total` is the size of the set you are authorized to read, on every page, so
141
+ pagination is exact.
142
+
143
+ Upsert, bulk delete, atomic batches, populate for related records, and
144
+ semantic / lexical / hybrid search over vectors all work exactly as in
145
+ `@koolbase/react-native` — see its README for the full reference; the API is
146
+ identical.
147
+
148
+ ---
149
+
150
+ ## Offline
151
+
152
+ Reads come from a local cache when the network is unavailable. `insert`,
153
+ `update` and `delete` queue and send when it returns.
154
+
155
+ ```typescript
156
+ const { records, isFromCache } = await Koolbase.db.query('posts', { limit: 20 });
157
+
158
+ await Koolbase.db.update(id, { title: 'Corrected' }); // queued if offline
159
+ await Koolbase.db.syncPendingWrites(); // or wait for reconnect
160
+
161
+ const pending = await Koolbase.db.pendingWrites(); // for a sync badge
162
+ const conflicts = await Koolbase.db.conflicts(); // writes the server refused
163
+ ```
164
+
165
+ A write the server refuses on replay — because the record changed meanwhile —
166
+ becomes a conflict you resolve: `resolveWithLocal()`, `resolveWithServer()`,
167
+ `resolveWithMerge({...})`, or `abandon()`. Conflicts survive reloads and do not
168
+ expire; surface them if you support offline editing.
169
+
170
+ **Single tab, in this release.** Two tabs share one IndexedDB and one queue,
171
+ and both may replay the same write. Inserts are idempotent so the damage is
172
+ bounded, but a conflict resolved in one tab can be re-resolved in another.
173
+ Multi-tab coordination is on the roadmap; until then, treat the offline queue
174
+ as belonging to one tab.
175
+
176
+ ---
177
+
178
+ ## Storage
179
+
180
+ ```typescript
181
+ const { object, downloadUrl } = await Koolbase.storage.upload({
182
+ bucket: 'avatars',
183
+ path: `user-${userId}.jpg`,
184
+ file: fileFromInput, // a File or Blob
185
+ });
186
+
187
+ const url = await Koolbase.storage.getDownloadUrl('avatars', `user-${userId}.jpg`);
188
+ await Koolbase.storage.delete('avatars', `user-${userId}.jpg`);
189
+ ```
190
+
191
+ Safe-by-default: uploading to a path that is already taken throws
192
+ `KoolbaseStorageConflictError` unless you pass `overwrite: true`. Bucket size
193
+ caps, per-file caps and content-type allowlists arrive as typed errors. Public
194
+ buckets have stable CDN URLs with edge image transforms; versioned buckets keep
195
+ history. Same API as React Native.
196
+
197
+ ---
198
+
199
+ ## Realtime
200
+
201
+ ```typescript
202
+ const unsubscribe = Koolbase.realtime.subscribe('messages', (event) => {
203
+ if (event.type === 'deleted') console.log('deleted', event.recordId);
204
+ else console.log(event.type, event.record!.data);
205
+ });
206
+ ```
207
+
208
+ One WebSocket, shared across subscriptions, reconnecting with backoff. Events
209
+ are filtered server-side by the collection's read rule, so a subscriber sees
210
+ only what a query would return them.
211
+
212
+ ---
213
+
214
+ ## Functions
215
+
216
+ ```typescript
217
+ const result = await Koolbase.functions.invoke('send-welcome-email', { userId });
218
+ if (result.success) console.log(result.data);
219
+ ```
220
+
221
+ The signed-in user's token is forwarded automatically; the function reads the
222
+ caller on `ctx.auth`. Failures are typed: `FunctionNotFoundError`,
223
+ `FunctionPermissionError`, `FunctionValidationError`,
224
+ `FunctionQuotaExceededError`, `FunctionExecutionError`.
225
+
226
+ ---
227
+
228
+ ## Feature flags and remote config
229
+
230
+ ```typescript
231
+ if (Koolbase.isEnabled('new_checkout')) { /* ... */ }
232
+
233
+ const timeout = Koolbase.configNumber('timeout_seconds', 30);
234
+ const apiUrl = Koolbase.configString('api_url', 'https://api.myapp.com');
235
+ const dark = Koolbase.configBool('force_dark_mode', false);
236
+
237
+ const v = Koolbase.checkVersion('1.2.3');
238
+ if (v.status === 'force_update') { /* block and prompt */ }
239
+ ```
240
+
241
+ ---
242
+
243
+ ## Analytics
244
+
245
+ ```typescript
246
+ Koolbase.analytics.track('purchase', { value: 1200, currency: 'GHS' });
247
+ Koolbase.analytics.screenView('checkout');
248
+ Koolbase.analytics.identify(user.id);
249
+ Koolbase.analytics.reset(); // on sign-out
250
+ ```
251
+
252
+ Events batch and flush every 30 seconds, when the tab is hidden, and on
253
+ `pagehide`.
254
+
255
+ ---
256
+
257
+ ## Not in the browser package
258
+
259
+ Stated here so nothing is discovered as a method that fails:
260
+
261
+ - **Code push** — a native-bundle concept. The web already ships on deploy.
262
+ - **Push messaging** — FCM device tokens come from a native module. Use Web
263
+ Push through your own service worker and your backend.
264
+ - **Native Google / Apple sign-in** — use the web OAuth flows, above.
265
+
266
+ ---
267
+
268
+ ## Error handling
269
+
270
+ Every error the SDK raises extends `KoolbaseError`. Authentication failures
271
+ (401) are `KoolbaseUnauthenticatedError` from any subsystem, and the user is
272
+ already signed out by the time you catch one. The full error reference is in
273
+ the [`@koolbase/react-native` README](https://www.npmjs.com/package/@koolbase/react-native#error-handling);
274
+ the classes are identical.
275
+
276
+ ---
277
+
278
+ ## Documentation
279
+
280
+ Full documentation at [docs.koolbase.com](https://docs.koolbase.com) ·
281
+ Dashboard at [app.koolbase.com](https://app.koolbase.com) ·
282
+ Issues at [github.com/koolbase/koolbase-react-native](https://github.com/koolbase/koolbase-react-native/issues) ·
283
+ Email <dev@koolbase.com>
284
+
285
+ ## License
286
+
287
+ MIT
@@ -0,0 +1,6 @@
1
+ import type { KoolbaseAuthStorage, KoolbaseSession } from '@koolbase/core';
2
+ export declare class BrowserAuthStorage implements KoolbaseAuthStorage {
3
+ readSession(): Promise<KoolbaseSession | null>;
4
+ saveSession(session: KoolbaseSession): Promise<void>;
5
+ clear(): Promise<void>;
6
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BrowserAuthStorage = void 0;
4
+ const core_1 = require("@koolbase/core");
5
+ // Session persistence for the browser.
6
+ //
7
+ // This is the default when an app injects nothing through
8
+ // KoolbaseConfig.authStorage. It keeps the session in the platform's storage
9
+ // — IndexedDB — so a page refresh does not sign the user out.
10
+ //
11
+ // What it is not: secure the way a keychain is. IndexedDB, localStorage and
12
+ // every other store a page can read are readable by any script running on
13
+ // that page, so an XSS vulnerability in the app exposes the session. That is
14
+ // true of every browser SDK from every vendor; the mitigations are an
15
+ // application's content-security policy and short session lifetimes, not a
16
+ // storage backend. An app that needs a different trade-off — an httpOnly
17
+ // cookie set by its own backend, say — injects its own KoolbaseAuthStorage.
18
+ const KEY = 'koolbase_session_v1';
19
+ class BrowserAuthStorage {
20
+ async readSession() {
21
+ const raw = await (0, core_1.getPlatform)().storage.getItem(KEY);
22
+ if (!raw)
23
+ return null;
24
+ try {
25
+ return JSON.parse(raw);
26
+ }
27
+ catch {
28
+ await this.clear();
29
+ return null;
30
+ }
31
+ }
32
+ async saveSession(session) {
33
+ await (0, core_1.getPlatform)().storage.setItem(KEY, JSON.stringify(session));
34
+ }
35
+ async clear() {
36
+ await (0, core_1.getPlatform)().storage.removeItem(KEY);
37
+ }
38
+ }
39
+ exports.BrowserAuthStorage = BrowserAuthStorage;
@@ -0,0 +1,18 @@
1
+ export * from '@koolbase/core';
2
+ export { BrowserAuthStorage } from './auth-storage.js';
3
+ export { browserPlatform } from './platform.js';
4
+ import { KoolbaseAuth, KoolbaseDatabase, KoolbaseStorage, KoolbaseRealtime, KoolbaseFunctions, KoolbaseAnalytics, type KoolbaseConfig, type VersionCheckResult } from '@koolbase/core';
5
+ export declare const Koolbase: {
6
+ initialize(config: KoolbaseConfig): Promise<void>;
7
+ readonly auth: KoolbaseAuth;
8
+ readonly db: KoolbaseDatabase;
9
+ readonly storage: KoolbaseStorage;
10
+ readonly realtime: KoolbaseRealtime;
11
+ readonly functions: KoolbaseFunctions;
12
+ readonly analytics: KoolbaseAnalytics;
13
+ isEnabled(key: string): boolean;
14
+ configString(key: string, fallback?: string): string;
15
+ configNumber(key: string, fallback?: number): number;
16
+ configBool(key: string, fallback?: boolean): boolean;
17
+ checkVersion(currentVersion: string): VersionCheckResult;
18
+ };
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ // @koolbase/js — composes @koolbase/core for the browser.
3
+ //
4
+ // The same surface as @koolbase/react-native where the browser can honour it.
5
+ // What is deliberately absent, so it is not discovered as a method that
6
+ // fails: code push (a native-bundle concept; the web already has one), the
7
+ // logic engine (its flows arrived only through code-push bundles), push
8
+ // messaging (FCM tokens come from a native module), and Apple/Google native
9
+ // sign-in (use the web OAuth flows against the same auth endpoints).
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
22
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
23
+ };
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.Koolbase = exports.browserPlatform = exports.BrowserAuthStorage = void 0;
26
+ __exportStar(require("@koolbase/core"), exports);
27
+ var auth_storage_js_1 = require("./auth-storage.js");
28
+ Object.defineProperty(exports, "BrowserAuthStorage", { enumerable: true, get: function () { return auth_storage_js_1.BrowserAuthStorage; } });
29
+ var platform_js_1 = require("./platform.js");
30
+ Object.defineProperty(exports, "browserPlatform", { enumerable: true, get: function () { return platform_js_1.browserPlatform; } });
31
+ const core_1 = require("@koolbase/core");
32
+ const platform_js_2 = require("./platform.js");
33
+ let _auth = null;
34
+ let _db = null;
35
+ let _storage = null;
36
+ let _realtime = null;
37
+ let _functions = null;
38
+ let _flags = null;
39
+ let _analytics = null;
40
+ let _initialized = false;
41
+ function ensureInitialized() {
42
+ if (!_initialized) {
43
+ throw new Error('Koolbase not initialized. Call Koolbase.initialize(config) first.');
44
+ }
45
+ }
46
+ exports.Koolbase = {
47
+ async initialize(config) {
48
+ if (_initialized)
49
+ return;
50
+ (0, core_1.setPlatform)(config.platform ?? (0, platform_js_2.browserPlatform)());
51
+ _auth = new core_1.KoolbaseAuth(config);
52
+ _db = new core_1.KoolbaseDatabase(config, () => _auth?.currentUser?.id ?? null, () => _auth?.validAccessToken() ?? Promise.resolve(null), async () => { await _auth?.clearStoredSession(); });
53
+ _storage = new core_1.KoolbaseStorage(config, () => _auth?.validAccessToken() ?? Promise.resolve(null), async () => { await _auth?.clearStoredSession(); });
54
+ _realtime = new core_1.KoolbaseRealtime(config, () => _auth?.validAccessToken() ?? Promise.resolve(null), () => _auth?.currentUser?.id ?? null);
55
+ _functions = new core_1.KoolbaseFunctions(config, () => _auth?.validAccessToken() ?? Promise.resolve(null), async () => { await _auth?.clearStoredSession(); });
56
+ const deviceId = await (0, core_1.getOrCreateDeviceId)();
57
+ _flags = new core_1.KoolbaseFlags(config, deviceId);
58
+ if (config.analyticsEnabled !== false) {
59
+ _analytics = new core_1.KoolbaseAnalytics(config);
60
+ await _analytics.init(config.appVersion);
61
+ }
62
+ _initialized = true;
63
+ },
64
+ get auth() { ensureInitialized(); return _auth; },
65
+ get db() { ensureInitialized(); return _db; },
66
+ get storage() { ensureInitialized(); return _storage; },
67
+ get realtime() { ensureInitialized(); return _realtime; },
68
+ get functions() { ensureInitialized(); return _functions; },
69
+ get analytics() {
70
+ ensureInitialized();
71
+ if (!_analytics)
72
+ throw new Error('Analytics is disabled (analyticsEnabled: false).');
73
+ return _analytics;
74
+ },
75
+ isEnabled(key) { ensureInitialized(); return _flags.isEnabled(key); },
76
+ configString(key, fallback = '') { ensureInitialized(); return _flags.getString(key, fallback); },
77
+ configNumber(key, fallback = 0) { ensureInitialized(); return _flags.getNumber(key, fallback); },
78
+ configBool(key, fallback = false) { ensureInitialized(); return _flags.getBool(key, fallback); },
79
+ checkVersion(currentVersion) { ensureInitialized(); return _flags.checkVersion(currentVersion); },
80
+ };
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,2 @@
1
+ import type { PlatformAdapter } from '@koolbase/core';
2
+ export declare function browserPlatform(): PlatformAdapter;
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.browserPlatform = browserPlatform;
4
+ const auth_storage_js_1 = require("./auth-storage.js");
5
+ // The browser host, expressed through the platform seam.
6
+ //
7
+ // Storage is IndexedDB behind the same four calls the core makes — get, set,
8
+ // remove, list. One object store, string keys, string values, one operation
9
+ // per transaction: exactly the contract the offline queue and conflict store
10
+ // were verified against, with nothing the RN adapter does not also do.
11
+ //
12
+ // What this adapter does NOT do, stated so it is not discovered later:
13
+ //
14
+ // - Multi-tab coordination. Two tabs share one IndexedDB and one write
15
+ // queue; both may replay the same pending write. Inserts are idempotent
16
+ // on the server (ids are UUIDs from birth), so the damage is bounded, but
17
+ // a conflict resolved in one tab can be re-resolved in another. v1 is
18
+ // single-tab; a leader election is the fix and belongs in its own change.
19
+ //
20
+ // - Secure token storage. There is no keychain in a browser. Anything
21
+ // JavaScript can read, a script injected by XSS can read. The core's
22
+ // KoolbaseAuthStorage is the injection point; the default here persists
23
+ // the session in IndexedDB so a refresh does not sign the user out, and
24
+ // the README says plainly what that means.
25
+ const DB_NAME = 'koolbase';
26
+ const STORE = 'kv';
27
+ function openDB() {
28
+ return new Promise((resolve, reject) => {
29
+ const req = indexedDB.open(DB_NAME, 1);
30
+ req.onupgradeneeded = () => {
31
+ const db = req.result;
32
+ if (!db.objectStoreNames.contains(STORE))
33
+ db.createObjectStore(STORE);
34
+ };
35
+ req.onsuccess = () => resolve(req.result);
36
+ req.onerror = () => reject(req.error ?? new Error('indexedDB open failed'));
37
+ });
38
+ }
39
+ function tx(db, mode, run) {
40
+ return new Promise((resolve, reject) => {
41
+ const t = db.transaction(STORE, mode);
42
+ const req = run(t.objectStore(STORE));
43
+ req.onsuccess = () => resolve(req.result);
44
+ req.onerror = () => reject(req.error ?? new Error('indexedDB request failed'));
45
+ });
46
+ }
47
+ function indexedDBStorage() {
48
+ let dbp = null;
49
+ const db = () => (dbp ?? (dbp = openDB()));
50
+ return {
51
+ // Not part of PlatformStorage. Test harnesses call it between cases so
52
+ // deleteDatabase is not blocked by a live connection.
53
+ close: async () => {
54
+ if (!dbp)
55
+ return;
56
+ (await dbp).close();
57
+ dbp = null;
58
+ },
59
+ getItem: async (k) => {
60
+ const v = await tx(await db(), 'readonly', s => s.get(k));
61
+ return v ?? null;
62
+ },
63
+ setItem: async (k, v) => { await tx(await db(), 'readwrite', s => s.put(v, k)); },
64
+ removeItem: async (k) => { await tx(await db(), 'readwrite', s => s.delete(k)); },
65
+ getAllKeys: async () => {
66
+ const keys = await tx(await db(), 'readonly', s => s.getAllKeys());
67
+ return keys.map(String);
68
+ },
69
+ };
70
+ }
71
+ // localStorage is synchronous and small, but present in more environments
72
+ // than IndexedDB (some private modes disable IDB). Used only when IDB is
73
+ // absent, so the common path keeps the larger, asynchronous store.
74
+ function localStorageStorage() {
75
+ return {
76
+ getItem: async (k) => localStorage.getItem(k),
77
+ setItem: async (k, v) => { localStorage.setItem(k, v); },
78
+ removeItem: async (k) => { localStorage.removeItem(k); },
79
+ getAllKeys: async () => {
80
+ const out = [];
81
+ for (let i = 0; i < localStorage.length; i++) {
82
+ const k = localStorage.key(i);
83
+ if (k !== null)
84
+ out.push(k);
85
+ }
86
+ return out;
87
+ },
88
+ };
89
+ }
90
+ function browserVersion() {
91
+ if (typeof navigator === 'undefined')
92
+ return '';
93
+ const ua = navigator.userAgent;
94
+ const m = /(Chrome|Firefox|Safari|Edg)\/([\d.]+)/.exec(ua);
95
+ return m ? `${m[1]} ${m[2]}` : '';
96
+ }
97
+ function browserPlatform() {
98
+ const hasIDB = typeof indexedDB !== 'undefined';
99
+ return {
100
+ storage: hasIDB ? indexedDBStorage() : localStorageStorage(),
101
+ network: {
102
+ onChange: (cb) => {
103
+ // navigator.onLine is a hint that the interface is up, not that
104
+ // Koolbase is reachable. The sync engine treats a false positive as
105
+ // a failed request, the same as any other; a false negative is only
106
+ // a delayed flush until the next event or manual sync.
107
+ // No window in a worker or during SSR: no events, and the sync
108
+ // engine falls back to discovering reachability by trying.
109
+ if (typeof window === 'undefined')
110
+ return () => { };
111
+ const on = () => cb(true);
112
+ const off = () => cb(false);
113
+ window.addEventListener('online', on);
114
+ window.addEventListener('offline', off);
115
+ return () => {
116
+ window.removeEventListener('online', on);
117
+ window.removeEventListener('offline', off);
118
+ };
119
+ },
120
+ },
121
+ lifecycle: {
122
+ onBackground: (cb) => {
123
+ if (typeof document === 'undefined' || typeof window === 'undefined')
124
+ return () => { };
125
+ const handler = () => { if (document.visibilityState === 'hidden')
126
+ cb(); };
127
+ document.addEventListener('visibilitychange', handler);
128
+ // pagehide is the reliable "tab is going away" signal on mobile
129
+ // browsers, where visibilitychange may not fire before unload.
130
+ window.addEventListener('pagehide', cb);
131
+ return () => {
132
+ document.removeEventListener('visibilitychange', handler);
133
+ window.removeEventListener('pagehide', cb);
134
+ };
135
+ },
136
+ },
137
+ info: {
138
+ os: 'web',
139
+ version: browserVersion(),
140
+ },
141
+ // IndexedDB-backed; see auth-storage.ts for what that does and does not
142
+ // protect against.
143
+ authStorage: () => new auth_storage_js_1.BrowserAuthStorage(),
144
+ };
145
+ }
@@ -0,0 +1,6 @@
1
+ import type { KoolbaseAuthStorage, KoolbaseSession } from '@koolbase/core';
2
+ export declare class BrowserAuthStorage implements KoolbaseAuthStorage {
3
+ readSession(): Promise<KoolbaseSession | null>;
4
+ saveSession(session: KoolbaseSession): Promise<void>;
5
+ clear(): Promise<void>;
6
+ }
@@ -0,0 +1,35 @@
1
+ import { getPlatform } from '@koolbase/core';
2
+ // Session persistence for the browser.
3
+ //
4
+ // This is the default when an app injects nothing through
5
+ // KoolbaseConfig.authStorage. It keeps the session in the platform's storage
6
+ // — IndexedDB — so a page refresh does not sign the user out.
7
+ //
8
+ // What it is not: secure the way a keychain is. IndexedDB, localStorage and
9
+ // every other store a page can read are readable by any script running on
10
+ // that page, so an XSS vulnerability in the app exposes the session. That is
11
+ // true of every browser SDK from every vendor; the mitigations are an
12
+ // application's content-security policy and short session lifetimes, not a
13
+ // storage backend. An app that needs a different trade-off — an httpOnly
14
+ // cookie set by its own backend, say — injects its own KoolbaseAuthStorage.
15
+ const KEY = 'koolbase_session_v1';
16
+ export class BrowserAuthStorage {
17
+ async readSession() {
18
+ const raw = await getPlatform().storage.getItem(KEY);
19
+ if (!raw)
20
+ return null;
21
+ try {
22
+ return JSON.parse(raw);
23
+ }
24
+ catch {
25
+ await this.clear();
26
+ return null;
27
+ }
28
+ }
29
+ async saveSession(session) {
30
+ await getPlatform().storage.setItem(KEY, JSON.stringify(session));
31
+ }
32
+ async clear() {
33
+ await getPlatform().storage.removeItem(KEY);
34
+ }
35
+ }
@@ -0,0 +1,18 @@
1
+ export * from '@koolbase/core';
2
+ export { BrowserAuthStorage } from './auth-storage.js';
3
+ export { browserPlatform } from './platform.js';
4
+ import { KoolbaseAuth, KoolbaseDatabase, KoolbaseStorage, KoolbaseRealtime, KoolbaseFunctions, KoolbaseAnalytics, type KoolbaseConfig, type VersionCheckResult } from '@koolbase/core';
5
+ export declare const Koolbase: {
6
+ initialize(config: KoolbaseConfig): Promise<void>;
7
+ readonly auth: KoolbaseAuth;
8
+ readonly db: KoolbaseDatabase;
9
+ readonly storage: KoolbaseStorage;
10
+ readonly realtime: KoolbaseRealtime;
11
+ readonly functions: KoolbaseFunctions;
12
+ readonly analytics: KoolbaseAnalytics;
13
+ isEnabled(key: string): boolean;
14
+ configString(key: string, fallback?: string): string;
15
+ configNumber(key: string, fallback?: number): number;
16
+ configBool(key: string, fallback?: boolean): boolean;
17
+ checkVersion(currentVersion: string): VersionCheckResult;
18
+ };
@@ -0,0 +1,61 @@
1
+ // @koolbase/js — composes @koolbase/core for the browser.
2
+ //
3
+ // The same surface as @koolbase/react-native where the browser can honour it.
4
+ // What is deliberately absent, so it is not discovered as a method that
5
+ // fails: code push (a native-bundle concept; the web already has one), the
6
+ // logic engine (its flows arrived only through code-push bundles), push
7
+ // messaging (FCM tokens come from a native module), and Apple/Google native
8
+ // sign-in (use the web OAuth flows against the same auth endpoints).
9
+ export * from '@koolbase/core';
10
+ export { BrowserAuthStorage } from './auth-storage.js';
11
+ export { browserPlatform } from './platform.js';
12
+ import { KoolbaseAuth, KoolbaseDatabase, KoolbaseStorage, KoolbaseRealtime, KoolbaseFunctions, KoolbaseFlags, KoolbaseAnalytics, getOrCreateDeviceId, setPlatform, } from '@koolbase/core';
13
+ import { browserPlatform } from './platform.js';
14
+ let _auth = null;
15
+ let _db = null;
16
+ let _storage = null;
17
+ let _realtime = null;
18
+ let _functions = null;
19
+ let _flags = null;
20
+ let _analytics = null;
21
+ let _initialized = false;
22
+ function ensureInitialized() {
23
+ if (!_initialized) {
24
+ throw new Error('Koolbase not initialized. Call Koolbase.initialize(config) first.');
25
+ }
26
+ }
27
+ export const Koolbase = {
28
+ async initialize(config) {
29
+ if (_initialized)
30
+ return;
31
+ setPlatform(config.platform ?? browserPlatform());
32
+ _auth = new KoolbaseAuth(config);
33
+ _db = new KoolbaseDatabase(config, () => _auth?.currentUser?.id ?? null, () => _auth?.validAccessToken() ?? Promise.resolve(null), async () => { await _auth?.clearStoredSession(); });
34
+ _storage = new KoolbaseStorage(config, () => _auth?.validAccessToken() ?? Promise.resolve(null), async () => { await _auth?.clearStoredSession(); });
35
+ _realtime = new KoolbaseRealtime(config, () => _auth?.validAccessToken() ?? Promise.resolve(null), () => _auth?.currentUser?.id ?? null);
36
+ _functions = new KoolbaseFunctions(config, () => _auth?.validAccessToken() ?? Promise.resolve(null), async () => { await _auth?.clearStoredSession(); });
37
+ const deviceId = await getOrCreateDeviceId();
38
+ _flags = new KoolbaseFlags(config, deviceId);
39
+ if (config.analyticsEnabled !== false) {
40
+ _analytics = new KoolbaseAnalytics(config);
41
+ await _analytics.init(config.appVersion);
42
+ }
43
+ _initialized = true;
44
+ },
45
+ get auth() { ensureInitialized(); return _auth; },
46
+ get db() { ensureInitialized(); return _db; },
47
+ get storage() { ensureInitialized(); return _storage; },
48
+ get realtime() { ensureInitialized(); return _realtime; },
49
+ get functions() { ensureInitialized(); return _functions; },
50
+ get analytics() {
51
+ ensureInitialized();
52
+ if (!_analytics)
53
+ throw new Error('Analytics is disabled (analyticsEnabled: false).');
54
+ return _analytics;
55
+ },
56
+ isEnabled(key) { ensureInitialized(); return _flags.isEnabled(key); },
57
+ configString(key, fallback = '') { ensureInitialized(); return _flags.getString(key, fallback); },
58
+ configNumber(key, fallback = 0) { ensureInitialized(); return _flags.getNumber(key, fallback); },
59
+ configBool(key, fallback = false) { ensureInitialized(); return _flags.getBool(key, fallback); },
60
+ checkVersion(currentVersion) { ensureInitialized(); return _flags.checkVersion(currentVersion); },
61
+ };
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,2 @@
1
+ import type { PlatformAdapter } from '@koolbase/core';
2
+ export declare function browserPlatform(): PlatformAdapter;
@@ -0,0 +1,142 @@
1
+ import { BrowserAuthStorage } from './auth-storage.js';
2
+ // The browser host, expressed through the platform seam.
3
+ //
4
+ // Storage is IndexedDB behind the same four calls the core makes — get, set,
5
+ // remove, list. One object store, string keys, string values, one operation
6
+ // per transaction: exactly the contract the offline queue and conflict store
7
+ // were verified against, with nothing the RN adapter does not also do.
8
+ //
9
+ // What this adapter does NOT do, stated so it is not discovered later:
10
+ //
11
+ // - Multi-tab coordination. Two tabs share one IndexedDB and one write
12
+ // queue; both may replay the same pending write. Inserts are idempotent
13
+ // on the server (ids are UUIDs from birth), so the damage is bounded, but
14
+ // a conflict resolved in one tab can be re-resolved in another. v1 is
15
+ // single-tab; a leader election is the fix and belongs in its own change.
16
+ //
17
+ // - Secure token storage. There is no keychain in a browser. Anything
18
+ // JavaScript can read, a script injected by XSS can read. The core's
19
+ // KoolbaseAuthStorage is the injection point; the default here persists
20
+ // the session in IndexedDB so a refresh does not sign the user out, and
21
+ // the README says plainly what that means.
22
+ const DB_NAME = 'koolbase';
23
+ const STORE = 'kv';
24
+ function openDB() {
25
+ return new Promise((resolve, reject) => {
26
+ const req = indexedDB.open(DB_NAME, 1);
27
+ req.onupgradeneeded = () => {
28
+ const db = req.result;
29
+ if (!db.objectStoreNames.contains(STORE))
30
+ db.createObjectStore(STORE);
31
+ };
32
+ req.onsuccess = () => resolve(req.result);
33
+ req.onerror = () => reject(req.error ?? new Error('indexedDB open failed'));
34
+ });
35
+ }
36
+ function tx(db, mode, run) {
37
+ return new Promise((resolve, reject) => {
38
+ const t = db.transaction(STORE, mode);
39
+ const req = run(t.objectStore(STORE));
40
+ req.onsuccess = () => resolve(req.result);
41
+ req.onerror = () => reject(req.error ?? new Error('indexedDB request failed'));
42
+ });
43
+ }
44
+ function indexedDBStorage() {
45
+ let dbp = null;
46
+ const db = () => (dbp ?? (dbp = openDB()));
47
+ return {
48
+ // Not part of PlatformStorage. Test harnesses call it between cases so
49
+ // deleteDatabase is not blocked by a live connection.
50
+ close: async () => {
51
+ if (!dbp)
52
+ return;
53
+ (await dbp).close();
54
+ dbp = null;
55
+ },
56
+ getItem: async (k) => {
57
+ const v = await tx(await db(), 'readonly', s => s.get(k));
58
+ return v ?? null;
59
+ },
60
+ setItem: async (k, v) => { await tx(await db(), 'readwrite', s => s.put(v, k)); },
61
+ removeItem: async (k) => { await tx(await db(), 'readwrite', s => s.delete(k)); },
62
+ getAllKeys: async () => {
63
+ const keys = await tx(await db(), 'readonly', s => s.getAllKeys());
64
+ return keys.map(String);
65
+ },
66
+ };
67
+ }
68
+ // localStorage is synchronous and small, but present in more environments
69
+ // than IndexedDB (some private modes disable IDB). Used only when IDB is
70
+ // absent, so the common path keeps the larger, asynchronous store.
71
+ function localStorageStorage() {
72
+ return {
73
+ getItem: async (k) => localStorage.getItem(k),
74
+ setItem: async (k, v) => { localStorage.setItem(k, v); },
75
+ removeItem: async (k) => { localStorage.removeItem(k); },
76
+ getAllKeys: async () => {
77
+ const out = [];
78
+ for (let i = 0; i < localStorage.length; i++) {
79
+ const k = localStorage.key(i);
80
+ if (k !== null)
81
+ out.push(k);
82
+ }
83
+ return out;
84
+ },
85
+ };
86
+ }
87
+ function browserVersion() {
88
+ if (typeof navigator === 'undefined')
89
+ return '';
90
+ const ua = navigator.userAgent;
91
+ const m = /(Chrome|Firefox|Safari|Edg)\/([\d.]+)/.exec(ua);
92
+ return m ? `${m[1]} ${m[2]}` : '';
93
+ }
94
+ export function browserPlatform() {
95
+ const hasIDB = typeof indexedDB !== 'undefined';
96
+ return {
97
+ storage: hasIDB ? indexedDBStorage() : localStorageStorage(),
98
+ network: {
99
+ onChange: (cb) => {
100
+ // navigator.onLine is a hint that the interface is up, not that
101
+ // Koolbase is reachable. The sync engine treats a false positive as
102
+ // a failed request, the same as any other; a false negative is only
103
+ // a delayed flush until the next event or manual sync.
104
+ // No window in a worker or during SSR: no events, and the sync
105
+ // engine falls back to discovering reachability by trying.
106
+ if (typeof window === 'undefined')
107
+ return () => { };
108
+ const on = () => cb(true);
109
+ const off = () => cb(false);
110
+ window.addEventListener('online', on);
111
+ window.addEventListener('offline', off);
112
+ return () => {
113
+ window.removeEventListener('online', on);
114
+ window.removeEventListener('offline', off);
115
+ };
116
+ },
117
+ },
118
+ lifecycle: {
119
+ onBackground: (cb) => {
120
+ if (typeof document === 'undefined' || typeof window === 'undefined')
121
+ return () => { };
122
+ const handler = () => { if (document.visibilityState === 'hidden')
123
+ cb(); };
124
+ document.addEventListener('visibilitychange', handler);
125
+ // pagehide is the reliable "tab is going away" signal on mobile
126
+ // browsers, where visibilitychange may not fire before unload.
127
+ window.addEventListener('pagehide', cb);
128
+ return () => {
129
+ document.removeEventListener('visibilitychange', handler);
130
+ window.removeEventListener('pagehide', cb);
131
+ };
132
+ },
133
+ },
134
+ info: {
135
+ os: 'web',
136
+ version: browserVersion(),
137
+ },
138
+ // IndexedDB-backed; see auth-storage.ts for what that does and does not
139
+ // protect against.
140
+ authStorage: () => new BrowserAuthStorage(),
141
+ };
142
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@koolbase/js",
3
+ "version": "10.0.0",
4
+ "description": "Koolbase SDK for the browser \u2014 auth, database, storage, realtime, functions, flags and offline sync in one package.",
5
+ "main": "./dist/cjs/index.js",
6
+ "types": "./dist/esm/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "CHANGELOG.md"
10
+ ],
11
+ "scripts": {
12
+ "build": "node ../../scripts/build-package.js js"
13
+ },
14
+ "keywords": [
15
+ "koolbase",
16
+ "baas",
17
+ "firebase-alternative",
18
+ "supabase-alternative",
19
+ "browser",
20
+ "typescript"
21
+ ],
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/koolbase/koolbase-react-native"
26
+ },
27
+ "dependencies": {
28
+ "@koolbase/core": "10.0.0"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "module": "./dist/esm/index.js",
34
+ "exports": {
35
+ ".": {
36
+ "import": {
37
+ "types": "./dist/esm/index.d.ts",
38
+ "default": "./dist/esm/index.js"
39
+ },
40
+ "require": {
41
+ "types": "./dist/cjs/index.d.ts",
42
+ "default": "./dist/cjs/index.js"
43
+ }
44
+ },
45
+ "./package.json": "./package.json"
46
+ },
47
+ "sideEffects": false
48
+ }