Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/doctypes #6

Merged
merged 7 commits into from
Apr 3, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10,308 changes: 10,225 additions & 83 deletions packages/ceramic-cli/package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion packages/ceramic-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@
"clean": "rm -rf ./lib"
},
"dependencies": {
"@ceramicnetwork/ceramic-core": "^0.0.0",
"@ceramicnetwork/ceramic-core": "^0.0.1",
"commander": "^4.1.1",
"express": "^4.17.1",
"identity-wallet": "file:../../../../3box/identity-wallet",
"ipfs-http-client": "^42.0.0",
"node-fetch": "^2.6.0"
},
Expand Down
8 changes: 8 additions & 0 deletions packages/ceramic-cli/src/@types/identity-wallet.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

declare module 'identity-wallet' {
export default class IdentityWallet {
constructor (getConsent: () => Promise<boolean>, opts: any)

get3idProvider (): any;
}
}
9 changes: 5 additions & 4 deletions packages/ceramic-cli/src/ceramic-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ const fetchJson = async (url: string, payload?: any): Promise<any> => {
headers: { 'Content-Type': 'application/json' }
}
}
const res = await fetch(url, opts)
return res.json()
const res = await (await fetch(url, opts)).json()
if (res.error) throw new Error(res.error)
return res
}

export enum SignatureStatus {
Expand Down Expand Up @@ -65,7 +66,7 @@ class Document extends EventEmitter {
}

async change (newContent: any, apiUrl: string): Promise<boolean> {
const { docId, state } = await fetchJson(apiUrl + '/change' + this.id, { content: newContent })
const { state } = await fetchJson(apiUrl + '/change' + this.id, { content: newContent })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's normalize the URL concatenation in order to avoid multiple slashes if the apiUrl already ends with /. Maybe a separate utility function.

this._state = state
return true
}
Expand Down Expand Up @@ -124,7 +125,7 @@ class CeramicClient {
return doc
}

async close () {
async close (): Promise<void> {
clearInterval(this.iid)
}
}
Expand Down
59 changes: 41 additions & 18 deletions packages/ceramic-cli/src/ceramic-daemon.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import ipfsClient from 'ipfs-http-client'
import express, { Request, Response, NextFunction } from 'express'
import IdentityWallet from 'identity-wallet'
import Ceramic from '@ceramicnetwork/ceramic-core'

const IPFS_HOST = 'http://localhost:5001'
const toApiPath = (ending: string):string => '/api/v0' + ending
// TODO - don't hardcode seed lol
const seed = '0x5872d6e0ae7347b72c9216db218ebbb9d9d0ae7ab818ead3557e8e78bf944184'

class CeramicDaemon {
private app: any
Expand All @@ -16,53 +19,73 @@ class CeramicDaemon {
next()
})
this.app.post(toApiPath('/create'), this.createDoc.bind(this))
this.app.get(toApiPath('/show/ceramic/:doctype/:cid'), this.show.bind(this))
this.app.get(toApiPath('/state/ceramic/:doctype/:cid'), this.state.bind(this))
this.app.post(toApiPath('/change/ceramic/:doctype/:cid'), this.change.bind(this))
this.app.get(toApiPath('/show/ceramic/:cid'), this.show.bind(this))
this.app.get(toApiPath('/state/ceramic/:cid'), this.state.bind(this))
this.app.post(toApiPath('/change/ceramic/:cid'), this.change.bind(this))
const server = this.app.listen(7007, () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can put a daemon port in the configuration file

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that should be configurable once we have a config file!

console.log('Ceramic API running on port 7007')
console.log('User DID:', ceramic.user.DID)
})
server.keepAliveTimeout = 60 * 1000
}

static async create (ipfsHost: string = IPFS_HOST) {
const ipfs = ipfsClient(ipfsHost)
const ceramic = await Ceramic.create(ipfs)
const idWallet = new IdentityWallet(async () => true, { seed })
await ceramic.setDIDProvider(idWallet.get3idProvider())
return new CeramicDaemon(ceramic)
}

async createDoc (req: Request, res: Response, next: NextFunction) {
const { doctype, genesis, onlyGenesis } = req.body
if (!doctype || !genesis) {} // TODO - reject somehow
const doc = await this.ceramic.createDocument(genesis, doctype, { onlyGenesis })
res.json({ docId: doc.id, state: doc.state })
try {
const doc = await this.ceramic.createDocument(genesis, doctype, { onlyGenesis })
res.json({ docId: doc.id, state: doc.state })
} catch (e) {
res.json({ error: e.message })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to handle Response error status codes?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, right now this isn't really using best practices for express or rest apis in general. We should make sure to do this properly!

}
next()
}

