@symbiote-native/secure-store 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +145 -0
  3. package/build/angular/index.d.ts +1 -0
  4. package/build/angular/index.js +4 -0
  5. package/build/core/index.d.ts +2 -0
  6. package/build/core/index.js +1 -0
  7. package/build/core/native-module.d.ts +17 -0
  8. package/build/core/native-module.js +3 -0
  9. package/build/core/secure-store.d.ts +83 -0
  10. package/build/core/secure-store.js +137 -0
  11. package/build/core/types.d.ts +43 -0
  12. package/build/core/types.js +1 -0
  13. package/build/react/index.d.ts +1 -0
  14. package/build/react/index.js +5 -0
  15. package/build/vue/index.d.ts +1 -0
  16. package/build/vue/index.js +4 -0
  17. package/build-ngc/angular/index.d.ts +1 -0
  18. package/build-ngc/angular/index.js +5 -0
  19. package/build-ngc/angular/index.js.map +1 -0
  20. package/build-ngc/core/index.d.ts +2 -0
  21. package/build-ngc/core/index.js +2 -0
  22. package/build-ngc/core/index.js.map +1 -0
  23. package/build-ngc/core/native-module.d.ts +17 -0
  24. package/build-ngc/core/native-module.js +4 -0
  25. package/build-ngc/core/native-module.js.map +1 -0
  26. package/build-ngc/core/secure-store.d.ts +83 -0
  27. package/build-ngc/core/secure-store.js +138 -0
  28. package/build-ngc/core/secure-store.js.map +1 -0
  29. package/build-ngc/core/types.d.ts +43 -0
  30. package/build-ngc/core/types.js +2 -0
  31. package/build-ngc/core/types.js.map +1 -0
  32. package/native-link.json +21 -0
  33. package/package.json +107 -0
  34. package/src/angular/index.ts +4 -0
  35. package/src/core/index.ts +17 -0
  36. package/src/core/native-module.ts +31 -0
  37. package/src/core/secure-store.test.ts +146 -0
  38. package/src/core/secure-store.ts +177 -0
  39. package/src/core/types.ts +44 -0
  40. package/src/react/index.ts +5 -0
  41. package/src/vue/index.ts +4 -0
