@@ -99,13 +99,125 @@ export {
9999 */
100100const mockStorage : Record < string , string > = { } ;
101101
102+ const FALLBACK_CAPABILITIES : DeviceCapabilities = {
103+ secureEnclave : false ,
104+ strongBox : false ,
105+ biometry : false ,
106+ deviceCredential : false ,
107+ iCloudSync : false ,
108+ } ;
109+
110+ type NativeSetOptions = StorageOptions & { service : string } ;
111+ type NativeGetOptions = RetrievalOptions & { service : string } ;
112+ type NativeScopedOptions = { service : string } ;
113+
114+ type NativeSensitiveInfoModule = {
115+ setItem ?: (
116+ key : string ,
117+ value : string ,
118+ options : NativeSetOptions
119+ ) => Promise < OperationResult > ;
120+ getItem ?: (
121+ key : string ,
122+ options : NativeGetOptions
123+ ) => Promise < { value ?: string | null } | null > ;
124+ hasItem ?: ( key : string , options : NativeScopedOptions ) => Promise < boolean > ;
125+ deleteItem ?: ( key : string , options : NativeScopedOptions ) => Promise < void > ;
126+ getAllItems ?: ( options : NativeScopedOptions ) => Promise < string [ ] > ;
127+ clearService ?: ( options : NativeScopedOptions ) => Promise < void > ;
128+ getSupportedSecurityLevels ?: ( ) => Promise < DeviceCapabilities > ;
129+ } ;
130+
131+ type NativeMethod = keyof NativeSensitiveInfoModule ;
132+
133+ interface NativeInvocationResult < T > {
134+ readonly didInvoke : boolean ;
135+ readonly result ?: T ;
136+ }
137+
138+ interface KeychainScopedOptions {
139+ readonly keychainService ?: string ;
140+ }
141+
142+ function resolveService ( options ?: KeychainScopedOptions ) : string {
143+ return options ?. keychainService || DEFAULT_KEYCHAIN_SERVICE ;
144+ }
145+
146+ async function invokeNative < T > (
147+ nativeModule : NativeSensitiveInfoModule | null ,
148+ method : NativeMethod ,
149+ args : unknown [ ]
150+ ) : Promise < NativeInvocationResult < T > > {
151+ const candidate = nativeModule ?. [ method ] ;
152+
153+ if ( typeof candidate !== 'function' ) {
154+ return { didInvoke : false } ;
155+ }
156+
157+ const result = await ( candidate as ( ...innerArgs : unknown [ ] ) => Promise < T > ) (
158+ ...args
159+ ) ;
160+
161+ return { didInvoke : true , result } ;
162+ }
163+
164+ function fallbackSetItem (
165+ service : string ,
166+ key : string ,
167+ value : string ,
168+ accessControl ?: StorageOptions [ 'accessControl' ]
169+ ) : OperationResult {
170+ const storageKey = createStorageKey ( service , key ) ;
171+ mockStorage [ storageKey ] = value ;
172+
173+ return {
174+ metadata : {
175+ timestamp : Math . floor ( Date . now ( ) / 1000 ) ,
176+ securityLevel : 'software' ,
177+ accessControl : accessControl ?? DEFAULT_ACCESS_CONTROL ,
178+ backend : 'androidKeystore' ,
179+ } ,
180+ } ;
181+ }
182+
183+ function fallbackGetItem ( service : string , key : string ) : string | null {
184+ const storageKey = createStorageKey ( service , key ) ;
185+ return mockStorage [ storageKey ] ?? null ;
186+ }
187+
188+ function fallbackHasItem ( service : string , key : string ) : boolean {
189+ const storageKey = createStorageKey ( service , key ) ;
190+ return storageKey in mockStorage ;
191+ }
192+
193+ function fallbackDeleteItem ( service : string , key : string ) : void {
194+ const storageKey = createStorageKey ( service , key ) ;
195+ delete mockStorage [ storageKey ] ;
196+ }
197+
198+ function fallbackGetAllItems ( service : string ) : string [ ] {
199+ const prefix = createStorageKey ( service , '' ) ;
200+ return Object . keys ( mockStorage )
201+ . filter ( ( key ) => key . startsWith ( prefix ) )
202+ . map ( ( key ) => key . substring ( prefix . length ) ) ;
203+ }
204+
205+ function fallbackClearService ( service : string ) : void {
206+ const prefix = createStorageKey ( service , '' ) ;
207+ Object . keys ( mockStorage ) . forEach ( ( key ) => {
208+ if ( key . startsWith ( prefix ) ) {
209+ delete mockStorage [ key ] ;
210+ }
211+ } ) ;
212+ }
213+
102214/**
103215 * Get native module instance
104216 * @private
105217 */
106- function getNativeModule ( ) {
218+ function getNativeModule ( ) : NativeSensitiveInfoModule | null {
107219 try {
108- return NativeModules . SensitiveInfo ;
220+ return NativeModules . SensitiveInfo as NativeSensitiveInfoModule ;
109221 } catch {
110222 return null ;
111223 }
@@ -119,7 +231,7 @@ function createStorageKey(service: string, key: string): string {
119231 return `${ service } :${ key } ` ;
120232}
121233
122- /**
234+ /*
123235 * Securely stores a value in encrypted storage with optional biometric protection
124236 *
125237 * The storage operation will:
@@ -177,30 +289,21 @@ export async function setItem(
177289 throw new Error ( 'Key and value are required' ) ;
178290 }
179291
180- const service = options ?. keychainService || DEFAULT_KEYCHAIN_SERVICE ;
292+ const service = resolveService ( options ) ;
181293 const nativeModule = getNativeModule ( ) ;
182294
183295 try {
184- if ( nativeModule ?. setItem ) {
185- // Use native implementation when available
186- return await nativeModule . setItem ( key , value , {
187- service,
188- ...options ,
189- } ) ;
296+ const { didInvoke, result } = await invokeNative < OperationResult > (
297+ nativeModule ,
298+ 'setItem' ,
299+ [ key , value , { service, ...options } ]
300+ ) ;
301+
302+ if ( didInvoke ) {
303+ return result as OperationResult ;
190304 }
191305
192- // Development/example fallback
193- const storageKey = createStorageKey ( service , key ) ;
194- mockStorage [ storageKey ] = value ;
195-
196- return {
197- metadata : {
198- timestamp : Math . floor ( Date . now ( ) / 1000 ) ,
199- securityLevel : 'software' ,
200- accessControl : options ?. accessControl || DEFAULT_ACCESS_CONTROL ,
201- backend : 'androidKeystore' ,
202- } ,
203- } ;
306+ return fallbackSetItem ( service , key , value , options ?. accessControl ) ;
204307 } catch ( error : any ) {
205308 const errorCode = error ?. code || ErrorCode . ENCRYPTION_FAILED ;
206309 const message = `Failed to store "${ key } ": ${ error ?. message } ` ;
@@ -297,23 +400,25 @@ export async function getItem(
297400 throw new Error ( 'Key is required' ) ;
298401 }
299402
300- const service = options ?. keychainService || DEFAULT_KEYCHAIN_SERVICE ;
403+ const service = resolveService ( options ) ;
301404 const nativeModule = getNativeModule ( ) ;
302405
303406 try {
304- if ( nativeModule ?. getItem ) {
305- // Use native implementation when available
306- const result = await nativeModule . getItem ( key , {
407+ const { didInvoke, result } = await invokeNative < {
408+ value ?: string | null ;
409+ } | null > ( nativeModule , 'getItem' , [
410+ key ,
411+ {
307412 service,
308413 ...options ,
309- } ) ;
310- // Extract the value field from the native result object
414+ } ,
415+ ] ) ;
416+
417+ if ( didInvoke ) {
311418 return result ?. value ?? null ;
312419 }
313420
314- // Development/example fallback
315- const storageKey = createStorageKey ( service , key ) ;
316- return mockStorage [ storageKey ] ?? null ;
421+ return fallbackGetItem ( service , key ) ;
317422 } catch ( error : any ) {
318423 const errorCode = error ?. code || ErrorCode . DECRYPTION_FAILED ;
319424 const message = `Failed to retrieve "${ key } ": ${ error ?. message } ` ;
@@ -364,17 +469,21 @@ export async function hasItem(
364469 throw new Error ( 'Key is required' ) ;
365470 }
366471
367- const service = options ?. keychainService || DEFAULT_KEYCHAIN_SERVICE ;
472+ const service = resolveService ( options ) ;
368473 const nativeModule = getNativeModule ( ) ;
369474
370475 try {
371- if ( nativeModule ?. hasItem ) {
372- return await nativeModule . hasItem ( key , { service } ) ;
476+ const { didInvoke, result } = await invokeNative < boolean > (
477+ nativeModule ,
478+ 'hasItem' ,
479+ [ key , { service } ]
480+ ) ;
481+
482+ if ( didInvoke ) {
483+ return Boolean ( result ) ;
373484 }
374485
375- // Development/example fallback
376- const storageKey = createStorageKey ( service , key ) ;
377- return storageKey in mockStorage ;
486+ return fallbackHasItem ( service , key ) ;
378487 } catch ( error : any ) {
379488 const message = `Failed to check "${ key } ": ${ error ?. message } ` ;
380489 throw Object . assign ( new Error ( message ) , {
@@ -424,18 +533,20 @@ export async function deleteItem(
424533 throw new Error ( 'Key is required' ) ;
425534 }
426535
427- const service = options ?. keychainService || DEFAULT_KEYCHAIN_SERVICE ;
536+ const service = resolveService ( options ) ;
428537 const nativeModule = getNativeModule ( ) ;
429538
430539 try {
431- if ( nativeModule ?. deleteItem ) {
432- await nativeModule . deleteItem ( key , { service } ) ;
540+ const { didInvoke } = await invokeNative < void > ( nativeModule , 'deleteItem' , [
541+ key ,
542+ { service } ,
543+ ] ) ;
544+
545+ if ( didInvoke ) {
433546 return ;
434547 }
435548
436- // Development/example fallback
437- const storageKey = createStorageKey ( service , key ) ;
438- delete mockStorage [ storageKey ] ;
549+ fallbackDeleteItem ( service , key ) ;
439550 } catch ( error : any ) {
440551 const message = `Failed to delete "${ key } ": ${ error ?. message } ` ;
441552 throw Object . assign ( new Error ( message ) , {
@@ -479,19 +590,21 @@ export async function deleteItem(
479590export async function getAllItems (
480591 options ?: Pick < StorageOptions , 'keychainService' >
481592) : Promise < string [ ] > {
482- const service = options ?. keychainService || DEFAULT_KEYCHAIN_SERVICE ;
593+ const service = resolveService ( options ) ;
483594 const nativeModule = getNativeModule ( ) ;
484595
485596 try {
486- if ( nativeModule ?. getAllItems ) {
487- return await nativeModule . getAllItems ( { service } ) ;
597+ const { didInvoke, result } = await invokeNative < string [ ] > (
598+ nativeModule ,
599+ 'getAllItems' ,
600+ [ { service } ]
601+ ) ;
602+
603+ if ( didInvoke ) {
604+ return result ?? [ ] ;
488605 }
489606
490- // Development/example fallback
491- const prefix = createStorageKey ( service , '' ) ;
492- return Object . keys ( mockStorage )
493- . filter ( ( k ) => k . startsWith ( prefix ) )
494- . map ( ( k ) => k . substring ( prefix . length ) ) ;
607+ return fallbackGetAllItems ( service ) ;
495608 } catch ( error : any ) {
496609 throw Object . assign ( new Error ( `Failed to list items: ${ error ?. message } ` ) , {
497610 code : ErrorCode . KEYSTORE_UNAVAILABLE ,
@@ -545,22 +658,21 @@ export async function getAllItems(
545658export async function clearService (
546659 options ?: Pick < StorageOptions , 'keychainService' >
547660) : Promise < void > {
548- const service = options ?. keychainService || DEFAULT_KEYCHAIN_SERVICE ;
661+ const service = resolveService ( options ) ;
549662 const nativeModule = getNativeModule ( ) ;
550663
551664 try {
552- if ( nativeModule ?. clearService ) {
553- await nativeModule . clearService ( { service } ) ;
665+ const { didInvoke } = await invokeNative < void > (
666+ nativeModule ,
667+ 'clearService' ,
668+ [ { service } ]
669+ ) ;
670+
671+ if ( didInvoke ) {
554672 return ;
555673 }
556674
557- // Development/example fallback
558- const prefix = createStorageKey ( service , '' ) ;
559- Object . keys ( mockStorage ) . forEach ( ( key ) => {
560- if ( key . startsWith ( prefix ) ) {
561- delete mockStorage [ key ] ;
562- }
563- } ) ;
675+ fallbackClearService ( service ) ;
564676 } catch ( error : any ) {
565677 const message = `Failed to clear service: ${ error ?. message } ` ;
566678 throw Object . assign ( new Error ( message ) , {
@@ -594,13 +706,23 @@ export async function clearService(
594706 * ```
595707 */
596708export async function getSupportedSecurityLevels ( ) : Promise < DeviceCapabilities > {
597- return {
598- secureEnclave : true ,
599- strongBox : true ,
600- biometry : false ,
601- deviceCredential : true ,
602- iCloudSync : false ,
603- } ;
709+ const nativeModule = getNativeModule ( ) ;
710+
711+ try {
712+ const { didInvoke, result } = await invokeNative < DeviceCapabilities > (
713+ nativeModule ,
714+ 'getSupportedSecurityLevels' ,
715+ [ ]
716+ ) ;
717+
718+ if ( didInvoke && result ) {
719+ return result ;
720+ }
721+
722+ return FALLBACK_CAPABILITIES ;
723+ } catch {
724+ return FALLBACK_CAPABILITIES ;
725+ }
604726}
605727
606728/**
0 commit comments