diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:35:49 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:35:49 +0000 |
commit | d8bbc7858622b6d9c278469aab701ca0b609cddf (patch) | |
tree | eff41dc61d9f714852212739e6b3738b82a2af87 /mobile/android/android-components/components/concept/sync | |
parent | Releasing progress-linux version 125.0.3-1~progress7.99u1. (diff) | |
download | firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.tar.xz firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.zip |
Merging upstream version 126.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/android-components/components/concept/sync')
8 files changed, 739 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/concept/sync/README.md b/mobile/android/android-components/components/concept/sync/README.md new file mode 100644 index 0000000000..787a6a3af2 --- /dev/null +++ b/mobile/android/android-components/components/concept/sync/README.md @@ -0,0 +1,26 @@ +# [Android Components](../../../README.md) > Concept > Sync + +The `concept-sync` component contains interfaces and types that describe various aspects of data synchronization. + +This abstraction makes it possible to create different implementations of synchronization backends, without tightly +coupling concrete implementations of storage, accounts and sync sub-systems. + +## Usage + +### Setting up the dependency + +Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)): + +```Groovy +implementation "org.mozilla.components:concept-sync:{latest-version}" +``` + +### Integration + +TODO + +## License + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/ diff --git a/mobile/android/android-components/components/concept/sync/build.gradle b/mobile/android/android-components/components/concept/sync/build.gradle new file mode 100644 index 0000000000..31a356155a --- /dev/null +++ b/mobile/android/android-components/components/concept/sync/build.gradle @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + defaultConfig { + minSdkVersion config.minSdkVersion + compileSdk config.compileSdkVersion + targetSdkVersion config.targetSdkVersion + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + namespace 'mozilla.components.concept.sync' +} + +dependencies { + // Necessary because we use 'suspend'. Fun fact: this module will compile just fine without this + // dependency, but it will crash at runtime. + // Included via 'api' because this module is unusable without coroutines. + api ComponentsDependencies.kotlin_coroutines + + // Observables are part of the public API of this module. + api project(':support-base') +} + +apply from: '../../../android-lint.gradle' +apply from: '../../../publish.gradle' +ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description) diff --git a/mobile/android/android-components/components/concept/sync/proguard-rules.pro b/mobile/android/android-components/components/concept/sync/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/mobile/android/android-components/components/concept/sync/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/mobile/android/android-components/components/concept/sync/src/main/AndroidManifest.xml b/mobile/android/android-components/components/concept/sync/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..e16cda1d34 --- /dev/null +++ b/mobile/android/android-components/components/concept/sync/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<manifest /> diff --git a/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/AccountEvent.kt b/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/AccountEvent.kt new file mode 100644 index 0000000000..fe46cc5b90 --- /dev/null +++ b/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/AccountEvent.kt @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.concept.sync + +/** + * Allows monitoring events targeted at the current account/device. + */ +interface AccountEventsObserver { + /** The callback called when an account event is received */ + fun onEvents(events: List<AccountEvent>) +} + +typealias OuterDeviceCommandIncoming = DeviceCommandIncoming + +/** + * Incoming account events. + */ +sealed class AccountEvent { + /** An incoming command from another device */ + data class DeviceCommandIncoming(val command: OuterDeviceCommandIncoming) : AccountEvent() + + /** The account's profile was updated */ + object ProfileUpdated : AccountEvent() + + /** The authentication state of the account changed - eg, the password changed */ + object AccountAuthStateChanged : AccountEvent() + + /** The account itself was destroyed */ + object AccountDestroyed : AccountEvent() + + /** Another device connected to the account */ + data class DeviceConnected(val deviceName: String) : AccountEvent() + + /** A device (possibly this one) disconnected from the account */ + data class DeviceDisconnected(val deviceId: String, val isLocalDevice: Boolean) : AccountEvent() + + /** An unknown account event. Should be gracefully ignore */ + object Unknown : AccountEvent() +} + +/** + * Incoming device commands (ie, targeted at the current device.) + */ +sealed class DeviceCommandIncoming { + /** A command to open a list of tabs on the current device */ + class TabReceived(val from: Device?, val entries: List<TabData>) : DeviceCommandIncoming() +} + +/** + * Outgoing device commands (ie, targeted at other devices.) + */ +sealed class DeviceCommandOutgoing { + /** A command to open a tab on another device */ + class SendTab(val title: String, val url: String) : DeviceCommandOutgoing() +} + +/** + * Information about a tab sent with tab related commands. + */ +data class TabData( + val title: String, + val url: String, +) diff --git a/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/Devices.kt b/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/Devices.kt new file mode 100644 index 0000000000..94b022ce20 --- /dev/null +++ b/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/Devices.kt @@ -0,0 +1,175 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.concept.sync + +import android.content.Context +import androidx.annotation.MainThread +import androidx.lifecycle.LifecycleOwner +import mozilla.components.support.base.observer.Observable + +/** + * Represents a result of interacting with a backend service which may return an authentication error. + */ +sealed class ServiceResult { + /** + * All good. + */ + object Ok : ServiceResult() + + /** + * Auth error. + */ + object AuthError : ServiceResult() + + /** + * Error that isn't auth. + */ + object OtherError : ServiceResult() +} + +/** + * Describes available interactions with the current device and other devices associated with an [OAuthAccount]. + */ +interface DeviceConstellation : Observable<AccountEventsObserver> { + /** + * Perform actions necessary to finalize device initialization based on [authType]. + * @param authType Type of an authentication event we're experiencing. + * @param config A [DeviceConfig] that describes current device. + * @return A boolean success flag. + */ + suspend fun finalizeDevice(authType: AuthType, config: DeviceConfig): ServiceResult + + /** + * Current state of the constellation. May be missing if state was never queried. + * @return [ConstellationState] describes current and other known devices in the constellation. + */ + fun state(): ConstellationState? + + /** + * Allows monitoring state of the device constellation via [DeviceConstellationObserver]. + * Use this to be notified of changes to the current device or other devices. + */ + @MainThread + fun registerDeviceObserver(observer: DeviceConstellationObserver, owner: LifecycleOwner, autoPause: Boolean) + + /** + * Set name of the current device. + * @param name New device name. + * @param context An application context, used for updating internal caches. + * @return A boolean success flag. + */ + suspend fun setDeviceName(name: String, context: Context): Boolean + + /** + * Set a [DevicePushSubscription] for the current device. + * @param subscription A new [DevicePushSubscription]. + * @return A boolean success flag. + */ + suspend fun setDevicePushSubscription(subscription: DevicePushSubscription): Boolean + + /** + * Send a command to a specified device. + * @param targetDeviceId A device ID of the recipient. + * @param outgoingCommand An event to send. + * @return A boolean success flag. + */ + suspend fun sendCommandToDevice(targetDeviceId: String, outgoingCommand: DeviceCommandOutgoing): Boolean + + /** + * Process a raw event, obtained via a push message or some other out-of-band mechanism. + * @param payload A raw, plaintext payload to be processed. + * @return A boolean success flag. + */ + suspend fun processRawEvent(payload: String): Boolean + + /** + * Refreshes [ConstellationState]. Registered [DeviceConstellationObserver] observers will be notified. + * + * @return A boolean success flag. + */ + suspend fun refreshDevices(): Boolean + + /** + * Polls for any pending [DeviceCommandIncoming] commands. + * In case of new commands, registered [AccountEventsObserver] observers will be notified. + * + * @return A boolean success flag. + */ + suspend fun pollForCommands(): Boolean +} + +/** + * Describes current device and other devices in the constellation. + */ +// N.B.: currentDevice should not be nullable. +// See https://github.com/mozilla-mobile/android-components/issues/8768 +data class ConstellationState(val currentDevice: Device?, val otherDevices: List<Device>) + +/** + * Allows monitoring constellation state. + */ +interface DeviceConstellationObserver { + fun onDevicesUpdate(constellation: ConstellationState) +} + +/** + * Describes a type of the physical device in the constellation. + */ +enum class DeviceType { + DESKTOP, + MOBILE, + TABLET, + TV, + VR, + UNKNOWN, +} + +/** + * Describes an Autopush-compatible push channel subscription. + */ +data class DevicePushSubscription( + val endpoint: String, + val publicKey: String, + val authKey: String, +) + +/** + * Configuration for the current device. + * + * @property name An initial name to use for the device record which will be created during authentication. + * This can be changed later via [DeviceConstellation.setDeviceName]. + * @property type Type of a device - mobile, desktop - used for displaying identifying icons on other devices. + * This cannot be changed once device record is created. + * @property capabilities A set of device capabilities, such as SEND_TAB. + * @property secureStateAtRest A flag indicating whether or not to use encrypted storage for the persisted account + * state. + */ +data class DeviceConfig( + val name: String, + val type: DeviceType, + val capabilities: Set<DeviceCapability>, + val secureStateAtRest: Boolean = false, +) + +/** + * Capabilities that a [Device] may have. + */ +enum class DeviceCapability { + SEND_TAB, +} + +/** + * Describes a device in the [DeviceConstellation]. + */ +data class Device( + val id: String, + val displayName: String, + val deviceType: DeviceType, + val isCurrentDevice: Boolean, + val lastAccessTime: Long?, + val capabilities: List<DeviceCapability>, + val subscriptionExpired: Boolean, + val subscription: DevicePushSubscription?, +) diff --git a/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/OAuthAccount.kt b/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/OAuthAccount.kt new file mode 100644 index 0000000000..7737d4bc36 --- /dev/null +++ b/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/OAuthAccount.kt @@ -0,0 +1,358 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.concept.sync + +import kotlinx.coroutines.Deferred + +/** + * An object that represents a login flow initiated by [OAuthAccount]. + * @property state OAuth state parameter, identifying a specific authentication flow. + * This string is randomly generated during [OAuthAccount.beginOAuthFlow] and [OAuthAccount.beginPairingFlow]. + * @property url Url which needs to be loaded to go through the authentication flow identified by [state]. + */ +data class AuthFlowUrl(val state: String, val url: String) + +/** + * Represents a specific type of an "in-flight" migration state that could result from intermittent + * issues during [OAuthAccount.migrateFromAccount]. + */ +enum class InFlightMigrationState(val reuseSessionToken: Boolean) { + /** + * "Copy" in-flight migration present. Can retry migration via [OAuthAccount.retryMigrateFromSessionToken]. + */ + COPY_SESSION_TOKEN(false), + + /** + * "Reuse" in-flight migration present. Can retry migration via [OAuthAccount.retryMigrateFromSessionToken]. + */ + REUSE_SESSION_TOKEN(true), +} + +/** + * Data structure describing FxA and Sync credentials necessary to sign-in into an FxA account. + */ +data class MigratingAccountInfo( + val sessionToken: String, + val kSync: String, + val kXCS: String, +) + +/** + * Representing all the possible entry points into FxA + * + * These entry points will be reflected in the authentication URL and will be tracked + * in server telemetry to allow studying authentication entry points independently. + * + * If you are introducing a new path to the firefox accounts sign in please add a new entry point + * here. + */ +interface FxAEntryPoint { + val entryName: String +} + +/** + * Facilitates testing consumers of FirefoxAccount. + */ +interface OAuthAccount : AutoCloseable { + + /** + * Constructs a URL used to begin the OAuth flow for the requested scopes and keys. + * + * @param scopes List of OAuth scopes for which the client wants access + * @param entryPoint The UI entryPoint used to start this flow. An arbitrary + * string which is recorded in telemetry by the server to help analyze the + * most effective touchpoints + * @return [AuthFlowUrl] if available, `null` in case of a failure + */ + suspend fun beginOAuthFlow( + scopes: Set<String>, + entryPoint: FxAEntryPoint, + ): AuthFlowUrl? + + /** + * Constructs a URL used to begin the pairing flow for the requested scopes and pairingUrl. + * + * @param pairingUrl URL string for pairing + * @param scopes List of OAuth scopes for which the client wants access + * @param entryPoint The UI entryPoint used to start this flow. An arbitrary + * string which is recorded in telemetry by the server to help analyze the + * most effective touchpoints + * @return [AuthFlowUrl] if available, `null` in case of a failure + */ + suspend fun beginPairingFlow( + pairingUrl: String, + scopes: Set<String>, + entryPoint: FxAEntryPoint, + ): AuthFlowUrl? + + /** + * Returns current FxA Device ID for an authenticated account. + * + * @return Current device's FxA ID, if available. `null` otherwise. + */ + fun getCurrentDeviceId(): String? + + /** + * Returns session token for an authenticated account. + * + * @return Current account's session token, if available. `null` otherwise. + */ + fun getSessionToken(): String? + + /** + * Fetches the profile object for the current client either from the existing cached state + * or from the server (requires the client to have access to the profile scope). + * + * @param ignoreCache Fetch the profile information directly from the server + * @return Profile (optional, if successfully retrieved) representing the user's basic profile info + */ + suspend fun getProfile(ignoreCache: Boolean = false): Profile? + + /** + * Authenticates the current account using the [code] and [state] parameters obtained via the + * OAuth flow initiated by [beginOAuthFlow]. + * + * Modifies the FirefoxAccount state. + * @param code OAuth code string + * @param state state token string + * @return Deferred boolean representing success or failure + */ + suspend fun completeOAuthFlow(code: String, state: String): Boolean + + /** + * Tries to fetch an access token for the given scope. + * + * @param singleScope Single OAuth scope (no spaces) for which the client wants access + * @return [AccessTokenInfo] that stores the token, along with its scope, key and + * expiration timestamp (in seconds) since epoch when complete + */ + suspend fun getAccessToken(singleScope: String): AccessTokenInfo? + + /** + * Call this whenever an authentication error was encountered while using an access token + * issued by [getAccessToken]. + */ + fun authErrorDetected() + + /** + * This method should be called when a request made with an OAuth token failed with an + * authentication error. It will re-build cached state and perform a connectivity check. + * + * In time, fxalib will grow a similar method, at which point we'll just relay to it. + * See https://github.com/mozilla/application-services/issues/1263 + * + * @param singleScope An oauth scope for which to check authorization state. + * @return An optional [Boolean] flag indicating if we're connected, or need to go through + * re-authentication. A null result means we were not able to determine state at this time. + */ + suspend fun checkAuthorizationStatus(singleScope: String): Boolean? + + /** + * Fetches the token server endpoint, for authentication using the SAML bearer flow. + * + * @return Token server endpoint URL string, `null` if it couldn't be obtained. + */ + suspend fun getTokenServerEndpointURL(): String? + + /** + * Fetches the URL for the user to manage their account + * + * @param entryPoint A string which will be included as a query param in the URL for metrics. + * @return The URL which should be opened in a browser tab. + */ + suspend fun getManageAccountURL(entryPoint: FxAEntryPoint): String? + + /** + * Get the pairing URL to navigate to on the Authority side (typically a computer). + * + * @return The URL to show the pairing user + */ + fun getPairingAuthorityURL(): String + + /** + * Registers a callback for when the account state gets persisted + * + * @param callback the account state persistence callback + */ + fun registerPersistenceCallback(callback: StatePersistenceCallback) + + /** + * Returns the device constellation for the current account + * + * @return Device constellation for the current account + */ + fun deviceConstellation(): DeviceConstellation + + /** + * Reset internal account state and destroy current device record. + * Use this when device record is no longer relevant, e.g. while logging out. On success, other + * devices will no longer see the current device in their device lists. + * + * @return A [Deferred] that will be resolved with a success flag once operation is complete. + * Failure indicates that we may have failed to destroy current device record. Nothing to do for + * the consumer; device record will be cleaned up eventually via TTL. + */ + suspend fun disconnect(): Boolean + + /** + * Serializes the current account's authentication state as a JSON string, for persistence in + * the Android KeyStore/shared preferences. The authentication state can be restored using + * [FirefoxAccount.fromJSONString]. + * + * @return String containing the authentication details in JSON format + */ + fun toJSONString(): String +} + +/** + * Describes a delegate object that is used by [OAuthAccount] to persist its internal state as it changes. + */ +interface StatePersistenceCallback { + /** + * @param data Account state representation as a string (e.g. as json). + */ + fun persist(data: String) +} + +sealed class AuthType { + /** + * Account restored from hydrated state on disk. + */ + object Existing : AuthType() + + /** + * Account created in response to a sign-in. + */ + object Signin : AuthType() + + /** + * Account created in response to a sign-up. + */ + object Signup : AuthType() + + /** + * Account created via pairing (similar to sign-in, but without requiring credentials). + */ + object Pairing : AuthType() + + /** + * Account was created for an unknown external reason, hopefully identified by [action]. + */ + data class OtherExternal(val action: String?) : AuthType() + + /** + * Account created via a shared account state from another app via the copy token flow. + */ + object MigratedCopy : AuthType() + + /** + * Account created via a shared account state from another app via the reuse token flow. + */ + object MigratedReuse : AuthType() + + /** + * Existing account was recovered from an authentication problem. + */ + object Recovered : AuthType() +} + +/** + * Different types of errors that may be encountered during authorization. + * Intermittent network problems are the most common reason for these errors. + */ +enum class AuthFlowError { + /** + * Couldn't begin authorization, i.e. failed to obtain an authorization URL. + */ + FailedToBeginAuth, + + /** + * Couldn't complete authorization after user entered valid credentials/paired correctly. + */ + FailedToCompleteAuth, +} + +/** + * Observer interface which lets its users monitor account state changes and major events. + * (XXX - there's some tension between this and the + * mozilla.components.concept.sync.AccountEvent we should resolve!) + */ +interface AccountObserver { + /** + * Account state has been resolved and can now be queried. + * + * @param authenticatedAccount Currently resolved authenticated account, if any. + */ + fun onReady(authenticatedAccount: OAuthAccount?) = Unit + + /** + * Account just got logged out. + */ + fun onLoggedOut() = Unit + + /** + * Account was successfully authenticated. + * + * @param account An authenticated instance of a [OAuthAccount]. + * @param authType Describes what kind of authentication event caused this invocation. + */ + fun onAuthenticated(account: OAuthAccount, authType: AuthType) = Unit + + /** + * Account's profile is now available. + * @param profile A fresh version of account's [Profile]. + */ + fun onProfileUpdated(profile: Profile) = Unit + + /** + * Account needs to be re-authenticated (e.g. due to a password change). + */ + fun onAuthenticationProblems() = Unit + + /** + * Encountered an error during an authentication or migration flow. + * @param error Exact error encountered. + */ + fun onFlowError(error: AuthFlowError) = Unit +} + +data class Avatar( + val url: String, + val isDefault: Boolean, +) + +data class Profile( + val uid: String?, + val email: String?, + val avatar: Avatar?, + val displayName: String?, +) + +/** + * Scoped key data. + * + * @property kid The JWK key identifier. + * @property k The JWK key data. + */ +data class OAuthScopedKey( + val kty: String, + val scope: String, + val kid: String, + val k: String, +) + +/** + * The result of authentication with FxA via an OAuth flow. + * + * @property token The access token produced by the flow. + * @property key An OAuthScopedKey if present. + * @property expiresAt The expiry date timestamp of this token since unix epoch (in seconds). + */ +data class AccessTokenInfo( + val scope: String, + val token: String, + val key: OAuthScopedKey?, + val expiresAt: Long, +) diff --git a/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/Sync.kt b/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/Sync.kt new file mode 100644 index 0000000000..51b24d0752 --- /dev/null +++ b/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/Sync.kt @@ -0,0 +1,53 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.concept.sync + +/** + * Results of running a sync via [SyncableStore.sync]. + */ +sealed class SyncStatus { + /** + * Sync succeeded successfully. + */ + object Ok : SyncStatus() + + /** + * Sync completed with an error. + */ + data class Error(val exception: Exception) : SyncStatus() +} + +/** + * A Firefox Sync friendly auth object which can be obtained from [OAuthAccount]. + * + * Why is there a Firefox Sync-shaped authentication object at the concept level, you ask? + * Mainly because this is what the [SyncableStore] consumes in order to actually perform + * synchronization, which is in turn implemented by `places`-backed storage layer. + * If this class lived in `services-firefox-accounts`, we'd end up with an ugly dependency situation + * between services and storage components. + * + * Turns out that building a generic description of an authentication/synchronization layer is not + * quite the way to go when you only have a single, legacy implementation. + * + * However, this may actually improve once we retire the tokenserver from the architecture. + * We could also consider a heavier use of generics, as well. + */ +data class SyncAuthInfo( + val kid: String, + val fxaAccessToken: String, + val fxaAccessTokenExpiresAt: Long, + val syncKey: String, + val tokenServerUrl: String, +) + +/** + * Describes a "sync" entry point for a storage layer. + */ +interface SyncableStore { + /** + * Registers this storage with a sync manager. + */ + fun registerWithSyncManager() +} |