@@ -0,0 +1,138 @@
1
+ import { UnavailabilityError } from 'expo-modules-core';
2
+ import { expoSecureStore } from './native-module';
3
+ const NATIVE_MODULE_NAME = 'expo-secure-store';
4
+ // Alphanumerics plus ".", "-" and "_": the same set the native side validates against, so an
5
+ // invalid key fails here with a readable message instead of deep inside the keychain call.
6
+ const VALID_KEY_PATTERN = /^[\w.-]+$/;
7
+ /**
8
+ * The item cannot be read after a restart until the device has been unlocked once. Useful for
9
+ * data the app needs while the phone is locked.
10
+ * @platform ios
11
+ */
12
+ export const AFTER_FIRST_UNLOCK = expoSecureStore.AFTER_FIRST_UNLOCK;
13
+ /**
14
+ * Like `AFTER_FIRST_UNLOCK`, except the entry does not migrate to a new device when restoring
15
+ * from a backup.
16
+ * @platform ios
17
+ */
18
+ export const AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY = expoSecureStore.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY;
19
+ /**
20
+ * The item can always be read, locked device or not. The least secure option.
21
+ * @deprecated Use a level that offers some user protection, such as `AFTER_FIRST_UNLOCK`.
22
+ * @platform ios
23
+ */
24
+ export const ALWAYS = expoSecureStore.ALWAYS;
25
+ /**
26
+ * Like `ALWAYS`, except the entry does not migrate to a new device when restoring from a backup.
27
+ * @deprecated Use a level that offers some user protection, such as
28
+ * `AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY`.
29
+ * @platform ios
30
+ */
31
+ export const ALWAYS_THIS_DEVICE_ONLY = expoSecureStore.ALWAYS_THIS_DEVICE_ONLY;
32
+ /**
33
+ * Like `WHEN_UNLOCKED_THIS_DEVICE_ONLY`, except the user must have set a passcode to store an
34
+ * entry at all. Removing the passcode deletes the entry.
35
+ * @platform ios
36
+ */
37
+ export const WHEN_PASSCODE_SET_THIS_DEVICE_ONLY = expoSecureStore.WHEN_PASSCODE_SET_THIS_DEVICE_ONLY;
38
+ /**
39
+ * The item can only be read while the device is unlocked.
40
+ * @platform ios
41
+ */
42
+ export const WHEN_UNLOCKED = expoSecureStore.WHEN_UNLOCKED;
43
+ /**
44
+ * Like `WHEN_UNLOCKED`, except the entry does not migrate to a new device when restoring from a
45
+ * backup.
46
+ * @platform ios
47
+ */
48
+ export const WHEN_UNLOCKED_THIS_DEVICE_ONLY = expoSecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY;
49
+ /**
50
+ * Whether the SecureStore API is usable on this device. Says nothing about app permissions.
51
+ * Resolves `true` on Android and iOS.
52
+ */
53
+ export async function isAvailableAsync() {
54
+ return !!expoSecureStore.getValueWithKeyAsync;
55
+ }
56
+ /**
57
+ * Read the value stored under `key`.
58
+ *
59
+ * Resolves `null` when there is no entry for the key, or when the key has been invalidated —
60
+ * the system invalidates keys stored with `requireAuthentication` whenever enrolled biometrics
61
+ * change (a new fingerprint, a re-registered face), and an invalidated value can never be read
62
+ * again. Rejects if reading itself fails.
63
+ */
64
+ export async function getItemAsync(key, options = {}) {
65
+ ensureValidKey(key);
66
+ if (!expoSecureStore.getValueWithKeyAsync) {
67
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getItemAsync');
68
+ }
69
+ return expoSecureStore.getValueWithKeyAsync(key, options);
70
+ }
71
+ /**
72
+ * Read the value stored under `key`, synchronously.
73
+ *
74
+ * > Blocks the JavaScript thread. With `requireAuthentication` on, the app stays unresponsive
75
+ * > until the user authenticates.
76
+ */
77
+ export function getItem(key, options = {}) {
78
+ ensureValidKey(key);
79
+ if (!expoSecureStore.getValueWithKeySync) {
80
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getItem');
81
+ }
82
+ return expoSecureStore.getValueWithKeySync(key, options);
83
+ }
84
+ /**
85
+ * Store a key–value pair. Keys may contain alphanumeric characters, `.`, `-` and `_`.
86
+ * Rejects if the value cannot be stored on the device.
87
+ */
88
+ export async function setItemAsync(key, value, options = {}) {
89
+ ensureValidKey(key);
90
+ ensureValidValue(value);
91
+ if (!expoSecureStore.setValueWithKeyAsync) {
92
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'setItemAsync');
93
+ }
94
+ await expoSecureStore.setValueWithKeyAsync(value, key, options);
95
+ }
96
+ /**
97
+ * Store a key–value pair, synchronously.
98
+ *
99
+ * > Blocks the JavaScript thread. With `requireAuthentication` on, the app stays unresponsive
100
+ * > until the user authenticates.
101
+ */
102
+ export function setItem(key, value, options = {}) {
103
+ ensureValidKey(key);
104
+ ensureValidValue(value);
105
+ if (!expoSecureStore.setValueWithKeySync) {
106
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'setItem');
107
+ }
108
+ expoSecureStore.setValueWithKeySync(value, key, options);
109
+ }
110
+ /** Delete the value stored under `key`. Rejects if the value cannot be deleted. */
111
+ export async function deleteItemAsync(key, options = {}) {
112
+ ensureValidKey(key);
113
+ if (!expoSecureStore.deleteValueWithKeyAsync) {
114
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'deleteItemAsync');
115
+ }
116
+ await expoSecureStore.deleteValueWithKeyAsync(key, options);
117
+ }
118
+ /**
119
+ * Whether a value can be stored with `requireAuthentication` — `true` when the device supports
120
+ * biometric authentication and the enrolled method is strong enough.
121
+ */
122
+ export function canUseBiometricAuthentication() {
123
+ if (!expoSecureStore.canUseBiometricAuthentication) {
124
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'canUseBiometricAuthentication');
125
+ }
126
+ return expoSecureStore.canUseBiometricAuthentication();
127
+ }
128
+ function ensureValidKey(key) {
129
+ if (typeof key !== 'string' || !VALID_KEY_PATTERN.test(key)) {
130
+ throw new Error('Invalid key provided to SecureStore. Keys must not be empty and contain only alphanumeric characters, ".", "-", and "_".');
131
+ }
132
+ }
133
+ function ensureValidValue(value) {
134
+ if (typeof value !== 'string') {
135
+ throw new Error('Invalid value provided to SecureStore. Values must be strings; consider JSON-encoding your values if they are serializable.');
136
+ }
137
+ }
138
+ //# sourceMappingURL=secure-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secure-store.js","sourceRoot":"","sources":["../../src/core/secure-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * An iOS [`kSecAttrAccessible`](https://developer.apple.com/documentation/security/ksecattraccessible/)
3
+ * value, as exposed by the native module. Opaque on purpose — pass one of the constants this
4
+ * package exports rather than a literal.
5
+ */
6
+ export type IKeychainAccessibilityConstant = number;
7
+ export type ISecureStoreOptions = {
8
+ /**
9
+ * - Android: equivalent of the public/private key pair `Alias`.
10
+ * - iOS: the item's service, equivalent to [`kSecAttrService`](https://developer.apple.com/documentation/security/ksecattrservice/).
11
+ *
12
+ * An item stored with a `keychainService` needs the same one to be read back.
13
+ */
14
+ keychainService?: string;
15
+ /**
16
+ * Require the device's own authentication (biometrics or passcode) to reach the stored value.
17
+ * - Android: [`setUserAuthenticationRequired(true)`](https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder#setUserAuthenticationRequired(boolean)).
18
+ * - iOS: [`biometryCurrentSet`](https://developer.apple.com/documentation/security/secaccesscontrolcreateflags/2937192-biometrycurrentset).
19
+ *
20
+ * The two platforms prompt at different moments: Android authenticates on every operation,
21
+ * iOS only when reading or updating an existing value, never when creating one. The full
22
+ * behavior needs a freshly generated key, so it does not combine with a `keychainService`
23
+ * already used for non-authenticated entries.
24
+ *
25
+ * > Emulators and simulators do not enforce the prompt when retrieving a secret — testing this
26
+ * > option means testing on a real device.
27
+ */
28
+ requireAuthentication?: boolean;
29
+ /** Message shown to the user in the prompt raised by `requireAuthentication`. */
30
+ authenticationPrompt?: string;
31
+ /**
32
+ * When the stored entry is accessible, via iOS's `kSecAttrAccessible` property.
33
+ * @default WHEN_UNLOCKED
34
+ * @platform ios
35
+ */
36
+ keychainAccessible?: IKeychainAccessibilityConstant;
37
+ /**
38
+ * The [access group](https://developer.apple.com/documentation/security/sharing-access-to-keychain-items-among-a-collection-of-apps)
39
+ * the stored entry belongs to.
40
+ * @platform ios
41
+ */
42
+ accessGroup?: string;
43
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,21 @@
1
+ {
2
+ "android": {
3
+ "gradleProjectName": "expo-secure-store",
4
+ "modules": [
5
+ {
6
+ "importPath": "expo.modules.securestore.SecureStoreModule",
7
+ "className": "SecureStoreModule",
8
+ "nativeName": "ExpoSecureStore"
9
+ }
10
+ ],
11
+ "manifestApplicationAttributes": {
12
+ "android:fullBackupContent": "@xml/secure_store_backup_rules",
13
+ "android:dataExtractionRules": "@xml/secure_store_data_extraction_rules"
14
+ }
15
+ },
16
+ "ios": {
17
+ "infoPlistKeys": {
18
+ "NSFaceIDUsageDescription": "This app uses Face ID to unlock your stored credentials."
19
+ }
20
+ }
21
+ }
package/package.json ADDED
@@ -0,0 +1,107 @@
1
+ {
2
+ "name": "@symbiote-native/secure-store",
3
+ "version": "0.0.1",
4
+ "description": "expo-secure-store wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, and Angular adapters. Encrypted key-value storage in the iOS Keychain and the Android Keystore, optionally gated behind biometrics.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/OneEyed1366/symbiote-native.git",
9
+ "directory": "packages/secure-store"
10
+ },
11
+ "homepage": "https://github.com/OneEyed1366/symbiote-native/tree/master/packages/secure-store#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/OneEyed1366/symbiote-native/issues"
14
+ },
15
+ "author": "Andrey Prokopenko <psevdoproger@gmail.com>",
16
+ "type": "module",
17
+ "main": "./build/core/index.js",
18
+ "module": "./build/core/index.js",
19
+ "types": "./build/core/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./build/core/index.d.ts",
23
+ "default": "./build/core/index.js"
24
+ },
25
+ "./vue": {
26
+ "types": "./build/vue/index.d.ts",
27
+ "default": "./build/vue/index.js"
28
+ },
29
+ "./react": {
30
+ "types": "./build/react/index.d.ts",
31
+ "default": "./build/react/index.js"
32
+ },
33
+ "./angular": {
34
+ "types": "./build-ngc/angular/index.d.ts",
35
+ "react-native": "./build-ngc/angular/index.js",
36
+ "default": "./src/angular/index.ts"
37
+ }
38
+ },
39
+ "files": [
40
+ "src",
41
+ "build",
42
+ "build-ngc",
43
+ "native-link.json"
44
+ ],
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "dependencies": {
49
+ "expo-secure-store": "57.0.1",
50
+ "expo-modules-core": "57.0.5"
51
+ },
52
+ "peerDependencies": {
53
+ "@symbiote-native/engine": ">=0.1.7",
54
+ "@angular/core": ">=20",
55
+ "@vue/runtime-core": "^3.5.13",
56
+ "react": ">=19.0.0",
57
+ "react-native": ">=0.86",
58
+ "vue": ">=3.5.0",
59
+ "@symbiote-native/react": "0.2.8",
60
+ "@symbiote-native/angular": "0.6.1",
61
+ "@symbiote-native/vue": "0.3.8"
62
+ },
63
+ "peerDependenciesMeta": {
64
+ "@symbiote-native/angular": {
65
+ "optional": true
66
+ },
67
+ "@symbiote-native/react": {
68
+ "optional": true
69
+ },
70
+ "@symbiote-native/vue": {
71
+ "optional": true
72
+ },
73
+ "@angular/core": {
74
+ "optional": true
75
+ },
76
+ "@vue/runtime-core": {
77
+ "optional": true
78
+ },
79
+ "react": {
80
+ "optional": true
81
+ },
82
+ "vue": {
83
+ "optional": true
84
+ }
85
+ },
86
+ "devDependencies": {
87
+ "@angular/compiler": "^22",
88
+ "@angular/compiler-cli": "^22",
89
+ "@angular/core": "^22",
90
+ "@types/node": "^26.0.0",
91
+ "@types/react": "^19.2.0",
92
+ "@vue/runtime-core": "^3.5.13",
93
+ "react": "19.2.3",
94
+ "typescript": "~6.0.0",
95
+ "@symbiote-native/engine": "0.1.7",
96
+ "@symbiote-native/test-utils": "0.1.6",
97
+ "@symbiote-native/react": "0.2.8",
98
+ "@symbiote-native/angular": "0.6.1",
99
+ "@symbiote-native/vue": "0.3.8"
100
+ },
101
+ "scripts": {
102
+ "typecheck": "tsc --build",
103
+ "clean": "rm -rf build-ngc",
104
+ "ng:build": "pnpm run clean && ngc -p tsconfig.angular.json",
105
+ "format": "prettier --write \"src/**/*.{ts,tsx}\""
106
+ }
107
+ }
@@ -0,0 +1,4 @@
1
+ // @symbiote-native/secure-store/angular: the Angular entry over the framework-agnostic core.
2
+ // Same reasoning as the React/Vue entries — no per-instance state or event stream to wrap in a
3
+ // service, so this is a plain re-export.
4
+ export * from '../core';
@@ -0,0 +1,17 @@
1
+ export {
2
+ AFTER_FIRST_UNLOCK,
3
+ AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY,
4
+ ALWAYS,
5
+ ALWAYS_THIS_DEVICE_ONLY,
6
+ WHEN_PASSCODE_SET_THIS_DEVICE_ONLY,
7
+ WHEN_UNLOCKED,
8
+ WHEN_UNLOCKED_THIS_DEVICE_ONLY,
9
+ isAvailableAsync,
10
+ getItemAsync,
11
+ getItem,
12
+ setItemAsync,
13
+ setItem,
14
+ deleteItemAsync,
15
+ canUseBiometricAuthentication,
16
+ } from './secure-store';
17
+ export type { IKeychainAccessibilityConstant, ISecureStoreOptions } from './types';
@@ -0,0 +1,31 @@
1
+ import { requireNativeModule } from 'expo-modules-core';
2
+ import type { IKeychainAccessibilityConstant, ISecureStoreOptions } from './types';
3
+
4
+ const EXPO_SECURE_STORE_MODULE_NAME = 'ExpoSecureStore';
5
+
6
+ // Every member is optional — each call site checks for its presence before calling through and
7
+ // throws an UnavailabilityError itself, matching upstream's own per-platform capability checks
8
+ // rather than assuming the native module implements the whole surface.
9
+ //
10
+ // The seven accessibility constants are genuinely iOS-only: Android's SecureStoreModule declares
11
+ // no Constant() at all, so they read `undefined` there. That is why the exported constants carry
12
+ // `| undefined` — they are only ever passed back into an option iOS reads.
13
+ export type INativeSecureStoreModule = {
14
+ AFTER_FIRST_UNLOCK?: IKeychainAccessibilityConstant;
15
+ AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY?: IKeychainAccessibilityConstant;
16
+ ALWAYS?: IKeychainAccessibilityConstant;
17
+ ALWAYS_THIS_DEVICE_ONLY?: IKeychainAccessibilityConstant;
18
+ WHEN_PASSCODE_SET_THIS_DEVICE_ONLY?: IKeychainAccessibilityConstant;
19
+ WHEN_UNLOCKED?: IKeychainAccessibilityConstant;
20
+ WHEN_UNLOCKED_THIS_DEVICE_ONLY?: IKeychainAccessibilityConstant;
21
+ getValueWithKeyAsync?(key: string, options: ISecureStoreOptions): Promise<string | null>;
22
+ getValueWithKeySync?(key: string, options: ISecureStoreOptions): string | null;
23
+ setValueWithKeyAsync?(value: string, key: string, options: ISecureStoreOptions): Promise<boolean>;
24
+ setValueWithKeySync?(value: string, key: string, options: ISecureStoreOptions): boolean;
25
+ deleteValueWithKeyAsync?(key: string, options: ISecureStoreOptions): Promise<void>;
26
+ canUseBiometricAuthentication?(): boolean;
27
+ };
28
+
29
+ export const expoSecureStore = requireNativeModule<INativeSecureStoreModule>(
30
+ EXPO_SECURE_STORE_MODULE_NAME,
31
+ );
@@ -0,0 +1,146 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const FAKE_NATIVE_SECURE_STORE = {
4
+ AFTER_FIRST_UNLOCK: 1,
5
+ WHEN_UNLOCKED: 5,
6
+ getValueWithKeyAsync: vi.fn(async () => 'stored'),
7
+ getValueWithKeySync: vi.fn(() => 'stored'),
8
+ setValueWithKeyAsync: vi.fn(async () => true),
9
+ setValueWithKeySync: vi.fn(() => true),
10
+ deleteValueWithKeyAsync: vi.fn(async () => undefined),
11
+ canUseBiometricAuthentication: vi.fn(() => true),
12
+ };
13
+
14
+ // The real ExpoSecureStore native module only exists on device — resolving it via
15
+ // requireNativeModule() at import time would throw in this headless run, so the module-lookup
16
+ // file is faked in place of expo-modules-core's runtime resolution, the same pattern
17
+ // packages/local-auth/src/core/local-authentication.test.ts uses.
18
+ vi.mock('./native-module', () => ({
19
+ expoSecureStore: FAKE_NATIVE_SECURE_STORE,
20
+ }));
21
+
22
+ // expo-modules-core's real entry transitively imports 'react-native', whose Flow-typed source
23
+ // Vitest's Oxc transform can't parse — so only the members used as values are faked.
24
+ vi.mock('expo-modules-core', () => ({
25
+ UnavailabilityError: class UnavailabilityError extends Error {
26
+ constructor(moduleName: string, propertyName: string) {
27
+ super(`${propertyName} is not available on ${moduleName}`);
28
+ }
29
+ },
30
+ }));
31
+
32
+ const {
33
+ AFTER_FIRST_UNLOCK,
34
+ WHEN_UNLOCKED,
35
+ isAvailableAsync,
36
+ getItemAsync,
37
+ getItem,
38
+ setItemAsync,
39
+ setItem,
40
+ deleteItemAsync,
41
+ canUseBiometricAuthentication,
42
+ } = await import('./secure-store');
43
+
44
+ afterEach(() => {
45
+ vi.clearAllMocks();
46
+ });
47
+
48
+ describe('keychain accessibility constants', () => {
49
+ it('come from the native module rather than being hardcoded', () => {
50
+ expect(AFTER_FIRST_UNLOCK).toBe(1);
51
+ expect(WHEN_UNLOCKED).toBe(5);
52
+ });
53
+ });
54
+
55
+ describe('isAvailableAsync', () => {
56
+ it('reports availability from the presence of the native read method', async () => {
57
+ await expect(isAvailableAsync()).resolves.toBe(true);
58
+ });
59
+ });
60
+
61
+ describe('getItemAsync', () => {
62
+ it('passes the key and options straight through', async () => {
63
+ await expect(getItemAsync('token', { keychainService: 'auth' })).resolves.toBe('stored');
64
+ expect(FAKE_NATIVE_SECURE_STORE.getValueWithKeyAsync).toHaveBeenCalledWith('token', {
65
+ keychainService: 'auth',
66
+ });
67
+ });
68
+
69
+ it('defaults the options to an empty object', async () => {
70
+ await getItemAsync('token');
71
+ expect(FAKE_NATIVE_SECURE_STORE.getValueWithKeyAsync).toHaveBeenCalledWith('token', {});
72
+ });
73
+
74
+ it('throws an UnavailabilityError-shaped error when the native method is absent', async () => {
75
+ const { getValueWithKeyAsync: native } = FAKE_NATIVE_SECURE_STORE;
76
+ // @ts-expect-error -- simulating a platform where the native module has no such method
77
+ FAKE_NATIVE_SECURE_STORE.getValueWithKeyAsync = undefined;
78
+
79
+ await expect(getItemAsync('token')).rejects.toThrow(
80
+ 'getItemAsync is not available on expo-secure-store',
81
+ );
82
+
83
+ FAKE_NATIVE_SECURE_STORE.getValueWithKeyAsync = native;
84
+ });
85
+ });
86
+
87
+ describe('setItemAsync', () => {
88
+ it('passes value, key and options in the native argument order', async () => {
89
+ await setItemAsync('token', 'secret', { requireAuthentication: true });
90
+ expect(FAKE_NATIVE_SECURE_STORE.setValueWithKeyAsync).toHaveBeenCalledWith('secret', 'token', {
91
+ requireAuthentication: true,
92
+ });
93
+ });
94
+ });
95
+
96
+ describe('the synchronous pair', () => {
97
+ it('reads and writes through the sync native methods', () => {
98
+ expect(getItem('token')).toBe('stored');
99
+ setItem('token', 'secret');
100
+ expect(FAKE_NATIVE_SECURE_STORE.getValueWithKeySync).toHaveBeenCalledWith('token', {});
101
+ expect(FAKE_NATIVE_SECURE_STORE.setValueWithKeySync).toHaveBeenCalledWith(
102
+ 'secret',
103
+ 'token',
104
+ {},
105
+ );
106
+ });
107
+ });
108
+
109
+ describe('deleteItemAsync', () => {
110
+ it('delegates to the native module', async () => {
111
+ await deleteItemAsync('token');
112
+ expect(FAKE_NATIVE_SECURE_STORE.deleteValueWithKeyAsync).toHaveBeenCalledWith('token', {});
113
+ });
114
+ });
115
+
116
+ describe('canUseBiometricAuthentication', () => {
117
+ it('delegates to the native module', () => {
118
+ expect(canUseBiometricAuthentication()).toBe(true);
119
+ });
120
+ });
121
+
122
+ // Validation happens before the native call so a bad key fails with a readable message here
123
+ // rather than deep inside the keychain, and the native module is never handed junk.
124
+ describe('key and value validation', () => {
125
+ it.each([
126
+ ['', 'empty'],
127
+ ['has space', 'a space'],
128
+ ['sla/sh', 'a slash'],
129
+ ['💥', 'an emoji'],
130
+ ])('rejects %j (%s)', async key => {
131
+ await expect(getItemAsync(key)).rejects.toThrow(/Invalid key provided to SecureStore/);
132
+ expect(FAKE_NATIVE_SECURE_STORE.getValueWithKeyAsync).not.toHaveBeenCalled();
133
+ });
134
+
135
+ it.each(['token', 'com.example.token', 'my-token', 'my_token'])('accepts %j', async key => {
136
+ await expect(getItemAsync(key)).resolves.toBe('stored');
137
+ });
138
+
139
+ it('rejects a non-string value before reaching the native module', async () => {
140
+ // @ts-expect-error -- the guard exists precisely for callers without type checking
141
+ await expect(setItemAsync('token', { a: 1 })).rejects.toThrow(
142
+ /Values must be strings; consider JSON-encoding/,
143
+ );
144
+ expect(FAKE_NATIVE_SECURE_STORE.setValueWithKeyAsync).not.toHaveBeenCalled();
145
+ });
146
+ });