Skip to content
This repository has been archived by the owner on Dec 12, 2024. It is now read-only.

Commit

Permalink
Simplify types, inheritance structure, and API (#156)
Browse files Browse the repository at this point in the history
* Simplify types, inheritance structure, and API

* Lint

* Update submodule to latest main

* Increase documentation coverage in protocol package

* Fix tsdocs inheritdoc

* typoooo

* Remove rawToMessageModel and rawToResourceModel from public API

* Fix capitalization on inheritDoc

* Give up on inheritDoc

* Resolve link doc tag warnings

* @jiyoontbd PR comments

* Add changeset

* Document raw to model functions

* Address tbdocs warnings for http-server
  • Loading branch information
Diane Huxley authored Feb 6, 2024
1 parent 85e5841 commit 1b48ad1
Show file tree
Hide file tree
Showing 36 changed files with 1,051 additions and 802 deletions.
7 changes: 7 additions & 0 deletions .changeset/violet-bats-battle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@tbdex/http-client": minor
"@tbdex/http-server": minor
"@tbdex/protocol": minor
---

Simplify types, inheritance structure, and API
81 changes: 39 additions & 42 deletions packages/http-client/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import type { JwtPayload } from '@web5/crypto'
import type { ErrorDetail } from './types.js'
import type { PortableDid } from '@web5/dids'
import type {
ResourceMetadata,
import {
MessageModel,
OfferingData,
ResourceModel,
MessageKind,
MessageKindClass,
Parser,
Rfq,
} from '@tbdex/protocol'

import {
Expand All @@ -20,10 +17,10 @@ import {
RequestTokenSigningError,
RequestTokenVerificationError
} from './errors/index.js'
import { resolveDid, Offering, Resource, Message } from '@tbdex/protocol'
import { resolveDid, Offering, Message } from '@tbdex/protocol'
import { utils as didUtils } from '@web5/dids'
import { typeid } from 'typeid-js'
import { Jwt } from '@web5/credentials'
import { Jwt, JwtVerifyResult } from '@web5/credentials'

import queryString from 'query-string'
import ms from 'ms'
Expand Down Expand Up @@ -64,24 +61,22 @@ export class TbdexHttpClient {
* @throws if recipient DID resolution fails
* @throws if recipient DID does not have a PFI service entry
*/
static async sendMessage<T extends MessageKind>(opts: SendMessageOptions<T>): Promise<void> {
static async sendMessage<T extends Message>(opts: SendMessageOptions<T>): Promise<void> {
const { message, replyTo } = opts

const jsonMessage: MessageModel<T> = message instanceof Message ? message.toJSON() : message

await Message.verify(jsonMessage)
await message.verify()

const { to: pfiDid, exchangeId, kind } = jsonMessage.metadata
const { to: pfiDid, exchangeId, kind } = message.metadata
const pfiServiceEndpoint = await TbdexHttpClient.getPfiServiceEndpoint(pfiDid)
const apiRoute = `${pfiServiceEndpoint}/exchanges/${exchangeId}/${kind}`

let response: Response
try {
let requestBody
if (jsonMessage.metadata.kind == 'rfq') {
requestBody = JSON.stringify({ rfq: jsonMessage, replyTo})
if (message.metadata.kind == 'rfq') {
requestBody = JSON.stringify({ rfq: message, replyTo})
} else {
requestBody = JSON.stringify(jsonMessage)
requestBody = JSON.stringify(message)
}
response = await fetch(apiRoute, {
method : 'POST',
Expand Down Expand Up @@ -174,20 +169,21 @@ export class TbdexHttpClient {
throw new ResponseError({ statusCode: response.status, details: errorDetails, recipientDid: pfiDid, url: apiRoute })
}

const responseBody = await response.json() as { data: ResourceModel<'offering'>[] }
for (let jsonResource of responseBody.data) {
const resource = await Resource.parse(jsonResource)
data.push(resource)
const responseBody = await response.json()
const jsonOfferings = responseBody.data as any[]
for (let jsonOffering of jsonOfferings) {
const offering = await Offering.parse(jsonOffering)
data.push(offering)
}

return data
}

/**
* get a specific exchange from the pfi provided
* @param _opts - options
* @param opts - options
*/
static async getExchange(opts: GetExchangeOptions): Promise<MessageKindClass[]> {
static async getExchange(opts: GetExchangeOptions): Promise<Message[]> {
const { pfiDid, exchangeId, did } = opts

const pfiServiceEndpoint = await TbdexHttpClient.getPfiServiceEndpoint(pfiDid)
Expand All @@ -205,16 +201,16 @@ export class TbdexHttpClient {
throw new RequestError({ message: `Failed to get exchange from ${pfiDid}`, recipientDid: pfiDid, url: apiRoute, cause: e })
}

const data: MessageKindClass[] = []
const data: Message[] = []

if (!response.ok) {
const errorDetails = await response.json() as ErrorDetail[]
throw new ResponseError({ statusCode: response.status, details: errorDetails, recipientDid: pfiDid, url: apiRoute })
}

const responseBody = await response.json() as { data: MessageModel<MessageKind>[] }
const responseBody = await response.json() as { data: MessageModel[] }
for (let jsonMessage of responseBody.data) {
const message = await Message.parse(jsonMessage)
const message = await Parser.parseMessage(jsonMessage)
data.push(message)
}

Expand All @@ -226,7 +222,7 @@ export class TbdexHttpClient {
* returns all exchanges created by requester
* @param _opts - options
*/
static async getExchanges(opts: GetExchangesOptions): Promise<MessageKindClass[][]> {
static async getExchanges(opts: GetExchangesOptions): Promise<Message[][]> {
const { pfiDid, filter, did } = opts

const pfiServiceEndpoint = await TbdexHttpClient.getPfiServiceEndpoint(pfiDid)
Expand All @@ -245,19 +241,19 @@ export class TbdexHttpClient {
throw new RequestError({ message: `Failed to get exchanges from ${pfiDid}`, recipientDid: pfiDid, url: apiRoute, cause: e })
}

const exchanges: MessageKindClass[][] = []
const exchanges: Message[][] = []

if (!response.ok) {
const errorDetails = await response.json() as ErrorDetail[]
throw new ResponseError({ statusCode: response.status, details: errorDetails, recipientDid: pfiDid, url: apiRoute })
}

const responseBody = await response.json() as { data: MessageModel<MessageKind>[][] }
const responseBody = await response.json() as { data: MessageModel[][] }
for (let jsonExchange of responseBody.data) {
const exchange: MessageKindClass[] = []
const exchange: Message[] = []

for (let jsonMessage of jsonExchange) {
const message = await Message.parse(jsonMessage)
const message = await Parser.parseMessage(jsonMessage)
exchange.push(message)
}

Expand Down Expand Up @@ -299,10 +295,10 @@ export class TbdexHttpClient {
* * `iss`
* * `exp`
* * `iat`
* * `jti`The JWT is then signed and returned.
* * `jti` The JWT is then signed and returned.
*
* @returns the request token (JWT)
* @throws {RequestTokenError} If an error occurs during the token generation.
* @throws {@link RequestTokenSigningError} If an error occurs during the token generation.
*/
static async generateRequestToken(params: GenerateRequestTokenParams): Promise<string> {
const now = Date.now()
Expand All @@ -325,17 +321,19 @@ export class TbdexHttpClient {

/**
* Validates and verifies the integrity of a request token ([JWT](https://datatracker.ietf.org/doc/html/rfc7519))
* generated by {@link generateRequestToken}. Specifically:
* generated by {@link TbdexHttpClient.generateRequestToken}. Specifically:
* * verifies integrity of the JWT
* * ensures all required claims are present and valid.
* * ensures the token has not expired
* * ensures token audience matches the expected PFI DID.
*
* @returns the requester's DID as a string if the token is valid.
* @throws {RequestTokenError} If the token is invalid, expired, or has been tampered with
* @throws {@link RequestTokenVerificationError} If the token is invalid, expired, or has been tampered with
* @throws {@link RequestTokenMissingClaimsError} If the token does not contain all required claims
* @throws {@link RequestTokenAudienceMismatchError} If the token's `aud` property does not match the PFI's DID
*/
static async verifyRequestToken(params: VerifyRequestTokenParams): Promise<string> {
let result
let result: JwtVerifyResult

try {
result = await Jwt.verify({ jwt: params.requestToken })
Expand Down Expand Up @@ -366,14 +364,14 @@ export class TbdexHttpClient {
* options passed to {@link TbdexHttpClient.sendMessage} method
* @beta
*/
export type SendMessageOptions<T extends MessageKind> = {
export type SendMessageOptions<T extends Message> = {
/** the message you want to send */
message: Message<T> | MessageModel<T>
message: T
/**
* A string containing a valid URI where new messages from the PFI will be sent.
* This field is only available as an option when sending an RFQ Message.
*/
replyTo?: T extends 'rfq' ? string : never
replyTo?: T extends Rfq ? string : never
}

/**
Expand All @@ -385,10 +383,10 @@ export type GetOfferingsOptions = {
pfiDid: string
filter?: {
/** ISO 3166 currency code string */
payinCurrency?: OfferingData['payinCurrency']['currencyCode']
payinCurrency?: string
/** ISO 3166 currency code string */
payoutCurrency?: OfferingData['payoutCurrency']['currencyCode']
id?: ResourceMetadata<any>['id']
payoutCurrency?: string
id?: string
}
}

Expand All @@ -401,7 +399,6 @@ export type GetExchangeOptions = {
pfiDid: string
/** the exchange you want to fetch */
exchangeId: string

/** the message author's DID */
did: PortableDid
}
Expand Down
3 changes: 2 additions & 1 deletion packages/http-client/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@

export * from '@tbdex/protocol'
export * from './client.js'
export * from './types.js'
export * from './types.js'
export * from './errors/index.js'
43 changes: 30 additions & 13 deletions packages/http-client/tests/client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
RequestTokenVerificationError,
RequestTokenSigningError
} from '../src/errors/index.js'
import { DevTools, Message, Rfq } from '@tbdex/protocol'
import { DevTools } from '@tbdex/protocol'
import * as sinon from 'sinon'
import { JwtHeaderParams, JwtPayload, PrivateKeyJwk, Secp256k1 } from '@web5/crypto'
import { Convert } from '@web5/common'
Expand All @@ -24,26 +24,32 @@ const dhtDid = await DidDhtMethod.create({
serviceEndpoint : 'https://localhost:9000'
}]
})

// TODO : Instead of stubbing fetch, consider using libraries like msw
const fetchStub = sinon.stub(globalThis, 'fetch')
const getPfiServiceEndpointStub = sinon.stub(TbdexHttpClient, 'getPfiServiceEndpoint')
sinon.stub(Message, 'verify').resolves('123')

describe('client', () => {
beforeEach(() => getPfiServiceEndpointStub.resolves('https://localhost:9000'))

describe('sendMessage', async () => {
let mockRfq: Rfq
describe('sendMessage', () => {
let aliceDid: PortableDid
let pfiDid: PortableDid

beforeEach(async () => {
mockRfq = await DevTools.createRfq({ sender: dhtDid, receiver: dhtDid })
aliceDid = await DevTools.createDid()
pfiDid = await DevTools.createDid()
})

it('throws RequestError if service endpoint url is garbage', async () => {
getPfiServiceEndpointStub.resolves('garbage')
fetchStub.rejects({message: 'Failed to fetch on URL'})

const rfq = await DevTools.createRfq({ sender: aliceDid, receiver: pfiDid })
await rfq.sign(aliceDid)

try {
await TbdexHttpClient.sendMessage({message: mockRfq})
await TbdexHttpClient.sendMessage({ message: rfq })
expect.fail()
} catch(e) {
expect(e.name).to.equal('RequestError')
Expand All @@ -63,38 +69,49 @@ describe('client', () => {
})
} as Response)

const rfq = await DevTools.createRfq({ sender: aliceDid, receiver: pfiDid })
await rfq.sign(aliceDid)

try {
await TbdexHttpClient.sendMessage({message: mockRfq})
await TbdexHttpClient.sendMessage({message: rfq })
expect.fail()
} catch(e) {
expect(e.name).to.equal('ResponseError')
expect(e).to.be.instanceof(ResponseError)
expect(e.statusCode).to.exist
expect(e.details).to.exist
expect(e.recipientDid).to.equal(dhtDid.did)
expect(e.url).to.equal(`https://localhost:9000/exchanges/${mockRfq.metadata.exchangeId}/rfq`)
expect(e.recipientDid).to.equal(pfiDid.did)
expect(e.url).to.equal(`https://localhost:9000/exchanges/${rfq.metadata.exchangeId}/rfq`)
}
})

it('should not throw errors if all is well when sending RFQ with replyTo field', async () => {
fetchStub.resolves({
ok : true,
json : () => Promise.resolve()
} as Response)

const rfq = await DevTools.createRfq({ sender: aliceDid, receiver: pfiDid })
await rfq.sign(aliceDid)

try {
await TbdexHttpClient.sendMessage({message: mockRfq, replyTo: 'https://tbdex.io/callback'})
await TbdexHttpClient.sendMessage({message: rfq, replyTo: 'https://tbdex.io/callback'})
} catch (e) {
expect.fail()
}
})

it('should not throw errors if all is well when sending RFQ without replyTo field', async () => {
fetchStub.resolves({
ok : true,
json : () => Promise.resolve()
} as Response)

const rfq = await DevTools.createRfq({ sender: aliceDid, receiver: pfiDid })
await rfq.sign(aliceDid)

try {
await TbdexHttpClient.sendMessage({message: mockRfq})
await TbdexHttpClient.sendMessage({ message: rfq })
} catch (e) {
expect.fail()
}
Expand Down Expand Up @@ -307,7 +324,7 @@ describe('client', () => {
it('sets expiration seconds to 1 minute after the time at which it was issued', async () => {
const requestToken = await TbdexHttpClient.generateRequestToken({ requesterDid: requesterPortableDid, pfiDid: 'did:key:1234' })
const decodedToken = await Jwt.verify({ jwt: requestToken })
expect(decodedToken.payload.exp - decodedToken.payload.iat).to.equal(60)
expect(decodedToken.payload.exp! - decodedToken.payload.iat!).to.equal(60)
})
})

Expand All @@ -316,7 +333,7 @@ describe('client', () => {
let header: JwtHeaderParams
let payload: JwtPayload

async function createRequestTokenFromPayload(payload) {
async function createRequestTokenFromPayload(payload: JwtPayload) {
const privateKeyJwk = pfiPortableDid.keySet.verificationMethodKeys![0].privateKeyJwk
const base64UrlEncodedHeader = Convert.object(header).toBase64Url()
const base64UrlEncodedPayload = Convert.object(payload).toBase64Url()
Expand Down
Loading

0 comments on commit 1b48ad1

Please sign in to comment.