Skip to content

Commit

Permalink
feat: use w3up-client in keyring
Browse files Browse the repository at this point in the history
per #295 rework the keyring to use w3up-client rather than the lower level access library

I'd like to refactor these APIs soon, but for now I'm keeping them unchanged to minimize downstream changes
  • Loading branch information
travis committed Nov 13, 2023
1 parent 745aa82 commit 9e93aea
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 205 deletions.
10 changes: 8 additions & 2 deletions packages/keyring-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,16 @@
},
"homepage": "https://github.com/web3-storage/w3ui/tree/main/packages/keyring-core",
"dependencies": {
"@ipld/dag-ucan": "^3.2.0",
"@ucanto/client": "^9.0.0",
"@ucanto/interface": "^9.0.0",
"@ucanto/principal": "^9.0.0",
"@web3-storage/access": "^16.3.0",
"@web3-storage/did-mailto": "^2.0.2"
"@ucanto/transport": "^9.0.0",
"@web3-storage/access": "^17.0.0",
"@web3-storage/did-mailto": "^2.0.2",
"@web3-storage/filecoin-client": "^3.1.0",
"@web3-storage/upload-client": "^12.0.0",
"@web3-storage/w3up-client": "10.0.0"
},
"eslintConfig": {
"extends": [
Expand Down
134 changes: 23 additions & 111 deletions packages/keyring-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,83 +3,24 @@ import type {
Capability,
DID,
Proof,
Signer,
ConnectionView,
Principal,
Delegation,
UCANOptions
} from '@ucanto/interface'
import { Agent as AccessAgent, authorizeWaitAndClaim, getAccountPlan } from '@web3-storage/access/agent'
import { StoreIndexedDB } from '@web3-storage/access/stores/store-indexeddb'
import * as RSASigner from '@ucanto/principal/rsa'
import { StoreIndexedDB, getAccountPlan } from '@web3-storage/access'
import * as Ucanto from '@ucanto/interface'
import * as DidMailto from '@web3-storage/did-mailto'
import { fromEmail as mailtoDidFromEmail } from '@web3-storage/did-mailto'
import { Client, create as createW3UPClient } from '@web3-storage/w3up-client'
import * as W3Account from '@web3-storage/w3up-client/account'
import { Space } from '@web3-storage/w3up-client/space'
import { createServiceConf, ServiceConfig } from './service'

export { Abilities, AgentMeta, Service }
export const authorize = authorizeWaitAndClaim
export { Abilities, AgentMeta, Service, Client, Space, ServiceConfig }

const DB_NAME = 'w3ui'
const DB_STORE_NAME = 'keyring'
export const W3UI_ACCOUNT_LOCALSTORAGE_KEY = 'w3ui-account-email'
export type Agent = AccessAgent & { store: StoreIndexedDB }
export type PlanGetResult = Ucanto.Result<PlanGetSuccess, PlanGetFailure | Ucanto.Failure>
/**
* A Space is the core organizational structure of web3-storage,
* similar to a bucket in S3 but with some special properties.
*
* At its core, a Space is just a public/private keypair that
* that users can associate web3-storage uploads with. The keypair
* is stored locally in a user's browser and can be registered with
* web3-storage to enable uploads and allow for recovery of upload
* capabilities in case the keypair is lost.
*/
export class Space implements Principal {
#did: DID
#meta: Record<string, any>

constructor (did: DID, meta: Record<string, any> = {}) {
this.#did = did
this.#meta = meta
}

/**
* The given space name.
*/
name (): string | undefined {
return this.#meta.name != null ? String(this.#meta.name) : undefined
}

/**
* The DID of the space.
*/
did (): DID {
return this.#did
}

/**
* Whether the space has been registered with the service.
*/
registered (): boolean {
return Boolean(this.#meta.isRegistered)
}

/**
* User defined space metadata.
*/
meta (): Record<string, any> {
return this.#meta
}

/**
* Compares this space's DID to `space`'s DID, returns
* true if they are the same, false otherwise.
* If `space` is null or undefined, returns false since
* this space is neither.
*/
sameAs (space?: Space): boolean {
return this.did() === space?.did()
}
}

export interface KeyringContextState {
/**
Expand All @@ -91,9 +32,9 @@ export interface KeyringContextState {
*/
spaces: Space[]
/**
* The current user agent (this device).
* The w3up client representing the current user agent (this device).
*/
agent?: Signer
client?: Client
/**
* The account this device is authorized to act as. Currently just an email address.
*/
Expand Down Expand Up @@ -173,41 +114,12 @@ export type CreateDelegationOptions = Omit<UCANOptions, 'audience'> & {
audienceMeta?: AgentMeta
}

export interface ServiceConfig {
servicePrincipal?: Principal
connection?: ConnectionView<Service>
}

/**
* Convenience function for returning an agent's current Space.
* @param agent
* @returns the currently selected Space for the given agent
*/
export function getCurrentSpace (agent: Agent): Space | undefined {
const did = agent.currentSpace()
if (did == null) return
const meta = agent.spaces.get(did)
return new Space(did, meta)
}

/**
* Convenience function for returning all of an agent's Spaces.
* @param agent
* @returns all of the given agent's Spaces
*/
export function getSpaces (agent: Agent): Space[] {
const spaces: Space[] = []
for (const [did, meta] of agent.spaces.entries()) {
spaces.push(new Space(did, meta))
}
return spaces
}

/**
* Get plan of the account identified by the given email.
*/
export async function getPlan (agent: Agent, email: Email): Promise<Ucanto.Result<PlanGetSuccess, PlanGetFailure | Ucanto.Failure>> {
return await getAccountPlan(agent, DidMailto.fromEmail(email))
export async function getPlan (client: Client, email: Email): Promise<Ucanto.Result<PlanGetSuccess, PlanGetFailure | Ucanto.Failure>> {
const agent = client.agent
return await getAccountPlan(agent, mailtoDidFromEmail(email))
}

export interface CreateAgentOptions extends ServiceConfig {}
Expand All @@ -216,21 +128,21 @@ export interface CreateAgentOptions extends ServiceConfig {}
* Create an agent for managing identity. It uses RSA keys that are stored in
* IndexedDB as unextractable `CryptoKey`s.
*/
export async function createAgent (
export async function createClient (
options: CreateAgentOptions = {}
): Promise<Agent> {
): Promise<Client> {
const dbName = `${DB_NAME}${options.servicePrincipal != null ? '@' + options.servicePrincipal.did() : ''}`
const store = new StoreIndexedDB(dbName, {
dbVersion: 1,
dbStoreName: DB_STORE_NAME
})
const raw = await store.load()
if (raw != null) {
return Object.assign(AccessAgent.from(raw, { ...options, store }), { store })
}
const principal = await RSASigner.generate()
return Object.assign(
await AccessAgent.create({ principal }, { ...options, store }),
{ store }
)
return await createW3UPClient({
store, serviceConf: createServiceConf(options)
})
}

// @ts-expect-error remove once new w3up ships
export const useAccount = (client: Client, { email }: { email?: string }): W3Account.Account | undefined => {
const accounts = Object.values(W3Account.list(client))
return accounts.find((account) => account.toEmail() === email)
}
37 changes: 37 additions & 0 deletions packages/keyring-core/src/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { Service as AccessService } from '@web3-storage/access/types'
import type { Service as UploadService } from '@web3-storage/upload-client/types'
import type { StorefrontService } from '@web3-storage/filecoin-client/storefront'
import { connect } from '@ucanto/client'
import { CAR, HTTP } from '@ucanto/transport'
import type {
ConnectionView,
Principal
} from '@ucanto/interface'
import * as DID from '@ipld/dag-ucan/did'
import { ServiceConf } from '@web3-storage/w3up-client/dist/src/types'

type Service = AccessService & UploadService & StorefrontService

export interface ServiceConfig {
servicePrincipal?: Principal
connection?: ConnectionView<Service>
}

export function createServiceConf ({ servicePrincipal, connection }: ServiceConfig): ServiceConf {
const id = servicePrincipal != null ? servicePrincipal : DID.parse('did:web:web3.storage')
const serviceConnection = (connection != null)
? connection
: connect<Service>({
id,
codec: CAR.outbound,
channel: HTTP.open<Service>({
url: new URL('https://up.web3.storage'),
method: 'POST'
})
})
return {
access: serviceConnection,
upload: serviceConnection,
filecoin: serviceConnection
}
}
23 changes: 9 additions & 14 deletions packages/keyring-core/test/agent.spec.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
import { test, expect } from 'vitest'
import 'fake-indexeddb/auto'

import { createAgent } from '../src/index.js'
import { createClient } from '../src/index.js'

test('createAgent', async () => {
const agent = await createAgent()
expect(agent).toBeTruthy()
expect(agent.did().startsWith('did:key')).toBe(true)
expect(agent.spaces.size).to.eql(0)
test('createClient', async () => {
const client = await createClient()
expect(client).toBeTruthy()
expect(client.did().startsWith('did:key')).toBe(true)
expect(client.spaces().length).to.eql(0)
})

test('createSpace', async () => {
const agent = await createAgent()
const space = await agent.createSpace('test')
const client = await createClient()
const space = await client.createSpace('test')
expect(space).toBeTruthy()
expect(space.did.startsWith('did:key:')).toBe(true)
})

test('registerSpace fails if no current space is set', async () => {
const agent = await createAgent()
await expect(agent.registerSpace('[email protected]')).rejects.toThrowError()
expect(space.did().startsWith('did:key:')).toBe(true)
})
11 changes: 8 additions & 3 deletions packages/react-keyring/src/Authenticator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export const AuthenticatorContext = createContext<AuthenticatorContextValue>([
}
])

export const AgentLoader = ({
export const ClientLoader = ({
children
}: {
children: JSX.Element
Expand All @@ -85,6 +85,11 @@ export const AgentLoader = ({
return children
}

/**
* @deprecated use ClientLoader
*/
export const AgentLoader = ClientLoader

export type AuthenticatorRootOptions<T extends As = typeof Fragment> =
Options<T>
export type AuthenticatorRootProps<T extends As = typeof Fragment> = Props<
Expand Down Expand Up @@ -131,11 +136,11 @@ export const AuthenticatorRoot: Component<AuthenticatorRootProps> =
[state, actions, email, submitted, handleRegisterSubmit]
)
return (
<AgentLoader>
<ClientLoader>
<AuthenticatorContext.Provider value={value}>
{createElement(Fragment, props)}
</AuthenticatorContext.Provider>
</AgentLoader>
</ClientLoader>
)
})

Expand Down
Loading

0 comments on commit 9e93aea

Please sign in to comment.