@symbiote-native/secure-store 3.0.2 → 3.0.3

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.
Files changed (2) hide show
  1. package/README.md +86 -70
  2. package/package.json +15 -15
package/README.md CHANGED
@@ -1,14 +1,13 @@
1
1
  # @symbiote-native/secure-store
2
2
 
3
- A wrapper package for [SymbioteNative](../../README.md) that makes
4
- [`expo-secure-store`](https://github.com/expo/expo/tree/main/packages/expo-secure-store) —
5
- encrypted key-value storage in the iOS Keychain and the Android Keystore, optionally gated behind
6
- the device's own biometrics — usable from **every** adapter, React, Vue, Svelte, Solid, and
7
- Angular, not just React. Built the same way as [`@symbiote-native/local-auth`](../local-auth): an
8
- `expo-modules-core`-based wrapper (see the `symbiote-expo-native-module` project skill for the
9
- full mechanism — why `expo-modules-core` is depended on directly and never the `expo`
10
- meta-package, why the upstream JS is hand-ported into `core/` rather than imported, and how
11
- autolinking picks up the native module).
3
+ Store tokens and other small secrets encrypted on the device: in the iOS Keychain and the Android
4
+ Keystore, optionally behind the user's fingerprint, face or passcode. One API for every
5
+ [SymbioteNative](../../README.md) adapter (React, Vue, Svelte, Solid and Angular).
6
+
7
+ It wraps [`expo-secure-store`](https://github.com/expo/expo/tree/main/packages/expo-secure-store)
8
+ the same way [`@symbiote-native/local-auth`](../local-auth) wraps its upstream: `expo-modules-core`
9
+ is a direct dependency, never the `expo` meta-package, and the upstream JS is hand-ported into
10
+ `core/`. The mechanics live in the `symbiote-expo-native-module` project skill.
12
11
 
13
12
  ## Install
14
13
 
@@ -24,77 +23,75 @@ npx @symbiote-native/cli new my-app --secure-store
24
23
  npx @symbiote-native/cli add --secure-store
25
24
  ```
26
25
 
27
- Either way: installs `@symbiote-native/secure-store` and wires the native autolinking
28
- automatically — see [`@symbiote-native/cli`](../cli).
26
+ Either way this installs `@symbiote-native/secure-store` and wires native autolinking. See
27
+ [`@symbiote-native/cli`](../cli).
29
28
 
30
29
  <details>
31
- <summary>Manual install (no CLI — installing and wiring native autolinking by hand)</summary>
30
+ <summary>Manual install (no CLI - installing and wiring native autolinking by hand)</summary>
32
31
 
33
32
  ```bash
34
33
  npm install @symbiote-native/secure-store
35
34
  ```
36
35
 
37
- `expo-secure-store` and `expo-modules-core` come along as regular, pinned dependencies — never
38
- install either yourself, and never add the `expo` meta-package to this project (it bundles its
39
- own Metro/Babel pipeline that conflicts with this project's own).
36
+ `expo-secure-store` and `expo-modules-core` come along as pinned dependencies. Do not install
37
+ either yourself, and do not add the `expo` meta-package: it bundles its own Metro/Babel pipeline,
38
+ which conflicts with this project's.
40
39
 
41
40
  ### Required one-time step: native autolinking wiring
42
41
 
43
- Unlike a plain RN native module, `expo-secure-store`'s native code is discovered by
44
- `expo-modules-autolinking` — this needs wiring into the native host app **once**, covering this
45
- package and every other `expo-modules-core` package with zero further changes:
42
+ `expo-secure-store`'s native code is discovered by `expo-modules-autolinking`. Wire it into the host
43
+ app once; the same wiring covers every other `expo-modules-core` package.
46
44
 
47
45
  | Platform | Touches |
48
46
  | -------- | ------------------------------------------------------------------------------------- |
49
- | iOS | `ios/Podfile` — add `use_expo_modules!` |
50
- | iOS | `AppDelegate.swift` — Expo's runtime-bootstrap hook |
51
- | Android | `settings.gradle` / `app/build.gradle` — resolve and include the Expo Gradle projects |
52
- | Android | `MainApplication.kt` — Expo's bootstrap hook, plus a native-module name map |
47
+ | iOS | `ios/Podfile`: add `use_expo_modules!` |
48
+ | iOS | `AppDelegate.swift`: Expo's runtime-bootstrap hook |
49
+ | Android | `settings.gradle` / `app/build.gradle`: resolve and include the Expo Gradle projects |
50
+ | Android | `MainApplication.kt`: Expo's bootstrap hook, plus a native-module name map |
53
51
 
54
- Full mechanics live in the `symbiote-expo-native-module` skill. The per-package half of that
55
- table — the Gradle dependency, the module map entry, the `NSFaceIDUsageDescription` string, and
56
- the Android backup rules below — is generated by
57
- [`@symbiote-native/expo-modules-link`](../expo-modules-link) from this package's
58
- `native-link.json` on every install.
52
+ Full mechanics: the `symbiote-expo-native-module` skill. The per-package half of that table (the
53
+ Gradle dependency, the module map entry, the `NSFaceIDUsageDescription` string and the Android
54
+ backup attributes below) is generated by
55
+ [`@symbiote-native/expo-modules-link`](../expo-modules-link) from this package's `native-link.json`
56
+ on every install.
59
57
 
60
58
  ### Android: Auto Backup must exclude the store
61
59
 
62
- `native-link.json` asks the linker to set two attributes on your app's `<application>` element:
60
+ `native-link.json` asks the linker to set two attributes on your `<application>` element:
63
61
 
64
62
  ```xml
65
63
  android:fullBackupContent="@xml/secure_store_backup_rules"
66
64
  android:dataExtractionRules="@xml/secure_store_data_extraction_rules"
67
65
  ```
68
66
 
69
- Both rule files ship inside `expo-secure-store` itself and merge in automatically; only the two
70
- attributes have to live in your own manifest. They matter: Android Auto Backup would otherwise
71
- upload the encrypted entries while leaving the Keystore keys that decrypt them behind, and a
72
- restore onto a new device would hand the app values it can no longer read. If your app already
73
- sets either attribute, the linker keeps yours and prints a notice — merge the rules yourself in
74
- that case (Expo's own [SecureStore docs](https://docs.expo.dev/versions/latest/sdk/securestore/)
75
- describe the rule files).
67
+ The rule files ship inside `expo-secure-store` and merge in automatically; only the attributes live
68
+ in your manifest. Without them Auto Backup uploads the encrypted entries but leaves the Keystore
69
+ keys behind, and a restore onto a new device hands the app values it can no longer decrypt. If your
70
+ app already sets either attribute the linker keeps yours and prints a notice; merge the rules by
71
+ hand in that case (see Expo's
72
+ [SecureStore docs](https://docs.expo.dev/versions/latest/sdk/securestore/)).
76
73
 
77
74
  </details>
78
75
 
79
76
  ## Shape
80
77
 
81
78
  ```
82
- src/core/ the whole API: seven keychain-accessibility constants plus the
83
- get/set/delete surface. native-module.ts resolves ExpoSecureStore
84
- through expo-modules-core's requireNativeModule.
85
- src/angular/ @symbiote-native/secure-store/angular
79
+ src/core/ the whole API: seven keychain-accessibility constants plus get/set/delete.
80
+ native-module.ts resolves ExpoSecureStore through requireNativeModule.
81
+ src/angular/ @symbiote-native/secure-store/angular
86
82
  ```
87
83
 
88
- `./react`, `./vue`, `./svelte`, and `./solid` are `exports`-map aliases straight onto `src/core/` —
89
- no physical per-framework file. Upstream ships free functions and constants — no per-instance state,
90
- no event stream — so there is nothing for a hook, composable, or service to wrap, the same reason
91
- [`@symbiote-native/local-auth`](../local-auth) does the same. `./angular` stays a physical
92
- file/subpath since Angular ships through a separate `ngc`/AOT build (`build-ngc/`). Import from
93
- `@symbiote-native/secure-store` directly if you don't care which adapter you're on; the
94
- per-adapter subpaths exist so every wrapper package has the same import surface.
84
+ `./react`, `./vue`, `./svelte` and `./solid` are `exports`-map aliases onto `src/core/`. Upstream
85
+ ships free functions and constants with no per-instance state and no event stream, so there is
86
+ nothing for a hook, composable or service to wrap (same as
87
+ [`@symbiote-native/local-auth`](../local-auth)). `./angular` stays a physical subpath because
88
+ Angular ships through a separate `ngc`/AOT build (`build-ngc/`). Import from
89
+ `@symbiote-native/secure-store` directly if you do not care which adapter you are on.
95
90
 
96
91
  ## Use it
97
92
 
93
+ Save, read and delete a token:
94
+
98
95
  ```ts
99
96
  import * as SecureStore from '@symbiote-native/secure-store';
100
97
 
@@ -103,7 +100,7 @@ const stored = await SecureStore.getItemAsync('session-token'); // string | null
103
100
  await SecureStore.deleteItemAsync('session-token');
104
101
  ```
105
102
 
106
- Behind the device's own biometrics or passcode:
103
+ Gate the value behind the device's biometrics or passcode:
107
104
 
108
105
  ```ts
109
106
  if (SecureStore.canUseBiometricAuthentication()) {
@@ -116,44 +113,63 @@ if (SecureStore.canUseBiometricAuthentication()) {
116
113
 
117
114
  The prompt fires at different moments per platform: Android authenticates on every operation, iOS
118
115
  only when reading or updating an entry that already exists. A simulator or emulator does not
119
- enforce it at all — this option can only be verified on a real device.
116
+ enforce it, so verify this option on a real device.
120
117
 
121
- Values are strings. JSON-encode anything else:
118
+ Values are strings. A missing key reads `null`, so guard before parsing:
122
119
 
123
120
  ```ts
124
121
  await SecureStore.setItemAsync('profile', JSON.stringify(profile));
125
- const profile = JSON.parse(
126
- (await SecureStore.getItemAsync('profile')) ?? 'null',
127
- );
122
+ const profile = JSON.parse((await SecureStore.getItemAsync('profile')) ?? 'null');
128
123
  ```
129
124
 
130
125
  ## API
131
126
 
132
- | Export | Signature | Notes |
133
- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
134
- | `isAvailableAsync` | `() => Promise<boolean>` | `true` on Android and iOS. Says nothing about permissions. |
135
- | `getItemAsync` | `(key, options?) => Promise<string \| null>` | `null` when there is no entry, or when the key has been invalidated. |
136
- | `getItem` | `(key, options?) => string \| null` | Blocks the JS thread. |
137
- | `setItemAsync` | `(key, value, options?) => Promise<void>` | Rejects if the value cannot be stored. |
138
- | `setItem` | `(key, value, options?) => void` | Blocks the JS thread. |
139
- | `deleteItemAsync` | `(key, options?) => Promise<void>` | |
140
- | `canUseBiometricAuthentication` | `() => boolean` | Whether `requireAuthentication` can be used at all. |
141
- | `AFTER_FIRST_UNLOCK`, `AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY`, `ALWAYS`, `ALWAYS_THIS_DEVICE_ONLY`, `WHEN_PASSCODE_SET_THIS_DEVICE_ONLY`, `WHEN_UNLOCKED`, `WHEN_UNLOCKED_THIS_DEVICE_ONLY` | `IKeychainAccessibilityConstant \| undefined` | Values for `options.keychainAccessible`. iOS only — Android's native module declares none of them, so they read `undefined` there. `ALWAYS` and `ALWAYS_THIS_DEVICE_ONLY` are deprecated upstream. |
127
+ | Export | Signature | Notes |
128
+ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
129
+ | `isAvailableAsync` | `() => Promise<boolean>` | `true` on Android and iOS. Says nothing about permissions. |
130
+ | `getItemAsync` | `(key, options?) => Promise<string \| null>` | `null` when there is no entry, or when the key has been invalidated. |
131
+ | `getItem` | `(key, options?) => string \| null` | Blocks the JS thread. |
132
+ | `setItemAsync` | `(key, value, options?) => Promise<void>` | Rejects if the value cannot be stored. |
133
+ | `setItem` | `(key, value, options?) => void` | Blocks the JS thread. |
134
+ | `deleteItemAsync` | `(key, options?) => Promise<void>` | Removes the entry. On iOS a missing key is ignored, not an error. |
135
+ | `canUseBiometricAuthentication` | `() => boolean` | Whether `requireAuthentication` can be used at all. |
136
+ | `AFTER_FIRST_UNLOCK`, `AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY`, `ALWAYS`, `ALWAYS_THIS_DEVICE_ONLY`, `WHEN_PASSCODE_SET_THIS_DEVICE_ONLY`, `WHEN_UNLOCKED`, `WHEN_UNLOCKED_THIS_DEVICE_ONLY` | `IKeychainAccessibilityConstant \| undefined` | Values for `options.keychainAccessible`. iOS only: Android's native module declares none of them, so they read `undefined` there. `ALWAYS` and `ALWAYS_THIS_DEVICE_ONLY` are deprecated upstream. |
142
137
 
143
138
  `ISecureStoreOptions`: `keychainService`, `requireAuthentication`, `authenticationPrompt`,
144
139
  `keychainAccessible` (iOS), `accessGroup` (iOS).
145
140
 
146
141
  ## Notes
147
142
 
148
- - **Keys are validated before the native call.** Alphanumerics plus `.`, `-` and `_`, non-empty —
149
- anything else throws with a readable message rather than failing inside the keychain.
143
+ - **Keys are validated before the native call.** Alphanumerics plus `.`, `-` and `_`, non-empty.
144
+ Anything else throws a readable error instead of failing inside the keychain.
145
+ - **Keep values small.** Expo does not enforce a limit, but the platform can reject large payloads:
146
+ historically iOS refused values above about 2048 bytes, and some Android devices fail near 4072
147
+ characters. The limit applies to the stringified value. Store a token or key here, not a document;
148
+ handle the rejection from `setItemAsync`.
150
149
  - **An invalidated key is gone for good.** The system invalidates entries stored with
151
150
  `requireAuthentication` whenever enrolled biometrics change (a new fingerprint, a re-registered
152
- face). `getItemAsync` then resolves `null` — treat it as "the user must sign in again", not as
153
- an error to retry.
151
+ face). `getItemAsync` then resolves `null`. Treat it as "the user must sign in again", not as an
152
+ error to retry.
154
153
  - **`requireAuthentication` does not combine with a shared `keychainService`.** The full behavior
155
- needs a freshly generated key, so reuse of a service already holding non-authenticated entries
154
+ needs a freshly generated key, so reusing a service that already holds non-authenticated entries
156
155
  gives partial behavior. Upstream documents the same limitation.
156
+ - **iOS Keychain entries can outlive an uninstall.** A reinstalled app may still read the previous
157
+ install's values. If that matters, write a first-run marker elsewhere and clear the store when it
158
+ is missing.
159
+
160
+ ## Common questions
161
+
162
+ - **Size limit?** Historically iOS refused values over about 2048 bytes; Expo does not enforce it.
163
+ Store large blobs in files and keep only a key here.
164
+ - **Old token after reinstall?** The iOS Keychain survives uninstall for the same bundle ID. Clear it
165
+ on first launch or use the `THIS_DEVICE_ONLY` variants.
166
+ - **`getItemAsync` returns `null`.** No entry, or the key was invalidated (biometrics changed).
167
+ - **Biometric prompt?** On iOS only when reading or updating an existing value.
168
+ - **Encrypted on Android?** SharedPreferences encrypted with the Android Keystore.
169
+
170
+ Sources: [Expo docs: SecureStore](https://docs.expo.dev/versions/latest/sdk/securestore/),
171
+ [expo/expo#4084](https://github.com/expo/expo/pull/4084),
172
+ [Expo SecureStore: Tokens, Limits, and the Uninstall Trap](https://www.shipnative.dev/blog/expo-secure-store).
157
173
 
158
174
  ## Test it
159
175
 
@@ -161,6 +177,6 @@ const profile = JSON.parse(
161
177
  pnpm vitest run packages/secure-store
162
178
  ```
163
179
 
164
- The core tests fake the native module in place of `requireNativeModule`'s runtime resolution —
165
- `ExpoSecureStore` only exists on a device, so a headless run would otherwise throw at import.
166
- Real storage, biometrics, and the backup rules can only be verified on a device or emulator.
180
+ The core tests fake the native module in place of `requireNativeModule`'s runtime resolution,
181
+ because `ExpoSecureStore` only exists on a device and a headless run would throw at import. Real
182
+ storage, biometrics and the backup rules can only be verified on a device or emulator.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/secure-store",
3
- "version": "3.0.2",
3
+ "version": "3.0.3",
4
4
  "description": "expo-secure-store wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, Svelte, Solid, and Angular adapters. Encrypted key-value storage in the iOS Keychain and the Android Keystore, optionally gated behind biometrics.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -71,7 +71,7 @@
71
71
  },
72
72
  "dependencies": {
73
73
  "expo-secure-store": "57.0.1",
74
- "expo-modules-core": "57.0.5"
74
+ "expo-modules-core": "57.0.20"
75
75
  },
76
76
  "peerDependencies": {
77
77
  "@angular/core": ">=20",
@@ -81,12 +81,12 @@
81
81
  "solid-js": ">=1.9.0",
82
82
  "svelte": ">=5.56.0",
83
83
  "vue": ">=3.5.0",
84
- "@symbiote-native/angular": "^3.1.2",
85
- "@symbiote-native/engine": "^1.3.1",
86
- "@symbiote-native/react": "^3.0.4",
87
- "@symbiote-native/solid": "^3.0.4",
88
- "@symbiote-native/svelte": "^3.0.4",
89
- "@symbiote-native/vue": "^3.0.4"
84
+ "@symbiote-native/angular": "^3.2.0",
85
+ "@symbiote-native/engine": "^1.5.0",
86
+ "@symbiote-native/react": "^3.2.0",
87
+ "@symbiote-native/solid": "^3.1.0",
88
+ "@symbiote-native/svelte": "^3.1.0",
89
+ "@symbiote-native/vue": "^3.2.0"
90
90
  },
91
91
  "peerDependenciesMeta": {
92
92
  "@symbiote-native/angular": {
@@ -134,13 +134,13 @@
134
134
  "solid-js": "^1.9.14",
135
135
  "svelte": "^5.56.0",
136
136
  "typescript": "~6.0.0",
137
- "@symbiote-native/angular": "3.1.2",
138
- "@symbiote-native/engine": "1.3.1",
139
- "@symbiote-native/react": "3.0.4",
140
- "@symbiote-native/solid": "3.0.4",
141
- "@symbiote-native/svelte": "3.0.4",
142
- "@symbiote-native/test-utils": "0.4.4",
143
- "@symbiote-native/vue": "3.0.4"
137
+ "@symbiote-native/angular": "3.2.0",
138
+ "@symbiote-native/engine": "1.5.0",
139
+ "@symbiote-native/react": "3.2.0",
140
+ "@symbiote-native/solid": "3.1.0",
141
+ "@symbiote-native/svelte": "3.1.0",
142
+ "@symbiote-native/test-utils": "0.4.6",
143
+ "@symbiote-native/vue": "3.2.0"
144
144
  },
145
145
  "scripts": {
146
146
  "typecheck": "tsc --build",