@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.
- package/README.md +86 -70
- package/package.json +15 -15
package/README.md
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
# @symbiote-native/secure-store
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
28
|
-
|
|
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
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
|
50
|
-
| iOS | `AppDelegate.swift
|
|
51
|
-
| Android | `settings.gradle` / `app/build.gradle
|
|
52
|
-
| Android | `MainApplication.kt
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
[`@symbiote-native/expo-modules-link`](../expo-modules-link) from this package's
|
|
58
|
-
|
|
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
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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/
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
[`@symbiote-native/local-auth`](../local-auth)
|
|
92
|
-
|
|
93
|
-
`@symbiote-native/secure-store` directly if you
|
|
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
|
-
|
|
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
|
|
116
|
+
enforce it, so verify this option on a real device.
|
|
120
117
|
|
|
121
|
-
Values are strings.
|
|
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
|
|
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
|
-
|
|
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
|
|
153
|
-
|
|
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
|
|
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
|
|
166
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
85
|
-
"@symbiote-native/engine": "^1.
|
|
86
|
-
"@symbiote-native/react": "^3.0
|
|
87
|
-
"@symbiote-native/solid": "^3.0
|
|
88
|
-
"@symbiote-native/svelte": "^3.0
|
|
89
|
-
"@symbiote-native/vue": "^3.0
|
|
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.
|
|
138
|
-
"@symbiote-native/engine": "1.
|
|
139
|
-
"@symbiote-native/react": "3.0
|
|
140
|
-
"@symbiote-native/solid": "3.0
|
|
141
|
-
"@symbiote-native/svelte": "3.0
|
|
142
|
-
"@symbiote-native/test-utils": "0.4.
|
|
143
|
-
"@symbiote-native/vue": "3.0
|
|
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",
|