Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Adding AS types & grant requests support to open-payments client #727

Merged
merged 19 commits into from
Nov 17, 2022

Conversation

mkurapov
Copy link
Contributor

@mkurapov mkurapov commented Nov 4, 2022

Changes proposed in this pull request

  • Generate open payment AS open APIs types, export them
  • Enable open payments client functionality for interactive and non-interactive grant creation (using AS spec under the hood)

Context

Checklist

  • Related issues linked using fixes #number
  • Tests added/updated
  • Documentation added
  • Make sure that all checks pass

@github-actions github-actions bot added pkg: open-payments type: source Changes business logic type: tests Testing related labels Nov 4, 2022
Comment on lines 2 to 5
OPEN_PAYMENTS_RS_OPEN_API_URL:
'https://raw.githubusercontent.com/interledger/open-payments/1e3e118d8b22c5d2942f972e28ebf0f0114d04f5/openapi/resource-server.yaml',
OPEN_PAYMENTS_AS_OPEN_API_URL:
'https://raw.githubusercontent.com/interledger/open-payments/1e3e118d8b22c5d2942f972e28ebf0f0114d04f5/openapi/auth-server.yaml',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Originally, I was thinking of doing something like

Suggested change
OPEN_PAYMENTS_RS_OPEN_API_URL:
'https://raw.githubusercontent.com/interledger/open-payments/1e3e118d8b22c5d2942f972e28ebf0f0114d04f5/openapi/resource-server.yaml',
OPEN_PAYMENTS_AS_OPEN_API_URL:
'https://raw.githubusercontent.com/interledger/open-payments/1e3e118d8b22c5d2942f972e28ebf0f0114d04f5/openapi/auth-server.yaml',
COMMIT_HASH: '1e3e118d8b22c5d2942f972e28ebf0f0114d04f5',
OPEN_PAYMENTS_RS_OPEN_API_URL:
'https://raw.githubusercontent.com/interledger/open-payments/<commitHash>/openapi/resource-server.yaml',
OPEN_PAYMENTS_AS_OPEN_API_URL:
'https://raw.githubusercontent.com/interledger/open-payments/<commitHash>/openapi/auth-server.yaml',

and then replace the <commitHash> whenever OPEN_PAYMENTS_AS_OPEN_API_URL was grabbed, but I thought overkill with only two configs

@@ -1,9 +1,36 @@
import { components, paths as Paths } from './generated/types'
import {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

decided to keep them in a single file, we don't have too many types

@@ -7,5 +7,5 @@
"declaration": true
},
"include": ["src/**/*"],
"exclude": ["**/*.test.ts", "src/scripts/*", "src/test/*"]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

we don't have a src/scripts folder

