diff --git a/openapi/resource-server.yaml b/openapi/resource-server.yaml index 55edbfa979..ee79f01850 100644 --- a/openapi/resource-server.yaml +++ b/openapi/resource-server.yaml @@ -126,7 +126,8 @@ paths: methods: - type: ilp ilpAddress: g.ilp.iwuyge987y.98y08y - sharedSecret: 1c7eaXa4rd2fFOBl1iydvCT1tV5TbM3RW1WLCafu_JA'401' + sharedSecret: 1c7eaXa4rd2fFOBl1iydvCT1tV5TbM3RW1WLCafu_JA + '401': $ref: '#/components/responses/401' '403': $ref: '#/components/responses/403' @@ -818,7 +819,7 @@ components: title: Wallet Address type: object description: A **wallet address** resource is the root of the API and contains the public details of the financial account represented by the Wallet Address that is also the service endpoint URL. - additionalProperties: false + additionalProperties: true examples: - id: 'https://ilp.rafiki.money/alice' publicName: Alice @@ -974,9 +975,16 @@ components: value: '0' assetCode: USD assetScale: 2 + - authServer: 'https://auth.rafiki.money' properties: receivedAmount: $ref: ./schemas.yaml#/components/schemas/amount + authServer: + type: string + format: uri + description: The URL of the authorization server endpoint for getting grants and access tokens for this wallet address. + required: + - authServer unresolvedProperites: false outgoing-payment: title: Outgoing Payment @@ -1283,7 +1291,6 @@ components: type: integer minimum: 1 maximum: 100 - default: 10 name: first in: query description: The number of items to return after the cursor. @@ -1292,7 +1299,6 @@ components: type: integer minimum: 1 maximum: 100 - default: 10 name: last in: query description: The number of items to return before the cursor. @@ -1329,14 +1335,16 @@ components: in: header schema: type: string - example: 'Signature: sig1=:EWJgAONk3D6542Scj8g51rYeMHw96cH2XiCMxcyL511wyemGcw==:' + examples: + - 'Signature: sig1=:EWJgAONk3D6542Scj8g51rYeMHw96cH2XiCMxcyL511wyemGcw==:' description: 'The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK.' optional-signature-input: name: Signature-Input in: header schema: type: string - example: 'Signature-Input: sig1=("@method" "@target-uri" "content-digest" "content-length" "content-type");created=1618884473;keyid="gnap-rsa"' + examples: + - 'Signature-Input: sig1=("@method" "@target-uri" "content-digest" "content-length" "content-type");created=1618884473;keyid="gnap-rsa"' description: 'The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member''s key is the label that uniquely identifies the message signature within the context of the HTTP message. The member''s value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization". When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details.' security: - GNAP: [] diff --git a/packages/backend/package.json b/packages/backend/package.json index e1933ab6e2..c14df09029 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -53,8 +53,8 @@ "@graphql-tools/load": "^8.0.0", "@graphql-tools/schema": "^10.0.0", "@interledger/http-signature-utils": "1.1.0", - "@interledger/open-payments": "5.2.2", - "@interledger/openapi": "1.2.0", + "@interledger/open-payments": "5.3.1", + "@interledger/openapi": "1.2.1", "@interledger/pay": "0.4.0-alpha.9", "@interledger/stream-receiver": "^0.3.3-alpha.3", "@koa/router": "^12.0.0", diff --git a/packages/backend/src/open_payments/receiver/service.test.ts b/packages/backend/src/open_payments/receiver/service.test.ts index ef17f65ced..088d6812d0 100644 --- a/packages/backend/src/open_payments/receiver/service.test.ts +++ b/packages/backend/src/open_payments/receiver/service.test.ts @@ -38,6 +38,8 @@ import assert from 'assert' import { Receiver } from './model' import { Grant } from '../grant/model' import { IncomingPaymentState } from '../payment/incoming/model' +import { PublicIncomingPayment } from '@interledger/open-payments/dist/types' +import { mockPublicIncomingPayment } from '@interledger/open-payments/dist/test/helpers' describe('Receiver Service', (): void => { let deps: IocContract @@ -123,10 +125,7 @@ describe('Receiver Service', (): void => { `('remote ($description)', ({ existingGrant }): void => { let walletAddress: OpenPaymentsWalletAddress let incomingPayment: OpenPaymentsIncomingPaymentWithPaymentMethods - let publicIncomingPayment: { - authServer: string - receivedAmount: OpenPaymentsIncomingPaymentWithPaymentMethods['receivedAmount'] - } + let publicIncomingPayment: PublicIncomingPayment const authServer = faker.internet.url({ appendSlash: false }) const INCOMING_PAYMENT_PATH = 'incoming-payments' const grantOptions = { @@ -172,8 +171,6 @@ describe('Receiver Service', (): void => { } } - let publicIncomingPaymentMock: () => Promise - beforeEach(async (): Promise => { walletAddress = mockWalletAddress({ authServer @@ -182,10 +179,10 @@ describe('Receiver Service', (): void => { id: `${walletAddress.id}/incoming-payments/${uuid()}`, walletAddress: walletAddress.id }) - publicIncomingPayment = { + publicIncomingPayment = mockPublicIncomingPayment({ authServer, receivedAmount: incomingPayment.receivedAmount - } + }) if (existingGrant) { await expect( grantService.create({ @@ -199,12 +196,6 @@ describe('Receiver Service', (): void => { managementId: '8f69de01-5bf9-4603-91ed-eeca101081f1' }) } - publicIncomingPaymentMock = () => - Promise.resolve({ - ok: true, - status: 200, - json: async () => publicIncomingPayment - } as Response) jest .spyOn(walletAddressService, 'getByUrl') .mockResolvedValueOnce(undefined) @@ -219,13 +210,14 @@ describe('Receiver Service', (): void => { ${false} | ${''} ${true} | ${'- after rotating access token'} `('resolves incoming payment $description', async ({ rotate }) => { - const fetchSpy = jest - .spyOn(global, 'fetch') - .mockImplementation(publicIncomingPaymentMock) const clientRequestGrantSpy = jest .spyOn(openPaymentsClient.grant, 'request') .mockResolvedValueOnce(grant) + const clientGetPublicIncomingPaymentSpy = jest + .spyOn(openPaymentsClient.incomingPayment, 'getPublic') + .mockResolvedValueOnce(publicIncomingPayment) + const clientGetIncomingPaymentSpy = jest .spyOn(openPaymentsClient.incomingPayment, 'get') .mockResolvedValueOnce(incomingPayment) @@ -270,13 +262,15 @@ describe('Receiver Service', (): void => { ] } }) - expect(fetchSpy).toHaveBeenCalledTimes(1) if (!existingGrant) { expect(clientRequestGrantSpy).toHaveBeenCalledWith( { url: authServer }, grantRequest ) } + expect(clientGetPublicIncomingPaymentSpy).toHaveBeenCalledWith({ + url: incomingPayment.id + }) expect(clientGetIncomingPaymentSpy).toHaveBeenCalledWith({ url: incomingPayment.id, accessToken: @@ -293,12 +287,9 @@ describe('Receiver Service', (): void => { }) test('returns undefined for invalid remote public incoming payment', async (): Promise => { - const fetchSpy = jest.spyOn(global, 'fetch').mockImplementation(() => - Promise.resolve({ - ok: false, - status: 400 - } as Response) - ) + const clientGetPublicIncomingPaymentSpy = jest + .spyOn(openPaymentsClient.incomingPayment, 'getPublic') + .mockRejectedValueOnce({ status: 404 }) await expect( receiverService.get( @@ -307,7 +298,7 @@ describe('Receiver Service', (): void => { }/${INCOMING_PAYMENT_PATH}/${uuid()}` ) ).resolves.toBeUndefined() - expect(fetchSpy).toHaveBeenCalledTimes(1) + expect(clientGetPublicIncomingPaymentSpy).toHaveBeenCalledTimes(1) }) if (existingGrant) { @@ -325,8 +316,8 @@ describe('Receiver Service', (): void => { expired: true } as Grant) jest - .spyOn(global, 'fetch') - .mockImplementation(publicIncomingPaymentMock) + .spyOn(openPaymentsClient.incomingPayment, 'getPublic') + .mockResolvedValueOnce(publicIncomingPayment) const clientRequestGrantSpy = jest.spyOn( openPaymentsClient.grant, 'request' @@ -345,8 +336,8 @@ describe('Receiver Service', (): void => { }) await grant?.$query(knex).patch({ expiresAt: new Date() }) jest - .spyOn(global, 'fetch') - .mockImplementation(publicIncomingPaymentMock) + .spyOn(openPaymentsClient.incomingPayment, 'getPublic') + .mockResolvedValueOnce(publicIncomingPayment) const clientRequestGrantSpy = jest.spyOn( openPaymentsClient.grant, 'request' @@ -360,8 +351,8 @@ describe('Receiver Service', (): void => { } else { test('returns undefined for invalid grant', async (): Promise => { jest - .spyOn(global, 'fetch') - .mockImplementation(publicIncomingPaymentMock) + .spyOn(openPaymentsClient.incomingPayment, 'getPublic') + .mockResolvedValueOnce(publicIncomingPayment) const clientRequestGrantSpy = jest .spyOn(openPaymentsClient.grant, 'request') .mockRejectedValueOnce(new Error('Could not request grant')) @@ -377,8 +368,8 @@ describe('Receiver Service', (): void => { test('returns undefined for interactive grant', async (): Promise => { jest - .spyOn(global, 'fetch') - .mockImplementation(publicIncomingPaymentMock) + .spyOn(openPaymentsClient.incomingPayment, 'getPublic') + .mockResolvedValueOnce(publicIncomingPayment) const clientRequestGrantSpy = jest .spyOn(openPaymentsClient.grant, 'request') .mockResolvedValueOnce({ @@ -401,8 +392,8 @@ describe('Receiver Service', (): void => { test('returns undefined when fetching remote incoming payment throws', async (): Promise => { jest - .spyOn(global, 'fetch') - .mockImplementation(publicIncomingPaymentMock) + .spyOn(openPaymentsClient.incomingPayment, 'getPublic') + .mockResolvedValueOnce(publicIncomingPayment) jest .spyOn(openPaymentsClient.grant, 'request') .mockResolvedValueOnce(grant) diff --git a/packages/backend/src/open_payments/receiver/service.ts b/packages/backend/src/open_payments/receiver/service.ts index 5c5b1086ba..0ee3211c2d 100644 --- a/packages/backend/src/open_payments/receiver/service.ts +++ b/packages/backend/src/open_payments/receiver/service.ts @@ -234,21 +234,10 @@ async function getIncomingPaymentGrant( deps: ServiceDependencies, incomingPaymentUrl: string ): Promise { - // TODO: replace with OP client method - // const publicIncomingPayment = - // await deps.openPaymentsClient.incomingPayment.getPublic(incomingPaymentUrl) - let url: string - if (deps.config.env === 'development') { - const parsedUrl = new URL(incomingPaymentUrl) - url = `http://${parsedUrl.hostname}${parsedUrl.pathname}` - } else { - url = incomingPaymentUrl - } - const publicIncomingPaymentResponse = await fetch(url) - if (!publicIncomingPaymentResponse.ok) { - return undefined - } - const publicIncomingPayment = await publicIncomingPaymentResponse.json() + const publicIncomingPayment = + await deps.openPaymentsClient.incomingPayment.getPublic({ + url: incomingPaymentUrl + }) if (!publicIncomingPayment || !publicIncomingPayment.authServer) { return undefined } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f5ce4ea6a..6c328193a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -275,11 +275,11 @@ importers: specifier: 1.1.0 version: 1.1.0 '@interledger/open-payments': - specifier: 5.2.2 - version: 5.2.2 + specifier: 5.3.1 + version: 5.3.1 '@interledger/openapi': - specifier: 1.2.0 - version: 1.2.0 + specifier: 1.2.1 + version: 1.2.1 '@interledger/pay': specifier: 0.4.0-alpha.9 version: 0.4.0-alpha.9 @@ -4330,11 +4330,11 @@ packages: - supports-color dev: false - /@interledger/open-payments@5.2.2: - resolution: {integrity: sha512-Tp65PDdgwX/Uc47zbuYpOoFAs9k4wV99LBKhEu804zBWeAmWy5ULRsJwDabSgqgmdnjkRhFJEmO/HOxVqb9rTg==} + /@interledger/open-payments@5.3.1: + resolution: {integrity: sha512-bFBoEKk6veFN2fwwkNGV4zrR+3GrIkv99ZWjIsHwV17lDht6NhsP3DttS0SwfYaR3VUIhmzAnCiHGOEKYkrybg==} dependencies: '@interledger/http-signature-utils': 1.1.0 - '@interledger/openapi': 1.2.0 + '@interledger/openapi': 1.2.1 axios: 1.5.1 base64url: 3.0.1 http-message-signatures: 0.1.2 @@ -4361,6 +4361,22 @@ packages: - supports-color dev: false + /@interledger/openapi@1.2.1: + resolution: {integrity: sha512-CVEMjLH94svT5ikaojplgZ3YlZAe7ml6G6bt0ZCbqkbwHszUwY1E6RexZZejiGju5QvwS/6Jl/Op5ymv/ECaLg==} + dependencies: + '@apidevtools/json-schema-ref-parser': 10.1.0 + ajv: 8.12.0 + ajv-formats: 2.1.1(ajv@8.12.0) + koa: 2.14.2 + openapi-default-setter: 12.1.3 + openapi-request-coercer: 12.1.3 + openapi-request-validator: 12.1.3 + openapi-response-validator: 12.1.3 + openapi-types: 12.1.3 + transitivePeerDependencies: + - supports-color + dev: false + /@interledger/pay@0.4.0-alpha.9: resolution: {integrity: sha512-ScT+hsAFBjpSy68VncSa6wW+VidgviKQE9W9lyiOBCrXfnrwrTdycEXWOG9ShoAYXpA3/FG/dYO9eImAPO5Pzg==} dependencies: diff --git a/postman/collections/Interledger.json b/postman/collections/Interledger.json index f5bc81be67..1f1ab213a0 100644 --- a/postman/collections/Interledger.json +++ b/postman/collections/Interledger.json @@ -720,7 +720,7 @@ "mode": "graphql", "graphql": { "query": "mutation CreateWalletAddressKey ($input: CreateWalletAddressKeyInput!) {\n createWalletAddressKey(input: $input) {\n code\n message\n success\n walletAddressKey {\n id\n revoked\n walletAddressId\n createdAt\n jwk {\n alg\n crv\n kid\n kty\n x\n }\n }\n }\n}", - "variables": "{\n \"input\": {\n \"walletAddresId\": \"{{walletAddressId}}\",\n \"jwk\": {\n \"alg\": \"EdDSA\",\n \"crv\": \"Ed25519\",\n \"kid\": \"kid_dad93e7f-f40b-484d-99d2-df12c8523176\",\n \"kty\": \"OKP\", \n \"x\": \"ubqoInifJ5sssIPPnQR1gVPfmoZnJtPhTkyMXNoJF_8\"\n }\n }\n}" + "variables": "{\n \"input\": {\n \"walletAddressId\": \"{{walletAddressId}}\",\n \"jwk\": {\n \"alg\": \"EdDSA\",\n \"crv\": \"Ed25519\",\n \"kid\": \"kid_dad93e7f-f40b-484d-99d2-df12c8523176\",\n \"kty\": \"OKP\", \n \"x\": \"ubqoInifJ5sssIPPnQR1gVPfmoZnJtPhTkyMXNoJF_8\"\n }\n }\n}" } }, "url": { @@ -2026,10 +2026,28 @@ "script": { "id": "dc5e8d6a-7834-405f-8198-465bb5795824", "exec": [ + "request.url = request.url", + " .replace(/{{(senderWalletAddress)}}/g, (_, key) => pm.environment.get(key))", + " .replace(/http:\\/\\/localhost:([3,4])000/g, (_, key) =>", + " key === '3'", + " ? 'https://' + pm.environment.get('host3000')", + " : 'https://' + pm.environment.get('host4000')", + " )", + "console.log(request.url)", + "", "if(pm.environment.get('preRequestHost')){", " eval(pm.environment.get('preRequestHost'))", "}", - "eval(pm.environment.get('preRequestSignatures'))" + "eval(pm.environment.get('preRequestSignatures'))", + "", + "pm.request.url.query.idx(2).value = pm.request.url.query.idx(2).value", + " .replace(/{{([A-Za-z]\\w+)}}/g, (_, key) => pm.environment.get(key))", + " .replace(/http:\\/\\/localhost:([3,4])000/g, (_, key) =>", + " key === '3'", + " ? 'https://' + pm.environment.get('host3000')", + " : 'https://' + pm.environment.get('host4000')", + " )", + "" ], "type": "text/javascript" }