fission-suite/webnative

View on GitHub
src/index.ts

Summary

Maintainability
A
0 mins
Test Coverage
/*

    %@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%
  @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%
@@@@@%     %@@@@@@%         %@@@@@@@%     %@@@@@
@@@@@       @@@@@%            @@@@@@       @@@@@
@@@@@%      @@@@@             %@@@@@      %@@@@@
@@@@@@%     @@@@@     %@@%     @@@@@     %@@@@@@
@@@@@@@     @@@@@    %@@@@%    @@@@@     @@@@@@@
@@@@@@@     @@@@%    @@@@@@    @@@@@     @@@@@@@
@@@@@@@    %@@@@     @@@@@@    @@@@@%    @@@@@@@
@@@@@@@    @@@@@     @@@@@@    %@@@@@    @@@@@@@
@@@@@@@    @@@@@@@@@@@@@@@@     @@@@@    @@@@@@@
@@@@@@@    %@@@@@@@@@@@@@@@     @@@@%    @@@@@@@
@@@@@@@     %@@%     @@@@@@     %@@%     @@@@@@@
@@@@@@@              @@@@@@              @@@@@@@
@@@@@@@%            %@@@@@@%            %@@@@@@@
@@@@@@@@@%        %@@@@@@@@@@%        %@@@@@@@@@
%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%
  @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
    %@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%

 */

import * as Uint8arrays from "uint8arrays"
import localforage from "localforage"

import * as Auth from "./components/auth/implementation.js"
import * as CapabilitiesImpl from "./components/capabilities/implementation.js"
import * as Capabilities from "./capabilities.js"
import * as Crypto from "./components/crypto/implementation.js"
import * as Depot from "./components/depot/implementation.js"
import * as DID from "./did/local.js"
import * as Events from "./events.js"
import * as Extension from "./extension/index.js"
import * as FileSystemData from "./fs/data.js"
import * as IpfsNode from "./components/depot/implementation/ipfs/node.js"
import * as Manners from "./components/manners/implementation.js"
import * as Reference from "./components/reference/implementation.js"
import * as RootKey from "./common/root-key.js"
import * as Semver from "./common/semver.js"
import * as SessionMod from "./session.js"
import * as Storage from "./components/storage/implementation.js"
import * as Ucan from "./ucan/index.js"

import { SESSION_TYPE as CAPABILITIES_SESSION_TYPE } from "./capabilities.js"
import { TYPE as WEB_CRYPTO_SESSION_TYPE } from "./components/auth/implementation/base.js"
import { VERSION } from "./common/version.js"
import { AccountLinkingConsumer, AccountLinkingProducer, createConsumer, createProducer } from "./linking/index.js"
import { Components } from "./components.js"
import { Configuration, namespace } from "./configuration.js"
import { isString, Maybe } from "./common/index.js"
import { Session } from "./session.js"
import { loadFileSystem, recoverFileSystem } from "./filesystem.js"
import FileSystem from "./fs/filesystem.js"


// TYPES


import { type RecoverFileSystemParams } from "./fs/types/params.js"


// IMPLEMENTATIONS


import * as BaseAuth from "./components/auth/implementation/base.js"
import * as BaseReference from "./components/reference/implementation/base.js"
import * as BrowserCrypto from "./components/crypto/implementation/browser.js"
import * as BrowserStorage from "./components/storage/implementation/browser.js"
import * as FissionIpfsProduction from "./components/depot/implementation/fission-ipfs-production.js"
import * as FissionIpfsStaging from "./components/depot/implementation/fission-ipfs-staging.js"
import * as FissionAuthBaseProduction from "./components/auth/implementation/fission-base-production.js"
import * as FissionAuthBaseStaging from "./components/auth/implementation/fission-base-staging.js"
import * as FissionAuthWnfsProduction from "./components/auth/implementation/fission-wnfs-production.js"
import * as FissionAuthWnfsStaging from "./components/auth/implementation/fission-wnfs-staging.js"
import * as FissionLobbyBase from "./components/capabilities/implementation/fission-lobby.js"
import * as FissionLobbyProduction from "./components/capabilities/implementation/fission-lobby-production.js"
import * as FissionLobbyStaging from "./components/capabilities/implementation/fission-lobby-staging.js"
import * as FissionReferenceProduction from "./components/reference/implementation/fission-production.js"
import * as FissionReferenceStaging from "./components/reference/implementation/fission-staging.js"
import * as MemoryStorage from "./components/storage/implementation/memory.js"
import * as ProperManners from "./components/manners/implementation/base.js"


