From ca1b23581d9525a10d2cac9e67858f40f6c6ef54 Mon Sep 17 00:00:00 2001 From: kirahsapong <102400653+kirahsapong@users.noreply.github.com> Date: Thu, 8 Feb 2024 16:33:42 -0800 Subject: [PATCH 1/6] Make `pfiDid` required property in `TbdexHttpServer` (#166) * mark pfiDid required property * add changeset * remove defaults obj --- .changeset/gentle-lies-provide.md | 5 +++++ packages/http-server/src/http-server.ts | 16 ++++------------ 2 files changed, 9 insertions(+), 12 deletions(-) create mode 100644 .changeset/gentle-lies-provide.md diff --git a/.changeset/gentle-lies-provide.md b/.changeset/gentle-lies-provide.md new file mode 100644 index 00000000..eec35f38 --- /dev/null +++ b/.changeset/gentle-lies-provide.md @@ -0,0 +1,5 @@ +--- +"@tbdex/http-server": minor +--- + +Requires consumer to pass pfiDid when instantiating a new TbdexHttpServer diff --git a/packages/http-server/src/http-server.ts b/packages/http-server/src/http-server.ts index 2f3f965b..eca593aa 100644 --- a/packages/http-server/src/http-server.ts +++ b/packages/http-server/src/http-server.ts @@ -36,13 +36,7 @@ type CallbackMap = { type NewHttpServerOptions = { offeringsApi?: OfferingsApi exchangesApi?: ExchangesApi, - pfiDid?: string -} - -const defaults: NewHttpServerOptions = { - offeringsApi : fakeOfferingsApi, - exchangesApi : fakeExchangesApi, - pfiDid : 'did:ex:pfi' + pfiDid: string } /** @@ -77,12 +71,10 @@ export class TbdexHttpServer { constructor(opts?: NewHttpServerOptions) { this.callbacks = {} - opts = { ...defaults, ...opts } - const { offeringsApi, exchangesApi, pfiDid } = opts - this.exchangesApi = exchangesApi - this.offeringsApi = offeringsApi - this.pfiDid = pfiDid + this.exchangesApi = opts?.exchangesApi ?? fakeExchangesApi + this.offeringsApi = opts?.offeringsApi ?? fakeOfferingsApi + this.pfiDid = opts?.pfiDid ?? 'did:ex:pfi' // initialize api here so that consumers can attach custom endpoints const api = express() From 629f0c772a0159e8bc06465dba7d2ec3a420b30d Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Tue, 13 Feb 2024 09:31:51 -0800 Subject: [PATCH 2/6] Stricten http-client (#169) * Stricten http-client * Changeset --- .changeset/mighty-pandas-speak.md | 5 ++ packages/http-client/src/client.ts | 77 ++++--------------- .../http-client/src/errors/request-error.ts | 2 +- packages/http-client/tests/client.spec.ts | 2 +- packages/http-client/tests/tsconfig.json | 2 + packages/http-client/tsconfig.json | 3 +- 6 files changed, 25 insertions(+), 66 deletions(-) create mode 100644 .changeset/mighty-pandas-speak.md diff --git a/.changeset/mighty-pandas-speak.md b/.changeset/mighty-pandas-speak.md new file mode 100644 index 00000000..36e78adf --- /dev/null +++ b/.changeset/mighty-pandas-speak.md @@ -0,0 +1,5 @@ +--- +"@tbdex/http-client": minor +--- + +Stricten, tested, and remove untested code diff --git a/packages/http-client/src/client.ts b/packages/http-client/src/client.ts index e00dcfff..e171cddd 100644 --- a/packages/http-client/src/client.ts +++ b/packages/http-client/src/client.ts @@ -1,6 +1,6 @@ import type { JwtPayload } from '@web5/crypto' import type { ErrorDetail } from './types.js' -import type { PortableDid } from '@web5/dids' +import type { DidDocument, PortableDid } from '@web5/dids' import { MessageModel, Parser, @@ -93,56 +93,6 @@ export class TbdexHttpClient { } } - /** - * Discover PFIs that are anchored via did:ion. These have a type of "PFI" and an id of PFI. - * You can then query the endpoints for offerings. - */ - static async discoverPFIs() { - const BASE_URL = 'https://ion.tbd.engineering' - const DID_TYPE_ENDPOINT = '/didtype/1669' - const IDENTIFIER_PREFIX = '/identifiers/' - - async function fetchDIDList() { - const response = await fetch(BASE_URL + DID_TYPE_ENDPOINT) - if (!response.ok) { - throw new Error('Failed to fetch DID list') - } - return await response.json() - } - - async function fetchDIDData(did) { - console.log(BASE_URL + IDENTIFIER_PREFIX + did) - const response = await fetch(BASE_URL + IDENTIFIER_PREFIX + did) - if (!response.ok) { - throw new Error('Failed to fetch DID data for ' + did) - } - return await response.json() - } - - const ids = await fetchDIDList() - const promises = ids.map(id => { - const ionDid = 'did:ion:' + id - return fetchDIDData(ionDid) - }) - const didDataList = await Promise.all(promises) - - const pfiServiceEndpoints = didDataList.reduce((results, didData) => { - const services = didData.didDocument.service - const pfiServices = services.filter(service => service.type === 'PFI') - - if (pfiServices.length > 0) { - results.push({ - did : didData.didDocument.id, - serviceEndpoint : pfiServices[0].serviceEndpoint - }) - } - - return results - }, []) - - return pfiServiceEndpoints - } - /** * gets offerings from the pfi provided * @param opts - options @@ -218,6 +168,7 @@ export class TbdexHttpClient { } + // TODO: Wrap Message[] in Exchange object and verify each message /** * returns all exchanges created by requester * @param _opts - options @@ -268,21 +219,20 @@ export class TbdexHttpClient { * @param did - the pfi's DID */ static async getPfiServiceEndpoint(did: string) { + let didDocument: DidDocument try { - const didDocument = await resolveDid(did) - const [didService] = didUtils.getServices({ didDocument, type: 'PFI' }) + didDocument = await resolveDid(did) + } catch (e) { + throw new InvalidDidError(e.message) + } - if (!didService?.serviceEndpoint) { - throw new MissingServiceEndpointError(`${did} has no PFI service entry`) - } + const [didService] = didUtils.getServices({ didDocument, type: 'PFI' }) - return didService.serviceEndpoint - } catch (e) { - if (e instanceof MissingServiceEndpointError) { - throw e - } - throw new InvalidDidError(e) + if (!didService?.serviceEndpoint) { + throw new MissingServiceEndpointError(`${did} has no PFI service entry`) } + + return didService.serviceEndpoint } /** @@ -356,7 +306,8 @@ export class TbdexHttpClient { throw new RequestTokenAudienceMismatchError({ message: 'Request token contains invalid audience. Expected aud property to be PFI DID.' }) } - return requestTokenPayload.iss + // TODO: check iss against signer DID + return requestTokenPayload.iss! } } diff --git a/packages/http-client/src/errors/request-error.ts b/packages/http-client/src/errors/request-error.ts index 09afb800..e6d30692 100644 --- a/packages/http-client/src/errors/request-error.ts +++ b/packages/http-client/src/errors/request-error.ts @@ -1,7 +1,7 @@ export type RequestErrorParams = { message: string recipientDid: string - url?: string + url: string cause?: unknown } diff --git a/packages/http-client/tests/client.spec.ts b/packages/http-client/tests/client.spec.ts index 02ed1ecb..e3d2eba1 100644 --- a/packages/http-client/tests/client.spec.ts +++ b/packages/http-client/tests/client.spec.ts @@ -257,7 +257,7 @@ describe('client', () => { } }) - it('returns exchanges array if response is ok', async () => { + it('returns empty exchanges array if response is ok and body is empty array', async () => { fetchStub.resolves({ ok : true, json : () => Promise.resolve({ data: [] }) diff --git a/packages/http-client/tests/tsconfig.json b/packages/http-client/tests/tsconfig.json index 7c6d2c8e..359d3749 100644 --- a/packages/http-client/tests/tsconfig.json +++ b/packages/http-client/tests/tsconfig.json @@ -1,6 +1,8 @@ { "extends": "../tsconfig.json", "compilerOptions": { + "strict": true, + "useUnknownInCatchVariables": false, "outDir": "compiled", "declarationDir": "compiled/types", "sourceMap": true, diff --git a/packages/http-client/tsconfig.json b/packages/http-client/tsconfig.json index c5c49c56..67194bd9 100644 --- a/packages/http-client/tsconfig.json +++ b/packages/http-client/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "strict": false, + "strict": true, + "useUnknownInCatchVariables": false, "lib": ["DOM", "es2022"], "rootDir": "./", From 589edc3aa0eac173d1c710f1f15f9cf9cfb983ec Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Tue, 13 Feb 2024 09:58:36 -0800 Subject: [PATCH 3/6] Add exchange state machine (#168) * Add exchange state machine * Fix tsdoc warnings * lint * Changeset * Additional tests * Fix --- .changeset/green-cheetahs-share.md | 5 + packages/protocol/src/exchange.ts | 124 ++++++++ packages/protocol/src/main.ts | 1 + packages/protocol/src/message-kinds/rfq.ts | 2 +- packages/protocol/tests/exchange.spec.ts | 340 +++++++++++++++++++++ 5 files changed, 471 insertions(+), 1 deletion(-) create mode 100644 .changeset/green-cheetahs-share.md create mode 100644 packages/protocol/src/exchange.ts create mode 100644 packages/protocol/tests/exchange.spec.ts diff --git a/.changeset/green-cheetahs-share.md b/.changeset/green-cheetahs-share.md new file mode 100644 index 00000000..f964e5b6 --- /dev/null +++ b/.changeset/green-cheetahs-share.md @@ -0,0 +1,5 @@ +--- +"@tbdex/protocol": patch +--- + +Add exchange state machine diff --git a/packages/protocol/src/exchange.ts b/packages/protocol/src/exchange.ts new file mode 100644 index 00000000..85080aff --- /dev/null +++ b/packages/protocol/src/exchange.ts @@ -0,0 +1,124 @@ +import { Close, Order, OrderStatus, Quote, Rfq } from './message-kinds/index.js' +import { Message } from './message.js' +import { MessageKind } from './types.js' + +/** + * State-machine for validating the order and metadata of Tbdex messages in an exchange. + * + * This state-machine does not validate the {@link Message.signature} or {@link Message.data} + * of messages in the exchange. + * + * Either add messages in order one at a time using {@link Exchange.addNextMessage}, + * or add a list of unsorted messages in an exchange using {@link Exchange.addMessages} + * + * @beta + */ +export class Exchange { + /** Message sent by Alice to PFI to request a quote */ + rfq: Rfq | undefined + /** Message sent by the PFI in response to an RFQ */ + quote: Quote | undefined + /** Message sent by Alice to the PFI to accept a quote*/ + order: Order | undefined + /** Message sent by the PFI to Alice to convet the current status of the order */ + orderstatus: OrderStatus | undefined + /** Message sent by either the PFI or Alice to terminate an exchange */ + close: Close | undefined + + constructor() {} + + /** + * Add a list of unsorted messages to an exchange. + * @param messages - An unsorted array of Tbdex messages in a given exchange + */ + addMessages(messages: Message[]): void { + // Sort with earliest dateCreated first + const sortedMessages = messages.sort((m1, m2) => { + const time1 = new Date(m1.metadata.createdAt).getTime() + const time2 = new Date(m2.metadata.createdAt).getTime() + return time1 - time2 + }) + + for (const message of sortedMessages) { + this.addNextMessage(message) + } + } + + /** + * Add the next message in the exchange + * @param message - The next allowed message in the exchange + * @throws if message is not a valid next message. See {@link Exchange.isValidNext} + */ + addNextMessage(message: Message): void { + if (!this.isValidNext(message.metadata.kind)) { + throw new Error( + `Could not add message (${message.metadata.id}) to exchange because ${message.metadata.kind} ` + + `is not a valid next message` + ) + } + + if (this.exchangeId !== undefined && message.metadata.exchangeId !== this.exchangeId) { + throw new Error( + `Could not add message with id ${message.metadata.id} to exchange because it does not have matching ` + + `exchange id ${this.exchangeId}` + ) + } + + if (message.isRfq()) { + this.rfq = message + } else if (message.isQuote()) { + this.quote = message + } else if (message.isClose()) { + this.close = message + } else if (message.isOrder()) { + this.order = message + } else if (message.isOrderStatus()) { + this.orderstatus = message + } else { + // Unreachable + throw new Error('Unrecognized message kind') + } + } + + /** + * Determines if the message kind is a valid next message in the current exchange + * @param messageKind - the kind of TBDex message + * @returns true if the next message in the exchange may have kind messageKind, false otherwise + */ + isValidNext(messageKind: MessageKind): boolean { + const validNext = this.latestMessage?.validNext ?? new Set(['rfq']) + return validNext.has(messageKind) + } + + /** + * Latest message in an exchange if there are any messages currently + */ + get latestMessage(): Message | undefined { + return this.close ?? + this.orderstatus ?? + this.order ?? + this.quote ?? + this.rfq + } + + /** + * The exchangeId of all messages in the Exchange + */ + get exchangeId(): string | undefined { + return this.rfq?.metadata?.exchangeId + } + + /** + * A sorted list of messages currently in the exchange. + */ + get messages(): Message[] { + const allPossibleMessages: (Message | undefined)[] = [ + this.rfq, + this.quote, + this.order, + this.orderstatus, + this.close + ] + return allPossibleMessages.filter((message): message is Message => message !== undefined) + } +} \ No newline at end of file diff --git a/packages/protocol/src/main.ts b/packages/protocol/src/main.ts index 5d62470e..c34c4b7c 100644 --- a/packages/protocol/src/main.ts +++ b/packages/protocol/src/main.ts @@ -13,6 +13,7 @@ import { Message } from './message.js' export * from './resource-kinds/index.js' export * from './message-kinds/index.js' +export * from './exchange.js' export * from './did-resolver.js' export * from './dev-tools.js' export * from './crypto.js' diff --git a/packages/protocol/src/message-kinds/rfq.ts b/packages/protocol/src/message-kinds/rfq.ts index d9b05309..c6b16479 100644 --- a/packages/protocol/src/message-kinds/rfq.ts +++ b/packages/protocol/src/message-kinds/rfq.ts @@ -17,7 +17,7 @@ export type CreateRfqOptions = { } /** - * Message sent by Alice to PFI to requesting for a quote (RFQ) + * Message sent by Alice to PFI to request a quote (RFQ) * @beta */ export class Rfq extends Message { diff --git a/packages/protocol/tests/exchange.spec.ts b/packages/protocol/tests/exchange.spec.ts new file mode 100644 index 00000000..e07a3d5a --- /dev/null +++ b/packages/protocol/tests/exchange.spec.ts @@ -0,0 +1,340 @@ +import { PortableDid } from '@web5/dids' +import { expect } from 'chai' +import { Close, DevTools, Exchange, Message, Order, OrderStatus, Quote, Rfq } from '../src/main.js' + +describe('Exchange', () => { + let aliceDid: PortableDid + let pfiDid: PortableDid + let rfq: Rfq + let quote: Quote + let closeByAlice: Close + let closeByPfi: Close + let order: Order + let orderStatus: OrderStatus + + beforeEach(async () => { + aliceDid = await DevTools.createDid() + pfiDid = await DevTools.createDid() + + rfq = Rfq.create({ + metadata: { + from : aliceDid.did, + to : pfiDid.did, + }, + data: await DevTools.createRfqData() + }) + await rfq.sign(aliceDid) + + closeByAlice = Close.create({ + metadata: { + from : aliceDid.did, + to : pfiDid.did, + exchangeId : rfq.metadata.exchangeId, + }, + data: { + reason: 'I dont like u anymore' + } + }) + await closeByAlice.sign(aliceDid) + + quote = Quote.create({ + metadata: { + from : pfiDid.did, + to : aliceDid.did, + exchangeId : rfq.metadata.exchangeId + }, + data: DevTools.createQuoteData() + }) + await quote.sign(pfiDid) + + closeByPfi = Close.create({ + metadata: { + from : pfiDid.did, + to : aliceDid.did, + exchangeId : rfq.metadata.exchangeId, + }, + data: { + reason: 'I dont like u anymore' + } + }) + await closeByPfi.sign(pfiDid) + + order = Order.create({ + metadata: { + from : aliceDid.did, + to : pfiDid.did, + exchangeId : rfq.metadata.exchangeId + }, + }) + await order.sign(aliceDid) + + orderStatus = OrderStatus.create({ + metadata: { + from : pfiDid.did, + to : aliceDid.did, + exchangeId : rfq.metadata.exchangeId, + }, + data: { + orderStatus: 'Done' + } + }) + }) + + describe('addMessages', () => { + it('adds an Rfq', async () => { + const exchange = new Exchange() + exchange.addMessages([rfq]) + + expect(exchange.rfq).to.deep.eq(rfq) + }) + + it('adds a list of messages in an exchange even if the list is out of order', async () => { + const exchange = new Exchange() + + // Messages are listed out of order + exchange.addMessages([order, quote, orderStatus, rfq]) + + expect(exchange.rfq).to.deep.eq(rfq) + expect(exchange.quote).to.deep.eq(quote) + expect(exchange.order).to.deep.eq(order) + expect(exchange.orderstatus).to.deep.eq(orderStatus) + }) + + it('throws if the messages listed do not form a valid exchange', async () => { + // scenario: We try to add messages RFQ and Order, without a Quote + + const exchange = new Exchange() + try { + exchange.addMessages([rfq, order]) + expect.fail() + } catch (e) { + expect(e.message).to.contain('is not a valid next message') + } + }) + + it('throws if the messages listed do not have matching exchange_id', async () => { + const quote = Quote.create({ + metadata: { + from : pfiDid.did, + to : aliceDid.did, + exchangeId : Message.generateId('rfq') + }, + data: DevTools.createQuoteData() + }) + await quote.sign(pfiDid) + + const exchange = new Exchange() + try { + exchange.addMessages([rfq, quote]) + expect.fail() + } catch (e) { + expect(e.message).to.contain('to exchange because it does not have matching exchange id') + } + }) + + it('throws if the messages listed have timestamp after Close', async () => { + const aliceDid = await DevTools.createDid() + const pfiDid = await DevTools.createDid() + + const close = Close.create({ + metadata: { + from : aliceDid.did, + to : pfiDid.did, + exchangeId : rfq.metadata.exchangeId, + }, + data: { + reason: 'I dont like u anymore' + } + }) + await close.sign(aliceDid) + + const quote = Quote.create({ + metadata: { + from : pfiDid.did, + to : aliceDid.did, + exchangeId : rfq.metadata.exchangeId + }, + data: DevTools.createQuoteData() + }) + await quote.sign(pfiDid) + + const exchange = new Exchange() + try { + exchange.addMessages([rfq, close, quote]) + expect.fail() + } catch (e) { + expect(e.message).to.contain('is not a valid next message') + } + }) + }) + + describe('addNextMessage', () => { + describe('message sequence', () => { + it('can add an Rfq first but not other message kinds first', async () => { + const exchange = new Exchange() + for (const message of [quote, closeByAlice, closeByPfi, order, orderStatus]) { + try { + exchange.addNextMessage(message) + expect.fail() + } catch (e) { + expect(e.message).to.contain('is not a valid next message') + } + } + + exchange.addNextMessage(rfq) + expect(exchange.rfq).to.deep.eq(rfq) + }) + + it('cannot add an Order, OrderStatus, or Rfq after Rfq', async () => { + const exchange = new Exchange() + exchange.addNextMessage(rfq) + + for (const message of [rfq, order, orderStatus]) { + try { + exchange.addNextMessage(message) + expect.fail() + } catch (e) { + expect(e.message).to.contain('is not a valid next message') + } + } + }) + + it('can add a Quote after Rfq', async () => { + const exchange = new Exchange() + exchange.addNextMessage(rfq) + + exchange.addNextMessage(quote) + expect(exchange.quote).to.deep.eq(quote) + }) + + it('can add a Close after Rfq', async () => { + const exchange = new Exchange() + exchange.addNextMessage(rfq) + + exchange.addNextMessage(closeByAlice) + expect(exchange.close).to.deep.eq(closeByAlice) + }) + + it('can add a Close after Quote', async () => { + const exchange = new Exchange() + exchange.addMessages([rfq, quote]) + + exchange.addNextMessage(closeByPfi) + expect(exchange.close).to.deep.eq(closeByPfi) + }) + + it('cannot add Rfq, Quote, Order, OrderStatus, or Close after Close', async () => { + const exchange = new Exchange() + exchange.addMessages([rfq, quote]) + exchange.addNextMessage(closeByAlice) + + for (const message of [rfq, quote, order, orderStatus, closeByAlice]) { + try { + exchange.addNextMessage(message) + expect.fail() + } catch (e) { + expect(e.message).to.contain('is not a valid next message') + } + } + }) + + it('can add an Order after Quote', async () => { + const exchange = new Exchange() + + exchange.addMessages([rfq, quote]) + + exchange.addNextMessage(order) + expect(exchange.order).to.deep.eq(order) + }) + + it('cannot add Rfq, Quote, or OrderStatus after Quote', async () => { + const exchange = new Exchange() + exchange.addMessages([rfq, quote]) + + for (const message of [rfq, quote, orderStatus]) { + try { + exchange.addNextMessage(message) + expect.fail() + } catch (e) { + expect(e.message).to.contain('is not a valid next message') + } + } + }) + + it('can add an OrderStatus after Order', async () => { + const exchange = new Exchange() + + exchange.addMessages([rfq, quote, order]) + + exchange.addNextMessage(orderStatus) + expect(exchange.orderstatus).to.deep.eq(orderStatus) + }) + + it('cannot add Rfq, Quote, Order, or Close after Order', async () => { + const exchange = new Exchange() + exchange.addMessages([rfq, quote, order]) + + for (const message of [rfq, quote, order, closeByAlice]) { + try { + exchange.addNextMessage(message) + expect.fail() + } catch (e) { + expect(e.message).to.contain('is not a valid next message') + } + } + }) + }) + }) + + describe('messages', () => { + it('returns the list of messages in the exchange', async () => { + const aliceDid = await DevTools.createDid() + const pfiDid = await DevTools.createDid() + + const rfq = Rfq.create({ + metadata: { + from : aliceDid.did, + to : pfiDid.did, + }, + data: await DevTools.createRfqData() + }) + await rfq.sign(aliceDid) + + const quote = Quote.create({ + metadata: { + from : pfiDid.did, + to : aliceDid.did, + exchangeId : rfq.metadata.exchangeId + }, + data: DevTools.createQuoteData() + }) + await quote.sign(pfiDid) + + const order = Order.create({ + metadata: { + from : aliceDid.did, + to : pfiDid.did, + exchangeId : rfq.metadata.exchangeId + }, + }) + await order.sign(aliceDid) + + const orderStatus = OrderStatus.create({ + metadata: { + from : pfiDid.did, + to : aliceDid.did, + exchangeId : rfq.metadata.exchangeId, + }, + data: { + orderStatus: 'Done' + } + }) + await orderStatus.sign(pfiDid) + + const exchange = new Exchange() + exchange.addMessages([rfq, quote, order, orderStatus]) + + expect(exchange.messages).to.deep.eq([rfq, quote, order, orderStatus]) + }) + }) +}) \ No newline at end of file From eba04b89ac89f745d175ede7124f8b2bdc687411 Mon Sep 17 00:00:00 2001 From: kirahsapong <102400653+kirahsapong@users.noreply.github.com> Date: Fri, 16 Feb 2024 09:25:55 -0800 Subject: [PATCH 4/6] Update web5/dids, web5/credentials, web5/crypto, web5/common to latest (#177) * Update http-client * Update http-server * Update protocol * update web5/common, web5/credentials, web5/crypto, web5/dids to latest * remove ion, replace key with jwk, remove commented lines, refactor tests, address pr comments * update tbdex * cleanup console log * update changeset --- .changeset/breezy-islands-play.md | 11 + packages/http-client/package.json | 8 +- packages/http-client/src/client.ts | 15 +- packages/http-client/tests/client.spec.ts | 112 +++-- packages/http-server/package.json | 2 +- .../http-server/tests/create-exchange.spec.ts | 2 +- .../http-server/tests/get-exchanges.spec.ts | 6 +- .../http-server/tests/submit-close.spec.ts | 12 +- .../http-server/tests/submit-order.spec.ts | 4 +- packages/protocol/package.json | 8 +- packages/protocol/src/crypto.ts | 77 +-- packages/protocol/src/dev-tools.ts | 36 +- packages/protocol/src/did-resolver.ts | 10 +- packages/protocol/src/message.ts | 6 +- packages/protocol/src/resource.ts | 4 +- packages/protocol/tests/crypto.spec.ts | 8 +- packages/protocol/tests/exchange.spec.ts | 65 ++- .../protocol/tests/generate-test-vectors.ts | 43 +- packages/protocol/tests/offering.spec.ts | 20 +- packages/protocol/tests/parse.spec.ts | 8 +- packages/protocol/tests/rfq.spec.ts | 68 +-- pnpm-lock.yaml | 437 +++--------------- tbdex | 2 +- 23 files changed, 304 insertions(+), 660 deletions(-) create mode 100644 .changeset/breezy-islands-play.md diff --git a/.changeset/breezy-islands-play.md b/.changeset/breezy-islands-play.md new file mode 100644 index 00000000..eb2ecca0 --- /dev/null +++ b/.changeset/breezy-islands-play.md @@ -0,0 +1,11 @@ +--- +"@tbdex/http-client": minor +"@tbdex/http-server": minor +"@tbdex/protocol": minor +--- + +Upgrade packages web5/dids@0.4.0, web5/credentials@0.4.2, web5/crypto@0.4.0, web5/common@0.2.3 + +* Deprecate did:ion and did:key in favour of did:jwk and did:dht +* Migrate from `PortableDid` to `BearerDid` with the latest @web5/dids upgrade +* Replaces dependency on `Web5Crypto` with `BearerDid` signer abstraction for signing operations diff --git a/packages/http-client/package.json b/packages/http-client/package.json index 6fe652ad..4107897f 100644 --- a/packages/http-client/package.json +++ b/packages/http-client/package.json @@ -50,10 +50,10 @@ }, "dependencies": { "@tbdex/protocol": "workspace:*", - "@web5/common": "0.2.2", - "@web5/credentials": "0.4.1", - "@web5/crypto": "0.2.4", - "@web5/dids": "0.2.4", + "@web5/common": "0.2.3", + "@web5/credentials": "0.4.2", + "@web5/crypto": "0.4.0", + "@web5/dids": "0.4.0", "ms": "2.1.3", "query-string": "8.1.0", "typeid-js": "0.3.0" diff --git a/packages/http-client/src/client.ts b/packages/http-client/src/client.ts index e171cddd..bcf775ea 100644 --- a/packages/http-client/src/client.ts +++ b/packages/http-client/src/client.ts @@ -1,6 +1,6 @@ import type { JwtPayload } from '@web5/crypto' import type { ErrorDetail } from './types.js' -import type { DidDocument, PortableDid } from '@web5/dids' +import type { DidDocument, BearerDid } from '@web5/dids' import { MessageModel, Parser, @@ -30,7 +30,7 @@ import ms from 'ms' * @beta */ export type GenerateRequestTokenParams = { - requesterDid: PortableDid + requesterDid: BearerDid pfiDid: string } @@ -251,19 +251,20 @@ export class TbdexHttpClient { * @throws {@link RequestTokenSigningError} If an error occurs during the token generation. */ static async generateRequestToken(params: GenerateRequestTokenParams): Promise { + const { pfiDid, requesterDid } = params const now = Date.now() const exp = (now + ms('1m')) const jwtPayload: JwtPayload = { - aud : params.pfiDid, - iss : params.requesterDid.did, + aud : pfiDid, + iss : requesterDid.uri, exp : Math.floor(exp / 1000), iat : Math.floor(now / 1000), jti : typeid().getSuffix() } try { - return await Jwt.sign({ signerDid: params.requesterDid, payload: jwtPayload }) + return await Jwt.sign({ signerDid: requesterDid, payload: jwtPayload }) } catch(e) { throw new RequestTokenSigningError({ message: e.message, cause: e }) } @@ -351,7 +352,7 @@ export type GetExchangeOptions = { /** the exchange you want to fetch */ exchangeId: string /** the message author's DID */ - did: PortableDid + did: BearerDid } /** @@ -361,7 +362,7 @@ export type GetExchangeOptions = { export type GetExchangesOptions = { /** the DID of the PFI from whom you want to get offerings */ pfiDid: string - did: PortableDid, + did: BearerDid, filter?: { id: string | string[] } diff --git a/packages/http-client/tests/client.spec.ts b/packages/http-client/tests/client.spec.ts index e3d2eba1..3f183389 100644 --- a/packages/http-client/tests/client.spec.ts +++ b/packages/http-client/tests/client.spec.ts @@ -1,5 +1,5 @@ import { expect } from 'chai' -import { DidDhtMethod, DidKeyMethod, PortableDid } from '@web5/dids' +import { DidDht, DidJwk, BearerDid } from '@web5/dids' import { TbdexHttpClient, requestTokenRequiredClaims } from '../src/client.js' import { RequestError,ResponseError, @@ -12,19 +12,22 @@ import { } from '../src/errors/index.js' import { DevTools } from '@tbdex/protocol' import * as sinon from 'sinon' -import { JwtHeaderParams, JwtPayload, PrivateKeyJwk, Secp256k1 } from '@web5/crypto' +import { JwtHeaderParams, JwtPayload } from '@web5/crypto' import { Convert } from '@web5/common' import { Jwt } from '@web5/credentials' -const dhtDid = await DidDhtMethod.create({ - publish : true, - services : [{ - type : 'PFI', - id : 'pfi', - serviceEndpoint : 'https://localhost:9000' - }] +const pfiDid: BearerDid = await DidDht.create({ + options: { + services: [{ + type : 'PFI', + id : 'pfi', + serviceEndpoint : 'https://localhost:9000' + }] + } }) +const aliceDid: BearerDid = await DidJwk.create() + // TODO : Instead of stubbing fetch, consider using libraries like msw const fetchStub = sinon.stub(globalThis, 'fetch') const getPfiServiceEndpointStub = sinon.stub(TbdexHttpClient, 'getPfiServiceEndpoint') @@ -33,13 +36,6 @@ describe('client', () => { beforeEach(() => getPfiServiceEndpointStub.resolves('https://localhost:9000')) describe('sendMessage', () => { - let aliceDid: PortableDid - let pfiDid: PortableDid - - beforeEach(async () => { - aliceDid = await DevTools.createDid() - pfiDid = await DevTools.createDid() - }) it('throws RequestError if service endpoint url is garbage', async () => { getPfiServiceEndpointStub.resolves('garbage') @@ -80,7 +76,7 @@ describe('client', () => { expect(e).to.be.instanceof(ResponseError) expect(e.statusCode).to.exist expect(e.details).to.exist - expect(e.recipientDid).to.equal(pfiDid.did) + expect(e.recipientDid).to.equal(pfiDid.uri) expect(e.url).to.equal(`https://localhost:9000/exchanges/${rfq.metadata.exchangeId}/rfq`) } }) @@ -124,7 +120,7 @@ describe('client', () => { fetchStub.rejects({message: 'Failed to fetch on URL'}) try { - await TbdexHttpClient.getOfferings({ pfiDid: dhtDid.did }) + await TbdexHttpClient.getOfferings({ pfiDid: pfiDid.uri }) expect.fail() } catch(e) { expect(e.name).to.equal('RequestError') @@ -145,14 +141,14 @@ describe('client', () => { } as Response) try { - await TbdexHttpClient.getOfferings({ pfiDid: dhtDid.did }) + await TbdexHttpClient.getOfferings({ pfiDid: pfiDid.uri }) 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.recipientDid).to.equal(pfiDid.uri) expect(e.url).to.equal('https://localhost:9000/offerings') } }) @@ -163,7 +159,7 @@ describe('client', () => { json : () => Promise.resolve({ data: [] }) } as Response) - const offerings = await TbdexHttpClient.getOfferings({ pfiDid: dhtDid.did }) + const offerings = await TbdexHttpClient.getOfferings({ pfiDid: pfiDid.uri }) expect(offerings).to.have.length(0) }) }) @@ -174,7 +170,7 @@ describe('client', () => { fetchStub.rejects({message: 'Failed to fetch on URL'}) try { - await TbdexHttpClient.getExchange({ pfiDid: dhtDid.did, exchangeId: '123', did: dhtDid }) + await TbdexHttpClient.getExchange({ pfiDid: pfiDid.uri, exchangeId: '123', did: pfiDid }) expect.fail() } catch(e) { expect(e.name).to.equal('RequestError') @@ -195,14 +191,14 @@ describe('client', () => { } as Response) try { - await TbdexHttpClient.getExchange({ pfiDid: dhtDid.did, exchangeId: '123', did: dhtDid }) + await TbdexHttpClient.getExchange({ pfiDid: pfiDid.uri, exchangeId: '123', did: pfiDid }) 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.recipientDid).to.equal(pfiDid.uri) expect(e.url).to.equal('https://localhost:9000/exchanges/123') } }) @@ -213,7 +209,7 @@ describe('client', () => { json : () => Promise.resolve({ data: [] }) } as Response) - const exchanges = await TbdexHttpClient.getExchange({ pfiDid: dhtDid.did, exchangeId: '123', did: dhtDid }) + const exchanges = await TbdexHttpClient.getExchange({ pfiDid: pfiDid.uri, exchangeId: '123', did: pfiDid }) expect(exchanges).to.have.length(0) }) }) @@ -224,7 +220,7 @@ describe('client', () => { fetchStub.rejects({message: 'Failed to fetch on URL'}) try { - await TbdexHttpClient.getExchanges({ pfiDid: dhtDid.did, did: dhtDid }) + await TbdexHttpClient.getExchanges({ pfiDid: pfiDid.uri, did: pfiDid }) expect.fail() } catch(e) { expect(e.name).to.equal('RequestError') @@ -245,14 +241,14 @@ describe('client', () => { } as Response) try { - await TbdexHttpClient.getExchanges({ pfiDid: dhtDid.did, did: dhtDid }) + await TbdexHttpClient.getExchanges({ pfiDid: pfiDid.uri, did: pfiDid }) 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.recipientDid).to.equal(pfiDid.uri) expect(e.url).to.equal('https://localhost:9000/exchanges') } }) @@ -263,7 +259,7 @@ describe('client', () => { json : () => Promise.resolve({ data: [] }) } as Response) - const exchanges = await TbdexHttpClient.getExchanges({ pfiDid: dhtDid.did, did: dhtDid }) + const exchanges = await TbdexHttpClient.getExchanges({ pfiDid: pfiDid.uri, did: pfiDid }) expect(exchanges).to.have.length(0) }) }) @@ -285,10 +281,8 @@ describe('client', () => { } }) it('throws MissingServiceEndpointError if did has no PFI service endpoint', async () => { - const keyDid = await DidKeyMethod.create() - try { - await TbdexHttpClient.getPfiServiceEndpoint(keyDid.did) + await TbdexHttpClient.getPfiServiceEndpoint(aliceDid.uri) expect.fail() } catch(e) { expect(e.name).to.equal('MissingServiceEndpointError') @@ -297,73 +291,75 @@ describe('client', () => { } }) it('returns pfi service endpoint if all is well', async () => { - const serviceEndpoint = await TbdexHttpClient.getPfiServiceEndpoint(dhtDid.did) + const serviceEndpoint = await TbdexHttpClient.getPfiServiceEndpoint(pfiDid.uri) expect(serviceEndpoint).to.equal('https://localhost:9000') }) }) describe('generateRequestToken', () => { - let requesterPortableDid: PortableDid + let requesterBearerDid: BearerDid before(async () => { - requesterPortableDid = await DidKeyMethod.create({ keyAlgorithm: 'secp256k1' }) - }) - it('throws a RequestTokenSigningError if requesterDid is not a valid PortableDid', async () => { - try { - await TbdexHttpClient.generateRequestToken({ requesterDid: {did: '', document: { id: '' }, keySet: {}}, pfiDid: '' }) - expect.fail() - } catch (e) { - expect(e).to.be.instanceOf(RequestTokenSigningError) - } + requesterBearerDid = await DidJwk.create() }) it('includes all expected claims', async () => { - const requestToken = await TbdexHttpClient.generateRequestToken({ requesterDid: requesterPortableDid, pfiDid: 'did:key:1234' }) + const requestToken = await TbdexHttpClient.generateRequestToken({ requesterDid: requesterBearerDid, pfiDid: 'did:key:1234' }) const decodedToken = await Jwt.verify({ jwt: requestToken }) expect(decodedToken.payload).to.have.all.keys(requestTokenRequiredClaims) }) // TODO: decide if we want to ensure that the expiration date is not longer than 1 minute after the issuance date 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 requestToken = await TbdexHttpClient.generateRequestToken({ requesterDid: requesterBearerDid, pfiDid: 'did:key:1234' }) const decodedToken = await Jwt.verify({ jwt: requestToken }) expect(decodedToken.payload.exp! - decodedToken.payload.iat!).to.equal(60) }) }) describe('verifyRequestToken', () => { - let pfiPortableDid: PortableDid let header: JwtHeaderParams let payload: JwtPayload + /* + ** helper function to help alice generate a valid request token to send to a pfi + */ async function createRequestTokenFromPayload(payload: JwtPayload) { - const privateKeyJwk = pfiPortableDid.keySet.verificationMethodKeys![0].privateKeyJwk + const signer = await pfiDid.getSigner() + header = { typ: 'JWT', alg: signer.algorithm, kid: signer.keyId } const base64UrlEncodedHeader = Convert.object(header).toBase64Url() const base64UrlEncodedPayload = Convert.object(payload).toBase64Url() const toSign = `${base64UrlEncodedHeader}.${base64UrlEncodedPayload}` const toSignBytes = Convert.string(toSign).toUint8Array() - const signatureBytes = await Secp256k1.sign({ key: privateKeyJwk as PrivateKeyJwk, data: toSignBytes }) + const signatureBytes = await signer.sign({ data: toSignBytes }) const base64UrlEncodedSignature = Convert.uint8Array(signatureBytes).toBase64Url() return `${toSign}.${base64UrlEncodedSignature}` } - before(async () => { - pfiPortableDid = await DidKeyMethod.create({ keyAlgorithm: 'secp256k1' }) - header = { typ: 'JWT', alg: 'ES256K', kid: pfiPortableDid.document.verificationMethod![0].id } - }) - beforeEach(() => { payload = { iat : Math.floor(Date.now() / 1000), - aud : pfiPortableDid.did, + aud : pfiDid.uri, iss : 'did:key:1234', exp : Math.floor(Date.now() / 1000 + 60), jti : 'randomnonce' } }) + it('throws a RequestTokenSigningError if token cannot be signed', async () => { + const jwtSigner = sinon.stub(Jwt, 'sign') + jwtSigner.throws() + try { + await TbdexHttpClient.generateRequestToken({ requesterDid: aliceDid, pfiDid: ''}) + expect.fail() + } catch (e) { + expect(e).to.be.instanceOf(RequestTokenSigningError) + } + jwtSigner.restore() + }) + it('throws RequestTokenVerificationError if request token is not a valid jwt', async () => { try { - await TbdexHttpClient.verifyRequestToken({ requestToken: '', pfiDid: pfiPortableDid.did }) + await TbdexHttpClient.verifyRequestToken({ requestToken: '', pfiDid: pfiDid.uri }) expect.fail() } catch(e) { expect(e).to.be.instanceof(RequestTokenVerificationError) @@ -375,7 +371,7 @@ describe('client', () => { try { delete payload[claim] const requestToken = await createRequestTokenFromPayload(payload) - await TbdexHttpClient.verifyRequestToken({ requestToken, pfiDid: pfiPortableDid.did }) + await TbdexHttpClient.verifyRequestToken({ requestToken, pfiDid: pfiDid.uri }) expect.fail() } catch(e) { expect(e).to.be.instanceof(RequestTokenMissingClaimsError) @@ -388,7 +384,7 @@ describe('client', () => { try { payload.aud = 'squirtle' const requestToken = await createRequestTokenFromPayload(payload) - await TbdexHttpClient.verifyRequestToken({ requestToken, pfiDid: pfiPortableDid.did }) + await TbdexHttpClient.verifyRequestToken({ requestToken, pfiDid: pfiDid.uri }) expect.fail() } catch(e) { expect(e).to.be.instanceof(RequestTokenAudienceMismatchError) @@ -397,7 +393,7 @@ describe('client', () => { }) it('returns requester\'s DID if request token is valid', async () => { const requestToken = await createRequestTokenFromPayload(payload) - const iss = await TbdexHttpClient.verifyRequestToken({ requestToken, pfiDid: pfiPortableDid.did }) + const iss = await TbdexHttpClient.verifyRequestToken({ requestToken, pfiDid: pfiDid.uri }) expect(iss).to.equal('did:key:1234') }) }) diff --git a/packages/http-server/package.json b/packages/http-server/package.json index 471a104d..cd690641 100644 --- a/packages/http-server/package.json +++ b/packages/http-server/package.json @@ -23,7 +23,7 @@ "dependencies": { "@tbdex/http-client": "workspace:*", "@tbdex/protocol": "workspace:*", - "@web5/dids": "0.2.2", + "@web5/dids": "0.4.0", "cors": "2.8.5", "express": "4.18.2" }, diff --git a/packages/http-server/tests/create-exchange.spec.ts b/packages/http-server/tests/create-exchange.spec.ts index d64d4d78..14f64a64 100644 --- a/packages/http-server/tests/create-exchange.spec.ts +++ b/packages/http-server/tests/create-exchange.spec.ts @@ -50,7 +50,7 @@ describe('POST /exchanges/:exchangeId/rfq', () => { 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 pfiDid = await DevTools.createDid('dht') const rfq = await DevTools.createRfq({ sender: aliceDid, receiver: pfiDid }) await rfq.sign(aliceDid) diff --git a/packages/http-server/tests/get-exchanges.spec.ts b/packages/http-server/tests/get-exchanges.spec.ts index 4c7f8cd1..a4373846 100644 --- a/packages/http-server/tests/get-exchanges.spec.ts +++ b/packages/http-server/tests/get-exchanges.spec.ts @@ -2,7 +2,7 @@ import type { ExchangesApi, GetExchangesFilter, Message } from '../src/main.js' import type { Server } from 'http' import { TbdexHttpServer, Rfq, Quote, Order, OrderStatus, Close, TbdexHttpClient } from '../src/main.js' -import { DidKeyMethod } from '@web5/dids' +import { DidJwk } from '@web5/dids' import { expect } from 'chai' let api = new TbdexHttpServer() @@ -32,13 +32,13 @@ describe('GET /exchanges', () => { it(`passes the requester's did to getExchanges method`, async () => { let functionReached = false - const alice = await DidKeyMethod.create() + const alice = await DidJwk.create() const exchangesApi: ExchangesApi = { getExchanges: async function (opts: { filter: GetExchangesFilter }): Promise { functionReached = true expect(opts.filter.from).to.exist - expect(opts.filter.from).to.equal(alice.did) + expect(opts.filter.from).to.equal(alice.uri) return [] }, diff --git a/packages/http-server/tests/submit-close.spec.ts b/packages/http-server/tests/submit-close.spec.ts index 73db1b19..62395959 100644 --- a/packages/http-server/tests/submit-close.spec.ts +++ b/packages/http-server/tests/submit-close.spec.ts @@ -57,8 +57,8 @@ describe('POST /exchanges/:exchangeId/close', () => { it(`returns a 404 if the exchange doesn't exist`, async () => { const close = Close.create({ metadata: { - from : did.did, - to : did.did, + from : did.uri, + to : did.uri, exchangeId : '123' }, data: {} @@ -82,8 +82,8 @@ describe('POST /exchanges/:exchangeId/close', () => { it(`returns a 409 if close is not allowed based on the exchange's current state`, async () => { const close = Close.create({ metadata: { - from : did.did, - to : did.did, + from : did.uri, + to : did.uri, exchangeId : '123' }, data: {} @@ -95,8 +95,8 @@ describe('POST /exchanges/:exchangeId/close', () => { const close2 = Close.create({ metadata: { - from : did.did, - to : did.did, + from : did.uri, + to : did.uri, exchangeId : '123' }, data: {} diff --git a/packages/http-server/tests/submit-order.spec.ts b/packages/http-server/tests/submit-order.spec.ts index 6d6f49b2..a94f3fc6 100644 --- a/packages/http-server/tests/submit-order.spec.ts +++ b/packages/http-server/tests/submit-order.spec.ts @@ -52,8 +52,8 @@ describe('POST /exchanges/:exchangeId/order', () => { it(`returns a 404 if the exchange doesn't exist`, async () => { const order = Order.create({ metadata: { - from : did.did, - to : did.did, + from : did.uri, + to : did.uri, exchangeId : '123' } }) diff --git a/packages/protocol/package.json b/packages/protocol/package.json index 3c379627..962abcdc 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -51,10 +51,10 @@ "dependencies": { "@noble/hashes": "1.3.3", "@types/node": "^7.0.5", - "@web5/common": "0.2.2", - "@web5/credentials": "0.4.1", - "@web5/crypto": "0.2.4", - "@web5/dids": "0.2.4", + "@web5/common": "0.2.3", + "@web5/credentials": "0.4.2", + "@web5/crypto": "0.4.0", + "@web5/dids": "0.4.0", "ajv": "8.12.0", "bignumber.js": "^9.1.2", "canonicalize": "2.0.0", diff --git a/packages/protocol/src/crypto.ts b/packages/protocol/src/crypto.ts index 25582eb9..e8536440 100644 --- a/packages/protocol/src/crypto.ts +++ b/packages/protocol/src/crypto.ts @@ -1,22 +1,20 @@ import type { - CryptoAlgorithm, - Web5Crypto, JwsHeaderParams, - JwkParamsEcPrivate, - JwkParamsOkpPrivate, JwkParamsEcPublic, JwkParamsOkpPublic, PrivateKeyJwk, - PublicKeyJwk + PublicKeyJwk, } from '@web5/crypto' import { sha256 } from '@noble/hashes/sha256' import { Convert } from '@web5/common' -import { EcdsaAlgorithm, EdDsaAlgorithm } from '@web5/crypto' +import { LocalKeyManager } from '@web5/crypto' import { DidResolver, isVerificationMethod } from './did-resolver.js' import canonicalize from 'canonicalize' -import { PortableDid, VerificationMethod } from '@web5/dids' +import { BearerDid, DidVerificationMethod } from '@web5/dids' + +const keyManager = new LocalKeyManager() /** * Options passed to {@link Crypto.sign} @@ -28,7 +26,7 @@ export type SignOptions = { /** The payload to be signed. */ payload: Uint8Array, /** the DID to sign with */ - did: PortableDid, + did: BearerDid, } /** @@ -41,44 +39,11 @@ export type VerifyOptions = { signature: string } -/** - * Used as value for each supported named curved listed in {@link Crypto.algorithms} - * @beta - */ -type SignerValue = { - signer: CryptoAlgorithm, - options: T, - alg: JwsHeader['alg'], - crv: JsonWebKey['crv'] -} - -const secp256k1Signer: SignerValue = { - signer : new EcdsaAlgorithm(), - options : { name: 'ECDSA' }, - alg : 'ES256K', - crv : 'secp256k1' -} - -const ed25519Signer: SignerValue = { - signer : new EdDsaAlgorithm(), - options : { name: 'EdDSA' }, - alg : 'EdDSA', - crv : 'Ed25519' -} - /** * Cryptographic utility functions, such as hashing, signing, and verifying * @beta */ export class Crypto { - /** supported cryptographic algorithms. keys are `${alg}:${crv}`. */ - static algorithms: { [alg: string]: SignerValue } = { - 'ES256K:' : secp256k1Signer, - 'ES256K:secp256k1' : secp256k1Signer, - ':secp256k1' : secp256k1Signer, - 'EdDSA:Ed25519' : ed25519Signer - } - /** * Computes a digest of the payload by: * * JSON serializing the payload as per [RFC-8785: JSON Canonicalization Scheme](https://www.rfc-editor.org/rfc/rfc8785) @@ -104,30 +69,23 @@ export class Crypto { static async sign(opts: SignOptions) { const { did, payload, detached } = opts - const privateKeyJwk = did.keySet.verificationMethodKeys?.[0]?.privateKeyJwk as JwkParamsEcPrivate | JwkParamsOkpPrivate + const signer = await did.getSigner() - const algorithmName = privateKeyJwk?.['alg'] || '' - let namedCurve = Crypto.extractNamedCurve(privateKeyJwk) - const algorithmId = `${algorithmName}:${namedCurve}` - - const algorithm = this.algorithms[algorithmId] - if (!algorithm) { - throw new Error(`Algorithm (${algorithmId}) not supported`) - } - - let verificationMethodId = did.document.verificationMethod?.[0]?.id || '' + let verificationMethodId = signer.keyId if (verificationMethodId.startsWith('#')) { - verificationMethodId = `${did.did}${verificationMethodId}` + verificationMethodId = `${did.uri}${verificationMethodId}` } - const jwsHeader: JwsHeader = { alg: algorithm.alg, kid: verificationMethodId } + + + const jwsHeader: JwsHeader = { alg: signer.algorithm, kid: verificationMethodId } const base64UrlEncodedJwsHeader = Convert.object(jwsHeader).toBase64Url() const base64urlEncodedJwsPayload = Convert.uint8Array(payload).toBase64Url() const toSign = `${base64UrlEncodedJwsHeader}.${base64urlEncodedJwsPayload}` const toSignBytes = Convert.string(toSign).toUint8Array() - const signatureBytes = await algorithm.signer.sign({ key: privateKeyJwk, data: toSignBytes, algorithm: algorithm.options }) + const signatureBytes = await signer.sign({ data: toSignBytes }) const base64UrlEncodedSignature = Convert.uint8Array(signatureBytes).toBase64Url() if (detached) { @@ -171,9 +129,9 @@ export class Crypto { throw new Error('Signature verification failed: Expected JWS header to contain alg and kid') } - const dereferenceResult = await DidResolver.dereference({ didUrl: jwsHeader.kid }) + const dereferenceResult = await DidResolver.dereference(jwsHeader.kid) - const verificationMethod = dereferenceResult.contentStream as VerificationMethod + const verificationMethod = dereferenceResult.contentStream as DidVerificationMethod if (!isVerificationMethod(verificationMethod)) { // ensure that appropriate verification method was found throw new Error('Signature verification failed: Expected kid in JWS header to dereference to a DID Document Verification Method') } @@ -190,10 +148,7 @@ export class Crypto { const signatureBytes = Convert.base64Url(base64UrlEncodedSignature).toUint8Array() - const algorithmId = `${jwsHeader['alg']}:${Crypto.extractNamedCurve(publicKeyJwk)}` - const { signer, options } = Crypto.algorithms[algorithmId] - - const isLegit = await signer.verify({ algorithm: options, key: publicKeyJwk, data: signedDataBytes, signature: signatureBytes }) + const isLegit = await keyManager.verify({ key: publicKeyJwk, data: signedDataBytes, signature: signatureBytes }) if (!isLegit) { throw new Error('Signature verification failed: Integrity mismatch') diff --git a/packages/protocol/src/dev-tools.ts b/packages/protocol/src/dev-tools.ts index 98c6b8db..38f84116 100644 --- a/packages/protocol/src/dev-tools.ts +++ b/packages/protocol/src/dev-tools.ts @@ -1,8 +1,8 @@ import type { OfferingData, QuoteData, RfqData } from './types.js' -import type { PortableDid } from '@web5/dids' +import type { BearerDid } from '@web5/dids' -import { DidDhtMethod, DidIonMethod, DidKeyMethod } from '@web5/dids' +import { DidDht, DidJwk } from '@web5/dids' import { Offering } from './resource-kinds/index.js' import { Order, Rfq } from './message-kinds/index.js' import { Resource } from './resource.js' @@ -13,7 +13,7 @@ import { VerifiableCredential } from '@web5/credentials' * Supported DID Methods * @beta */ -export type DidMethodOptions = 'key' | 'ion' | 'dht' +export type DidMethodOptions = 'dht' | 'jwk' /** * Options passed to {@link DevTools.createRfq} @@ -21,14 +21,14 @@ export type DidMethodOptions = 'key' | 'ion' | 'dht' */ export type MessageOptions = { /** - * {@link @web5/dids#PortableDid} of the message sender. When generating RFQ, it is used to generate a random credential that fulfills the vcRequirements + * {@link @web5/dids#BearerDid} 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 + sender: BearerDid /** - * {@link @web5/dids#PortableDid} of the rfq receiver. + * {@link @web5/dids#BearerDid} of the rfq receiver. */ - receiver?: PortableDid + receiver?: BearerDid } /** @@ -38,15 +38,13 @@ export type MessageOptions = { export class DevTools { /** * creates and returns a DID - * @param didMethod - the type of DID to create. defaults to did:key + * @param didMethod - the type of DID to create. defaults to did:jwk */ - static async createDid(didMethod: DidMethodOptions = 'key') { - if (didMethod === 'key') { - return await DidKeyMethod.create() - } else if (didMethod === 'ion') { - return DidIonMethod.create() + static async createDid(didMethod: DidMethodOptions = 'jwk'): Promise { + if (didMethod === 'jwk') { + return await DidJwk.create() } else if (didMethod === 'dht') { - return DidDhtMethod.create() + return await DidDht.create() } else { throw new Error(`${didMethod} method not implemented.`) } @@ -182,7 +180,7 @@ export class DevTools { const rfqData: RfqData = await DevTools.createRfqData(opts) return Rfq.create({ - metadata : { from: sender.did, to: receiver?.did ?? 'did:ex:pfi' }, + metadata : { from: sender.uri, to: receiver?.uri ?? 'did:ex:pfi' }, data : rfqData }) } @@ -197,8 +195,8 @@ export class DevTools { return Order.create({ metadata: { - from : sender.did, - to : receiver?.did ?? 'did:ex:pfi', + from : sender.uri, + to : receiver?.uri ?? 'did:ex:pfi', exchangeId : Message.generateId('rfq') } }) @@ -213,8 +211,8 @@ export class DevTools { if (opts?.sender) { const vc = await VerifiableCredential.create({ type : 'YoloCredential', - issuer : opts.sender.did, - subject : opts.sender.did, + issuer : opts.sender.uri, + subject : opts.sender.uri, data : { 'beep': 'boop' } diff --git a/packages/protocol/src/did-resolver.ts b/packages/protocol/src/did-resolver.ts index ddb960a7..3e899e84 100644 --- a/packages/protocol/src/did-resolver.ts +++ b/packages/protocol/src/did-resolver.ts @@ -1,6 +1,6 @@ -import type { DidDocument, DidService, VerificationMethod } from '@web5/dids' +import type { DidDocument, DidService, DidVerificationMethod } from '@web5/dids' -import { DidResolver as Web5DidResolver, DidKeyMethod, DidIonMethod, DidDhtMethod } from '@web5/dids' +import { DidResolver as Web5DidResolver, DidDht, DidJwk, DidWeb } from '@web5/dids' /** * Can be used to resolve did:ion and did:key DIDs @@ -8,7 +8,7 @@ import { DidResolver as Web5DidResolver, DidKeyMethod, DidIonMethod, DidDhtMetho * @beta */ export const DidResolver = new Web5DidResolver({ - didResolvers: [DidIonMethod, DidKeyMethod, DidDhtMethod] + didResolvers: [DidDht, DidJwk, DidWeb] }) /** @@ -34,7 +34,7 @@ export async function resolveDid(did: string): Promise { * A DID Resource is either a DID Document, a DID Verification method or a DID Service * @beta */ -export type DidResource = DidDocument | VerificationMethod | DidService +export type DidResource = DidDocument | DidVerificationMethod | DidService /** * type guard for {@link @web5/dids#VerificationMethod} @@ -42,6 +42,6 @@ export type DidResource = DidDocument | VerificationMethod | DidService * @returns true if the didResource is a `VerificationMethod` * @beta */ -export function isVerificationMethod(didResource: DidResource | null): didResource is VerificationMethod { +export function isVerificationMethod(didResource: DidResource | null): didResource is DidVerificationMethod { return !!didResource && 'id' in didResource && 'type' in didResource && 'controller' in didResource } \ No newline at end of file diff --git a/packages/protocol/src/message.ts b/packages/protocol/src/message.ts index e7de5660..a9b0447c 100644 --- a/packages/protocol/src/message.ts +++ b/packages/protocol/src/message.ts @@ -3,7 +3,7 @@ import { Rfq, Quote, Order, OrderStatus, Close } from './message-kinds/index.js' import { Crypto } from './crypto.js' import { typeid } from 'typeid-js' -import { PortableDid } from '@web5/dids' +import { BearerDid } from '@web5/dids' import { validate } from './validator.js' /** @@ -49,8 +49,8 @@ export abstract class Message { * @param did - the signer's DID * @throws If the signature could not be produced */ - async sign(did: PortableDid): Promise { - this._signature = await Crypto.sign({ did: did, payload: this.digest(), detached: true }) + async sign(did: BearerDid): Promise { + this._signature = await Crypto.sign({ did, payload: this.digest(), detached: true }) } /** diff --git a/packages/protocol/src/resource.ts b/packages/protocol/src/resource.ts index 280b376c..adea113c 100644 --- a/packages/protocol/src/resource.ts +++ b/packages/protocol/src/resource.ts @@ -4,7 +4,7 @@ import type { Offering } from './resource-kinds/index.js' import { typeid } from 'typeid-js' import { Crypto } from './crypto.js' import { validate } from './validator.js' -import { PortableDid } from '@web5/dids' +import { BearerDid } from '@web5/dids' /** @@ -46,7 +46,7 @@ export abstract class Resource { * @param did - the signer's DID * @throws If the signature could not be produced */ - async sign(did: PortableDid): Promise { + async sign(did: BearerDid): Promise { this._signature = await Crypto.sign({ did, payload: this.digest(), detached: true }) } diff --git a/packages/protocol/tests/crypto.spec.ts b/packages/protocol/tests/crypto.spec.ts index 678a1f8c..a65145d9 100644 --- a/packages/protocol/tests/crypto.spec.ts +++ b/packages/protocol/tests/crypto.spec.ts @@ -6,7 +6,7 @@ import { Crypto, DevTools } from '../src/main.js' describe('Crypto', () => { describe('sign / verify', () => { it('works with did:ion', async () => { - const alice = await DevTools.createDid('ion') + const alice = await DevTools.createDid() const payload = { timestamp: new Date().toISOString() } const payloadBytes = Convert.object(payload).toUint8Array() @@ -15,7 +15,7 @@ describe('Crypto', () => { }).timeout(30_000) it('works with did:key', async () => { - const alice = await DevTools.createDid('key') + const alice = await DevTools.createDid() const payload = { timestamp: new Date().toISOString() } const payloadBytes = Convert.object(payload).toUint8Array() @@ -25,14 +25,14 @@ describe('Crypto', () => { }) it('works with detached content', async () => { - const alice = await DevTools.createDid('ion') + const alice = await DevTools.createDid() const payload = { timestamp: new Date().toISOString() } const payloadBytes = Convert.object(payload).toUint8Array() const token = await Crypto.sign({ did: alice, payload: payloadBytes, detached: true }) const did = await Crypto.verify({ signature: token, detachedPayload: payloadBytes }) - expect(alice.did).to.equal(did) + expect(alice.uri).to.equal(did) }).timeout(30_000) }) }) \ No newline at end of file diff --git a/packages/protocol/tests/exchange.spec.ts b/packages/protocol/tests/exchange.spec.ts index e07a3d5a..57a93c8e 100644 --- a/packages/protocol/tests/exchange.spec.ts +++ b/packages/protocol/tests/exchange.spec.ts @@ -1,10 +1,10 @@ -import { PortableDid } from '@web5/dids' +import { BearerDid } from '@web5/dids' import { expect } from 'chai' import { Close, DevTools, Exchange, Message, Order, OrderStatus, Quote, Rfq } from '../src/main.js' describe('Exchange', () => { - let aliceDid: PortableDid - let pfiDid: PortableDid + let aliceDid: BearerDid + let pfiDid: BearerDid let rfq: Rfq let quote: Quote let closeByAlice: Close @@ -14,12 +14,12 @@ describe('Exchange', () => { beforeEach(async () => { aliceDid = await DevTools.createDid() - pfiDid = await DevTools.createDid() + pfiDid = await DevTools.createDid('dht') rfq = Rfq.create({ metadata: { - from : aliceDid.did, - to : pfiDid.did, + from : aliceDid.uri, + to : pfiDid.uri, }, data: await DevTools.createRfqData() }) @@ -27,8 +27,8 @@ describe('Exchange', () => { closeByAlice = Close.create({ metadata: { - from : aliceDid.did, - to : pfiDid.did, + from : aliceDid.uri, + to : pfiDid.uri, exchangeId : rfq.metadata.exchangeId, }, data: { @@ -39,8 +39,8 @@ describe('Exchange', () => { quote = Quote.create({ metadata: { - from : pfiDid.did, - to : aliceDid.did, + from : pfiDid.uri, + to : aliceDid.uri, exchangeId : rfq.metadata.exchangeId }, data: DevTools.createQuoteData() @@ -49,8 +49,8 @@ describe('Exchange', () => { closeByPfi = Close.create({ metadata: { - from : pfiDid.did, - to : aliceDid.did, + from : pfiDid.uri, + to : aliceDid.uri, exchangeId : rfq.metadata.exchangeId, }, data: { @@ -61,8 +61,8 @@ describe('Exchange', () => { order = Order.create({ metadata: { - from : aliceDid.did, - to : pfiDid.did, + from : aliceDid.uri, + to : pfiDid.uri, exchangeId : rfq.metadata.exchangeId }, }) @@ -70,8 +70,8 @@ describe('Exchange', () => { orderStatus = OrderStatus.create({ metadata: { - from : pfiDid.did, - to : aliceDid.did, + from : pfiDid.uri, + to : aliceDid.uri, exchangeId : rfq.metadata.exchangeId, }, data: { @@ -115,8 +115,8 @@ describe('Exchange', () => { it('throws if the messages listed do not have matching exchange_id', async () => { const quote = Quote.create({ metadata: { - from : pfiDid.did, - to : aliceDid.did, + from : pfiDid.uri, + to : aliceDid.uri, exchangeId : Message.generateId('rfq') }, data: DevTools.createQuoteData() @@ -133,13 +133,10 @@ describe('Exchange', () => { }) it('throws if the messages listed have timestamp after Close', async () => { - const aliceDid = await DevTools.createDid() - const pfiDid = await DevTools.createDid() - const close = Close.create({ metadata: { - from : aliceDid.did, - to : pfiDid.did, + from : aliceDid.uri, + to : pfiDid.uri, exchangeId : rfq.metadata.exchangeId, }, data: { @@ -150,8 +147,8 @@ describe('Exchange', () => { const quote = Quote.create({ metadata: { - from : pfiDid.did, - to : aliceDid.did, + from : pfiDid.uri, + to : aliceDid.uri, exchangeId : rfq.metadata.exchangeId }, data: DevTools.createQuoteData() @@ -288,13 +285,11 @@ describe('Exchange', () => { describe('messages', () => { it('returns the list of messages in the exchange', async () => { - const aliceDid = await DevTools.createDid() - const pfiDid = await DevTools.createDid() const rfq = Rfq.create({ metadata: { - from : aliceDid.did, - to : pfiDid.did, + from : aliceDid.uri, + to : pfiDid.uri, }, data: await DevTools.createRfqData() }) @@ -302,8 +297,8 @@ describe('Exchange', () => { const quote = Quote.create({ metadata: { - from : pfiDid.did, - to : aliceDid.did, + from : pfiDid.uri, + to : aliceDid.uri, exchangeId : rfq.metadata.exchangeId }, data: DevTools.createQuoteData() @@ -312,8 +307,8 @@ describe('Exchange', () => { const order = Order.create({ metadata: { - from : aliceDid.did, - to : pfiDid.did, + from : aliceDid.uri, + to : pfiDid.uri, exchangeId : rfq.metadata.exchangeId }, }) @@ -321,8 +316,8 @@ describe('Exchange', () => { const orderStatus = OrderStatus.create({ metadata: { - from : pfiDid.did, - to : aliceDid.did, + from : pfiDid.uri, + to : aliceDid.uri, exchangeId : rfq.metadata.exchangeId, }, data: { diff --git a/packages/protocol/tests/generate-test-vectors.ts b/packages/protocol/tests/generate-test-vectors.ts index 81b849bc..4871d8d2 100644 --- a/packages/protocol/tests/generate-test-vectors.ts +++ b/packages/protocol/tests/generate-test-vectors.ts @@ -1,4 +1,3 @@ -import { DidKeyMethod } from '@web5/dids' import { VerifiableCredential } from '@web5/credentials' import { Close, DevTools, Message, Order, OrderStatus, Quote, Rfq } from '../src/main.js' import fs from 'fs' @@ -14,10 +13,10 @@ type TestVector = { } const generateParseOfferingVector = async () => { - const did = await DidKeyMethod.create() - const offering = DevTools.createOffering({ from: did.did }) + const pfiDid = await await DevTools.createDid('dht') + const offering = DevTools.createOffering({ from: pfiDid.uri }) - await offering.sign(did) + await offering.sign(pfiDid) return { description : 'Offering parses from string', @@ -28,16 +27,16 @@ const generateParseOfferingVector = async () => { } const generateParseQuoteVector = async () => { - const did = await DidKeyMethod.create() + const pfiDid = await DevTools.createDid('dht') const quote = Quote.create({ metadata: { exchangeId : Message.generateId('rfq'), - from : did.did, + from : pfiDid.uri, to : 'did:ex:pfi' }, data: DevTools.createQuoteData() }) - await quote.sign(did) + await quote.sign(pfiDid) return { description : 'Quote parses from string', @@ -48,20 +47,20 @@ const generateParseQuoteVector = async () => { } const generateParseRfqVector = async () => { - const did = await DidKeyMethod.create() + const aliceDid = await DevTools.createDid() const vc = await VerifiableCredential.create({ type : 'PuupuuCredential', - issuer : did.did, - subject : did.did, + issuer : aliceDid.uri, + subject : aliceDid.uri, data : { 'beep': 'boop' } }) - const vcJwt = await vc.sign({ did }) + const vcJwt = await vc.sign({ did: aliceDid }) const rfq = Rfq.create({ - metadata : { from: did.did, to: 'did:ex:pfi' }, + metadata : { from: aliceDid.uri, to: 'did:ex:pfi' }, data : { offeringId : 'abcd123', payinMethod : { @@ -84,7 +83,7 @@ const generateParseRfqVector = async () => { } }) - await rfq.sign(did) + await rfq.sign(aliceDid) return { description : 'RFQ parses from string', @@ -95,12 +94,12 @@ const generateParseRfqVector = async () => { } const generateParseOrderVector = async () => { - const did = await DidKeyMethod.create() + const aliceDid = await DevTools.createDid() const order = Order.create({ - metadata: { from: did.did, to: 'did:ex:pfi', exchangeId: 'abcd123' } + metadata: { from: aliceDid.uri, to: 'did:ex:pfi', exchangeId: 'abcd123' } }) - await order.sign(did) + await order.sign(aliceDid) return { description : 'Order parses from string', @@ -111,15 +110,15 @@ const generateParseOrderVector = async () => { } const generateParseCloseVector = async () => { - const did = await DidKeyMethod.create() + const pfiDid = await DevTools.createDid('dht') const close = Close.create({ - metadata : { from: did.did, to: 'did:ex:pfi', exchangeId: 'abcd123' }, + metadata : { from: pfiDid.uri, to: 'did:ex:alice', exchangeId: 'abcd123' }, data : { reason: 'The reason for closing the exchange' } }) - await close.sign(did) + await close.sign(pfiDid) return { description : 'Close parses from string', @@ -130,15 +129,15 @@ const generateParseCloseVector = async () => { } const generateParseOrderStatusVector = async () => { - const did = await DidKeyMethod.create() + const pfiDid = await DevTools.createDid() const orderStatus = OrderStatus.create({ - metadata : { from: did.did, to: 'did:ex:pfi', exchangeId: 'abcd123' }, + metadata : { from: pfiDid.uri, to: 'did:ex:alice', exchangeId: 'abcd123' }, data : { orderStatus: 'wee' } }) - await orderStatus.sign(did) + await orderStatus.sign(pfiDid) return { description : 'Order Status parses from string', diff --git a/packages/protocol/tests/offering.spec.ts b/packages/protocol/tests/offering.spec.ts index e8c30d9a..3527132d 100644 --- a/packages/protocol/tests/offering.spec.ts +++ b/packages/protocol/tests/offering.spec.ts @@ -36,9 +36,9 @@ describe('Offering', () => { describe('sign', () => { it('sets signature property', async () => { - const pfi = await DevTools.createDid() + const pfi = await DevTools.createDid('dht') const offering = Offering.create({ - metadata : { from: pfi.did }, + metadata : { from: pfi.uri }, data : DevTools.createOfferingData() }) @@ -50,9 +50,9 @@ describe('Offering', () => { }) it('includes alg and kid in jws header', async () => { - const pfi = await DevTools.createDid() + const pfi = await DevTools.createDid('dht') const offering = Offering.create({ - metadata : { from: pfi.did }, + metadata : { from: pfi.uri }, data : DevTools.createOfferingData() }) @@ -68,9 +68,9 @@ describe('Offering', () => { describe('verify', () => { it('does not throw an exception if resource integrity is intact', async () => { - const pfi = await DevTools.createDid() + const pfi = await DevTools.createDid('dht') const offering = Offering.create({ - metadata : { from: pfi.did }, + metadata : { from: pfi.uri }, data : DevTools.createOfferingData() }) @@ -79,9 +79,9 @@ describe('Offering', () => { }) it('throws an error if no signature is present on the resource provided', async () => { - const pfi = await DevTools.createDid() + const pfi = await DevTools.createDid('dht') const offering = Offering.create({ - metadata : { from: pfi.did }, + metadata : { from: pfi.uri }, data : DevTools.createOfferingData() }) @@ -113,9 +113,9 @@ describe('Offering', () => { }) it('returns a Resource instance if parsing is successful', async () => { - const pfi = await DevTools.createDid() + const pfi = await DevTools.createDid('dht') const offering = Offering.create({ - metadata : { from: pfi.did }, + metadata : { from: pfi.uri }, data : DevTools.createOfferingData() }) diff --git a/packages/protocol/tests/parse.spec.ts b/packages/protocol/tests/parse.spec.ts index ed1f853b..37d768d8 100644 --- a/packages/protocol/tests/parse.spec.ts +++ b/packages/protocol/tests/parse.spec.ts @@ -4,9 +4,9 @@ import { DevTools, Parser } from '../src/main.js' describe('Parser', () => { describe('parseMessage', async () => { it('throws if an unrecognized message kind is passed', async () => { - const did = await DevTools.createDid() + const aliceDid = await DevTools.createDid() const unrecognizedMessageKind = { - metadata : { from: did.did, to: 'did:ex:pfi' }, + metadata : { from: aliceDid.uri, to: 'did:ex:pfi' }, data : {}, signature : '1234', } @@ -24,9 +24,9 @@ describe('Parser', () => { describe('parseResource', async () => { it('throws if an unrecognized resource kind is passed', async () => { - const did = await DevTools.createDid() + const aliceDid = await DevTools.createDid() const unrecognizedResourceKind = { - metadata : { from: did.did, to: 'did:ex:pfi' }, + metadata : { from: aliceDid.uri, to: 'did:ex:pfi' }, data : {}, signature : '1234', } diff --git a/packages/protocol/tests/rfq.spec.ts b/packages/protocol/tests/rfq.spec.ts index 82fcaee8..6c82ffcc 100644 --- a/packages/protocol/tests/rfq.spec.ts +++ b/packages/protocol/tests/rfq.spec.ts @@ -8,9 +8,9 @@ import { expect } from 'chai' describe('Rfq', () => { describe('create', () => { it('creates an rfq', async () => { - const alice = await DevTools.createDid() + const aliceDid = await DevTools.createDid() const message = Rfq.create({ - metadata : { from: alice.did, to: 'did:ex:pfi' }, + metadata : { from: aliceDid.uri, to: 'did:ex:pfi' }, data : await DevTools.createRfqData() }) @@ -23,51 +23,51 @@ describe('Rfq', () => { describe('sign', () => { it('sets signature property', async () => { - const did = await DevTools.createDid() + const aliceDid = await DevTools.createDid() const rfq = Rfq.create({ - metadata : { from: did.did, to: 'did:ex:pfi' }, + metadata : { from: aliceDid.uri, to: 'did:ex:pfi' }, data : await DevTools.createRfqData() }) - await rfq.sign(did) + await rfq.sign(aliceDid) expect(rfq.signature).to.not.be.undefined expect(typeof rfq.signature).to.equal('string') }) it('includes alg and kid in jws header', async () => { - const did = await DevTools.createDid() + const aliceDid = await DevTools.createDid() const rfq = Rfq.create({ - metadata : { from: did.did, to: 'did:ex:pfi' }, + metadata : { from: aliceDid.uri, to: 'did:ex:pfi' }, data : await DevTools.createRfqData() }) - await rfq.sign(did) + await rfq.sign(aliceDid) const [base64UrlEncodedJwsHeader] = rfq.signature!.split('.') const jwsHeader: { kid?: string, alg?: string} = Convert.base64Url(base64UrlEncodedJwsHeader).toObject() - expect(jwsHeader['kid']).to.equal(did.document.verificationMethod![0].id) + expect(jwsHeader['kid']).to.equal(aliceDid.document.verificationMethod![0].id) expect(jwsHeader['alg']).to.exist }) }) describe('verify', () => { it('does not throw an exception if message integrity is intact', async () => { - const did = await DevTools.createDid() + const aliceDid = await DevTools.createDid() const rfq = Rfq.create({ - metadata : { from: did.did, to: 'did:ex:pfi' }, + metadata : { from: aliceDid.uri, to: 'did:ex:pfi' }, data : await DevTools.createRfqData() }) - await rfq.sign(did) + await rfq.sign(aliceDid) await rfq.verify() }) it('throws an error if no signature is present on the message provided', async () => { - const alice = await DevTools.createDid() + const aliceDid = await DevTools.createDid() const rfq = Rfq.create({ - metadata : { from: alice.did, to: 'did:ex:pfi' }, + metadata : { from: aliceDid.uri, to: 'did:ex:pfi' }, data : await DevTools.createRfqData() }) @@ -98,13 +98,13 @@ describe('Rfq', () => { }) it('returns an instance of Message if parsing is successful', async () => { - const did = await DevTools.createDid() + const aliceDid = await DevTools.createDid() const rfq = Rfq.create({ - metadata : { from: did.did, to: 'did:ex:pfi' }, + metadata : { from: aliceDid.uri, to: 'did:ex:pfi' }, data : await DevTools.createRfqData() }) - await rfq.sign(did) + await rfq.sign(aliceDid) const jsonMessage = JSON.stringify(rfq) const parsedMessage = await Rfq.parse(jsonMessage) @@ -134,18 +134,18 @@ describe('Rfq', () => { let rfqOptions: CreateRfqOptions beforeEach(async () => { - const did = await DevTools.createDid() + const aliceDid = await DevTools.createDid() const vc = await VerifiableCredential.create({ // this credential fulfills the offering's required claims type : 'SanctionsCredential', - issuer : did.did, - subject : did.did, + issuer : aliceDid.uri, + subject : aliceDid.uri, data : { 'beep': 'boop' } }) offering = DevTools.createOffering() - const vcJwt = await vc.sign({ did }) + const vcJwt = await vc.sign({ did: aliceDid }) rfqOptions = { metadata: { @@ -157,7 +157,7 @@ describe('Rfq', () => { offeringId: offering.id, } } - rfqOptions.metadata.from = did.did + rfqOptions.metadata.from = aliceDid.uri rfqOptions.data.claims = [vcJwt] }) @@ -167,13 +167,13 @@ describe('Rfq', () => { }) it('succeeds if Rfq satisfies required payin amount and Offering has no required claims', async () => { - const pfi = await DevTools.createDid() + const pfi = await DevTools.createDid('dht') const offeringData = DevTools.createOfferingData() offeringData.requiredClaims = undefined const offering = Offering.create({ metadata: { - from: pfi.did + from: pfi.uri }, data: offeringData }) @@ -345,24 +345,24 @@ describe('Rfq', () => { describe('verifyClaims', () => { it(`does not throw an exception if an rfq's claims fulfill the provided offering's requirements`, async () => { - const did = await DevTools.createDid() + const aliceDid = await DevTools.createDid() const offering = DevTools.createOffering() const vc = await VerifiableCredential.create({ // this credential fulfills the offering's required claims type : 'SanctionsCredential', - issuer : did.did, - subject : did.did, + issuer : aliceDid.uri, + subject : aliceDid.uri, data : { 'beep': 'boop' } }) - const vcJwt = await vc.sign({ did }) + const vcJwt = await vc.sign({ did: aliceDid }) const rfqData = await DevTools.createRfqData() rfqData.claims = [vcJwt] const rfq = Rfq.create({ - metadata : { from: did.did, to: 'did:ex:pfi' }, + metadata : { from: aliceDid.uri, to: 'did:ex:pfi' }, data : rfqData }) @@ -370,24 +370,24 @@ describe('Rfq', () => { }) it(`throws an exception if an rfq's claims dont fulfill the provided offering's requirements`, async () => { - const did = await DevTools.createDid() + const aliceDid = await DevTools.createDid() const offering = DevTools.createOffering() const vc = await VerifiableCredential.create({ type : 'PuupuuCredential', - issuer : did.did, - subject : did.did, + issuer : aliceDid.uri, + subject : aliceDid.uri, data : { 'beep': 'boop' } }) - const vcJwt = await vc.sign({ did}) + const vcJwt = await vc.sign({ did: aliceDid }) const rfqData = await DevTools.createRfqData() rfqData.claims = [vcJwt] const rfq = Rfq.create({ - metadata : { from: did.did, to: 'did:ex:pfi' }, + metadata : { from: aliceDid.uri, to: 'did:ex:pfi' }, data : rfqData }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d1b3d43..e27a8da5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -61,17 +61,17 @@ importers: specifier: workspace:* version: link:../protocol '@web5/common': - specifier: 0.2.2 - version: 0.2.2 + specifier: 0.2.3 + version: 0.2.3 '@web5/credentials': - specifier: 0.4.1 - version: 0.4.1 + specifier: 0.4.2 + version: 0.4.2 '@web5/crypto': - specifier: 0.2.4 - version: 0.2.4 + specifier: 0.4.0 + version: 0.4.0 '@web5/dids': - specifier: 0.2.4 - version: 0.2.4 + specifier: 0.4.0 + version: 0.4.0 ms: specifier: 2.1.3 version: 2.1.3 @@ -158,8 +158,8 @@ importers: specifier: workspace:* version: link:../protocol '@web5/dids': - specifier: 0.2.2 - version: 0.2.2 + specifier: 0.4.0 + version: 0.4.0 cors: specifier: 2.8.5 version: 2.8.5 @@ -204,17 +204,17 @@ importers: specifier: ^7.0.5 version: 7.10.14 '@web5/common': - specifier: 0.2.2 - version: 0.2.2 + specifier: 0.2.3 + version: 0.2.3 '@web5/credentials': - specifier: 0.4.1 - version: 0.4.1 + specifier: 0.4.2 + version: 0.4.2 '@web5/crypto': - specifier: 0.2.4 - version: 0.2.4 + specifier: 0.4.0 + version: 0.4.0 '@web5/dids': - specifier: 0.2.4 - version: 0.2.4 + specifier: 0.4.0 + version: 0.4.0 ajv: specifier: 8.12.0 version: 8.12.0 @@ -512,16 +512,6 @@ packages: prettier: 2.8.8 dev: true - /@decentralized-identity/ion-pow-sdk@1.0.17: - resolution: {integrity: sha512-vk7DTDM8aKDbFyu1ad/qkoRrGL4q+KvNeL/FNZXhkWPaDhVExBN/qGEoRLf1YSfFe+myto3+4RYTPut+riiqnw==} - dependencies: - buffer: 6.0.3 - cross-fetch: 3.1.5 - hash-wasm: 4.9.0 - transitivePeerDependencies: - - encoding - dev: false - /@decentralized-identity/ion-sdk@1.0.1: resolution: {integrity: sha512-+P+DXcRSFjsEsI5KIqUmVjpzgUT28B2lWpTO+IxiBcfibAN/1Sg20NebGTO/+serz2CnSZf95N2a1OZ6eXypGQ==} dependencies: @@ -533,6 +523,14 @@ packages: uri-js: 4.4.1 dev: false + /@dnsquery/dns-packet@6.1.1: + resolution: {integrity: sha512-WXTuFvL3G+74SchFAtz3FgIYVOe196ycvGsMgvSH/8Goptb1qpIQtIuM4SOK9G9lhMWYpHxnXyy544ZhluFOew==} + engines: {node: '>=6'} + dependencies: + '@leichtgewicht/ip-codec': 2.0.4 + utf8-codec: 1.0.0 + dev: false + /@esbuild/android-arm64@0.16.17: resolution: {integrity: sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==} engines: {node: '>=12'} @@ -864,40 +862,20 @@ packages: resolution: {integrity: sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw==} dev: false - /@noble/ciphers@0.1.4: - resolution: {integrity: sha512-d3ZR8vGSpy3v/nllS+bD/OMN5UZqusWiQqkyj7AwzTnhXFH72pF5oB4Ach6DQ50g5kXxC28LdaYBEpsyv9KOUQ==} - dev: false - - /@noble/ciphers@0.4.0: - resolution: {integrity: sha512-xaUaUUDWbHIFSxaQ/pIe+33VG2mfJp6N/KxKLmZr5biWdNznCAmfu24QRhX10BbVAuqOahAoyp0S4M9md6GPDw==} + /@noble/ciphers@0.4.1: + resolution: {integrity: sha512-QCOA9cgf3Rc33owG0AYBB9wszz+Ul2kramWN8tXG44Gyciud/tbkEqvxRF/IpqQaBpRBNi9f4jdNxqB2CQCIXg==} dev: false - /@noble/curves@1.1.0: - resolution: {integrity: sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==} + /@noble/curves@1.3.0: + resolution: {integrity: sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==} dependencies: - '@noble/hashes': 1.3.1 - dev: false - - /@noble/curves@1.2.0: - resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} - dependencies: - '@noble/hashes': 1.3.2 + '@noble/hashes': 1.3.3 dev: false /@noble/ed25519@2.0.0: resolution: {integrity: sha512-/extjhkwFupyopDrt80OMWKdLgP429qLZj+z6sYJz90rF2Iz0gjZh2ArMKPImUl13Kx+0EXI2hN9T/KJV0/Zng==} dev: false - /@noble/hashes@1.3.1: - resolution: {integrity: sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==} - engines: {node: '>= 16'} - dev: false - - /@noble/hashes@1.3.2: - resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} - engines: {node: '>= 16'} - dev: false - /@noble/hashes@1.3.3: resolution: {integrity: sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==} engines: {node: '>= 16'} @@ -1931,16 +1909,8 @@ packages: - utf-8-validate dev: true - /@web5/common@0.2.1: - resolution: {integrity: sha512-Tt5P17HgQCx+Epw0IHnhRKqp5UU3E4xtsE8PkdghOBnvntBB0op5P6efvR1WqmJft5+VunDHt3yZAZstuqQkNg==} - engines: {node: '>=18.0.0'} - dependencies: - level: 8.0.0 - multiformats: 11.0.2 - dev: false - - /@web5/common@0.2.2: - resolution: {integrity: sha512-dRn6SmALExeTLMTK/W5ozGarfaddK+Lraf5OjuIGLAaLfcX1RWx3oDMoY5Hr9LjfxHJC8mGXB8DnKflbeYJRgA==} + /@web5/common@0.2.3: + resolution: {integrity: sha512-WTbIS6l5inrQTS5cwOoQP5KEuUHZqOWCKCDAA62qGUn4QyySolog9Gt2HCNUEClIzzk/d5DHcpBgLCadHoJ6rQ==} engines: {node: '>=18.0.0'} dependencies: level: 8.0.0 @@ -1948,75 +1918,37 @@ packages: readable-stream: 4.4.2 dev: false - /@web5/credentials@0.4.1: - resolution: {integrity: sha512-GMbS/N9AqAURjH6+Mvo49AJJxe9uli5mU+dzXtGgFJhiwD4FfqRPxdO1C3R0Tj2aCY3OAQVcCKQgmLRm2hHZlw==} + /@web5/credentials@0.4.2: + resolution: {integrity: sha512-/IMeKDAqrugGuv1hX9rNEEPlj3AiGFszWjoblGec0lw2+JMy0ZIXatCU5gujg8/ejdDFzO7wu9TsQa0SKzGWXw==} engines: {node: '>=18.0.0'} dependencies: '@sphereon/pex': 2.1.0 - '@web5/common': 0.2.2 - '@web5/crypto': 0.2.4 - '@web5/dids': 0.2.4 - transitivePeerDependencies: - - encoding - - supports-color - dev: false - - /@web5/crypto@0.2.2: - resolution: {integrity: sha512-vHFg0wXQSQXrwuBNQyDHnmSZchfTfO6/Sv+7rDsNkvofs+6lGTE8CZ02cwUYMeIwTRMLer12c+fMfzYrXokEUQ==} - engines: {node: '>=18.0.0'} - dependencies: - '@noble/ciphers': 0.1.4 - '@noble/curves': 1.1.0 - '@noble/hashes': 1.3.1 - '@web5/common': 0.2.1 - dev: false - - /@web5/crypto@0.2.4: - resolution: {integrity: sha512-heRUuV10mZ04dWp1C2mNF/EEPw8nnRe+yAXvmclJ+4XUHL6+mY7j+hjYOTKUAQzd4ouvbHrpJM0uYcUntA3AeA==} - engines: {node: '>=18.0.0'} - dependencies: - '@noble/ciphers': 0.4.0 - '@noble/curves': 1.2.0 - '@noble/hashes': 1.3.2 - '@web5/common': 0.2.2 + '@web5/common': 0.2.3 + '@web5/crypto': 0.4.0 + '@web5/dids': 0.4.0 dev: false - /@web5/dids@0.2.2: - resolution: {integrity: sha512-dARcpQIMzmayINWFemxP+shhfHLtYB+ZXoSZ1TKKnjF7qzraL30KwBSDwaNEPYaAzgo0x/1rvyv+zAgfa5MeNw==} + /@web5/crypto@0.4.0: + resolution: {integrity: sha512-GIKn2CizQKeATvHQqmC4ky26b3q2pJOd2GjIYsOSw/3Y3QIEm3holDGqu9FHs6kacmr6u0Pv5ELvccs58+cKEg==} engines: {node: '>=18.0.0'} dependencies: - '@decentralized-identity/ion-pow-sdk': 1.0.17 - '@decentralized-identity/ion-sdk': 1.0.1 - '@web5/common': 0.2.1 - '@web5/crypto': 0.2.2 - did-resolver: 4.1.0 - dns-packet: 5.6.1 - level: 8.0.0 - ms: 2.1.3 - pkarr: 1.1.1 - z32: 1.0.1 - transitivePeerDependencies: - - encoding - - supports-color + '@noble/ciphers': 0.4.1 + '@noble/curves': 1.3.0 + '@noble/hashes': 1.3.3 + '@web5/common': 0.2.3 dev: false - /@web5/dids@0.2.4: - resolution: {integrity: sha512-e+m+xgpiM8ydTJgWcPdwmjILLMZYdl2kwahlO22mK0azSKVrg1klpGrUODzqkrWrQ5O0tnOyqEy39FcD5Sy11w==} + /@web5/dids@0.4.0: + resolution: {integrity: sha512-e+QcrKWlWPJVBbpz4QKrdcgPoj+uC8YUXt4tGKj1mC4kV0rCtIioMLw9Djg+54pp3VXl3eGKxju98UEddyZwqA==} engines: {node: '>=18.0.0'} dependencies: - '@decentralized-identity/ion-pow-sdk': 1.0.17 '@decentralized-identity/ion-sdk': 1.0.1 - '@web5/common': 0.2.2 - '@web5/crypto': 0.2.2 - did-resolver: 4.1.0 - dns-packet: 5.6.1 + '@dnsquery/dns-packet': 6.1.1 + '@web5/common': 0.2.3 + '@web5/crypto': 0.4.0 + bencode: 4.0.0 level: 8.0.0 ms: 2.1.3 - pkarr: 1.1.1 - z32: 1.0.1 - transitivePeerDependencies: - - encoding - - supports-color dev: false /abort-controller@3.0.0: @@ -2286,6 +2218,7 @@ packages: /b4a@1.6.4: resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==} + dev: true /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -2304,17 +2237,6 @@ packages: engines: {node: '>=10.0.0'} dev: true - /bencode@2.0.3: - resolution: {integrity: sha512-D/vrAD4dLVX23NalHwb8dSvsUsxeRPO8Y7ToKA015JQYq69MLDOMkC0uGZYA/MPpltLO8rt8eqFC2j8DxjTZ/w==} - dev: false - - /bencode@3.1.1: - resolution: {integrity: sha512-btsxX9201yoWh45TdqYg6+OZ5O1xTYKTYSGvJndICDFtznE/9zXgow8yjMvvhOqKKuzuL7h+iiCMpfkG8+QuBA==} - engines: {node: '>=12.20.0'} - dependencies: - uint8-util: 2.2.4 - dev: false - /bencode@4.0.0: resolution: {integrity: sha512-AERXw18df0pF3ziGOCyUjqKZBVNH8HV3lBxnx5w0qtgMIk4a1wb9BkcCQbkp9Zstfrn/dzRwl7MmUHHocX3sRQ==} engines: {node: '>=12.20.0'} @@ -2338,36 +2260,6 @@ packages: engines: {node: '>=8'} dev: true - /bittorrent-dht@11.0.5: - resolution: {integrity: sha512-R09D6uNaziRqsc+B/j5QzkjceTak+wH9vcNLnkmt8A52EWF9lQwBP0vvCKgSA3AJOYYl+41n3osA2KYYn/z5uQ==} - engines: {node: '>=12.20.0'} - dependencies: - bencode: 4.0.0 - debug: 4.3.4(supports-color@8.1.1) - k-bucket: 5.1.0 - k-rpc: 5.1.0 - last-one-wins: 1.0.4 - lru: 3.1.0 - randombytes: 2.1.0 - record-cache: 1.2.0 - transitivePeerDependencies: - - supports-color - dev: false - - /blake2b-wasm@2.4.0: - resolution: {integrity: sha512-S1kwmW2ZhZFFFOghcx73+ZajEfKBqhP82JMssxtLVMxlaPea1p9uoLiUZ5WYyHn0KddwbLc+0vh4wR0KBNoT5w==} - dependencies: - b4a: 1.6.4 - nanoassert: 2.0.0 - dev: false - - /blake2b@2.1.4: - resolution: {integrity: sha512-AyBuuJNI64gIvwx13qiICz6H6hpmjvYS5DGkG6jbXMOT8Z3WUJ3V1X0FlhIoT1b/5JtHE3ki+xjtMvu1nn+t9A==} - dependencies: - blake2b-wasm: 2.4.0 - nanoassert: 2.0.0 - dev: false - /bn.js@4.12.0: resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} dev: true @@ -2607,12 +2499,6 @@ packages: engines: {node: '>=6'} dev: false - /chacha20-universal@1.0.4: - resolution: {integrity: sha512-/IOxdWWNa7nRabfe7+oF+jVkGjlr2xUL4J8l/OvzZhj+c9RpMqoo3Dq+5nU1j/BflRV4BKnaQ4+4oH1yBpQG1Q==} - dependencies: - nanoassert: 2.0.0 - dev: false - /chai@4.3.10: resolution: {integrity: sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==} engines: {node: '>=4'} @@ -2650,11 +2536,6 @@ packages: supports-color: 7.2.0 dev: true - /chalk@5.3.0: - resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - dev: false - /chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} dev: true @@ -2684,19 +2565,6 @@ packages: fsevents: 2.3.3 dev: true - /chrome-dgram@3.0.6: - resolution: {integrity: sha512-bqBsUuaOiXiqxXt/zA/jukNJJ4oaOtc7ciwqJpZVEaaXwwxqgI2/ZdG02vXYWUhHGziDlvGMQWk0qObgJwVYKA==} - dependencies: - inherits: 2.0.4 - run-series: 1.1.9 - dev: false - - /chrome-dns@1.0.1: - resolution: {integrity: sha512-HqsYJgIc8ljJJOqOzLphjAs79EUuWSX3nzZi2LNkzlw3GIzAeZbaSektC8iT/tKvLqZq8yl1GJu5o6doA4TRbg==} - dependencies: - chrome-net: 3.3.4 - dev: false - /chrome-launcher@0.15.2: resolution: {integrity: sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==} engines: {node: '>=12.13.0'} @@ -2710,12 +2578,6 @@ packages: - supports-color dev: true - /chrome-net@3.3.4: - resolution: {integrity: sha512-Jzy2EnzmE+ligqIZUsmWnck9RBXLuUy6CaKyuNMtowFG3ZvLt8d+WBJCTPEludV0DHpIKjAOlwjFmTaEdfdWCw==} - dependencies: - inherits: 2.0.4 - dev: false - /chromium-bidi@0.4.16(devtools-protocol@0.0.1147663): resolution: {integrity: sha512-7ZbXdWERxRxSwo3txsBjjmc/NLxqb1Bk30mRb0BMS4YIaiV6zvKZqL/UAH+DdqcDYayDWk2n/y8klkBDODrPvA==} peerDependencies: @@ -2949,14 +2811,6 @@ packages: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: true - /cross-fetch@3.1.5: - resolution: {integrity: sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==} - dependencies: - node-fetch: 2.6.7 - transitivePeerDependencies: - - encoding - dev: false - /cross-fetch@4.0.0: resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} dependencies: @@ -3065,6 +2919,7 @@ packages: dependencies: ms: 2.1.2 supports-color: 8.1.1 + dev: true /decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} @@ -3194,10 +3049,6 @@ packages: wrappy: 1.0.2 dev: true - /did-resolver@4.1.0: - resolution: {integrity: sha512-S6fWHvCXkZg2IhS4RcVHxwuyVejPR7c+a4Go0xbQ9ps5kILa8viiYQgrM4gfTyeTjJ0ekgJH9gk/BawTpmkbZA==} - dev: false - /diff@5.0.0: resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} engines: {node: '>=0.3.1'} @@ -3223,13 +3074,6 @@ packages: path-type: 4.0.0 dev: true - /dns-packet@5.6.1: - resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==} - engines: {node: '>=6'} - dependencies: - '@leichtgewicht/ip-codec': 2.0.4 - dev: false - /doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} @@ -4072,12 +3916,6 @@ packages: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} dev: true - /graceful-goodbye@1.3.0: - resolution: {integrity: sha512-hcZOs20emYlTM7MmUE0FpuZcjlk2GPsR+UYTHDeWxtGjXcbh2CawGi8vlzqsIvspqAbot7mRv3sC/uhgtKc4hQ==} - dependencies: - safety-catch: 1.0.2 - dev: false - /grapheme-splitter@1.0.4: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} dev: true @@ -4115,6 +3953,7 @@ packages: /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + dev: true /has-property-descriptors@1.0.1: resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} @@ -4144,10 +3983,6 @@ packages: safe-buffer: 5.2.1 dev: true - /hash-wasm@4.9.0: - resolution: {integrity: sha512-7SW7ejyfnRxuOc7ptQHSf4LDoZaWOivfzqw+5rpcQku0nHfmicPKE51ra9BiRLAmT8+gGLestr1XroUkqdjL6w==} - dev: false - /hash.js@1.1.7: resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} dependencies: @@ -4661,29 +4496,6 @@ packages: resolution: {integrity: sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==} dev: false - /k-bucket@5.1.0: - resolution: {integrity: sha512-Fac7iINEovXIWU20GPnOMLUbjctiS+cnmyjC4zAUgvs3XPf1vo9akfCHkigftSic/jiKqKl+KA3a/vFcJbHyCg==} - dependencies: - randombytes: 2.1.0 - dev: false - - /k-rpc-socket@1.11.1: - resolution: {integrity: sha512-8xtA8oqbZ6v1Niryp2/g4GxW16EQh5MvrUylQoOG+zcrDff5CKttON2XUXvMwlIHq4/2zfPVFiinAccJ+WhxoA==} - dependencies: - bencode: 2.0.3 - chrome-dgram: 3.0.6 - chrome-dns: 1.0.1 - chrome-net: 3.3.4 - dev: false - - /k-rpc@5.1.0: - resolution: {integrity: sha512-FGc+n70Hcjoa/X2JTwP+jMIOpBz+pkRffHnSl9yrYiwUxg3FIgD50+u1ePfJUOnRCnx6pbjmVk5aAeB1wIijuQ==} - dependencies: - k-bucket: 5.1.0 - k-rpc-socket: 1.11.1 - randombytes: 2.1.0 - dev: false - /keygrip@1.1.0: resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} engines: {node: '>= 0.6'} @@ -4777,10 +4589,6 @@ packages: - supports-color dev: true - /last-one-wins@1.0.4: - resolution: {integrity: sha512-t+KLJFkHPQk8lfN6WBOiGkiUXoub+gnb2XTYI2P3aiISL+94xgZ1vgz1SXN/N4hthuOoLXarXfBZPUruyjQtfA==} - dev: false - /level-supports@4.0.1: resolution: {integrity: sha512-PbXpve8rKeNcZ9C1mUicC9auIYFyGpkV9/i6g76tLgANwWhtG2v7I4xNBUlkn3lE2/dZF3Pi0ygYGtLc4RXXdA==} engines: {node: '>=12'} @@ -4931,13 +4739,6 @@ packages: engines: {node: '>=16.14'} dev: true - /lru@3.1.0: - resolution: {integrity: sha512-5OUtoiVIGU4VXBOshidmtOsvBIvcQR6FD/RzWSvaeHyxCGB+PCUCu+52lqMfdc0h/2CLvHhZS4TwUmMQrrMbBQ==} - engines: {node: '>= 0.4.0'} - dependencies: - inherits: 2.0.4 - dev: false - /lunr@2.3.9: resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} dev: true @@ -5210,6 +5011,7 @@ packages: /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -5245,10 +5047,6 @@ packages: varint: 5.0.2 dev: false - /nanoassert@2.0.0: - resolution: {integrity: sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==} - dev: false - /nanocolors@0.2.13: resolution: {integrity: sha512-0n3mSAQLPpGLV9ORXT5+C/D4mwew7Ebws69Hx4E2sgz2ZA5+32Q80B9tL8PbL7XHnRDiAxH/pnrUJ9a4fkTNTA==} dev: true @@ -5305,18 +5103,6 @@ packages: lodash: 4.17.21 dev: true - /node-fetch@2.6.7: - resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - dependencies: - whatwg-url: 5.0.0 - dev: false - /node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -5715,21 +5501,6 @@ packages: engines: {node: '>=6'} dev: true - /pkarr@1.1.1: - resolution: {integrity: sha512-X27LKqf83X3WuJd2Z9qdfVxkmfOu6HUbY0pm11LqeBbFmgmZRPgOxJG8bKiIsmmD6Vjc25j45KHYflF2lfodyQ==} - hasBin: true - dependencies: - bencode: 3.1.1 - bittorrent-dht: 11.0.5 - chalk: 5.3.0 - dns-packet: 5.6.1 - graceful-goodbye: 1.3.0 - sodium-universal: 4.0.0 - z32: 1.0.1 - transitivePeerDependencies: - - supports-color - dev: false - /pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} @@ -5953,6 +5724,7 @@ packages: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} dependencies: safe-buffer: 5.2.1 + dev: true /randomfill@1.0.4: resolution: {integrity: sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==} @@ -6042,12 +5814,6 @@ packages: picomatch: 2.3.1 dev: true - /record-cache@1.2.0: - resolution: {integrity: sha512-kyy3HWCez2WrotaL3O4fTn0rsIdfRKOdQQcEJ9KpvmKmbffKVvwsloX063EgRUlpJIXHiDQFhJcTbZequ2uTZw==} - dependencies: - b4a: 1.6.4 - dev: false - /redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} @@ -6192,10 +5958,6 @@ packages: queue-microtask: 1.2.3 dev: true - /run-series@1.1.9: - resolution: {integrity: sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g==} - dev: false - /rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} dependencies: @@ -6225,10 +5987,6 @@ packages: /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - /safety-catch@1.0.2: - resolution: {integrity: sha512-C1UYVZ4dtbBxEtvOcpjBaaD27nP8MlvyAQEp2fOTOEe6pfUpk1cDUxij6BR1jZup6rSyUTaBBplK7LanskrULA==} - dev: false - /semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -6321,34 +6079,6 @@ packages: safe-buffer: 5.2.1 dev: true - /sha256-universal@1.2.1: - resolution: {integrity: sha512-ghn3muhdn1ailCQqqceNxRgkOeZSVfSE13RQWEg6njB+itsFzGVSJv+O//2hvNXZuxVIRyNzrgsZ37SPDdGJJw==} - dependencies: - b4a: 1.6.4 - sha256-wasm: 2.2.2 - dev: false - - /sha256-wasm@2.2.2: - resolution: {integrity: sha512-qKSGARvao+JQlFiA+sjJZhJ/61gmW/3aNLblB2rsgIxDlDxsJPHo8a1seXj12oKtuHVgJSJJ7QEGBUYQN741lQ==} - dependencies: - b4a: 1.6.4 - nanoassert: 2.0.0 - dev: false - - /sha512-universal@1.2.1: - resolution: {integrity: sha512-kehYuigMoRkIngCv7rhgruLJNNHDnitGTBdkcYbCbooL8Cidj/bS78MDxByIjcc69M915WxcQTgZetZ1JbeQTQ==} - dependencies: - b4a: 1.6.4 - sha512-wasm: 2.3.4 - dev: false - - /sha512-wasm@2.3.4: - resolution: {integrity: sha512-akWoxJPGCB3aZCrZ+fm6VIFhJ/p8idBv7AWGFng/CZIrQo51oQNsvDbTSRXWAzIiZJvpy16oIDiCCPqTe21sKg==} - dependencies: - b4a: 1.6.4 - nanoassert: 2.0.0 - dev: false - /shebang-command@1.2.0: resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} engines: {node: '>=0.10.0'} @@ -6409,12 +6139,6 @@ packages: supports-color: 7.2.0 dev: true - /siphash24@1.3.1: - resolution: {integrity: sha512-moemC3ZKiTzH29nbFo3Iw8fbemWWod4vNs/WgKbQ54oEs6mE6XVlguxvinYjB+UmaE0PThgyED9fUkWvirT8hA==} - dependencies: - nanoassert: 2.0.0 - dev: false - /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -6466,39 +6190,6 @@ packages: smart-buffer: 4.2.0 dev: true - /sodium-javascript@0.8.0: - resolution: {integrity: sha512-rEBzR5mPxPES+UjyMDvKPIXy9ImF17KOJ32nJNi9uIquWpS/nfj+h6m05J5yLJaGXjgM72LmQoUbWZVxh/rmGg==} - dependencies: - blake2b: 2.1.4 - chacha20-universal: 1.0.4 - nanoassert: 2.0.0 - sha256-universal: 1.2.1 - sha512-universal: 1.2.1 - siphash24: 1.3.1 - xsalsa20: 1.2.0 - dev: false - - /sodium-native@4.0.5: - resolution: {integrity: sha512-YGimGhy7Ho6pTAAvuNdn3Tv9C2MD7HP89X1omReHat0Fd1mMnapGqwzb5YoHTAbIEh8tQmKP6+uLlwYCkf+EOA==} - requiresBuild: true - dependencies: - node-gyp-build: 4.8.0 - dev: false - - /sodium-universal@4.0.0: - resolution: {integrity: sha512-iKHl8XnBV96k1c75gwwzANFdephw/MDWSjQAjPmBE+du0y3P23Q8uf7AcdcfFsYAMwLg7WVBfSAIBtV/JvRsjA==} - dependencies: - blake2b: 2.1.4 - chacha20-universal: 1.0.4 - nanoassert: 2.0.0 - sha256-universal: 1.2.1 - sha512-universal: 1.2.1 - siphash24: 1.3.1 - sodium-javascript: 0.8.0 - sodium-native: 4.0.5 - xsalsa20: 1.2.0 - dev: false - /source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -6732,6 +6423,7 @@ packages: engines: {node: '>=10'} dependencies: has-flag: 4.0.0 + dev: true /supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} @@ -6828,6 +6520,7 @@ packages: /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: true /tr46@3.0.0: resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} @@ -7086,6 +6779,10 @@ packages: qs: 6.11.2 dev: true + /utf8-codec@1.0.0: + resolution: {integrity: sha512-S/QSLezp3qvG4ld5PUfXiH7mCFxLKjSVZRFkB3DOjgwHuJPFDkInAXc/anf7BAbHt/D38ozDzL+QMZ6/7gsI6w==} + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true @@ -7161,6 +6858,7 @@ packages: /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: true /webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} @@ -7180,6 +6878,7 @@ packages: dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 + dev: true /which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} @@ -7314,10 +7013,6 @@ packages: resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==} dev: true - /xsalsa20@1.2.0: - resolution: {integrity: sha512-FIr/DEeoHfj7ftfylnoFt3rAIRoWXpx2AoDfrT2qD2wtp7Dp+COajvs/Icb7uHqRW9m60f5iXZwdsJJO3kvb7w==} - dev: false - /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -7440,9 +7135,3 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true - - /z32@1.0.1: - resolution: {integrity: sha512-Uytfqf6VEVchHKZDw0NRdCViOARHP84uzvOw0CXCMLOwhgHZUL9XibpEPLLQN10mCVLxOlGCQWbkV7km7yNYcw==} - dependencies: - b4a: 1.6.4 - dev: false diff --git a/tbdex b/tbdex index 7ec25f98..cb6d62ad 160000 --- a/tbdex +++ b/tbdex @@ -1 +1 @@ -Subproject commit 7ec25f981b8ae44c179b3206944ac604329561c5 +Subproject commit cb6d62ad6973d0deafcb5cdee6d28d546afb0c41 From 0702d91491a10d07c984956a2cc2bd1df900b17c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 09:39:58 -0800 Subject: [PATCH 5/6] Version Packages (#167) Co-authored-by: github-actions[bot] --- .changeset/breezy-islands-play.md | 11 ----------- .changeset/gentle-lies-provide.md | 5 ----- .changeset/green-cheetahs-share.md | 5 ----- .changeset/mighty-pandas-speak.md | 5 ----- packages/http-client/CHANGELOG.md | 18 ++++++++++++++++++ packages/http-client/package.json | 2 +- packages/http-server/CHANGELOG.md | 20 ++++++++++++++++++++ packages/http-server/package.json | 2 +- packages/protocol/CHANGELOG.md | 14 ++++++++++++++ packages/protocol/package.json | 2 +- 10 files changed, 55 insertions(+), 29 deletions(-) delete mode 100644 .changeset/breezy-islands-play.md delete mode 100644 .changeset/gentle-lies-provide.md delete mode 100644 .changeset/green-cheetahs-share.md delete mode 100644 .changeset/mighty-pandas-speak.md diff --git a/.changeset/breezy-islands-play.md b/.changeset/breezy-islands-play.md deleted file mode 100644 index eb2ecca0..00000000 --- a/.changeset/breezy-islands-play.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@tbdex/http-client": minor -"@tbdex/http-server": minor -"@tbdex/protocol": minor ---- - -Upgrade packages web5/dids@0.4.0, web5/credentials@0.4.2, web5/crypto@0.4.0, web5/common@0.2.3 - -* Deprecate did:ion and did:key in favour of did:jwk and did:dht -* Migrate from `PortableDid` to `BearerDid` with the latest @web5/dids upgrade -* Replaces dependency on `Web5Crypto` with `BearerDid` signer abstraction for signing operations diff --git a/.changeset/gentle-lies-provide.md b/.changeset/gentle-lies-provide.md deleted file mode 100644 index eec35f38..00000000 --- a/.changeset/gentle-lies-provide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@tbdex/http-server": minor ---- - -Requires consumer to pass pfiDid when instantiating a new TbdexHttpServer diff --git a/.changeset/green-cheetahs-share.md b/.changeset/green-cheetahs-share.md deleted file mode 100644 index f964e5b6..00000000 --- a/.changeset/green-cheetahs-share.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@tbdex/protocol": patch ---- - -Add exchange state machine diff --git a/.changeset/mighty-pandas-speak.md b/.changeset/mighty-pandas-speak.md deleted file mode 100644 index 36e78adf..00000000 --- a/.changeset/mighty-pandas-speak.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@tbdex/http-client": minor ---- - -Stricten, tested, and remove untested code diff --git a/packages/http-client/CHANGELOG.md b/packages/http-client/CHANGELOG.md index 1170e75a..e8dbfde3 100644 --- a/packages/http-client/CHANGELOG.md +++ b/packages/http-client/CHANGELOG.md @@ -1,5 +1,23 @@ # @tbdex/http-client +## 0.26.0 + +### Minor Changes + +- eba04b8: Upgrade packages web5/dids@0.4.0, web5/credentials@0.4.2, web5/crypto@0.4.0, web5/common@0.2.3 + + - Deprecate did:ion and did:key in favour of did:jwk and did:dht + - Migrate from `PortableDid` to `BearerDid` with the latest @web5/dids upgrade + - Replaces dependency on `Web5Crypto` with `BearerDid` signer abstraction for signing operations + +- 629f0c7: Stricten, tested, and remove untested code + +### Patch Changes + +- Updated dependencies [eba04b8] +- Updated dependencies [589edc3] + - @tbdex/protocol@0.26.0 + ## 0.25.0 ### Minor Changes diff --git a/packages/http-client/package.json b/packages/http-client/package.json index 4107897f..95862e1f 100644 --- a/packages/http-client/package.json +++ b/packages/http-client/package.json @@ -1,6 +1,6 @@ { "name": "@tbdex/http-client", - "version": "0.25.0", + "version": "0.26.0", "type": "module", "description": "Http client that can be used to send tbdex messages", "license": "Apache-2.0", diff --git a/packages/http-server/CHANGELOG.md b/packages/http-server/CHANGELOG.md index ca30c667..d85f7998 100644 --- a/packages/http-server/CHANGELOG.md +++ b/packages/http-server/CHANGELOG.md @@ -1,5 +1,25 @@ # @tbdex/http-server +## 0.26.0 + +### Minor Changes + +- eba04b8: Upgrade packages web5/dids@0.4.0, web5/credentials@0.4.2, web5/crypto@0.4.0, web5/common@0.2.3 + + - Deprecate did:ion and did:key in favour of did:jwk and did:dht + - Migrate from `PortableDid` to `BearerDid` with the latest @web5/dids upgrade + - Replaces dependency on `Web5Crypto` with `BearerDid` signer abstraction for signing operations + +- ca1b235: Requires consumer to pass pfiDid when instantiating a new TbdexHttpServer + +### Patch Changes + +- Updated dependencies [eba04b8] +- Updated dependencies [589edc3] +- Updated dependencies [629f0c7] + - @tbdex/http-client@0.26.0 + - @tbdex/protocol@0.26.0 + ## 0.25.0 ### Minor Changes diff --git a/packages/http-server/package.json b/packages/http-server/package.json index cd690641..7b45ddc9 100644 --- a/packages/http-server/package.json +++ b/packages/http-server/package.json @@ -9,7 +9,7 @@ }, "license": "Apache-2.0", "type": "module", - "version": "0.25.0", + "version": "0.26.0", "module": "./dist/main.js", "types": "./dist/types/main.d.ts", "files": [ diff --git a/packages/protocol/CHANGELOG.md b/packages/protocol/CHANGELOG.md index 63cb9168..29b604d2 100644 --- a/packages/protocol/CHANGELOG.md +++ b/packages/protocol/CHANGELOG.md @@ -1,5 +1,19 @@ # @tbdex/protocol +## 0.26.0 + +### Minor Changes + +- eba04b8: Upgrade packages web5/dids@0.4.0, web5/credentials@0.4.2, web5/crypto@0.4.0, web5/common@0.2.3 + + - Deprecate did:ion and did:key in favour of did:jwk and did:dht + - Migrate from `PortableDid` to `BearerDid` with the latest @web5/dids upgrade + - Replaces dependency on `Web5Crypto` with `BearerDid` signer abstraction for signing operations + +### Patch Changes + +- 589edc3: Add exchange state machine + ## 0.25.0 ### Minor Changes diff --git a/packages/protocol/package.json b/packages/protocol/package.json index 962abcdc..ae5edcaa 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -1,6 +1,6 @@ { "name": "@tbdex/protocol", - "version": "0.25.0", + "version": "0.26.0", "type": "module", "description": "Library that includes type definitions for tbdex messages", "license": "Apache-2.0", From ebefc547ddacf1191a5eb8d1863fd5f8f5e6183f Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Fri, 16 Feb 2024 14:42:52 -0800 Subject: [PATCH 6/6] Stricten, test, and bugfix http-server (#170) * Stricten, test, and bugfix http-server * PR comments * Update packages/http-server/tests/submit-close.spec.ts Co-authored-by: Jiyoon Koo --------- Co-authored-by: Jiyoon Koo --- packages/http-server/package.json | 3 + packages/http-server/src/fakes.ts | 58 ---- packages/http-server/src/http-server.ts | 64 ++-- .../src/in-memory-exchanges-api.ts | 89 +++++ .../src/in-memory-offerings-api.ts | 72 ++++ .../src/request-handlers/create-exchange.ts | 100 +++--- .../src/request-handlers/get-exchanges.ts | 68 ++-- .../src/request-handlers/get-offerings.ts | 30 +- .../src/request-handlers/submit-close.ts | 88 +++-- .../src/request-handlers/submit-order.ts | 87 +++-- packages/http-server/src/types.ts | 29 +- .../http-server/tests/create-exchange.spec.ts | 314 +++++++++++++++++- .../http-server/tests/get-exchanges.spec.ts | 176 +++++++--- .../http-server/tests/get-offerings.spec.ts | 78 ++++- .../http-server/tests/submit-close.spec.ts | 292 ++++++++++++++-- .../http-server/tests/submit-order.spec.ts | 273 ++++++++++++++- packages/http-server/tests/tsconfig.json | 2 + packages/http-server/tsconfig.json | 3 + packages/protocol/src/exchange.ts | 12 +- .../src/message-kinds/order-status.ts | 2 +- packages/protocol/tests/exchange.spec.ts | 6 +- pnpm-lock.yaml | 21 ++ 22 files changed, 1489 insertions(+), 378 deletions(-) delete mode 100644 packages/http-server/src/fakes.ts create mode 100644 packages/http-server/src/in-memory-exchanges-api.ts create mode 100644 packages/http-server/src/in-memory-offerings-api.ts diff --git a/packages/http-server/package.json b/packages/http-server/package.json index 7b45ddc9..5b738da5 100644 --- a/packages/http-server/package.json +++ b/packages/http-server/package.json @@ -29,12 +29,15 @@ }, "devDependencies": { "@types/chai": "4.3.6", + "@types/cors": "^2.8.17", "@types/express": "4.17.17", "@types/http-errors": "2.0.4", "@types/mocha": "10.0.1", "@types/node": "20.9.4", + "@types/sinon": "^17.0.3", "chai": "4.3.10", "rimraf": "5.0.1", + "sinon": "17.0.1", "supertest": "6.3.3", "typescript": "5.2.2" }, diff --git a/packages/http-server/src/fakes.ts b/packages/http-server/src/fakes.ts deleted file mode 100644 index c544f1b4..00000000 --- a/packages/http-server/src/fakes.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { DevTools, Message, Rfq, Quote, Order, OrderStatus, Close } from '@tbdex/protocol' -import { OfferingsApi, ExchangesApi } from './main.js' - -const offering = DevTools.createOffering() - -export const fakeOfferingsApi: OfferingsApi = { - async getOffering() { return offering }, - async getOfferings() { return [offering] } -} - -export interface FakeExchangesApi extends ExchangesApi { - exchangeMessagesMap: Map, - addMessage(message: Message): void - clearMessages(): void -} - -export const fakeExchangesApi: FakeExchangesApi = { - exchangeMessagesMap: new Map(), - - getExchanges: function (): Promise { - throw new Error('Function not implemented.') - }, - - getExchange: function (opts: { id: string} ): Promise { - const messages = this.exchangeMessagesMap.get(opts.id) || undefined - return Promise.resolve(messages) - }, - - getRfq: function (): Promise { - throw new Error('Function not implemented.') - }, - - getQuote: function (): Promise { - throw new Error('Function not implemented.') - }, - - getOrder: function (): Promise { - throw new Error('Function not implemented.') - }, - - getOrderStatuses: function (): Promise { - throw new Error('Function not implemented.') - }, - - getClose: function (): Promise { - throw new Error('Function not implemented.') - }, - - addMessage: function (message: Message): void { - const messages = this.exchangeMessagesMap.get(message.exchangeId) || [] - messages.push(message) - this.exchangeMessagesMap.set(message.exchangeId, messages) - }, - - clearMessages: function (): void { - this.exchangeMessagesMap = new Map() - } -} \ No newline at end of file diff --git a/packages/http-server/src/http-server.ts b/packages/http-server/src/http-server.ts index eca593aa..a99fc0a0 100644 --- a/packages/http-server/src/http-server.ts +++ b/packages/http-server/src/http-server.ts @@ -8,14 +8,15 @@ import type { GetOfferingsCallback, } from './types.js' -import type { Express } from 'express' +import type { Express, Request, Response } from 'express' import express from 'express' import cors from 'cors' import { getExchanges, getOfferings, submitOrder, submitClose, createExchange } from './request-handlers/index.js' import { jsonBodyParser } from './middleware/index.js' -import { fakeExchangesApi, fakeOfferingsApi } from './fakes.js' +import { InMemoryOfferingsApi } from './in-memory-offerings-api.js' +import { InMemoryExchangesApi } from './in-memory-exchanges-api.js' /** * Maps the requests to their respective callbacks handlers @@ -72,8 +73,8 @@ export class TbdexHttpServer { constructor(opts?: NewHttpServerOptions) { this.callbacks = {} - this.exchangesApi = opts?.exchangesApi ?? fakeExchangesApi - this.offeringsApi = opts?.offeringsApi ?? fakeOfferingsApi + this.exchangesApi = opts?.exchangesApi ?? new InMemoryExchangesApi() + this.offeringsApi = opts?.offeringsApi ?? new InMemoryOfferingsApi() this.pfiDid = opts?.pfiDid ?? 'did:ex:pfi' // initialize api here so that consumers can attach custom endpoints @@ -139,25 +140,42 @@ export class TbdexHttpServer { listen(port: number | string, callback?: () => void) { const { offeringsApi, exchangesApi, pfiDid } = this - this.api.post('/exchanges/:exchangeId/rfq', createExchange({ - callback: this.callbacks['rfq'], offeringsApi, exchangesApi, - })) - - this.api.post('/exchanges/:exchangeId/order', submitOrder({ - callback: this.callbacks['order'], exchangesApi - })) - - this.api.post('/exchanges/:exchangeId/close', submitClose({ - callback: this.callbacks['close'], exchangesApi - })) - - this.api.get('/exchanges', getExchanges({ - callback: this.callbacks['exchanges'], exchangesApi, pfiDid - })) - - this.api.get('/offerings', getOfferings({ - callback: this.callbacks['offerings'], offeringsApi - })) + this.api.post('/exchanges/:exchangeId/rfq', (req: Request, res: Response) => + createExchange(req, res, { + callback: this.callbacks['rfq'], + offeringsApi, + exchangesApi, + }) + ) + + this.api.post('/exchanges/:exchangeId/order', (req: Request, res: Response) => + submitOrder(req, res, { + callback: this.callbacks['order'], + exchangesApi + }) + ) + + this.api.post('/exchanges/:exchangeId/close', (req: Request, res: Response) => + submitClose(req, res,{ + callback: this.callbacks.close, + exchangesApi, + }) + ) + + this.api.get('/exchanges', (req: Request, res: Response) => + getExchanges(req, res, { + callback: this.callbacks.exchanges, + exchangesApi, + pfiDid, + }) + ) + + this.api.get('/offerings', (req, res) => + getOfferings(req, res, { + callback: this.callbacks['offerings'], + offeringsApi + }) + ) // TODO: support hostname and backlog arguments return this.api.listen(port, callback) diff --git a/packages/http-server/src/in-memory-exchanges-api.ts b/packages/http-server/src/in-memory-exchanges-api.ts new file mode 100644 index 00000000..16053041 --- /dev/null +++ b/packages/http-server/src/in-memory-exchanges-api.ts @@ -0,0 +1,89 @@ +import { Exchange } from '@tbdex/protocol' +import { Message, Rfq, Quote, Order, OrderStatus, Close } from '@tbdex/protocol' +import { ExchangesApi, GetExchangesFilter } from './main.js' + +/** + * An in-memory implementation of {@link ExchangesApi} for example and default purposes. + * InMemoryExchangesApi has additional methods {@link InMemoryExchangesApi.addMessage} + * and {@link InMemoryExchangesApi.clearMessages} + */ +export class InMemoryExchangesApi implements ExchangesApi { + /** Map from exchange_id to Exchange */ + exchangeMessagesMap: Map + + constructor() { + this.exchangeMessagesMap = new Map() + } + + async getExchanges(opts?: { filter: GetExchangesFilter }): Promise { + if (opts === undefined || opts.filter === undefined) { + // In production, this should probably return an empty list. + // For example and testing purposes, we return all exchanges. + + return Array.from(this.exchangeMessagesMap.values()) + } + + const exchanges: Exchange[] = [] + if (opts.filter.id) { + // filter has `id` and `from` + + for (const id of opts.filter.id) { + const exchange = this.exchangeMessagesMap.get(id) + if (exchange?.rfq?.from === opts.filter.from) { + exchanges.push(exchange) + } + } + } else { + // filter only has `from` + this.exchangeMessagesMap.forEach((exchange, _id) => { + // You definitely shouldn't use InMemoryExchangesApi in production. + // This will get really slow + if (exchange?.rfq?.from === opts.filter.from) { + exchanges.push(exchange) + } + }) + } + + return exchanges + } + + async getExchange(opts: { id: string} ): Promise { + const exchange = this.exchangeMessagesMap.get(opts.id) + return Promise.resolve(exchange) + } + + async getRfq(opts: { exchangeId: string }): Promise { + const exchange = this.exchangeMessagesMap.get(opts.exchangeId) + return exchange?.rfq + } + + async getQuote(opts: { exchangeId: string }): Promise { + const exchange = this.exchangeMessagesMap.get(opts.exchangeId) + return Promise.resolve(exchange?.quote) + } + + async getOrder(opts: { exchangeId: string }): Promise { + const exchange = this.exchangeMessagesMap.get(opts.exchangeId) + return exchange?.order + } + + async getOrderStatuses(opts: { exchangeId: string }): Promise { + const exchange = this.exchangeMessagesMap.get(opts.exchangeId) + return exchange?.orderstatus ?? [] + } + + async getClose(opts: { exchangeId: string }): Promise { + const exchange = this.exchangeMessagesMap.get(opts.exchangeId) + return exchange?.close + } + + addMessage(message: Message): void { + const exchange = this.exchangeMessagesMap.get(message.exchangeId) ?? new Exchange() + exchange.addNextMessage(message) + this.exchangeMessagesMap.set(message.exchangeId, exchange) + } + + clearMessages(): void { + this.exchangeMessagesMap = new Map() + } +} \ No newline at end of file diff --git a/packages/http-server/src/in-memory-offerings-api.ts b/packages/http-server/src/in-memory-offerings-api.ts new file mode 100644 index 00000000..cc0c651a --- /dev/null +++ b/packages/http-server/src/in-memory-offerings-api.ts @@ -0,0 +1,72 @@ +import { Offering } from '@tbdex/protocol' +import { GetOfferingsFilter, OfferingsApi } from './types.js' + +/** + * An in-memory implementation of {@link OfferingsApi} for example and default purposes. + * InMemoryOfferingsApi has additional methods {@link InMemoryOfferingsApi.addOffering} + * and {@link InMemoryOfferingsApi.clearOfferings} + */ +export class InMemoryOfferingsApi implements OfferingsApi { + /** Map from offering_id to Offering */ + offeringsMap: Map + + constructor() { + this.offeringsMap = new Map() + } + + /** + * Add a single offering + * @param offering - Offering to be added to the {@link offeringsMap} + */ + addOffering(offering: Offering): void { + this.offeringsMap.set(offering.metadata.id, offering) + } + + /** + * Clear existing list offerings + */ + clearOfferings(): void { + this.offeringsMap.clear() + } + + /** + * Retrieve a single offering if found + * @param opts - Filter with id used to select an offering + * @returns An offering if one exists, else undefined + */ + async getOffering(opts: { id: string }): Promise{ + return this.offeringsMap.get(opts.id) + } + + /** + * + * @param opts - Filter used to select offerings + * @returns A list of offerings matching the filter + */ + async getOfferings(opts?: { filter: GetOfferingsFilter }): Promise { + const allOfferings = Array.from(this.offeringsMap.values()) + + if (opts?.filter === undefined || Object.values(opts.filter).every(v => v === undefined)) { + // If no filter is provided, return all offerings + return allOfferings + } + + const { filter: { + id, + payinCurrency, + payoutCurrency, + payinMethodKind, + payoutMethodKind, + } } = opts + + return allOfferings.filter((offering) => { + // If filter includes a field, make sure the returned offerings match + return (!id || id === offering.metadata.id) && + (!payinCurrency || payinCurrency === offering.data.payinCurrency.currencyCode) && + (!payoutCurrency || payoutCurrency === offering.data.payoutCurrency.currencyCode) && + (!payinMethodKind || offering.data.payinMethods.map(pm => pm.kind).includes(payinMethodKind)) && + (!payoutMethodKind || offering.data.payoutMethods.map(pm => pm.kind).includes(payoutMethodKind)) + }) + } + +} \ No newline at end of file diff --git a/packages/http-server/src/request-handlers/create-exchange.ts b/packages/http-server/src/request-handlers/create-exchange.ts index 6d44a275..3205c2ed 100644 --- a/packages/http-server/src/request-handlers/create-exchange.ts +++ b/packages/http-server/src/request-handlers/create-exchange.ts @@ -1,69 +1,79 @@ -import type { RequestHandler, OfferingsApi, ExchangesApi, SubmitRfqCallback } from '../types.js' +import type { OfferingsApi, ExchangesApi, SubmitRfqCallback } from '../types.js' import { Rfq } from '@tbdex/protocol' import type { ErrorDetail } from '@tbdex/http-client' import { CallbackError } from '../callback-error.js' +import { Request, Response } from 'express' type CreateExchangeOpts = { - callback: SubmitRfqCallback + callback?: SubmitRfqCallback offeringsApi: OfferingsApi exchangesApi: ExchangesApi } -export function createExchange(options: CreateExchangeOpts): RequestHandler { +export async function createExchange(req: Request, res: Response, options: CreateExchangeOpts): Promise { const { offeringsApi, exchangesApi, callback } = options - return async function (req, res) { - let rfq: Rfq + const replyTo: string | undefined = req.body.replyTo - if (req.body.replyTo && !isValidUrl(req.body.replyTo)) { - return res.status(400).json({ errors: [{ detail: 'replyTo must be a valid url' }] }) - } + let rfq: Rfq - try { - rfq = await Rfq.parse(req.body.rfq) - } catch(e) { - const errorResponse: ErrorDetail = { detail: `Parsing of TBDex Rfq message failed: ${e.message}` } - return res.status(400).json({ errors: [errorResponse] }) - } + if (replyTo && !isValidUrl(replyTo)) { + res.status(400).json({ errors: [{ detail: 'replyTo must be a valid url' }] }) + return + } - // TODO: check message.from against allowlist + try { + rfq = await Rfq.parse(req.body.rfq) + } catch(e) { + const errorResponse: ErrorDetail = { detail: `Parsing of TBDex Rfq message failed: ${e.message}` } + res.status(400).json({ errors: [errorResponse] }) + return + } - const rfqExists = !! await exchangesApi.getRfq({ exchangeId: rfq.id }) - if (rfqExists) { - const errorResponse: ErrorDetail = { detail: `rfq ${rfq.id} already exists`} - return res.status(409).json({ errors: [errorResponse] }) - } + // TODO: check message.from against allowlist - const offering = await offeringsApi.getOffering({ id: rfq.data.offeringId }) - if (!offering) { - const errorResponse: ErrorDetail = { detail: `offering ${rfq.data.offeringId} does not exist` } - return res.status(400).json({ errors: [errorResponse] }) - } + const rfqExists = !! await exchangesApi.getRfq({ exchangeId: rfq.id }) + if (rfqExists) { + const errorResponse: ErrorDetail = { detail: `rfq ${rfq.id} already exists`} + res.status(409).json({ errors: [errorResponse] }) + return + } - try { - await rfq.verifyOfferingRequirements(offering) - } catch(e) { - const errorResponse: ErrorDetail = { detail: `Failed to verify offering requirements: ${e.message}` } - return res.status(400).json({ errors: [errorResponse] }) - } + const offering = await offeringsApi.getOffering({ id: rfq.data.offeringId }) + if (!offering) { + const errorResponse: ErrorDetail = { detail: `offering ${rfq.data.offeringId} does not exist` } + res.status(400).json({ errors: [errorResponse] }) + return + } - if (!callback) { - return res.sendStatus(202) - } + try { + await rfq.verifyOfferingRequirements(offering) + } catch(e) { + const errorResponse: ErrorDetail = { detail: `Failed to verify offering requirements: ${e.message}` } + res.status(400).json({ errors: [errorResponse] }) + return + } - try { - await callback({ request: req, response: res }, rfq, { offering }) - } catch(e) { - if (e instanceof CallbackError) { - return res.status(e.statusCode).json({ errors: e.details }) - } else { - const errorDetail: ErrorDetail = { detail: 'Internal Server Error' } - return res.status(500).json({ errors: [errorDetail] }) - } - } + if (!callback) { + res.sendStatus(202) + return + } - return res.sendStatus(202) + try { + await callback({ request: req, response: res }, rfq, { offering, replyTo }) + } catch(e) { + if (e instanceof CallbackError) { + res.status(e.statusCode).json({ errors: e.details }) + return + } else { + const errorDetail: ErrorDetail = { detail: 'Internal Server Error' } + res.status(500).json({ errors: [errorDetail] }) + return + } } + + res.sendStatus(202) + return } function isValidUrl(replyToUrl: string) { diff --git a/packages/http-server/src/request-handlers/get-exchanges.ts b/packages/http-server/src/request-handlers/get-exchanges.ts index f31faae6..0c94d90d 100644 --- a/packages/http-server/src/request-handlers/get-exchanges.ts +++ b/packages/http-server/src/request-handlers/get-exchanges.ts @@ -1,49 +1,57 @@ -import type { ExchangesApi, GetExchangesCallback, GetExchangesFilter, RequestHandler } from '../types.js' +import type { ExchangesApi, GetExchangesCallback, GetExchangesFilter } from '../types.js' import { TbdexHttpClient } from '@tbdex/http-client' +import { Request, Response } from 'express' type GetExchangesOpts = { - callback: GetExchangesCallback + callback?: GetExchangesCallback exchangesApi: ExchangesApi, pfiDid: string } -export function getExchanges(opts: GetExchangesOpts): RequestHandler { +export async function getExchanges(request: Request, response: Response, opts: GetExchangesOpts): Promise { const { callback, exchangesApi, pfiDid } = opts - return async function (request, response) { - const authzHeader = request.headers['authorization'] - if (!authzHeader) { - return response.status(401).json({ errors: [{ detail: 'Authorization header required' }] }) - } - const [_, requestToken] = authzHeader.split('Bearer ') + const authzHeader = request.headers['authorization'] + if (!authzHeader) { + response.status(401).json({ errors: [{ detail: 'Authorization header required' }] }) + return + } - if (!requestToken) { - return response.status(401).json({ errors: [{ detail: 'Malformed Authorization header. Expected: Bearer TOKEN_HERE' }] }) - } + const [_, requestToken] = authzHeader.split('Bearer ') - let requesterDid: string - try { - requesterDid = await TbdexHttpClient.verifyRequestToken({ requestToken: requestToken, pfiDid }) - } catch(e) { - return response.status(401).json({ errors: [{ detail: `Malformed Authorization header: ${e}` }] }) - } + if (!requestToken) { + response.status(401).json({ errors: [{ detail: 'Malformed Authorization header. Expected: Bearer TOKEN_HERE' }] }) + return + } - const queryParams: GetExchangesFilter = { from: requesterDid } - for (let param in request.query) { - const val = request.query[param] - queryParams[param] = Array.isArray(val) ? val : [val] - } + let requesterDid: string + try { + requesterDid = await TbdexHttpClient.verifyRequestToken({ requestToken: requestToken, pfiDid }) + } catch(e) { + response.status(401).json({ errors: [{ detail: `Malformed Authorization header: ${e}` }] }) + return + } - // check exchanges exist - what to do if some exist but others don't? - const exchanges = await exchangesApi.getExchanges({ filter: queryParams }) + const queryParams: GetExchangesFilter = { + from: requesterDid, + } - if (callback) { - // TODO: figure out what to do with callback result. should we pass through the exchanges we've fetched - // and allow the callback to modify what's returned? (issue #10) - const _result = await callback({ request, response }, queryParams) + if (request.query.id !== undefined) { + if (Array.isArray(request.query.id)) { + queryParams.id = request.query.id.map((id) => id.toString()) + } else { + queryParams.id = [request.query.id.toString()] } + } - return response.status(200).json({ data: exchanges }) + const exchanges = await exchangesApi.getExchanges({ filter: queryParams }) + + if (callback) { + // TODO: figure out what to do with callback result. should we pass through the exchanges we've fetched + // and allow the callback to modify what's returned? (issue #10) + const _result = await callback({ request, response }, queryParams) } + + response.status(200).json({ data: exchanges }) } \ No newline at end of file diff --git a/packages/http-server/src/request-handlers/get-offerings.ts b/packages/http-server/src/request-handlers/get-offerings.ts index fea90b46..8ddb8e14 100644 --- a/packages/http-server/src/request-handlers/get-offerings.ts +++ b/packages/http-server/src/request-handlers/get-offerings.ts @@ -1,23 +1,29 @@ -import type { GetOfferingsCallback, GetOfferingsFilter, OfferingsApi, RequestHandler } from '../types.js' +import { Request, Response } from 'express' +import type { GetOfferingsCallback, GetOfferingsFilter, OfferingsApi } from '../types.js' type GetOfferingsOpts = { - callback: GetOfferingsCallback + callback?: GetOfferingsCallback offeringsApi: OfferingsApi } -export function getOfferings(opts: GetOfferingsOpts): RequestHandler { +export async function getOfferings(request: Request, response: Response, opts: GetOfferingsOpts): Promise { const { callback, offeringsApi } = opts - return async function (request, response) { - const queryParams = request.query as GetOfferingsFilter - const offerings = await offeringsApi.getOfferings({ filter: queryParams || {} }) + const filter: GetOfferingsFilter = { + payinCurrency : request.query.payinCurrency?.toString(), + payoutCurrency : request.query.payoutCurrency?.toString(), + payinMethodKind : request.query.payinMethodKind?.toString(), + payoutMethodKind : request.query.payoutMethodKind?.toString(), + id : request.query.id?.toString(), + } - if (callback) { - // TODO: figure out what to do with callback result. should we pass through the offerings we've fetched - // and allow the callback to modify what's returned? (issue #11) - await callback({ request, response }, queryParams) - } + const offerings = await offeringsApi.getOfferings({ filter }) - return response.status(200).json({ data: offerings }) + if (callback) { + // TODO: figure out what to do with callback result. should we pass through the offerings we've fetched + // and allow the callback to modify what's returned? (issue #11) + await callback({ request, response }, filter) } + + response.status(200).json({ data: offerings }) } \ No newline at end of file diff --git a/packages/http-server/src/request-handlers/submit-close.ts b/packages/http-server/src/request-handlers/submit-close.ts index 4d1bda12..e5aace8f 100644 --- a/packages/http-server/src/request-handlers/submit-close.ts +++ b/packages/http-server/src/request-handlers/submit-close.ts @@ -1,55 +1,79 @@ -import type { RequestHandler, ExchangesApi, SubmitCloseCallback } from '../types.js' +import type { ExchangesApi, SubmitCloseCallback } from '../types.js' import type { ErrorDetail } from '@tbdex/http-client' import { Close } from '@tbdex/protocol' import { CallbackError } from '../callback-error.js' +import { Request, Response } from 'express' type SubmitCloseOpts = { - callback: SubmitCloseCallback + callback?: SubmitCloseCallback exchangesApi: ExchangesApi } -export function submitClose(opts: SubmitCloseOpts): RequestHandler { +export async function submitClose(req: Request, res: Response, opts: SubmitCloseOpts): Promise { const { callback, exchangesApi } = opts - return async function (req, res) { - let close: Close + let close: Close - try { - close = await Close.parse(req.body) - } catch(e) { - const errorResponse: ErrorDetail = { detail: e.message } - return res.status(400).json({ errors: [errorResponse] }) - } + try { + close = await Close.parse(req.body) + } catch(e) { + const errorResponse: ErrorDetail = { detail: 'Request body was not a valid Close message' } + res.status(400).json({ errors: [errorResponse] }) + return + } - const exchange = await exchangesApi.getExchange({id: close.exchangeId}) - if(exchange == undefined) { - const errorResponse: ErrorDetail = { detail: `No exchange found for ${close.exchangeId}` } + // Ensure that an exchange exists to be closed + const exchange = await exchangesApi.getExchange({ id: close.exchangeId }) - return res.status(404).json({ errors: [errorResponse] }) - } + if(exchange === undefined || exchange.messages.length === 0) { + const errorResponse: ErrorDetail = { detail: `No exchange found for ${close.exchangeId}` } - const last = exchange[exchange.length-1] - if(!last.validNext.has(close.kind)) { - const errorResponse: ErrorDetail = { detail: `cannot submit Close for an exchange where the last message is kind: ${last.kind}` } + res.status(404).json({ errors: [errorResponse] }) + return + } + + console.log('exchange.isValidNext(close.metadata.kind)::::', exchange.isValidNext(close.metadata.kind)) - return res.status(409).json({ errors: [errorResponse] }) + // Ensure this exchange can be Closed + if(!exchange.isValidNext(close.metadata.kind)) { + const errorResponse: ErrorDetail = { + detail: `cannot submit Close for an exchange where the last message is kind: ${exchange.latestMessage!.metadata.kind}` } - if (!callback) { - return res.sendStatus(202) + res.status(409).json({ errors: [errorResponse] }) + return + } + + // Ensure that Close is from either Alice or PFI + const rfq = exchange.rfq! + if (close.metadata.from === rfq.metadata.from && close.metadata.to === rfq.metadata.to) { + // Alice may Close an exchange + } else if (close.metadata.from === rfq.metadata.to && close.metadata.to === rfq.metadata.from) { + // The PFI may Close an exchange + } else { + const errorResponse: ErrorDetail = { + detail: `Only the creator and receiver of an exchange may close the exchange` } - try { - await callback({ request: req, response: res }, close) - return res.sendStatus(202) - } catch(e) { - if (e instanceof CallbackError) { - return res.status(e.statusCode).json({ errors: e.details }) - } else { - const errorDetail: ErrorDetail = { detail: 'Internal Server Error' } - return res.status(500).json({ errors: [errorDetail] }) - } + res.status(400).json({ errors: [errorResponse] }) + return + } + + if (!callback) { + res.sendStatus(202) + return + } + + try { + await callback({ request: req, response: res }, close) + res.sendStatus(202) + } catch(e) { + if (e instanceof CallbackError) { + res.status(e.statusCode).json({ errors: e.details }) + } else { + const errorDetail: ErrorDetail = { detail: 'Internal Server Error' } + res.status(500).json({ errors: [errorDetail] }) } } } diff --git a/packages/http-server/src/request-handlers/submit-order.ts b/packages/http-server/src/request-handlers/submit-order.ts index dd2e8fad..f0bde3a5 100644 --- a/packages/http-server/src/request-handlers/submit-order.ts +++ b/packages/http-server/src/request-handlers/submit-order.ts @@ -1,67 +1,66 @@ -import type { RequestHandler, ExchangesApi, SubmitOrderCallback } from '../types.js' +import type { ExchangesApi, SubmitOrderCallback } from '../types.js' import type { ErrorDetail } from '@tbdex/http-client' -import { Order, Quote } from '@tbdex/protocol' +import { Order } from '@tbdex/protocol' import { CallbackError } from '../callback-error.js' +import { Request, Response } from 'express' type SubmitOrderOpts = { - callback: SubmitOrderCallback + callback?: SubmitOrderCallback exchangesApi: ExchangesApi } -export function submitOrder(opts: SubmitOrderOpts): RequestHandler { +export async function submitOrder(req: Request, res: Response, opts: SubmitOrderOpts): Promise { const { callback, exchangesApi } = opts - return async function (req, res) { - let order: Order + let order: Order - try { - order = await Order.parse(req.body) - } catch(e) { - const errorResponse: ErrorDetail = { detail: e.message } - return res.status(400).json({ errors: [errorResponse] }) - } - - const exchange = await exchangesApi.getExchange({id: order.exchangeId}) - if(exchange == undefined) { - const errorResponse: ErrorDetail = { detail: `No exchange found for ${order.exchangeId}` } + try { + order = await Order.parse(req.body) + } catch(e) { + const errorResponse: ErrorDetail = { detail: 'Request body was not a valid Order message' } + res.status(400).json({ errors: [errorResponse] }) + return + } - return res.status(404).json({ errors: [errorResponse] }) - } + const exchange = await exchangesApi.getExchange({id: order.exchangeId}) + if(exchange == undefined) { + const errorResponse: ErrorDetail = { detail: `No exchange found for ${order.exchangeId}` } - const last = exchange[exchange.length-1] - if(!last.validNext.has('order')) { - const errorResponse: ErrorDetail = { detail: `Cannot submit Order for an exchange where the last message is kind: ${last.kind}` } + res.status(404).json({ errors: [errorResponse] }) + return + } - return res.status(409).json({ errors: [errorResponse] }) + if(!exchange.isValidNext('order')) { + const errorResponse: ErrorDetail = { + detail: `Cannot submit Order for an exchange where the last message is kind: ${exchange.latestMessage!.metadata}` } - const quote = exchange.find((message) => message.isQuote()) as Quote - if(quote == undefined) { - const errorResponse: ErrorDetail = { detail: 'Quote not found' } - return res.status(404).json({errors: [errorResponse]}) - } + res.status(409).json({ errors: [errorResponse] }) + return + } - if(new Date(quote.data.expiresAt) < new Date(order.metadata.createdAt)){ - const errorResponse: ErrorDetail = { detail: `Quote is expired` } + if(new Date(exchange.quote!.data.expiresAt) < new Date()){ + const errorResponse: ErrorDetail = { detail: 'Quote is expired' } - return res.status(410).json({ errors: [errorResponse] }) - } + res.status(410).json({ errors: [errorResponse] }) + return + } - if (!callback) { - return res.sendStatus(202) - } + if (!callback) { + res.sendStatus(202) + return + } - try { - await callback({ request: req, response: res }, order) - return res.sendStatus(202) - } catch(e) { - if (e instanceof CallbackError) { - return res.status(e.statusCode).json({ errors: e.details }) - } else { - const errorDetail: ErrorDetail = { detail: 'Internal Server Error' } - return res.status(500).json({ errors: [errorDetail] }) - } + try { + await callback({ request: req, response: res }, order) + res.sendStatus(202) + } catch(e) { + if (e instanceof CallbackError) { + res.status(e.statusCode).json({ errors: e.details }) + } else { + const errorDetail: ErrorDetail = { detail: 'Internal Server Error' } + res.status(500).json({ errors: [errorDetail] }) } } } \ No newline at end of file diff --git a/packages/http-server/src/types.ts b/packages/http-server/src/types.ts index 1956e98a..277e8fd2 100644 --- a/packages/http-server/src/types.ts +++ b/packages/http-server/src/types.ts @@ -1,6 +1,5 @@ import type { Request, Response } from 'express' -import type { Close, Message, Offering, Order, OrderStatus, Quote, Rfq } from '@tbdex/protocol' -import type { ErrorDetail } from '@tbdex/http-client' +import type { Close, Exchange, Offering, Order, OrderStatus, Quote, Rfq } from '@tbdex/protocol' /** * Callback handler for GetExchanges requests @@ -18,7 +17,7 @@ export type GetOfferingsCallback = (ctx: RequestContext, filter: GetOfferingsFil * Callback handler for the SubmitRfq requests * @beta */ -export type SubmitRfqCallback = (ctx: RequestContext, message: Rfq, opts: { offering: Offering }) => Promise +export type SubmitRfqCallback = (ctx: RequestContext, message: Rfq, opts: { offering: Offering, replyTo?: string }) => Promise /** * Callback handler for the SubmitOrder requests @@ -43,6 +42,12 @@ export type GetOfferingsFilter = { /** Currency that the PFI is selling - ISO 3166 currency code string */ payoutCurrency?: string + /** The payin method used to pay money to the PFI */ + payinMethodKind?: string + + /** The payout method to receive money from the PFI */ + payoutMethodKind?: string + /** Offering ID */ id?: string } @@ -69,12 +74,6 @@ export type RequestContext = { response: Response } -/** - * Type alias for the request handler - * @beta - */ -export type RequestHandler = (request: Request, response: Response<{ errors?: ErrorDetail[], data?: any }>) => any - /** * PFI Offerings API * @beta @@ -88,7 +87,7 @@ export interface OfferingsApi { /** * Retrieve a list of offerings based on the given filter */ - getOfferings(opts?: { filter: GetOfferingsFilter }): Promise + getOfferings(opts?: { filter: GetOfferingsFilter }): Promise } /** @@ -99,12 +98,12 @@ export interface ExchangesApi { /** * Retrieve a list of exchanges based on the given filter */ - getExchanges(opts?: { filter: GetExchangesFilter }): Promise + getExchanges(opts?: { filter: GetExchangesFilter }): Promise /** * Retrieve a single exchange if found */ - getExchange(opts: { id: string }): Promise + getExchange(opts: { id: string }): Promise /** * Retrieve a RFQ if found @@ -122,12 +121,12 @@ export interface ExchangesApi { getOrder(opts: { exchangeId: string }): Promise /** - * Retrieve the order statuses if found + * Retrieve the OrderStatuses if found */ - getOrderStatuses(opts: { exchangeId: string }): Promise + getOrderStatuses(opts: { exchangeId: string }): Promise /** - * Retrieve the close reason if found + * Retrieve the Close reason if found */ getClose(opts: { exchangeId: string }): Promise } \ No newline at end of file diff --git a/packages/http-server/tests/create-exchange.spec.ts b/packages/http-server/tests/create-exchange.spec.ts index 14f64a64..b192e14f 100644 --- a/packages/http-server/tests/create-exchange.spec.ts +++ b/packages/http-server/tests/create-exchange.spec.ts @@ -1,18 +1,23 @@ -import type { ErrorDetail } from '@tbdex/http-client' +import { ErrorDetail, Offering, Rfq } from '@tbdex/http-client' import type { Server } from 'http' -import { DevTools, TbdexHttpServer } from '../src/main.js' +import { DevTools, RequestContext, TbdexHttpServer } from '../src/main.js' import { expect } from 'chai' - -let api = new TbdexHttpServer() -let server: Server +import { InMemoryExchangesApi } from '../src/in-memory-exchanges-api.js' +import { InMemoryOfferingsApi } from '../src/in-memory-offerings-api.js' +import { BearerDid } from '@web5/dids' +import Sinon from 'sinon' describe('POST /exchanges/:exchangeId/rfq', () => { - before(() => { + let api: TbdexHttpServer + let server: Server + + beforeEach(() => { + api = new TbdexHttpServer() server = api.listen(8000) }) - after(() => { + afterEach(() => { server.close() server.closeAllConnections() }) @@ -56,7 +61,7 @@ describe('POST /exchanges/:exchangeId/rfq', () => { const resp = await fetch('http://localhost:8000/exchanges/123/rfq', { method : 'POST', - body : JSON.stringify({ rfq: rfq, replyTo: 'foo'}) + body : JSON.stringify({ rfq: rfq, replyTo: 'foo' }) }) expect(resp.status).to.equal(400) @@ -69,10 +74,291 @@ describe('POST /exchanges/:exchangeId/rfq', () => { 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') - xit('returns a 400 if request body if offering doesnt exist') - xit(`returns a 400 if request body if RFQ does not fulfill offering's requirements`) - xit(`returns a 202 if RFQ is accepted`) + it('returns a 400 if request body is not a valid RFQ', async () => { + const aliceDid = await DevTools.createDid() + const pfiDid = await DevTools.createDid() + + const order = await DevTools.createOrder({ sender: aliceDid, receiver: pfiDid }) + await order.sign(aliceDid) + + const resp = await fetch('http://localhost:8000/exchanges/123/rfq', { + method : 'POST', + body : JSON.stringify({ rfq: order }) + }) + + 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('Parsing of TBDex Rfq message failed') + }) + + it('returns a 400 if request body if integrity check fails', async () => { + const aliceDid = await DevTools.createDid() + const pfiDid = await DevTools.createDid() + + const rfq = await DevTools.createRfq({ sender: aliceDid, receiver: pfiDid }) + // deliberately omit rfq.sign(aliceDid) + + const resp = await fetch('http://localhost:8000/exchanges/123/rfq', { + method : 'POST', + body : JSON.stringify({ rfq }) + }) + + 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('Parsing of TBDex Rfq message failed') + }) + + it('returns a 409 if request body if RFQ already exists', async () => { + const aliceDid = await DevTools.createDid() + const pfiDid = await DevTools.createDid() + + const rfq = await DevTools.createRfq({ sender: aliceDid, receiver: pfiDid }) + await rfq.sign(aliceDid); + + (api.exchangesApi as InMemoryExchangesApi).addMessage(rfq) + + const resp = await fetch('http://localhost:8000/exchanges/123/rfq', { + method : 'POST', + body : JSON.stringify({ rfq }) + }) + + expect(resp.status).to.equal(409) + + 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('already exists') + }) + + it('returns a 400 if request body if offering doesnt exist', async () => { + const aliceDid = await DevTools.createDid() + const pfiDid = await DevTools.createDid() + + const offering = DevTools.createOffering() + // deliberately omit (api.offeringsApi as InMemoryOfferingsApi).addOffering(offering) + const rfq = Rfq.create({ + metadata: { + from : aliceDid.uri, + to : pfiDid.uri, + }, + data: { + ...await DevTools.createRfqData(), + offeringId: offering.metadata.id, + }, + }) + await rfq.sign(aliceDid) + + const resp = await fetch('http://localhost:8000/exchanges/123/rfq', { + method : 'POST', + body : JSON.stringify({ rfq }) + }) + + 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(`offering ${offering.metadata.id} does not exist`) + }) + + it(`returns a 400 if request body if RFQ does not fulfill offering's requirements`, async () => { + const aliceDid = await DevTools.createDid() + const pfiDid = await DevTools.createDid() + + // Add offering to api.offeringsApi + const offering = DevTools.createOffering() + await offering.sign(pfiDid); + (api.offeringsApi as InMemoryOfferingsApi).addOffering(offering) + + // Create Rfq which doesn't contain the required claims + const rfq = Rfq.create({ + metadata: { + from : aliceDid.uri, + to : pfiDid.uri, + }, + data: { + ...await DevTools.createRfqData(), + offeringId : offering.metadata.id, + claims : [], + }, + }) + await rfq.sign(aliceDid) + + const resp = await fetch('http://localhost:8000/exchanges/123/rfq', { + method : 'POST', + body : JSON.stringify({ rfq }) + }) + + 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('Failed to verify offering requirements') + }) + + describe('RFQ satisfies all requirements', () => { + let aliceDid: BearerDid + let pfiDid: BearerDid + let offering: Offering + let rfq: Rfq + + beforeEach(async () => { + aliceDid = await DevTools.createDid() + pfiDid = await DevTools.createDid() + + // Add offering with no required claims to api.offeringsApi + offering = Offering.create({ + metadata: { + from: pfiDid.uri, + }, + data: { + ...DevTools.createOfferingData(), + requiredClaims : undefined, + payinCurrency : { + currencyCode : 'BTC', + minAmount : '1000.0' + }, + payoutCurrency: { + currencyCode : 'BTC', + minAmount : '1000.0', + }, + payinMethods: [{ + kind : 'BTC_ADDRESS', + requiredPaymentDetails : { + $schema : 'http://json-schema.org/draft-07/schema', + type : 'object', + properties : { + btcAddress: { + type : 'string', + description : 'your Bitcoin wallet address' + } + }, + required : ['btcAddress'], + additionalProperties : false + } + }], + payoutMethods: [{ + kind : 'BTC_ADDRESS', + requiredPaymentDetails : { + $schema : 'http://json-schema.org/draft-07/schema', + type : 'object', + properties : { + btcAddress: { + type : 'string', + description : 'your Bitcoin wallet address' + } + }, + required : ['btcAddress'], + additionalProperties : false + } + }] + }, + }) + await offering.sign(pfiDid); + (api.offeringsApi as InMemoryOfferingsApi).addOffering(offering) + + // Create Rfq which satisfies Offering requirements + rfq = Rfq.create({ + metadata: { + from : aliceDid.uri, + to : pfiDid.uri, + }, + data: { + ...DevTools.createRfqData(), + offeringId : offering.metadata.id, + claims : [], + payinAmount : offering.data.payinCurrency.minAmount!, + payinMethod : { + kind : offering.data.payinMethods[0].kind, + paymentDetails : { + btcAddress: '1234', + } + }, + payoutMethod: { + kind : offering.data.payoutMethods[0].kind, + paymentDetails : { + btcAddress: '1234', + } + } + } + }) + await rfq.sign(aliceDid) + }) + + it('returns a 202 if RFQ is accepted', async () => { + const resp = await fetch('http://localhost:8000/exchanges/123/rfq', { + method : 'POST', + body : JSON.stringify({ rfq }) + }) + + expect(resp.status).to.equal(202) + }) + + it('returns a 202 if the provided callback succeeds and passes correct arguments to callback', async () => { + const callbackSpy = Sinon.spy( + (_ctx: RequestContext, _message: Rfq, _opts: { offering: Offering, replyTo?: string }) => { + return Promise.resolve() + }) + api.onSubmitRfq(callbackSpy) + + const resp = await fetch('http://localhost:8000/exchanges/123/rfq', { + method : 'POST', + body : JSON.stringify({ rfq }) + }) + + expect(resp.status).to.equal(202) + + expect(callbackSpy.callCount).to.eq(1) + expect(callbackSpy.firstCall.args.length).to.eq(3) + + expect(callbackSpy.firstCall.args.at(1)).to.deep.eq(rfq) + const lastCallbackArg = callbackSpy.firstCall.args.at(2) as { offering: Offering, replyTo?: string } + expect(lastCallbackArg.offering).to.deep.eq(offering) + expect(lastCallbackArg.replyTo).to.be.undefined + }) + + it('passes replyTo to the callback if it is provided in the request', async () => { + const callbackSpy = Sinon.spy( + (_ctx: RequestContext, _message: Rfq, _opts: { offering: Offering, replyTo?: string }) =>{ + return Promise.resolve() + }) + api.onSubmitRfq(callbackSpy) + + const replyTo = 'https://tbdex.io/example' + + const resp = await fetch('http://localhost:8000/exchanges/123/rfq', { + method : 'POST', + body : JSON.stringify({ rfq, replyTo }) + }) + + expect(resp.status).to.equal(202) + + expect(callbackSpy.callCount).to.eq(1) + expect(callbackSpy.firstCall.args.length).to.eq(3) + + expect(callbackSpy.firstCall.args.at(1)).to.deep.eq(rfq) + const lastCallbackArg = callbackSpy.firstCall.args.at(2) as { offering: Offering, replyTo?: string } + expect(lastCallbackArg.offering).to.deep.eq(offering) + expect(lastCallbackArg.replyTo).to.eq(replyTo) + }) + + xit('creates the filter for OfferingsApi if it is provided in the request') + }) }) \ No newline at end of file diff --git a/packages/http-server/tests/get-exchanges.spec.ts b/packages/http-server/tests/get-exchanges.spec.ts index a4373846..c3c924b4 100644 --- a/packages/http-server/tests/get-exchanges.spec.ts +++ b/packages/http-server/tests/get-exchanges.spec.ts @@ -1,19 +1,22 @@ -import type { ExchangesApi, GetExchangesFilter, Message } from '../src/main.js' import type { Server } from 'http' +import Sinon, * as sinon from 'sinon' -import { TbdexHttpServer, Rfq, Quote, Order, OrderStatus, Close, TbdexHttpClient } from '../src/main.js' +import { TbdexHttpServer, RequestContext, GetExchangesFilter } from '../src/main.js' import { DidJwk } from '@web5/dids' import { expect } from 'chai' - -let api = new TbdexHttpServer() -let server: Server +import { InMemoryExchangesApi } from '../src/in-memory-exchanges-api.js' +import { DevTools, ErrorDetail, TbdexHttpClient } from '@tbdex/http-client' describe('GET /exchanges', () => { - before(() => { + let server: Server + let api: TbdexHttpServer + + beforeEach(() => { + api = new TbdexHttpServer() server = api.listen(8000) }) - after(() => { + afterEach(() => { server.close() server.closeAllConnections() }) @@ -24,56 +27,137 @@ describe('GET /exchanges', () => { expect(resp.ok).to.be.false expect(resp.status).to.equal(401) - const respBody = await resp.json() + const respBody = await resp.json() as { errors: ErrorDetail[] } expect(respBody['errors']).to.exist expect(respBody['errors'].length).to.equal(1) expect(respBody['errors'][0]['detail']).to.include('Authorization') }) - it(`passes the requester's did to getExchanges method`, async () => { - let functionReached = false - const alice = await DidJwk.create() - - const exchangesApi: ExchangesApi = { - getExchanges: async function (opts: { filter: GetExchangesFilter }): Promise { - functionReached = true - expect(opts.filter.from).to.exist - expect(opts.filter.from).to.equal(alice.uri) - - return [] - }, - getExchange: function (): Promise { - throw new Error('Function not implemented.') - }, - getRfq: function (): Promise { - throw new Error('Function not implemented.') - }, - getQuote: function (): Promise { - throw new Error('Function not implemented.') - }, - getOrder: function (): Promise { - throw new Error('Function not implemented.') - }, - getOrderStatuses: function (): Promise { - throw new Error('Function not implemented.') - }, - getClose: function (): Promise { - throw new Error('Function not implemented.') + it('returns 401 if bearer token is missing from the Authorization header', async () => { + const resp = await fetch('http://localhost:8000/exchanges', { + headers: { + 'Authorization': 'Not well formatted token' } - } + }) - const testApi = new TbdexHttpServer({ exchangesApi, pfiDid: 'did:ex:pfi' }) - const server = testApi.listen(8001) - const requestToken = await TbdexHttpClient.generateRequestToken({ requesterDid: alice, pfiDid: 'did:ex:pfi' }) - const resp = await fetch('http://localhost:8001/exchanges', { + const respBody = await resp.json() as { errors: ErrorDetail[] } + expect(respBody['errors']).to.exist + expect(respBody['errors'].length).to.equal(1) + expect(respBody['errors'][0]['detail']).to.include('Malformed Authorization header. Expected: Bearer TOKEN_HERE') + }) + + it('returns 401 if the bearer token is malformed in the Authorization header', async () => { + const resp = await fetch('http://localhost:8000/exchanges', { headers: { - 'Authorization': `Bearer ${requestToken}` + 'Authorization': 'Bearer MALFORMED' } }) - expect(resp.ok).to.be.true - expect(functionReached).to.be.true + const respBody = await resp.json() as { errors: ErrorDetail[] } + expect(respBody['errors']).to.exist + expect(respBody['errors'].length).to.equal(1) + expect(respBody['errors'][0]['detail']).to.include('Malformed Authorization header') + }) - server.closeAllConnections() + describe('Passes filter to ExchangesApi.getExchanges', () => { + it(`passes the requester's did to the filter of ExchangesApi.getExchanges`, async () => { + const alice = await DidJwk.create() + + const exchangesApiSpy = sinon.spy(api.exchangesApi, 'getExchanges') + + const requestToken = await TbdexHttpClient.generateRequestToken({ requesterDid: alice, pfiDid: api.pfiDid }) + const resp = await fetch('http://localhost:8000/exchanges', { + headers: { + 'Authorization': `Bearer ${requestToken}` + } + }) + + expect(resp.ok).to.be.true + expect(exchangesApiSpy.calledOnce).to.be.true + expect(exchangesApiSpy.calledWith({ + filter: { + from: alice.uri + } + })).to.be.true + + exchangesApiSpy.restore() + }) + + it('passes the id non-array query param as an array to the filter of ExchangesApi.getExchanges', async () => { + const alice = await DidJwk.create() + + const exchangesApiSpy = sinon.spy(api.exchangesApi, 'getExchanges') + + const requestToken = await TbdexHttpClient.generateRequestToken({ requesterDid: alice, pfiDid: api.pfiDid }) + + // `id` query param contains a single string + const idQueryParam = '1234' + const resp = await fetch(`http://localhost:8000/exchanges?id=${idQueryParam}`, { + headers: { + 'Authorization': `Bearer ${requestToken}` + } + }) + + expect(resp.ok).to.be.true + expect(exchangesApiSpy.calledOnce).to.be.true + expect(exchangesApiSpy.calledWith({ + filter: { + from : alice.uri, + id : [idQueryParam] + } + })) + + exchangesApiSpy.restore() + }) + + it('passes the id array query param as an array to the filter of ExchangesApi.getExchanges', async () => { + const alice = await DidJwk.create() + + const exchangesApiSpy = sinon.spy(api.exchangesApi, 'getExchanges') + + const requestToken = await TbdexHttpClient.generateRequestToken({ requesterDid: alice, pfiDid: api.pfiDid }) + + // `id` query param contains an array + const idQueryParam = ['1234', '5678'] + const resp = await fetch(`http://localhost:8000/exchanges?id=[${idQueryParam.join(',')}]`, { + headers: { + 'Authorization': `Bearer ${requestToken}` + } + }) + + expect(resp.ok).to.be.true + expect(exchangesApiSpy.calledOnce).to.be.true + expect(exchangesApiSpy.calledWith({ + filter: { + from : alice.uri, + id : idQueryParam + } + })) + + exchangesApiSpy.restore() + }) + }) + + it('calls the callback if it is provided', async () => { + const aliceDid = await DevTools.createDid() + const pfiDid = await DevTools.createDid() + const rfq = await DevTools.createRfq({ sender: aliceDid, receiver: pfiDid }) + await rfq.sign(aliceDid); + (api.exchangesApi as InMemoryExchangesApi).addMessage(rfq) + + const callbackSpy = Sinon.spy((_ctx: RequestContext, _filter: GetExchangesFilter) => Promise.resolve()) + api.onGetExchanges(callbackSpy) + + const requestToken = await TbdexHttpClient.generateRequestToken({ requesterDid: aliceDid, pfiDid: api.pfiDid }) + + const resp = await fetch(`http://localhost:8000/exchanges`, { + headers: { + 'Authorization': `Bearer ${requestToken}` + } + }) + expect(resp.status).to.equal(200) + + expect(callbackSpy.callCount).to.eq(1) + // TODO: Check what arguments are passed to callback after we finalize its behavior }) }) \ No newline at end of file diff --git a/packages/http-server/tests/get-offerings.spec.ts b/packages/http-server/tests/get-offerings.spec.ts index 9144a2a1..436d043a 100644 --- a/packages/http-server/tests/get-offerings.spec.ts +++ b/packages/http-server/tests/get-offerings.spec.ts @@ -1,28 +1,86 @@ -import type { Offering } from '@tbdex/protocol' +import { DevTools, Offering } from '@tbdex/protocol' import type { Server } from 'http' -import { TbdexHttpServer } from '../src/main.js' +import { GetOfferingsFilter, RequestContext, TbdexHttpServer } from '../src/main.js' import { expect } from 'chai' - -let api = new TbdexHttpServer() -let server: Server +import { InMemoryOfferingsApi } from '../src/in-memory-offerings-api.js' +import Sinon from 'sinon' describe('GET /offerings', () => { - before(() => { + let api: TbdexHttpServer + let server: Server + + beforeEach(() => { + api = new TbdexHttpServer() server = api.listen(8000) }) - after(() => { + afterEach(() => { server.close() server.closeAllConnections() }) it('returns an array of offerings', async () => { + const pfiDid = await DevTools.createDid() + const offering = DevTools.createOffering() + await offering.sign(pfiDid); + (api.offeringsApi as InMemoryOfferingsApi).addOffering(offering) + const response = await fetch('http://localhost:8000/offerings') expect(response.status).to.equal(200) - const respaunzBody = await response.json() as { data: Offering[] } - expect(respaunzBody.data).to.exist - expect(respaunzBody.data.length).to.equal(1) + const responseBody = await response.json() as { data: Offering[] } + expect(responseBody.data).to.exist + expect(responseBody.data.length).to.equal(1) + expect(responseBody.data[0]).to.deep.eq(offering.toJSON()) + }) + + it('constructs the filter from query params and passes it to OfferingsApi', async () => { + const pfiDid = await DevTools.createDid() + + // Add an offering to OfferingsApi + const offering = DevTools.createOffering() + await offering.sign(pfiDid); + (api.offeringsApi as InMemoryOfferingsApi).addOffering(offering) + + // Set up spy + const exchangesApiSpy = Sinon.spy(api.offeringsApi, 'getOfferings') + + // Specify query params + const queryParams: GetOfferingsFilter = { + id : offering.metadata.id, + payinCurrency : offering.data.payinCurrency.currencyCode, + payoutCurrency : offering.data.payoutCurrency.currencyCode, + payinMethodKind : offering.data.payinMethods[0].kind, + payoutMethodKind : offering.data.payoutMethods[0].kind + } + const queryParamsString: string = + Object.entries(queryParams) + .map(([k, v]) => `${k}=${v}`) + .join('&') + + + const response = await fetch(`http://localhost:8000/offerings?${queryParamsString}`) + + expect(response.status).to.equal(200) + + expect(exchangesApiSpy.calledOnce).to.be.true + expect(exchangesApiSpy.firstCall.args[0]?.filter).to.deep.eq(queryParams) + }) + + it('calls the callback if it is provided', async () => { + const pfiDid = await DevTools.createDid() + const offering = DevTools.createOffering() + await offering.sign(pfiDid); + (api.offeringsApi as InMemoryOfferingsApi).addOffering(offering) + + const callbackSpy = Sinon.spy((_ctx: RequestContext, _filter: GetOfferingsFilter) => Promise.resolve()) + api.onGetOfferings(callbackSpy) + + const response = await fetch('http://localhost:8000/offerings?filter=') + expect(response.status).to.equal(200) + + expect(callbackSpy.callCount).to.eq(1) + // TODO: Check what arguments are passed to callback after we finalize its behavior }) }) \ No newline at end of file diff --git a/packages/http-server/tests/submit-close.spec.ts b/packages/http-server/tests/submit-close.spec.ts index 62395959..2da13d12 100644 --- a/packages/http-server/tests/submit-close.spec.ts +++ b/packages/http-server/tests/submit-close.spec.ts @@ -1,24 +1,22 @@ -import type { ErrorDetail } from '@tbdex/http-client' +import { ErrorDetail, Message } from '@tbdex/http-client' import type { Server } from 'http' import { Close, DevTools, TbdexHttpServer } from '../src/main.js' import { expect } from 'chai' -import { FakeExchangesApi } from '../src/fakes.js' +import { InMemoryExchangesApi } from '../src/in-memory-exchanges-api.js' +import Sinon from 'sinon' -let api = new TbdexHttpServer() -let server: Server -const did = await DevTools.createDid() describe('POST /exchanges/:exchangeId/close', () => { - before(() => { + let api: TbdexHttpServer + let server: Server + + beforeEach(() => { + api = new TbdexHttpServer() server = api.listen(8000) }) afterEach(() => { - (api.exchangesApi as FakeExchangesApi).clearMessages() - }) - - after(() => { server.close() server.closeAllConnections() }) @@ -54,16 +52,38 @@ describe('POST /exchanges/:exchangeId/close', () => { expect(error.detail).to.include('JSON') }) + it('returns a 400 if request body is not a valid close object', async () => { + const alice = await DevTools.createDid() + const rfq = DevTools.createRfq({ + sender: alice + }) + const resp = await fetch('http://localhost:8000/exchanges/123/close', { + method : 'POST', + body : JSON.stringify(rfq) + }) + + 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('Request body was not a valid Close message') + }) + it(`returns a 404 if the exchange doesn't exist`, async () => { + const alice = await DevTools.createDid() + const pfi = await DevTools.createDid() const close = Close.create({ metadata: { - from : did.uri, - to : did.uri, + from : alice.uri, + to : pfi.uri, exchangeId : '123' }, data: {} }) - await close.sign(did) + await close.sign(alice) const resp = await fetch('http://localhost:8000/exchanges/123/close', { method : 'POST', body : JSON.stringify(close) @@ -80,28 +100,37 @@ describe('POST /exchanges/:exchangeId/close', () => { }) it(`returns a 409 if close is not allowed based on the exchange's current state`, async () => { + const alice = await DevTools.createDid() + const pfi = await DevTools.createDid() + + const rfq = await DevTools.createRfq({ + sender : alice, + receiver : pfi, + }) + await rfq.sign(alice) const close = Close.create({ metadata: { - from : did.uri, - to : did.uri, - exchangeId : '123' + from : alice.uri, + to : pfi.uri, + exchangeId : rfq.metadata.exchangeId }, data: {} }) - await close.sign(did) + await close.sign(alice) - const exchangesApi = api.exchangesApi as FakeExchangesApi + const exchangesApi = api.exchangesApi as InMemoryExchangesApi + exchangesApi.addMessage(rfq) exchangesApi.addMessage(close) const close2 = Close.create({ metadata: { - from : did.uri, - to : did.uri, - exchangeId : '123' + from : alice.uri, + to : pfi.uri, + exchangeId : rfq.metadata.exchangeId }, data: {} }) - await close2.sign(did) + await close2.sign(alice) const resp = await fetch('http://localhost:8000/exchanges/123/close', { method : 'POST', body : JSON.stringify(close2) @@ -117,7 +146,220 @@ describe('POST /exchanges/:exchangeId/close', () => { expect(error.detail).to.include('cannot submit Close for an exchange where the last message is kind: close') }) - xit('returns a 400 if request body is not a valid Close') - xit('returns a 400 if request body if integrity check fails') - xit(`returns a 202 if close is accepted`) + it('returns a 400 if request body if integrity check fails', async () => { + const alice = await DevTools.createDid() + const pfi = await DevTools.createDid() + + const rfq = await DevTools.createRfq({ + sender : alice, + receiver : pfi, + }) + await rfq.sign(alice) + + const exchangesApi = api.exchangesApi as InMemoryExchangesApi + exchangesApi.addMessage(rfq) + + // Create but do not sign Close message + const close = Close.create({ + metadata: { + from : alice.uri, + to : pfi.uri, + exchangeId : rfq.metadata.exchangeId + }, + data: {} + }) + + + const resp = await fetch('http://localhost:8000/exchanges/123/close', { + method : 'POST', + body : JSON.stringify(close) + }) + + 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('Request body was not a valid Close message') + }) + + it('returns a 202 if close is created by alice', async () => { + // scenario: Alice creates an exchange and submits a Close message + + const alice = await DevTools.createDid() + const pfi = await DevTools.createDid() + + const rfq = await DevTools.createRfq({ + sender : alice, + receiver : pfi, + }) + await rfq.sign(alice) + + const exchangesApi = api.exchangesApi as InMemoryExchangesApi + exchangesApi.addMessage(rfq) + + // Close message signed by Alice + const close = Close.create({ + metadata: { + from : alice.uri, + to : pfi.uri, + exchangeId : rfq.metadata.exchangeId + }, + data: {} + }) + await close.sign(alice) + + const resp = await fetch('http://localhost:8000/exchanges/123/close', { + method : 'POST', + body : JSON.stringify(close) + }) + + expect(resp.status).to.equal(202) + }) + + it('returns a 202 if close is created by pfi', async () => { + // scenario: Alice creates an exchange and PFI submits a Close message + + const alice = await DevTools.createDid() + const pfi = await DevTools.createDid() + + const rfq = await DevTools.createRfq({ + sender : alice, + receiver : pfi, + }) + await rfq.sign(alice) + + const exchangesApi = api.exchangesApi as InMemoryExchangesApi + exchangesApi.addMessage(rfq) + + // Close message signed by PFI + const close = Close.create({ + metadata: { + from : pfi.uri, + to : alice.uri, + exchangeId : rfq.metadata.exchangeId + }, + data: {} + }) + await close.sign(pfi) + + const resp = await fetch('http://localhost:8000/exchanges/123/close', { + method : 'POST', + body : JSON.stringify(close) + }) + + expect(resp.status).to.equal(202) + }) + + it('returns a 400 if the close is created by neither alice nor pfi', async () => { + + const alice = await DevTools.createDid() + const pfi = await DevTools.createDid() + const imposter = await DevTools.createDid() + + const rfq = await DevTools.createRfq({ + sender : alice, + receiver : pfi, + }) + await rfq.sign(alice) + + const exchangesApi = api.exchangesApi as InMemoryExchangesApi + exchangesApi.addMessage(rfq) + + // Close message signed by the imposter + const close = Close.create({ + metadata: { + from : imposter.uri, + to : pfi.uri, + exchangeId : rfq.metadata.exchangeId + }, + data: {} + }) + await close.sign(imposter) + + const resp = await fetch('http://localhost:8000/exchanges/123/close', { + method : 'POST', + body : JSON.stringify(close) + }) + + 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('Only the creator and receiver of an exchange may close the exchange') + }) + + describe('onSubmitClose callback', () => { + it('does not call the callback if the close is is not valid for the current exchange', async () => { + const alice = await DevTools.createDid() + const pfi = await DevTools.createDid() + + // Close message signed by Alice + const close = Close.create({ + metadata: { + from : alice.uri, + to : pfi.uri, + exchangeId : Message.generateId('rfq') + }, + data: {} + }) + await close.sign(alice) + + const callbackSpy = Sinon.spy(() => Promise.resolve()) + api.onSubmitClose(callbackSpy) + + const resp = await fetch('http://localhost:8000/exchanges/123/close', { + method : 'POST', + body : JSON.stringify(close) + }) + + expect(resp.status).to.equal(404) + expect(callbackSpy.notCalled).to.be.true + }) + + it('returns a 202 if the provided callback succeeds and passes correct arguments to callback', async () => { + const alice = await DevTools.createDid() + const pfi = await DevTools.createDid() + + const rfq = await DevTools.createRfq({ + sender : alice, + receiver : pfi, + }) + await rfq.sign(alice) + + const exchangesApi = api.exchangesApi as InMemoryExchangesApi + exchangesApi.addMessage(rfq) + + // Close message signed by Alice + const close = Close.create({ + metadata: { + from : alice.uri, + to : pfi.uri, + exchangeId : rfq.metadata.exchangeId + }, + data: {} + }) + await close.sign(alice) + + const callbackSpy = Sinon.spy(() => Promise.resolve()) + api.onSubmitClose(callbackSpy) + + const resp = await fetch('http://localhost:8000/exchanges/123/close', { + method : 'POST', + body : JSON.stringify(close) + }) + + expect(resp.status).to.equal(202) + + expect(callbackSpy.calledOnce).to.be.true + expect(callbackSpy.firstCall.lastArg).to.deep.eq(close) + }) + + xit('returns error if the callback throws a CallbackError', async () => {}) + }) }) \ No newline at end of file diff --git a/packages/http-server/tests/submit-order.spec.ts b/packages/http-server/tests/submit-order.spec.ts index a94f3fc6..84781b20 100644 --- a/packages/http-server/tests/submit-order.spec.ts +++ b/packages/http-server/tests/submit-order.spec.ts @@ -1,19 +1,22 @@ -import type { ErrorDetail } from '@tbdex/http-client' +import { ErrorDetail, Message, Quote, Rfq } from '@tbdex/http-client' import type { Server } from 'http' -import { DevTools, Order, TbdexHttpServer } from '../src/main.js' +import { DevTools, Order, RequestContext, TbdexHttpServer } from '../src/main.js' import { expect } from 'chai' +import { InMemoryExchangesApi } from '../src/in-memory-exchanges-api.js' +import Sinon from 'sinon' -let api = new TbdexHttpServer() -let server: Server -const did = await DevTools.createDid() describe('POST /exchanges/:exchangeId/order', () => { - before(() => { + let api: TbdexHttpServer + let server: Server + + beforeEach(() => { + api = new TbdexHttpServer() server = api.listen(8000) }) - after(() => { + afterEach(() => { server.close() server.closeAllConnections() }) @@ -50,14 +53,16 @@ describe('POST /exchanges/:exchangeId/order', () => { }) it(`returns a 404 if the exchange doesn't exist`, async () => { + const aliceDid = await DevTools.createDid() + const pfiDid = await DevTools.createDid() const order = Order.create({ metadata: { - from : did.uri, - to : did.uri, + from : aliceDid.uri, + to : pfiDid.uri, exchangeId : '123' } }) - await order.sign(did) + await order.sign(aliceDid) const resp = await fetch('http://localhost:8000/exchanges/123/order', { method : 'POST', body : JSON.stringify(order) @@ -73,9 +78,247 @@ describe('POST /exchanges/:exchangeId/order', () => { expect(error.detail).to.include('No exchange found for') }) - xit(`returns a 409 if order is not allowed based on the exchange's current state`) - xit(`returns a 400 if quote has expired`) - xit('returns a 400 if request body is not a valid Order') - xit('returns a 400 if request body if integrity check fails') - xit(`returns a 202 if order is accepted`) + it('returns a 400 if request body is not a valid order object', async () => { + // scenario: Send an Rfq to the submitOrder endpoint + + 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/order', { + method : 'POST', + body : JSON.stringify(rfq) + }) + + 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('Request body was not a valid Order message') + }) + + it('returns a 400 if request body if integrity check fails', async () => { + const aliceDid = await DevTools.createDid() + const pfiDid = await DevTools.createDid() + + const order = Order.create({ + metadata: { + from : aliceDid.uri, + to : pfiDid.uri, + exchangeId : Message.generateId('rfq'), + }, + }) + // deliberately omit await order.sign(aliceDid) + + const resp = await fetch('http://localhost:8000/exchanges/123/order', { + method : 'POST', + body : JSON.stringify(order) + }) + + 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( 'Request body was not a valid Order message') + }) + + it(`returns a 409 if order is not allowed based on the exchange's current state`, async () => { + const aliceDid = await DevTools.createDid() + const pfiDid = await DevTools.createDid() + const rfq = Rfq.create({ + metadata: { + from : aliceDid.uri, + to : pfiDid.uri + }, + data: await DevTools.createRfqData() + }) + await rfq.sign(aliceDid); + (api.exchangesApi as InMemoryExchangesApi).addMessage(rfq) + + const order = Order.create({ + metadata: { + from : aliceDid.uri, + to : pfiDid.uri, + exchangeId : rfq.metadata.exchangeId, + }, + }) + await order.sign(aliceDid) + + const resp = await fetch('http://localhost:8000/exchanges/123/order', { + method : 'POST', + body : JSON.stringify(order) + }) + + expect(resp.status).to.equal(409) + + 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('Cannot submit Order for an exchange where the last message is kind:') + }) + + it(`returns a 400 if quote has expired`, async () => { + const aliceDid = await DevTools.createDid() + const pfiDid = await DevTools.createDid() + + // Add an exchange which has a Quote that expired 10 seconds ago + const rfq = Rfq.create({ + metadata: { + from : aliceDid.uri, + to : pfiDid.uri + }, + data: await DevTools.createRfqData() + }) + await rfq.sign(aliceDid) + const quote = Quote.create({ + metadata: { + from : pfiDid.uri, + to : aliceDid.uri, + exchangeId : rfq.metadata.exchangeId, + }, + data: { + ...DevTools.createQuoteData(), + expiresAt: new Date(Date.now() - 10_000).toISOString() + } + }) + await quote.sign(pfiDid); + (api.exchangesApi as InMemoryExchangesApi).addMessage(rfq); + (api.exchangesApi as InMemoryExchangesApi).addMessage(quote) + + const order = Order.create({ + metadata: { + from : aliceDid.uri, + to : pfiDid.uri, + exchangeId : rfq.metadata.exchangeId, + }, + }) + await order.sign(aliceDid) + + const resp = await fetch('http://localhost:8000/exchanges/123/order', { + method : 'POST', + body : JSON.stringify(order) + }) + + expect(resp.status).to.equal(410) + + 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('Quote is expired') + }) + + it('returns a 202 if order is accepted', async () => { + const aliceDid = await DevTools.createDid() + const pfiDid = await DevTools.createDid() + + // Add an exchange of Rfq and Quote to the exchangesApi + const rfq = Rfq.create({ + metadata: { + from : aliceDid.uri, + to : pfiDid.uri + }, + data: await DevTools.createRfqData() + }) + await rfq.sign(aliceDid) + const quote = Quote.create({ + metadata: { + from : pfiDid.uri, + to : aliceDid.uri, + exchangeId : rfq.metadata.exchangeId, + }, + data: { + ...DevTools.createQuoteData(), + expiresAt: new Date(Date.now() + 10_000).toISOString() + } + }) + await quote.sign(pfiDid); + + (api.exchangesApi as InMemoryExchangesApi).addMessage(rfq); + (api.exchangesApi as InMemoryExchangesApi).addMessage(quote) + + // Create order that is valid within the existing exchange + const order = Order.create({ + metadata: { + from : aliceDid.uri, + to : pfiDid.uri, + exchangeId : rfq.metadata.exchangeId, + }, + }) + await order.sign(aliceDid) + + const resp = await fetch('http://localhost:8000/exchanges/123/order', { + method : 'POST', + body : JSON.stringify(order) + }) + + expect(resp.status).to.equal(202) + }) + + describe('onSubmitClose callback', () => { + it('returns a 202 if the provided callback succeeds and passes correct arguments to callback', async () => { + const aliceDid = await DevTools.createDid() + const pfiDid = await DevTools.createDid() + + // Add an exchange of Rfq and Quote to the exchangesApi + const rfq = Rfq.create({ + metadata: { + from : aliceDid.uri, + to : pfiDid.uri + }, + data: await DevTools.createRfqData() + }) + await rfq.sign(aliceDid) + const quote = Quote.create({ + metadata: { + from : pfiDid.uri, + to : aliceDid.uri, + exchangeId : rfq.metadata.exchangeId, + }, + data: { + ...DevTools.createQuoteData(), + expiresAt: new Date(Date.now() + 10_000).toISOString() + } + }) + await quote.sign(pfiDid); + + (api.exchangesApi as InMemoryExchangesApi).addMessage(rfq); + (api.exchangesApi as InMemoryExchangesApi).addMessage(quote) + + // Create order that is valid within the existing exchange + const order = Order.create({ + metadata: { + from : aliceDid.uri, + to : pfiDid.uri, + exchangeId : rfq.metadata.exchangeId, + }, + }) + await order.sign(aliceDid) + + const callbackSpy = Sinon.spy((_ctx: RequestContext, _message: Order) => Promise.resolve()) + + api.onSubmitOrder(callbackSpy) + + const resp = await fetch('http://localhost:8000/exchanges/123/order', { + method : 'POST', + body : JSON.stringify(order) + }) + + expect(resp.status).to.equal(202) + + expect(callbackSpy.calledOnce).to.be.true + expect(callbackSpy.firstCall.lastArg).to.deep.eq(order) + }) + }) }) \ No newline at end of file diff --git a/packages/http-server/tests/tsconfig.json b/packages/http-server/tests/tsconfig.json index 7c6d2c8e..359d3749 100644 --- a/packages/http-server/tests/tsconfig.json +++ b/packages/http-server/tests/tsconfig.json @@ -1,6 +1,8 @@ { "extends": "../tsconfig.json", "compilerOptions": { + "strict": true, + "useUnknownInCatchVariables": false, "outDir": "compiled", "declarationDir": "compiled/types", "sourceMap": true, diff --git a/packages/http-server/tsconfig.json b/packages/http-server/tsconfig.json index eb52ab80..ae6f0159 100644 --- a/packages/http-server/tsconfig.json +++ b/packages/http-server/tsconfig.json @@ -1,5 +1,8 @@ { "compilerOptions": { + "strict": true, + "strictFunctionTypes": true, + "useUnknownInCatchVariables": false, "lib": ["es2022"], "target": "es2022", "module": "node16", diff --git a/packages/protocol/src/exchange.ts b/packages/protocol/src/exchange.ts index 85080aff..391154c3 100644 --- a/packages/protocol/src/exchange.ts +++ b/packages/protocol/src/exchange.ts @@ -21,11 +21,13 @@ export class Exchange { /** Message sent by Alice to the PFI to accept a quote*/ order: Order | undefined /** Message sent by the PFI to Alice to convet the current status of the order */ - orderstatus: OrderStatus | undefined + orderstatus: OrderStatus[] /** Message sent by either the PFI or Alice to terminate an exchange */ close: Close | undefined - constructor() {} + constructor() { + this.orderstatus = [] + } /** * Add a list of unsorted messages to an exchange. @@ -73,7 +75,7 @@ export class Exchange { } else if (message.isOrder()) { this.order = message } else if (message.isOrderStatus()) { - this.orderstatus = message + this.orderstatus.push(message) } else { // Unreachable throw new Error('Unrecognized message kind') @@ -95,7 +97,7 @@ export class Exchange { */ get latestMessage(): Message | undefined { return this.close ?? - this.orderstatus ?? + this.orderstatus[this.orderstatus.length - 1] ?? this.order ?? this.quote ?? this.rfq @@ -116,7 +118,7 @@ export class Exchange { this.rfq, this.quote, this.order, - this.orderstatus, + ...this.orderstatus, this.close ] return allPossibleMessages.filter((message): message is Message => message !== undefined) diff --git a/packages/protocol/src/message-kinds/order-status.ts b/packages/protocol/src/message-kinds/order-status.ts index 0f34ee35..9f9b5da8 100644 --- a/packages/protocol/src/message-kinds/order-status.ts +++ b/packages/protocol/src/message-kinds/order-status.ts @@ -18,7 +18,7 @@ export type CreateOrderStatusOptions = { */ export class OrderStatus extends Message { /** a set of valid Message kinds that can come after an order status */ - readonly validNext = new Set([]) + readonly validNext = new Set(['orderstatus']) /** The message kind (orderstatus) */ readonly kind = 'orderstatus' diff --git a/packages/protocol/tests/exchange.spec.ts b/packages/protocol/tests/exchange.spec.ts index 57a93c8e..e7c6f70e 100644 --- a/packages/protocol/tests/exchange.spec.ts +++ b/packages/protocol/tests/exchange.spec.ts @@ -97,7 +97,7 @@ describe('Exchange', () => { expect(exchange.rfq).to.deep.eq(rfq) expect(exchange.quote).to.deep.eq(quote) expect(exchange.order).to.deep.eq(order) - expect(exchange.orderstatus).to.deep.eq(orderStatus) + expect(exchange.orderstatus).to.deep.eq([orderStatus]) }) it('throws if the messages listed do not form a valid exchange', async () => { @@ -264,7 +264,7 @@ describe('Exchange', () => { exchange.addMessages([rfq, quote, order]) exchange.addNextMessage(orderStatus) - expect(exchange.orderstatus).to.deep.eq(orderStatus) + expect(exchange.orderstatus).to.deep.eq([orderStatus]) }) it('cannot add Rfq, Quote, Order, or Close after Order', async () => { @@ -332,4 +332,4 @@ describe('Exchange', () => { expect(exchange.messages).to.deep.eq([rfq, quote, order, orderStatus]) }) }) -}) \ No newline at end of file +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e27a8da5..9cc03501 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -170,6 +170,9 @@ importers: '@types/chai': specifier: 4.3.6 version: 4.3.6 + '@types/cors': + specifier: ^2.8.17 + version: 2.8.17 '@types/express': specifier: 4.17.17 version: 4.17.17 @@ -182,12 +185,18 @@ importers: '@types/node': specifier: 20.9.4 version: 20.9.4 + '@types/sinon': + specifier: ^17.0.3 + version: 17.0.3 chai: specifier: 4.3.10 version: 4.3.10 rimraf: specifier: 5.0.1 version: 5.0.1 + sinon: + specifier: 17.0.1 + version: 17.0.1 supertest: specifier: 6.3.3 version: 6.3.3 @@ -1227,6 +1236,12 @@ packages: '@types/node': 7.10.14 dev: true + /@types/cors@2.8.17: + resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} + dependencies: + '@types/node': 7.10.14 + dev: true + /@types/debounce@1.2.4: resolution: {integrity: sha512-jBqiORIzKDOToaF63Fm//haOCHuwQuLa2202RK4MozpA6lh93eCBc+/8+wZn5OzjJt3ySdc+74SXWXB55Ewtyw==} dev: true @@ -1408,6 +1423,12 @@ packages: '@types/sinonjs__fake-timers': 8.1.5 dev: true + /@types/sinon@17.0.3: + resolution: {integrity: sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==} + dependencies: + '@types/sinonjs__fake-timers': 8.1.5 + dev: true + /@types/sinonjs__fake-timers@8.1.5: resolution: {integrity: sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==} dev: true