Skip to content

Commit

Permalink
Merge branch 'main' into simplify-types
Browse files Browse the repository at this point in the history
* main:
  adding `replyTo` field in http request body to submit RFQ (#142)
  • Loading branch information
diehuxx committed Feb 5, 2024
2 parents ecab9d7 + 85e5841 commit e406f8e
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 28 deletions.
22 changes: 17 additions & 5 deletions packages/http-client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { PortableDid } from '@web5/dids'
import {
MessageModel,
parseMessage,
Rfq,
} from '@tbdex/protocol'

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

await message.verify()

Expand All @@ -71,10 +72,16 @@ export class TbdexHttpClient {

let response: Response
try {
let requestBody
if (message.metadata.kind == 'rfq') {
requestBody = JSON.stringify({ rfq: message, replyTo})
} else {
requestBody = JSON.stringify(message)

Check warning on line 79 in packages/http-client/src/client.ts

View check run for this annotation

Codecov / codecov/patch

packages/http-client/src/client.ts#L79

Added line #L79 was not covered by tests
}
response = await fetch(apiRoute, {
method : 'POST',
headers : { 'content-type': 'application/json' },
body : JSON.stringify(message)
body : requestBody
})
} catch (e) {
throw new RequestError({ message: `Failed to send message to ${pfiDid}`, recipientDid: pfiDid, url: apiRoute, cause: e })
Expand Down Expand Up @@ -355,9 +362,14 @@ export class TbdexHttpClient {
* options passed to {@link TbdexHttpClient.sendMessage} method
* @beta
*/
export type SendMessageOptions = {
export type SendMessageOptions<T extends Message> = {
/** the message you want to send */
message: Message
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
}

/**
Expand Down
27 changes: 20 additions & 7 deletions packages/http-client/tests/client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
RequestTokenVerificationError,
RequestTokenSigningError
} from '../src/errors/index.js'

import { DevTools } from '@tbdex/protocol'
import * as sinon from 'sinon'
import { JwtHeaderParams, JwtPayload, PrivateKeyJwk, Secp256k1 } from '@web5/crypto'
Expand All @@ -33,8 +32,7 @@ const getPfiServiceEndpointStub = sinon.stub(TbdexHttpClient, 'getPfiServiceEndp
describe('client', () => {
beforeEach(() => getPfiServiceEndpointStub.resolves('https://localhost:9000'))


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

Expand All @@ -54,7 +52,6 @@ describe('client', () => {
await TbdexHttpClient.sendMessage({ message: rfq })
expect.fail()
} catch(e) {
console.log(e)
expect(e.name).to.equal('RequestError')
expect(e).to.be.instanceof(RequestError)
expect(e.message).to.include('Failed to send message')
Expand Down Expand Up @@ -88,7 +85,23 @@ describe('client', () => {
}
})

it('should not throw errors if all is well', async () => {
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: 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()
Expand Down Expand Up @@ -311,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 @@ -320,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
4 changes: 2 additions & 2 deletions packages/http-server/src/http-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { Express } from 'express'
import express from 'express'
import cors from 'cors'

import { getExchanges, getOfferings, submitOrder, submitClose, submitRfq } from './request-handlers/index.js'
import { getExchanges, getOfferings, submitOrder, submitClose, createExchange } from './request-handlers/index.js'
import { jsonBodyParser } from './middleware/index.js'
import { fakeExchangesApi, fakeOfferingsApi } from './fakes.js'

Expand Down Expand Up @@ -142,7 +142,7 @@ export class TbdexHttpServer {
listen(port: number | string, callback?: () => void) {
const { offeringsApi, exchangesApi, pfiDid } = this

this.api.post('/exchanges/:exchangeId/rfq', submitRfq({
this.api.post('/exchanges/:exchangeId/rfq', createExchange({
callback: this.callbacks['rfq'], offeringsApi, exchangesApi,
}))

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,23 @@ import type { ErrorDetail } from '@tbdex/http-client'

import { CallbackError } from '../callback-error.js'

type SubmitRfqOpts = {
type CreateExchangeOpts = {
callback: SubmitRfqCallback
offeringsApi: OfferingsApi
exchangesApi: ExchangesApi
}

export function submitRfq(options: SubmitRfqOpts): RequestHandler {
export function createExchange(options: CreateExchangeOpts): RequestHandler {
const { offeringsApi, exchangesApi, callback } = options
return async function (req, res) {
let rfq: Rfq

if (req.body.replyTo && !isValidUrl(req.body.replyTo)) {
return res.status(400).json({ errors: [{ detail: 'replyTo must be a valid url' }] })
}

try {
rfq = await Rfq.parse(req.body)
rfq = await Rfq.parse(req.body.rfq)

Check warning on line 23 in packages/http-server/src/request-handlers/create-exchange.ts

View check run for this annotation

Codecov / codecov/patch

packages/http-server/src/request-handlers/create-exchange.ts#L23

Added line #L23 was not covered by tests
} catch(e) {
const errorResponse: ErrorDetail = { detail: `Parsing of TBDex Rfq message failed: ${e.message}` }

Check warning on line 25 in packages/http-server/src/request-handlers/create-exchange.ts

View check run for this annotation

Codecov / codecov/patch

packages/http-server/src/request-handlers/create-exchange.ts#L25

Added line #L25 was not covered by tests
return res.status(400).json({ errors: [errorResponse] })
Expand Down Expand Up @@ -61,3 +65,12 @@ export function submitRfq(options: SubmitRfqOpts): RequestHandler {
return res.sendStatus(202)
}
}

function isValidUrl(replyToUrl: string) {
try {
new URL(replyToUrl)
return true
} catch (err) {
return false
}
}
2 changes: 1 addition & 1 deletion packages/http-server/src/request-handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ export * from './get-exchanges.js'
export * from './get-offerings.js'
export * from './submit-close.js'
export * from './submit-order.js'
export * from './submit-rfq.js'
export * from './create-exchange.js'
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ErrorDetail } from '@tbdex/http-client'
import type { Server } from 'http'

import { TbdexHttpServer } from '../src/main.js'
import { DevTools, TbdexHttpServer } from '../src/main.js'
import { expect } from 'chai'

let api = new TbdexHttpServer()
Expand Down Expand Up @@ -48,6 +48,27 @@ describe('POST /exchanges/:exchangeId/rfq', () => {
expect(error.detail).to.include('JSON')
})

it('returns a 400 if create exchange request contains a replyTo which is not a valid URL', async () => {
const aliceDid = await DevTools.createDid()
const pfiDid = await DevTools.createDid()
const rfq = await DevTools.createRfq({ sender: aliceDid, receiver: pfiDid })
await rfq.sign(aliceDid)

const resp = await fetch('http://localhost:8000/exchanges/123/rfq', {
method : 'POST',
body : JSON.stringify({ rfq: rfq, replyTo: 'foo'})
})

expect(resp.status).to.equal(400)

const responseBody = await resp.json() as { errors: ErrorDetail[] }
expect(responseBody.errors.length).to.equal(1)

const [ error ] = responseBody.errors
expect(error.detail).to.exist
expect(error.detail).to.include('replyTo must be a valid url')
})

xit('returns a 400 if request body is not a valid RFQ')
xit('returns a 400 if request body if integrity check fails')
xit('returns a 409 if request body if RFQ already exists')
Expand Down
37 changes: 28 additions & 9 deletions packages/protocol/src/dev-tools.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@

import type { OfferingData, QuoteData, RfqData } from './types.js'
import type { PortableDid } from '@web5/dids'

import { DidDhtMethod, DidIonMethod, DidKeyMethod } from '@web5/dids'
import { VerifiableCredential } from '@web5/credentials'
import { Offering } from './resource-kinds/index.js'
import { Rfq } from './message-kinds/index.js'
import { Order, Rfq } from './message-kinds/index.js'
import { Resource } from './resource.js'
import { Message } from './main.js'
import { VerifiableCredential } from '@web5/credentials'

/**
* Supported DID Methods
* @beta
*/
export type DidMethodOptions = 'key' | 'ion'
export type DidMethodOptions = 'key' | 'ion' | 'dht'

/**
* Options passed to {@link DevTools.createRfq}
* @beta
*/
export type RfqOptions = {
export type MessageOptions = {
/**
* {@link @web5/dids#PortableDid} of the rfq sender. used to generate a random credential that fulfills the vcRequirements
* {@link @web5/dids#PortableDid} of the message sender. When generating RFQ, it is used to generate a random credential that fulfills the vcRequirements
* of the offering returned by {@link DevTools.createOffering}
*/
sender: PortableDid
Expand All @@ -42,9 +44,9 @@ export class DevTools {
if (didMethod === 'key') {
return await DidKeyMethod.create()
} else if (didMethod === 'ion') {
return await DidIonMethod.create()
return DidIonMethod.create()
} else if (didMethod === 'dht') {
return await DidDhtMethod.create()
return DidDhtMethod.create()
} else {
throw new Error(`${didMethod} method not implemented.`)
}
Expand Down Expand Up @@ -174,7 +176,7 @@ export class DevTools {
*
* **NOTE**: generates a random credential that fulfills the offering's required claims
*/
static async createRfq(opts: RfqOptions) {
static async createRfq(opts: MessageOptions) {
const { sender, receiver } = opts

const rfqData: RfqData = await DevTools.createRfqData(opts)
Expand All @@ -185,10 +187,27 @@ export class DevTools {
})
}

/**
* Creates and returns an example Order with a generated exchangeId. Useful for testing purposes
* @param opts - options used to create a Message
* @returns Order message
*/
static createOrder(opts: MessageOptions) {
const { sender, receiver } = opts

return Order.create({
metadata: {
from : sender.did,
to : receiver?.did ?? 'did:ex:pfi',
exchangeId : Message.generateId('rfq')
}
})
}

/**
* creates an example RfqData. Useful for testing purposes
*/
static async createRfqData(opts?: RfqOptions): Promise<RfqData> {
static async createRfqData(opts?: MessageOptions): Promise<RfqData> {
let vcJwt: string = ''

if (opts?.sender) {
Expand Down

0 comments on commit e406f8e

Please sign in to comment.