@techfinityedge/koolbase-react-native 1.7.0 → 1.9.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 CHANGED
@@ -1,18 +1,6 @@
1
- # Koolbase React Native SDK
1
+ # @techfinityedge/koolbase-react-native
2
2
 
3
- ⚠️ ## This package has moved
4
-
5
- This package is deprecated.
6
-
7
- 👉 Install the official version:
8
-
9
- ```bash
10
- npm install @techfinityedge/koolbase-react-native
11
- ```
12
-
13
- ---
14
-
15
- [![npm](https://img.shields.io/npm/v/koolbase-react-native.svg)](https://www.npmjs.com/package/koolbase-react-native)
3
+ [![npm](https://img.shields.io/npm/v/@techfinityedge/koolbase-react-native.svg)](https://www.npmjs.com/package/@techfinityedge/koolbase-react-native)
16
4
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
17
5
 
18
6
  React Native SDK for [Koolbase](https://koolbase.com) — Backend as a Service built for mobile developers.
@@ -30,15 +18,15 @@ Auth, database, storage, realtime, functions, feature flags, remote config, vers
30
18
  3. Add the SDK:
31
19
 
32
20
  ```bash
33
- npm install koolbase-react-native
21
+ npm install @techfinityedge/koolbase-react-native@^1.8.0
34
22
  # or
35
- yarn add koolbase-react-native
23
+ yarn add @techfinityedge/koolbase-react-native@^1.8.0
36
24
  ```
37
25
 
38
26
  **4. Initialize at app startup:**
39
27
 
40
28
  ```typescript
41
- import { Koolbase } from 'koolbase-react-native';
29
+ import { Koolbase } from '@techfinityedge/koolbase-react-native';
42
30
 
43
31
  await Koolbase.initialize({
44
32
  publicKey: 'pk_live_xxxx',
@@ -52,6 +40,8 @@ That's it. Every feature below is now available via `Koolbase.*`.
52
40
 
53
41
  ## Authentication
54
42
 
43
+ Email + password, Google, Apple, and phone + OTP — out of the box.
44
+
55
45
  ```typescript
56
46
  // Register
57
47
  await Koolbase.auth.register({ email: 'user@example.com', password: 'password' });
@@ -69,6 +59,50 @@ await Koolbase.auth.logout();
69
59
  await Koolbase.auth.forgotPassword('user@example.com');
70
60
  ```
71
61
 
62
+ ### OAuth — Google & Apple
63
+
64
+ ```typescript
65
+ // Google
66
+ await Koolbase.auth.signInWithGoogle({ idToken: googleIdToken });
67
+ ```
68
+
69
+ Apple uses the native authentication flow via `@invertase/react-native-apple-authentication` as a peer dependency:
70
+
71
+ ```typescript
72
+ import { KoolbaseAppleAuth } from '@techfinityedge/koolbase-react-native';
73
+ import { appleAuth } from '@invertase/react-native-apple-authentication';
74
+
75
+ const session = await KoolbaseAppleAuth.signIn(async () => {
76
+ return await appleAuth.performRequest({
77
+ requestedOperation: appleAuth.Operation.LOGIN,
78
+ requestedScopes: [appleAuth.Scope.EMAIL, appleAuth.Scope.FULL_NAME],
79
+ });
80
+ });
81
+ ```
82
+
83
+ Full setup guide at [docs.koolbase.com/auth/oauth](https://docs.koolbase.com/auth/oauth).
84
+
85
+ ### Phone + OTP
86
+
87
+ ```typescript
88
+ // Send a one-time code
89
+ await Koolbase.auth.sendOtp({ phoneE164: '+233200000000' });
90
+
91
+ // Verify and sign in
92
+ await Koolbase.auth.verifyOtp({
93
+ phoneE164: '+233200000000',
94
+ code: '123456',
95
+ });
96
+
97
+ // Or link a phone to an existing account
98
+ await Koolbase.auth.linkPhone({
99
+ phoneE164: '+233200000000',
100
+ code: '123456',
101
+ });
102
+ ```
103
+
104
+ Configure your SMS provider (Twilio, Africa's Talking, or Hubtel) in the dashboard under Phone Auth.
105
+
72
106
  ---
73
107
 
74
108
  ## Database
@@ -86,7 +120,7 @@ const { records } = await Koolbase.db.query('posts', {
86
120
  });
87
121
 
88
122
  // Populate related records
89
- const { records } = await Koolbase.db.query('posts', {
123
+ const { records: postsWithAuthor } = await Koolbase.db.query('posts', {
90
124
  populate: ['author_id:users'],
91
125
  });
92
126
 
@@ -134,10 +168,40 @@ unsubscribe();
134
168
 
135
169
  ---
136
170
 
171
+ ## Functions
172
+
173
+ Invoke deployed serverless functions. When a user is signed in via `Koolbase.auth`, their access token is automatically forwarded — the function receives the caller's identity via `ctx.auth`. No token handling on the client side.
174
+
175
+ ```typescript
176
+ // Invoke a deployed function
177
+ const result = await Koolbase.functions.invoke('send-welcome-email', {
178
+ userId: '123',
179
+ });
180
+
181
+ if (result.success) console.log(result.data);
182
+ ```
183
+
184
+ Inside the function (Deno runtime), read the caller:
185
+
186
+ ```typescript
187
+ export async function handler(ctx) {
188
+ const userId = ctx.auth?.user_id;
189
+ if (!userId) {
190
+ return { error: { code: 'AUTH_REQUIRED' }, status: 401 };
191
+ }
192
+ // Authenticated logic here
193
+ return { ok: true };
194
+ }
195
+ ```
196
+
197
+ Token refresh is transparent — the SDK reads the current token fresh on every invoke. Full docs at [docs.koolbase.com/functions/authentication](https://docs.koolbase.com/functions/authentication).
198
+
199
+ ---
200
+
137
201
  ## Feature Flags & Remote Config
138
202
 
139
203
  ```typescript
140
- if (Koolbase.isEnabled('new_checkout')) { ... }
204
+ if (Koolbase.isEnabled('new_checkout')) { /* ... */ }
141
205
 
142
206
  const timeout = Koolbase.configNumber('timeout_seconds', 30);
143
207
  const apiUrl = Koolbase.configString('api_url', 'https://api.myapp.com');
@@ -159,6 +223,8 @@ if (result.status === 'force_update') {
159
223
 
160
224
  ## Code Push
161
225
 
226
+ Push config overrides, feature flag overrides, and directive-driven behaviour without a store release.
227
+
162
228
  ```typescript
163
229
  await Koolbase.initialize({
164
230
  publicKey: 'pk_live_xxxx',
@@ -180,10 +246,13 @@ Koolbase.codePush.applyDirectives();
180
246
 
181
247
  ## Logic Engine
182
248
 
249
+ Define conditional app behavior as data in your Runtime Bundle — no code changes required.
250
+
183
251
  ```typescript
184
- // Define flows in your bundle's flows.json
185
- // Execute from anywhere in your app
186
- const result = Koolbase.executeFlow('on_checkout_tap', { plan: user.plan });
252
+ const result = Koolbase.executeFlow('on_checkout_tap', {
253
+ plan: user.plan,
254
+ usage: user.usage,
255
+ });
187
256
 
188
257
  if (result.hasEvent) {
189
258
  switch (result.eventName) {
@@ -193,10 +262,16 @@ if (result.hasEvent) {
193
262
  }
194
263
  ```
195
264
 
265
+ **v2 operators:** `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `contains`, `starts_with`, `ends_with`, `in_list`, `not_in_list`, `between`, `is_true`, `is_false`, `exists`, `not_exists`, `and`, `or`
266
+
267
+ Full docs at [docs.koolbase.com/sdk/logic-engine](https://docs.koolbase.com/sdk/logic-engine).
268
+
196
269
  ---
197
270
 
198
271
  ## Analytics
199
272
 
273
+ Track screen views, custom events, and user behaviour. View DAU, WAU, MAU, funnels, and retention in the Koolbase dashboard.
274
+
200
275
  ```typescript
201
276
  await Koolbase.initialize({
202
277
  publicKey: 'pk_live_xxxx',
@@ -248,43 +323,22 @@ await Koolbase.messaging.send({
248
323
 
249
324
  ---
250
325
 
251
- ## Logic Engine v2
252
-
253
- ```typescript
254
- const result = Koolbase.executeFlow('on_checkout_tap', {
255
- plan: user.plan,
256
- usage: user.usage,
257
- });
258
-
259
- if (result.hasEvent) {
260
- switch (result.eventName) {
261
- case 'show_upgrade': navigation.navigate('Upgrade'); break;
262
- case 'go_checkout': navigation.navigate('Checkout'); break;
263
- }
264
- }
265
- ```
266
-
267
- **v2 operators:** `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `contains`, `starts_with`, `ends_with`, `in_list`, `not_in_list`, `between`, `is_true`, `is_false`, `exists`, `not_exists`, `and`, `or`
268
-
269
- Full docs at [docs.koolbase.com/sdk/logic-engine](https://docs.koolbase.com/sdk/logic-engine).
270
-
271
- ---
272
-
273
- ## Sign in with Apple
274
-
275
- ```typescript
276
- import { KoolbaseAppleAuth } from 'koolbase-react-native';
277
- import { appleAuth } from '@invertase/react-native-apple-authentication';
278
-
279
- const session = await KoolbaseAppleAuth.signIn(async () => {
280
- return await appleAuth.performRequest({
281
- requestedOperation: appleAuth.Operation.LOGIN,
282
- requestedScopes: [appleAuth.Scope.EMAIL, appleAuth.Scope.FULL_NAME],
283
- });
284
- });
285
- ```
286
-
287
- Install `@invertase/react-native-apple-authentication` as a peer dependency. Full setup guide at [docs.koolbase.com/auth/oauth](https://docs.koolbase.com/auth/oauth).
326
+ ## What's included
327
+
328
+ | Feature | Koolbase | Firebase | Supabase |
329
+ | --- | --- | --- | --- |
330
+ | TypeScript SDK | Yes | Yes | Yes |
331
+ | Feature flags | Yes | — | — |
332
+ | Remote config | Yes | Yes | — |
333
+ | Version enforcement | Yes | — | — |
334
+ | Offline-first database | Yes | Yes | — |
335
+ | Code push | Yes | — | — |
336
+ | Logic engine (flows OTA) | Yes | — | — |
337
+ | Analytics | Yes | Yes | — |
338
+ | Cloud Messaging | Yes | Yes | — |
339
+ | Sign in with Apple | Yes | Yes | Yes |
340
+ | Phone + OTP | Yes | Yes | Yes |
341
+ | Authenticated functions (`ctx.auth`) | Yes | Yes | Yes |
288
342
 
289
343
  ---
290
344
 
@@ -300,7 +354,7 @@ Manage your projects at [app.koolbase.com](https://app.koolbase.com)
300
354
 
301
355
  - [GitHub Issues](https://github.com/kennedyowusu/koolbase-react-native/issues)
302
356
  - [docs.koolbase.com](https://docs.koolbase.com)
303
- - Email: hello@koolbase.com
357
+ - Email: <hello@koolbase.com>
304
358
 
305
359
  ## License
306
360
 
@@ -1,7 +1,78 @@
1
+ /**
2
+ * Base error type for all Koolbase auth errors. Catchable via
3
+ * `instanceof KoolbaseAuthError` to handle any auth-related failure
4
+ * generically; subclasses let you handle specific cases.
5
+ */
1
6
  export declare class KoolbaseAuthError extends Error {
2
7
  code?: string;
3
8
  constructor(message: string, code?: string);
4
9
  }
10
+ export declare class InvalidCredentialsError extends KoolbaseAuthError {
11
+ constructor();
12
+ }
13
+ export declare class EmailAlreadyInUseError extends KoolbaseAuthError {
14
+ constructor();
15
+ }
16
+ export declare class UserDisabledError extends KoolbaseAuthError {
17
+ constructor();
18
+ }
19
+ export declare class WeakPasswordError extends KoolbaseAuthError {
20
+ constructor();
21
+ }
22
+ export declare class SessionExpiredError extends KoolbaseAuthError {
23
+ constructor();
24
+ }
25
+ /**
26
+ * Thrown when the access token references a session that has been revoked
27
+ * centrally — either by the user (sessions endpoint) or an administrator.
28
+ * Distinct from {@link SessionExpiredError} which indicates the access
29
+ * token TTL elapsed without a successful refresh.
30
+ *
31
+ * Forward-compatible: matches multiple server message patterns so it stays
32
+ * accurate as the server's revocation signaling evolves.
33
+ */
34
+ export declare class TokenRevokedError extends KoolbaseAuthError {
35
+ constructor();
36
+ }
37
+ /**
38
+ * Thrown when the account is temporarily locked due to too many failed
39
+ * login attempts. The server uses progressive 5/10/20-attempt lockouts;
40
+ * if an unlock email was issued (level 2+), the user can clear the lock
41
+ * by passing that token to {@link KoolbaseAuth.unlock}.
42
+ *
43
+ * [lockedUntil] is currently null — the server returns a generic 429 but
44
+ * does not yet include the unlock timestamp in the response body. Field
45
+ * is forward-compatible for when the server adds it.
46
+ */
47
+ export declare class AccountLockedError extends KoolbaseAuthError {
48
+ lockedUntil?: Date;
49
+ constructor(lockedUntil?: Date);
50
+ }
51
+ /**
52
+ * Thrown when the unlock token from a brute-force unlock email is
53
+ * invalid, expired, or already consumed. Unlock tokens are one-shot.
54
+ */
55
+ export declare class UnlockTokenInvalidError extends KoolbaseAuthError {
56
+ constructor();
57
+ }
58
+ /**
59
+ * Thrown when the server rate-limits a non-phone authentication endpoint
60
+ * (HTTP 429 without the "account temporarily locked" marker). Phone OTP
61
+ * endpoints throw {@link OtpRateLimitError} instead — they hit a
62
+ * separate server-side rate-limiter.
63
+ */
64
+ export declare class RateLimitError extends KoolbaseAuthError {
65
+ constructor(message?: string);
66
+ }
67
+ /**
68
+ * Generic network error. The SDK does NOT throw this directly — fetch
69
+ * failures (DNS, no connection, timeout) propagate as native TypeErrors.
70
+ * This class exists for consumer code that wants to construct or
71
+ * `instanceof`-check a typed network error from their own retry logic.
72
+ */
73
+ export declare class NetworkError extends KoolbaseAuthError {
74
+ constructor();
75
+ }
5
76
  export declare class InvalidPhoneNumberError extends KoolbaseAuthError {
6
77
  constructor();
7
78
  }
@@ -1,6 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SmsConfigMissingError = exports.PhoneAlreadyLinkedError = exports.OtpRateLimitError = exports.OtpMaxAttemptsError = exports.OtpInvalidError = exports.OtpExpiredError = exports.InvalidPhoneNumberError = exports.KoolbaseAuthError = void 0;
3
+ exports.SmsConfigMissingError = exports.PhoneAlreadyLinkedError = exports.OtpRateLimitError = exports.OtpMaxAttemptsError = exports.OtpInvalidError = exports.OtpExpiredError = exports.InvalidPhoneNumberError = exports.NetworkError = exports.RateLimitError = exports.UnlockTokenInvalidError = exports.AccountLockedError = exports.TokenRevokedError = exports.SessionExpiredError = exports.WeakPasswordError = exports.UserDisabledError = exports.EmailAlreadyInUseError = exports.InvalidCredentialsError = exports.KoolbaseAuthError = void 0;
4
+ /**
5
+ * Base error type for all Koolbase auth errors. Catchable via
6
+ * `instanceof KoolbaseAuthError` to handle any auth-related failure
7
+ * generically; subclasses let you handle specific cases.
8
+ */
4
9
  class KoolbaseAuthError extends Error {
5
10
  constructor(message, code) {
6
11
  super(message);
@@ -10,6 +15,127 @@ class KoolbaseAuthError extends Error {
10
15
  }
11
16
  }
12
17
  exports.KoolbaseAuthError = KoolbaseAuthError;
18
+ // ─── Credentials / Registration ────────────────────────────────────────────
19
+ class InvalidCredentialsError extends KoolbaseAuthError {
20
+ constructor() {
21
+ super('Invalid email or password', 'invalid_credentials');
22
+ this.name = 'InvalidCredentialsError';
23
+ Object.setPrototypeOf(this, InvalidCredentialsError.prototype);
24
+ }
25
+ }
26
+ exports.InvalidCredentialsError = InvalidCredentialsError;
27
+ class EmailAlreadyInUseError extends KoolbaseAuthError {
28
+ constructor() {
29
+ super('Email is already in use', 'email_taken');
30
+ this.name = 'EmailAlreadyInUseError';
31
+ Object.setPrototypeOf(this, EmailAlreadyInUseError.prototype);
32
+ }
33
+ }
34
+ exports.EmailAlreadyInUseError = EmailAlreadyInUseError;
35
+ class UserDisabledError extends KoolbaseAuthError {
36
+ constructor() {
37
+ super('This account has been disabled', 'user_disabled');
38
+ this.name = 'UserDisabledError';
39
+ Object.setPrototypeOf(this, UserDisabledError.prototype);
40
+ }
41
+ }
42
+ exports.UserDisabledError = UserDisabledError;
43
+ class WeakPasswordError extends KoolbaseAuthError {
44
+ constructor() {
45
+ super('Password must be at least 8 characters', 'weak_password');
46
+ this.name = 'WeakPasswordError';
47
+ Object.setPrototypeOf(this, WeakPasswordError.prototype);
48
+ }
49
+ }
50
+ exports.WeakPasswordError = WeakPasswordError;
51
+ // ─── Session lifecycle ─────────────────────────────────────────────────────
52
+ class SessionExpiredError extends KoolbaseAuthError {
53
+ constructor() {
54
+ super('Session expired, please log in again', 'session_expired');
55
+ this.name = 'SessionExpiredError';
56
+ Object.setPrototypeOf(this, SessionExpiredError.prototype);
57
+ }
58
+ }
59
+ exports.SessionExpiredError = SessionExpiredError;
60
+ /**
61
+ * Thrown when the access token references a session that has been revoked
62
+ * centrally — either by the user (sessions endpoint) or an administrator.
63
+ * Distinct from {@link SessionExpiredError} which indicates the access
64
+ * token TTL elapsed without a successful refresh.
65
+ *
66
+ * Forward-compatible: matches multiple server message patterns so it stays
67
+ * accurate as the server's revocation signaling evolves.
68
+ */
69
+ class TokenRevokedError extends KoolbaseAuthError {
70
+ constructor() {
71
+ super('Session has been revoked, please log in again', 'token_revoked');
72
+ this.name = 'TokenRevokedError';
73
+ Object.setPrototypeOf(this, TokenRevokedError.prototype);
74
+ }
75
+ }
76
+ exports.TokenRevokedError = TokenRevokedError;
77
+ // ─── Brute-force protection ────────────────────────────────────────────────
78
+ /**
79
+ * Thrown when the account is temporarily locked due to too many failed
80
+ * login attempts. The server uses progressive 5/10/20-attempt lockouts;
81
+ * if an unlock email was issued (level 2+), the user can clear the lock
82
+ * by passing that token to {@link KoolbaseAuth.unlock}.
83
+ *
84
+ * [lockedUntil] is currently null — the server returns a generic 429 but
85
+ * does not yet include the unlock timestamp in the response body. Field
86
+ * is forward-compatible for when the server adds it.
87
+ */
88
+ class AccountLockedError extends KoolbaseAuthError {
89
+ constructor(lockedUntil) {
90
+ super('Account temporarily locked due to too many failed attempts', 'account_locked');
91
+ this.lockedUntil = lockedUntil;
92
+ this.name = 'AccountLockedError';
93
+ Object.setPrototypeOf(this, AccountLockedError.prototype);
94
+ }
95
+ }
96
+ exports.AccountLockedError = AccountLockedError;
97
+ /**
98
+ * Thrown when the unlock token from a brute-force unlock email is
99
+ * invalid, expired, or already consumed. Unlock tokens are one-shot.
100
+ */
101
+ class UnlockTokenInvalidError extends KoolbaseAuthError {
102
+ constructor() {
103
+ super('Unlock link is invalid or has expired', 'unlock_token_invalid');
104
+ this.name = 'UnlockTokenInvalidError';
105
+ Object.setPrototypeOf(this, UnlockTokenInvalidError.prototype);
106
+ }
107
+ }
108
+ exports.UnlockTokenInvalidError = UnlockTokenInvalidError;
109
+ /**
110
+ * Thrown when the server rate-limits a non-phone authentication endpoint
111
+ * (HTTP 429 without the "account temporarily locked" marker). Phone OTP
112
+ * endpoints throw {@link OtpRateLimitError} instead — they hit a
113
+ * separate server-side rate-limiter.
114
+ */
115
+ class RateLimitError extends KoolbaseAuthError {
116
+ constructor(message) {
117
+ super(message ?? 'Too many requests, please wait before trying again', 'rate_limit');
118
+ this.name = 'RateLimitError';
119
+ Object.setPrototypeOf(this, RateLimitError.prototype);
120
+ }
121
+ }
122
+ exports.RateLimitError = RateLimitError;
123
+ // ─── Network ───────────────────────────────────────────────────────────────
124
+ /**
125
+ * Generic network error. The SDK does NOT throw this directly — fetch
126
+ * failures (DNS, no connection, timeout) propagate as native TypeErrors.
127
+ * This class exists for consumer code that wants to construct or
128
+ * `instanceof`-check a typed network error from their own retry logic.
129
+ */
130
+ class NetworkError extends KoolbaseAuthError {
131
+ constructor() {
132
+ super('Network error, please check your connection', 'network_error');
133
+ this.name = 'NetworkError';
134
+ Object.setPrototypeOf(this, NetworkError.prototype);
135
+ }
136
+ }
137
+ exports.NetworkError = NetworkError;
138
+ // ─── Phone OTP (unchanged from earlier releases) ───────────────────────────
13
139
  class InvalidPhoneNumberError extends KoolbaseAuthError {
14
140
  constructor() {
15
141
  super('Phone number must be in E.164 format (e.g. +233XXXXXXXXX)', 'invalid_phone');
@@ -0,0 +1,26 @@
1
+ import { KoolbaseAuthStorage, KoolbaseSession } from './types';
2
+ /**
3
+ * Probe whether react-native-keychain is installed without throwing.
4
+ * Used by KoolbaseAuth to decide whether to instantiate a default
5
+ * SecureAuthStorage or proceed without persistence.
6
+ */
7
+ export declare function isKeychainAvailable(): boolean;
8
+ /**
9
+ * Default secure storage implementation backed by `react-native-keychain`.
10
+ *
11
+ * - iOS: Keychain (encrypted, never synced via iCloud by default)
12
+ * - Android: Android Keystore-backed encryption
13
+ *
14
+ * Requires `react-native-keychain` as a peer dependency. Apps that prefer a
15
+ * different secure backend (e.g. `expo-secure-store`,
16
+ * `react-native-encrypted-storage`, an in-memory mock for testing, or a
17
+ * compliance-grade encryption layer) should implement the
18
+ * {@link KoolbaseAuthStorage} interface and pass it via
19
+ * `KoolbaseConfig.authStorage`.
20
+ */
21
+ export declare class SecureAuthStorage implements KoolbaseAuthStorage {
22
+ private static readonly SERVICE;
23
+ saveSession(session: KoolbaseSession): Promise<void>;
24
+ readSession(): Promise<KoolbaseSession | null>;
25
+ clear(): Promise<void>;
26
+ }
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SecureAuthStorage = void 0;
4
+ exports.isKeychainAvailable = isKeychainAvailable;
5
+ // Lazy-load react-native-keychain so apps without it installed (e.g. Expo Go,
6
+ // or those providing a custom adapter) can still import this module without
7
+ // crashing. The default SecureAuthStorage will throw a clear error on first
8
+ // use if the peer dependency is missing.
9
+ let _keychain = null;
10
+ let _keychainAttempted = false;
11
+ function loadKeychain() {
12
+ if (!_keychainAttempted) {
13
+ _keychainAttempted = true;
14
+ try {
15
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
16
+ _keychain = require('react-native-keychain');
17
+ }
18
+ catch {
19
+ _keychain = null;
20
+ }
21
+ }
22
+ if (!_keychain) {
23
+ throw new Error('[Koolbase] SecureAuthStorage requires react-native-keychain. ' +
24
+ 'Install it with:\n npm install react-native-keychain\n' +
25
+ 'Or provide your own storage adapter via ' +
26
+ 'KoolbaseConfig.authStorage. For Expo Go (where ' +
27
+ 'react-native-keychain is unavailable), implement KoolbaseAuthStorage ' +
28
+ 'with expo-secure-store and pass it via authStorage.');
29
+ }
30
+ return _keychain;
31
+ }
32
+ /**
33
+ * Probe whether react-native-keychain is installed without throwing.
34
+ * Used by KoolbaseAuth to decide whether to instantiate a default
35
+ * SecureAuthStorage or proceed without persistence.
36
+ */
37
+ function isKeychainAvailable() {
38
+ if (!_keychainAttempted) {
39
+ _keychainAttempted = true;
40
+ try {
41
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
42
+ _keychain = require('react-native-keychain');
43
+ }
44
+ catch {
45
+ _keychain = null;
46
+ }
47
+ }
48
+ return _keychain !== null;
49
+ }
50
+ /**
51
+ * Default secure storage implementation backed by `react-native-keychain`.
52
+ *
53
+ * - iOS: Keychain (encrypted, never synced via iCloud by default)
54
+ * - Android: Android Keystore-backed encryption
55
+ *
56
+ * Requires `react-native-keychain` as a peer dependency. Apps that prefer a
57
+ * different secure backend (e.g. `expo-secure-store`,
58
+ * `react-native-encrypted-storage`, an in-memory mock for testing, or a
59
+ * compliance-grade encryption layer) should implement the
60
+ * {@link KoolbaseAuthStorage} interface and pass it via
61
+ * `KoolbaseConfig.authStorage`.
62
+ */
63
+ class SecureAuthStorage {
64
+ async saveSession(session) {
65
+ const Keychain = loadKeychain();
66
+ await Keychain.setGenericPassword('session', JSON.stringify(session), { service: SecureAuthStorage.SERVICE });
67
+ }
68
+ async readSession() {
69
+ let Keychain;
70
+ try {
71
+ Keychain = loadKeychain();
72
+ }
73
+ catch {
74
+ // Peer dep missing — caller will fall back to no persistence.
75
+ return null;
76
+ }
77
+ try {
78
+ const credentials = await Keychain.getGenericPassword({
79
+ service: SecureAuthStorage.SERVICE,
80
+ });
81
+ if (!credentials || !credentials.password)
82
+ return null;
83
+ return JSON.parse(credentials.password);
84
+ }
85
+ catch {
86
+ // Corrupt data, schema mismatch, or platform-level keychain error.
87
+ // Treat as no session — caller will trigger fresh login.
88
+ return null;
89
+ }
90
+ }
91
+ async clear() {
92
+ let Keychain;
93
+ try {
94
+ Keychain = loadKeychain();
95
+ }
96
+ catch {
97
+ return; // No-op if peer dep missing.
98
+ }
99
+ await Keychain.resetGenericPassword({
100
+ service: SecureAuthStorage.SERVICE,
101
+ });
102
+ }
103
+ }
104
+ exports.SecureAuthStorage = SecureAuthStorage;
105
+ SecureAuthStorage.SERVICE = 'koolbase_session_v1';