// RE-EXPORTS


export * from "./appInfo.js"
export * from "./components.js"
export * from "./configuration.js"
export * from "./common/cid.js"
export * from "./common/types.js"
export * from "./common/version.js"
export * from "./permissions.js"

export * as apps from "./apps/index.js"
export * as did from "./did/index.js"
export * as fission from "./common/fission.js"
export * as path from "./path/index.js"
export * as ucan from "./ucan/index.js"

export { AccountLinkingConsumer, AccountLinkingProducer } from "./linking/index.js"
export { FileSystem } from "./fs/filesystem.js"
export { Session } from "./session.js"



// TYPES & CONSTANTS


export type AuthenticationStrategy = {
  implementation: Auth.Implementation<Components>

  accountConsumer: (username: string) => Promise<AccountLinkingConsumer>
  accountProducer: (username: string) => Promise<AccountLinkingProducer>
  isUsernameAvailable: (username: string) => Promise<boolean>
  isUsernameValid: (username: string) => Promise<boolean>
  register: (options: { username: string; email?: string }) => Promise<{ success: boolean }>
  session: () => Promise<Maybe<Session>>
}


export type Program = ShortHands & Events.ListenTo<Events.All<Session>> & {
  /**
   * Authentication strategy, use this interface to register an account and link devices.
   */
  auth: AuthenticationStrategy

  capabilities: {
    /**
     * Collect capabilities.
     */
    collect: () => Promise<Maybe<string>> // returns username

    /**
     * Request capabilities.
     *
     * Permissions from your configuration are passed automatically,
     * but you can add additional permissions or override existing ones.
     */
    request: (options?: CapabilitiesImpl.RequestOptions) => Promise<void>

    /**
     * Try to create a `Session` based on capabilities.
     */
    session: (username: string) => Promise<Maybe<Session>>
  }

  /**
   * Configuration used to build this program.
   */
  configuration: Configuration

  /**
   * Components used to build this program.
   */
  components: Components

  /**
   * Various file system methods.
   */
  fileSystem: FileSystemShortHands

  /**
   * Existing session, if there is one.
   */
  session: Maybe<Session>
}


export enum ProgramError {
  InsecureContext = "INSECURE_CONTEXT",
  UnsupportedBrowser = "UNSUPPORTED_BROWSER"
}


export type ShortHands = {
  accountDID: (username: string) => Promise<string>
  agentDID: () => Promise<string>
  sharingDID: () => Promise<string>
}


export type FileSystemShortHands = {
  addPublicExchangeKey: (fs: FileSystem) => Promise<void>
  addSampleData: (fs: FileSystem) => Promise<void>
  hasPublicExchangeKey: (fs: FileSystem) => Promise<boolean>

  /**
   * Load the file system of a given username.
   */
  load: (username: string) => Promise<FileSystem>

  /**
   * Recover a file system.
   */
  recover: (params: RecoverFileSystemParams) => Promise<{ success: boolean }>
}



// ENTRY POINTS


/**
 * 🚀 Build an ODD program.
 *
 * This will give you a `Program` object which has the following properties:
 * - `session`, a `Session` object if a session was created before.
 * - `auth`, a means to control the various auth strategies you configured. Use this to create sessions. Read more about auth components in the toplevel `auth` object documention.
 * - `capabilities`, a means to control capabilities. Use this to collect & request capabilities, and to create a session based on them. Read more about capabilities in the toplevel `capabilities` object documentation.
 * - `components`, your full set of `Components`.
 *
 * This object also has a few other functions, for example to load a filesystem.
 * These are called "shorthands" because they're the same functions available
 * through other places in the ODD SDK, but you don't have to pass in the components.
 *
 * See `assemble` for more information. Note that this function checks for browser support,
 * while `assemble` does not. Use the latter in case you want to bypass the indexedDB check,
 * which might not be needed, or available, in certain environments or using certain components.
 */
export async function program(settings: Partial<Components> & Configuration): Promise<Program> {
  if (!settings) throw new Error("Expected a settings object of the type `Partial<Components> & Configuration` as the first parameter")

  // Check if the browser and context is supported
  if (globalThis.isSecureContext === false) throw ProgramError.InsecureContext
  if (await isSupported() === false) throw ProgramError.UnsupportedBrowser

  // Initialise components & assemble program
  const components = await gatherComponents(settings)
  return assemble(extractConfig(settings), components)
}



// PREDEFINED COMPONENTS


/**
 * Predefined auth configurations.
 *
 * This component goes hand in hand with the "reference" and "depot" components.
 * The "auth" component registers a DID and the reference looks it up.
 * The reference component also manages the "data root", the pointer to an account's entire filesystem.
 * The depot component is responsible for getting data to and from the other side.
 *
 * For example, using the Fission architecture, when a data root is updated on the Fission server,
 * the server fetches the data from the depot in your app.
 *
 * So if you want to build a service independent of Fission's infrastructure,
 * you will need to write your own reference and depot implementations (see source code).
 *
 * NOTE: If you're using a non-default component, you'll want to pass that in here as a parameter as well.
 *       Dependencies: crypto, manners, reference, storage.
 */
export const auth = {
  /**
   * A standalone authentication system that uses the browser's Web Crypto API
   * to create an identity based on a RSA key-pair.
   *
   * NOTE: This uses a Fission server to register an account (DID).
   *       Check out the `wnfs` and `base` auth implementations if
   *       you want to build something without the Fission infrastructure.
   */
  async fissionWebCrypto(settings: Configuration & {
    disableWnfs?: boolean
    staging?: boolean

    // Dependencies
    crypto?: Crypto.Implementation
    manners?: Manners.Implementation
    reference?: Reference.Implementation
    storage?: Storage.Implementation
  }): Promise<Auth.Implementation<Components>> {
    const { disableWnfs, staging } = settings

    const manners = settings.manners || defaultMannersComponent(settings)
    const crypto = settings.crypto || await defaultCryptoComponent(settings)
    const storage = settings.storage || defaultStorageComponent(settings)
    const reference = settings.reference || await defaultReferenceComponent({ crypto, manners, storage })

    if (disableWnfs) {
      if (staging) return FissionAuthBaseStaging.implementation({ crypto, reference, storage })
      return FissionAuthBaseProduction.implementation({ crypto, reference, storage })
    } else {
      if (staging) return FissionAuthWnfsStaging.implementation({ crypto, reference, storage })
      return FissionAuthWnfsProduction.implementation({ crypto, reference, storage })
    }
  }
}


/**
 * Predefined capabilities configurations.
 *
 * If you want partial read and/or write access to the filesystem you'll want
 * a "capabilities" component. This component is responsible for requesting
 * and receiving UCANs, read keys and namefilters from other sources to enable this.
 *
 * NOTE: If you're using a non-default component, you'll want to pass that in here as a parameter as well.
 *       Dependencies: crypto, depot.
 */
export const capabilities = {
  /**
   * A secure enclave in the form of a ODD app which serves as the root authority.
   * Your app is redirected to the lobby where the user can create an account or link a device,
   * and then request permissions from the user for reading or write to specific parts of the filesystem.
   */
  async fissionLobby(settings: Configuration & {
    staging?: boolean

    // Dependencies
    crypto?: Crypto.Implementation
  }): Promise<CapabilitiesImpl.Implementation> {
    const { staging } = settings
    const crypto = settings.crypto || await defaultCryptoComponent(settings)

    if (staging) return FissionLobbyStaging.implementation({ crypto })
    return FissionLobbyProduction.implementation({ crypto })
  }
}


/**
 * Predefined crypto configurations.
 *
 * The crypto component is responsible for various cryptographic operations.
 * This includes AES and RSA encryption & decryption, creating and storing
 * key pairs, verifying DIDs and defining their magic bytes, etc.
 */
export const crypto = {
  /**
   * The default crypto component, uses primarily the Web Crypto API and [keystore-idb](https://github.com/fission-codes/keystore-idb).
   * Keys are stored in a non-exportable way in indexedDB using the Web Crypto API.
   *
   * IndexedDB store is namespaced.
   */
  browser(settings: Configuration): Promise<Crypto.Implementation> {
    return defaultCryptoComponent(settings)
  }
}


/**
 * Predefined depot configurations.
 *
 * The depot component gets data in and out your program.
 * For example, say I want to load and then update a file system.
 * The depot will get that file system data for me,
 * and after updating it, send the data to where it needs to be.
 */
