Skip to content
This repository was archived by the owner on Mar 8, 2024. It is now read-only.

Commit efa7f81

Browse files
authored
Private API version (#418)
Adds a version header to the private API, to provide a better experience for any partner that uses our reference implementation as their production PayID server. The PayID-API-Version header needs to be an ISO8601 date string, of the form YYYY-MM-DD. This is what Coinbase, Stripe, and various other companies use to version their hosted API services. Note that I kept the /v1/ in the path for the private API. I chose to do this as Coinbase, Stripe, etc. do the same thing. We should never need to increment to /v2/ in the path, but it's cheap to leave it there, makes a smaller diff, and gives us a safety valve for the future.
1 parent 8997997 commit efa7f81

File tree

14 files changed

+259
-73
lines changed

14 files changed

+259
-73
lines changed

aws-deploy.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ You can set up a PayID server on AWS (Amazon Web Services).
4646
13. Load up your desired PayID to the database using the [private PayID API](readme.md). If you use a subdomain rather than a path, then you must set up a DNS record for the subdomain as described in step 3.
4747
**Note:** You can add PayIDs for each (payId, network, environment) tuple. Use this cURL command to set up a PayID.
4848
```
49-
curl --location --request POST 'http://127.0.0.1:8081/v1/users' \
49+
curl --location --request POST 'http://127.0.0.1:8081/users' \
5050
--header 'Content-Type: application/json' \
5151
--data-raw '{
5252
"payId": "$<your-pay-id-address>",

example/instructions.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ In this tutorial, we'll be walking through the travel rule handshake protocol
55
We'll start off by creating a user:
66

77
```
8-
curl --location --request POST http://localhost:8081/v1/users --header 'Content-Type: application/json' --data-raw '{
8+
curl --location --request POST http://localhost:8081/users --header 'Content-Type: application/json' --data-raw '{
99
"payId": "$127.0.0.1/exampleUser",
1010
"addresses": [
1111
{
@@ -26,13 +26,15 @@ curl --location --request GET 'http://127.0.0.1:8080/exampleUser' --header 'Acce
2626
```
2727

2828
We've confirmed that our PayID is working as expected so let's send a payment setup details request:
29+
2930
```
3031
curl --location --request GET 'http://127.0.0.1:8080/exampleUser/payment-setup-details' \
3132
--header 'Accept: application/xrpl-testnet+json' \
3233
--header 'Content-Type: application/json'
3334
```
3435

3536
The returned JSON object is our payment setup details. In this example, the institution is a VASP and has listed any laws that must be complied with in the `complianceRequirements` field of the payment setup details. Specifically, as the originator of the payment we are being asked to comply with the Travel Rule. In order to do that, we'll POST the compliance data to the same URL in order to upgrade our payment setup details object.
37+
3638
```
3739
curl --location --request POST 'http://127.0.0.1:8080/exampleUser/payment-setup-details' \
3840
--header 'Content-Type: application/json' \
@@ -65,8 +67,8 @@ curl --location --request POST 'http://127.0.0.1:8080/exampleUser/payment-setup-
6567
}'
6668
```
6769

68-
6970
Upon submission of this data, the beneficiary should identify that we have fulfilled all compliance requirements and send us an upgraded payment setup details object. This upgraded payment setup details cryptographically correlates our submission of compliance data through the complianceHashes field. Now that we have been informed of all compliance requirements, and fulfilled them, we can submit our transaction on ledger and POST back the payment proof.
71+
7072
```
7173
curl --location --request POST 'http://127.0.0.1:8080/exampleUser/payment-proofs' \
7274
--header 'Content-Type: application/json' \

payid-metrics.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ docker run -it -p 8080:8080 -p 8081:8081 --name payid-server --network payid-net
128128
Test whether the PayID server is running by creating a PayID with this cURL command.
129129

130130
```
131-
curl --location --request POST 'http://127.0.0.1:8081/v1/users' --header 'Content-Type: application/json' --header 'Content-Type: text/plain' --data-raw '{
131+
curl --location --request POST 'http://127.0.0.1:8081/users' --header 'Content-Type: application/json' --header 'Content-Type: text/plain' --data-raw '{
132132
"payId": "alice$127.0.0.1",
133133
"addresses": [
134134
{

readme.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,12 @@ You can then use the Private PayID API to:
121121
The private APIs run by default on port 8081. Make sure to adjust this value if needed.
122122
The list of private endpoints is:
123123

124-
| HTTP Method | Endpoint | Description |
125-
| ---------------------------------------- | :----------------------- | ------------------------------: |
126-
| [GET](#421-get-a-payid-user-information) | /v1/users/{user}\${host} | Get a PayID user information |
127-
| [POST](#422-create-a-payid-user) | /v1/users | Create a PayID user |
128-
| [PUT](#423-update-a-payid-user) | /v1/users/{user}\${host} | Update a PayID user information |
129-
| [DELETE](#424-delete-a-payid-user) | /v1/users/{user}\${host} | Delete a PayID user |
124+
| HTTP Method | Endpoint | Description |
125+
| ---------------------------------------- | :-------------------- | ------------------------------: |
126+
| [GET](#421-get-a-payid-user-information) | /users/{user}\${host} | Get a PayID user information |
127+
| [POST](#422-create-a-payid-user) | /users | Create a PayID user |
128+
| [PUT](#423-update-a-payid-user) | /users/{user}\${host} | Update a PayID user information |
129+
| [DELETE](#424-delete-a-payid-user) | /users/{user}\${host} | Delete a PayID user |
130130

131131
Once you have set up your PayID server, you can access the Private PayID API endpoints using Postman or these cURL commands.
132132

@@ -135,7 +135,7 @@ Once you have set up your PayID server, you can access the Private PayID API end
135135
**Request format**
136136

137137
```
138-
GET {pay_id_base_url}/v1/users/{user}${host}
138+
GET {pay_id_base_url}/users/{user}${host}
139139
```
140140

141141
**Path parameters (Required)**
@@ -166,7 +166,7 @@ This operation creates a single user.
166166
Request (Success)
167167

168168
```HTTP
169-
GET http://127.0.0.1:8081/v1/users/bob$127.0.0.1 HTTP/1.1
169+
GET http://127.0.0.1:8081/users/bob$127.0.0.1 HTTP/1.1
170170
```
171171

172172
Response (Success)
@@ -200,7 +200,7 @@ The following table lists the HTTP status codes and messages returned for this m
200200
**Request format**
201201

202202
```
203-
POST {pay_id_base_url}/v1/users
203+
POST {pay_id_base_url}/users
204204
```
205205

206206
**Path parameters (None)**
@@ -251,7 +251,7 @@ Request (Success)
251251
The addresses array can contain 1 or more objects.
252252

253253
```HTTP
254-
POST http://127.0.0.1:8081/v1/users HTTP/1.1
254+
POST http://127.0.0.1:8081/users HTTP/1.1
255255
256256
{
257257
"payId": "bob$127.0.0.1",
@@ -297,7 +297,7 @@ You can modify the user information associated with a particular PayID address.
297297
**Request format**
298298

299299
```
300-
PUT {pay_id_base_url}/v1/users/{user}${host}
300+
PUT {pay_id_base_url}/users/{user}${host}
301301
```
302302

303303
**Path parameters (Required)**
@@ -348,7 +348,7 @@ A successful response to the "Update a PayID user" method returns a 201 HTTP sta
348348
Request (Success)
349349

350350
```HTTP
351-
PUT http://127.0.0.1:8081/v1/users/bob$127.0.0.1 HTTP/1.1
351+
PUT http://127.0.0.1:8081/users/bob$127.0.0.1 HTTP/1.1
352352
353353
{
354354
"payId": "bob$127.0.0.1",
@@ -388,7 +388,7 @@ The following table lists the HTTP status codes and messages returned for this m
388388
**Request format**
389389

390390
```
391-
DELETE {pay_id_base_url}/v1/users/{user}${host}
391+
DELETE {pay_id_base_url}/users/{user}${host}
392392
```
393393

394394
**Path parameters (Required)**
@@ -417,7 +417,7 @@ A successful response to the "Delete a PayID user" method returns a 204 HTTP sta
417417
Request (Success)
418418

419419
```HTTP
420-
DELETE http://127.0.0.1:8081/v1/users/bob$127.0.0.1 HTTP/1.1
420+
DELETE http://127.0.0.1:8081/users/bob$127.0.0.1 HTTP/1.1
421421
422422
{
423423
"payId": "bob$127.0.0.1",

src/app.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,7 @@ export default class App {
6161
}
6262

6363
private launchPrivateAPI(appConfig: typeof config.app): Server {
64-
this.privateAPIExpress.use(
65-
`${appConfig.privateApiVersion}/users`,
66-
privateAPIRouter,
67-
)
64+
this.privateAPIExpress.use('/users', privateAPIRouter)
6865
this.privateAPIExpress.use('/metrics', metricsRouter)
6966

7067
return this.privateAPIExpress.listen(appConfig.privateAPIPort, () =>

src/config.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
export enum PrivateApiVersion {
2-
V1 = '/v1',
3-
}
4-
51
export const payIdServerVersions: readonly string[] = ['0.2']
2+
export const privateApiVersions: readonly string[] = ['2020-05-28']
63

74
/**
85
* Application configuration.
@@ -27,7 +24,7 @@ const config = {
2724
publicAPIPort: Number(process.env.PUBLIC_API_PORT) || 8080,
2825
privateAPIPort: Number(process.env.PRIVATE_API_PORT) || 8081,
2926
payIdVersion: payIdServerVersions[payIdServerVersions.length - 1],
30-
privateApiVersion: PrivateApiVersion.V1,
27+
privateApiVersion: privateApiVersions[privateApiVersions.length - 1],
3128
logLevel: process.env.LOG_LEVEL ?? 'INFO',
3229
httpsRequired: process.env.HTTPS_REQUIRED === 'true',
3330
},
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Request, Response, NextFunction } from 'express'
2+
3+
import config, { privateApiVersions } from '../config'
4+
import { ParseError, ParseErrorType } from '../utils/errors'
5+
6+
export default function checkPublicApiVersionHeaders(
7+
req: Request,
8+
res: Response,
9+
next: NextFunction,
10+
): void {
11+
// Add our Server-Version headers to all successful responses.
12+
// This should be the most recent version of the PayID protocol / PayID private API this server knows how to handle.
13+
// We add it early so even errors will respond with Server-Version headers.
14+
res.header('PayID-Server-Version', config.app.payIdVersion)
15+
res.header('PayID-API-Server-Version', config.app.privateApiVersion)
16+
17+
const payIdApiVersionHeader = req.header('PayID-API-Version')
18+
19+
// Checks if the PayID-API-Version header exists
20+
if (!payIdApiVersionHeader) {
21+
throw new ParseError(
22+
"A PayID-API-Version header is required in the request, of the form 'PayID-API-Version: YYYY-MM-DD'.",
23+
ParseErrorType.MissingPayIdApiVersionHeader,
24+
)
25+
}
26+
27+
const dateRegex = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/u
28+
const regexResult = dateRegex.exec(payIdApiVersionHeader)
29+
if (!regexResult) {
30+
throw new ParseError(
31+
"A PayID-API-Version header must be in the form 'PayID-API-Version: YYYY-MM-DD'.",
32+
ParseErrorType.InvalidPayIdApiVersionHeader,
33+
)
34+
}
35+
36+
// Because they are ISO8601 date strings, we can just do a string comparison
37+
if (payIdApiVersionHeader < privateApiVersions[0]) {
38+
throw new ParseError(
39+
`The PayID-API-Version ${payIdApiVersionHeader} is not supported, please try upgrading your request to at least 'PayID-API-Version: ${privateApiVersions[0]}'`,
40+
ParseErrorType.UnsupportedPayIdApiVersionHeader,
41+
)
42+
}
43+
44+
next()
45+
}

src/middlewares/sendSuccess.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ export default function sendSuccess(req: Request, res: Response): void {
99
if (status === HttpStatus.Created) {
1010
// The first part of the destructured array will be "", because the string starts with "/"
1111
// And for PUT commands, the path could potentially hold more after `userPath`.
12-
const [, version, userPath] = req.originalUrl.split('/')
12+
const [, userPath] = req.originalUrl.split('/')
1313

14-
const locationHeader = ['', version, userPath, res.locals.payId].join('/')
14+
const locationHeader = ['', userPath, res.locals.payId].join('/')
1515

1616
res.location(locationHeader)
1717
}

src/middlewares/users.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export async function getUser(
2424
if (!payId) {
2525
return handleHttpError(
2626
HttpStatus.BadRequest,
27-
'A `payId` must be provided in the path. A well-formed API call would look like `GET /v1/users/alice$xpring.money`.',
27+
'A `payId` must be provided in the path. A well-formed API call would look like `GET /users/alice$xpring.money`.',
2828
res,
2929
)
3030
}
@@ -106,7 +106,7 @@ export async function putUser(
106106
if (!payId) {
107107
return handleHttpError(
108108
HttpStatus.BadRequest,
109-
'A `payId` must be provided in the path. A well-formed API call would look like `PUT /v1/users/alice$xpring.money`.',
109+
'A `payId` must be provided in the path. A well-formed API call would look like `PUT /users/alice$xpring.money`.',
110110
res,
111111
)
112112
}
@@ -178,7 +178,7 @@ export async function deleteUser(
178178
if (!payId) {
179179
return handleHttpError(
180180
HttpStatus.BadRequest,
181-
'A PayID must be provided in the path. A well-formed API call would look like `GET /v1/users/alice$xpring.money`.',
181+
'A PayID must be provided in the path. A well-formed API call would look like `GET /users/alice$xpring.money`.',
182182
res,
183183
)
184184
}

src/routes/privateAPIRouter.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as express from 'express'
22

3+
import checkPrivateApiVersionHeaders from '../middlewares/checkPrivateApiVersionHeaders'
34
import errorHandler, { wrapAsync } from '../middlewares/errorHandler'
45
import sendSuccess from '../middlewares/sendSuccess'
56
import { getUser, postUser, putUser, deleteUser } from '../middlewares/users'
@@ -10,10 +11,27 @@ const privateAPIRouter = express.Router()
1011
* Routes for the private API so that authorized parties can post PayID mappings to the PayID database.
1112
*/
1213
privateAPIRouter
13-
.get('/*', wrapAsync(getUser), sendSuccess)
14-
.post('/', express.json(), wrapAsync(postUser), sendSuccess)
15-
.put('/*', express.json(), wrapAsync(putUser), sendSuccess)
16-
.delete('/*', wrapAsync(deleteUser), sendSuccess)
14+
.get('/*', checkPrivateApiVersionHeaders, wrapAsync(getUser), sendSuccess)
15+
.post(
16+
'/',
17+
express.json(),
18+
checkPrivateApiVersionHeaders,
19+
wrapAsync(postUser),
20+
sendSuccess,
21+
)
22+
.put(
23+
'/*',
24+
express.json(),
25+
checkPrivateApiVersionHeaders,
26+
wrapAsync(putUser),
27+
sendSuccess,
28+
)
29+
.delete(
30+
'/*',
31+
checkPrivateApiVersionHeaders,
32+
wrapAsync(deleteUser),
33+
sendSuccess,
34+
)
1735

1836
// Error handling middleware needs to be defined last
1937
.use(errorHandler)

0 commit comments

Comments
 (0)