@pollar/core 0.8.0 → 0.8.2

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
@@ -27,11 +27,46 @@ yarn add @pollar/core
27
27
  For React Native / Expo, also install one of the storage adapter peer deps:
28
28
 
29
29
  ```bash
30
- # Expo
30
+ # Expo (works in Expo Go — no native module required)
31
31
  npx expo install expo-secure-store react-native-get-random-values
32
+ npm i react-native-polyfill-globals
32
33
 
33
34
  # Bare React Native
34
- npm i react-native-keychain react-native-get-random-values
35
+ npm i react-native-keychain react-native-get-random-values react-native-polyfill-globals
36
+ ```
37
+
38
+ > **`react-native-quick-crypto` is optional.** SHA-256 and the ECDSA P-256 keypair now run on pure-JS
39
+ > [`@noble/hashes`](https://github.com/paulmillr/noble-hashes) / [`@noble/curves`](https://github.com/paulmillr/noble-curves)
40
+ > (`NobleKeyManager`), so the SDK no longer needs `crypto.subtle` on React Native — it runs in Expo Go. Installing
41
+ > `react-native-quick-crypto` (a native module → Expo **dev build**, not Expo Go) is only a security upgrade: when
42
+ > `crypto.subtle` is present the SDK uses `WebCryptoKeyManager`, whose private key is **non-extractable**. With Noble
43
+ > the private scalar is held in JS and persisted through the storage adapter. Both produce valid DPoP proofs.
44
+
45
+ > **React Native runtime requirements.** The SDK builds a DPoP proof (RFC 9449) for **every** authenticated request.
46
+ > That path uses three standard Web primitives that React Native / Hermes does **not** all ship by default. Register
47
+ > them at the very top of your entry file, **before** any `@pollar/core` import. If any is missing, DPoP proof
48
+ > construction fails and **no authenticated request works** — the SDK is not at fault, the runtime is.
49
+ >
50
+ > | Primitive | Used by | Polyfill |
51
+ > | ----------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
52
+ > | `crypto.getRandomValues` | keypair generation (`NobleKeyManager`), DPoP `jti` | [`react-native-get-random-values`](https://github.com/LinusU/react-native-get-random-values) |
53
+ > | `TextEncoder` / `TextDecoder` | DPoP proof encoding, base64url, JWK thumbprint | bundled in [`react-native-polyfill-globals`](https://github.com/acostalima/react-native-polyfill-globals) (or `text-encoding`) |
54
+ > | `URL` (spec-compliant) | DPoP `htu` normalization (`new URL(request.url)` on every proof) | bundled in `react-native-polyfill-globals` (or [`react-native-url-polyfill`](https://github.com/charpeni/react-native-url-polyfill)) |
55
+ >
56
+ > SHA-256 no longer needs `crypto.subtle.digest` — it runs on `@noble/hashes`, so no `react-native-quick-crypto` is
57
+ > required (see the note above).
58
+ >
59
+ > `react-native-polyfill-globals/auto` is the pragmatic one-liner — it installs `TextEncoder`/`TextDecoder` **and**
60
+ > `URL` together (plus base64 / fetch-streaming you don't strictly need). The SDK does **not** rely on `fetch` response
61
+ > streaming on React Native: it polls the non-streaming `/auth/session/status/{id}/poll` endpoint instead, so you do
62
+ > **not** need a fetch-streaming polyfill for auth — but you still need TextEncoder + URL from that same package.
63
+
64
+ Entry-file setup (e.g. `index.js`), **before importing `@pollar/core`**:
65
+
66
+ ```ts
67
+ import 'react-native-get-random-values'; // crypto.getRandomValues
68
+ import 'react-native-polyfill-globals/auto'; // TextEncoder/TextDecoder + URL
69
+ // Optional: import 'react-native-quick-crypto' to upgrade to non-extractable WebCrypto keys (needs an Expo dev build).
35
70
  ```
36
71
 
37
72
  ## Overview
@@ -59,8 +94,10 @@ const client = new PollarClient({ apiKey: 'your-api-key' });
59
94
  ## React Native (Expo)
60
95
 
61
96
  ```ts
62
- // At your app entry — `crypto.getRandomValues` polyfill
63
- import 'react-native-get-random-values';
97
+ // At your app entry, BEFORE importing @pollar/core runtime polyfills (see table above):
98
+ import 'react-native-get-random-values'; // crypto.getRandomValues
99
+ import 'react-native-polyfill-globals/auto'; // TextEncoder/TextDecoder + URL
100
+ // Optional: import 'react-native-quick-crypto' to upgrade to non-extractable WebCrypto keys (needs an Expo dev build).
64
101
 
65
102
  import { PollarClient } from '@pollar/core';
66
103
  import { createSecureStoreAdapter } from '@pollar/core/adapters/expo';
@@ -78,7 +115,9 @@ const client = new PollarClient({ apiKey: 'your-api-key', storage });
78
115
  ## React Native (`react-native-keychain`)
79
116
 
80
117
  ```ts
81
- import 'react-native-get-random-values';
118
+ import 'react-native-get-random-values'; // crypto.getRandomValues
119
+ import 'react-native-polyfill-globals/auto'; // TextEncoder/TextDecoder + URL
120
+ // Optional: import 'react-native-quick-crypto' to upgrade to non-extractable WebCrypto keys (needs an Expo dev build).
82
121
  import { PollarClient } from '@pollar/core';
83
122
  import { createKeychainAdapter } from '@pollar/core/adapters/react-native-keychain';
84
123
 
@@ -86,6 +125,86 @@ const storage = await createKeychainAdapter();
86
125
  const client = new PollarClient({ apiKey: 'your-api-key', storage });
87
126
  ```
88
127
 
128
+ ## Framework integration
129
+
130
+ `@pollar/core` is framework-agnostic: `PollarClient` is a plain class and the `on*StateChange` methods are
131
+ callback subscriptions. To render reactively, bridge those callbacks into your framework's state primitive. The
132
+ client instance should be a singleton (module scope, context, or DI) — never recreate it on every render.
133
+
134
+ ### React / Next.js
135
+
136
+ `useSyncExternalStore` is the idiomatic bridge. In Next.js, only instantiate in Client Components (`'use client'`)
137
+ — server-side, the SDK degrades to a no-op and warns.
138
+
139
+ ```tsx
140
+ 'use client';
141
+ import { useSyncExternalStore } from 'react';
142
+ import { PollarClient, type AuthState } from '@pollar/core';
143
+
144
+ const client = new PollarClient({ apiKey: 'pk_...' }); // module scope = one instance
145
+
146
+ export function useAuthState(): AuthState {
147
+ return useSyncExternalStore(
148
+ (cb) => client.onAuthStateChange(cb), // returns the unsubscribe fn
149
+ () => client.getAuthState(), // client snapshot
150
+ () => client.getAuthState(), // server snapshot (idle)
151
+ );
152
+ }
153
+
154
+ // const auth = useAuthState(); // re-renders on every auth transition
155
+ ```
156
+
157
+ ### Angular
158
+
159
+ The SDK's callbacks fire **outside Angular's zone**, so wrap state updates in `NgZone.run()` (or use signals) or the
160
+ view won't update. Expose the client through a service.
161
+
162
+ ```ts
163
+ import { Injectable, NgZone, signal } from '@angular/core';
164
+ import { PollarClient, type AuthState } from '@pollar/core';
165
+
166
+ @Injectable({ providedIn: 'root' })
167
+ export class PollarService {
168
+ private client = new PollarClient({ apiKey: 'pk_...' });
169
+ readonly authState = signal<AuthState>(this.client.getAuthState());
170
+
171
+ constructor(private zone: NgZone) {
172
+ this.client.onAuthStateChange((state) => {
173
+ this.zone.run(() => this.authState.set(state)); // re-enter Angular's zone
174
+ });
175
+ }
176
+
177
+ login = (email: string) => this.client.login({ provider: 'email', email });
178
+ }
179
+ ```
180
+
181
+ ### Vue 3
182
+
183
+ Assign the callback payload into a `ref` (or `shallowRef`) inside `onMounted`; unsubscribe in `onUnmounted`.
184
+
185
+ ```ts
186
+ import { ref, shallowRef, onMounted, onUnmounted } from 'vue';
187
+ import { PollarClient, type AuthState } from '@pollar/core';
188
+
189
+ const client = new PollarClient({ apiKey: 'pk_...' });
190
+
191
+ export function useAuth() {
192
+ const authState = shallowRef<AuthState>(client.getAuthState());
193
+ let unsub = () => {};
194
+ onMounted(() => {
195
+ unsub = client.onAuthStateChange((s) => (authState.value = s)); // ref assign = reactive
196
+ });
197
+ onUnmounted(() => unsub());
198
+ return { authState, login: client.login.bind(client) };
199
+ }
200
+ ```
201
+
202
+ ### React Native
203
+
204
+ Same as React (`useSyncExternalStore`), plus the entry-file polyfills and injected adapters shown in the
205
+ React Native sections above. OAuth and external-wallet logins require `openAuthUrl` / `walletAdapter` to be injected
206
+ (the built-in popup/extension adapters are web-only).
207
+
89
208
  ## Preserved-on-disk storage shape
90
209
 
91
210
  0.7.0 persists exactly:
@@ -167,6 +286,7 @@ client.login({ provider: 'email', email: 'user@example.com' });
167
286
 
168
287
  // Stellar wallet
169
288
  import { WalletType } from '@pollar/core';
289
+
170
290
  client.login({ provider: 'wallet', type: WalletType.FREIGHTER });
171
291
  client.login({ provider: 'wallet', type: WalletType.ALBEDO });
172
292
  ```
@@ -7,6 +7,16 @@ import { S as Storage } from '../types-DqgJIJBl.mjs';
7
7
  * than letting the platform truncate or silently fail.
8
8
  */
9
9
  declare const SECURE_STORE_MAX_VALUE_BYTES = 4096;
10
+ /**
11
+ * Map a logical storage key onto one Expo SecureStore accepts. SecureStore keys
12
+ * must match `[A-Za-z0-9._-]`, but the SDK namespaces its keys with `:`
13
+ * (`pollar:<apiKeyHash>:session`, `pollar:dpop-key:<apiKeyHash>`, …), which
14
+ * SecureStore rejects with an `Invalid key` error. Replace every disallowed
15
+ * character with `_`. The transform is deterministic and collision-free for the
16
+ * SDK's fixed key templates — their only variable segment is a hex hash — so a
17
+ * given logical key always resolves to the same SecureStore key.
18
+ */
19
+ declare function sanitizeKey(key: string): string;
10
20
  interface SecureStoreAdapterOptions {
11
21
  /**
12
22
  * Override the iOS Keychain accessibility class. Defaults to
@@ -23,4 +33,4 @@ interface SecureStoreAdapterOptions {
23
33
  */
24
34
  declare function createSecureStoreAdapter(options?: SecureStoreAdapterOptions): Promise<Storage>;
25
35
 
26
- export { SECURE_STORE_MAX_VALUE_BYTES, type SecureStoreAdapterOptions, createSecureStoreAdapter };
36
+ export { SECURE_STORE_MAX_VALUE_BYTES, type SecureStoreAdapterOptions, createSecureStoreAdapter, sanitizeKey };
@@ -7,6 +7,16 @@ import { S as Storage } from '../types-DqgJIJBl.js';
7
7
  * than letting the platform truncate or silently fail.
8
8
  */
9
9
  declare const SECURE_STORE_MAX_VALUE_BYTES = 4096;
10
+ /**
11
+ * Map a logical storage key onto one Expo SecureStore accepts. SecureStore keys
12
+ * must match `[A-Za-z0-9._-]`, but the SDK namespaces its keys with `:`
13
+ * (`pollar:<apiKeyHash>:session`, `pollar:dpop-key:<apiKeyHash>`, …), which
14
+ * SecureStore rejects with an `Invalid key` error. Replace every disallowed
15
+ * character with `_`. The transform is deterministic and collision-free for the
16
+ * SDK's fixed key templates — their only variable segment is a hex hash — so a
17
+ * given logical key always resolves to the same SecureStore key.
18
+ */
19
+ declare function sanitizeKey(key: string): string;
10
20
  interface SecureStoreAdapterOptions {
11
21
  /**
12
22
  * Override the iOS Keychain accessibility class. Defaults to
@@ -23,4 +33,4 @@ interface SecureStoreAdapterOptions {
23
33
  */
24
34
  declare function createSecureStoreAdapter(options?: SecureStoreAdapterOptions): Promise<Storage>;
25
35
 
26
- export { SECURE_STORE_MAX_VALUE_BYTES, type SecureStoreAdapterOptions, createSecureStoreAdapter };
36
+ export { SECURE_STORE_MAX_VALUE_BYTES, type SecureStoreAdapterOptions, createSecureStoreAdapter, sanitizeKey };
@@ -2,6 +2,9 @@
2
2
 
3
3
  // src/adapters/expo-secure-store.ts
4
4
  var SECURE_STORE_MAX_VALUE_BYTES = 4096;
5
+ function sanitizeKey(key) {
6
+ return key.replace(/[^A-Za-z0-9._-]/g, "_");
7
+ }
5
8
  async function loadSecureStore() {
6
9
  try {
7
10
  const mod = await import('expo-secure-store');
@@ -30,7 +33,7 @@ async function createSecureStoreAdapter(options = {}) {
30
33
  const accessible = options.keychainAccessible !== void 0 ? options.keychainAccessible : SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY;
31
34
  return {
32
35
  async get(key) {
33
- return SecureStore.getItemAsync(key);
36
+ return SecureStore.getItemAsync(sanitizeKey(key));
34
37
  },
35
38
  async set(key, value) {
36
39
  const size = utf8ByteLength(value);
@@ -39,15 +42,20 @@ async function createSecureStoreAdapter(options = {}) {
39
42
  `[PollarClient:storage] Value for "${key}" is ${size} bytes, exceeds SecureStore limit ${SECURE_STORE_MAX_VALUE_BYTES}`
40
43
  );
41
44
  }
42
- await SecureStore.setItemAsync(key, value, accessible !== void 0 ? { keychainAccessible: accessible } : void 0);
45
+ await SecureStore.setItemAsync(
46
+ sanitizeKey(key),
47
+ value,
48
+ accessible !== void 0 ? { keychainAccessible: accessible } : void 0
49
+ );
43
50
  },
44
51
  async remove(key) {
45
- await SecureStore.deleteItemAsync(key);
52
+ await SecureStore.deleteItemAsync(sanitizeKey(key));
46
53
  }
47
54
  };
48
55
  }
49
56
 
50
57
  exports.SECURE_STORE_MAX_VALUE_BYTES = SECURE_STORE_MAX_VALUE_BYTES;
51
58
  exports.createSecureStoreAdapter = createSecureStoreAdapter;
59
+ exports.sanitizeKey = sanitizeKey;
52
60
  //# sourceMappingURL=expo-secure-store.js.map
53
61
  //# sourceMappingURL=expo-secure-store.js.map
@@ -1 +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-expect-error -- 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"]}
1
+ {"version":3,"sources":["../../src/adapters/expo-secure-store.ts"],"names":[],"mappings":";;;AAqCO,IAAM,4BAAA,GAA+B;AAWrC,SAAS,YAAY,GAAA,EAAqB;AAC/C,EAAA,OAAO,GAAA,CAAI,OAAA,CAAQ,kBAAA,EAAoB,GAAG,CAAA;AAC5C;AAWA,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,YAAA,CAAa,WAAA,CAAY,GAAG,CAAC,CAAA;AAAA,IAClD,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;AAAA,QAChB,YAAY,GAAG,CAAA;AAAA,QACf,KAAA;AAAA,QACA,UAAA,KAAe,MAAA,GAAY,EAAE,kBAAA,EAAoB,YAAW,GAAI;AAAA,OAClE;AAAA,IACF,CAAA;AAAA,IACA,MAAM,OAAO,GAAA,EAAK;AAChB,MAAA,MAAM,WAAA,CAAY,eAAA,CAAgB,WAAA,CAAY,GAAG,CAAC,CAAA;AAAA,IACpD;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\n/**\n * Map a logical storage key onto one Expo SecureStore accepts. SecureStore keys\n * must match `[A-Za-z0-9._-]`, but the SDK namespaces its keys with `:`\n * (`pollar:<apiKeyHash>:session`, `pollar:dpop-key:<apiKeyHash>`, …), which\n * SecureStore rejects with an `Invalid key` error. Replace every disallowed\n * character with `_`. The transform is deterministic and collision-free for the\n * SDK's fixed key templates — their only variable segment is a hex hash — so a\n * given logical key always resolves to the same SecureStore key.\n */\nexport function sanitizeKey(key: string): string {\n return key.replace(/[^A-Za-z0-9._-]/g, '_');\n}\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-expect-error -- 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(sanitizeKey(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(\n sanitizeKey(key),\n value,\n accessible !== undefined ? { keychainAccessible: accessible } : undefined,\n );\n },\n async remove(key) {\n await SecureStore.deleteItemAsync(sanitizeKey(key));\n },\n };\n}\n"]}
@@ -1,5 +1,8 @@
1
1
  // src/adapters/expo-secure-store.ts
2
2
  var SECURE_STORE_MAX_VALUE_BYTES = 4096;
3
+ function sanitizeKey(key) {
4
+ return key.replace(/[^A-Za-z0-9._-]/g, "_");
5
+ }
3
6
  async function loadSecureStore() {
4
7
  try {
5
8
  const mod = await import('expo-secure-store');
@@ -28,7 +31,7 @@ async function createSecureStoreAdapter(options = {}) {
28
31
  const accessible = options.keychainAccessible !== void 0 ? options.keychainAccessible : SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY;
29
32
  return {
30
33
  async get(key) {
31
- return SecureStore.getItemAsync(key);
34
+ return SecureStore.getItemAsync(sanitizeKey(key));
32
35
  },
33
36
  async set(key, value) {
34
37
  const size = utf8ByteLength(value);
@@ -37,14 +40,18 @@ async function createSecureStoreAdapter(options = {}) {
37
40
  `[PollarClient:storage] Value for "${key}" is ${size} bytes, exceeds SecureStore limit ${SECURE_STORE_MAX_VALUE_BYTES}`
38
41
  );
39
42
  }
40
- await SecureStore.setItemAsync(key, value, accessible !== void 0 ? { keychainAccessible: accessible } : void 0);
43
+ await SecureStore.setItemAsync(
44
+ sanitizeKey(key),
45
+ value,
46
+ accessible !== void 0 ? { keychainAccessible: accessible } : void 0
47
+ );
41
48
  },
42
49
  async remove(key) {
43
- await SecureStore.deleteItemAsync(key);
50
+ await SecureStore.deleteItemAsync(sanitizeKey(key));
44
51
  }
45
52
  };
46
53
  }
47
54
 
48
- export { SECURE_STORE_MAX_VALUE_BYTES, createSecureStoreAdapter };
55
+ export { SECURE_STORE_MAX_VALUE_BYTES, createSecureStoreAdapter, sanitizeKey };
49
56
  //# sourceMappingURL=expo-secure-store.mjs.map
50
57
  //# sourceMappingURL=expo-secure-store.mjs.map
@@ -1 +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-expect-error -- 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"]}
1
+ {"version":3,"sources":["../../src/adapters/expo-secure-store.ts"],"names":[],"mappings":";AAqCO,IAAM,4BAAA,GAA+B;AAWrC,SAAS,YAAY,GAAA,EAAqB;AAC/C,EAAA,OAAO,GAAA,CAAI,OAAA,CAAQ,kBAAA,EAAoB,GAAG,CAAA;AAC5C;AAWA,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,YAAA,CAAa,WAAA,CAAY,GAAG,CAAC,CAAA;AAAA,IAClD,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;AAAA,QAChB,YAAY,GAAG,CAAA;AAAA,QACf,KAAA;AAAA,QACA,UAAA,KAAe,MAAA,GAAY,EAAE,kBAAA,EAAoB,YAAW,GAAI;AAAA,OAClE;AAAA,IACF,CAAA;AAAA,IACA,MAAM,OAAO,GAAA,EAAK;AAChB,MAAA,MAAM,WAAA,CAAY,eAAA,CAAgB,WAAA,CAAY,GAAG,CAAC,CAAA;AAAA,IACpD;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\n/**\n * Map a logical storage key onto one Expo SecureStore accepts. SecureStore keys\n * must match `[A-Za-z0-9._-]`, but the SDK namespaces its keys with `:`\n * (`pollar:<apiKeyHash>:session`, `pollar:dpop-key:<apiKeyHash>`, …), which\n * SecureStore rejects with an `Invalid key` error. Replace every disallowed\n * character with `_`. The transform is deterministic and collision-free for the\n * SDK's fixed key templates — their only variable segment is a hex hash — so a\n * given logical key always resolves to the same SecureStore key.\n */\nexport function sanitizeKey(key: string): string {\n return key.replace(/[^A-Za-z0-9._-]/g, '_');\n}\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-expect-error -- 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(sanitizeKey(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(\n sanitizeKey(key),\n value,\n accessible !== undefined ? { keychainAccessible: accessible } : undefined,\n );\n },\n async remove(key) {\n await SecureStore.deleteItemAsync(sanitizeKey(key));\n },\n };\n}\n"]}
@@ -0,0 +1,10 @@
1
+ import { V as VisibilityProvider } from '../types-84G_htcn.mjs';
2
+
3
+ /**
4
+ * Create a `VisibilityProvider` backed by React Native's `AppState`.
5
+ *
6
+ * Throws (via the returned Promise) if `react-native` cannot be loaded.
7
+ */
8
+ declare function createAppStateVisibilityProvider(): Promise<VisibilityProvider>;
9
+
10
+ export { createAppStateVisibilityProvider };
@@ -0,0 +1,10 @@
1
+ import { V as VisibilityProvider } from '../types-84G_htcn.js';
2
+
3
+ /**
4
+ * Create a `VisibilityProvider` backed by React Native's `AppState`.
5
+ *
6
+ * Throws (via the returned Promise) if `react-native` cannot be loaded.
7
+ */
8
+ declare function createAppStateVisibilityProvider(): Promise<VisibilityProvider>;
9
+
10
+ export { createAppStateVisibilityProvider };
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ // src/adapters/react-native-appstate.ts
4
+ async function loadAppState() {
5
+ try {
6
+ const mod = await import('react-native');
7
+ const AppState = mod.AppState ?? mod.default?.AppState;
8
+ if (!AppState) {
9
+ throw new Error("'react-native' loaded but exposes no AppState export");
10
+ }
11
+ return AppState;
12
+ } catch (error) {
13
+ const message = `[PollarClient:visibility] Failed to load 'react-native' AppState. This adapter only runs inside a React Native app. Original error: ${error instanceof Error ? error.message : String(error)}`;
14
+ throw new Error(message);
15
+ }
16
+ }
17
+ async function createAppStateVisibilityProvider() {
18
+ const AppState = await loadAppState();
19
+ const isActive = (state) => state === "active";
20
+ return {
21
+ isVisible: () => isActive(AppState.currentState),
22
+ onChange: (cb) => {
23
+ let last = isActive(AppState.currentState);
24
+ const subscription = AppState.addEventListener("change", (state) => {
25
+ const next = isActive(state);
26
+ if (next !== last) {
27
+ last = next;
28
+ cb(next);
29
+ }
30
+ });
31
+ return () => subscription.remove();
32
+ }
33
+ };
34
+ }
35
+
36
+ exports.createAppStateVisibilityProvider = createAppStateVisibilityProvider;
37
+ //# sourceMappingURL=react-native-appstate.js.map
38
+ //# sourceMappingURL=react-native-appstate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/react-native-appstate.ts"],"names":[],"mappings":";;;AA8BA,eAAe,YAAA,GAAqC;AAClD,EAAA,IAAI;AAGF,IAAA,MAAM,GAAA,GAAM,MAAM,OAAO,cAAc,CAAA;AACvC,IAAA,MAAM,QAAA,GAAY,GAAA,CAAmC,QAAA,IAAa,GAAA,CAAiD,OAAA,EAAS,QAAA;AAC5H,IAAA,IAAI,CAAC,QAAA,EAAU;AACb,MAAA,MAAM,IAAI,MAAM,sDAAsD,CAAA;AAAA,IACxE;AACA,IAAA,OAAO,QAAA;AAAA,EACT,SAAS,KAAA,EAAO;AACd,IAAA,MAAM,OAAA,GACJ,uIAEmB,KAAA,YAAiB,KAAA,GAAQ,MAAM,OAAA,GAAU,MAAA,CAAO,KAAK,CAAC,CAAA,CAAA;AAC3E,IAAA,MAAM,IAAI,MAAM,OAAO,CAAA;AAAA,EACzB;AACF;AAOA,eAAsB,gCAAA,GAAgE;AACpF,EAAA,MAAM,QAAA,GAAW,MAAM,YAAA,EAAa;AAEpC,EAAA,MAAM,QAAA,GAAW,CAAC,KAAA,KAA2B,KAAA,KAAU,QAAA;AAEvD,EAAA,OAAO;AAAA,IACL,SAAA,EAAW,MAAM,QAAA,CAAS,QAAA,CAAS,YAAY,CAAA;AAAA,IAC/C,QAAA,EAAU,CAAC,EAAA,KAAO;AAIhB,MAAA,IAAI,IAAA,GAAO,QAAA,CAAS,QAAA,CAAS,YAAY,CAAA;AACzC,MAAA,MAAM,YAAA,GAAe,QAAA,CAAS,gBAAA,CAAiB,QAAA,EAAU,CAAC,KAAA,KAAU;AAClE,QAAA,MAAM,IAAA,GAAO,SAAS,KAAK,CAAA;AAC3B,QAAA,IAAI,SAAS,IAAA,EAAM;AACjB,UAAA,IAAA,GAAO,IAAA;AACP,UAAA,EAAA,CAAG,IAAI,CAAA;AAAA,QACT;AAAA,MACF,CAAC,CAAA;AACD,MAAA,OAAO,MAAM,aAAa,MAAA,EAAO;AAAA,IACnC;AAAA,GACF;AACF","file":"react-native-appstate.js","sourcesContent":["import type { VisibilityProvider } from '../visibility/types';\n\n/**\n * `AppState`-backed {@link VisibilityProvider} for React Native.\n *\n * Wire it into the silent-refresh scheduler so proactive token renewals are\n * skipped while the app is backgrounded and run the moment it returns to the\n * foreground — matching the web `visibilitychange` behavior and sidestepping\n * RN's aggressive background timer throttling.\n *\n * `react-native` is the consumer's framework (not a dependency of this SDK),\n * so the module is loaded lazily via dynamic `import('react-native')`. That\n * keeps web/Node bundles from ever resolving it. Because loading is async, the\n * factory is async too — mirror the `createSecureStoreAdapter` usage:\n *\n * import { createAppStateVisibilityProvider } from '@pollar/core/adapters/react-native-appstate';\n * const visibilityProvider = await createAppStateVisibilityProvider();\n * new PollarClient({ apiKey, storage, visibilityProvider });\n */\n\n/**\n * Minimal structural type for the slice of `react-native`'s `AppState` we use.\n * Typed here instead of importing the package's types because `react-native`\n * is an optional peer the SDK is not type-checked against.\n */\ntype AppStateApi = {\n currentState: string;\n addEventListener: (type: 'change', handler: (state: string) => void) => { remove: () => void };\n};\n\nasync function loadAppState(): Promise<AppStateApi> {\n try {\n // @ts-expect-error -- optional peer dep; resolved at runtime in RN apps,\n // absent when the SDK is built or run on web/Node.\n const mod = await import('react-native');\n const AppState = (mod as { AppState?: AppStateApi }).AppState ?? (mod as { default?: { AppState?: AppStateApi } }).default?.AppState;\n if (!AppState) {\n throw new Error(\"'react-native' loaded but exposes no AppState export\");\n }\n return AppState;\n } catch (error) {\n const message =\n `[PollarClient:visibility] Failed to load 'react-native' AppState. ` +\n `This adapter only runs inside a React Native app. ` +\n `Original error: ${error instanceof Error ? error.message : String(error)}`;\n throw new Error(message);\n }\n}\n\n/**\n * Create a `VisibilityProvider` backed by React Native's `AppState`.\n *\n * Throws (via the returned Promise) if `react-native` cannot be loaded.\n */\nexport async function createAppStateVisibilityProvider(): Promise<VisibilityProvider> {\n const AppState = await loadAppState();\n\n const isActive = (state: string): boolean => state === 'active';\n\n return {\n isVisible: () => isActive(AppState.currentState),\n onChange: (cb) => {\n // Filter duplicate notifications — listeners only see real transitions,\n // matching the web provider's contract. RN also emits 'inactive'\n // (iOS transition state) which we collapse into \"not visible\".\n let last = isActive(AppState.currentState);\n const subscription = AppState.addEventListener('change', (state) => {\n const next = isActive(state);\n if (next !== last) {\n last = next;\n cb(next);\n }\n });\n return () => subscription.remove();\n },\n };\n}\n"]}
@@ -0,0 +1,36 @@
1
+ // src/adapters/react-native-appstate.ts
2
+ async function loadAppState() {
3
+ try {
4
+ const mod = await import('react-native');
5
+ const AppState = mod.AppState ?? mod.default?.AppState;
6
+ if (!AppState) {
7
+ throw new Error("'react-native' loaded but exposes no AppState export");
8
+ }
9
+ return AppState;
10
+ } catch (error) {
11
+ const message = `[PollarClient:visibility] Failed to load 'react-native' AppState. This adapter only runs inside a React Native app. Original error: ${error instanceof Error ? error.message : String(error)}`;
12
+ throw new Error(message);
13
+ }
14
+ }
15
+ async function createAppStateVisibilityProvider() {
16
+ const AppState = await loadAppState();
17
+ const isActive = (state) => state === "active";
18
+ return {
19
+ isVisible: () => isActive(AppState.currentState),
20
+ onChange: (cb) => {
21
+ let last = isActive(AppState.currentState);
22
+ const subscription = AppState.addEventListener("change", (state) => {
23
+ const next = isActive(state);
24
+ if (next !== last) {
25
+ last = next;
26
+ cb(next);
27
+ }
28
+ });
29
+ return () => subscription.remove();
30
+ }
31
+ };
32
+ }
33
+
34
+ export { createAppStateVisibilityProvider };
35
+ //# sourceMappingURL=react-native-appstate.mjs.map
36
+ //# sourceMappingURL=react-native-appstate.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/react-native-appstate.ts"],"names":[],"mappings":";AA8BA,eAAe,YAAA,GAAqC;AAClD,EAAA,IAAI;AAGF,IAAA,MAAM,GAAA,GAAM,MAAM,OAAO,cAAc,CAAA;AACvC,IAAA,MAAM,QAAA,GAAY,GAAA,CAAmC,QAAA,IAAa,GAAA,CAAiD,OAAA,EAAS,QAAA;AAC5H,IAAA,IAAI,CAAC,QAAA,EAAU;AACb,MAAA,MAAM,IAAI,MAAM,sDAAsD,CAAA;AAAA,IACxE;AACA,IAAA,OAAO,QAAA;AAAA,EACT,SAAS,KAAA,EAAO;AACd,IAAA,MAAM,OAAA,GACJ,uIAEmB,KAAA,YAAiB,KAAA,GAAQ,MAAM,OAAA,GAAU,MAAA,CAAO,KAAK,CAAC,CAAA,CAAA;AAC3E,IAAA,MAAM,IAAI,MAAM,OAAO,CAAA;AAAA,EACzB;AACF;AAOA,eAAsB,gCAAA,GAAgE;AACpF,EAAA,MAAM,QAAA,GAAW,MAAM,YAAA,EAAa;AAEpC,EAAA,MAAM,QAAA,GAAW,CAAC,KAAA,KAA2B,KAAA,KAAU,QAAA;AAEvD,EAAA,OAAO;AAAA,IACL,SAAA,EAAW,MAAM,QAAA,CAAS,QAAA,CAAS,YAAY,CAAA;AAAA,IAC/C,QAAA,EAAU,CAAC,EAAA,KAAO;AAIhB,MAAA,IAAI,IAAA,GAAO,QAAA,CAAS,QAAA,CAAS,YAAY,CAAA;AACzC,MAAA,MAAM,YAAA,GAAe,QAAA,CAAS,gBAAA,CAAiB,QAAA,EAAU,CAAC,KAAA,KAAU;AAClE,QAAA,MAAM,IAAA,GAAO,SAAS,KAAK,CAAA;AAC3B,QAAA,IAAI,SAAS,IAAA,EAAM;AACjB,UAAA,IAAA,GAAO,IAAA;AACP,UAAA,EAAA,CAAG,IAAI,CAAA;AAAA,QACT;AAAA,MACF,CAAC,CAAA;AACD,MAAA,OAAO,MAAM,aAAa,MAAA,EAAO;AAAA,IACnC;AAAA,GACF;AACF","file":"react-native-appstate.mjs","sourcesContent":["import type { VisibilityProvider } from '../visibility/types';\n\n/**\n * `AppState`-backed {@link VisibilityProvider} for React Native.\n *\n * Wire it into the silent-refresh scheduler so proactive token renewals are\n * skipped while the app is backgrounded and run the moment it returns to the\n * foreground — matching the web `visibilitychange` behavior and sidestepping\n * RN's aggressive background timer throttling.\n *\n * `react-native` is the consumer's framework (not a dependency of this SDK),\n * so the module is loaded lazily via dynamic `import('react-native')`. That\n * keeps web/Node bundles from ever resolving it. Because loading is async, the\n * factory is async too — mirror the `createSecureStoreAdapter` usage:\n *\n * import { createAppStateVisibilityProvider } from '@pollar/core/adapters/react-native-appstate';\n * const visibilityProvider = await createAppStateVisibilityProvider();\n * new PollarClient({ apiKey, storage, visibilityProvider });\n */\n\n/**\n * Minimal structural type for the slice of `react-native`'s `AppState` we use.\n * Typed here instead of importing the package's types because `react-native`\n * is an optional peer the SDK is not type-checked against.\n */\ntype AppStateApi = {\n currentState: string;\n addEventListener: (type: 'change', handler: (state: string) => void) => { remove: () => void };\n};\n\nasync function loadAppState(): Promise<AppStateApi> {\n try {\n // @ts-expect-error -- optional peer dep; resolved at runtime in RN apps,\n // absent when the SDK is built or run on web/Node.\n const mod = await import('react-native');\n const AppState = (mod as { AppState?: AppStateApi }).AppState ?? (mod as { default?: { AppState?: AppStateApi } }).default?.AppState;\n if (!AppState) {\n throw new Error(\"'react-native' loaded but exposes no AppState export\");\n }\n return AppState;\n } catch (error) {\n const message =\n `[PollarClient:visibility] Failed to load 'react-native' AppState. ` +\n `This adapter only runs inside a React Native app. ` +\n `Original error: ${error instanceof Error ? error.message : String(error)}`;\n throw new Error(message);\n }\n}\n\n/**\n * Create a `VisibilityProvider` backed by React Native's `AppState`.\n *\n * Throws (via the returned Promise) if `react-native` cannot be loaded.\n */\nexport async function createAppStateVisibilityProvider(): Promise<VisibilityProvider> {\n const AppState = await loadAppState();\n\n const isActive = (state: string): boolean => state === 'active';\n\n return {\n isVisible: () => isActive(AppState.currentState),\n onChange: (cb) => {\n // Filter duplicate notifications — listeners only see real transitions,\n // matching the web provider's contract. RN also emits 'inactive'\n // (iOS transition state) which we collapse into \"not visible\".\n let last = isActive(AppState.currentState);\n const subscription = AppState.addEventListener('change', (state) => {\n const next = isActive(state);\n if (next !== last) {\n last = next;\n cb(next);\n }\n });\n return () => subscription.remove();\n },\n };\n}\n"]}