Comment on lines 40 to 57
return {
requestInteractiveGrant: (
args: RequestGrantArgs<InteractiveGrantRequest>
) =>
post(
{ axiosInstance, logger },
{ url: args.url, body: args.request },
createInteractiveGrantValidator
),
requestNonInteractiveGrant: (
args: RequestGrantArgs<NonInteractiveGrantRequest>
) =>
post(
{ axiosInstance, logger },
{ url: args.url, body: args.request },
createNonInteractiveGrantValidator
)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

At first, I wanted to do something tricky with types & method overloading, where we have one grant.create method that determined whether an "interactive" or "non-interactive" grant arguments were passed in, and returned with the correct response, but I decided something more explicit is much better to read and use.

Comment on lines 29 to 38
const createInteractiveGrantValidator =
openApi.createResponseValidator<InteractiveGrant>({
path: getASPath('/'),
method: HttpMethod.POST
})
const createNonInteractiveGrantValidator =
openApi.createResponseValidator<NonInteractiveGrant>({
path: getASPath('/'),
method: HttpMethod.POST
})
Copy link
Contributor Author

Choose a reason for hiding this comment

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

unfourtunately, needed both to pass type checking when given as arguments to the request*Grant methods

Copy link
Contributor

Choose a reason for hiding this comment

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

If we keep requestInteractiveGrant/requestNonInteractiveGrant separate, I think we'll need custom validators that include the openApi.createResponseValidator as well as a subsequent type guard that the response is the expected InteractiveGrant or NonInteractiveGrant. (In which case, we might be able to have a single openApi.createResponseValidator for both?)
As is, I think the OpenAPI validator would allow a NonInteractiveGrant response for requestInteractiveGrant and vice versa.

Comment on lines 18 to 20
requestInteractiveGrant(
args: RequestGrantArgs<InteractiveGrantRequest>
): Promise<InteractiveGrant>
Copy link
Contributor

Choose a reason for hiding this comment

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

My concern with splitting requestInteractiveGrant/requestNonInteractiveGrant is that I don't think we've standardized what grant requests are interactive. And thus, a client might not be expected to know when to use either.
For example, Rafiki has made incoming-payment-only grants non-interactive, but I don't know that that's required:

@sabineschaller or @njlie or @adrianhopebailie might be able to correct me on this.

A compromise could be to do:

Suggested change
requestInteractiveGrant(
args: RequestGrantArgs<InteractiveGrantRequest>
): Promise<InteractiveGrant>
requestInteractiveGrant(
args: RequestGrantArgs<InteractiveGrantRequest>
): Promise<InteractiveGrant | NonInteractiveGrant>

Copy link
Contributor

Choose a reason for hiding this comment

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

requestNonInteractiveGrant seems fine as is, since the request should just fail if the AS requires RO interaction for that grant.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I separated these, since at some point, I thought the client need to know that they need to pass in interact ?
https://github.com/interledger/open-payments/blob/main/openapi/auth-server.yaml#L93-L94

Copy link
Contributor

Choose a reason for hiding this comment

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

Given our mix of grants for different resources needing interact or not, I've been assuming a client might just always assume a grant request requires interaction, but also be able to handle getting back a NonInteractiveGrant response.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm still leaning towards

Suggested change
requestInteractiveGrant(
args: RequestGrantArgs<InteractiveGrantRequest>
): Promise<InteractiveGrant>
requestInteractiveGrant(
args: RequestGrantArgs<InteractiveGrantRequest>
): Promise<InteractiveGrant | NonInteractiveGrant>

and clients can utilize isInteractiveGrant/isNonInteractiveGrant.

I got the impression from Adrian that their policy agent AS might be determining what grant requests require interaction (vs having it Open Payments standardized).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, I see what you mean, it's more of a configurable, per-instance thing instead of standard, makes sense.

Going to make changes to assume client will always require interaction, but call might return either or.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm ok also keeping:

  requestNonInteractiveGrant(
    args: RequestGrantArgs<NonInteractiveGrantRequest>
  ): Promise<NonInteractiveGrant>

But maybe requiring the interact field to be specified isn't unreasonable? 🤔

Copy link
Contributor Author

@mkurapov mkurapov Nov 17, 2022

Choose a reason for hiding this comment

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

Yeah even in the demo Postman example, there is an interact field present when requesting the incoming payment grant -> which returns a non-interactive one.
We could run with a single call to make things simple for now. Going to play around w it

packages/open-payments/src/config.ts Outdated Show resolved Hide resolved
@mkurapov mkurapov marked this pull request as ready for review November 7, 2022 16:51
Comment on lines 29 to 38
const createInteractiveGrantValidator =
openApi.createResponseValidator<InteractiveGrant>({
path: getASPath('/'),
method: HttpMethod.POST
})
const createNonInteractiveGrantValidator =
openApi.createResponseValidator<NonInteractiveGrant>({
path: getASPath('/'),
method: HttpMethod.POST
})
Copy link
Contributor

Choose a reason for hiding this comment

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

If we keep requestInteractiveGrant/requestNonInteractiveGrant separate, I think we'll need custom validators that include the openApi.createResponseValidator as well as a subsequent type guard that the response is the expected InteractiveGrant or NonInteractiveGrant. (In which case, we might be able to have a single openApi.createResponseValidator for both?)
As is, I think the OpenAPI validator would allow a NonInteractiveGrant response for requestInteractiveGrant and vice versa.

): Promise<NonInteractiveGrant>
}

