import * as plugins from './plugins.js'; export interface IFlexCredentialRuntimeStore { getEntry(accountArg: string): Promise; setEntry(accountArg: string, valueArg: Uint8Array): Promise; deleteEntry(accountArg: string): Promise; } export interface IFlexCredentialRuntimeStorePair< TKernelStore extends IFlexCredentialRuntimeStore, TSealedStore extends IFlexCredentialRuntimeStore, > { kernelStore: TKernelStore; sealedStore: TSealedStore; } export interface IFlexCredentialKernelStore extends IFlexCredentialRuntimeStore { readonly service: string; deleteEntryIfValue( accountArg: string, expectedValueArg: Uint8Array, ): Promise<'deleted' | 'alreadyAbsent'>; close(): Promise; } export interface IFlexCredentialSealedStore extends IFlexCredentialRuntimeStore { close(): Promise; } export interface IFlexCredentialStoreCreationOptions { service: string; storeId: string; directoryPath: string; } export interface IFlexCredentialSealedStoreCreationOptions< TKernelStore extends IFlexCredentialKernelStore, > { service: string; storeId: string; directoryPath: string; legacyKernelStore: TKernelStore; } export interface IFlexCredentialStoreFactories< TKernelStore extends IFlexCredentialKernelStore, TSealedStore extends IFlexCredentialSealedStore, > { createKernelStore(optionsArg: { service: string }): Promise; createSealedStore( optionsArg: IFlexCredentialSealedStoreCreationOptions, ): Promise; } interface IFlexCredentialRuntimeOperationOptions< TKernelStore extends IFlexCredentialRuntimeStore, TSealedStore extends IFlexCredentialRuntimeStore, > { stores: IFlexCredentialRuntimeStorePair; recreateStores: ( storesArg: IFlexCredentialRuntimeStorePair, ) => Promise>; } const maximumMutationAttempts = 2; const maximumStoreCreationAttempts = 2; const maximumStoreCleanupAttempts = 2; const isMutationOutcomeUnknown = (errorArg: unknown): boolean => ( errorArg instanceof plugins.smartsecret.SmartSecretKernelStoreError || errorArg instanceof plugins.smartsecret.SmartSecretSealedFileStoreError ) && errorArg.code === 'MUTATION_OUTCOME_UNKNOWN'; const closeFlexCredentialKernelStore = async ( kernelStoreArg: IFlexCredentialKernelStore, ): Promise => { const errors: unknown[] = []; for (let attempt = 0; attempt < maximumStoreCleanupAttempts; attempt += 1) { try { await kernelStoreArg.close(); return; } catch (errorArg) { errors.push(errorArg); } } throw new AggregateError(errors, 'Flex credential kernel store cleanup failed.'); }; export const createFlexCredentialStores = async < TKernelStore extends IFlexCredentialKernelStore, TSealedStore extends IFlexCredentialSealedStore, >( optionsArg: IFlexCredentialStoreCreationOptions, factoriesArg: IFlexCredentialStoreFactories, ): Promise> => { for (let attempt = 0; attempt < maximumStoreCreationAttempts; attempt += 1) { const kernelStore = await factoriesArg.createKernelStore({ service: optionsArg.service }); const sealedStoreOptions: IFlexCredentialSealedStoreCreationOptions = { service: optionsArg.service, storeId: optionsArg.storeId, directoryPath: optionsArg.directoryPath, legacyKernelStore: kernelStore, }; try { const sealedStore = await factoriesArg.createSealedStore(sealedStoreOptions); return { kernelStore, sealedStore }; } catch (errorArg) { try { await closeFlexCredentialKernelStore(kernelStore); } catch (closeErrorArg) { throw new AggregateError( [errorArg, closeErrorArg], 'Flex credential store creation cleanup failed.', ); } if (!isMutationOutcomeUnknown(errorArg) || attempt === maximumStoreCreationAttempts - 1) { throw errorArg; } } } throw new Error('Flex credential store creation exhausted its retry bound.'); }; const bytesEqual = (leftArg: Uint8Array, rightArg: Uint8Array): boolean => leftArg.byteLength === rightArg.byteLength && plugins.crypto.timingSafeEqual(leftArg, rightArg); const reconciliationError = (): Error => new Error('Flex provider credential mutation reconciliation failed.'); export const writeFlexProviderCredentialEntry = async < TKernelStore extends IFlexCredentialRuntimeStore, TSealedStore extends IFlexCredentialRuntimeStore, >( optionsArg: IFlexCredentialRuntimeOperationOptions, accountArg: string, bytesArg: Uint8Array, ): Promise> => { let stores = optionsArg.stores; let previousBytes = await stores.sealedStore.getEntry(accountArg); let requiresConfirmedRetry = false; try { for (let attempt = 0; attempt < maximumMutationAttempts; attempt += 1) { let outcomeWasUnknown = false; try { await stores.sealedStore.setEntry(accountArg, bytesArg); } catch (errorArg) { if (!isMutationOutcomeUnknown(errorArg)) throw errorArg; stores = await optionsArg.recreateStores(stores); outcomeWasUnknown = true; requiresConfirmedRetry = true; } const readback = await stores.sealedStore.getEntry(accountArg); try { if (readback && bytesEqual(readback, bytesArg)) { if (!requiresConfirmedRetry || !outcomeWasUnknown) return stores; previousBytes?.fill(0); previousBytes = readback; continue; } const unchanged = previousBytes ? Boolean(readback && bytesEqual(readback, previousBytes)) : readback === null; if (!outcomeWasUnknown || !unchanged || attempt === maximumMutationAttempts - 1) { throw reconciliationError(); } previousBytes?.fill(0); previousBytes = readback; } finally { if (readback !== previousBytes) readback?.fill(0); } } throw reconciliationError(); } finally { previousBytes?.fill(0); } }; export const deleteFlexProviderCredentialEntry = async < TKernelStore extends IFlexCredentialRuntimeStore, TSealedStore extends IFlexCredentialRuntimeStore, >( optionsArg: IFlexCredentialRuntimeOperationOptions, accountArg: string, ): Promise> => { let stores = optionsArg.stores; const previousBytes = await stores.sealedStore.getEntry(accountArg); let requiresConfirmedRetry = false; try { for (let attempt = 0; attempt < maximumMutationAttempts; attempt += 1) { let outcomeWasUnknown = false; try { await stores.sealedStore.deleteEntry(accountArg); } catch (errorArg) { if (!isMutationOutcomeUnknown(errorArg)) throw errorArg; stores = await optionsArg.recreateStores(stores); outcomeWasUnknown = true; requiresConfirmedRetry = true; } const readback = await stores.sealedStore.getEntry(accountArg); try { if (!readback) { if (!requiresConfirmedRetry || !outcomeWasUnknown) return stores; continue; } const unchanged = Boolean(previousBytes && bytesEqual(readback, previousBytes)); if (!outcomeWasUnknown || !unchanged || attempt === maximumMutationAttempts - 1) { throw reconciliationError(); } } finally { readback?.fill(0); } } throw reconciliationError(); } finally { previousBytes?.fill(0); } };