export const depot = {
  /**
   * This depot uses IPFS and the Fission servers.
   * The data is transferred to the Fission IPFS node,
   * where all of your encrypted and public data lives.
   * Other ODD programs with this depot fetch the data from there.
   */
  async fissionIPFS(
    settings: Configuration & {
      staging?: boolean

      // Dependencies
      storage?: Storage.Implementation
    }
  ): Promise<Depot.Implementation> {
    const repoName = `${namespace(settings)}/ipfs`
    const storage = settings.storage || defaultStorageComponent(settings)

    if (settings.staging) return FissionIpfsStaging.implementation({ storage }, repoName)
    return FissionIpfsProduction.implementation({ storage }, repoName)
  }
}


/**
 * Predefined manners configurations.
 *
 * The manners component allows you to tweak various behaviours of an ODD program,
 * such as logging and file system hooks (eg. what to do after a new file system is created).
 */
export const manners = {
  /**
   * The default ODD SDK behaviour.
   */
  default(settings: Configuration): Manners.Implementation {
    return defaultMannersComponent(settings)
  }
}


/**
 * Predefined reference configurations.
 *
 * The reference component is responsible for looking up and updating various pointers.
 * Specifically, the data root, a user's DID root, DNSLinks, DNS TXT records.
 * It also holds repositories (see `Repository` class), which contain UCANs and CIDs.
 *
 * NOTE: If you're using a non-default component, you'll want to pass that in here as a parameter as well.
 *       Dependencies: crypto, manners, storage.
 */
export const reference = {
  /**
   * Use the Fission servers as your reference.
   */
  async fission(settings: Configuration & {
    staging?: boolean

    // Dependencies
    crypto?: Crypto.Implementation
    manners?: Manners.Implementation
    storage?: Storage.Implementation
  }): Promise<Reference.Implementation> {
    const { staging } = settings

    const manners = settings.manners || defaultMannersComponent(settings)
    const crypto = settings.crypto || await defaultCryptoComponent(settings)
    const storage = settings.storage || defaultStorageComponent(settings)

    if (staging) return FissionReferenceStaging.implementation({ crypto, manners, storage })
    return FissionReferenceProduction.implementation({ crypto, manners, storage })
  }
}


/**
 * Predefined storage configuration.
 *
 * A key-value storage abstraction responsible for storing various
 * pieces of data, such as session data and UCANs.
 */
export const storage = {
  /**
   * IndexedDB through the `localForage` library, automatically namespaced.
   */
  browser(settings: Configuration): Storage.Implementation {
    return defaultStorageComponent(settings)
  },

  /**
   * In-memory store.
   */
  memory(): Storage.Implementation {
    return MemoryStorage.implementation()
  }
}



// ASSEMBLE


/**
 * Build an ODD Program based on a given set of `Components`.
 * These are various customisable components that determine how an ODD app works.
 * Use `program` to work with a default, or partial, set of components.
 *
 * Additionally this does a few other things:
 * - Restores a session if one was made before and loads the user's file system if needed.
 * - Attempts to collect capabilities if the configuration has permissions.
 * - Provides shorthands to functions so you don't have to pass in components.
 * - Ensure backwards compatibility with older ODD SDK clients.
 *
 * See the `program.fileSystem.load` function if you want to load the user's file system yourself.
 */