export const createGrantRoutes = (clientDeps: ClientDeps): GrantRoutes => {
Copy link
Contributor

Choose a reason for hiding this comment

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

We'll also need the grant continuation route, in order to support the full interactive grant flow.
https://github.com/interledger/open-payments/blob/62c4b4a9875e3adaa21f89f597e88db43016fe0b/openapi/auth-server.yaml#L163-L171

@github-actions github-actions bot added the pkg: backend Changes in the backend package. label Nov 16, 2022
@mkurapov mkurapov changed the base branch from main to bw-httpsig November 16, 2022 23:04
@github-actions github-actions bot added pkg: auth Changes in the GNAP auth package. type: localenv Local playground labels Nov 16, 2022
@github-actions github-actions bot added pkg: map Changes in mock-account-provider package pkg: openapi labels Nov 16, 2022
Base automatically changed from bw-httpsig to main November 17, 2022 14:11
@github-actions github-actions bot removed pkg: auth Changes in the GNAP auth package. pkg: openapi pkg: map Changes in mock-account-provider package type: localenv Local playground type: ci Changes to the CI labels Nov 17, 2022
# Conflicts:
#	packages/open-payments/src/client/index.ts
#	packages/open-payments/src/client/requests.test.ts
#	packages/open-payments/src/client/requests.ts
#	packages/open-payments/src/client/signatures.ts
#	packages/open-payments/src/index.ts
@github-actions github-actions bot removed the pkg: backend Changes in the backend package. label Nov 17, 2022
@mkurapov mkurapov changed the title 633/mk/adding as types to client Adding AS types & grant requests support to open-payments client Nov 17, 2022
@mkurapov
Copy link
Contributor Author

Going to add grant continuation as a separate PR

packages/open-payments/src/client/incoming-payment.ts Outdated Show resolved Hide resolved
Comment on lines 108 to 110
!('access_token' in grant) &&
'interact' in grant &&
!!(grant as InteractiveGrant).interact
Copy link
Contributor

Choose a reason for hiding this comment

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

Could this just be

Suggested change
!('access_token' in grant) &&
'interact' in grant &&
!!(grant as InteractiveGrant).interact
!!(grant as InteractiveGrant).interact

I'm assuming the OpenAPI validator would disallow access_token + interact.
ditto below

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I wasn't sure how lenient the validator was with oneOf in the spec, but this can be less strict

Comment on lines 18 to 20
requestInteractiveGrant(
args: RequestGrantArgs<InteractiveGrantRequest>
): Promise<InteractiveGrant>
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm still leaning towards

Suggested change
requestInteractiveGrant(
args: RequestGrantArgs<InteractiveGrantRequest>
): Promise<InteractiveGrant>
requestInteractiveGrant(
args: RequestGrantArgs<InteractiveGrantRequest>
): Promise<InteractiveGrant | NonInteractiveGrant>

and clients can utilize isInteractiveGrant/isNonInteractiveGrant.

I got the impression from Adrian that their policy agent AS might be determining what grant requests require interaction (vs having it Open Payments standardized).

Comment on lines +38 to +44
export const isInteractiveGrant = (
grant: InteractiveGrant | NonInteractiveGrant
): grant is InteractiveGrant => !!(grant as InteractiveGrant).interact

export const isNonInteractiveGrant = (
grant: InteractiveGrant | NonInteractiveGrant
): grant is NonInteractiveGrant => !!(grant as NonInteractiveGrant).access_token
Copy link
Contributor Author

Choose a reason for hiding this comment

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

could live in src/client/grants.ts or src/type-guard.ts but decided to leave them here

export type GrantRequest = {
access_token: ASOperations['post-request']['requestBody']['content']['application/json']['access_token']
client: ASOperations['post-request']['requestBody']['content']['application/json']['client']
interact?: ASOperations['post-request']['requestBody']['content']['application/json']['interact']
Copy link
Contributor Author

@mkurapov mkurapov Nov 17, 2022

Choose a reason for hiding this comment

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

@wilsonianb single grant request with optional interact in line with the Open Payments API

Copy link
Contributor

@wilsonianb wilsonianb Nov 17, 2022

Choose a reason for hiding this comment

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

I was thinking we'd require interact
#727 (comment)
#727 (comment)
but I'm ok as is. The grant request would just throw if interaction is required and interact wasn't specified.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Was going back and forth but less errors and throws are better here IMO. Made it required.

wilsonianb
wilsonianb previously approved these changes Nov 17, 2022
export type GrantRequest = {
access_token: ASOperations['post-request']['requestBody']['content']['application/json']['access_token']
client: ASOperations['post-request']['requestBody']['content']['application/json']['client']
interact?: ASOperations['post-request']['requestBody']['content']['application/json']['interact']
Copy link
Contributor

@wilsonianb wilsonianb Nov 17, 2022

Choose a reason for hiding this comment

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

I was thinking we'd require interact
#727 (comment)
#727 (comment)
but I'm ok as is. The grant request would just throw if interaction is required and interact wasn't specified.

}

export interface GrantRoutes {
requestGrant(
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
requestGrant(
request(

or

Suggested change
requestGrant(
requestAccess(

https://datatracker.ietf.org/doc/html/draft-ietf-gnap-core-protocol#section-2

Copy link
Contributor Author

@mkurapov mkurapov Nov 17, 2022

Choose a reason for hiding this comment

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

yup, had request in my follow-up PR, will do it here instead

@mkurapov mkurapov merged commit 461fb76 into main Nov 17, 2022
@mkurapov mkurapov deleted the 633/mk/adding-AS-types-to-client branch November 17, 2022 20:46
@wilsonianb
Copy link
Contributor

commit 300! 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: source Changes business logic type: tests Testing related
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants