@pollar/core 0.8.1 → 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 +24 -16
- package/dist/adapters/expo-secure-store.d.mts +11 -1
- package/dist/adapters/expo-secure-store.d.ts +11 -1
- package/dist/adapters/expo-secure-store.js +11 -3
- package/dist/adapters/expo-secure-store.js.map +1 -1
- package/dist/adapters/expo-secure-store.mjs +11 -4
- package/dist/adapters/expo-secure-store.mjs.map +1 -1
- package/dist/index.js +3 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3 -4
- package/dist/index.mjs.map +1 -1
- package/dist/index.rn.js +6 -8
- package/dist/index.rn.js.map +1 -1
- package/dist/index.rn.mjs +3 -5
- package/dist/index.rn.mjs.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -27,25 +27,34 @@ 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-
|
|
32
|
+
npm i react-native-polyfill-globals
|
|
33
33
|
|
|
34
34
|
# Bare React Native
|
|
35
|
-
npm i react-native-keychain react-native-get-random-values react-native-
|
|
35
|
+
npm i react-native-keychain react-native-get-random-values react-native-polyfill-globals
|
|
36
36
|
```
|
|
37
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
|
+
|
|
38
45
|
> **React Native runtime requirements.** The SDK builds a DPoP proof (RFC 9449) for **every** authenticated request.
|
|
39
|
-
> That path uses
|
|
46
|
+
> That path uses three standard Web primitives that React Native / Hermes does **not** all ship by default. Register
|
|
40
47
|
> them at the very top of your entry file, **before** any `@pollar/core` import. If any is missing, DPoP proof
|
|
41
48
|
> construction fails and **no authenticated request works** — the SDK is not at fault, the runtime is.
|
|
42
49
|
>
|
|
43
|
-
> | Primitive
|
|
44
|
-
> |
|
|
45
|
-
> | `crypto.getRandomValues`
|
|
46
|
-
> | `
|
|
47
|
-
> | `
|
|
48
|
-
>
|
|
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).
|
|
49
58
|
>
|
|
50
59
|
> `react-native-polyfill-globals/auto` is the pragmatic one-liner — it installs `TextEncoder`/`TextDecoder` **and**
|
|
51
60
|
> `URL` together (plus base64 / fetch-streaming you don't strictly need). The SDK does **not** rely on `fetch` response
|
|
@@ -57,7 +66,7 @@ Entry-file setup (e.g. `index.js`), **before importing `@pollar/core`**:
|
|
|
57
66
|
```ts
|
|
58
67
|
import 'react-native-get-random-values'; // crypto.getRandomValues
|
|
59
68
|
import 'react-native-polyfill-globals/auto'; // TextEncoder/TextDecoder + URL
|
|
60
|
-
import 'react-native-quick-crypto'
|
|
69
|
+
// Optional: import 'react-native-quick-crypto' to upgrade to non-extractable WebCrypto keys (needs an Expo dev build).
|
|
61
70
|
```
|
|
62
71
|
|
|
63
72
|
## Overview
|
|
@@ -88,7 +97,7 @@ const client = new PollarClient({ apiKey: 'your-api-key' });
|
|
|
88
97
|
// At your app entry, BEFORE importing @pollar/core — runtime polyfills (see table above):
|
|
89
98
|
import 'react-native-get-random-values'; // crypto.getRandomValues
|
|
90
99
|
import 'react-native-polyfill-globals/auto'; // TextEncoder/TextDecoder + URL
|
|
91
|
-
import 'react-native-quick-crypto'
|
|
100
|
+
// Optional: import 'react-native-quick-crypto' to upgrade to non-extractable WebCrypto keys (needs an Expo dev build).
|
|
92
101
|
|
|
93
102
|
import { PollarClient } from '@pollar/core';
|
|
94
103
|
import { createSecureStoreAdapter } from '@pollar/core/adapters/expo';
|
|
@@ -108,7 +117,7 @@ const client = new PollarClient({ apiKey: 'your-api-key', storage });
|
|
|
108
117
|
```ts
|
|
109
118
|
import 'react-native-get-random-values'; // crypto.getRandomValues
|
|
110
119
|
import 'react-native-polyfill-globals/auto'; // TextEncoder/TextDecoder + URL
|
|
111
|
-
import 'react-native-quick-crypto'
|
|
120
|
+
// Optional: import 'react-native-quick-crypto' to upgrade to non-extractable WebCrypto keys (needs an Expo dev build).
|
|
112
121
|
import { PollarClient } from '@pollar/core';
|
|
113
122
|
import { createKeychainAdapter } from '@pollar/core/adapters/react-native-keychain';
|
|
114
123
|
|
|
@@ -181,8 +190,7 @@ const client = new PollarClient({ apiKey: 'pk_...' });
|
|
|
181
190
|
|
|
182
191
|
export function useAuth() {
|
|
183
192
|
const authState = shallowRef<AuthState>(client.getAuthState());
|
|
184
|
-
let unsub = () => {
|
|
185
|
-
};
|
|
193
|
+
let unsub = () => {};
|
|
186
194
|
onMounted(() => {
|
|
187
195
|
unsub = client.onAuthStateChange((s) => (authState.value = s)); // ref assign = reactive
|
|
188
196
|
});
|
|
@@ -250,7 +258,7 @@ const sessions = await client.listSessions();
|
|
|
250
258
|
### `new PollarClient(config)`
|
|
251
259
|
|
|
252
260
|
| Option | Type | Required | Description |
|
|
253
|
-
|
|
261
|
+
| ------------------ | ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------- |
|
|
254
262
|
| `apiKey` | `string` | Yes | Your Pollar API key |
|
|
255
263
|
| `baseUrl` | `string` | No | Override the default API endpoint |
|
|
256
264
|
| `stellarNetwork` | `'mainnet' \| 'testnet'` | No | Target Stellar network (default: `testnet`) |
|
|
@@ -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(
|
|
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;
|
|
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(
|
|
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;
|
|
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"]}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var sha2 = require('@noble/hashes/sha2');
|
|
4
|
+
|
|
3
5
|
var __create = Object.create;
|
|
4
6
|
var __defProp = Object.defineProperty;
|
|
5
7
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -184,11 +186,8 @@ function defaultKeyManager(storage, apiKey) {
|
|
|
184
186
|
}
|
|
185
187
|
return _factory(storage, apiKey);
|
|
186
188
|
}
|
|
187
|
-
|
|
188
|
-
// src/lib/sha256.ts
|
|
189
189
|
async function sha256(data) {
|
|
190
|
-
|
|
191
|
-
return new Uint8Array(buf);
|
|
190
|
+
return sha2.sha256(data);
|
|
192
191
|
}
|
|
193
192
|
|
|
194
193
|
// src/lib/api-key-hash.ts
|