async show (req: Request, res: Response, next: NextFunction) {
const { doctype, cid } = req.params
const docId = ['/ceramic', doctype, cid].join('/')
const doc = await this.ceramic.loadDocument(docId)
const docId = ['/ceramic', req.params.cid].join('/')
try {
const doc = await this.ceramic.loadDocument(docId)
res.json({ docId: doc.id, content: doc.content })
} catch (e) {
res.json({ error: e.message })
}
next()
}

async state (req: Request, res: Response, next: NextFunction) {
const { doctype, cid } = req.params
const docId = ['/ceramic', doctype, cid].join('/')
const doc = await this.ceramic.loadDocument(docId)
res.json({ docId: doc.id, state: doc.state })
const docId = ['/ceramic', req.params.cid].join('/')
try {
const doc = await this.ceramic.loadDocument(docId)
res.json({ docId: doc.id, state: doc.state })
} catch (e) {
res.json({ error: e.message })
}
next()
}

async change (req: Request, res: Response, next: NextFunction) {
const { content } = req.body
if (!content) {} // TODO - reject somehow
const { doctype, cid } = req.params
const docId = ['/ceramic', doctype, cid].join('/')
const doc = await this.ceramic.loadDocument(docId)
await doc.change(content)
res.json({ docId: doc.id, state: doc.state })
if (!content) {
res.json({ error: 'content required to change document' })
next()
return
}
const docId = ['/ceramic', req.params.cid].join('/')
try {
const doc = await this.ceramic.loadDocument(docId)
await doc.change(content)
res.json({ docId: doc.id, state: doc.state })
} catch (e) {
res.json({ error: e.message })
}
next()
}
}
Expand Down
49 changes: 35 additions & 14 deletions packages/ceramic-cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import CeramicDaemon from './ceramic-daemon'
import CeramicClient from './ceramic-client'
import program from 'commander'