export async function assemble(config: Configuration, components: Components): Promise<Program> {
  const permissions = config.permissions

  // Backwards compatibility (data)
  await ensureBackwardsCompatibility(components, config)

  // Event emitters
  const fsEvents = Events.createEmitter<Events.FileSystem>()
  const sessionEvents = Events.createEmitter<Events.Session<Session>>()
  const allEvents = Events.merge(fsEvents, sessionEvents)

  // Authenticated user
  const sessionInfo = await SessionMod.restore(components.storage)

  // Auth implementations
  const auth: AuthenticationStrategy = (method => {
    return {
      implementation: method,

      accountConsumer(username: string) {
        return createConsumer(
          { auth: method, crypto: components.crypto, manners: components.manners },
          { username }
        )
      },

      accountProducer(username: string) {
        return createProducer(
          { auth: method, crypto: components.crypto, manners: components.manners },
          { username }
        )
      },

      isUsernameAvailable: method.isUsernameAvailable,
      isUsernameValid: method.isUsernameValid,
      register: method.register,

      async session(): Promise<Maybe<Session>> {
        const newSessionInfo = await SessionMod.restore(components.storage)
        if (!newSessionInfo) return null

        return this.implementation.session(
          components,
          newSessionInfo.username,
          config,
          { fileSystem: fsEvents, session: sessionEvents }
        )
      }
    }
  })(components.auth)

  // Capabilities
  const capabilities = {
    async collect() {
      const c = await components.capabilities.collect()
      if (!c) return null

      await Capabilities.collect({
        capabilities: c,
        crypto: components.crypto,
        reference: components.reference,
        storage: components.storage
      })

      return c.username
    },
    request(options?: CapabilitiesImpl.RequestOptions) {
      return components.capabilities.request({
        permissions,
        ...(options || {})
      })
    },
    async session(username: string) {
      const ucan = Capabilities.validatePermissions(
        components.reference.repositories.ucans,
        permissions || {}
      )

      if (!ucan) {
        console.warn("The present UCANs did not satisfy the configured permissions.")
        return null
      }

      const accountDID = await components.reference.didRoot.lookup(username)

      const validSecrets = await Capabilities.validateSecrets(
        components.crypto,
        accountDID,
        permissions || {}
      )

      if (!validSecrets) {
        console.warn("The present filesystem secrets did not satisfy the configured permissions.")
        return null
      }

      await SessionMod.provide(components.storage, { type: CAPABILITIES_SESSION_TYPE, username })

      const fs = config.fileSystem?.loadImmediately === false ?
        undefined :
        await loadFileSystem({
          config,
          dependencies: components,
          eventEmitter: fsEvents,
          username,
        })

      return new Session({
        fs,
        username,
        crypto: components.crypto,
        storage: components.storage,
        type: CAPABILITIES_SESSION_TYPE,
        eventEmitter: sessionEvents
      })
    }
  }

  // Session
  let session = null

  if (isCapabilityBasedAuthConfiguration(config)) {
    const username = await capabilities.collect()
    if (username) session = await capabilities.session(username)
    if (sessionInfo && sessionInfo.type === CAPABILITIES_SESSION_TYPE) session = await capabilities.session(sessionInfo.username)

  } else if (sessionInfo && sessionInfo.type !== CAPABILITIES_SESSION_TYPE) {
    session = await auth.session()

  }

  // Shorthands
  const shorthands = {
    // DIDs
    accountDID: (username: string) => components.reference.didRoot.lookup(username),
    agentDID: () => DID.agent(components.crypto),
    sharingDID: () => DID.sharing(components.crypto),

    // File system
    fileSystem: {
      addPublicExchangeKey: (fs: FileSystem) => FileSystemData.addPublicExchangeKey(components.crypto, fs),
      addSampleData: (fs: FileSystem) => FileSystemData.addSampleData(fs),
      hasPublicExchangeKey: (fs: FileSystem) => FileSystemData.hasPublicExchangeKey(components.crypto, fs),
      load: (username: string) => loadFileSystem({ config, username, dependencies: components, eventEmitter: fsEvents }),
      recover: (params: RecoverFileSystemParams) => recoverFileSystem({ auth, dependencies: components, ...params }),
    }
  }

  // Create `Program`
  const program = {
    ...shorthands,
    ...Events.listenTo(allEvents),

    configuration: { ...config },
    auth,
    components,
    capabilities,
    session,
  }

  // Inject into global context if necessary
  if (config.debug) {
    const inject = config.debugging?.injectIntoGlobalContext === undefined
      ? true
      : config.debugging?.injectIntoGlobalContext

    if (inject) {
      const container = globalThis as any
      container.__odd = container.__odd || {}
      container.__odd.programs = container.__odd.programs || {}
      container.__odd.programs[namespace(config)] = program
    }

    const emitMessages = config.debugging?.emitWindowPostMessages === undefined
      ? true
      : config.debugging?.emitWindowPostMessages

    if (emitMessages) {
      const { connect, disconnect } = await Extension.create({
        namespace: config.namespace,
        session,
        capabilities: config.permissions,
        dependencies: components,
        eventEmitters: {
          fileSystem: fsEvents,
          session: sessionEvents
        }
      })

      const container = globalThis as any
      container.__odd = container.__odd || {}
      container.__odd.extension = container.__odd.extension || {}
      container.__odd.extension.connect = connect
      container.__odd.extension.disconnect = disconnect

      // Notify extension that the ODD SDK is ready
      globalThis.postMessage({
        id: "odd-devtools-ready-message",
      })
    }
  }

  // Fin
  return program
}



