Skip to content

Commit

Permalink
feat: import a space into w3console (#309)
Browse files Browse the repository at this point in the history
Add an import flow to add a space from a user provided ucan that
delegates * capabilites on a space created by another agent.

Provides the w3console agent DID, so you can send it to the other party
to create a delegation _just for you_


https://user-images.githubusercontent.com/58871/215516054-de617762-2e2d-4ff1-b6f9-c20772b46327.mov




License: MIT
Signed-off-by: Oli Evans <[email protected]>
  • Loading branch information
olizilla authored Feb 6, 2023
1 parent 46fa273 commit a69a95b
Show file tree
Hide file tree
Showing 11 changed files with 356 additions and 162 deletions.
2 changes: 2 additions & 0 deletions examples/react/w3console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
"devDependencies": {
"@preact/preset-vite": "^2.4.0",
"@types/blueimp-md5": "^2.18.0",
"@ucanto/core": "^4.1.0",
"@ucanto/interface": "^4.1.0",
"autoprefixer": "^10.4.13",
"postcss": "^8.4.21",
"tailwindcss": "^3.2.4",
Expand Down
68 changes: 43 additions & 25 deletions examples/react/w3console/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ShareIcon } from '@heroicons/react/20/solid'
import md5 from 'blueimp-md5'
import '@w3ui/react/src/styles/all.css'
import { SpaceShare } from './share'
import { DIDKey } from '@ucanto/interface'

function SpaceRegistrar (): JSX.Element {
const [, { registerSpace }] = useKeyring()
Expand Down Expand Up @@ -64,8 +65,14 @@ function SpaceRegistrar (): JSX.Element {
)
}

function SpaceSection (): JSX.Element {
const [share, setShare] = useState(false)
interface SpaceSectionProps {
viewSpace: (did: string) => void
setShare: (share: boolean) => void
share: boolean
}

function SpaceSection (props: SpaceSectionProps): JSX.Element {
const { viewSpace, share, setShare } = props
const [{ space }] = useKeyring()
const [, { reload }] = useUploadsList()
// reload the uploads list when the space changes
Expand Down Expand Up @@ -93,7 +100,7 @@ function SpaceSection (): JSX.Element {

</header>
<div className='container mx-auto'>
{share && <SpaceShare />}
{share && <SpaceShare viewSpace={viewSpace} />}
{registered && !share && (
<>
<Uploader onUploadComplete={() => { void reload() }} />
Expand All @@ -102,7 +109,7 @@ function SpaceSection (): JSX.Element {
</div>
</>
)}
{!registered && (
{!registered && !share && (
<SpaceRegistrar />
)}
</div>
Expand All @@ -111,14 +118,11 @@ function SpaceSection (): JSX.Element {
}

function SpaceSelector (props: any): JSX.Element {
const [{ space: currentSpace, spaces }, { setCurrentSpace }] = useKeyring()
async function selectSpace (space: Space): Promise<void> {
await setCurrentSpace(space.did())
}
const { selected, setSelected, spaces } = props
return (
<div>
<h3 className='text-xs tracking-wider uppercase font-bold my-2 text-gray-400 font-mono'>Spaces</h3>
<SpaceFinder spaces={spaces} selected={currentSpace} setSelected={(space: Space) => { void selectSpace(space) }} />
<SpaceFinder spaces={spaces} selected={selected} setSelected={(space: Space) => { void setSelected(space.did()) }} />
</div>
)
}
Expand All @@ -132,26 +136,40 @@ export function Logo (): JSX.Element {
)
}

export function Layout (): JsxElement {
const [share, setShare] = useState(false)
const [{ space, spaces }, { setCurrentSpace }] = useKeyring()

function viewSpace (did: DIDKey): void {
setShare(false)
void setCurrentSpace(did)
}

return (
<div className='flex min-h-full w-full'>
<nav className='flex-none w-64 bg-gray-900 text-white px-4 pb-4 border-r border-gray-800'>
<div className='flex flex-col justify-between min-h-full'>
<div class='flex-none'>
<SpaceSelector selected={space} setSelected={viewSpace} spaces={spaces} />
</div>
<div>
<SpaceCreator className='mb-4' />
<Logo />
</div>
</div>
</nav>
<main className='grow bg-gray-dark text-white p-4'>
<SpaceSection viewSpace={viewSpace} share={share} setShare={setShare} />
</main>
</div>
)
}

export function App (): JSX.Element {
return (
<W3APIProvider uploadsListPageSize={20}>
<Authenticator className='h-full'>
<div className='flex min-h-full w-full'>
<nav className='flex-none w-64 bg-gray-900 text-white px-4 pb-4 border-r border-gray-800'>
<div className='flex flex-col justify-between min-h-full'>
<div class='flex-none'>
<SpaceSelector />
</div>
<div>
<SpaceCreator className='mb-4' />
<Logo />
</div>
</div>
</nav>
<main className='grow bg-gray-dark text-white p-4'>
<SpaceSection />
</main>
</div>
<Layout />
</Authenticator>
</W3APIProvider>
)
Expand Down
6 changes: 6 additions & 0 deletions examples/react/w3console/src/components/did-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import md5 from 'blueimp-md5'

export function DidIcon ({ did }: { did: string }): JSX.Element {
const src = `https://www.gravatar.com/avatar/${md5(did)}?d=identicon`
return <img title={did} src={src} className='w-10 hover:saturate-200 saturate-0 invert border-solid border-gray-500 border' />
}
82 changes: 78 additions & 4 deletions examples/react/w3console/src/share.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import { ChangeEvent, useState } from 'react'
import { useKeyring } from '@w3ui/react-keyring'
import * as DID from '@ipld/dag-ucan/did'
import { CarWriter } from '@ipld/car/writer'
import { CarReader } from '@ipld/car/reader'
import { importDAG } from '@ucanto/core/delegation'
import type { PropsWithChildren } from 'react'
import type { Delegation } from '@ucanto/interface'
import type { Delegation, DIDKey } from '@ucanto/interface'
import { DidIcon } from './components/did-icon'

function Header (props: PropsWithChildren): JSX.Element {
return <h3 className='font-semibold text-xs font-mono uppercase tracking-wider mb-4 text-gray-400'>{props.children}</h3>
Expand All @@ -27,10 +30,21 @@ export async function toCarBlob (delegation: Delegation): Promise<Blob> {
return car
}

export function SpaceShare (): JSX.Element {
const [, { createDelegation }] = useKeyring()
export async function toDelegation (car: Blob): Promise<Delegation> {
const blocks = []
const bytes = new Uint8Array(await car.arrayBuffer())
const reader = await CarReader.fromBytes(bytes)
for await (const block of reader.blocks()) {
blocks.push(block)
}
return importDAG(blocks)
}

export function SpaceShare ({ viewSpace }: { viewSpace: (did: DIDKey) => void }): JSX.Element {
const [{ agent }, { createDelegation, addSpace }] = useKeyring()
const [value, setValue] = useState('')
const [downloadUrl, setDownloadUrl] = useState('')
const [proof, setProof] = useState<Delegation>()

async function makeDownloadLink (input: string): Promise<void> {
let audience
Expand Down Expand Up @@ -68,6 +82,24 @@ export function SpaceShare (): JSX.Element {
return `did-${method}-${id?.substring(0, 10)}.ucan`
}

async function onImport (e: ChangeEvent<HTMLInputElement>): Promise<void> {
const input = e.target.files?.[0]
if (input === undefined) return
let delegation
try {
delegation = await toDelegation(input)
} catch (err) {
console.log(err)
return
}
try {
await addSpace(delegation)
setProof(delegation)
} catch (err) {
console.log(err)
}
}

return (
<div className='pt-12'>
<div className=''>
Expand All @@ -80,9 +112,51 @@ export function SpaceShare (): JSX.Element {
value={value}
onChange={onChange}
/>
<a className='w3ui-button text-center block w-52 opacity-30' style={{ opacity: downloadUrl !== '' ? '1' : '0.2' }} href={downloadUrl ?? ''} download={downloadName(downloadUrl !== '', value)}>Download UCAN</a>
<a className='w3ui-button text-center block w-52' style={{ opacity: downloadUrl !== '' ? '1' : '0.2' }} href={downloadUrl ?? ''} download={downloadName(downloadUrl !== '', value)}>Download UCAN</a>
</form>
</div>
<div className='mt-16 py-16 border-t border-gray-700'>
<Header>Import a space</Header>
<p className='mb-2'>Copy and paste your DID to your friend</p>
<div className='bg-opacity-10 bg-white font-mono text-sm py-2 px-3 rounded break-words max-w-4xl'>
{agent?.did()}
</div>
<div className='mt-8'>
<label className='w3ui-button text-center block w-52'>
Import UCAN
<input type='file' accept='.ucan,.car,application/vnd.ipfs.car' className='hidden' onChange={(e: ChangeEvent<HTMLInputElement>) => { void onImport(e) }} />
</label>
</div>
{proof !== undefined && (
<div className='mt-4 pt-4'>
<Header>Added</Header>
<div className='max-w-3xl border border-gray-700 shadow-xl'>
{proof.capabilities.map((cap, i) => (
<figure className='p-4 flex flex-row items-start gap-2' key={i}>
<DidIcon did={cap.with} />
<figcaption className='grow'>
<a href='#' onClick={() => viewSpace(cap.with)} className='block'>
<span className='block text-xl font-semibold leading-5 mb-1'>
{proof.facts.at(i)?.space.name ?? 'Untitled Space'}
</span>
<span className='block font-mono text-xs text-gray-500 truncate'>{cap.with}</span>
</a>
</figcaption>
<div>
<a
href='#'
className='font-sm font-semibold align-[-8px]'
onClick={() => viewSpace(cap.with)}
>
View
</a>
</div>
</figure>
))}
</div>
</div>
)}
</div>
</div>
)
}
2 changes: 1 addition & 1 deletion examples/solid/uploads-list/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@
"dependencies": {
"@w3ui/solid-keyring": "workspace:^",
"@w3ui/solid-uploads-list": "workspace:^",
"solid-js": "^1.5.1"
"solid-js": "^1.6.10"
}
}
28 changes: 14 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,41 +25,41 @@
},
"devDependencies": {
"@babel/core": "^7.20.12",
"@babel/plugin-transform-modules-commonjs": "^7.18.6",
"@babel/preset-env": "^7.19.0",
"@babel/plugin-transform-modules-commonjs": "^7.20.11",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@babel/register": "^7.18.9",
"@rollup/plugin-babel": "^5.3.1",
"@rollup/plugin-commonjs": "^22.0.2",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^14.0.0",
"@rollup/plugin-node-resolve": "^14.1.0",
"@rollup/plugin-replace": "^4.0.0",
"@types/jest": "^29.0.2",
"@types/jest": "^29.4.0",
"@types/jsdom": "^20.0.1",
"@types/react": "^18.0.18",
"@types/react": "^18.0.26",
"@ucanto/client": "^4.2.3",
"@ucanto/server": "^4.2.3",
"@ucanto/transport": "^4.2.3",
"@web-std/file": "^3.0.2",
"@web3-storage/capabilities": "^2.2.0",
"esm": "^3.2.25",
"fake-indexeddb": "^4.0.1",
"jsdom": "^21.0.0",
"jsdom": "^21.1.0",
"path": "^0.12.7",
"react": "^18.2.0",
"rollup": "^2.79.0",
"rollup": "^2.79.1",
"rollup-plugin-size": "^0.2.2",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-visualizer": "^5.8.1",
"serve": "^14.1.2",
"solid-js": "^1.5.6",
"rollup-plugin-visualizer": "^5.9.0",
"serve": "^14.2.0",
"solid-js": "^1.6.10",
"ts-node": "^10.9.1",
"ts-standard": "^12.0.1",
"typescript": "^4.8.2",
"vitest": "^0.27.0",
"ts-standard": "^12.0.2",
"typescript": "^4.9.4",
"vitest": "^0.27.3",
"vitest-environment-w3ui": "workspace:^",
"vue": "^3.2.39"
"vue": "^3.2.45"
},
"ts-standard": {
"ignore": [
Expand Down
4 changes: 4 additions & 0 deletions packages/keyring-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ export interface KeyringContextActions {
* the _current_ space as the resource.
*/
createDelegation: (audience: Principal, abilities: Abilities[], options: CreateDelegationOptions) => Promise<Delegation>
/**
* Import a proof that delegates `*` ability on a space to this agent
*/
addSpace: (proof: Delegation) => Promise<void>
}

export type CreateDelegationOptions = Omit<UCANOptions, 'audience'> & { audienceMeta?: AgentMeta }
Expand Down
3 changes: 2 additions & 1 deletion packages/react-keyring/src/Authenticator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ export const AuthenticatorContext = createContext<AuthenticatorContextValue>([
registerSpace: async () => { },
cancelRegisterSpace: () => { },
getProofs: async () => [],
createDelegation: async () => { throw new Error('missing keyring context provider') }
createDelegation: async () => { throw new Error('missing keyring context provider') },
addSpace: async () => { }
}
])

Expand Down
11 changes: 9 additions & 2 deletions packages/react-keyring/src/providers/Keyring.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export const keyringContextDefaultValue: KeyringContextValue = [
registerSpace: async () => { },
cancelRegisterSpace: () => { },
getProofs: async () => [],
createDelegation: async () => { throw new Error('missing keyring context provider') }
createDelegation: async () => { throw new Error('missing keyring context provider') },
addSpace: async () => { }
}
]

Expand Down Expand Up @@ -129,6 +130,11 @@ export function KeyringProvider ({ children, servicePrincipal, connection }: Key
})
}

const addSpace = async (proof: Delegation): Promise<void> => {
const agent = await getAgent()
await agent.importSpaceFromDelegation(proof)
}

const state = {
space,
spaces,
Expand All @@ -143,7 +149,8 @@ export function KeyringProvider ({ children, servicePrincipal, connection }: Key
cancelRegisterSpace,
setCurrentSpace,
getProofs,
createDelegation
createDelegation,
addSpace
}

return (
Expand Down
11 changes: 9 additions & 2 deletions packages/solid-keyring/src/providers/Keyring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export const AuthContext = createContext<KeyringContextValue>([
registerSpace: async () => { },
cancelRegisterSpace: () => { },
getProofs: async () => [],
createDelegation: async () => { throw new Error('missing keyring context provider') }
createDelegation: async () => { throw new Error('missing keyring context provider') },
addSpace: async () => { }
}
])

Expand Down Expand Up @@ -134,6 +135,11 @@ export const KeyringProvider: ParentComponent<KeyringProviderProps> = props => {
})
}

const addSpace = async (proof: Delegation): Promise<void> => {
const agent = await getAgent()
await agent.importSpaceFromDelegation(proof)
}

const actions = {
loadAgent,
unloadAgent,
Expand All @@ -143,7 +149,8 @@ export const KeyringProvider: ParentComponent<KeyringProviderProps> = props => {
cancelRegisterSpace,
setCurrentSpace,
getProofs,
createDelegation
createDelegation,
addSpace
}

return createComponent(AuthContext.Provider, {
Expand Down
Loading

0 comments on commit a69a95b

Please sign in to comment.