src/index.ts
/*
%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%
@@@@@% %@@@@@@% %@@@@@@@% %@@@@@
@@@@@ @@@@@% @@@@@@ @@@@@
@@@@@% @@@@@ %@@@@@ %@@@@@
@@@@@@% @@@@@ %@@% @@@@@ %@@@@@@
@@@@@@@ @@@@@ %@@@@% @@@@@ @@@@@@@
@@@@@@@ @@@@% @@@@@@ @@@@@ @@@@@@@
@@@@@@@ %@@@@ @@@@@@ @@@@@% @@@@@@@
@@@@@@@ @@@@@ @@@@@@ %@@@@@ @@@@@@@
@@@@@@@ @@@@@@@@@@@@@@@@ @@@@@ @@@@@@@
@@@@@@@ %@@@@@@@@@@@@@@@ @@@@% @@@@@@@
@@@@@@@ %@@% @@@@@@ %@@% @@@@@@@
@@@@@@@ @@@@@@ @@@@@@@
@@@@@@@% %@@@@@@% %@@@@@@@
@@@@@@@@@% %@@@@@@@@@@% %@@@@@@@@@
%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%
*/
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
}