// COMPOSITIONS


/**
 * Full component sets.
 */
export const compositions = {
  /**
   * The default Fission stack using web crypto auth.
   */
  async fission(settings: Configuration & {
    disableWnfs?: boolean
    staging?: boolean

    // Dependencies
    crypto?: Crypto.Implementation
    manners?: Manners.Implementation
    storage?: Storage.Implementation
  }): Promise<Components> {
    const crypto = settings.crypto || await defaultCryptoComponent(settings)
    const manners = settings.manners || defaultMannersComponent(settings)
    const storage = settings.storage || defaultStorageComponent(settings)

    const settingsWithComponents = { ...settings, crypto, manners, storage }

    const r = await reference.fission(settingsWithComponents)
    const d = await depot.fissionIPFS(settingsWithComponents)
    const c = await capabilities.fissionLobby(settingsWithComponents)
    const a = await auth.fissionWebCrypto({ ...settingsWithComponents, reference: r })

    return {
      auth: a,
      capabilities: c,
      depot: d,
      reference: r,
      crypto,
      manners,
      storage,
    }
  }
}


export async function gatherComponents(setup: Partial<Components> & Configuration): Promise<Components> {
  const config = extractConfig(setup)

  const crypto = setup.crypto || await defaultCryptoComponent(config)
  const manners = setup.manners || defaultMannersComponent(config)
  const storage = setup.storage || defaultStorageComponent(config)

  const reference = setup.reference || await defaultReferenceComponent({ crypto, manners, storage })
  const depot = setup.depot || await defaultDepotComponent({ storage }, config)
  const capabilities = setup.capabilities || defaultCapabilitiesComponent({ crypto })
  const auth = setup.auth || defaultAuthComponent({ crypto, reference, storage })

  return {
    auth,
    capabilities,
    crypto,
    depot,
    manners,
    reference,
    storage,
  }
}



// DEFAULT COMPONENTS


export function defaultAuthComponent({ crypto, reference, storage }: BaseAuth.Dependencies): Auth.Implementation<Components> {
  return FissionAuthWnfsProduction.implementation({
    crypto, reference, storage,
  })
}

export function defaultCapabilitiesComponent({ crypto }: FissionLobbyBase.Dependencies): CapabilitiesImpl.Implementation {
  return FissionLobbyProduction.implementation({ crypto })
}

export function defaultCryptoComponent(config: Configuration): Promise<Crypto.Implementation> {
  return BrowserCrypto.implementation({
    storeName: namespace(config),
    exchangeKeyName: "exchange-key",
    writeKeyName: "write-key"
  })
}

export function defaultDepotComponent({ storage }: IpfsNode.Dependencies, config: Configuration): Promise<Depot.Implementation> {
  return FissionIpfsProduction.implementation(
    { storage },
    `${namespace(config)}/ipfs`
  )
}

export function defaultMannersComponent(config: Configuration): Manners.Implementation {
  return ProperManners.implementation({
    configuration: config
  })
}

export function defaultReferenceComponent({ crypto, manners, storage }: BaseReference.Dependencies): Promise<Reference.Implementation> {
  return FissionReferenceProduction.implementation({
    crypto,
    manners,
    storage,
  })
}

export function defaultStorageComponent(config: Configuration): Storage.Implementation {
  return BrowserStorage.implementation({
    name: namespace(config)
  })
}



// 🛟


/**
 * Is this browser supported?
 */
export async function isSupported(): Promise<boolean> {
  return localforage.supports(localforage.INDEXEDDB)

    // Firefox in private mode can't use indexedDB properly,
    // so we test if we can actually make a database.
    && await (() => new Promise(resolve => {
      const db = indexedDB.open("testDatabase")
      db.onsuccess = () => resolve(true)
      db.onerror = () => resolve(false)
    }))() as boolean
}



// BACKWARDS COMPAT