program
.command('daemon')
.option('--ipfs-api <url>', 'The ipfs http api to use')
Expand All @@ -18,9 +19,13 @@ program
.action(async (doctype, content, { onlyGenesis }) => {
content = JSON.parse(content)
const ceramic = new CeramicClient()
const doc = await ceramic.createDocument(content, doctype, { onlyGenesis })
console.log(doc.id)
console.log(JSON.stringify(doc.content, null, 2))
try {
const doc = await ceramic.createDocument(content, doctype, { onlyGenesis })
console.log(doc.id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can introduce a specific logger (e.g. winston) instead of directly using console.log

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely, created an issue here: #8

console.log(JSON.stringify(doc.content, null, 2))
} catch (e) {
console.error(e)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe process.exit() with a specific code?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that makes sense!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#9

}
ceramic.close()
})

Expand All @@ -30,8 +35,12 @@ program
.description('Show the content of a document')
.action(async (docId) => {
const ceramic = new CeramicClient()
const doc = await ceramic.loadDocument(docId)
console.log(JSON.stringify(doc.content, null, 2))
try {
const doc = await ceramic.loadDocument(docId)
console.log(JSON.stringify(doc.content, null, 2))
} catch (e) {
console.error(e)
}
ceramic.close()
})

Expand All @@ -40,8 +49,12 @@ program
.description('Show the state of a document')
.action(async (docId) => {
const ceramic = new CeramicClient()
const doc = await ceramic.loadDocument(docId)
console.log(JSON.stringify(doc.state, null, 2))
try {
const doc = await ceramic.loadDocument(docId)
console.log(JSON.stringify(doc.state, null, 2))
} catch (e) {
console.error(e)
}
ceramic.close()
})

Expand All @@ -50,12 +63,16 @@ program
.description('Watch for updates in a document')
.action(async (docId) => {
const ceramic = new CeramicClient()
const doc = await ceramic.loadDocument(docId)
console.log(JSON.stringify(doc.content, null, 2))
doc.on('change', () => {
console.log('--- document changed ---')
try {
const doc = await ceramic.loadDocument(docId)
console.log(JSON.stringify(doc.content, null, 2))
})
doc.on('change', () => {
console.log('--- document changed ---')
console.log(JSON.stringify(doc.content, null, 2))
})
} catch (e) {
console.error(e)
}
})

program
Expand All @@ -65,8 +82,12 @@ program
.action(async (docId, content) => {
content = JSON.parse(content)
const ceramic = new CeramicClient()
const doc = await ceramic._updateDocument(docId, content)
console.log(JSON.stringify(doc.id, null, 2))
try {
const doc = await ceramic._updateDocument(docId, content)
console.log(JSON.stringify(doc.content, null, 2))
} catch (e) {
console.error(e)
}
ceramic.close()
})

Expand Down
51 changes: 27 additions & 24 deletions packages/ceramic-core/src/3id-did-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,39 @@ import Ceramic from './ceramic'
import { ParsedDID, DIDResolver, DIDDocument } from 'did-resolver'

interface ResolverRegistry {
[index: string]: DIDResolver
[index: string]: DIDResolver;
}

export function wrapDocument (content: any, did: string): DIDDocument {
// this should be generated in a much more dynamic way based on the content of the doc
// keys should be encoded using multicodec, by looking at the codec bits
// we can determine the key type.
return {
'@context': 'https://w3id.org/did/v1',
id: did,
publicKey: [{
id: `${did}#signingKey`,
type: 'Secp256k1VerificationKey2018',
owner: did,
publicKeyHex: content.publicKeys.signing
}, {
id: `${did}#encryptionKey`,
type: 'Curve25519EncryptionPublicKey',
owner: did,
publicKeyBase64: content.publicKeys.encryption
}],
authentication: [{
type: 'Secp256k1SignatureAuthentication2018',
publicKey: `${did}#signingKey`,
}]
}
}

export default {
getResolver: (ceramic: Ceramic): ResolverRegistry => ({
'3': async (did: string, parsed: ParsedDID): Promise<DIDDocument | null> => {
const doc = await ceramic.loadDocument(`/ceramic/${parsed.id}`)
const content = doc.content
// this should be generated in a much more dynamic way based on the content of the doc
// keys should be encoded using multicodec, by looking at the codec bits
// we can determine the key type.
return {
'@context': 'https://w3id.org/did/v1',
id: did,
publicKey: [{
id: `${did}#signingKey`,
type: 'Secp256k1VerificationKey2018',
owner: did,
publicKeyHex: content.publicKeys.signing
}, {
id: `${did}#encryptionKey`,
type: 'Curve25519EncryptionPublicKey',
owner: did,
publicKeyBase64: content.publicKeys.encryption
}],
authentication: [{
type: 'Secp256k1SignatureAuthentication2018',
publicKey: `${did}#signingKey`,
}]
}
return wrapDocument(doc.content, did)
}
})
}
8 changes: 4 additions & 4 deletions packages/ceramic-core/src/__tests__/document.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ jest.mock('../dispatcher', () => {
import Dispatcher from '../dispatcher'
jest.mock('../user')
import User from '../user'
jest.mock('did-jwt/src/VerifierAlgorithm.ts', () => () => {
// TODO - make sure this actually work
return (): any => 'verified'
})
jest.mock('did-jwt', () => ({
// TODO - We should test for when this function throws as well
verifyJWT: (): any => 'verified'
}))


describe('Document', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import ThreeIdHandler from '../threeIdHandler'

jest.mock('../../user')
import User from '../../user'
jest.mock('did-jwt/src/VerifierAlgorithm.ts', () => () => {
jest.mock('did-jwt', () => ({
// TODO - We should test for when this function throws as well
return (): any => 'verified'
})
verifyJWT: (): any => 'verified'
}))

const RECORDS = {
genesis: { doctype: '3id', owners: [ '0x123' ], content: { publicKeys: { test: '0xabc' } } },
Expand Down
16 changes: 8 additions & 8 deletions packages/ceramic-core/src/doctypes/threeIdHandler.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import DoctypeHandler from './doctypeHandler'
import { DocState, SignatureStatus } from '../document'
import { wrapDocument } from '../3id-did-resolver'
import jsonpatch from 'fast-json-patch'
// @ts-ignore
import VerifierAlrgorithm from 'did-jwt/src/VerifierAlgorithm.ts'
import { verifyJWT } from 'did-jwt'
import { DIDDocument } from 'did-resolver'

const DOCTYPE = '3id'

Expand Down Expand Up @@ -56,14 +57,13 @@ class ThreeIdHandler extends DoctypeHandler {
iss: record.iss
})).toString('base64')
if (payload.endsWith('=')) payload = payload.slice(0, -1)
const data = [header, payload].join('.')
const authenticators = state.owners.map(key => ({
publicKeyHex: key
}))
const jwt = [header, payload, signature].join('.')
try {
await VerifierAlrgorithm('ES256K')(data, signature, authenticators)
// verify the jwt with a fake DID resolver that uses the current state of the 3ID
const didDoc = wrapDocument({ publicKeys: { signing: state.owners[0], encryption: '' } }, 'did:fake:123')
await verifyJWT(jwt, { resolver: { resolve: async (): Promise<DIDDocument> => didDoc } })
} catch (e) {
throw new Error('Invalid signature for signed record')
throw new Error('Invalid signature for signed record:' + e)
}
return {
...state,
Expand Down
Loading