@pollar/core 0.6.0 → 0.7.1

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
@@ -2,6 +2,11 @@
2
2
 
3
3
  Core SDK for [Pollar](https://pollar.xyz) — authentication and transaction utilities for Stellar-based applications.
4
4
 
5
+ > **0.7.0 ships sender-constrained tokens via DPoP (RFC 9449), pluggable storage and key managers, automatic
6
+ refresh-on-401, and removes PII from persisted storage.** This is a breaking change — read
7
+ > the [CHANGELOG](../../CHANGELOG.md) before upgrading. Requires HTTPS and
8
+ `sdk-api` ≥ Phase 5.
9
+
5
10
  ## Installation
6
11
 
7
12
  ```bash
@@ -12,43 +17,130 @@ pnpm add @pollar/core
12
17
  yarn add @pollar/core
13
18
  ```
14
19
 
20
+ For React Native / Expo, also install one of the storage adapter peer deps:
21
+
22
+ ```bash
23
+ # Expo
24
+ npx expo install expo-secure-store react-native-get-random-values
25
+
26
+ # Bare React Native
27
+ npm i react-native-keychain react-native-get-random-values
28
+ ```
29
+
15
30
  ## Overview
16
31
 
17
32
  `@pollar/core` provides the `PollarClient` class and utilities to:
18
33
 
19
34
  - Authenticate users via **Google**, **GitHub**, **Email (OTP)**, or **Stellar wallets** (Freighter, Albedo)
35
+ - Sign every authenticated request with **DPoP** (RFC 9449), making stolen tokens useless to an attacker without the
36
+ per-session keypair
20
37
  - Build and submit Stellar transactions
21
38
  - Fetch Stellar account balances
22
39
  - React to real-time authentication state changes
23
40
 
24
- ## Quick Start
41
+ ## Quick Start (web)
42
+
43
+ ```ts
44
+ import { PollarClient } from '@pollar/core';
45
+
46
+ const client = new PollarClient({ apiKey: 'your-api-key' });
47
+ // Storage and KeyManager autodetect:
48
+ // storage → localStorage with in-memory fallback
49
+ // keypair → WebCrypto ECDSA P-256, non-extractable, persisted in IndexedDB
50
+ ```
51
+
52
+ ## React Native (Expo)
53
+
54
+ ```ts
55
+ // At your app entry — `crypto.getRandomValues` polyfill
56
+ import 'react-native-get-random-values';
57
+
58
+ import { PollarClient } from '@pollar/core';
59
+ import { createSecureStoreAdapter } from '@pollar/core/adapters/expo';
60
+
61
+ // `await`: SecureStore is loaded via dynamic import.
62
+ const storage = await createSecureStoreAdapter({
63
+ // Default: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY
64
+ // Prevents iCloud Keychain from carrying the key to another device.
65
+ });
66
+
67
+ const client = new PollarClient({ apiKey: 'your-api-key', storage });
68
+ // KeyManager autodetects → NobleKeyManager (pure-JS @noble/curves p256).
69
+ ```
70
+
71
+ ## React Native (`react-native-keychain`)
72
+
73
+ ```ts
74
+ import 'react-native-get-random-values';
75
+ import { PollarClient } from '@pollar/core';
76
+ import { createKeychainAdapter } from '@pollar/core/adapters/react-native-keychain';
77
+
78
+ const storage = await createKeychainAdapter();
79
+ const client = new PollarClient({ apiKey: 'your-api-key', storage });
80
+ ```
81
+
82
+ ## Preserved-on-disk storage shape
83
+
84
+ 0.7.0 persists exactly:
85
+
86
+ ```
87
+ clientSessionId, userId, status,
88
+ token { accessToken, refreshToken, expiresAt },
89
+ user { id?, ready },
90
+ wallet { publicKey, existsOnStellar?, createdAt? }
91
+ ```
92
+
93
+ PII (`mail`, `first_name`, `last_name`, `avatar`, `providers.*`) lives **in memory only** on the `PollarClient` instance
94
+ and is fetched after auth. Reach it via:
95
+
96
+ ```ts
97
+ const profile = client.getUserProfile();
98
+ // { mail, first_name, last_name, avatar, providers } | null
99
+ ```
100
+
101
+ Storage keys are namespaced by `apiKeyHash` (first 8 hex chars of SHA-256 of your API key) so multiple SDK instances on
102
+ the same origin don't cross-contaminate.
103
+
104
+ ## End-to-end example
25
105
 
26
106
  ```ts
27
107
  import { PollarClient } from '@pollar/core';
28
108
 
29
109
  const client = new PollarClient({ apiKey: 'your-api-key' });
30
110
 
31
- // Listen to state changes
32
- client.onStateChange((entry) => {
33
- console.log(entry.var, entry.code, entry.status);
111
+ // React to auth state
112
+ const unsubscribe = client.onAuthStateChange((state) => {
113
+ console.log(state.step, state.errorCode ?? '');
34
114
  });
35
115
 
36
- // Login with email
37
- const { cancelLogin } = client.login({ provider: 'email', email: 'user@example.com' });
116
+ // Wait until the keypair is ready and any persisted session has been restored
117
+ await client.ready();
118
+
119
+ // Start an email login
120
+ client.login({ provider: 'email', email: 'user@example.com' });
121
+
122
+ // Submit the OTP — clientSessionId is tracked internally
123
+ client.verifyEmailCode('123456');
38
124
 
39
- // Verify the OTP code sent to the user
40
- await client.verifyEmailCode(clientSessionId, '123456');
125
+ // After success
126
+ const profile = client.getUserProfile(); // PII (memory-only)
127
+ const sessions = await client.listSessions();
41
128
  ```
42
129
 
43
130
  ## API Reference
44
131
 
45
132
  ### `new PollarClient(config)`
46
133
 
47
- | Option | Type | Required | Description |
48
- | --------------- | -------- | -------- | -------------------------------------------------- |
49
- | `apiKey` | `string` | Yes | Your Pollar API key |
50
- | `baseUrl` | `string` | No | Override the default API endpoint |
51
- | `stellarNetwork`| `'mainnet' \| 'testnet'` | No | Target Stellar network (default: `testnet`) |
134
+ | Option | Type | Required | Description |
135
+ |--------------------|--------------------------|----------|----------------------------------------------------------------------------------------------------------|
136
+ | `apiKey` | `string` | Yes | Your Pollar API key |
137
+ | `baseUrl` | `string` | No | Override the default API endpoint |
138
+ | `stellarNetwork` | `'mainnet' \| 'testnet'` | No | Target Stellar network (default: `testnet`) |
139
+ | `storage` | `Storage` | No | Pluggable storage adapter. Web autodetects `localStorage` with in-memory fallback; RN must inject one |
140
+ | `keyManager` | `KeyManager` | No | Pluggable DPoP key manager. Web picks `WebCryptoKeyManager`; otherwise `NobleKeyManager` |
141
+ | `walletAdapter` | `WalletAdapterResolver` | No | External wallet stack (e.g. Stellar Wallets Kit). Falls back to built-in `FreighterAdapter`/`AlbedoAdapter` |
142
+ | `deviceLabel` | `string` | No | UI-friendly device label sent at `/auth/login` time and shown in `listSessions()` rows |
143
+ | `onStorageDegrade` | `OnStorageDegrade` | No | Notified the first time `localStorage` falls back to in-memory mode (SSR, private browsing, quota, …) |
52
144
 
53
145
  ---
54
146
 
@@ -72,18 +164,87 @@ client.login({ provider: 'wallet', type: WalletType.FREIGHTER });
72
164
  client.login({ provider: 'wallet', type: WalletType.ALBEDO });
73
165
  ```
74
166
 
75
- #### `client.verifyEmailCode(clientSessionId, code)`
167
+ #### `client.verifyEmailCode(code)`
168
+
169
+ Submits the OTP code for email authentication. The active `clientSessionId` is tracked internally — no need to pass it.
76
170
 
77
- Submits the OTP code for email authentication. Use `clientSessionId` received from the `EMAIL_AUTH_START_SUCCESS` state event.
171
+ #### `client.loginWallet(walletId)`
78
172
 
79
- #### `client.logout()`
173
+ Lower-level entry point for wallet flows. Accepts any `WalletId` (`WalletType.FREIGHTER`, `WalletType.ALBEDO`, or an
174
+ opaque string id like `'xbull'` / `'lobstr'` resolved by `walletAdapter`).
80
175
 
81
- Clears the current session from memory and storage.
176
+ #### `client.cancelLogin()`
177
+
178
+ Aborts any in-flight login flow and resets `authState` to `idle`. Safe to call from any step (including `error`).
179
+
180
+ #### `client.logout(options?): Promise<void>`
181
+
182
+ Server-side revokes the refresh-token family via `POST /v1/auth/logout`, then clears local storage and resets the
183
+ keypair. Server revocation is best-effort: a failed POST still clears local state.
184
+
185
+ ```ts
186
+ await client.logout(); // sign out this device
187
+ await client.logout({ everywhere: true }); // revoke every active session for this user
188
+ ```
189
+
190
+ > Returns `Promise<void>` (was `void` pre-0.7.0). Existing fire-and-forget call sites keep working, but `await` it if
191
+ > you want to observe server-side revocation.
192
+
193
+ #### `client.logoutEverywhere(): Promise<void>`
194
+
195
+ Shorthand for `logout({ everywhere: true })`.
82
196
 
83
197
  #### `client.isAuthenticated()`
84
198
 
85
199
  Returns `true` if a valid session with a wallet public key is present.
86
200
 
201
+ #### `client.getUserProfile(): PollarUserProfile | null`
202
+
203
+ Returns the in-memory profile (`mail`, `first_name`, `last_name`, `avatar`, `providers`). `null` until `/auth/login`
204
+ completes. **This is the only way to read PII as of 0.7.0** — PII is no longer persisted to storage.
205
+
206
+ #### `client.ready(): Promise<void>`
207
+
208
+ Resolves once the keypair is initialized and any persisted session has been restored. Useful in tests and
209
+ server-side rendering.
210
+
211
+ #### `client.destroy(): void`
212
+
213
+ Detaches the cross-tab `storage` listener, aborts in-flight logins, and releases the keypair. Call this on unmount in
214
+ environments that re-instantiate `PollarClient`.
215
+
216
+ #### `client.refresh(): Promise<void>`
217
+
218
+ Forces an access-token refresh. Race-safe: concurrent calls coalesce into a single `/v1/auth/refresh` request.
219
+ Request middleware also calls this automatically on 401 with `DPoP-Nonce` rotation.
220
+
221
+ ---
222
+
223
+ ### Sessions
224
+
225
+ #### `client.listSessions(): Promise<SessionInfo[]>`
226
+
227
+ Returns one row per active refresh-token family for the authenticated user:
228
+
229
+ ```ts
230
+ interface SessionInfo {
231
+ familyId: string;
232
+ createdAt: string;
233
+ lastUsedAt: string;
234
+ userAgent: string;
235
+ ipHash: string;
236
+ deviceLabel?: string;
237
+ expiresAt: string;
238
+ current: boolean; // true for the family backing this client
239
+ }
240
+ ```
241
+
242
+ #### `client.revokeSession(familyId): Promise<void>`
243
+
244
+ Revokes a specific refresh-token family. Revoking the **current** family does not immediately clear local state — the
245
+ next 401 triggers an auto-refresh, which fails (family revoked) and clears the session. Call `logout()` for an
246
+ immediate teardown.
247
+
87
248
  ---
88
249
 
89
250
  ### Transactions
@@ -112,32 +273,36 @@ await client.submitTx(signedXdr);
112
273
 
113
274
  ### State
114
275
 
115
- #### `client.onStateChange(callback)`
116
-
117
- Subscribe to state changes. Returns an unsubscribe function.
276
+ Each state domain has its own typed subscriber. All `on*StateChange` methods return an unsubscribe function.
118
277
 
119
278
  ```ts
120
- const unsubscribe = client.onStateChange((entry) => {
121
- // entry.var — 'authentication' | 'transaction' | 'network'
122
- // entry.code granular event code (see STATE_VAR_CODES)
123
- // entry.status'NONE' | 'LOADING' | 'SUCCESS' | 'ERROR'
124
- // entry.data — optional payload
125
- // entry.level — 'info' | 'warn' | 'error'
126
- // entry.ts — timestamp (ms)
279
+ const unsubAuth = client.onAuthStateChange((state) => {
280
+ // state.step — 'idle' | 'oauth' | 'email' | 'wallet' | 'success' | 'error'
281
+ // state.session PollarPersistedSession (when step === 'success')
282
+ // state.errorCodeAuthErrorCode (when step === 'error')
127
283
  });
128
284
 
129
- // Unsubscribe
130
- unsubscribe();
285
+ const unsubTx = client.onTransactionStateChange((s) => { /* build → sign → submit */ });
286
+ const unsubHistory = client.onTxHistoryStateChange((s) => { /* paginated rows */ });
287
+ const unsubBalance = client.onWalletBalanceStateChange((s) => { /* balances */ });
288
+ const unsubNetwork = client.onNetworkStateChange((s) => { /* mainnet / testnet */ });
289
+
290
+ unsubAuth();
131
291
  ```
132
292
 
133
- All state codes are available via `STATE_VAR_CODES`:
293
+ Snapshot getters are also available: `getAuthState()`, `getTransactionState()`, `getTxHistoryState()`,
294
+ `getWalletBalanceState()`, `getNetworkState()`.
295
+
296
+ Error codes for the auth flow are surfaced via `AUTH_ERROR_CODES` / `AuthErrorCode`:
134
297
 
135
298
  ```ts
136
- import { STATE_VAR_CODES } from '@pollar/core';
299
+ import { AUTH_ERROR_CODES, type AuthErrorCode } from '@pollar/core';
137
300
 
138
- // STATE_VAR_CODES.authentication.EMAIL_AUTH_START_SUCCESS
139
- // STATE_VAR_CODES.transaction.BUILD_TRANSACTION_SUCCESS
140
- // STATE_VAR_CODES.network.NETWORK_UPDATED
301
+ // AUTH_ERROR_CODES.EMAIL_CODE_INVALID
302
+ // AUTH_ERROR_CODES.EMAIL_CODE_EXPIRED
303
+ // AUTH_ERROR_CODES.SESSION_CREATE_FAILED
304
+ // AUTH_ERROR_CODES.WALLET_CONNECT_FAILED
305
+ // …see types.ts for the full list
141
306
  ```
142
307
 
143
308
  ---
@@ -178,6 +343,50 @@ if (available) {
178
343
  }
179
344
  ```
180
345
 
346
+ To plug in external wallet stacks (e.g. Stellar Wallets Kit) without `@pollar/core` having to depend on them, pass a
347
+ `WalletAdapterResolver` to the client:
348
+
349
+ ```ts
350
+ import { PollarClient, WalletType } from '@pollar/core';
351
+ import { stellarWalletsKit } from '@pollar/stellar-wallets-kit-adapter';
352
+ import { Networks } from '@creit.tech/stellar-wallets-kit';
353
+
354
+ const client = new PollarClient({
355
+ apiKey: 'pk_...',
356
+ walletAdapter: stellarWalletsKit({ network: Networks.PUBLIC }),
357
+ });
358
+
359
+ client.loginWallet('xbull'); // any string id the kit understands
360
+ ```
361
+
362
+ The resolver signature is:
363
+
364
+ ```ts
365
+ type WalletAdapterResolver = (id: WalletId) => WalletAdapter | Promise<WalletAdapter>;
366
+ type WalletId = WalletType | (string & {});
367
+ ```
368
+
369
+ ---
370
+
371
+ ### Custom adapters (`AdapterFn` / `PollarAdapter`)
372
+
373
+ Generic adapter contract for wrapping external signing flows (e.g. Trustless Work SDK). Adapter functions receive
374
+ params and return an unsigned XDR; the client handles signing and submission.
375
+
376
+ ```ts
377
+ import type { AdapterFn, PollarAdapter, PollarAdapters } from '@pollar/core';
378
+
379
+ const trustlessWork: PollarAdapter = {
380
+ initialize: (async (params) => ({ unsignedTransaction: '...' })) satisfies AdapterFn,
381
+ release: (async (params) => ({ unsignedTransaction: '...' })) satisfies AdapterFn,
382
+ };
383
+
384
+ const adapters: PollarAdapters = { trustlessWork };
385
+ ```
386
+
387
+ > **Renamed in 0.7.0** — `EscrowFn` → `AdapterFn` and `EscrowAdapter` → `PollarAdapter`. Runtime contract is unchanged;
388
+ > rename your imports.
389
+
181
390
  ## TypeScript
182
391
 
183
392
  `@pollar/core` is written in TypeScript and ships full type declarations.
@@ -186,20 +395,45 @@ Key exported types:
186
395
 
187
396
  ```ts
188
397
  import type {
398
+ // Client
189
399
  PollarClientConfig,
190
400
  PollarLoginOptions,
191
- PollarState,
192
- PollarStateEntry,
193
- StateAuthenticationCodes,
194
- StateTransactionCodes,
195
- StateNetworkCodes,
196
- StateVarCodes,
401
+ PollarPersistedSession,
402
+ PollarUserProfile,
403
+ AuthState,
404
+ AuthErrorCode,
405
+
406
+ // Storage / keys / DPoP
407
+ Storage,
408
+ OnStorageDegrade,
409
+ StorageDegradeReason,
410
+ KeyManager,
411
+ PublicEcJwk,
412
+ BuildProofArgs,
413
+
414
+ // Sessions
415
+ SessionInfo,
416
+
417
+ // Wallets
418
+ WalletType,
419
+ WalletId,
420
+ WalletAdapter,
421
+ WalletAdapterResolver,
422
+
423
+ // Adapters (renamed from Escrow*)
424
+ AdapterFn,
425
+ PollarAdapter,
426
+ PollarAdapters,
427
+
428
+ // Stellar
197
429
  StellarNetwork,
430
+ StellarClientConfig,
198
431
  StellarBalance,
199
- GetBalancesResult,
200
432
  } from '@pollar/core';
433
+
434
+ import { AUTH_ERROR_CODES } from '@pollar/core';
201
435
  ```
202
436
 
203
437
  ## License
204
438
 
205
- MIT
439
+ MIT
@@ -0,0 +1,26 @@
1
+ import { S as Storage } from '../types-DqgJIJBl.mjs';
2
+
3
+ /**
4
+ * Hard cap per stored value. Generously above what the SDK actually writes
5
+ * (sessions ≈ 600–800 bytes, private scalars ≈ 43 chars), and well within
6
+ * iOS Keychain's practical limit. Refuses oversized writes loudly rather
7
+ * than letting the platform truncate or silently fail.
8
+ */
9
+ declare const SECURE_STORE_MAX_VALUE_BYTES = 4096;
10
+ interface SecureStoreAdapterOptions {
11
+ /**
12
+ * Override the iOS Keychain accessibility class. Defaults to
13
+ * `WHEN_UNLOCKED_THIS_DEVICE_ONLY` when available on the loaded module.
14
+ * On Android this is a no-op (the platform manages access via Keystore).
15
+ */
16
+ keychainAccessible?: number;
17
+ }
18
+ /**
19
+ * Create a `Storage` adapter backed by Expo SecureStore.
20
+ *
21
+ * Throws synchronously (via the returned Promise) at construction time if
22
+ * `expo-secure-store` cannot be loaded.
23
+ */
24
+ declare function createSecureStoreAdapter(options?: SecureStoreAdapterOptions): Promise<Storage>;
25
+
26
+ export { SECURE_STORE_MAX_VALUE_BYTES, type SecureStoreAdapterOptions, createSecureStoreAdapter };
@@ -0,0 +1,26 @@
1
+ import { S as Storage } from '../types-DqgJIJBl.js';
2
+
3
+ /**
4
+ * Hard cap per stored value. Generously above what the SDK actually writes
5
+ * (sessions ≈ 600–800 bytes, private scalars ≈ 43 chars), and well within
6
+ * iOS Keychain's practical limit. Refuses oversized writes loudly rather
7
+ * than letting the platform truncate or silently fail.
8
+ */
9
+ declare const SECURE_STORE_MAX_VALUE_BYTES = 4096;
10
+ interface SecureStoreAdapterOptions {
11
+ /**
12
+ * Override the iOS Keychain accessibility class. Defaults to
13
+ * `WHEN_UNLOCKED_THIS_DEVICE_ONLY` when available on the loaded module.
14
+ * On Android this is a no-op (the platform manages access via Keystore).
15
+ */
16
+ keychainAccessible?: number;
17
+ }
18
+ /**
19
+ * Create a `Storage` adapter backed by Expo SecureStore.
20
+ *
21
+ * Throws synchronously (via the returned Promise) at construction time if
22
+ * `expo-secure-store` cannot be loaded.
23
+ */
24
+ declare function createSecureStoreAdapter(options?: SecureStoreAdapterOptions): Promise<Storage>;
25
+
26
+ export { SECURE_STORE_MAX_VALUE_BYTES, type SecureStoreAdapterOptions, createSecureStoreAdapter };
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ // src/adapters/expo-secure-store.ts
4
+ var SECURE_STORE_MAX_VALUE_BYTES = 4096;
5
+ async function loadSecureStore() {
6
+ try {
7
+ const mod = await import('expo-secure-store');
8
+ return mod;
9
+ } catch (error) {
10
+ const message = `[PollarClient:storage] Failed to load 'expo-secure-store'. Install it in your Expo app: \`npx expo install expo-secure-store\`. Original error: ${error instanceof Error ? error.message : String(error)}`;
11
+ throw new Error(message);
12
+ }
13
+ }
14
+ function utf8ByteLength(value) {
15
+ if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(value).length;
16
+ let bytes = 0;
17
+ for (let i = 0; i < value.length; i++) {
18
+ const code = value.charCodeAt(i);
19
+ if (code < 128) bytes += 1;
20
+ else if (code < 2048) bytes += 2;
21
+ else if (code >= 55296 && code <= 56319) {
22
+ bytes += 4;
23
+ i++;
24
+ } else bytes += 3;
25
+ }
26
+ return bytes;
27
+ }
28
+ async function createSecureStoreAdapter(options = {}) {
29
+ const SecureStore = await loadSecureStore();
30
+ const accessible = options.keychainAccessible !== void 0 ? options.keychainAccessible : SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY;
31
+ return {
32
+ async get(key) {
33
+ return SecureStore.getItemAsync(key);
34
+ },
35
+ async set(key, value) {
36
+ const size = utf8ByteLength(value);
37
+ if (size > SECURE_STORE_MAX_VALUE_BYTES) {
38
+ throw new Error(
39
+ `[PollarClient:storage] Value for "${key}" is ${size} bytes, exceeds SecureStore limit ${SECURE_STORE_MAX_VALUE_BYTES}`
40
+ );
41
+ }
42
+ await SecureStore.setItemAsync(key, value, accessible !== void 0 ? { keychainAccessible: accessible } : void 0);
43
+ },
44
+ async remove(key) {
45
+ await SecureStore.deleteItemAsync(key);
46
+ }
47
+ };
48
+ }
49
+
50
+ exports.SECURE_STORE_MAX_VALUE_BYTES = SECURE_STORE_MAX_VALUE_BYTES;
51
+ exports.createSecureStoreAdapter = createSecureStoreAdapter;
52
+ //# sourceMappingURL=expo-secure-store.js.map
53
+ //# sourceMappingURL=expo-secure-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/expo-secure-store.ts"],"names":[],"mappings":";;;AAqCO,IAAM,4BAAA,GAA+B;AAW5C,eAAe,eAAA,GAA2C;AACxD,EAAA,IAAI;AAGF,IAAA,MAAM,GAAA,GAAM,MAAM,OAAO,mBAAmB,CAAA;AAC5C,IAAA,OAAO,GAAA;AAAA,EACT,SAAS,KAAA,EAAO;AACd,IAAA,MAAM,OAAA,GACJ,mJAEmB,KAAA,YAAiB,KAAA,GAAQ,MAAM,OAAA,GAAU,MAAA,CAAO,KAAK,CAAC,CAAA,CAAA;AAC3E,IAAA,MAAM,IAAI,MAAM,OAAO,CAAA;AAAA,EACzB;AACF;AAEA,SAAS,eAAe,KAAA,EAAuB;AAC7C,EAAA,IAAI,OAAO,gBAAgB,WAAA,EAAa,OAAO,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,KAAK,CAAA,CAAE,MAAA;AAE/E,EAAA,IAAI,KAAA,GAAQ,CAAA;AACZ,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,EAAA,EAAK;AACrC,IAAA,MAAM,IAAA,GAAO,KAAA,CAAM,UAAA,CAAW,CAAC,CAAA;AAC/B,IAAA,IAAI,IAAA,GAAO,KAAM,KAAA,IAAS,CAAA;AAAA,SAAA,IACjB,IAAA,GAAO,MAAO,KAAA,IAAS,CAAA;AAAA,SAAA,IACvB,IAAA,IAAQ,KAAA,IAAU,IAAA,IAAQ,KAAA,EAAQ;AAEzC,MAAA,KAAA,IAAS,CAAA;AACT,MAAA,CAAA,EAAA;AAAA,IACF,OAAO,KAAA,IAAS,CAAA;AAAA,EAClB;AACA,EAAA,OAAO,KAAA;AACT;AAQA,eAAsB,wBAAA,CAAyB,OAAA,GAAqC,EAAC,EAAqB;AACxG,EAAA,MAAM,WAAA,GAAc,MAAM,eAAA,EAAgB;AAE1C,EAAA,MAAM,aACJ,OAAA,CAAQ,kBAAA,KAAuB,MAAA,GAAY,OAAA,CAAQ,qBAAqB,WAAA,CAAY,8BAAA;AAEtF,EAAA,OAAO;AAAA,IACL,MAAM,IAAI,GAAA,EAAK;AACb,MAAA,OAAO,WAAA,CAAY,aAAa,GAAG,CAAA;AAAA,IACrC,CAAA;AAAA,IACA,MAAM,GAAA,CAAI,GAAA,EAAK,KAAA,EAAO;AACpB,MAAA,MAAM,IAAA,GAAO,eAAe,KAAK,CAAA;AACjC,MAAA,IAAI,OAAO,4BAAA,EAA8B;AACvC,QAAA,MAAM,IAAI,KAAA;AAAA,UACR,CAAA,kCAAA,EAAqC,GAAG,CAAA,KAAA,EAAQ,IAAI,qCAAqC,4BAA4B,CAAA;AAAA,SACvH;AAAA,MACF;AACA,MAAA,MAAM,WAAA,CAAY,YAAA,CAAa,GAAA,EAAK,KAAA,EAAO,UAAA,KAAe,SAAY,EAAE,kBAAA,EAAoB,UAAA,EAAW,GAAI,MAAS,CAAA;AAAA,IACtH,CAAA;AAAA,IACA,MAAM,OAAO,GAAA,EAAK;AAChB,MAAA,MAAM,WAAA,CAAY,gBAAgB,GAAG,CAAA;AAAA,IACvC;AAAA,GACF;AACF","file":"expo-secure-store.js","sourcesContent":["import type { Storage } from '../storage/types';\n\n/**\n * Adapter that persists session and key material in the iOS Keychain / Android\n * Keystore via [`expo-secure-store`](https://docs.expo.dev/versions/latest/sdk/securestore/).\n *\n * `expo-secure-store` is an optional peer dependency; install it in your Expo\n * project with `npx expo install expo-secure-store`.\n *\n * The module is loaded lazily via dynamic `import('expo-secure-store')` so web\n * bundlers strip the dependency from web builds entirely.\n */\n\n/**\n * Minimal structural type for the parts of `expo-secure-store` we use. We\n * type the surface here instead of importing the package's types because the\n * package is an optional peer dependency and may not be installed when this\n * SDK is type-checked (e.g. web-only consumers).\n */\ntype SecureStoreApi = {\n getItemAsync: (key: string) => Promise<string | null>;\n setItemAsync: (key: string, value: string, options?: { keychainAccessible?: number }) => Promise<void>;\n deleteItemAsync: (key: string) => Promise<void>;\n /**\n * Default we use: requires the device to be unlocked and disables iCloud\n * Keychain backup of the value (so a stolen iCloud backup cannot exfiltrate\n * the SDK's private key material to another device).\n */\n WHEN_UNLOCKED_THIS_DEVICE_ONLY?: number;\n};\n\n/**\n * Hard cap per stored value. Generously above what the SDK actually writes\n * (sessions ≈ 600–800 bytes, private scalars ≈ 43 chars), and well within\n * iOS Keychain's practical limit. Refuses oversized writes loudly rather\n * than letting the platform truncate or silently fail.\n */\nexport const SECURE_STORE_MAX_VALUE_BYTES = 4096;\n\nexport interface SecureStoreAdapterOptions {\n /**\n * Override the iOS Keychain accessibility class. Defaults to\n * `WHEN_UNLOCKED_THIS_DEVICE_ONLY` when available on the loaded module.\n * On Android this is a no-op (the platform manages access via Keystore).\n */\n keychainAccessible?: number;\n}\n\nasync function loadSecureStore(): Promise<SecureStoreApi> {\n try {\n // @ts-ignore -- optional peer dep; not present when the SDK is built or\n // when the SDK runs on web. Resolved at runtime in Expo / RN apps.\n const mod = await import('expo-secure-store');\n return mod as unknown as SecureStoreApi;\n } catch (error) {\n const message =\n `[PollarClient:storage] Failed to load 'expo-secure-store'. ` +\n `Install it in your Expo app: \\`npx expo install expo-secure-store\\`. ` +\n `Original error: ${error instanceof Error ? error.message : String(error)}`;\n throw new Error(message);\n }\n}\n\nfunction utf8ByteLength(value: string): number {\n if (typeof TextEncoder !== 'undefined') return new TextEncoder().encode(value).length;\n // Fallback: count UTF-8 bytes manually for environments without TextEncoder.\n let bytes = 0;\n for (let i = 0; i < value.length; i++) {\n const code = value.charCodeAt(i);\n if (code < 0x80) bytes += 1;\n else if (code < 0x800) bytes += 2;\n else if (code >= 0xd800 && code <= 0xdbff) {\n // Surrogate pair → 4 bytes; advance the index.\n bytes += 4;\n i++;\n } else bytes += 3;\n }\n return bytes;\n}\n\n/**\n * Create a `Storage` adapter backed by Expo SecureStore.\n *\n * Throws synchronously (via the returned Promise) at construction time if\n * `expo-secure-store` cannot be loaded.\n */\nexport async function createSecureStoreAdapter(options: SecureStoreAdapterOptions = {}): Promise<Storage> {\n const SecureStore = await loadSecureStore();\n\n const accessible =\n options.keychainAccessible !== undefined ? options.keychainAccessible : SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY;\n\n return {\n async get(key) {\n return SecureStore.getItemAsync(key);\n },\n async set(key, value) {\n const size = utf8ByteLength(value);\n if (size > SECURE_STORE_MAX_VALUE_BYTES) {\n throw new Error(\n `[PollarClient:storage] Value for \"${key}\" is ${size} bytes, exceeds SecureStore limit ${SECURE_STORE_MAX_VALUE_BYTES}`,\n );\n }\n await SecureStore.setItemAsync(key, value, accessible !== undefined ? { keychainAccessible: accessible } : undefined);\n },\n async remove(key) {\n await SecureStore.deleteItemAsync(key);\n },\n };\n}\n"]}
@@ -0,0 +1,50 @@
1
+ // src/adapters/expo-secure-store.ts
2
+ var SECURE_STORE_MAX_VALUE_BYTES = 4096;
3
+ async function loadSecureStore() {
4
+ try {
5
+ const mod = await import('expo-secure-store');
6
+ return mod;
7
+ } catch (error) {
8
+ const message = `[PollarClient:storage] Failed to load 'expo-secure-store'. Install it in your Expo app: \`npx expo install expo-secure-store\`. Original error: ${error instanceof Error ? error.message : String(error)}`;
9
+ throw new Error(message);
10
+ }
11
+ }
12
+ function utf8ByteLength(value) {
13
+ if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(value).length;
14
+ let bytes = 0;
15
+ for (let i = 0; i < value.length; i++) {
16
+ const code = value.charCodeAt(i);
17
+ if (code < 128) bytes += 1;
18
+ else if (code < 2048) bytes += 2;
19
+ else if (code >= 55296 && code <= 56319) {
20
+ bytes += 4;
21
+ i++;
22
+ } else bytes += 3;
23
+ }
24
+ return bytes;
25
+ }
26
+ async function createSecureStoreAdapter(options = {}) {
27
+ const SecureStore = await loadSecureStore();
28
+ const accessible = options.keychainAccessible !== void 0 ? options.keychainAccessible : SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY;
29
+ return {
30
+ async get(key) {
31
+ return SecureStore.getItemAsync(key);
32
+ },
33
+ async set(key, value) {
34
+ const size = utf8ByteLength(value);
35
+ if (size > SECURE_STORE_MAX_VALUE_BYTES) {
36
+ throw new Error(
37
+ `[PollarClient:storage] Value for "${key}" is ${size} bytes, exceeds SecureStore limit ${SECURE_STORE_MAX_VALUE_BYTES}`
38
+ );
39
+ }
40
+ await SecureStore.setItemAsync(key, value, accessible !== void 0 ? { keychainAccessible: accessible } : void 0);
41
+ },
42
+ async remove(key) {
43
+ await SecureStore.deleteItemAsync(key);
44
+ }
45
+ };
46
+ }
47
+
48
+ export { SECURE_STORE_MAX_VALUE_BYTES, createSecureStoreAdapter };
49
+ //# sourceMappingURL=expo-secure-store.mjs.map
50
+ //# sourceMappingURL=expo-secure-store.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/expo-secure-store.ts"],"names":[],"mappings":";AAqCO,IAAM,4BAAA,GAA+B;AAW5C,eAAe,eAAA,GAA2C;AACxD,EAAA,IAAI;AAGF,IAAA,MAAM,GAAA,GAAM,MAAM,OAAO,mBAAmB,CAAA;AAC5C,IAAA,OAAO,GAAA;AAAA,EACT,SAAS,KAAA,EAAO;AACd,IAAA,MAAM,OAAA,GACJ,mJAEmB,KAAA,YAAiB,KAAA,GAAQ,MAAM,OAAA,GAAU,MAAA,CAAO,KAAK,CAAC,CAAA,CAAA;AAC3E,IAAA,MAAM,IAAI,MAAM,OAAO,CAAA;AAAA,EACzB;AACF;AAEA,SAAS,eAAe,KAAA,EAAuB;AAC7C,EAAA,IAAI,OAAO,gBAAgB,WAAA,EAAa,OAAO,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,KAAK,CAAA,CAAE,MAAA;AAE/E,EAAA,IAAI,KAAA,GAAQ,CAAA;AACZ,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,EAAA,EAAK;AACrC,IAAA,MAAM,IAAA,GAAO,KAAA,CAAM,UAAA,CAAW,CAAC,CAAA;AAC/B,IAAA,IAAI,IAAA,GAAO,KAAM,KAAA,IAAS,CAAA;AAAA,SAAA,IACjB,IAAA,GAAO,MAAO,KAAA,IAAS,CAAA;AAAA,SAAA,IACvB,IAAA,IAAQ,KAAA,IAAU,IAAA,IAAQ,KAAA,EAAQ;AAEzC,MAAA,KAAA,IAAS,CAAA;AACT,MAAA,CAAA,EAAA;AAAA,IACF,OAAO,KAAA,IAAS,CAAA;AAAA,EAClB;AACA,EAAA,OAAO,KAAA;AACT;AAQA,eAAsB,wBAAA,CAAyB,OAAA,GAAqC,EAAC,EAAqB;AACxG,EAAA,MAAM,WAAA,GAAc,MAAM,eAAA,EAAgB;AAE1C,EAAA,MAAM,aACJ,OAAA,CAAQ,kBAAA,KAAuB,MAAA,GAAY,OAAA,CAAQ,qBAAqB,WAAA,CAAY,8BAAA;AAEtF,EAAA,OAAO;AAAA,IACL,MAAM,IAAI,GAAA,EAAK;AACb,MAAA,OAAO,WAAA,CAAY,aAAa,GAAG,CAAA;AAAA,IACrC,CAAA;AAAA,IACA,MAAM,GAAA,CAAI,GAAA,EAAK,KAAA,EAAO;AACpB,MAAA,MAAM,IAAA,GAAO,eAAe,KAAK,CAAA;AACjC,MAAA,IAAI,OAAO,4BAAA,EAA8B;AACvC,QAAA,MAAM,IAAI,KAAA;AAAA,UACR,CAAA,kCAAA,EAAqC,GAAG,CAAA,KAAA,EAAQ,IAAI,qCAAqC,4BAA4B,CAAA;AAAA,SACvH;AAAA,MACF;AACA,MAAA,MAAM,WAAA,CAAY,YAAA,CAAa,GAAA,EAAK,KAAA,EAAO,UAAA,KAAe,SAAY,EAAE,kBAAA,EAAoB,UAAA,EAAW,GAAI,MAAS,CAAA;AAAA,IACtH,CAAA;AAAA,IACA,MAAM,OAAO,GAAA,EAAK;AAChB,MAAA,MAAM,WAAA,CAAY,gBAAgB,GAAG,CAAA;AAAA,IACvC;AAAA,GACF;AACF","file":"expo-secure-store.mjs","sourcesContent":["import type { Storage } from '../storage/types';\n\n/**\n * Adapter that persists session and key material in the iOS Keychain / Android\n * Keystore via [`expo-secure-store`](https://docs.expo.dev/versions/latest/sdk/securestore/).\n *\n * `expo-secure-store` is an optional peer dependency; install it in your Expo\n * project with `npx expo install expo-secure-store`.\n *\n * The module is loaded lazily via dynamic `import('expo-secure-store')` so web\n * bundlers strip the dependency from web builds entirely.\n */\n\n/**\n * Minimal structural type for the parts of `expo-secure-store` we use. We\n * type the surface here instead of importing the package's types because the\n * package is an optional peer dependency and may not be installed when this\n * SDK is type-checked (e.g. web-only consumers).\n */\ntype SecureStoreApi = {\n getItemAsync: (key: string) => Promise<string | null>;\n setItemAsync: (key: string, value: string, options?: { keychainAccessible?: number }) => Promise<void>;\n deleteItemAsync: (key: string) => Promise<void>;\n /**\n * Default we use: requires the device to be unlocked and disables iCloud\n * Keychain backup of the value (so a stolen iCloud backup cannot exfiltrate\n * the SDK's private key material to another device).\n */\n WHEN_UNLOCKED_THIS_DEVICE_ONLY?: number;\n};\n\n/**\n * Hard cap per stored value. Generously above what the SDK actually writes\n * (sessions ≈ 600–800 bytes, private scalars ≈ 43 chars), and well within\n * iOS Keychain's practical limit. Refuses oversized writes loudly rather\n * than letting the platform truncate or silently fail.\n */\nexport const SECURE_STORE_MAX_VALUE_BYTES = 4096;\n\nexport interface SecureStoreAdapterOptions {\n /**\n * Override the iOS Keychain accessibility class. Defaults to\n * `WHEN_UNLOCKED_THIS_DEVICE_ONLY` when available on the loaded module.\n * On Android this is a no-op (the platform manages access via Keystore).\n */\n keychainAccessible?: number;\n}\n\nasync function loadSecureStore(): Promise<SecureStoreApi> {\n try {\n // @ts-ignore -- optional peer dep; not present when the SDK is built or\n // when the SDK runs on web. Resolved at runtime in Expo / RN apps.\n const mod = await import('expo-secure-store');\n return mod as unknown as SecureStoreApi;\n } catch (error) {\n const message =\n `[PollarClient:storage] Failed to load 'expo-secure-store'. ` +\n `Install it in your Expo app: \\`npx expo install expo-secure-store\\`. ` +\n `Original error: ${error instanceof Error ? error.message : String(error)}`;\n throw new Error(message);\n }\n}\n\nfunction utf8ByteLength(value: string): number {\n if (typeof TextEncoder !== 'undefined') return new TextEncoder().encode(value).length;\n // Fallback: count UTF-8 bytes manually for environments without TextEncoder.\n let bytes = 0;\n for (let i = 0; i < value.length; i++) {\n const code = value.charCodeAt(i);\n if (code < 0x80) bytes += 1;\n else if (code < 0x800) bytes += 2;\n else if (code >= 0xd800 && code <= 0xdbff) {\n // Surrogate pair → 4 bytes; advance the index.\n bytes += 4;\n i++;\n } else bytes += 3;\n }\n return bytes;\n}\n\n/**\n * Create a `Storage` adapter backed by Expo SecureStore.\n *\n * Throws synchronously (via the returned Promise) at construction time if\n * `expo-secure-store` cannot be loaded.\n */\nexport async function createSecureStoreAdapter(options: SecureStoreAdapterOptions = {}): Promise<Storage> {\n const SecureStore = await loadSecureStore();\n\n const accessible =\n options.keychainAccessible !== undefined ? options.keychainAccessible : SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY;\n\n return {\n async get(key) {\n return SecureStore.getItemAsync(key);\n },\n async set(key, value) {\n const size = utf8ByteLength(value);\n if (size > SECURE_STORE_MAX_VALUE_BYTES) {\n throw new Error(\n `[PollarClient:storage] Value for \"${key}\" is ${size} bytes, exceeds SecureStore limit ${SECURE_STORE_MAX_VALUE_BYTES}`,\n );\n }\n await SecureStore.setItemAsync(key, value, accessible !== undefined ? { keychainAccessible: accessible } : undefined);\n },\n async remove(key) {\n await SecureStore.deleteItemAsync(key);\n },\n };\n}\n"]}
@@ -0,0 +1,25 @@
1
+ import { S as Storage } from '../types-DqgJIJBl.mjs';
2
+
3
+ /**
4
+ * Hard cap per stored value. iOS Keychain has no formal byte limit but
5
+ * practical limits sit a few KB; we refuse oversized writes loudly.
6
+ */
7
+ declare const KEYCHAIN_MAX_VALUE_BYTES = 4096;
8
+ interface KeychainAdapterOptions {
9
+ /**
10
+ * Override the iOS Keychain accessibility class. Defaults to
11
+ * `ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY` when available on the loaded
12
+ * module — that prevents iCloud Keychain backup from carrying the SDK's
13
+ * private key material to another device.
14
+ */
15
+ accessible?: string;
16
+ }
17
+ /**
18
+ * Create a `Storage` adapter backed by `react-native-keychain`.
19
+ *
20
+ * Throws (via the returned Promise) at construction time if the package
21
+ * cannot be loaded.
22
+ */
23
+ declare function createKeychainAdapter(options?: KeychainAdapterOptions): Promise<Storage>;
24
+
25
+ export { KEYCHAIN_MAX_VALUE_BYTES, type KeychainAdapterOptions, createKeychainAdapter };
@@ -0,0 +1,25 @@
1
+ import { S as Storage } from '../types-DqgJIJBl.js';
2
+
3
+ /**
4
+ * Hard cap per stored value. iOS Keychain has no formal byte limit but
5
+ * practical limits sit a few KB; we refuse oversized writes loudly.
6
+ */
7
+ declare const KEYCHAIN_MAX_VALUE_BYTES = 4096;
8
+ interface KeychainAdapterOptions {
9
+ /**
10
+ * Override the iOS Keychain accessibility class. Defaults to
11
+ * `ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY` when available on the loaded
12
+ * module — that prevents iCloud Keychain backup from carrying the SDK's
13
+ * private key material to another device.
14
+ */
15
+ accessible?: string;
16
+ }
17
+ /**
18
+ * Create a `Storage` adapter backed by `react-native-keychain`.
19
+ *
20
+ * Throws (via the returned Promise) at construction time if the package
21
+ * cannot be loaded.
22
+ */
23
+ declare function createKeychainAdapter(options?: KeychainAdapterOptions): Promise<Storage>;
24
+
25
+ export { KEYCHAIN_MAX_VALUE_BYTES, type KeychainAdapterOptions, createKeychainAdapter };