async function ensureBackwardsCompatibility(components: Components, config: Configuration): Promise<void> {
  // Old pieces:
  // - Key pairs: IndexedDB → keystore → exchange-key & write-key
  // - UCAN used for account linking/delegation: IndexedDB → localforage → ucan
  // - Root read key of the filesystem: IndexedDB → localforage → readKey
  // - Authenticated username: IndexedDB → localforage → webnative.auth_username

  const [migK, migV] = ["migrated", VERSION]
  const currentVersion = Semver.fromString(VERSION)
  if (!currentVersion) throw new Error("The ODD SDK VERSION should be a semver string")

  // If already migrated, stop here.
  const migrationOccurred = await components.storage
    .getItem(migK)
    .then(v => typeof v === "string" ? Semver.fromString(v) : null)
    .then(v => v && Semver.isBiggerThanOrEqualTo(v, currentVersion))

  if (migrationOccurred) return

  // Only try to migrate if environment supports indexedDB
  if (!globalThis.indexedDB) return

  // Migration
  const existingDatabases = globalThis.indexedDB.databases
    ? (await globalThis.indexedDB.databases()).map(db => db.name)
    : ["keystore", "localforage"]

  const keystoreDB = existingDatabases.includes("keystore") ? await bwOpenDatabase("keystore") : null

  if (keystoreDB) {
    const exchangeKeyPair = await bwGetValue(keystoreDB, "keyvaluepairs", "exchange-key")
    const writeKeyPair = await bwGetValue(keystoreDB, "keyvaluepairs", "write-key")

    if (exchangeKeyPair && writeKeyPair) {
      await components.storage.setItem("exchange-key", exchangeKeyPair)
      await components.storage.setItem("write-key", writeKeyPair)
    }
  }

  const localforageDB = existingDatabases.includes("localforage") ? await bwOpenDatabase("localforage") : null

  if (localforageDB) {
    const accountUcan = await bwGetValue(localforageDB, "keyvaluepairs", "ucan")
    const permissionedUcans = await bwGetValue(localforageDB, "keyvaluepairs", "webnative.auth_ucans")
    const rootKey = await bwGetValue(localforageDB, "keyvaluepairs", "readKey")
    const authedUser = await bwGetValue(localforageDB, "keyvaluepairs", "webnative.auth_username")

    if (rootKey && isString(rootKey)) {
      const anyUcan = accountUcan || (Array.isArray(permissionedUcans) ? permissionedUcans[0] : undefined)
      const accountDID = anyUcan ? Ucan.rootIssuer(anyUcan) : (typeof authedUser === "string" ? await components.reference.didRoot.lookup(authedUser) : null)
      if (!accountDID) throw new Error("Failed to retrieve account DID")

      await RootKey.store({
        accountDID,
        crypto: components.crypto,
        readKey: Uint8arrays.fromString(rootKey, "base64pad"),
      })
    }

    if (accountUcan) {
      await components.storage.setItem(
        components.storage.KEYS.ACCOUNT_UCAN,
        accountUcan
      )
    }

    if (authedUser) {
      await components.storage.setItem(
        components.storage.KEYS.SESSION,
        JSON.stringify({
          type: isCapabilityBasedAuthConfiguration(config) ? CAPABILITIES_SESSION_TYPE : WEB_CRYPTO_SESSION_TYPE,
          username: authedUser
        })
      )
    }
  }

  await components.storage.setItem(migK, migV)
}


function bwGetValue(db: IDBDatabase, storeName: string, key: string): Promise<Maybe<unknown>> {
  return new Promise((resolve, reject) => {
    if (!db.objectStoreNames.contains(storeName)) return resolve(null)

    const transaction = db.transaction([storeName], "readonly")
    const store = transaction.objectStore(storeName)
    const req = store.get(key)

    req.onerror = () => {
      // No store, moving on.
      resolve(null)
    }

    req.onsuccess = () => {
      resolve(req.result)
    }
  })
}


function bwOpenDatabase(name: string): Promise<Maybe<IDBDatabase>> {
  return new Promise((resolve, reject) => {
    const req = globalThis.indexedDB.open(name)

    req.onerror = () => {
      // No database, moving on.
      resolve(null)
    }

    req.onsuccess = () => {
      resolve(req.result)
    }

    req.onupgradeneeded = e => {
      // Don't create database if it didn't exist before
      req.transaction?.abort()
      globalThis.indexedDB.deleteDatabase(name)
    }
  })
}



// 🛠


export function extractConfig(opts: Partial<Components> & Configuration): Configuration {
  return {
    namespace: opts.namespace,
    debug: opts.debug,
    fileSystem: opts.fileSystem,
    permissions: opts.permissions,
    userMessages: opts.userMessages,
  }
}


/**
 * Is this a configuration that uses capabilities?
 */
export function isCapabilityBasedAuthConfiguration(config: Configuration): boolean {
  return !!config.permissions
}